From b4a6085833b7c6f7c5b31003c1fb8dc69989a5a7 Mon Sep 17 00:00:00 2001 From: Dylan Wagstaff Date: Wed, 20 Dec 2017 14:19:06 +1300 Subject: [PATCH 1/6] FIX: re-rename classes to make more more sense And cross off a few more areas of bringing the syntax up to date. FIX: error generated by not closing the zip file handle. More updates to the ServiceConnector with regards to setting configuration details. The are now environment, config, and locally injectable. --- .upgrade.yml | 8 +- composer.json | 3 +- lang/ar.yml | 8 +- lang/de.yml | 4 +- lang/en.yml | 8 +- lang/eo.yml | 8 +- lang/fa_IR.yml | 4 +- lang/fi.yml | 8 +- lang/id.yml | 4 +- lang/mi.yml | 6 +- lang/ru.yml | 8 +- lang/zh.yml | 8 +- ...umentImporterField.php => ImportField.php} | 59 +++++----- ...cumentsExtension.php => PageExtension.php} | 14 +-- ...mentConverter.php => ServiceConnector.php} | 109 +++++++++++++----- ...tConversionField.php => SettingsField.php} | 16 +-- tests/DocumentConverterDecoratorTest.php | 8 +- 17 files changed, 167 insertions(+), 116 deletions(-) rename src/{DocumentImporterField.php => ImportField.php} (92%) rename src/{ConvertsDocumentsExtension.php => PageExtension.php} (65%) rename src/{DocumentConverter.php => ServiceConnector.php} (60%) rename src/{DocumentConversionField.php => SettingsField.php} (93%) diff --git a/.upgrade.yml b/.upgrade.yml index 189dffc..f8b12ee 100644 --- a/.upgrade.yml +++ b/.upgrade.yml @@ -1,6 +1,6 @@ mappings: - DocumentConverterDecorator: SilverStripe\DocumentConverter\ConvertsDocumentsExtension - DocumentImportField: SilverStripe\DocumentConverter\DocumentConversionField - DocumentImportIFrameField_Importer: SilverStripe\DocumentConverter\DocumentConverter - DocumentImportInnerField: SilverStripe\DocumentConverter\DocumentImporterField + DocumentConverterDecorator: SilverStripe\DocumentConverter\PageExtension + DocumentImportField: SilverStripe\DocumentConverter\SettingsField + DocumentImportIFrameField_Importer: SilverStripe\DocumentConverter\ServiceConnector + DocumentImportInnerField: SilverStripe\DocumentConverter\ImportField DocumentConverterTest: SilverStripe\DocumentConverter\Tests\DocumentConverterTest diff --git a/composer.json b/composer.json index 8a43c26..a5f5246 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,8 @@ "license": "BSD-3-Clause", "keywords": ["silverstripe", "cwp"], "require": { - "silverstripe/cms": "^4" + "silverstripe/cms": "^4", + "silverstripe/asset-admin": "^1" }, "require-dev": { "phpunit/phpunit": "^5.7", diff --git a/lang/ar.yml b/lang/ar.yml index 992c2b3..503092a 100644 --- a/lang/ar.yml +++ b/lang/ar.yml @@ -1,10 +1,10 @@ ar: - SilverStripe\DocumentConverter\DocumentConverter: + SilverStripe\DocumentConverter\ServiceConnector: PROCESSFAILED: 'لم يتم التمكن من معالجة الوثيقة, من فضلك قم بمراجعة أنك قمت برفع الملف بصيغة .دى أو سى أو صيغة .دى أو سى إكس.' SERVERUNREACHABLE: 'لم يتم التواصل مع سيرفر تحويل الوثائق. من فضلك حاول مرة أخرى لاحقا أو اتصل بمشرف النظام الخاص بك.' - SilverStripe\DocumentConverter\ConvertsDocumentsExtension: + SilverStripe\DocumentConverter\PageExtension: ImportTab: استيراد - SilverStripe\DocumentConverter\DocumentConversionField: + SilverStripe\DocumentConverter\SettingsField: ChooseFolder: 'اختر مجلد لحفظ هذا الملف' EachH1: 'لكل عنوان واحد' EachH2: 'لكل عنوان اثنين' @@ -14,7 +14,7 @@ ar: KeepSource: 'احتفظ بالملف الأصلي. وقم بإضافة رابط له في جدول المحتويات، إذا أمكن ذلك.' SplitHeader: 'تقسيم المستند إلى صفحات' publishPages: 'نشر الصفحات المُعدلة (لا يُوصى به إلا إذا كنت متأكد من ناتج التحويل)' - SilverStripe\DocumentConverter\DocumentImporterField: + SilverStripe\DocumentConverter\ImportField: ATTACHONCESAVED2: 'يمكن إرفاق الملفات عند حفظ السجل للمرة الأولى.' UploadField: ATTACHFILE: 'إرفاق ملف' diff --git a/lang/de.yml b/lang/de.yml index c03d14d..4ce7cd7 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -1,7 +1,7 @@ de: - SilverStripe\DocumentConverter\ConvertsDocumentsExtension: + SilverStripe\DocumentConverter\PageExtension: ImportTab: Importieren - SilverStripe\DocumentConverter\DocumentConversionField: + SilverStripe\DocumentConverter\SettingsField: ChooseFolder: 'Wählen Sie einen Ordner zum Speichern dieser Datei aus' EachH1: 'für jede Überschrift 1' EachH2: 'für jede Überschrift 2' diff --git a/lang/en.yml b/lang/en.yml index a2b125c..e88fee9 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -1,10 +1,10 @@ en: - SilverStripe\DocumentConverter\DocumentConverter: + SilverStripe\DocumentConverter\ServiceConnector: PROCESSFAILED: 'Could not process document, please double-check you uploaded a .doc or .docx format.' SERVERUNREACHABLE: 'Could not contact document conversion server. Please try again later or contact your system administrator.' - SilverStripe\DocumentConverter\ConvertsDocumentsExtension: + SilverStripe\DocumentConverter\PageExtension: ImportTab: Import - SilverStripe\DocumentConverter\DocumentConversionField: + SilverStripe\DocumentConverter\SettingsField: ChooseFolder: 'Choose a folder to save this file' EachH1: 'for each heading 1' EachH2: 'for each heading 2' @@ -15,7 +15,7 @@ en: 'No': 'no' SplitHeader: 'Split document into pages' publishPages: 'Publish modified pages (not recommended unless you are sure about the conversion outcome)' - SilverStripe\DocumentConverter\DocumentImporterField: + SilverStripe\DocumentConverter\ImportField: ATTACHONCESAVED2: 'Files can be attached once you have saved the record for the first time.' UploadField: ATTACHFILE: 'Attach a file' diff --git a/lang/eo.yml b/lang/eo.yml index ff7961a..c37e151 100644 --- a/lang/eo.yml +++ b/lang/eo.yml @@ -1,10 +1,10 @@ eo: - SilverStripe\DocumentConverter\DocumentConverter: + SilverStripe\DocumentConverter\ServiceConnector: PROCESSFAILED: 'Ne povis trakti dokumenton. Bonvole kontrolu ke vi alŝutis en formato .doc aŭ .docx.' SERVERUNREACHABLE: 'Ne povis kontakti servilon por konverti dokumenton. Bonvole reprovu postiome aŭ kontaktu vian administranton.' - SilverStripe\DocumentConverter\ConvertsDocumentsExtension: + SilverStripe\DocumentConverter\PageExtension: ImportTab: Importi - SilverStripe\DocumentConverter\DocumentConversionField: + SilverStripe\DocumentConverter\SettingsField: ChooseFolder: 'Elektu dosierujon en kiu konservi la dosieron' EachH1: 'por ĉiu titolo 1' EachH2: 'por ĉiu titolo 2' @@ -15,7 +15,7 @@ eo: 'No': ne SplitHeader: 'Dividi dokumenton en paĝojn' publishPages: 'Publikigi ŝanĝitajn paĝojn (ne rekomendinde krom se vi certas pri la rezulto de konverto)' - SilverStripe\DocumentConverter\DocumentImporterField: + SilverStripe\DocumentConverter\ImportField: ATTACHONCESAVED2: 'Kiam vi unuafoje estos konservinta la rikordon vi povos alligi dosierojn.' UploadField: ATTACHFILE: 'Alligi dosieron' diff --git a/lang/fa_IR.yml b/lang/fa_IR.yml index a98ba83..d678140 100644 --- a/lang/fa_IR.yml +++ b/lang/fa_IR.yml @@ -1,7 +1,7 @@ fa_IR: - SilverStripe\DocumentConverter\ConvertsDocumentsExtension: + SilverStripe\DocumentConverter\PageExtension: ImportTab: وارد کردن - SilverStripe\DocumentConverter\DocumentConversionField: + SilverStripe\DocumentConverter\SettingsField: ChooseFolder: 'یک پوشه برای ذخیره این فایل را انتخاب کنید' UploadField: ATTACHFILE: 'یک فایل ضمیمه کنید' diff --git a/lang/fi.yml b/lang/fi.yml index 2432796..cda8648 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -1,10 +1,10 @@ fi: - SilverStripe\DocumentConverter\DocumentConverter: + SilverStripe\DocumentConverter\ServiceConnector: PROCESSFAILED: 'Dokumenttia ei voitu prosessoida, ole hyvä ja tarkista että siirtämäsi dokumentit olivat varmasti .doc tai .docx muodossa.' SERVERUNREACHABLE: 'Muunnospalvelimeen ei saatu yhteyttä. Ole hyvä ja yritä myöhemmin uudelleen tai ota yhteyttä järjestelmän ylläpitäjään.' - SilverStripe\DocumentConverter\ConvertsDocumentsExtension: + SilverStripe\DocumentConverter\PageExtension: ImportTab: Tuo - SilverStripe\DocumentConverter\DocumentConversionField: + SilverStripe\DocumentConverter\SettingsField: ChooseFolder: 'Valitse kansio, jonne tiedosto tallennetaan' EachH1: 'jokaiselle otsikko 1' EachH2: 'jokaiselle otsikko 2' @@ -15,7 +15,7 @@ fi: 'No': ei SplitHeader: 'Jaa dokumentti sivuiksi' publishPages: 'Julkaise muokatut sivut (ei suositella, jollet ole varma muunnoksen lopputulemasta)' - SilverStripe\DocumentConverter\DocumentImporterField: + SilverStripe\DocumentConverter\ImportField: ATTACHONCESAVED2: 'Tiedostot voidaan liittää, kun olet tallentanut ensin tietueen.' UploadField: ATTACHFILE: 'Liitä tiedosto' diff --git a/lang/id.yml b/lang/id.yml index f0683c6..97850e8 100644 --- a/lang/id.yml +++ b/lang/id.yml @@ -1,7 +1,7 @@ id: - SilverStripe\DocumentConverter\ConvertsDocumentsExtension: + SilverStripe\DocumentConverter\PageExtension: ImportTab: Impor - SilverStripe\DocumentConverter\DocumentConversionField: + SilverStripe\DocumentConverter\SettingsField: ChooseFolder: 'Pilih sebuah map untuk menyimpan berkas ini' UploadField: FROMCOMPUTER: 'Dari komputermu' diff --git a/lang/mi.yml b/lang/mi.yml index 67ed3fa..d4c05ae 100644 --- a/lang/mi.yml +++ b/lang/mi.yml @@ -1,10 +1,10 @@ mi: - SilverStripe\DocumentConverter\DocumentConverter: + SilverStripe\DocumentConverter\ServiceConnector: PROCESSFAILED: 'Kāore i taea te tukatuka te tuhinga, whakatūturu pūtia kua tukuna atu he hōputu .doc, .docx rānei.' SERVERUNREACHABLE: 'Kāore i taea te whakapā ki te tūmau huri tuhinga. Me ngana anō ā muri ake, ka whakapā rānei ki tō kaiwhakahaere pūnaha.' - DocumentCSilverStripe\DocumentConverter\ConvertsDocumentsExtension: + DocumentCSilverStripe\DocumentConverter\PageExtension: ImportTab: Kawemai - SilverStripe\DocumentConverter\DocumentImporterField: + SilverStripe\DocumentConverter\ImportField: ATTACHONCESAVED2: 'Ka taea te āpiti kōnae ina oti te tiaki tuatahi o te pūkete.' UploadField: ATTACHFILE: 'Tāpiritia tētahi kōnae' diff --git a/lang/ru.yml b/lang/ru.yml index 5158ff1..aa93319 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -1,10 +1,10 @@ ru: - SilverStripe\DocumentConverter\DocumentConverter: + SilverStripe\DocumentConverter\ServiceConnector: PROCESSFAILED: 'Не могу обработать документ, пожалуйста проверьте что документ загружен в формате .doc или .docx.' SERVERUNREACHABLE: 'Не могу подключиться к серверу конвертации документов. Пожалуйста попробуйте позднее или свяжитесь с вашим системным администратором.' - DocumentConverterDecorator: + SilverStripe\DocumentConverter\PageExtension: ImportTab: Импорт - DocumentImportField: + SilverStripe\DocumentConverter\SettingsField: ChooseFolder: 'Выберите папку для сохранения файла' EachH1: 'для каждого заголовка 1' EachH2: 'для каждого заголовка 2' @@ -15,7 +15,7 @@ ru: 'No': нет SplitHeader: 'Разделить документ на страницы' publishPages: 'Опубликовать модифицированные страницы (не рекомендуется если вы не уверены в последствиях)' - SilverStripe\DocumentConverter\DocumentImporterField: + SilverStripe\DocumentConverter\ImportField: ATTACHONCESAVED2: 'Вы сможете прикрепитьфайлы после первого сохранения записи.' UploadField: ATTACHFILE: 'Прикрепить файл' diff --git a/lang/zh.yml b/lang/zh.yml index 4caab4c..70dcbba 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -1,10 +1,10 @@ zh: - SilverStripe\DocumentConverter\DocumentConverter: + SilverStripe\DocumentConverter\ServiceConnector: PROCESSFAILED: '无法处理文档,请复核您上传的是 .doc 或 .docx 格式。' SERVERUNREACHABLE: '无法连到文档转换服务器。请稍后再试或联系系统管理员。' - SilverStripe\DocumentConverter\ConvertsDocumentsExtension: + SilverStripe\DocumentConverter\PageExtension: ImportTab: 导入 - SilverStripe\DocumentConverter\DocumentConversionField: + SilverStripe\DocumentConverter\SettingsField: ChooseFolder: '选择文件夹保存此文件' EachH1: '每个标题1' EachH2: '每个标题2' @@ -14,7 +14,7 @@ zh: KeepSource: '保留原始文档。如果允许的话,在 TOC 上为其添加一个链接。' SplitHeader: 拆分文件转换成网页 publishPages: '发布修改页面(除非你确认转换结果,否则不推荐)' - SilverStripe\DocumentConverter\DocumentImporterField: + SilverStripe\DocumentConverter\ImportField: ATTACHONCESAVED2: '只要您第一次保存记录,文件就可以被附加。' UploadField: ATTACHFILE: '附加一个文件' diff --git a/src/DocumentImporterField.php b/src/ImportField.php similarity index 92% rename from src/DocumentImporterField.php rename to src/ImportField.php index 67fdb51..45be3cc 100644 --- a/src/DocumentImporterField.php +++ b/src/ImportField.php @@ -19,7 +19,8 @@ use SilverStripe\Control\Director; use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPResponse; -use SilverStripe\Forms\HTMLEditor\HtmlEditorConfig; +use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig; +use SilverStripe\Forms\HTMLEditor\HTMLEditorSanitiser; use SilverStripe\ORM\DataObject; use SilverStripe\Versioned\Versioned; use Tidy; @@ -50,11 +51,11 @@ * * Caveat: there is some coupling between the above parameters. */ -class DocumentImporterField extends UploadField { +class ImportField extends UploadField { private static $allowed_actions = ['upload']; - private static $importer_class = DocumentConverter::class; + private static $importer_class = ServiceConnector::class; /** * Process the document immediately upon upload. @@ -66,19 +67,22 @@ public function upload(HTTPRequest $request) { $token = $this->getForm()->getSecurityToken(); if(!$token->checkRequest($request)) return $this->httpError(400); - $name = $this->getName(); - $tmpfile = $request->postVar($name); + $tmpfile = $request->postVar('Upload'); // Check if the file has been uploaded into the temporary storage. if (!$tmpfile) { - $return = array('error' => _t('SilverStripe\\AssetAdmin\\Forms\\UploadField.FIELDNOTSET', 'File information not found')); + $return = [ + 'error' => _t( + 'SilverStripe\\AssetAdmin\\Forms\\UploadField.FIELDNOTSET', 'File information not found' + ) + ]; } else { - $return = array( + $return = [ 'name' => $tmpfile['name'], 'size' => $tmpfile['size'], 'type' => $tmpfile['type'], 'error' => $tmpfile['error'] - ); + ]; } if (!$return['error']) { @@ -101,7 +105,7 @@ public function upload(HTTPRequest $request) { } } - $response = HTTPResponse::create(Convert::raw2json(array($return))); + $response = HTTPResponse::create(Convert::raw2json([$return])); $response->addHeader('Content-Type', 'text/plain'); return $response; } @@ -242,14 +246,14 @@ protected function writeContent($subtitle, $subdoc, $subnode, $sort = null, $pub */ public function importFromPOST($tmpFile, $splitHeader = false, $publishPages = false, $chosenFolderID = null) { - $fileDescriptor = array( + $fileDescriptor = [ 'name' => $tmpFile['name'], 'path' => $tmpFile['tmp_name'], 'mimeType' => $tmpFile['type'] - ); + ]; $sourcePage = $this->form->getRecord(); - $importerClass = Config::inst()->get(__CLASS__, 'importer_class'); + $importerClass = $this->config()->get('importer_class'); $importer = Injector::inst()->create($importerClass, $fileDescriptor, $chosenFolderID); $content = $importer->import(); @@ -259,7 +263,7 @@ public function importFromPOST($tmpFile, $splitHeader = false, $publishPages = f // Clean up with tidy (requires tidy module) $tidy = new Tidy(); - $tidy->parseString($content, array('output-xhtml' => true), 'utf8'); + $tidy->parseString($content, ['output-xhtml' => true], 'utf8'); $tidy->cleanRepair(); $fragment = []; @@ -270,7 +274,7 @@ public function importFromPOST($tmpFile, $splitHeader = false, $publishPages = f $htmlValue = Injector::inst()->create('HTMLValue', implode("\n", $fragment)); // Sanitise - $santiser = Injector::inst()->create('HtmlEditorSanitiser', HtmlEditorConfig::get_active()); + $santiser = Injector::inst()->create(HTMLEditorSanitiser::class, HTMLEditorConfig::get_active()); $santiser->sanitise($htmlValue); // Load in the HTML @@ -286,10 +290,13 @@ public function importFromPOST($tmpFile, $splitHeader = false, $publishPages = f $originalPath = 'assets/' . $folderName . '/' . $img->getAttribute('src'); $name = FileNameFilter::create()->filter(basename($originalPath)); - $image = Image::get()->filter(array('Name' => $name, 'ParentID' => (int) $chosenFolderID))->first(); + $image = Image::get()->filter([ + 'Name' => $name, + 'ParentID' => (int)$chosenFolderID + ])->first(); if(!($image && $image->exists())) { $image = Image::create(); - $image->ParentID = (int) $chosenFolderID; + $image->ParentID = (int)$chosenFolderID; $image->Name = $name; $image->write(); } @@ -301,10 +308,10 @@ public function importFromPOST($tmpFile, $splitHeader = false, $publishPages = f $img->setAttribute('src', $image->getFilename()); } - $remove_rules = array( + $remove_rules = [ '//h1[.//font[not(@face)]]' => 'p', // Change any headers that contain font tags (other than font face tags) into p elements '//font' // Remove any font tags - ); + ]; foreach($remove_rules as $rule => $parenttag) { if(is_numeric($rule)) { @@ -312,7 +319,7 @@ public function importFromPOST($tmpFile, $splitHeader = false, $publishPages = f $parenttag = null; } - $nodes = array(); + $nodes = []; foreach($xpath->query($rule) as $node) $nodes[] = $node; foreach($nodes as $node) { @@ -340,17 +347,17 @@ public function importFromPOST($tmpFile, $splitHeader = false, $publishPages = f $els = $doc->getElementsByTagName('*'); // Remove a bunch of unwanted elements - $clean = array( + $clean = [ '//p[not(descendant-or-self::text() | descendant-or-self::img)]', // Empty paragraphs '//*[self::h1 | self::h2 | self::h3 | self::h4 | self::h5 | self::h6][not(descendant-or-self::text() | descendant-or-self::img)]', // Empty headers '//a[not(@href)]', // Anchors '//br' // BR tags - ); + ]; foreach($clean as $query) { // First get all the nodes. Need to build array, as they'll disappear from the nodelist while we're deleteing them, causing the indexing // to screw up. - $nodes = array(); + $nodes = []; foreach($xpath->query($query) as $node) $nodes[] = $node; // Then remove them all @@ -360,16 +367,16 @@ public function importFromPOST($tmpFile, $splitHeader = false, $publishPages = f // Now split the document into portions by H1 $body = $doc->getElementsByTagName('body')->item(0); - $this->unusedChildren = array(); + $this->unusedChildren = []; foreach($sourcePage->Children() as $child) { $this->unusedChildren[$child->ID] = $child; } - $documentImporterFieldError; + $documentImporterFieldError = false; $documentImporterFieldErrorHandler = function ($errno, $errstr, $errfile, $errline) use ( $documentImporterFieldError ) { $documentImporterFieldError = _t( - 'SilverStripe\\DocumentConverter\\DocumentConverter.PROCESSFAILED', + 'SilverStripe\\DocumentConverter\\ServiceConnector.PROCESSFAILED', 'Could not process document, please double-check you uploaded a .doc or .docx format.', 'Document Converter processes Word documents into HTML.' ); @@ -412,7 +419,7 @@ public function importFromPOST($tmpFile, $splitHeader = false, $publishPages = f restore_error_handler(); if ($documentImporterFieldError) { - return array('error' => $documentImporterFieldError); + return ['error' => $documentImporterFieldError]; } foreach($this->unusedChildren as $child) { diff --git a/src/ConvertsDocumentsExtension.php b/src/PageExtension.php similarity index 65% rename from src/ConvertsDocumentsExtension.php rename to src/PageExtension.php index 3c8df15..0a7b5e3 100644 --- a/src/ConvertsDocumentsExtension.php +++ b/src/PageExtension.php @@ -6,26 +6,26 @@ use SilverStripe\Forms\FieldList; use SilverStripe\ORM\DataExtension; -class ConvertsDocumentsExtension extends DataExtension { +class PageExtension extends DataExtension { - private static $has_one = array( + private static $has_one = [ 'ImportedFromFile' => File::class - ); + ]; function updateCMSFields(FieldList $fields) { /* // Currently the ToggleCompositeField plays badly with TreeDropdownField formatting. // Could be switched back in the future, if this is fixed. $fields->addFieldToTab('Root.Main', - ToggleCompositeField::create('Import', 'Import', array( - new DocumentImportField() - ))->setHeadingLevel(4) + ToggleCompositeField::create('Import', 'Import', [ + SettingsField::create() + ])->setHeadingLevel(4) ); */ $fields->findOrMakeTab( 'Root.Import', _t(__CLASS__ . '.ImportTab', 'Import') ); - $fields->addFieldToTab('Root.Import', DocumentConversionField::create()); + $fields->addFieldToTab('Root.Import', SettingsField::create()); } } diff --git a/src/DocumentConverter.php b/src/ServiceConnector.php similarity index 60% rename from src/DocumentConverter.php rename to src/ServiceConnector.php index 74fe6f3..a62b099 100644 --- a/src/DocumentConverter.php +++ b/src/ServiceConnector.php @@ -5,25 +5,36 @@ use CURLFile; use SilverStripe\Assets\Folder; use SilverStripe\Core\Config\Configurable; +use SilverStripe\Core\Environment; +use SilverStripe\Core\Injector\Injectable; use SilverStripe\ORM\DataObject; use ZipArchive; /** * Utility class hiding the specifics of the document conversion process. */ -class DocumentConverter { +class ServiceConnector { use Configurable; + use Injectable; /** - * @var array Docvert connection details * @config + * @var array Docvert connection username */ - private static $docvert_details = [ - 'username' => '', - 'password' => '', - 'url' => '' - ]; + private static $username = null; + + /** + * @config + * @var array Docvert connection password + */ + private static $password = null; + + /** + * @config + * @var array Docvert service URL + */ + private static $url = null; /** * Associative array of: @@ -41,42 +52,76 @@ class DocumentConverter { /** * @var array instance specific connection details - * initially filled with the config settings */ protected $docvertDetails = [ - 'username' => '', - 'password' => '', - 'url' => '' + 'username' => null, + 'password' => null, + 'url' => null ]; public function __construct($fileDescriptor, $chosenFolderID = null) { $this->fileDescriptor = $fileDescriptor; $this->chosenFolderID = $chosenFolderID; - array_merge($this->docvertDetails, (array)$this->config()->get('docvert_details')); } - public function setDocvertUsername($username = null) { + public function setUsername($username = null) { $this->docvertDetails['username'] = $username; } - public function getDocvertUsername() { - return $this->docvertDetails['username']; + public function getUsername() { + $username = $this->docvertDetails['username']; + if ($username) { + return $username; + } + $username = $this->config()->get('username'); + if ($username) { + return $username; + } + $username = Environment::getEnv('DOCVERT_USERNAME'); + if ($username) { + return $username; + } + return null; } - public function setDocvertPassword($password = null) { + public function setPassword($password = null) { $this->docvertDetails['password'] = $password; } - public function getDocvertPassword() { - return $this->docvertDetails['password']; + public function getPassword() { + $username = $this->docvertDetails['password']; + if ($username) { + return $username; + } + $username = $this->config()->get('password'); + if ($username) { + return $username; + } + $username = Environment::getEnv('DOCVERT_PASSWORD'); + if ($username) { + return $username; + } + return null; } - public function setDocvertUrl($url = null) { + public function setUrl($url = null) { $this->docvertDetails['url'] = $url; } - public function getDocvertUrl() { - return $this->docvertDetails['url']; + public function getUrl() { + $username = $this->docvertDetails['url']; + if ($username) { + return $username; + } + $username = $this->config()->get('url'); + if ($username) { + return $username; + } + $username = Environment::getEnv('DOCVERT_URL'); + if ($username) { + return $username; + } + return null; } public function import() { @@ -93,20 +138,19 @@ public function import() { $file = '@' . $this->fileDescriptor['path']; } - curl_setopt_array($ch, array( - CURLOPT_URL => $this->getDocvertUrl(), - CURLOPT_USERPWD => sprintf('%s:%s', $this->getDocvertUsername(), $this->getDocvertPassword()), + curl_setopt_array($ch, [ + CURLOPT_URL => $this->getUrl(), + CURLOPT_USERPWD => sprintf('%s:%s', $this->getUsername(), $this->getPassword()), CURLOPT_POST => 1, - CURLOPT_POSTFIELDS => array('file' => $file), + CURLOPT_POSTFIELDS => ['file' => $file], CURLOPT_CONNECTTIMEOUT => 25, CURLOPT_TIMEOUT => 100, - )); + ]); $chosenFolder = ($this->chosenFolderID) ? DataObject::get_by_id(Folder::class, $this->chosenFolderID) : null; $folderName = ($chosenFolder) ? '/' . $chosenFolder->Name : ''; $outname = tempnam(ASSETS_PATH, 'convert'); $outzip = $outname . '.zip'; - $out = fopen($outzip, 'w'); curl_setopt($ch, CURLOPT_FILE, $out); $returnValue = curl_exec($ch); @@ -116,19 +160,20 @@ public function import() { chmod($outzip, 0666); if (!$returnValue || ($status != 200)) { - return array('error' => _t( + return ['error' => _t( __CLASS__ . '.SERVERUNREACHABLE', 'Could not contact document conversion server. Please try again later or contact your system administrator.', 'Document Converter process Word documents into HTML.' - )); + )]; } // extract the converted document into assets - // you need php zip, i.e. port install php5-zip + // you need php zip, e.g. apt-get install php-zip $zip = new ZipArchive(); if($zip->open($outzip)) { $zip->extractTo(ASSETS_PATH .$folderName); + $zip->close(); } // remove temporary files @@ -136,11 +181,11 @@ public function import() { unlink($outzip); if (!file_exists(ASSETS_PATH . $folderName . '/index.html')) { - return array('error' => _t( + return ['error' => _t( __CLASS__ . '.PROCESSFAILED', 'Could not process document, please double-check you uploaded a .doc or .docx format.', 'Document Converter processes Word documents into HTML.' - )); + )]; } $content = file_get_contents(ASSETS_PATH . $folderName . '/index.html'); diff --git a/src/DocumentConversionField.php b/src/SettingsField.php similarity index 93% rename from src/DocumentConversionField.php rename to src/SettingsField.php index 843cbb5..d514a2f 100644 --- a/src/DocumentConversionField.php +++ b/src/SettingsField.php @@ -17,9 +17,9 @@ * Includes several options fields, which are bundled together with an UploadField * into a CompositeField. */ -class DocumentConversionField extends CompositeField { +class SettingsField extends CompositeField { /** - * Reference to the inner upload field (DocumentImporterField). + * Reference to the inner upload field (ImportField). */ private $innerField = null; @@ -35,7 +35,7 @@ public function __construct($children = null) { // Add JS specific to this field. Requirements::javascript('silverstripe/documentconverter: javascript/DocumentConversionField.js'); - $fields = FieldList::create(array( + $fields = FieldList::create([ HeaderField::create( 'FileWarningHeader', _t( @@ -50,11 +50,11 @@ public function __construct($children = null) { __CLASS__ . '.SplitHeader', 'Split document into pages' ), - array( + [ 0 => _t(__CLASS__ . '.No','no'), 1 => _t(__CLASS__ . '.EachH1','for each heading 1'), 2 => _t(__CLASS__ . '.EachH2','for each heading 2') - ) + ] ), $keepSource = CheckboxField::create( 'DocumentConversionField-KeepSource', @@ -79,11 +79,11 @@ public function __construct($children = null) { 'Publish modified pages (not recommended unless you are sure about the conversion outcome)' ) ), - $this->innerField = DocumentImporterField::create( + $this->innerField = ImportField::create( 'ImportedFromFile', _t(__CLASS__ . '.ImportedFromFile','Import content from a word document') - ), - )); + ) + ]); // Prevent the warning popup that appears when navigating away from the page. $splitHeader->addExtraClass('no-change-track'); diff --git a/tests/DocumentConverterDecoratorTest.php b/tests/DocumentConverterDecoratorTest.php index 37d0828..e634a84 100644 --- a/tests/DocumentConverterDecoratorTest.php +++ b/tests/DocumentConverterDecoratorTest.php @@ -2,11 +2,9 @@ class DocumentConverterDecoratorTest extends SapphireTest { - protected $requiredExtensions = array( - 'SiteTree' => array( - 'DocumentConverterDecorator', - ), - ); + protected $requiredExtensions = [ + 'SiteTree' => ['DocumentConverterDecorator'] + ]; public function testFieldListHasDocumentImportField() { From 30b94797bb8998ef517677aaecd45a2218a9c83a Mon Sep 17 00:00:00 2001 From: Dylan Wagstaff Date: Wed, 20 Dec 2017 16:54:06 +1300 Subject: [PATCH 2/6] FIX: clean codebase to be PSR-2 compliant --- src/ImportField.php | 838 ++++++++++++----------- src/PageExtension.php | 26 +- src/ServiceConnector.php | 373 +++++----- src/SettingsField.php | 163 +++-- tests/DocumentConverterDecoratorTest.php | 4 + tests/DocumentConverterTest.php | 8 +- tests/DocumentImportFieldTest.php | 4 + 7 files changed, 754 insertions(+), 662 deletions(-) diff --git a/src/ImportField.php b/src/ImportField.php index 45be3cc..306c403 100644 --- a/src/ImportField.php +++ b/src/ImportField.php @@ -25,7 +25,6 @@ use SilverStripe\Versioned\Versioned; use Tidy; - /** * DocumentImporterField is built on top of UploadField to access a document * conversion capabilities. The original field is stripped down to allow only @@ -51,391 +50,454 @@ * * Caveat: there is some coupling between the above parameters. */ -class ImportField extends UploadField { - - private static $allowed_actions = ['upload']; - - private static $importer_class = ServiceConnector::class; - - /** - * Process the document immediately upon upload. - */ - public function upload(HTTPRequest $request) { - if($this->isDisabled() || $this->isReadonly()) return $this->httpError(403); - - // Protect against CSRF on destructive action - $token = $this->getForm()->getSecurityToken(); - if(!$token->checkRequest($request)) return $this->httpError(400); - - $tmpfile = $request->postVar('Upload'); - - // Check if the file has been uploaded into the temporary storage. - if (!$tmpfile) { - $return = [ - 'error' => _t( - 'SilverStripe\\AssetAdmin\\Forms\\UploadField.FIELDNOTSET', 'File information not found' - ) - ]; - } else { - $return = [ - 'name' => $tmpfile['name'], - 'size' => $tmpfile['size'], - 'type' => $tmpfile['type'], - 'error' => $tmpfile['error'] - ]; - } - - if (!$return['error']) { - // Get options for this import. - $splitHeader = (int)$request->postVar('SplitHeader'); - $keepSource = (bool)$request->postVar('KeepSource'); - $chosenFolderID = (int)$request->postVar('ChosenFolderID'); - $publishPages = (bool)$request->postVar('PublishPages'); - $includeTOC = (bool)$request->postVar('IncludeTOC'); - - // Process the document and write the page. - $preservedDocument = null; - if ($keepSource) $preservedDocument = $this->preserveSourceDocument($tmpfile, $chosenFolderID); - - $importResult = $this->importFromPOST($tmpfile, $splitHeader, $publishPages, $chosenFolderID); - if (is_array($importResult) && isset($importResult['error'])) { - $return['error'] = $importResult['error']; - } else if ($includeTOC) { - $this->writeTOC($publishPages, $keepSource ? $preservedDocument : null); - } - } - - $response = HTTPResponse::create(Convert::raw2json([$return])); - $response->addHeader('Content-Type', 'text/plain'); - return $response; - } - - /** - * Preserves the source file by copying it to a specified folder. - * - * @param $tmpfile Temporary file data structure. - * @param int $chosenFolderID Target folder. - * @return File Stored file. - */ - protected function preserveSourceDocument($tmpfile, $chosenFolderID = null) { - $upload = Upload::create(); - - $file = File::create(); - $upload->loadIntoFile($tmpfile, $file, $chosenFolderID); - - $page = $this->form->getRecord(); - $page->ImportedFromFileID = $file->ID; - $page->write(); - - return $file; - } - - /** - * Builds and writes the table of contents for the document. - * - * @param bool $publishPage Should the parent page be published. - * @param File $preservedDocument Set if the link to the original document should be added. - */ - protected function writeTOC($publishPages = false, $preservedDocument = null) { - $page = $this->form->getRecord(); - $content = ''; - } else { - $doc = new DOMDocument(); - $doc->loadHTML($page->Content); - $body = $doc->getElementsByTagName('body')->item(0); - $node = $body->firstChild; - $h1 = $h2 = 1; - while($node) { - if($node instanceof DOMElement && $node->tagName == 'h1') { - $content .= '
  • '. trim(preg_replace('/\n|\r/', '', Convert::html2raw($node->textContent))) . '
  • '; - $node->setAttributeNode(new DOMAttr("id", "h1.".$h1)); - $h1++; - } elseif($node instanceof DOMElement && $node->tagName == 'h2') { - $content .= ''; - $node->setAttributeNode(new DOMAttr("id", "h2.".$h2)); - $h2++; - } - $node = $node->nextSibling; - } - $page->Content = $content . '' . $doc->saveHTML(); - } - - // Add in the link to the original document, if provided. - if($preservedDocument) { - $page->Content = 'download original document (' . - $preservedDocument->getSize() . ')' . $page->Content; - } - - // Store the result - $page->write(); - if($publishPages) $page->doPublish(); - } - } - - protected function getBodyText($doc, $node) { - // Build a new doc - $htmldoc = new DOMDocument(); - // Create the html element - $html = $htmldoc->createElement('html'); - $htmldoc->appendChild($html); - // Append the body node - $html->appendChild($htmldoc->importNode($node, true)); - - // Get the text as html, remove the entry and exit root tags and return - $text = $htmldoc->saveHTML(); - $text = preg_replace('/^.*/', '', $text); - $text = preg_replace('/<\/body>.*$/', '', $text); - - return $text; - } - - /** - * Used only when writing the document that has been split by headers. - * Can write both to the chapter pages as well as the master page. - * - * @param string $subtitle Title of the chapter - if missing, it will write to the master page. - * @param $subdoc - * @param $subnode - * @param int $sort Order of the chapter page. - * @param $publishPages Whether to publish the resulting child/master pages. - */ - protected function writeContent($subtitle, $subdoc, $subnode, $sort = null, $publishPages = false) { - $record = $this->form->getRecord(); - - if($subtitle) { - // Write the chapter page to a subpage. - $page = DataObject::get_one('Page', sprintf('"Title" = \'%s\' AND "ParentID" = %d', $subtitle, $record->ID)); - if(!$page) { - $page = Page::create(); - $page->ParentID = $record->ID; - $page->Title = $subtitle; - } - - unset($this->unusedChildren[$page->ID]); - file_put_contents(ASSETS_PATH . '/index-' . $sort . '.html', $this->getBodyText($subdoc, $subnode)); - - if ($sort) $page->Sort = $sort; - $page->Content = $this->getBodyText($subdoc, $subnode); - $page->write(); - if($publishPages) $page->doPublish(); - } else { - // Write to the master page. - $record->Content = $this->getBodyText($subdoc, $subnode); - $record->write(); - - if($publishPages) $record->doPublish(); - } - - } - - /** - * Imports a document at a certain path onto the current page and writes it. - * CAUTION: Overwrites any existing content on the page! - * - * @param array $tmpFile Array as received from PHP's POST upload. - * @param bool $splitHeader Heading level to split by. - * @param bool $publishPages Whether the underlying pages should be published after import. - * @param int $chosenFolderID ID of the working folder - here the converted file and images will be stored. - */ - public function importFromPOST($tmpFile, $splitHeader = false, $publishPages = false, $chosenFolderID = null) { - - $fileDescriptor = [ - 'name' => $tmpFile['name'], - 'path' => $tmpFile['tmp_name'], - 'mimeType' => $tmpFile['type'] - ]; - - $sourcePage = $this->form->getRecord(); - $importerClass = $this->config()->get('importer_class'); - $importer = Injector::inst()->create($importerClass, $fileDescriptor, $chosenFolderID); - $content = $importer->import(); - - if (is_array($content) && isset($content['error'])) { - return $content; - } - - // Clean up with tidy (requires tidy module) - $tidy = new Tidy(); - $tidy->parseString($content, ['output-xhtml' => true], 'utf8'); - $tidy->cleanRepair(); - - $fragment = []; - foreach($tidy->body()->child as $child) { - $fragment[] = $child->value; - } - - $htmlValue = Injector::inst()->create('HTMLValue', implode("\n", $fragment)); - - // Sanitise - $santiser = Injector::inst()->create(HTMLEditorSanitiser::class, HTMLEditorConfig::get_active()); - $santiser->sanitise($htmlValue); - - // Load in the HTML - $doc = $htmlValue->getDocument(); - $xpath = new DOMXPath($doc); - - // make sure any images are added as Image records with a relative link to assets - $chosenFolder = ($this->chosenFolderID) ? DataObject::get_by_id(Folder::class, $this->chosenFolderID) : null; - $folderName = ($chosenFolder) ? '/' . $chosenFolder->Name : ''; - $imgs = $xpath->query('//img'); - for($i = 0; $i < $imgs->length; $i++) { - $img = $imgs->item($i); - $originalPath = 'assets/' . $folderName . '/' . $img->getAttribute('src'); - $name = FileNameFilter::create()->filter(basename($originalPath)); - - $image = Image::get()->filter([ - 'Name' => $name, - 'ParentID' => (int)$chosenFolderID - ])->first(); - if(!($image && $image->exists())) { - $image = Image::create(); - $image->ParentID = (int)$chosenFolderID; - $image->Name = $name; - $image->write(); - } - - // make sure it's put in place correctly so Image record knows where it is. - // e.g. in the case of underscores being renamed to dashes. - @rename(Director::getAbsFile($originalPath), Director::getAbsFile($image->getFilename())); - - $img->setAttribute('src', $image->getFilename()); - } - - $remove_rules = [ - '//h1[.//font[not(@face)]]' => 'p', // Change any headers that contain font tags (other than font face tags) into p elements - '//font' // Remove any font tags - ]; - - foreach($remove_rules as $rule => $parenttag) { - if(is_numeric($rule)) { - $rule = $parenttag; - $parenttag = null; - } - - $nodes = []; - foreach($xpath->query($rule) as $node) $nodes[] = $node; - - foreach($nodes as $node) { - $parent = $node->parentNode; - - if($parenttag) { - $parent = $doc->createElement($parenttag); - $node->nextSibling ? $node->parentNode->insertBefore($parent, $node->nextSibling) : $node->parentNode->appendChild($parent); - } - - while($node->firstChild) $parent->appendChild($node->firstChild); - $node->parentNode->removeChild($node); - } - } - - // Strip style, class, lang attributes. - $els = $doc->getElementsByTagName('*'); - for ($i = 0; $i < $els->length; $i++) { - $el = $els->item($i); - $el->removeAttribute('class'); - $el->removeAttribute('style'); - $el->removeAttribute('lang'); - } - - $els = $doc->getElementsByTagName('*'); - - // Remove a bunch of unwanted elements - $clean = [ - '//p[not(descendant-or-self::text() | descendant-or-self::img)]', // Empty paragraphs - '//*[self::h1 | self::h2 | self::h3 | self::h4 | self::h5 | self::h6][not(descendant-or-self::text() | descendant-or-self::img)]', // Empty headers - '//a[not(@href)]', // Anchors - '//br' // BR tags - ]; - - foreach($clean as $query) { - // First get all the nodes. Need to build array, as they'll disappear from the nodelist while we're deleteing them, causing the indexing - // to screw up. - $nodes = []; - foreach($xpath->query($query) as $node) $nodes[] = $node; - - // Then remove them all - foreach ($nodes as $node) { if ($node->parentNode) $node->parentNode->removeChild($node); } - } - - // Now split the document into portions by H1 - $body = $doc->getElementsByTagName('body')->item(0); - - $this->unusedChildren = []; - foreach($sourcePage->Children() as $child) { - $this->unusedChildren[$child->ID] = $child; - } - - $documentImporterFieldError = false; - - $documentImporterFieldErrorHandler = function ($errno, $errstr, $errfile, $errline) use ( $documentImporterFieldError ) { - $documentImporterFieldError = _t( - 'SilverStripe\\DocumentConverter\\ServiceConnector.PROCESSFAILED', - 'Could not process document, please double-check you uploaded a .doc or .docx format.', - 'Document Converter processes Word documents into HTML.' - ); - - // Do not cascade the error through other handlers - return true; - }; - - set_error_handler($documentImporterFieldErrorHandler); - - $subtitle = null; - $subdoc = new DOMDocument(); - $subnode = $subdoc->createElement('body'); - $node = $body->firstChild; - $sort = 1; - if($splitHeader == 1 || $splitHeader == 2) { - while($node && !$documentImporterFieldError) { - if($node instanceof DOMElement && $node->tagName == 'h' . $splitHeader) { - if($subnode->hasChildNodes()) { - $this->writeContent($subtitle, $subdoc, $subnode, $sort, $publishPages); - $sort++; - } - - $subdoc = new DOMDocument(); - $subnode = $subdoc->createElement('body'); - $subtitle = trim(preg_replace('/\n|\r/', '', Convert::html2raw($node->textContent))); - } else { - $subnode->appendChild($subdoc->importNode($node, true)); - } - - $node = $node->nextSibling; - } - } else { - $this->writeContent($subtitle, $subdoc, $body, null, $publishPages); - } - - if($subnode->hasChildNodes() && !$documentImporterFieldError) { - $this->writeContent($subtitle, $subdoc, $subnode, null, $publishPages); - } - - restore_error_handler(); - if ($documentImporterFieldError) { - return ['error' => $documentImporterFieldError]; - } - - foreach($this->unusedChildren as $child) { - $origStage = Versioned::current_stage(); - - Versioned::reading_stage('Stage'); - $clone = clone $child; - $clone->delete(); - - Versioned::reading_stage('Live'); - $clone = clone $child; - $clone->delete(); - - Versioned::reading_stage($origStage); - } - - $sourcePage->write(); - } +class ImportField extends UploadField +{ + + private static $allowed_actions = ['upload']; + + private static $importer_class = ServiceConnector::class; + + /** + * Process the document immediately upon upload. + */ + public function upload(HTTPRequest $request) + { + if ($this->isDisabled() || $this->isReadonly()) { + return $this->httpError(403); + } + + // Protect against CSRF on destructive action + $token = $this->getForm()->getSecurityToken(); + if (!$token->checkRequest($request)) { + return $this->httpError(400); + } + + $tmpfile = $request->postVar('Upload'); + + // Check if the file has been uploaded into the temporary storage. + if (!$tmpfile) { + $return = [ + 'error' => _t( + 'SilverStripe\\AssetAdmin\\Forms\\UploadField.FIELDNOTSET', + 'File information not found' + ) + ]; + } else { + $return = [ + 'name' => $tmpfile['name'], + 'size' => $tmpfile['size'], + 'type' => $tmpfile['type'], + 'error' => $tmpfile['error'] + ]; + } + + if (!$return['error']) { + // Get options for this import. + $splitHeader = (int)$request->postVar('SplitHeader'); + $keepSource = (bool)$request->postVar('KeepSource'); + $chosenFolderID = (int)$request->postVar('ChosenFolderID'); + $publishPages = (bool)$request->postVar('PublishPages'); + $includeTOC = (bool)$request->postVar('IncludeTOC'); + + // Process the document and write the page. + $preservedDocument = null; + if ($keepSource) { + $preservedDocument = $this->preserveSourceDocument($tmpfile, $chosenFolderID); + } + + $importResult = $this->importFromPOST($tmpfile, $splitHeader, $publishPages, $chosenFolderID); + if (is_array($importResult) && isset($importResult['error'])) { + $return['error'] = $importResult['error']; + } elseif ($includeTOC) { + $this->writeTOC($publishPages, $keepSource ? $preservedDocument : null); + } + } + + $response = HTTPResponse::create(Convert::raw2json([$return])); + $response->addHeader('Content-Type', 'text/plain'); + return $response; + } + + /** + * Preserves the source file by copying it to a specified folder. + * + * @param $tmpfile Temporary file data structure. + * @param int $chosenFolderID Target folder. + * @return File Stored file. + */ + protected function preserveSourceDocument($tmpfile, $chosenFolderID = null) + { + $upload = Upload::create(); + + $file = File::create(); + $upload->loadIntoFile($tmpfile, $file, $chosenFolderID); + + $page = $this->form->getRecord(); + $page->ImportedFromFileID = $file->ID; + $page->write(); + + return $file; + } + + /** + * Builds and writes the table of contents for the document. + * + * @param bool $publishPage Should the parent page be published. + * @param File $preservedDocument Set if the link to the original document should be added. + */ + protected function writeTOC($publishPages = false, $preservedDocument = null) + { + $page = $this->form->getRecord(); + $content = ''; + } else { + $doc = new DOMDocument(); + $doc->loadHTML($page->Content); + $body = $doc->getElementsByTagName('body')->item(0); + $node = $body->firstChild; + $h1 = $h2 = 1; + while ($node) { + if ($node instanceof DOMElement && $node->tagName == 'h1') { + $content .= '
  • ' . + trim(preg_replace('/\n|\r/', '', Convert::html2raw($node->textContent))) . + '
  • '; + $node->setAttributeNode(new DOMAttr("id", "h1.".$h1)); + $h1++; + } elseif ($node instanceof DOMElement && $node->tagName == 'h2') { + $content .= ''; + $node->setAttributeNode(new DOMAttr("id", "h2.".$h2)); + $h2++; + } + $node = $node->nextSibling; + } + $page->Content = $content . '' . $doc->saveHTML(); + } + + // Add in the link to the original document, if provided. + if ($preservedDocument) { + $page->Content = 'download original document (' . + $preservedDocument->getSize() . + ')' . + $page->Content; + } + + // Store the result + $page->write(); + if ($publishPages) { + $page->doPublish(); + } + } + } + + protected function getBodyText($doc, $node) + { + // Build a new doc + $htmldoc = new DOMDocument(); + // Create the html element + $html = $htmldoc->createElement('html'); + $htmldoc->appendChild($html); + // Append the body node + $html->appendChild($htmldoc->importNode($node, true)); + + // Get the text as html, remove the entry and exit root tags and return + $text = $htmldoc->saveHTML(); + $text = preg_replace('/^.*/', '', $text); + $text = preg_replace('/<\/body>.*$/', '', $text); + + return $text; + } + + /** + * Used only when writing the document that has been split by headers. + * Can write both to the chapter pages as well as the master page. + * + * @param string $subtitle Title of the chapter - if missing, it will write to the master page. + * @param $subdoc + * @param $subnode + * @param int $sort Order of the chapter page. + * @param $publishPages Whether to publish the resulting child/master pages. + */ + protected function writeContent($subtitle, $subdoc, $subnode, $sort = null, $publishPages = false) + { + $record = $this->form->getRecord(); + + if ($subtitle) { + // Write the chapter page to a subpage. + $page = DataObject::get_one( + 'Page', + sprintf('"Title" = \'%s\' AND "ParentID" = %d', $subtitle, $record->ID) + ); + if (!$page) { + $page = Page::create(); + $page->ParentID = $record->ID; + $page->Title = $subtitle; + } + + unset($this->unusedChildren[$page->ID]); + file_put_contents(ASSETS_PATH . '/index-' . $sort . '.html', $this->getBodyText($subdoc, $subnode)); + + if ($sort) { + $page->Sort = $sort; + } + $page->Content = $this->getBodyText($subdoc, $subnode); + $page->write(); + if ($publishPages) { + $page->doPublish(); + } + } else { + // Write to the master page. + $record->Content = $this->getBodyText($subdoc, $subnode); + $record->write(); + + if ($publishPages) { + $record->doPublish(); + } + } + } + + /** + * Imports a document at a certain path onto the current page and writes it. + * CAUTION: Overwrites any existing content on the page! + * + * @param array $tmpFile Array as received from PHP's POST upload. + * @param bool $splitHeader Heading level to split by. + * @param bool $publishPages Whether the underlying pages should be published after import. + * @param int $chosenFolderID ID of the working folder - here the converted file and images will be stored. + */ + public function importFromPOST($tmpFile, $splitHeader = false, $publishPages = false, $chosenFolderID = null) + { + + $fileDescriptor = [ + 'name' => $tmpFile['name'], + 'path' => $tmpFile['tmp_name'], + 'mimeType' => $tmpFile['type'] + ]; + + $sourcePage = $this->form->getRecord(); + $importerClass = $this->config()->get('importer_class'); + $importer = Injector::inst()->create($importerClass, $fileDescriptor, $chosenFolderID); + $content = $importer->import(); + + if (is_array($content) && isset($content['error'])) { + return $content; + } + + // Clean up with tidy (requires tidy module) + $tidy = new Tidy(); + $tidy->parseString($content, ['output-xhtml' => true], 'utf8'); + $tidy->cleanRepair(); + + $fragment = []; + foreach ($tidy->body()->child as $child) { + $fragment[] = $child->value; + } + + $htmlValue = Injector::inst()->create('HTMLValue', implode("\n", $fragment)); + + // Sanitise + $santiser = Injector::inst()->create(HTMLEditorSanitiser::class, HTMLEditorConfig::get_active()); + $santiser->sanitise($htmlValue); + + // Load in the HTML + $doc = $htmlValue->getDocument(); + $xpath = new DOMXPath($doc); + + // make sure any images are added as Image records with a relative link to assets + $chosenFolder = ($this->chosenFolderID) ? DataObject::get_by_id(Folder::class, $this->chosenFolderID) : null; + $folderName = ($chosenFolder) ? '/' . $chosenFolder->Name : ''; + $imgs = $xpath->query('//img'); + for ($i = 0; $i < $imgs->length; $i++) { + $img = $imgs->item($i); + $originalPath = 'assets/' . $folderName . '/' . $img->getAttribute('src'); + $name = FileNameFilter::create()->filter(basename($originalPath)); + + $image = Image::get()->filter([ + 'Name' => $name, + 'ParentID' => (int)$chosenFolderID + ])->first(); + if (!($image && $image->exists())) { + $image = Image::create(); + $image->ParentID = (int)$chosenFolderID; + $image->Name = $name; + $image->write(); + } + + // make sure it's put in place correctly so Image record knows where it is. + // e.g. in the case of underscores being renamed to dashes. + @rename(Director::getAbsFile($originalPath), Director::getAbsFile($image->getFilename())); + + $img->setAttribute('src', $image->getFilename()); + } + + $remove_rules = [ + // Change any headers that contain font tags (other than font face tags) into p elements + '//h1[.//font[not(@face)]]' => 'p', + // Remove any font tags + '//font' + ]; + + foreach ($remove_rules as $rule => $parenttag) { + if (is_numeric($rule)) { + $rule = $parenttag; + $parenttag = null; + } + + $nodes = []; + foreach ($xpath->query($rule) as $node) { + $nodes[] = $node; + } + + foreach ($nodes as $node) { + $parent = $node->parentNode; + + if ($parenttag) { + $parent = $doc->createElement($parenttag); + $node->nextSibling ? + $node->parentNode->insertBefore($parent, $node->nextSibling) : + $node->parentNode->appendChild($parent); + } + + while ($node->firstChild) { + $parent->appendChild($node->firstChild); + } + $node->parentNode->removeChild($node); + } + } + + // Strip style, class, lang attributes. + $els = $doc->getElementsByTagName('*'); + for ($i = 0; $i < $els->length; $i++) { + $el = $els->item($i); + $el->removeAttribute('class'); + $el->removeAttribute('style'); + $el->removeAttribute('lang'); + } + + $els = $doc->getElementsByTagName('*'); + + $headingXPath = [ + 'self::h1', + 'self::h2', + 'self::h3', + 'self::h4', + 'self::h5', + 'self::h6', + ]; + // Remove a bunch of unwanted elements + $clean = [ + // Empty paragraphs + '//p[not(descendant-or-self::text() | descendant-or-self::img)]', + // Empty headers + '//*[' . implode(' | ', $headingXPath) . '][not(descendant-or-self::text() | descendant-or-self::img)]', + // Anchors + '//a[not(@href)]', + // BR tags + '//br' + ]; + + foreach ($clean as $query) { + // First get all the nodes. Need to build array, as they'll disappear from the + // nodelist while we're deleteing them, causing the indexing to screw up. + $nodes = []; + foreach ($xpath->query($query) as $node) { + $nodes[] = $node; + } + + // Then remove them all + foreach ($nodes as $node) { + if ($node->parentNode) { + $node->parentNode->removeChild($node); + } + } + } + + // Now split the document into portions by H1 + $body = $doc->getElementsByTagName('body')->item(0); + + $this->unusedChildren = []; + foreach ($sourcePage->Children() as $child) { + $this->unusedChildren[$child->ID] = $child; + } + + $documentImporterFieldError = false; + + $documentImporterFieldErrorHandler = function ( + $errno, + $errstr, + $errfile, + $errline + ) use ($documentImporterFieldError) { + $documentImporterFieldError = _t( + 'SilverStripe\\DocumentConverter\\ServiceConnector.PROCESSFAILED', + 'Could not process document, please double-check you uploaded a .doc or .docx format.', + 'Document Converter processes Word documents into HTML.' + ); + + // Do not cascade the error through other handlers + return true; + }; + + set_error_handler($documentImporterFieldErrorHandler); + + $subtitle = null; + $subdoc = new DOMDocument(); + $subnode = $subdoc->createElement('body'); + $node = $body->firstChild; + $sort = 1; + if ($splitHeader == 1 || $splitHeader == 2) { + while ($node && !$documentImporterFieldError) { + if ($node instanceof DOMElement && $node->tagName == 'h' . $splitHeader) { + if ($subnode->hasChildNodes()) { + $this->writeContent($subtitle, $subdoc, $subnode, $sort, $publishPages); + $sort++; + } + + $subdoc = new DOMDocument(); + $subnode = $subdoc->createElement('body'); + $subtitle = trim(preg_replace('/\n|\r/', '', Convert::html2raw($node->textContent))); + } else { + $subnode->appendChild($subdoc->importNode($node, true)); + } + + $node = $node->nextSibling; + } + } else { + $this->writeContent($subtitle, $subdoc, $body, null, $publishPages); + } + + if ($subnode->hasChildNodes() && !$documentImporterFieldError) { + $this->writeContent($subtitle, $subdoc, $subnode, null, $publishPages); + } + + restore_error_handler(); + if ($documentImporterFieldError) { + return ['error' => $documentImporterFieldError]; + } + + foreach ($this->unusedChildren as $child) { + $origStage = Versioned::current_stage(); + + Versioned::reading_stage('Stage'); + $clone = clone $child; + $clone->delete(); + + Versioned::reading_stage('Live'); + $clone = clone $child; + $clone->delete(); + + Versioned::reading_stage($origStage); + } + + $sourcePage->write(); + } } diff --git a/src/PageExtension.php b/src/PageExtension.php index 0a7b5e3..7ac3c25 100644 --- a/src/PageExtension.php +++ b/src/PageExtension.php @@ -6,14 +6,16 @@ use SilverStripe\Forms\FieldList; use SilverStripe\ORM\DataExtension; -class PageExtension extends DataExtension { +class PageExtension extends DataExtension +{ - private static $has_one = [ - 'ImportedFromFile' => File::class - ]; + private static $has_one = [ + 'ImportedFromFile' => File::class + ]; - function updateCMSFields(FieldList $fields) { - /* + public function updateCMSFields(FieldList $fields) + { + /* // Currently the ToggleCompositeField plays badly with TreeDropdownField formatting. // Could be switched back in the future, if this is fixed. $fields->addFieldToTab('Root.Main', @@ -22,10 +24,10 @@ function updateCMSFields(FieldList $fields) { ])->setHeadingLevel(4) ); */ - $fields->findOrMakeTab( - 'Root.Import', - _t(__CLASS__ . '.ImportTab', 'Import') - ); - $fields->addFieldToTab('Root.Import', SettingsField::create()); - } + $fields->findOrMakeTab( + 'Root.Import', + _t(__CLASS__ . '.ImportTab', 'Import') + ); + $fields->addFieldToTab('Root.Import', SettingsField::create()); + } } diff --git a/src/ServiceConnector.php b/src/ServiceConnector.php index a62b099..e1aa102 100644 --- a/src/ServiceConnector.php +++ b/src/ServiceConnector.php @@ -13,186 +13,195 @@ /** * Utility class hiding the specifics of the document conversion process. */ -class ServiceConnector { - - use Configurable; - use Injectable; - - /** - * @config - * @var array Docvert connection username - */ - private static $username = null; - - /** - * @config - * @var array Docvert connection password - */ - private static $password = null; - - /** - * @config - * @var array Docvert service URL - */ - private static $url = null; - - /** - * Associative array of: - * - name: the full name of the file including the extension. - * - path: the path to the file on the local filesystem. - * - mimeType - */ - protected $fileDescriptor; - - /** - * @var int - * ID of a SilverStripe\Assets\Folder - */ - protected $chosenFolderID; - - /** - * @var array instance specific connection details - */ - protected $docvertDetails = [ - 'username' => null, - 'password' => null, - 'url' => null - ]; - - public function __construct($fileDescriptor, $chosenFolderID = null) { - $this->fileDescriptor = $fileDescriptor; - $this->chosenFolderID = $chosenFolderID; - } - - public function setUsername($username = null) { - $this->docvertDetails['username'] = $username; - } - - public function getUsername() { - $username = $this->docvertDetails['username']; - if ($username) { - return $username; - } - $username = $this->config()->get('username'); - if ($username) { - return $username; - } - $username = Environment::getEnv('DOCVERT_USERNAME'); - if ($username) { - return $username; - } - return null; - } - - public function setPassword($password = null) { - $this->docvertDetails['password'] = $password; - } - - public function getPassword() { - $username = $this->docvertDetails['password']; - if ($username) { - return $username; - } - $username = $this->config()->get('password'); - if ($username) { - return $username; - } - $username = Environment::getEnv('DOCVERT_PASSWORD'); - if ($username) { - return $username; - } - return null; - } - - public function setUrl($url = null) { - $this->docvertDetails['url'] = $url; - } - - public function getUrl() { - $username = $this->docvertDetails['url']; - if ($username) { - return $username; - } - $username = $this->config()->get('url'); - if ($username) { - return $username; - } - $username = Environment::getEnv('DOCVERT_URL'); - if ($username) { - return $username; - } - return null; - } - - public function import() { - $ch = curl_init(); - - // PHP 5.5+ introduced CURLFile which makes the '@/path/to/file' syntax deprecated. - if(class_exists('CURLFile')) { - $file = new CURLFile( - $this->fileDescriptor['path'], - $this->fileDescriptor['mimeType'], - $this->fileDescriptor['name'] - ); - } else { - $file = '@' . $this->fileDescriptor['path']; - } - - curl_setopt_array($ch, [ - CURLOPT_URL => $this->getUrl(), - CURLOPT_USERPWD => sprintf('%s:%s', $this->getUsername(), $this->getPassword()), - CURLOPT_POST => 1, - CURLOPT_POSTFIELDS => ['file' => $file], - CURLOPT_CONNECTTIMEOUT => 25, - CURLOPT_TIMEOUT => 100, - ]); - - $chosenFolder = ($this->chosenFolderID) ? DataObject::get_by_id(Folder::class, $this->chosenFolderID) : null; - $folderName = ($chosenFolder) ? '/' . $chosenFolder->Name : ''; - $outname = tempnam(ASSETS_PATH, 'convert'); - $outzip = $outname . '.zip'; - $out = fopen($outzip, 'w'); - curl_setopt($ch, CURLOPT_FILE, $out); - $returnValue = curl_exec($ch); - $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - fclose($out); - chmod($outzip, 0666); - - if (!$returnValue || ($status != 200)) { - return ['error' => _t( - __CLASS__ . '.SERVERUNREACHABLE', - 'Could not contact document conversion server. Please try again later or contact your system administrator.', - 'Document Converter process Word documents into HTML.' - )]; - } - - // extract the converted document into assets - // you need php zip, e.g. apt-get install php-zip - $zip = new ZipArchive(); - - if($zip->open($outzip)) { - $zip->extractTo(ASSETS_PATH .$folderName); - $zip->close(); - } - - // remove temporary files - unlink($outname); - unlink($outzip); - - if (!file_exists(ASSETS_PATH . $folderName . '/index.html')) { - return ['error' => _t( - __CLASS__ . '.PROCESSFAILED', - 'Could not process document, please double-check you uploaded a .doc or .docx format.', - 'Document Converter processes Word documents into HTML.' - )]; - } - - $content = file_get_contents(ASSETS_PATH . $folderName . '/index.html'); - - unlink(ASSETS_PATH . $folderName . '/index.html'); - - return $content; - } - +class ServiceConnector +{ + + use Configurable; + use Injectable; + + /** + * @config + * @var array Docvert connection username + */ + private static $username = null; + + /** + * @config + * @var array Docvert connection password + */ + private static $password = null; + + /** + * @config + * @var array Docvert service URL + */ + private static $url = null; + + /** + * Associative array of: + * - name: the full name of the file including the extension. + * - path: the path to the file on the local filesystem. + * - mimeType + */ + protected $fileDescriptor; + + /** + * @var int + * ID of a SilverStripe\Assets\Folder + */ + protected $chosenFolderID; + + /** + * @var array instance specific connection details + */ + protected $docvertDetails = [ + 'username' => null, + 'password' => null, + 'url' => null + ]; + + public function __construct($fileDescriptor, $chosenFolderID = null) + { + $this->fileDescriptor = $fileDescriptor; + $this->chosenFolderID = $chosenFolderID; + } + + public function setUsername($username = null) + { + $this->docvertDetails['username'] = $username; + } + + public function getUsername() + { + $username = $this->docvertDetails['username']; + if ($username) { + return $username; + } + $username = $this->config()->get('username'); + if ($username) { + return $username; + } + $username = Environment::getEnv('DOCVERT_USERNAME'); + if ($username) { + return $username; + } + return null; + } + + public function setPassword($password = null) + { + $this->docvertDetails['password'] = $password; + } + + public function getPassword() + { + $username = $this->docvertDetails['password']; + if ($username) { + return $username; + } + $username = $this->config()->get('password'); + if ($username) { + return $username; + } + $username = Environment::getEnv('DOCVERT_PASSWORD'); + if ($username) { + return $username; + } + return null; + } + + public function setUrl($url = null) + { + $this->docvertDetails['url'] = $url; + } + + public function getUrl() + { + $username = $this->docvertDetails['url']; + if ($username) { + return $username; + } + $username = $this->config()->get('url'); + if ($username) { + return $username; + } + $username = Environment::getEnv('DOCVERT_URL'); + if ($username) { + return $username; + } + return null; + } + + public function import() + { + $ch = curl_init(); + + // PHP 5.5+ introduced CURLFile which makes the '@/path/to/file' syntax deprecated. + if (class_exists('CURLFile')) { + $file = new CURLFile( + $this->fileDescriptor['path'], + $this->fileDescriptor['mimeType'], + $this->fileDescriptor['name'] + ); + } else { + $file = '@' . $this->fileDescriptor['path']; + } + + curl_setopt_array($ch, [ + CURLOPT_URL => $this->getUrl(), + CURLOPT_USERPWD => sprintf('%s:%s', $this->getUsername(), $this->getPassword()), + CURLOPT_POST => 1, + CURLOPT_POSTFIELDS => ['file' => $file], + CURLOPT_CONNECTTIMEOUT => 25, + CURLOPT_TIMEOUT => 100, + ]); + + $chosenFolder = ($this->chosenFolderID) ? DataObject::get_by_id(Folder::class, $this->chosenFolderID) : null; + $folderName = ($chosenFolder) ? '/' . $chosenFolder->Name : ''; + $outname = tempnam(ASSETS_PATH, 'convert'); + $outzip = $outname . '.zip'; + $out = fopen($outzip, 'w'); + curl_setopt($ch, CURLOPT_FILE, $out); + $returnValue = curl_exec($ch); + $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + fclose($out); + chmod($outzip, 0666); + + if (!$returnValue || ($status != 200)) { + return ['error' => _t( + __CLASS__ . '.SERVERUNREACHABLE', + 'Could not contact document conversion server. Please try again later ' . + 'or contact your system administrator.', + 'Document Converter process Word documents into HTML.' + )]; + } + + // extract the converted document into assets + // you need php zip, e.g. apt-get install php-zip + $zip = new ZipArchive(); + + if ($zip->open($outzip)) { + $zip->extractTo(ASSETS_PATH .$folderName); + $zip->close(); + } + + // remove temporary files + unlink($outname); + unlink($outzip); + + if (!file_exists(ASSETS_PATH . $folderName . '/index.html')) { + return ['error' => _t( + __CLASS__ . '.PROCESSFAILED', + 'Could not process document, please double-check you uploaded a .doc or .docx format.', + 'Document Converter processes Word documents into HTML.' + )]; + } + + $content = file_get_contents(ASSETS_PATH . $folderName . '/index.html'); + + unlink(ASSETS_PATH . $folderName . '/index.html'); + + return $content; + } } diff --git a/src/SettingsField.php b/src/SettingsField.php index d514a2f..c60bf0c 100644 --- a/src/SettingsField.php +++ b/src/SettingsField.php @@ -17,85 +17,98 @@ * Includes several options fields, which are bundled together with an UploadField * into a CompositeField. */ -class SettingsField extends CompositeField { - /** - * Reference to the inner upload field (ImportField). - */ - private $innerField = null; +class SettingsField extends CompositeField +{ + /** + * Reference to the inner upload field (ImportField). + */ + private $innerField = null; - /** - * Augments a simple CompositeField with uploader and import options. - * - * @param $children FieldSet/array Any additional children. - */ - public function __construct($children = null) { - if (is_string($children)) throw new InvalidArgumentException('DocumentConversionField::__construct does not accept a name as its parameter, it defaults to "ImportedFromFile" instead. Use DocumentConversionField::getInnerField()->setName() if you want to change it.'); - if ($children) throw new InvalidArgumentException('DocumentConversionField::__construct provides its own fields and does not accept additional children.'); + /** + * Augments a simple CompositeField with uploader and import options. + * + * @param $children FieldSet/array Any additional children. + */ + public function __construct($children = null) + { + if (is_string($children)) { + throw new InvalidArgumentException( + 'DocumentConversionField::__construct does not accept a name as its parameter,' . + ' it defaults to "ImportedFromFile" instead. Use DocumentConversionField::getInnerField()->setName()' . + ' if you want to change it.' + ); + } + if ($children) { + throw new InvalidArgumentException( + 'DocumentConversionField::__construct provides its own fields and does not accept additional children.' + ); + } - // Add JS specific to this field. - Requirements::javascript('silverstripe/documentconverter: javascript/DocumentConversionField.js'); + // Add JS specific to this field. + Requirements::javascript('silverstripe/documentconverter: javascript/DocumentConversionField.js'); - $fields = FieldList::create([ - HeaderField::create( - 'FileWarningHeader', - _t( - __CLASS__ . '.FileWarningHeader', - 'Warning: import will remove all content and subpages of this page.' - ), - 4 - ), - $splitHeader = DropdownField::create( - 'DocumentConversionField-SplitHeader', - _t( - __CLASS__ . '.SplitHeader', - 'Split document into pages' - ), - [ - 0 => _t(__CLASS__ . '.No','no'), - 1 => _t(__CLASS__ . '.EachH1','for each heading 1'), - 2 => _t(__CLASS__ . '.EachH2','for each heading 2') - ] - ), - $keepSource = CheckboxField::create( - 'DocumentConversionField-KeepSource', - _t( - __CLASS__ . '.KeepSource', - 'Keep the original document. Adds a link to it on TOC, if enabled.' - ) - ), - $chosenFolderID = TreeDropdownField::create( - 'DocumentConversionField-ChosenFolderID', - _t(__CLASS__ . '.ChooseFolder', 'Choose a folder to save this file'), - Folder::class - ), - $includeTOC = CheckboxField::create( - 'DocumentConversionField-IncludeTOC', - _t(__CLASS__ . '.IncludeTOC', 'Replace this page with a Table of Contents.') - ), - $publishPages = CheckboxField::create( - 'DocumentConversionField-PublishPages', - _t( - __CLASS__ . '.publishPages', - 'Publish modified pages (not recommended unless you are sure about the conversion outcome)' - ) - ), - $this->innerField = ImportField::create( - 'ImportedFromFile', - _t(__CLASS__ . '.ImportedFromFile','Import content from a word document') - ) - ]); + $fields = FieldList::create([ + HeaderField::create( + 'FileWarningHeader', + _t( + __CLASS__ . '.FileWarningHeader', + 'Warning: import will remove all content and subpages of this page.' + ), + 4 + ), + $splitHeader = DropdownField::create( + 'DocumentConversionField-SplitHeader', + _t( + __CLASS__ . '.SplitHeader', + 'Split document into pages' + ), + [ + 0 => _t(__CLASS__ . '.No', 'no'), + 1 => _t(__CLASS__ . '.EachH1', 'for each heading 1'), + 2 => _t(__CLASS__ . '.EachH2', 'for each heading 2') + ] + ), + $keepSource = CheckboxField::create( + 'DocumentConversionField-KeepSource', + _t( + __CLASS__ . '.KeepSource', + 'Keep the original document. Adds a link to it on TOC, if enabled.' + ) + ), + $chosenFolderID = TreeDropdownField::create( + 'DocumentConversionField-ChosenFolderID', + _t(__CLASS__ . '.ChooseFolder', 'Choose a folder to save this file'), + Folder::class + ), + $includeTOC = CheckboxField::create( + 'DocumentConversionField-IncludeTOC', + _t(__CLASS__ . '.IncludeTOC', 'Replace this page with a Table of Contents.') + ), + $publishPages = CheckboxField::create( + 'DocumentConversionField-PublishPages', + _t( + __CLASS__ . '.publishPages', + 'Publish modified pages (not recommended unless you are sure about the conversion outcome)' + ) + ), + $this->innerField = ImportField::create( + 'ImportedFromFile', + _t(__CLASS__ . '.ImportedFromFile', 'Import content from a word document') + ) + ]); - // Prevent the warning popup that appears when navigating away from the page. - $splitHeader->addExtraClass('no-change-track'); - $keepSource->addExtraClass('no-change-track'); - $chosenFolderID->addExtraClass('no-change-track'); - $includeTOC->addExtraClass('no-change-track'); - $publishPages->addExtraClass('no-change-track'); + // Prevent the warning popup that appears when navigating away from the page. + $splitHeader->addExtraClass('no-change-track'); + $keepSource->addExtraClass('no-change-track'); + $chosenFolderID->addExtraClass('no-change-track'); + $includeTOC->addExtraClass('no-change-track'); + $publishPages->addExtraClass('no-change-track'); - return parent::__construct($fields); - } + return parent::__construct($fields); + } - public function getInnerField() { - return $this->innerField; - } + public function getInnerField() + { + return $this->innerField; + } } diff --git a/tests/DocumentConverterDecoratorTest.php b/tests/DocumentConverterDecoratorTest.php index e634a84..2885e64 100644 --- a/tests/DocumentConverterDecoratorTest.php +++ b/tests/DocumentConverterDecoratorTest.php @@ -1,5 +1,9 @@ markTestIncomplete(); } -} \ No newline at end of file +} diff --git a/tests/DocumentImportFieldTest.php b/tests/DocumentImportFieldTest.php index 0564a4d..915f964 100644 --- a/tests/DocumentImportFieldTest.php +++ b/tests/DocumentImportFieldTest.php @@ -1,5 +1,9 @@ Date: Thu, 21 Dec 2017 12:13:51 +1300 Subject: [PATCH 3/6] FIX: Shift from non-fuctioning to functioning ... somehow. Assuming the HTMLValue change from a string referenced identifier for Injector to deal with to the acutal fully qualified class name is what has done it. --- src/ImportField.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/ImportField.php b/src/ImportField.php index 306c403..2620750 100644 --- a/src/ImportField.php +++ b/src/ImportField.php @@ -23,6 +23,7 @@ use SilverStripe\Forms\HTMLEditor\HTMLEditorSanitiser; use SilverStripe\ORM\DataObject; use SilverStripe\Versioned\Versioned; +use SilverStripe\View\Parsers\HTMLValue; use Tidy; /** @@ -114,7 +115,7 @@ public function upload(HTTPRequest $request) } $response = HTTPResponse::create(Convert::raw2json([$return])); - $response->addHeader('Content-Type', 'text/plain'); + $response->addHeader('Content-Type', 'application/json'); return $response; } @@ -302,7 +303,7 @@ public function importFromPOST($tmpFile, $splitHeader = false, $publishPages = f $fragment[] = $child->value; } - $htmlValue = Injector::inst()->create('HTMLValue', implode("\n", $fragment)); + $htmlValue = Injector::inst()->create(HTMLValue::class, implode("\n", $fragment)); // Sanitise $santiser = Injector::inst()->create(HTMLEditorSanitiser::class, HTMLEditorConfig::get_active()); @@ -488,12 +489,12 @@ public function importFromPOST($tmpFile, $splitHeader = false, $publishPages = f $origStage = Versioned::current_stage(); Versioned::reading_stage('Stage'); - $clone = clone $child; - $clone->delete(); + $draft = clone $child; + $draft->delete(); Versioned::reading_stage('Live'); - $clone = clone $child; - $clone->delete(); + $published = clone $child; + $published->delete(); Versioned::reading_stage($origStage); } From d0c4ae1ea1eeb22bc255668053e1fdd673a16b69 Mon Sep 17 00:00:00 2001 From: Dylan Wagstaff Date: Fri, 22 Dec 2017 12:19:51 +1300 Subject: [PATCH 4/6] FIX: update tests to new code names so that they can still pass --- src/SettingsField.php | 20 +++---- tests/DocumentConverterDecoratorTest.php | 21 ------- tests/DocumentConverterTest.php | 15 ----- tests/DocumentImportFieldTest.php | 55 ------------------ tests/PageExtensionTest.php | 25 ++++++++ tests/SettingsFieldTest.php | 74 ++++++++++++++++++++++++ tests/Stubs/MockDocvertService.php | 14 +++++ tests/Stubs/TestPage.php | 15 +++++ 8 files changed, 135 insertions(+), 104 deletions(-) delete mode 100644 tests/DocumentConverterDecoratorTest.php delete mode 100644 tests/DocumentConverterTest.php delete mode 100644 tests/DocumentImportFieldTest.php create mode 100644 tests/PageExtensionTest.php create mode 100644 tests/SettingsFieldTest.php create mode 100644 tests/Stubs/MockDocvertService.php create mode 100644 tests/Stubs/TestPage.php diff --git a/src/SettingsField.php b/src/SettingsField.php index c60bf0c..9405798 100644 --- a/src/SettingsField.php +++ b/src/SettingsField.php @@ -31,16 +31,10 @@ class SettingsField extends CompositeField */ public function __construct($children = null) { - if (is_string($children)) { - throw new InvalidArgumentException( - 'DocumentConversionField::__construct does not accept a name as its parameter,' . - ' it defaults to "ImportedFromFile" instead. Use DocumentConversionField::getInnerField()->setName()' . - ' if you want to change it.' - ); - } if ($children) { + $class = get_class(); throw new InvalidArgumentException( - 'DocumentConversionField::__construct provides its own fields and does not accept additional children.' + "${class}::__construct does not accept extra parameters." ); } @@ -57,7 +51,7 @@ public function __construct($children = null) 4 ), $splitHeader = DropdownField::create( - 'DocumentConversionField-SplitHeader', + 'DocumentConversionSettings-SplitHeader', _t( __CLASS__ . '.SplitHeader', 'Split document into pages' @@ -69,23 +63,23 @@ public function __construct($children = null) ] ), $keepSource = CheckboxField::create( - 'DocumentConversionField-KeepSource', + 'DocumentConversionSettings-KeepSource', _t( __CLASS__ . '.KeepSource', 'Keep the original document. Adds a link to it on TOC, if enabled.' ) ), $chosenFolderID = TreeDropdownField::create( - 'DocumentConversionField-ChosenFolderID', + 'DocumentConversionSettings-ChosenFolderID', _t(__CLASS__ . '.ChooseFolder', 'Choose a folder to save this file'), Folder::class ), $includeTOC = CheckboxField::create( - 'DocumentConversionField-IncludeTOC', + 'DocumentConversionSettings-IncludeTOC', _t(__CLASS__ . '.IncludeTOC', 'Replace this page with a Table of Contents.') ), $publishPages = CheckboxField::create( - 'DocumentConversionField-PublishPages', + 'DocumentConversionSettings-PublishPages', _t( __CLASS__ . '.publishPages', 'Publish modified pages (not recommended unless you are sure about the conversion outcome)' diff --git a/tests/DocumentConverterDecoratorTest.php b/tests/DocumentConverterDecoratorTest.php deleted file mode 100644 index 2885e64..0000000 --- a/tests/DocumentConverterDecoratorTest.php +++ /dev/null @@ -1,21 +0,0 @@ - ['DocumentConverterDecorator'] - ]; - - public function testFieldListHasDocumentImportField() - { - $fields = (new SiteTree)->getCMSFields(); - $this->assertInstanceOf( - 'DocumentImportField', - $fields->fieldByName('Root.Import')->Fields()->First() - ); - } -} diff --git a/tests/DocumentConverterTest.php b/tests/DocumentConverterTest.php deleted file mode 100644 index 160fedb..0000000 --- a/tests/DocumentConverterTest.php +++ /dev/null @@ -1,15 +0,0 @@ -markTestIncomplete(); - } -} diff --git a/tests/DocumentImportFieldTest.php b/tests/DocumentImportFieldTest.php deleted file mode 100644 index 915f964..0000000 --- a/tests/DocumentImportFieldTest.php +++ /dev/null @@ -1,55 +0,0 @@ -clear(); - - new DocumentImportField(); - $javascript = Requirements::backend()->get_javascript(); - $this->assertNotEmpty($javascript); - } - - public function testFieldListGeneration() - { - $importField = new DocumentImportField(); - - $fields = $importField->getChildren(); - $this->assertInstanceOf('FieldList', $fields); - - // We don't need to check that all of the fields are there, but just check a couple - $this->assertInstanceOf('HeaderField', $fields->fieldByName('FileWarningHeader')); - $innerField = $fields->fieldByName('ImportedFromFile'); - $this->assertInstanceOf('DocumentImportInnerField', $innerField); - - // Check the getter works - $this->assertSame($innerField, $importField->getInnerField()); - - // Check the fields have been given has the change tracker disabled - $splitHeader = $fields->fieldByName('DocumentImportField-SplitHeader'); - $this->assertInstanceOf('DropdownField', $splitHeader); - $this->assertContains('no-change-track', $splitHeader->extraClass()); - } -} diff --git a/tests/PageExtensionTest.php b/tests/PageExtensionTest.php new file mode 100644 index 0000000..bac5c60 --- /dev/null +++ b/tests/PageExtensionTest.php @@ -0,0 +1,25 @@ + [PageExtension::class] + ]; + + public function testFieldListHasDocumentImportField() + { + $siteTree = new SiteTree; + $fields = $siteTree->getCMSFields(); + $this->assertInstanceOf( + SettingsField::class, + $fields->fieldByName('Root.Import')->Fields()->First() + ); + } +} diff --git a/tests/SettingsFieldTest.php b/tests/SettingsFieldTest.php new file mode 100644 index 0000000..0decfee --- /dev/null +++ b/tests/SettingsFieldTest.php @@ -0,0 +1,74 @@ +clear(); + + new SettingsField(); + $javascript = Requirements::backend()->getJavascript(); + $this->assertNotEmpty($javascript); + } + + public function testFieldListGeneration() + { + $importField = new SettingsField(); + + $fields = $importField->getChildren(); + $this->assertInstanceOf(FieldList::class, $fields); + + // We don't need to check that all of the fields are there, but just check a couple + $this->assertInstanceOf(HeaderField::class, $fields->fieldByName('FileWarningHeader')); + $innerField = $fields->fieldByName('ImportedFromFile'); + $this->assertInstanceOf(ImportField::class, $innerField); + + // Check the getter works + $this->assertSame($innerField, $importField->getInnerField()); + + // Check the fields have been given has the change tracker disabled + $settingsFields = [ + 'SplitHeader' => DropdownField::class, + 'KeepSource' => CheckboxField::class, + 'ChosenFolderID' => TreeDropdownField::class, + 'IncludeTOC' => CheckboxField::class, + 'PublishPages' => CheckboxField::class + ]; + foreach($settingsFields as $fieldName => $className) { + $field = $fields->fieldByName( + 'DocumentConversionSettings-' . $fieldName + ); + $this->assertInstanceOf($className, $field); + $this->assertContains('no-change-track', $field->extraClass()); + } + } +} diff --git a/tests/Stubs/MockDocvertService.php b/tests/Stubs/MockDocvertService.php new file mode 100644 index 0000000..dd1efa1 --- /dev/null +++ b/tests/Stubs/MockDocvertService.php @@ -0,0 +1,14 @@ +Fake document

    For testing purposes.

    '; + } +} \ No newline at end of file diff --git a/tests/Stubs/TestPage.php b/tests/Stubs/TestPage.php new file mode 100644 index 0000000..0b73610 --- /dev/null +++ b/tests/Stubs/TestPage.php @@ -0,0 +1,15 @@ + '

    Default TestPage

    With pre-import content.

    ' + ]; +} From 6c6a6b1d4ae23665a7cb5e0d05c28e1c67972f68 Mon Sep 17 00:00:00 2001 From: Dylan Wagstaff Date: Fri, 22 Dec 2017 12:53:25 +1300 Subject: [PATCH 5/6] FIX: Linting issues --- src/PageExtension.php | 16 ++++++++-------- tests/SettingsFieldTest.php | 2 +- tests/Stubs/MockDocvertService.php | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/PageExtension.php b/src/PageExtension.php index 7ac3c25..1e780d4 100644 --- a/src/PageExtension.php +++ b/src/PageExtension.php @@ -16,14 +16,14 @@ class PageExtension extends DataExtension public function updateCMSFields(FieldList $fields) { /* - // Currently the ToggleCompositeField plays badly with TreeDropdownField formatting. - // Could be switched back in the future, if this is fixed. - $fields->addFieldToTab('Root.Main', - ToggleCompositeField::create('Import', 'Import', [ - SettingsField::create() - ])->setHeadingLevel(4) - ); - */ + // Currently the ToggleCompositeField plays badly with TreeDropdownField formatting. + // Could be switched back in the future, if this is fixed. + $fields->addFieldToTab('Root.Main', + ToggleCompositeField::create('Import', 'Import', [ + SettingsField::create() + ])->setHeadingLevel(4) + ); + */ $fields->findOrMakeTab( 'Root.Import', _t(__CLASS__ . '.ImportTab', 'Import') diff --git a/tests/SettingsFieldTest.php b/tests/SettingsFieldTest.php index 0decfee..1498af2 100644 --- a/tests/SettingsFieldTest.php +++ b/tests/SettingsFieldTest.php @@ -63,7 +63,7 @@ public function testFieldListGeneration() 'IncludeTOC' => CheckboxField::class, 'PublishPages' => CheckboxField::class ]; - foreach($settingsFields as $fieldName => $className) { + foreach ($settingsFields as $fieldName => $className) { $field = $fields->fieldByName( 'DocumentConversionSettings-' . $fieldName ); diff --git a/tests/Stubs/MockDocvertService.php b/tests/Stubs/MockDocvertService.php index dd1efa1..cc3540a 100644 --- a/tests/Stubs/MockDocvertService.php +++ b/tests/Stubs/MockDocvertService.php @@ -11,4 +11,4 @@ public function import() { return '

    Fake document

    For testing purposes.

    '; } -} \ No newline at end of file +} From 9125e20ccf0b5d06ce7662f6ec68a2930164319a Mon Sep 17 00:00:00 2001 From: Dylan Wagstaff Date: Fri, 22 Dec 2017 13:03:17 +1300 Subject: [PATCH 6/6] FIX: specify reuiqred php extensions for composer As a precaution for those you might have PHP compiled without the --with-tidy flag, or without --enable-zip --- composer.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/composer.json b/composer.json index a5f5246..530e9ad 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,8 @@ "silverstripe/asset-admin": "^1" }, "require-dev": { + "ext-tidy": "*", + "ext-zip": "*", "phpunit/phpunit": "^5.7", "squizlabs/php_codesniffer": "^3.0" },