diff --git a/.craftplugin b/.craftplugin index 734caed5..d8d51bcb 100644 --- a/.craftplugin +++ b/.craftplugin @@ -1,7 +1,7 @@ { "pluginName": "Translations for Craft", "pluginDescription": "Easily launch and manage multilingual Craft websites without having to copy/paste content or manually track updates.", - "pluginVersion": "1.7.1", + "pluginVersion": "1.7.2", "pluginAuthorName": "Acclaro", "pluginVendorName": "Acclaro", "pluginAuthorUrl": "http://www.acclaro.com/", diff --git a/CHANGELOG.md b/CHANGELOG.md index 92ce57c9..4ac0f015 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## 1.7.2 - 2020-07-24 + +### Added +- Support for selecting Upload Volumes + + ## 1.7.1 - 2020-07-14 ### Changed diff --git a/src/controllers/FilesController.php b/src/controllers/FilesController.php index ede38bcf..a1315eec 100644 --- a/src/controllers/FilesController.php +++ b/src/controllers/FilesController.php @@ -28,6 +28,10 @@ use acclaro\translations\Translations; use acclaro\translations\services\job\ImportFiles; use acclaro\translations\services\repository\SiteRepository; +use craft\elements\Asset; +use craft\errors\UploadFailedException; +use craft\helpers\ArrayHelper; +use yii\base\ErrorException; /** * @author Acclaro @@ -169,7 +173,6 @@ public function actionImportFile() //Track error and success messages. $message = ""; - // Upload the file and drop it in the temporary folder $file = UploadedFile::getInstanceByName('zip-upload'); //Get Order Data @@ -187,72 +190,72 @@ public function actionImportFile() $this->showUserMessages("Invalid extention: The plugin only support [ZIP, XML] files."); } - $fileName = Assets::prepareAssetName($file->name, true, true); - $folderPath = Craft::$app->path->getTempAssetUploadsPath().'/'; - FileHelper::clearDirectory($folderPath); - //If is a Zip File if ($file->extension === 'zip') { //Unzip File ZipArchive $zip = new \ZipArchive(); - if (move_uploaded_file($file->tempName, $folderPath.$fileName)) { - if ($zip->open($folderPath.$fileName)) { - $xmlPath = $folderPath.$orderId; + $assetPath = $file->saveAsTempFile(); + + if ($zip->open($assetPath)) { + $xmlPath = $assetPath.$orderId; + + $zip->extractTo($xmlPath); + + $fileName = preg_replace('/\\.[^.\\s]{3,4}$/', '', Assets::prepareAssetName($file->name)); - $zip->extractTo($xmlPath); + $files = FileHelper::findFiles($assetPath.$orderId); - $fileName = preg_replace('/\\.[^.\\s]{3,4}$/', '', $fileName); + $assetIds = []; - $files = FileHelper::findFiles($folderPath.$orderId); + foreach ($files as $key => $file) { + if (! is_bool(strpos($file, '__MACOSX'))) { + unlink($file); - foreach ($files as $key => $file) { - rename($file, $folderPath.$orderId.'/'.pathinfo($file)['basename']); + continue; } - FileHelper::removeDirectory($folderPath.$orderId.'/'.$fileName); + $filename = Assets::prepareAssetName($file); + + $uploadVolumeId = ArrayHelper::getValue(Translations::getInstance()->getSettings(), 'uploadVolume'); + + $folder = Craft::$app->getAssets()->getRootFolderByVolumeId($uploadVolumeId); + + $pathInfo = pathinfo($file); - $zip->close(); + $compatibleFilename = $pathInfo['dirname'] . '/' . $pathInfo['filename'] . '.txt'; - $job = Craft::$app->queue->push(new ImportFiles([ - 'description' => 'Updating translation drafts', - 'orderId' => $orderId, - 'totalFiles' => $total_files, - 'xmlPath' => $xmlPath, - ])); + rename($file, $compatibleFilename); - if ($job) { - $params = [ - 'id' => (int) $job, - 'notice' => 'Done updating translation drafts', - 'url' => 'translations/orders/detail/'. $orderId - ]; - Craft::$app->getView()->registerJs('$(function(){ Craft.Translations.trackJobProgressById(true, false, '. json_encode($params) .'); });'); + $asset = new Asset(); + $asset->tempFilePath = $compatibleFilename; + $asset->filename = $compatibleFilename; + $asset->newFolderId = $folder->id; + $asset->volumeId = $folder->volumeId; + $asset->avoidFilenameConflicts = true; + $asset->uploaderId = Craft::$app->getUser()->getId(); + $asset->setScenario(Asset::SCENARIO_CREATE); + + if (! Craft::$app->getElements()->saveElement($asset)) { + $errors = $asset->getFirstErrors(); + + return $this->asErrorJson(Craft::t('app', 'Failed to save the asset:') . ' ' . implode(";\n", $errors)); } - // $this->redirect('translations/orders/detail/'. $orderId, 302, true); - $this->showUserMessages("File uploaded successfully: $fileName", true); - } else { - $this->showUserMessages("Unable to unzip ". $file->name ." Operation not permitted or Decompression Failed "); + + $assetIds[] = $asset->id; } - } else { - $this->showUserMessages("Unable to upload file: $fileName"); - } - } elseif ($file->extension === 'xml') { - $xmlPath = $folderPath.$orderId; - mkdir($xmlPath, 0777, true); + FileHelper::removeDirectory($assetPath.$orderId.'/'.$fileName); - //Upload File - if( move_uploaded_file($file->tempName, $xmlPath.'/'.$fileName)) { + $zip->close(); - // This generally executes too fast for page to refresh $job = Craft::$app->queue->push(new ImportFiles([ 'description' => 'Updating translation drafts', 'orderId' => $orderId, 'totalFiles' => $total_files, - 'xmlPath' => $xmlPath, + 'assets' => $assetIds, ])); - + if ($job) { $params = [ 'id' => (int) $job, @@ -261,11 +264,55 @@ public function actionImportFile() ]; Craft::$app->getView()->registerJs('$(function(){ Craft.Translations.trackJobProgressById(true, false, '. json_encode($params) .'); });'); } - + // $this->redirect('translations/orders/detail/'. $orderId, 302, true); $this->showUserMessages("File uploaded successfully: $fileName", true); } else { - $this->showUserMessages("Unable to upload file: $fileName"); + $this->showUserMessages("Unable to unzip ". $file->name ." Operation not permitted or Decompression Failed "); } + } elseif ($file->extension === 'xml') { + $filename = Assets::prepareAssetName($file->name); + + $uploadVolumeId = ArrayHelper::getValue(Translations::getInstance()->getSettings(), 'uploadVolume'); + + $folder = Craft::$app->getAssets()->getRootFolderByVolumeId($uploadVolumeId); + + $compatibleFilename = $file->tempName . '.txt'; + + rename($file->tempName, $compatibleFilename); + + $asset = new Asset(); + $asset->tempFilePath = $compatibleFilename; + $asset->filename = $compatibleFilename; + $asset->newFolderId = $folder->id; + $asset->volumeId = $folder->volumeId; + $asset->avoidFilenameConflicts = true; + $asset->uploaderId = Craft::$app->getUser()->getId(); + $asset->setScenario(Asset::SCENARIO_CREATE); + + if (! Craft::$app->getElements()->saveElement($asset)) { + $errors = $asset->getFirstErrors(); + + return $this->asErrorJson(Craft::t('app', 'Failed to save the asset:') . ' ' . implode(";\n", $errors)); + } + + // This generally executes too fast for page to refresh + $job = Craft::$app->queue->push(new ImportFiles([ + 'description' => 'Updating translation drafts', + 'orderId' => $orderId, + 'totalFiles' => $total_files, + 'assets' => [$asset->id], + ])); + + if ($job) { + $params = [ + 'id' => (int) $job, + 'notice' => 'Done updating translation drafts', + 'url' => 'translations/orders/detail/'. $orderId + ]; + Craft::$app->getView()->registerJs('$(function(){ Craft.Translations.trackJobProgressById(true, false, '. json_encode($params) .'); });'); + } + + $this->showUserMessages("File uploaded successfully: {$file->name}", true); } else { $this->showUserMessages("Invalid extention: The plugin only support [ZIP, XML] files."); } diff --git a/src/controllers/SettingsController.php b/src/controllers/SettingsController.php index 961ed9a4..cb684b0f 100644 --- a/src/controllers/SettingsController.php +++ b/src/controllers/SettingsController.php @@ -22,6 +22,7 @@ use craft\helpers\FileHelper; use acclaro\translations\models\Settings; use acclaro\translations\services\job\DeleteDrafts; +use craft\base\VolumeInterface; /** * @author Acclaro @@ -232,6 +233,22 @@ public function actionConfigurationOptions() } $variables['chkDuplicateEntries'] = Translations::getInstance()->settings->chkDuplicateEntries; + $variables['uploadVolume'] = Translations::getInstance()->settings->uploadVolume; + + $allVolumes = Craft::$app->getVolumes()->getAllVolumes(); + + $variables['volumeOptions'] = array_map(function (VolumeInterface $volume) { + return [ + 'label' => $volume->name, + 'value' => $volume->id, + ]; + }, $allVolumes); + + // Add default temp uploads option + array_unshift($variables['volumeOptions'], [ + 'label' => 'Temp Uploads', + 'value' => 0, + ]); $this->renderTemplate('translations/settings/configuration-options', $variables); } @@ -245,12 +262,13 @@ public function actionSaveConfigurationOptions() $request = Craft::$app->getRequest(); $duplicateEntries = $request->getParam('chkDuplicateEntries'); + $selectedVolume = $request->getParam('uploadVolume'); try { $pluginService = Craft::$app->getPlugins(); $plugin = $pluginService->getPlugin('translations'); - if (!$pluginService->savePluginSettings($plugin, ['chkDuplicateEntries' => $duplicateEntries])) { + if (!$pluginService->savePluginSettings($plugin, ['chkDuplicateEntries' => $duplicateEntries, 'uploadVolume' => $selectedVolume])) { Craft::$app->getSession()->setError(Translations::$plugin->translator->translate('app', 'Unable to save setting.')); } else { Craft::$app->getSession()->setNotice(Translations::$plugin->translator->translate('app', 'Setting saved.')); diff --git a/src/models/Settings.php b/src/models/Settings.php index 65d9321b..9014fa54 100644 --- a/src/models/Settings.php +++ b/src/models/Settings.php @@ -26,6 +26,9 @@ class Settings extends Model { public $chkDuplicateEntries = true; + /** @var int The Volume ID where uploads will be saved */ + public $uploadVolume = 0; + public function rules() { return []; diff --git a/src/services/job/ImportFiles.php b/src/services/job/ImportFiles.php index 9dd52b8f..6afda2e3 100644 --- a/src/services/job/ImportFiles.php +++ b/src/services/job/ImportFiles.php @@ -13,14 +13,14 @@ use Craft; use Exception; use craft\base\Element; - +use craft\elements\Asset; use craft\queue\BaseJob; use acclaro\translations\Translations; class ImportFiles extends BaseJob { - public $xmlPath; + public $assets; public $orderId; public $order; public $totalFiles; @@ -28,14 +28,16 @@ class ImportFiles extends BaseJob public function execute($queue) { $this->order = Translations::$plugin->orderRepository->getOrderById($this->orderId); - $dir = new \DirectoryIterator($this->xmlPath); $currentFile = 0; - foreach ($dir as $xml) - { + foreach ($this->assets as $assetId) { + $asset = Craft::$app->getAssets()->getAssetById($assetId); + $this->setProgress($queue, $currentFile++ / $this->totalFiles); //Process XML Files - $this->processFile($xml, $this->xmlPath); + $this->processFile($asset); + + Craft::$app->getElements()->deleteElement($asset); } } @@ -48,150 +50,148 @@ protected function defaultDescription() * Process each file entry per orden * Validates */ - public function processFile( $xml, $path ) + public function processFile( Asset $asset ) { - //Ignore __MAXOSX & ../ ./ Dir - if ($xml->getFileName() !== '__MACOSX' && !$xml->isDot()) + // DEV: Since some Asset Volumes could disallow XML files, we're + // working with files using a 'txt' extension added when the files + // were uploaded. Could alternatively just validate that the + // selected volume in Settings has the xml permission before saving. + if ($asset->getExtension() === 'txt') { - if ($xml->getExtension() === 'xml' && $xml->isReadable()) - { - $translated_file = $path . '/' . $xml; - - $xml_content = file_get_contents( $translated_file ); + $xml_content = $asset->getContents(); - // check if the file is empty - if (empty($xml_content)) { - $this->order->logActivity(Translations::$plugin->translator->translate('app', $xml." file you are trying to import is empty.")); - Translations::$plugin->orderRepository->saveOrder($this->order); - return false; - } + // check if the file is empty + if (empty($xml_content)) { + $this->order->logActivity(Translations::$plugin->translator->translate('app', $asset->getFilename()." file you are trying to import is empty.")); + Translations::$plugin->orderRepository->saveOrder($this->order); + return false; + } - $dom = new \DOMDocument('1.0', 'utf-8'); + $dom = new \DOMDocument('1.0', 'utf-8'); - try + try + { + //Turn LibXml Internal Errors Reporting On! + libxml_use_internal_errors(true); + if (!$dom->loadXML( $xml_content )) { - //Turn LibXml Internal Errors Reporting On! - libxml_use_internal_errors(true); - if (!$dom->loadXML( $xml_content )) + $errors = $this->reportXmlErrors(); + if($errors) { - $errors = $this->reportXmlErrors(); - if($errors) - { - $this->order->logActivity(Translations::$plugin->translator->translate('app', "We found errors on $xml : " . $errors)); - Translations::$plugin->orderRepository->saveOrder($this->order); - return; - } + $this->order->logActivity(Translations::$plugin->translator->translate('app', "We found errors on $asset->getFilename() : " . $errors)); + Translations::$plugin->orderRepository->saveOrder($this->order); + return; } } - catch(Exception $e) - { - $this->order->logActivity(Translations::$plugin->translator->translate('app', $e->getMessage())); - Translations::$plugin->orderRepository->saveOrder($this->order); - } + } + catch(Exception $e) + { + $this->order->logActivity(Translations::$plugin->translator->translate('app', $e->getMessage())); + Translations::$plugin->orderRepository->saveOrder($this->order); + } - //Get DraftId & Lang Nodes From Document - $draftId = false; - $draftElements = $dom->getElementsByTagName('meta'); + //Get DraftId & Lang Nodes From Document + $draftId = false; + $draftElements = $dom->getElementsByTagName('meta'); - //Source & Target Sites - $sites = $dom->getElementsByTagName('sites'); - $sites = isset($sites[0]) ? $sites[0] : $sites; - $sourceSite = (string)$sites->getAttribute('source-site'); - $targetSite = (string)$sites->getAttribute('target-site'); + //Source & Target Sites + $sites = $dom->getElementsByTagName('sites'); + $sites = isset($sites[0]) ? $sites[0] : $sites; + $sourceSite = (string)$sites->getAttribute('source-site'); + $targetSite = (string)$sites->getAttribute('target-site'); - //Source & Target Languages - $langs = $dom->getElementsByTagName('langs'); - $langs = isset($langs[0]) ? $langs[0] : $langs; - $sourceLanguage = (string)$langs->getAttribute('source-language'); - $targetLanguage = (string)$langs->getAttribute('target-language'); + //Source & Target Languages + $langs = $dom->getElementsByTagName('langs'); + $langs = isset($langs[0]) ? $langs[0] : $langs; + $sourceLanguage = (string)$langs->getAttribute('source-language'); + $targetLanguage = (string)$langs->getAttribute('target-language'); - //Iterate Over Draft XML Nodes - foreach ($draftElements as $node) - { - $name = (string) $node->getAttribute('name'); - $value = (string) $node->getAttribute('content'); + //Iterate Over Draft XML Nodes + foreach ($draftElements as $node) + { + $name = (string) $node->getAttribute('name'); + $value = (string) $node->getAttribute('content'); - if ($name === 'draftId') - { - $draftId = (int) $value; - } + if ($name === 'draftId') + { + $draftId = (int) $value; } + } - $draft_file = null; + $draft_file = null; - foreach ($this->order->files as $file) - { - if ($draftId === $file->draftId) - { //Get File - $draft_file = Translations::$plugin->fileRepository->getFileByDraftId($draftId); - } + foreach ($this->order->files as $file) + { + if ($draftId === $file->draftId) + { //Get File + $draft_file = Translations::$plugin->fileRepository->getFileByDraftId($draftId); } + } - //Validate If the draft was found - if (is_null($draft_file)) - { - $this->order->logActivity(Translations::$plugin->translator->translate('app', $xml ." does not match any known entries.")); - Translations::$plugin->orderRepository->saveOrder($this->order); - return; - } + //Validate If the draft was found + if (is_null($draft_file)) + { + $this->order->logActivity(Translations::$plugin->translator->translate('app', $asset->getFilename() ." does not match any known entries.")); + Translations::$plugin->orderRepository->saveOrder($this->order); + return; + } - // Don't process published files - if ($draft_file->status === 'published') - { - $this->order->logActivity(Translations::$plugin->translator->translate('app', "This entry was already published.")); - Translations::$plugin->orderRepository->saveOrder($this->order); - return; - } + // Don't process published files + if ($draft_file->status === 'published') + { + $this->order->logActivity(Translations::$plugin->translator->translate('app', "This entry was already published.")); + Translations::$plugin->orderRepository->saveOrder($this->order); + return; + } - //Translation Service - $translationService = Translations::$plugin->translatorFactory->makeTranslationService($this->order->translator->service, $this->order->translator->getSettings()); + //Translation Service + $translationService = Translations::$plugin->translatorFactory->makeTranslationService($this->order->translator->service, $this->order->translator->getSettings()); - $fileUpdated = $isDraftSave = true; + $fileUpdated = $isDraftSave = true; - try { - $isDraftSave = $translationService->updateIOFile($this->order, $draft_file, $xml_content, $xml); - } catch(Exception $e) { - $fileUpdated = false; - $this->order->logActivity(Translations::$plugin->translator->translate('app', 'Could not update '. $xml. ' Error: ' .$e->getMessage())); - Translations::$plugin->orderRepository->saveOrder($this->order); - } + try { + $isDraftSave = $translationService->updateIOFile($this->order, $draft_file, $xml_content, $asset->getFilename()); + } catch(Exception $e) { + $fileUpdated = false; + $this->order->logActivity(Translations::$plugin->translator->translate('app', 'Could not update '. $asset->getFilename(). ' Error: ' .$e->getMessage())); + Translations::$plugin->orderRepository->saveOrder($this->order); + } - if (!$isDraftSave) { - $draft_file->status = 'failed'; - } else if ($fileUpdated) { - $draft_file->status = 'complete'; - } else { - $draft_file->status = 'in progress'; - } + if (!$isDraftSave) { + $draft_file->status = 'failed'; + } else if ($fileUpdated) { + $draft_file->status = 'complete'; + } else { + $draft_file->status = 'in progress'; + } - //If Successfully saved - $success = Translations::$plugin->fileRepository->saveFile($draft_file); + //If Successfully saved + $success = Translations::$plugin->fileRepository->saveFile($draft_file); - if ($success) - { - if ($isDraftSave) { - $this->order->logActivity( - sprintf(Translations::$plugin->translator->translate('app', "File %s imported successfully!"), $xml) - ); - - //Verify All files on this order were successfully imported. - if ($this->isOrderCompleted()) - { - //Save Order with status complete - $translationService->updateOrder($this->order); - } - } + if ($success) + { + if ($isDraftSave) { + $this->order->logActivity( + sprintf(Translations::$plugin->translator->translate('app', "File %s imported successfully!"), $asset->getFilename()) + ); - Translations::$plugin->orderRepository->saveOrder($this->order); + //Verify All files on this order were successfully imported. + if ($this->isOrderCompleted()) + { + //Save Order with status complete + $translationService->updateOrder($this->order); + } } - } - else - { - //Invalid - $this->order->logActivity(Translations::$plugin->translator->translate('app', "File $xml is invalid, please try again with a valid xml file.")); + Translations::$plugin->orderRepository->saveOrder($this->order); } } + else + { + //Invalid + $this->order->logActivity(Translations::$plugin->translator->translate('app', "File {$asset->getFilename()} is invalid, please try again with a valid xml file.")); + Translations::$plugin->orderRepository->saveOrder($this->order); + } } /** diff --git a/src/templates/settings/configuration-options.twig b/src/templates/settings/configuration-options.twig index 90288c78..5b0a4d7d 100644 --- a/src/templates/settings/configuration-options.twig +++ b/src/templates/settings/configuration-options.twig @@ -26,6 +26,16 @@ on: chkDuplicateEntries, }) }} + {{ forms.selectField({ + label: "Upload Volume"|t('app'), + instructions: "Specify an Asset Volume to use for uploads."|t('app'), + id: 'uploadVolume', + name: 'uploadVolume', + options: volumeOptions, + value: uploadVolume ?? 0, + toggle: true + }) }} +