From c47fc672989847b2f9b5339b1486ec03f83d2a0d Mon Sep 17 00:00:00 2001 From: Roman Klein Date: Tue, 10 Jul 2018 14:25:38 +0200 Subject: [PATCH] Release 1.3.0 + PHP7 compatibility + Symphony 2.7 compatibility + Code cleanup + Added clean file name to data source output --- LICENCE.txt | 4 +- README.md | 18 ++- extension.driver.php | 239 ++++++++++++++++------------- extension.meta.xml | 18 ++- fields/field.reflectedupload.php | 256 +++++++++++++++++++++++++++---- 5 files changed, 390 insertions(+), 145 deletions(-) diff --git a/LICENCE.txt b/LICENCE.txt index c0bcb0b..27ecc75 100644 --- a/LICENCE.txt +++ b/LICENCE.txt @@ -4,7 +4,7 @@ licence as follows: ----- begin license block ----- -Copyright 2008-2016 Rowan Lewis, Michael Eichelsdoerfer, Simon de Turck, Roman Klein +Copyright 2008-2018 Rowan Lewis, Michael Eichelsdoerfer, Simon de Turck, Roman Klein Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -24,4 +24,4 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ------ end license block ----- \ No newline at end of file +----- end license block ----- diff --git a/README.md b/README.md index 7963a25..2d929dc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Symphony CMS : Reflected Upload Field # +# Reflected Upload Field -An upload field that dynamically renames files (based on values from other fields in the same entry). +#### An upload field for Symphony CMS that dynamically renames files (based on values from other fields in the same entry). ## 1. Installation @@ -10,11 +10,19 @@ An upload field that dynamically renames files (based on values from other field 3. You can now add the '**Reflected File Upload**' field to your sections. -## 2. Configuration & Usage ## +## 2. Field Settings -This field enables you to specify the naming expression using XPath (like in the reflection field). When uniqueness is important you can enable the "Always create unique name" option. This will add "Unique Upload Field" behavior by appending a unique token to the filename. +Compared to Symphony's default upload field **Reflected Upload Field** comes with the following two additional settings: + +1. **Expression** represents the "formula" that's used to generate the reflected filename. You can either use static text or access other fields of the current entry via XPath: {//entry/field-one} static text {//entry/field-two}. +2. **Create unique filenames** gives you the option to add a unique token to the end of the generated filename. This random token will change whenever you save or resave an entry – so this option guarantees that the generated filenames won't get you into caching-troubles whenever you swap files in an entry. ## 3. Acknowledgements ## -This extension is based on [Unique Upload Field](https://github.com/michael-e/uniqueuploadfield) and [Reflection Field](https://github.com/symphonists/reflectionfield). +This extension was initially developed by [Simon de Turck][1] and is based on the extensions [Unique Upload Field][2] and [Reflection Field][3]. + + +[1]: https://github.com/zimmen +[2]: https://github.com/michael-e/uniqueuploadfield +[3]: https://github.com/symphonists/reflectionfield diff --git a/extension.driver.php b/extension.driver.php index 8275344..3b8e590 100644 --- a/extension.driver.php +++ b/extension.driver.php @@ -1,92 +1,152 @@ + * GET SUBSCRIBES DELEGATES + * + * http://www.getsymphony.com/learn/api/2.4/toolkit/extension/#getSubscribedDelegates + * + * @since version 1.0.0 + */ + + public function getSubscribedDelegates() + { + return array( + array( + 'page' => '/publish/new/' , + 'delegate' => 'EntryPostCreate' , + 'callback' => 'compileFields' + ), + array( + 'page' => '/publish/edit/' , + 'delegate' => 'EntryPostEdit' , + 'callback' => 'compileFields' + ), + array( + 'page' => '/frontend/' , + 'delegate' => 'EventPostSaveFilter' , + 'callback' => 'compileFields' + ) + ); + } + + /** + * COMPILE FIELDS (delegate callback) + * + * @param $context + * @return void + * @since version 1.0.0 */ - public static function getXPath($entry) { - $entry_xml = new XMLElement('entry'); - $section_id = $entry->get('section_id'); - $data = $entry->getData(); - $fields = array(); - $entry_xml->setAttribute('id' , $entry->get('id')); - - $associated = $entry->fetchAllAssociatedEntryCounts(); - - if (is_array($associated) and !empty($associated)) { - foreach ($associated as $section => $count) { - $handle = Symphony::Database()->fetchVar('handle' , 0 , " - SELECT s.handle FROM `tbl_sections` AS s - WHERE s.id = '{$section}' - LIMIT 1 - "); - $entry_xml->setAttribute($handle , (string)$count); + + public function compileFields($context) + { + foreach (self::$fields as $field) { + if (!$field->compile($context['entry'])) { + // TODO:ERROR } } + } - // Add fields: - foreach ($data as $field_id => $values) { - if (empty($field_id)) continue; - $fm = new FieldManager($entry); - $field =& $fm->fetch($field_id); - $field->appendFormattedElement($entry_xml , $values , false , null); - } + /*------------------------------------------------------------------------*/ + /* INSTALL / UPDATE / UNINSTALL + /*------------------------------------------------------------------------*/ - $xml = new XMLElement('data'); - $xml->appendChild($entry_xml); - $dom = new DOMDocument(); - $dom->strictErrorChecking = false; - $dom->loadXML($xml->generate(true)); + /** + * INSTALL + * + * http://www.getsymphony.com/learn/api/2.4/toolkit/extension/#install + * + * @since version 1.0.0 + */ - $xpath = new DOMXPath($dom); + public function install() + { + return self::createFieldTable(); + } - if (version_compare(phpversion() , '5.3' , '>=')) { - $xpath->registerPhpFunctions(); - } + /** + * CREATE FIELD TABLE + * + * @since version 1.3.0 + */ - return $xpath; + public static function createFieldTable() + { + $tbl = self::FIELD_TBL_NAME; + + return Symphony::Database()->query(" + CREATE TABLE IF NOT EXISTS `$tbl` ( + `id` int(11) unsigned NOT NULL auto_increment, + `field_id` int(11) unsigned NOT NULL, + `destination` varchar(255) NOT NULL, + `validator` varchar(50), + `expression` VARCHAR(255) DEFAULT NULL, + `unique` tinyint(1) default '0', + PRIMARY KEY (`id`), + KEY `field_id` (`field_id`) + ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + "); } - ############################ - ##### INSTANCE METHODS ##### - ############################ + /** + * UPDATE + * + * http://www.getsymphony.com/learn/api/2.4/toolkit/extension/#update + * + * @since version 1.0.0 + */ - public function update($previousVersion) { - - if(version_compare($previousVersion, '1.0','<=')){ + public function update($previousVersion = false) + { + // updating from versions prior to 1.0 + if(version_compare($previousVersion, '1.0','<=')) { Symphony::Database()->query("ALTER TABLE `tbl_fields_reflectedupload` ADD `unique` tinyint(1) default '0'"); } - - // Before 1.2 + + // updating from versions prior to 1.2 if (version_compare($previous_version, '1.2', '<')) { + // Remove directory from the upload fields, fixes Symphony Issue #1719 $upload_tables = Symphony::Database()->fetchCol("field_id", "SELECT `field_id` FROM `tbl_fields_reflectedupload`"); - + if(is_array($upload_tables) && !empty($upload_tables)) foreach($upload_tables as $field) { Symphony::Database()->query(sprintf( "UPDATE tbl_entries_data_%d SET file = substring_index(file, '/', -1)", @@ -96,61 +156,32 @@ public function update($previousVersion) { } } - public function uninstall() { - - Symphony::Database()->query("DROP TABLE `tbl_fields_reflectedupload`"); - } - - public function install() { - return Symphony::Database()->query( - "CREATE TABLE `tbl_fields_reflectedupload` ( - `id` int(11) unsigned NOT NULL auto_increment, - `field_id` int(11) unsigned NOT NULL, - `destination` varchar(255) NOT NULL, - `validator` varchar(50), - `expression` VARCHAR(255) DEFAULT NULL, - `unique` tinyint(1) default '0', - PRIMARY KEY (`id`), - KEY `field_id` (`field_id`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;" - ); - } - /** - * Delegation - * @return array + * UNINSTALL + * + * http://www.getsymphony.com/learn/api/2.4/toolkit/extension/#uninstall + * + * @since version 1.0.0 */ - public function getSubscribedDelegates() { - return array( - array( - 'page' => '/publish/new/' , - 'delegate' => 'EntryPostCreate' , - 'callback' => 'compileFields' - ) , - array( - 'page' => '/publish/edit/' , - 'delegate' => 'EntryPostEdit' , - 'callback' => 'compileFields' - ) , - array( - 'page' => '/frontend/' , - 'delegate' => 'EventPostSaveFilter' , - 'callback' => 'compileFields' - ) - ); + public function uninstall() + { + return self::deleteFieldTable(); } /** - * @param $context - * @return void - * Delegate callback + * DELETE FIELD TABLE + * + * @since version 1.3.0 */ - public function compileFields($context) { - foreach (self::$fields as $field) { - if (!$field->compile($context['entry'])) { - //TODO:Error - } - } + + public static function deleteFieldTable() + { + $tbl = self::FIELD_TBL_NAME; + + return Symphony::Database()->query(" + DROP TABLE IF EXISTS `$tbl` + "); } + } diff --git a/extension.meta.xml b/extension.meta.xml index 75957e1..bc004a5 100644 --- a/extension.meta.xml +++ b/extension.meta.xml @@ -2,28 +2,32 @@ Reflected Upload Field An upload field that dynamically renames files (based on values from other fields in the same entry). - https://github.com/twiro/reflecteduploadfield + https://github.com/twiro/reflecteduploadfield/ + https://github.com/twiro/reflecteduploadfield/issues http://www.getsymphony.com/discuss/thread/80974/ Field + Reflection Roman Klein http://romanklein.com - - Simon de Turck - http://www.zimmen.com - - + + * PHP7 compatibility + * Symphony 2.7 compatibility + * Code cleanup + * Added clean file name to data source output + + * Added compatibility with Symphony 2.3.3 * File Path no longer saved as part of the file name * Updated readme and licence - + * Added compatibility with Symphony 2.3.0 * Fix when removing a file and not picking a new one diff --git a/fields/field.reflectedupload.php b/fields/field.reflectedupload.php index a7da73a..c388a85 100644 --- a/fields/field.reflectedupload.php +++ b/fields/field.reflectedupload.php @@ -4,31 +4,79 @@ require_once(TOOLKIT . '/fields/field.upload.php'); -class FieldReflectedUpload extends FieldUpload { +class FieldReflectedUpload extends FieldUpload +{ + /*------------------------------------------------------------------------*/ + /* DEFINITION + /*------------------------------------------------------------------------*/ - public function __construct() { + /** + * CONSTRUCT + * + * Constructor for the Field object + * + * http://www.getsymphony.com/learn/api/2.4/toolkit/fieldupload/#__construct + * + * @since version 1.0.0 + */ + + public function __construct() + { + // call the parent constructor parent::__construct(); + + // set the name of the field $this->_name = __('Reflected File Upload'); } - public function displaySettingsPanel($wrapper , $errors = null) { + /*------------------------------------------------------------------------*/ + /* SETTINGS / SECTION EDITOR + /*------------------------------------------------------------------------*/ + + /** + * DISPLAY SETTINGS PANEL + * + * http://www.getsymphony.com/learn/api/2.4/toolkit/fieldupload/#displaySettingsPanel + * + * @since version 1.0.0 + */ + + public function displaySettingsPanel(XMLElement &$wrapper , $errors = null) + { parent::displaySettingsPanel($wrapper , $errors); - $label = Widget::Label('Name Expression - To access the other fields, use XPath: {entry/field-one} static text {entry/field-two}'); + $label = Widget::Label('Expression'); $label->appendChild(Widget::Input( - 'fields[' . $this->get('sortorder') . '][expression]' , - $this->get('expression') - )); + 'fields[' . $this->get('sortorder') . '][expression]' , + $this->get('expression') + )); + + $help = new XMLElement('p'); + $help->setAttribute('class', 'help'); + $help->setValue(__('Use XPath to access other fields: {//entry/field-one} static text {//entry/field-two}.')); + $label->appendChild($help); - if (isset($errors['expression'])) $wrapper->appendChild(Widget::wrapFormElementWithError($label , $errors['expression'])); - else $wrapper->appendChild($label); + if (isset($errors['expression'])) { + $wrapper->appendChild(Widget::wrapFormElementWithError($label , $errors['expression'])); + } else { + $wrapper->appendChild($label); + } - $setting = new XMLElement('label', 'get('unique') == 0 ? '' : ' checked="checked"') . '/> ' . __('Always create unique name') . ' ' . __('This will append a unique token (uniqueupload behavior)') . ''); + $setting = new XMLElement('label', 'get('unique') == 0 ? '' : ' checked="checked"') . '/> ' . __('Create unique filenames') . ' ' . __('This will append a random token to the filename to guarantee uniqueness') . ''); $wrapper->appendChild($setting); } - public function checkFields($errors , $checkForDuplicates = true) { + /** + * CHECK FIELDS + * + * Check the field's settings to ensure they are valid on the section editor + * + * http://www.getsymphony.com/learn/api/2.4/toolkit/fieldupload/#checkFields + * + * @since version 1.0.0 + */ + + public function checkFields(array &$errors , $checkForDuplicates = true) { $expression = $this->get('expression'); if (empty($expression)) { @@ -37,7 +85,19 @@ public function checkFields($errors , $checkForDuplicates = true) { parent::checkFields($errors , $checkForDuplicates); } - public function commit() { + /** + * COMMIT + * + * Commit the settings of this field from the section editor to create an + * instance of this field in a section. + * + * http://www.getsymphony.com/learn/api/2.4/toolkit/fieldupload/#commit + * + * @since version 1.0.0 + */ + + public function commit() + { if (!parent::commit()) return false; $id = $this->get('id'); @@ -45,41 +105,126 @@ public function commit() { if ($id === false) return false; $fields = array(); - $fields['field_id'] = $id; $fields['destination'] = $this->get('destination'); $fields['validator'] = ($fields['validator'] == 'custom' ? NULL : $this->get('validator')); $fields['expression'] = $this->get('expression'); $fields['unique'] = $this->get('unique'); $fields['unique'] = ($this->get('unique') ? 1 : 0); + Symphony::Database()->query("DELETE FROM `tbl_fields_" . $this->handle() . "` WHERE `field_id` = '$id' LIMIT 1"); return Symphony::Database()->insert($fields , 'tbl_fields_' . $this->handle()); } - private function getUniqueFilename($filename) { - ## since uniqid() is 13 bytes, the unique filename will be limited to ($crop+1+13) characters; - $crop = '30'; - return preg_replace("/([^\/]*)(\.[^\.]+)$/e" , "substr('$1', 0, $crop).'-'.uniqid().'$2'" , $filename); - } + /*------------------------------------------------------------------------*/ + /* PROCESSS & SAVE DATA + /*------------------------------------------------------------------------*/ + + /** + * CHECK POST FIELD DATA + * + * Check the field data that has been posted from a form. This will set the + * input message to the error message or to null if there is none. + * Any existing message value will be overwritten. + * + * http://www.getsymphony.com/learn/api/2.4/toolkit/fieldupload/#checkPostFieldData + * + * @since version 1.0.0 + */ - public function checkPostFieldData($data , $message , $entry_id = NULL) { + public function checkPostFieldData($data , &$message , $entry_id = NULL) + { extension_reflecteduploadfield::registerField($this); return self::__OK__; } - public function processRawFieldData($data, $status, $message=null, $simulate = false, $entry_id = NULL) { - if (is_array($data) and isset($data['name'])) $data['name'] = $this->getUniqueFilename($data['name']); + /** + * PROCESS RAW FIELD DATA + * + * http://www.getsymphony.com/learn/api/2.4/toolkit/fieldupload/#processRawFieldData + * + * @since version 1.0.0 + */ + + public function processRawFieldData($data, &$status, &$message=null, $simulate = false, $entry_id = NULL) + { + if (is_array($data) and isset($data['name'])) { + $data['name'] = $this->getUniqueFilename($data['name']); + } + return parent::processRawFieldData($data, $status, $message, $simulate, $entry_id); } /** + * GET UNIQUE FILENAME + * + * @since version 1.0.0 + */ + + private static function getUniqueFilename($filename) + { + return preg_replace_callback( + '/([^\/]*)(\.[^\.]+)$/', + function ($m) { + // uniqid() is 13 bytes, so the unique filename will be limited to ($crop + 1 + 13) characters + $crop = '60'; + return substr($m[1], 0, $crop) . '-' . uniqid() . $m[2]; + }, + $filename + ); + } + + /*------------------------------------------------------------------------*/ + /* DATA SOURCE OUTPUT + /*------------------------------------------------------------------------*/ + + /** + * APPEND FORMATTED ELEMENT + * + * http://www.getsymphony.com/learn/api/2.4/toolkit/fieldupload/#appendFormattedElement + * + * @since version 1.3.0 + */ + + public function appendFormattedElement(XMLElement &$wrapper, $data, $encode = false, $mode = null, $entry_id = null) + { + parent::appendFormattedElement($wrapper, $data); + $field = $wrapper->getChildrenByName($this->get('element_name')); + if(!empty($field)) { + end($field)->appendChild(new XMLElement('clean-filename', General::sanitize(self::getCleanFilename(basename($data['file']))))); + } + } + + /** + * GET CLEAN FILENAME + * + * @since version 1.3.0 + */ + + private static function getCleanFilename($filename) + { + return preg_replace("/([^\/]*)(\-[a-f0-9]{13})(\.[^\.]+)$/", '$1$3', $filename); + } + + /*------------------------------------------------------------------------*/ + /* XPATH & REFLECTION + /*------------------------------------------------------------------------*/ + + /** + * COMPILE + * * @param $entry * @return boolean + * * Renames the file based on the expression. * Inspired by Rowan Lewis + * + * @since version 1.0.0 */ - public function compile($entry) { - $xpath = extension_reflecteduploadfield::getXPath($entry); + + public function compile($entry) + { + $xpath = $this->getXPath($entry); $entry_id = $entry->get('id'); $field_id = $this->get('id'); @@ -103,9 +248,7 @@ public function compile($entry) { $result = @$xpath->evaluate('string(' . trim($match , '{}') . ')'); if (!is_null($result)) { $replacements[$match] = trim($result); - } - - else { + } else { $replacements[$match] = ''; } } @@ -116,6 +259,7 @@ public function compile($entry) { array_values($replacements) , $expression ); + if($unique){ $new_value = $value . '-' . uniqid() . $file_extension; }else{ @@ -128,6 +272,7 @@ public function compile($entry) { $old = $abs_path . '/' . $old_filename . $file_extension; $new = $abs_path . '/' . $new_value; + if (rename($old , $new)) { // Save: $result = Symphony::Database()->update( @@ -143,4 +288,61 @@ public function compile($entry) { return false; } } + + /** + * GET XPATH + * + * @static + * @param $entry + * @return DOMXPath + * + * Function by Rowan Lewis + * + * @since version 1.0.0 + */ + + public static function getXPath($entry) + { + $entry_xml = new XMLElement('entry'); + $section_id = $entry->get('section_id'); + $data = $entry->getData(); + $fields = array(); + $entry_xml->setAttribute('id' , $entry->get('id')); + + $associated = $entry->fetchAllAssociatedEntryCounts(); + + if (is_array($associated) and !empty($associated)) { + foreach ($associated as $section => $count) { + $handle = Symphony::Database()->fetchVar('handle' , 0 , " + SELECT s.handle FROM `tbl_sections` AS s + WHERE s.id = '{$section}' + LIMIT 1 + "); + $entry_xml->setAttribute($handle , (string)$count); + } + } + + // Add fields: + foreach ($data as $field_id => $values) { + if (empty($field_id)) continue; + $fm = new FieldManager($entry); + $field =& $fm->fetch($field_id); + $field->appendFormattedElement($entry_xml , $values , false , null); + } + + $xml = new XMLElement('data'); + $xml->appendChild($entry_xml); + $dom = new DOMDocument(); + $dom->strictErrorChecking = false; + $dom->loadXML($xml->generate(true)); + + $xpath = new DOMXPath($dom); + + if (version_compare(phpversion() , '5.3' , '>=')) { + $xpath->registerPhpFunctions(); + } + + return $xpath; + } + }