File Manager

Current Directory: /home/astoriaah/www/administrator/components/com_akeeba/BackupEngine/Postproc
Viewing File: /home/astoriaah/www/administrator/components/com_akeeba/BackupEngine/Postproc/Dropbox2.php
<?php /** * Akeeba Engine * * @package akeebaengine * @copyright Copyright (c)2006-2022 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 3, or later */ namespace Akeeba\Engine\Postproc; defined('AKEEBAENGINE') || die(); use Akeeba\Engine\Base\Exceptions\ErrorException; use Akeeba\Engine\Configuration; use Akeeba\Engine\Factory; use Akeeba\Engine\Platform; use Akeeba\Engine\Postproc\Connector\Dropbox2 as ConnectorDropboxV2; use Akeeba\Engine\Postproc\Exception\BadConfiguration; use Akeeba\Engine\Postproc\Exception\RangeDownloadNotSupported; use Exception; use RuntimeException; class Dropbox2 extends Base { /** * The retry count of this file (allow up to 2 retries after the first upload failure) * * @var int */ private $tryCount = 0; /** * The currently configured directory * * @var string */ private $directory; /** * Are we using chunk uploads? * * @var bool */ private $chunked = false; /** * Chunk size (MB) * * @var int */ private $chunk_size = 10; public function __construct() { $this->supportsDownloadToBrowser = true; $this->supportsDelete = true; $this->supportsDownloadToFile = true; } public function oauthCallback(array $params) { $input = $params['input']; $data = (object) [ 'access_token' => $input['access_token'], 'refresh_token' => $input['refresh_token'], ]; $serialisedData = json_encode($data); return <<< HTML <script type="application/javascript"> window.opener.akeeba_dropbox2_oauth_callback($serialisedData); </script> HTML; } public function processPart($localFilepath, $remoteBaseName = null) { /** @var ConnectorDropboxV2 $connector */ $connector = $this->getConnector(); $config = Factory::getConfiguration(); // Store the absolute remote path in the class property $directory = $this->directory; $basename = empty($remoteBaseName) ? basename($localFilepath) : $remoteBaseName; $this->remotePath = $directory . '/' . $basename; // Have I already made sure the remote directory exists? $haveCheckedRemoteDirectory = $config->get('volatile.engine.postproc.dropbox2.check_directory', 0); if (!$haveCheckedRemoteDirectory) { try { $connector->ping(); $connector->makeDirectory($directory); $config->set('volatile.engine.postproc.dropbox2.check_directory', 1); } catch (Exception $e) { throw new RuntimeException(sprintf("Could not create Dropbox directory %s.", $directory), 500, $e); } } // Get the remote file's pathname $remotePath = trim($directory, '/') . '/' . basename($localFilepath); // Check if the size of the file is compatible with chunked uploading clearstatcache(); $totalSize = filesize($localFilepath); $isBigEnough = $this->chunked ? ($totalSize > $this->chunk_size) : false; // Chunked uploads if the feature is enabled and the file is at least as big as the chunk size. if ($this->chunked && $isBigEnough) { return $this->processPartChunked($config, $localFilepath, $remotePath, $totalSize); } // Single part upload return $this->processPartSingleUpload($localFilepath, $remotePath); } public function downloadToFile($remotePath, $localFile, $fromOffset = null, $length = null) { /** @var ConnectorDropboxV2 $connector */ $connector = $this->getConnector(); if (!is_null($fromOffset)) { throw new RangeDownloadNotSupported(); } // Download the file $connector->ping(); $connector->download($remotePath, $localFile); return true; } public function downloadToBrowser($remotePath) { /** @var ConnectorDropboxV2 $connector */ $connector = $this->getConnector(); $connector->ping(); return $connector->getAuthenticatedUrl($remotePath); } public function delete($path) { /** @var ConnectorDropboxV2 $connector */ $connector = $this->getConnector(); $connector->ping(); $connector->delete($path); } protected function getOAuth2HelperUrl() { return ConnectorDropboxV2::helperUrl; } /** * Returns the Dropbox configuration settings * * @return array */ protected function getSettings() { // Retrieve engine configuration data $config = Factory::getConfiguration(); $accessToken = trim($config->get('engine.postproc.dropbox2.access_token', '')); $refreshToken = trim($config->get('engine.postproc.dropbox2.refresh_token', '')); $this->chunked = $config->get('engine.postproc.dropbox2.chunk_upload', true); $this->chunk_size = $config->get('engine.postproc.dropbox2.chunk_upload_size', 10) * 1024 * 1024; $defaultDirectory = $config->get('engine.postproc.dropbox2.directory', ''); $this->directory = $config->get('volatile.postproc.directory', $defaultDirectory); // Sanity checks $dlid = Platform::getInstance()->get_platform_configuration_option('update_dlid', ''); if (empty($dlid)) { throw new BadConfiguration('You must enter your Download ID in the application configuration before using the “Upload to Dropbox” feature.'); } if (empty($accessToken) && empty($refreshToken)) { throw new BadConfiguration('You have not linked Akeeba Backup with your Dropbox account'); } if (!function_exists('curl_init')) { throw new BadConfiguration('cURL is not enabled, please enable it in order to post-process your archives'); } /** * Remove the trailing and leading slashes from the directory. Why? The directory must never have a trailing * slash. The directory must have a leading slash UNLESS it's the root, in which case it needs to be normalized * to an empty string. So we first remove leading & trailing slashes, then check if it's empty (root) or not. * * Then remove any leftover leading/trailing slash. They will produce an invalid path in Dropbox. However, if * the directory is not empty we must prefix it with a leading slash. */ $this->directory = trim($this->directory, '/'); $this->directory = trim($this->directory); $this->directory = empty($this->directory) ? '' : ('/' . $this->directory); $this->directory = ltrim(Factory::getFilesystemTools()->TranslateWinPath($this->directory), '/'); $this->directory = Factory::getFilesystemTools()->replace_archive_name_variables($this->directory); $config->set('volatile.postproc.directory', $this->directory); return [ 'token' => $accessToken, 'refresh_token' => $refreshToken, 'dlid' => $dlid, ]; } protected function makeConnector() { // Do we have a cached root namespace ID? $configuration = Factory::getConfiguration(); $namespaceId = $configuration->get('volatile.engine.postproc.dropbox2.namespaceId', null); // Get and cache the root namespace ID. if (is_null($namespaceId)) { $namespaceId = $this->getDropboxForBusinessRootNamespaceId(); $configuration->set('volatile.engine.postproc.dropbox2.namespaceId', $namespaceId); } $config = $this->getSettings(); $connector = new ConnectorDropboxV2($config['token'], $config['refresh_token'], $config['dlid']); $connector->setNamespaceId($namespaceId); return $connector; } /** * Forcibly refresh the Dropbox tokens * * @param ConnectorDropboxV2|null $connector The connector to use * * @return void * * @throws Exception */ protected function forceRefreshTokens($connector = null) { // Retrieve engine configuration data $config = Factory::getConfiguration(); /** @var ConnectorDropboxV2 $connector */ if (empty($connector)) { $connector = $this->getConnector(); } $pingResult = $connector->ping(true); Factory::getLog()->debug(__METHOD__ . " - Dropbox tokens were forcibly refreshed"); $config->set('engine.postproc.dropbox2.access_token', $pingResult['access_token'], false); $config->set('engine.postproc.dropbox2.refresh_token', $pingResult['refresh_token'], false); $profile_id = Platform::getInstance()->get_active_profile(); Platform::getInstance()->save_configuration($profile_id); } /** * Returns the root namespace for the Dropbox for Business team space * * @see https://www.dropbox.com/developers/reference/namespace-guide * * @param bool $refreshTokensOnFailure Should I try to forcibly update the token on connection failure? * * @return string * @throws Exception */ private function getDropboxForBusinessRootNamespaceId($refreshTokensOnFailure = true) { // Do I need to use a folder under a team space? $configuration = Factory::getConfiguration(); $useTeam = $configuration->get('engine.postproc.dropbox2.team', 0) == 1; // Team space is not in use. Default to the user's home (same as the legacy behavior) if (!$useTeam) { return ''; } // Try to get the current account information try { $config = $this->getSettings(); $connector = new ConnectorDropboxV2($config['token'], $config['refresh_token'], $config['dlid']); $currentAccount = $connector->getCurrentAccount(); } catch (Exception $e) { if ($refreshTokensOnFailure) { $this->forceRefreshTokens($connector); return $this->getDropboxForBusinessRootNamespaceId(false); } throw new ErrorException("Cannot connect to Dropbox for Business", 0, $e); } if ( !is_array($currentAccount) || !array_key_exists('root_info', $currentAccount) || !is_array($currentAccount['root_info']) || !array_key_exists('root_namespace_id', $currentAccount['root_info']) ) { throw new ErrorException("Dropbox for Business did not return any user account information"); } return $currentAccount['root_info']['root_namespace_id']; } /** * Handles the multipart upload of a file to Dropbox * * @param Configuration $config * @param string $absolute_filename * @param string $remotePath * @param int $totalSize * * @return bool * * @throws Exception */ private function processPartChunked(Configuration $config, $absolute_filename, $remotePath, $totalSize) { /** @var ConnectorDropboxV2 $connector */ $connector = $this->getConnector(); // Are we already processing a multipart upload? Factory::getLog()->debug(sprintf( "%s - Using chunked upload, part size {$this->chunk_size}", __METHOD__ )); $offset = $config->get('volatile.engine.postproc.dropbox2.offset', 0); $upload_id = $config->get('volatile.engine.postproc.dropbox2.upload_id', null); if (empty($upload_id)) { Factory::getLog()->debug(sprintf( "%s - Creating new upload session", __METHOD__ )); try { $upload_id = $connector->createUploadSession(); } catch (Exception $e) { // Fail immediately if there is no refresh token $config = $this->getSettings(); if (empty($config['refresh_token'])) { throw new RuntimeException("Cannot create upload session", 500, $e); } Factory::getLog()->debug(sprintf( "%s - Failed to create a new upload session; will try to refresh the tokens first", __METHOD__ )); $upload_id = null; } if (is_null($upload_id)) { try { $this->forceRefreshTokens(); $upload_id = $connector->createUploadSession(); } catch (Exception $e) { throw new RuntimeException("Cannot create upload session", 500, $e); } } Factory::getLog()->debug(sprintf( "%s - New upload session $upload_id", __METHOD__ )); $config->set('volatile.engine.postproc.dropbox2.upload_id', $upload_id); } $exception = null; try { if (empty($offset)) { $offset = 0; } Factory::getLog()->debug(sprintf("%s - Uploading chunked part", __METHOD__)); $connector->uploadPart($upload_id, $absolute_filename, $offset, $this->chunk_size); $result = true; } catch (Exception $e) { Factory::getLog()->debug(sprintf( "%s - Got uploadPart Exception %s: %s", __METHOD__, 500, $e->getMessage()) ); $exception = $e; $result = false; } // Did we fail uploading? if ($result === false) { // Let's retry $this->tryCount++; // However, if we've already retried twice, we stop retrying and call it a failure if ($this->tryCount > 2) { throw new RuntimeException(sprintf( "%s - Maximum number of retries exceeded. The upload has failed.", __METHOD__ ), 500, $exception); } // Retry to refresh the Access Token if a Refresh Token is provided. $config = $this->getSettings(); if (!empty($config['refresh_token'])) { Factory::getLog()->debug(sprintf( "%s - Error detected, trying to force-refresh the tokens", __METHOD__ )); $this->forceRefreshTokens(); } Factory::getLog()->debug(sprintf("%s - Retrying chunk upload", __METHOD__)); return false; } // Are we done uploading? $nextOffset = $offset + $this->chunk_size - 1; if (isset($result['name']) || ($nextOffset > $totalSize)) { Factory::getLog()->debug(sprintf( "%s - Finializing chunked upload, saving uploaded file as %s", __METHOD__, $remotePath )); try { $connector->finishUploadSession($upload_id, $remotePath, $totalSize); } catch (Exception $e) { throw new RuntimeException('Chunk upload finalization failed', 500, $e); } Factory::getLog()->debug(sprintf( "%s - Chunked upload is now complete", __METHOD__ )); $config->set('volatile.engine.postproc.dropbox2.offset', null); $config->set('volatile.engine.postproc.dropbox2.upload_id', null); return true; } // Otherwise, continue uploading $config->set('volatile.engine.postproc.dropbox2.offset', $offset + $this->chunk_size); return false; } /** * Handles the single part upload of a file to Dropbox * * @param string $absolute_filename * @param string $remotePath * * @return bool * * @throws Exception */ private function processPartSingleUpload($absolute_filename, $remotePath) { /** @var ConnectorDropboxV2 $connector */ $connector = $this->getConnector(); $exception = null; try { Factory::getLog()->debug(sprintf( "%s - Performing simplified upload of %s to %s", __METHOD__, $absolute_filename, $remotePath )); $result = $connector->upload($remotePath, $absolute_filename); } catch (Exception $e) { Factory::getLog()->debug(sprintf( "%s - Simplified upload failed, %s: %s", __METHOD__, $e->getCode(), $e->getMessage() )); $exception = $e; $result = false; } if ($result === false) { // Let's retry $this->tryCount++; // However, if we've already retried twice, we stop retrying and call it a failure if ($this->tryCount > 2) { throw new RuntimeException(sprintf( "%s - Maximum number of retries exceeded. The upload has failed.", __METHOD__ ), 500, $exception); } // Retry to refresh the Access Token if a Refresh Token is provided. $config = $this->getSettings(); if (!empty($config['refresh_token'])) { Factory::getLog()->debug(sprintf( "%s - Error detected, trying to force-refresh the tokens", __METHOD__ )); $this->forceRefreshTokens(); } Factory::getLog()->debug(sprintf("%s - Retrying upload", __METHOD__)); return false; } // Upload complete. Reset the retry counter. $this->tryCount = 0; return true; } }