Implemented revert feature for tracked versions
authorAlexander Ebert <ebert@woltlab.com>
Tue, 28 Mar 2017 15:45:41 +0000 (17:45 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Tue, 28 Mar 2017 15:45:41 +0000 (17:45 +0200)
See #2240

wcfsetup/install/files/acp/templates/versionTrackerList.tpl
wcfsetup/install/files/js/WCF.Message.js
wcfsetup/install/files/lib/data/IVersionTrackerObject.class.php
wcfsetup/install/files/lib/data/article/ArticleAction.class.php
wcfsetup/install/files/lib/data/article/ArticleVersionTracker.class.php
wcfsetup/install/files/lib/system/version/ArticleVersionTrackerProvider.class.php
wcfsetup/install/files/lib/system/version/IVersionTrackerProvider.class.php
wcfsetup/install/files/lib/system/version/VersionTracker.class.php
wcfsetup/install/files/lib/system/version/VersionTrackerEntry.class.php

index 224827433ad542f9b50178a6f7f5a4a1ad9a5310..248fc0a7c2a694946f09e9cdd0241aa164930198 100644 (file)
@@ -9,6 +9,7 @@
        
        <nav class="contentHeaderNavigation">
                <ul>
+                       <li><a href="{$object->getEditLink()}" class="button"><span class="icon icon16 fa-pencil"></span> <span>{lang}wcf.global.button.edit{/lang}</span></a></li>
                        <li><a href="{$object->getLink()}" class="button"><span class="icon icon16 fa-arrow-right"></span> <span>{lang}wcf.edit.button.goToContent{/lang}</span></a></li>
                        
                        {event name='contentHeaderNavigation'}
                                        </tr>
                                {/foreach}
                        </tbody>
+                       
+                       {js application='wcf' file='WCF.Message' bundle='WCF.Combined'}
                        <script data-relocate="true">
                                $(function () {
-                                       /*
-                                               TODO: this needs to be adjusted
-                                       */
-                                       new WCF.Message.EditHistory($('input[name=oldID]'), $('input[name=newID]'), '.jsEditRow');
+                                       new WCF.Message.EditHistory($('input[name=oldID]'), $('input[name=newID]'), '.jsEditRow', undefined, {
+                                               isVersionTracker: true,
+                                               versionTrackerObjectType: '{$objectType}',
+                                               versionTrackerObjectId: {@$objectID},
+                                               redirectUrl: '{$object->getEditLink()}'
+                                       });
                                });
                        </script>
                </table>
index 7fc5bfd96180080c444454fe11f44e19e7ea41f8..27b2a442da57b0f207e50388b09aeb244eb3ef20 100644 (file)
@@ -121,12 +121,19 @@ WCF.Message.EditHistory = Class.extend({
         * @param       object  newIDInputs
         * @param       string  containerSelector
         * @param       string  buttonSelector
+        * @param       {Object}        options
         */
-       init: function(oldIDInputs, newIDInputs, containerSelector, buttonSelector) {
+       init: function(oldIDInputs, newIDInputs, containerSelector, buttonSelector, options) {
                this._oldIDInputs = oldIDInputs;
                this._newIDInputs = newIDInputs;
                this._containerSelector = containerSelector;
                this._buttonSelector = (buttonSelector) ? buttonSelector : '.jsRevertButton';
+               this._options = $.extend({
+                       isVersionTracker: false,
+                       versionTrackerObjectType: '',
+                       versionTrackerObjectId: 0,
+                       redirectUrl: ''
+               }, options);
                
                this.proxy = new WCF.Action.Proxy({
                        success: $.proxy(this._success, this)
@@ -221,11 +228,26 @@ WCF.Message.EditHistory = Class.extend({
         * @param       jQuery  object
         */
        _sendRequest: function(object) {
-               this.proxy.setOption('data', {
-                       actionName: 'revert',
-                       className: 'wcf\\data\\edit\\history\\entry\\EditHistoryEntryAction',
-                       objectIDs: [ $(object).data('objectID') ]
-               });
+               if (this._options.isVersionTracker) {
+                       //noinspection JSUnresolvedVariable
+                       this.proxy.setOption('url', window.WSC_API_URL + 'index.php?ajax-invoke/&t=' + window.SECURITY_TOKEN);
+                       this.proxy.setOption('data', {
+                               actionName: 'revert',
+                               className: 'wcf\\system\\version\\VersionTracker',
+                               parameters: {
+                                       objectType: this._options.versionTrackerObjectType,
+                                       objectID: this._options.versionTrackerObjectId,
+                                       versionID: $(object).data('objectID')
+                               }
+                       });
+               }
+               else {
+                       this.proxy.setOption('data', {
+                               actionName: 'revert',
+                               className: 'wcf\\data\\edit\\history\\entry\\EditHistoryEntryAction',
+                               objectIDs: [$(object).data('objectID')]
+                       });
+               }
                
                this.proxy.sendRequest();
        },
@@ -238,7 +260,14 @@ WCF.Message.EditHistory = Class.extend({
         * @param       object          jqXHR
         */
        _success: function(data, textStatus, jqXHR) {
-               window.location.reload(true);
+               if (this._options.redirectUrl) {
+                       new WCF.System.Notification().show((function () {
+                               window.location = this._options.redirectUrl;
+                       }).bind(this));
+               }
+               else {
+                       window.location.reload(true);
+               }
        }
 });
 
index afd84b4297f38476c259ca9a216e51b1f4c85138..882adf1efb2d5416c15f13520421877099cd4862 100644 (file)
@@ -10,6 +10,13 @@ namespace wcf\data;
  * @package    WoltLabSuite\Core\Data
  */
 interface IVersionTrackerObject extends IUserContent {
+       /**
+        * Returns the link to the object's edit page.
+        * 
+        * @return      string
+        */
+       public function getEditLink();
+       
        /**
         * Returns the object's unique id.
         * 
index dd16c41b3b6bf9373b14b3cb5917601b487c2312..b99faf709499c78dc8d4f7cf49f9dc404a936439 100644 (file)
@@ -124,6 +124,8 @@ class ArticleAction extends AbstractDatabaseObjectAction {
        public function update() {
                parent::update();
                
+               $isRevert = (!empty($this->parameters['isRevert']));
+               
                // update article content
                if (!empty($this->parameters['content'])) {
                        foreach ($this->getObjects() as $article) {
@@ -145,8 +147,8 @@ class ArticleAction extends AbstractDatabaseObjectAction {
                                                        'title' => $content['title'],
                                                        'teaser' => $content['teaser'],
                                                        'content' => $content['content'],
-                                                       'imageID' => $content['imageID'],
-                                                       'teaserImageID' => $content['teaserImageID']
+                                                       'imageID' => ($isRevert) ? $articleContent->imageID : $content['imageID'],
+                                                       'teaserImageID' => ($isRevert) ? $articleContent->teaserImageID : $content['teaserImageID']
                                                ]);
                                                
                                                $versionData[] = $articleContent;
@@ -155,7 +157,7 @@ class ArticleAction extends AbstractDatabaseObjectAction {
                                                }
                                                
                                                // delete tags
-                                               if (empty($content['tags'])) {
+                                               if (!$isRevert && empty($content['tags'])) {
                                                        TagEngine::getInstance()->deleteObjectTags('com.woltlab.wcf.article', $articleContent->articleContentID, ($languageID ?: null));
                                                }
                                        }
@@ -167,8 +169,8 @@ class ArticleAction extends AbstractDatabaseObjectAction {
                                                        'title' => $content['title'],
                                                        'teaser' => $content['teaser'],
                                                        'content' => $content['content'],
-                                                       'imageID' => $content['imageID'],
-                                                       'teaserImageID' => $content['teaserImageID']
+                                                       'imageID' => ($isRevert) ? null : $content['imageID'],
+                                                       'teaserImageID' => ($isRevert) ? null : $content['teaserImageID']
                                                ]);
                                                $articleContentEditor = new ArticleContentEditor($articleContent);
                                                
@@ -177,7 +179,7 @@ class ArticleAction extends AbstractDatabaseObjectAction {
                                        }
                                        
                                        // save tags
-                                       if (!empty($content['tags'])) {
+                                       if (!$isRevert && !empty($content['tags'])) {
                                                TagEngine::getInstance()->addObjectTags('com.woltlab.wcf.article', $articleContent->articleContentID, $content['tags'], ($languageID ?: LanguageFactory::getInstance()->getDefaultLanguageID()));
                                        }
                                        
index 90a084df8ef0ee9abadd2d11f4da5328a994e173..931339fd2b41e00a135d9bdb0bd571b6c83f115d 100644 (file)
@@ -3,6 +3,7 @@ namespace wcf\data\article;
 use wcf\data\article\content\ArticleContent;
 use wcf\data\DatabaseObjectDecorator;
 use wcf\data\IVersionTrackerObject;
+use wcf\system\request\LinkHandler;
 
 /**
  * Represents an article with version tracking.
@@ -96,4 +97,11 @@ class ArticleVersionTracker extends DatabaseObjectDecorator implements IVersionT
        public function getTitle() {
                return $this->getDecoratedObject()->getTitle();
        }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getEditLink() {
+               return LinkHandler::getInstance()->getLink('ArticleEdit', ['isACP' => true, 'id' => $this->getDecoratedObject()->articleID]);
+       }
 }
index 5fecaa6f172c0c0e8f615502a0d02f8c5e2d65ad..8bc3a530b3f6371ede3b510bbbb2b9524202fd56 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 namespace wcf\system\version;
 use wcf\data\article\Article;
+use wcf\data\article\ArticleAction;
 use wcf\data\article\ArticleList;
 use wcf\data\article\ArticleVersionTracker;
 use wcf\data\IVersionTrackerObject;
@@ -107,4 +108,21 @@ class ArticleVersionTrackerProvider extends AbstractVersionTrackerProvider {
                /** @var Article $object */
                return $object->isMultilingual == 1;
        }
+       
+       /**
+        * @inheritDoc
+        */
+       public function revert(IVersionTrackerObject $object, VersionTrackerEntry $entry) {
+               /** @var ArticleVersionTracker $object */
+               
+               // build the content data
+               $properties = $this->getTrackedProperties();
+               $content = [];
+               foreach ($object->getArticleContents() as $articleContent) {
+                       $content[$articleContent->languageID ?: 0] = $entry->getPayloadForProperties($properties, $articleContent->languageID ?: 0);
+               }
+               
+               $action = new ArticleAction([$object->getDecoratedObject()], 'update', ['content' => $content, 'isRevert' => true]);
+               $action->executeAction();
+       }
 }
index 563f6b3b80faa5b0a31c941fab9be9ae021ce431..44c1ea3a334c13ea87af4c440c543568b818db0d 100644 (file)
@@ -73,4 +73,12 @@ interface IVersionTrackerProvider extends IObjectTypeProvider {
         * @return      boolean
         */
        public function isI18n(IVersionTrackerObject $object);
+       
+       /**
+        * Reverts an object to a previous version.
+        * 
+        * @param       IVersionTrackerObject   $object         target object
+        * @param       VersionTrackerEntry     $entry          previous version
+        */
+       public function revert(IVersionTrackerObject $object, VersionTrackerEntry $entry);
 }
index ca13ed5b4e876c8094b4e530b4658af7c593c3a8..f45b7a0e6e472d522c24070114df41cd5e719cd4 100644 (file)
@@ -7,9 +7,14 @@ use wcf\data\object\type\ObjectTypeList;
 use wcf\data\package\Package;
 use wcf\data\package\PackageList;
 use wcf\data\IVersionTrackerObject;
+use wcf\system\exception\PermissionDeniedException;
 use wcf\system\exception\SystemException;
+use wcf\system\exception\UserInputException;
+use wcf\system\IAJAXInvokeAction;
+use wcf\system\request\RequestHandler;
 use wcf\system\SingletonFactory;
 use wcf\system\WCF;
+use wcf\util\StringUtil;
 
 /**
  * Represents objects that support some of their properties to be saved.
@@ -19,13 +24,37 @@ use wcf\system\WCF;
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @package    WoltLabSuite\Core\System\Version
  */
-class VersionTracker extends SingletonFactory {
+class VersionTracker extends SingletonFactory implements IAJAXInvokeAction {
+       /**
+        * list of methods that may be invoked via ajax
+        * @var string[]
+        */
+       public static $allowInvoke = ['revert'];
+       
        /**
         * list of available object types
         * @var ObjectType[]
         */
        protected $availableObjectTypes = [];
        
+       /**
+        * version tracker object used for the version revert process
+        * @var IVersionTrackerObject
+        */
+       protected $object;
+       
+       /**
+        * object type processor object
+        * @var IVersionTrackerProvider
+        */
+       protected $processor;
+       
+       /**
+        * version tracker entry used for the version revert process
+        * @var VersionTrackerEntry
+        */
+       protected $version;
+       
        /**
         * @inheritDoc
         */
@@ -180,6 +209,50 @@ class VersionTracker extends SingletonFactory {
                throw new \InvalidArgumentException("Unknown object type '".$name."' for definition 'com.woltlab.wcf.versionTracker.objectType'.");
        }
        
+       /**
+        * Validates parameters to revert an object to a previous version.
+        * 
+        * @throws      PermissionDeniedException
+        * @throws      UserInputException
+        */
+       public function validateRevert() {
+               if (!RequestHandler::getInstance()->isACPRequest()) {
+                       throw new PermissionDeniedException();
+               }
+               
+               if (!isset($_POST['parameters'])) {
+                       throw new UserInputException('parameters');
+               }
+               
+               $objectTypeName = (isset($_POST['parameters']['objectType'])) ? StringUtil::trim($_POST['parameters']['objectType']) : '';
+               $objectID = (isset($_POST['parameters']['objectID'])) ? intval($_POST['parameters']['objectID']) : 0;
+               $versionID = (isset($_POST['parameters']['versionID'])) ? intval($_POST['parameters']['versionID']) : 0;
+               
+               $objectType = $this->getObjectType($objectTypeName);
+               /** @var IVersionTrackerProvider $processor */
+               $this->processor = $objectType->getProcessor();
+               if (!$this->processor->canAccess()) {
+                       throw new PermissionDeniedException();
+               }
+               
+               $this->object = $this->processor->getObjectByID($objectID);
+               if (!$this->object->getObjectID()) {
+                       throw new UserInputException('objectID');
+               }
+               
+               $this->version = $this->getVersion($objectTypeName, $versionID);
+               if (!$this->version->versionID) {
+                       throw new UserInputException('versionID');
+               }
+       }
+       
+       /**
+        * Reverts an object to a previous version.
+        */
+       public function revert() {
+               $this->processor->revert($this->object, $this->version);
+       }
+       
        /**
         * Creates a database table for an object type unless it exists already.
         * 
index f9591fd1e776e1b93b333b7fb9cfe65975f3b3d8..b6f85b01885b82611c0b2983722324a136904a8d 100644 (file)
@@ -93,6 +93,27 @@ class VersionTrackerEntry {
                return '';
        }
        
+       /**
+        * Returns the stored values for all given properties. Unknown or missing
+        * properties will be set to an empty string.
+        * 
+        * @param       string[]        $properties     list of property names
+        * @param       integer         $languageID     language id
+        * @return      string[]
+        */
+       public function getPayloadForProperties(array $properties, $languageID) {
+               $payload = [];
+               foreach ($properties as $property) {
+                       $payload[$property] = '';
+                       
+                       if (isset($this->payload[$languageID]) && isset($this->payload[$languageID][$property])) {
+                               $payload[$property] = $this->payload[$languageID][$property];
+                       }
+               }
+               
+               return $payload;
+       }
+       
        /**
         * Returns the list of language ids.
         *