Added 'sync all', improved behavior, fixes
authorAlexander Ebert <ebert@woltlab.com>
Thu, 13 Jul 2017 12:11:55 +0000 (14:11 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Thu, 13 Jul 2017 12:11:55 +0000 (14:11 +0200)
See #2331

43 files changed:
com.woltlab.wcf/option.xml
wcfsetup/install/files/acp/templates/devtoolsProjectSync.tpl
wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Devtools/Project/Sync.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ajax.js
wcfsetup/install/files/js/WoltLabSuite/Core/Ajax/Request.js
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Dialog.js
wcfsetup/install/files/lib/acp/form/DevtoolsProjectSyncForm.class.php [deleted file]
wcfsetup/install/files/lib/acp/page/DevtoolsProjectSyncPage.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/devtools/project/DevtoolsProject.class.php
wcfsetup/install/files/lib/data/package/installation/plugin/PackageInstallationPluginAction.class.php
wcfsetup/install/files/lib/system/devtools/package/DevtoolsInstaller.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/devtools/package/DevtoolsPackageArchive.class.php
wcfsetup/install/files/lib/system/devtools/package/DevtoolsTar.class.php
wcfsetup/install/files/lib/system/devtools/pip/DevtoolsPackageInstallationDispatcher.class.php
wcfsetup/install/files/lib/system/devtools/pip/DevtoolsPackageInstallationQueue.class.php
wcfsetup/install/files/lib/system/devtools/pip/DevtoolsPip.class.php
wcfsetup/install/files/lib/system/devtools/pip/IIdempotentPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/ACPSearchProviderPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/ACPTemplatePackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/AbstractMenuPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/AbstractOptionPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/BBCodePackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/BoxPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/ClipboardActionPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/CoreObjectPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/CronjobPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/EventListenerPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/FilePackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/LanguagePackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/MenuItemPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/MenuPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/ObjectTypeDefinitionPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/ObjectTypePackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/PIPPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/PagePackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/SmileyPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/TemplateListenerPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/TemplatePackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/UserNotificationEventPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/package/plugin/UserProfileMenuPackageInstallationPlugin.class.php
wcfsetup/install/files/lib/system/setup/Installer.class.php
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml

index a3aaf39649f0311c0de448d6a922d59c1a0b2e6e..ba97bd11506bd6b79cbc4c2f60e1cb9fc09cf7d9 100644 (file)
@@ -21,7 +21,7 @@
                                <category name="module.community">
                                        <parent>module</parent>
                                </category>
-                               <category name="module.developer">
+                               <category name="module.development">
                                        <parent>module</parent>
                                </category>
                        <!-- /modules -->
                        </option>
                        
                        <option name="enable_debug_mode">
-                               <categoryname>module.developer</categoryname>
+                               <categoryname>module.development</categoryname>
                                <optiontype>boolean</optiontype>
                                <defaultvalue>0</defaultvalue>
                        </option>
                        <option name="enable_benchmark">
-                               <categoryname>module.developer</categoryname>
+                               <categoryname>module.development</categoryname>
                                <optiontype>boolean</optiontype>
                                <defaultvalue>0</defaultvalue>
                        </option>
                        <option name="enable_developer_tools">
-                               <categoryname>module.developer</categoryname>
+                               <categoryname>module.development</categoryname>
                                <optiontype>boolean</optiontype>
                                <defaultvalue>0</defaultvalue>
                        </option>
index 441289be05f3431634b48b06edc3abe348e43c12..e7750fe0efbbcfe827bdec1f81bd1390144f8793 100644 (file)
 {if $object->validate() === ''}
        <p class="info">{lang}wcf.acp.devtools.pip.notice{/lang}</p>
        
-       <form method="post" action="{link controller='DevtoolsProjectSync' id=$objectID}{/link}">
-               <div class="section">
-                       <dl>
-                               <dt></dt>
-                               <dd>
-                                       <label><input type="checkbox" id="syncShowOnlyMatches" checked> {lang}wcf.acp.devtools.pip.showOnlyMatches{/lang}</label>
-                                       <small>{lang}wcf.acp.devtools.pip.showOnlyMatches.description{/lang}</small>
-                               </dd>
-                       </dl>
-               </div>
-               <div class="section tabularBox jsShowOnlyMatches" id="syncPipMatches">
-                       <table class="table">
-                               <thead>
-                                       <tr>
-                                               <th class="columnText">{lang}wcf.acp.devtools.pip.pluginName{/lang}</th>
-                                               <th class="columnText">{lang}wcf.acp.devtools.pip.defaultFilename{/lang}</th>
-                                               <th class="columnIcon">{lang}wcf.acp.devtools.pip.target{/lang}</th>
-                                       </tr>
-                               </thead>
-                               
-                               <tbody>
-                                       {foreach from=$object->getPips() item=pip}
-                                               {assign var=_isSupported value=$pip->isSupported()}
-                                               {assign var=_targets value=$pip->getTargets($object)}
-                                               
-                                               <tr data-is-supported="{if $_isSupported}true{else}false{/if}" {if !$_targets|empty} class="jsHasPipTargets"{/if}>
-                                                       <td class="columnText">{$pip->pluginName}</td>
-                                                       {if $_isSupported}
-                                                               <td class="columnText"><small>{$pip->getDefaultFilename()}</small></td>
-                                                               <td class="columnIcon">
-                                                                       {hascontent}
-                                                                               <ul class="buttonGroup">
-                                                                                       {content}
-                                                                                               {foreach from=$_targets item=target}
-                                                                                                       <li><button class="small jsInvokePip" data-plugin-name="{$pip->pluginName}" data-target="{$target}">{$target}</button></li>
-                                                                                               {/foreach}
-                                                                                       {/content}
-                                                                               </ul>
-                                                                       {hascontentelse}
-                                                                               <small>{lang}wcf.acp.devtools.pip.target.noMatches{/lang}</small>
-                                                                       {/hascontent}
-                                                               </td>
+       <div class="section">
+               <dl>
+                       <dt></dt>
+                       <dd>
+                               <label><input type="checkbox" id="syncShowOnlyMatches" checked> {lang}wcf.acp.devtools.pip.showOnlyMatches{/lang}</label>
+                               <small>{lang}wcf.acp.devtools.pip.showOnlyMatches.description{/lang}</small>
+                       </dd>
+               </dl>
+       </div>
+       <div class="section tabularBox jsShowOnlyMatches" id="syncPipMatches">
+               <table class="table">
+                       <thead>
+                               <tr>
+                                       <th class="columnText">{lang}wcf.acp.devtools.pip.pluginName{/lang}</th>
+                                       <th class="columnText">{lang}wcf.acp.devtools.pip.defaultFilename{/lang}</th>
+                                       <th class="columnIcon" colspan="2">{lang}wcf.acp.devtools.pip.target{/lang}</th>
+                               </tr>
+                       </thead>
+                       
+                       <tbody>
+                               {foreach from=$object->getPips() item=pip}
+                                       {assign var=_isSupported value=$pip->isSupported()}
+                                       {assign var=_targets value=$pip->getTargets($object)}
+                                       {assign var=_targetCount value=$_targets|count}
+                                       
+                                       <tr data-plugin-name="{$pip->pluginName}" data-is-supported="{if $_isSupported}true{else}false{/if}" {if $_targetCount} class="jsHasPipTargets" data-sync-dependencies="{$pip->getSyncDependencies(true)}"{/if}>
+                                               <td class="columnText" rowspan="{$_targetCount}">{$pip->pluginName}</td>
+                                               {if $_isSupported}
+                                                       <td class="columnText pipDefaultFilename" rowspan="{$_targetCount}"><small>{$pip->getEffectiveDefaultFilename()}</small></td>
+                                                       {if $_targetCount}
+                                                               <td class="columnIcon"><button class="small jsInvokePip" data-target="{$_targets[0]}">{$_targets[0]}</button></td>
+                                                               <td class="columnText"><small class="jsInvokePipResult" data-target="{$_targets[0]}">{lang}wcf.acp.devtools.sync.status.idle{/lang}</td>
                                                        {else}
-                                                               <td class="columnText" colspan="2">{$pip->getFirstError()}</td>
+                                                               <td class="columnText" colspan="2">
+                                                                       <small>{lang}wcf.acp.devtools.pip.target.noMatches{/lang}</small>
+                                                               </td>
                                                        {/if}
-                                               </tr>
-                                       {/foreach}
-                               </tbody>
-                       </table>
-               </div>
-               
-               {event name='sections'}
-               
-               <div class="formSubmit">
-                       <input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s">
-                       {@SECURITY_TOKEN_INPUT_TAG}
-               </div>
-       </form>
+                                               {else}
+                                                       <td class="columnText" colspan="3">{$pip->getFirstError()}</td>
+                                               {/if}
+                                       </tr>
+                                       {if $_targetCount}
+                                               {section name=i loop=$_targets start=1}
+                                                       <tr data-plugin-name="{$pip->pluginName}" {if $_targetCount} class="jsHasPipTargets jsSkipTargetDetection"{/if}>
+                                                               <td class="columnIcon"><button class="small jsInvokePip" data-target="{$_targets[$i]}">{$_targets[$i]}</button></td>
+                                                               <td class="columnText"><small class="jsInvokePipResult" data-target="{$_targets[$i]}">{lang}wcf.acp.devtools.sync.status.idle{/lang}</td>
+                                                       </tr>
+                                               {/section}
+                                       {/if}
+                               {/foreach}
+                       </tbody>
+               </table>
+       </div>
        
        <script data-relocate="true">
-               var container = elById('syncPipMatches');
-               elById('syncShowOnlyMatches').addEventListener('change', function() {
-                       container.classList.toggle('jsShowOnlyMatches');
-               });
-               
-               require(['Ajax'], function(Ajax) {
-                       var that = {
-                               _ajaxSetup: function() {
-                                       return {
-                                               data: {
-                                                       actionName: 'invoke',
-                                                       className: 'wcf\\data\\package\\installation\\plugin\\PackageInstallationPluginAction',
-                                                       parameters: {
-                                                               projectID: {@$object->projectID}
-                                                       }
-                                               }
-                                       }
-                               }
-                       };
-                       
-                       elBySelAll('.jsInvokePip', container, function(button) {
-                               button.addEventListener(WCF_CLICK_EVENT, function(event) {
-                                       event.preventDefault();
-                                       
-                                       Ajax.api(that, {
-                                               parameters: {
-                                                       pluginName: elData(button, 'plugin-name'),
-                                                       target: elData(button, 'target')
-                                               }
-                                       });
-                               });
+               require(['Language', 'WoltLabSuite/Core/Acp/Ui/Devtools/Project/Sync'], function(Language, AcpUiDevtoolsProjectSync) {
+                       Language.addObject({
+                               'wcf.acp.devtools.sync.status.failure': '{lang}wcf.acp.devtools.sync.status.failure{/lang}',
+                               'wcf.acp.devtools.sync.syncAll': '{lang}wcf.acp.devtools.sync.syncAll{/lang}'
                        });
+                       
+                       AcpUiDevtoolsProjectSync.init({$object->projectID});
                });
        </script>
        
                #syncPipMatches.jsShowOnlyMatches tbody > tr:not(.jsHasPipTargets) {
                        display: none;
                }
+               
+               #syncPipMatches > table {
+                       /*table-layout: fixed;*/
+               }
+               
+               #syncPipMatches td:first-child {
+                       width: 300px;
+               }
+               
+               #syncPipMatches td.pipDefaultFilename {
+                       width: 300px;
+               }
+               
+               #syncPipMatches td:last-child {
+                       width: auto;
+               }
+               
+               .syncStatusContainer {
+                       overflow: hidden;
+               }
        </style>
 {else}
        <p class="error">{$object->validate()}</p>
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Devtools/Project/Sync.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Devtools/Project/Sync.js
new file mode 100644 (file)
index 0000000..41c6ae1
--- /dev/null
@@ -0,0 +1,191 @@
+define(['Ajax', 'Dictionary', 'Language', 'Ui/Dialog'], function (Ajax, Dictionary, Language, UiDialog) {
+       "use strict";
+       
+       var _buttons = new Dictionary();
+       var _buttonStatus = new Dictionary();
+       var _buttonSyncAll = null;
+       var _container = elById('syncPipMatches');
+       var _pips = [];
+       var _projectId = 0;
+       var _queue = [];
+       
+       return {
+               init: function (projectId) {
+                       _projectId = projectId;
+                       
+                       
+                       elById('syncShowOnlyMatches').addEventListener('change', function() {
+                               _container.classList.toggle('jsShowOnlyMatches');
+                       });
+                       
+                       var knownPips = [], tmpPips = [];
+                       elBySelAll('.jsHasPipTargets:not(.jsSkipTargetDetection)', _container, (function (pip) {
+                               var pluginName = elData(pip, 'plugin-name');
+                               var targets = [];
+                               
+                               elBySelAll('.jsHasPipTargets[data-plugin-name="' + pluginName + '"] .jsInvokePip', _container, (function(button) {
+                                       var target = elData(button, 'target');
+                                       targets.push(target);
+                                       
+                                       button.addEventListener(WCF_CLICK_EVENT, (function(event) {
+                                               event.preventDefault();
+                                               
+                                               if (_queue.length > 0) return;
+                                               
+                                               this._sync(pluginName, target);
+                                       }).bind(this));
+                                       
+                                       _buttons.set(pluginName + '-' + target, button);
+                                       _buttonStatus.set(pluginName + '-' + target, elBySel('.jsHasPipTargets[data-plugin-name="' + pluginName + '"] .jsInvokePipResult[data-target="' + target + '"]', _container));
+                               }).bind(this));
+                               
+                               var data = {
+                                       dependencies: JSON.parse(elData(pip, 'sync-dependencies')),
+                                       pluginName: pluginName,
+                                       targets: targets
+                               };
+                               
+                               if (data.dependencies.length > 0) {
+                                       tmpPips.push(data);
+                               }
+                               else {
+                                       _pips.push(data);
+                                       knownPips.push(pluginName);
+                               }
+                       }).bind(this));
+                       
+                       var resolvedDependency = false;
+                       while (tmpPips.length > 0) {
+                               resolvedDependency = false;
+                               
+                               var openDependencies, item, length = tmpPips.length;
+                               for (var i = 0; i < length; i++) {
+                                       item = tmpPips[i];
+                                       
+                                       openDependencies = item.dependencies.filter(function (dependency) {
+                                               return (knownPips.indexOf(dependency) === -1);
+                                       });
+                                       
+                                       if (openDependencies.length === 0) {
+                                               knownPips.push(item.pluginName);
+                                               _pips.push(item);
+                                               tmpPips.splice(i, 1);
+                                               
+                                               resolvedDependency = true;
+                                               break;
+                                       }
+                               }
+                               
+                               if (!resolvedDependency) {
+                                       // We could not resolve any dependency, either because there is no more pip
+                                       // in `tmpPips` or we're facing a circular dependency. In case there are items
+                                       // left, we simply append them to the end and hope for the operation to
+                                       // complete anyway, despite unmatched dependencies.
+                                       tmpPips.forEach(function(pip) {
+                                               window.console.warn('Unable to resolve dependencies for', pip);
+                                               
+                                               _pips.push(pip);
+                                       });
+                                       
+                                       break;
+                               }
+                       }
+                       
+                       var syncAll = elCreate('li');
+                       syncAll.innerHTML = '<a href="#" class="button"><span class="icon icon16 fa-refresh"></span> ' + Language.get('wcf.acp.devtools.sync.syncAll') + '</a>';
+                       _buttonSyncAll = syncAll.children[0];
+                       _buttonSyncAll.addEventListener(WCF_CLICK_EVENT, this._syncAll.bind(this));
+                       
+                       var list = elBySel('.contentHeaderNavigation > ul');
+                       list.insertBefore(syncAll, list.firstElementChild);
+               },
+               
+               _sync: function (pluginName, target) {
+                       _buttons.get(pluginName + '-' + target).disabled = true;
+                       _buttonStatus.get(pluginName + '-' + target).innerHTML = '<span class="icon icon16 fa-spinner"></span>';
+                       
+                       Ajax.api(this, {
+                               parameters: {
+                                       pluginName: pluginName,
+                                       target: target
+                               }
+                       });
+               },
+               
+               _syncAll: function (event) {
+                       event.preventDefault();
+                       
+                       if (_buttonSyncAll.classList.contains('disabled')) {
+                               return;
+                       }
+                       
+                       _buttonSyncAll.classList.add('disabled');
+                       
+                       _queue = [];
+                       _pips.forEach(function(pip) {
+                               pip.targets.forEach(function (target) {
+                                       _queue.push([pip.pluginName, target]);
+                               });
+                       });
+                       this._syncNext();
+               },
+               
+               _syncNext: function () {
+                       if (_queue.length === 0) {
+                               _buttonSyncAll.classList.remove('disabled');
+                               
+                               // TODO: do stuff
+                               return;
+                       }
+                       
+                       var next = _queue.shift();
+                       this._sync(next[0], next[1]);
+               },
+               
+               _ajaxSuccess: function(data) {
+                       _buttons.get(data.returnValues.pluginName + '-' + data.returnValues.target).disabled = false;
+                       _buttonStatus.get(data.returnValues.pluginName + '-' + data.returnValues.target).innerHTML = data.returnValues.timeElapsed;
+                       
+                       this._syncNext();
+               },
+               
+               _ajaxFailure: function (data, responseText, xhr, requestData) {
+                       _buttons.get(requestData.parameters.pluginName + '-' + requestData.parameters.target).disabled = false;
+                       
+                       var buttonStatus = _buttonStatus.get(requestData.parameters.pluginName + '-' + requestData.parameters.target);
+                       buttonStatus.innerHTML = '<a href="#">' + Language.get('wcf.acp.devtools.sync.status.failure') + '</a>';
+                       buttonStatus.children[0].addEventListener(WCF_CLICK_EVENT, (function (event) {
+                               event.preventDefault();
+                               
+                               UiDialog.open(
+                                       this,
+                                       Ajax.getRequestObject(this).getErrorHtml(data, xhr)
+                               );
+                       }).bind(this));
+                       
+                       _buttonSyncAll.classList.remove('disabled');
+               },
+               
+               _ajaxSetup: function () {
+                       return {
+                               data: {
+                                       actionName: 'invoke',
+                                       className: 'wcf\\data\\package\\installation\\plugin\\PackageInstallationPluginAction',
+                                       parameters: {
+                                               projectID: _projectId
+                                       }
+                               }
+                       }
+               },
+               
+               _dialogSetup: function() {
+                       return {
+                               id: 'devtoolsProjectSyncPipError',
+                               options: {
+                                       title: Language.get('wcf.global.error.title')
+                               },
+                               source: null
+                       }
+               }
+       };
+});
\ No newline at end of file
index 5a8398df5d6af9ef251b8cb0433b1a4e00bad533..15a4947e9b169c6eff3b8722d348c3076e68567c 100644 (file)
@@ -14,7 +14,7 @@ define(['AjaxRequest', 'Core', 'ObjectMap'], function(AjaxRequest, Core, ObjectM
        /**
         * @exports     WoltLabSuite/Core/Ajax
         */
-       var Ajax = {
+       return {
                /**
                 * Shorthand function to perform a request against the WCF-API with overrides
                 * for success and failure callbacks.
@@ -91,9 +91,21 @@ define(['AjaxRequest', 'Core', 'ObjectMap'], function(AjaxRequest, Core, ObjectM
                        }
                        
                        var request = new AjaxRequest(options);
-                       request.sendRequest();
+                       request.sendRequest(false);
+               },
+               
+               /**
+                * Returns the request object used for an earlier call to `api()`.
+                * 
+                * @param       {Object}        callbackObject  callback object
+                * @return      {AjaxRequest}
+                */
+               getRequestObject: function(callbackObject) {
+                       if (!_requests.has(callbackObject)) {
+                               throw new Error('Expected a previously used callback object, provided object is unknown.');
+                       }
+                       
+                       return _requests.get(callbackObject);
                }
        };
-       
-       return Ajax;
 });
index 9e5a23e4de9e0a0a09674287b9f0912499674bf4..ef21f39f394878669da634ca94a9eaf935c23a7c 100644 (file)
@@ -277,29 +277,7 @@ define(['Core', 'Language', 'Dom/ChangeListener', 'Dom/Util', 'Ui/Dialog', 'Wolt
                        }
                        
                        if (options.ignoreError !== true && showError !== false) {
-                               var details = '';
-                               var message = '';
-                               
-                               if (data !== null) {
-                                       if (data.stacktrace) details = '<br><p>Stacktrace:</p><p>' + data.stacktrace + '</p>';
-                                       else if (data.exceptionID) details = '<br><p>Exception ID: <code>' + data.exceptionID + '</code></p>';
-                                       
-                                       message = data.message;
-                                       
-                                       data.previous.forEach(function(previous) {
-                                               details += '<hr><p>' + previous.message + '</p>';
-                                               details += '<br><p>Stacktrace</p><p>' + previous.stacktrace + '</p>';
-                                       });
-                               }
-                               else {
-                                       message = xhr.responseText;
-                               }
-                               
-                               if (!message || message === 'undefined') {
-                                       return;
-                               }
-                               
-                               var html = '<div class="ajaxDebugMessage"><p>' + message + '</p>' + details + '</div>';
+                               var html = this.getErrorHtml(data, xhr);
                                
                                if (UiDialog === undefined) UiDialog = require('Ui/Dialog');
                                UiDialog.openStatic(DomUtil.getUniqueId(), html, {
@@ -310,6 +288,39 @@ define(['Core', 'Language', 'Dom/ChangeListener', 'Dom/Util', 'Ui/Dialog', 'Wolt
                        this._finalize(options);
                },
                
+               /**
+                * Returns the inner HTML for an error/exception display.
+                * 
+                * @param       {Object}                data
+                * @param       {XMLHttpRequest}        xhr
+                * @return      {string}
+                */
+               getErrorHtml: function(data, xhr) {
+                       var details = '';
+                       var message = '';
+                       
+                       if (data !== null) {
+                               if (data.stacktrace) details = '<br><p>Stacktrace:</p><p>' + data.stacktrace + '</p>';
+                               else if (data.exceptionID) details = '<br><p>Exception ID: <code>' + data.exceptionID + '</code></p>';
+                               
+                               message = data.message;
+                               
+                               data.previous.forEach(function(previous) {
+                                       details += '<hr><p>' + previous.message + '</p>';
+                                       details += '<br><p>Stacktrace</p><p>' + previous.stacktrace + '</p>';
+                               });
+                       }
+                       else {
+                               message = xhr.responseText;
+                       }
+                       
+                       if (!message || message === 'undefined') {
+                               return;
+                       }
+                       
+                       return '<div class="ajaxDebugMessage"><p>' + message + '</p>' + details + '</div>';
+               },
+               
                /**
                 * Finalizes a request.
                 * 
index c78433a01dc717f78d71d4b9a1a24e637460a0a6..67f5a060f6e3215d252b7cd032e64b5ebef65c01 100644 (file)
@@ -134,7 +134,7 @@ define(
                        if (setupData.source === undefined) {
                                var dialogElement = elById(setupData.id);
                                if (dialogElement === null) {
-                                       throw new Error("Element id '" + setupData.id + "' is invalid and no source attribute was given.");
+                                       throw new Error("Element id '" + setupData.id + "' is invalid and no source attribute was given. If you want to use the `html` argument instead, please add `source: null` to your dialog configuration.");
                                }
                                
                                setupData.source = document.createDocumentFragment();
diff --git a/wcfsetup/install/files/lib/acp/form/DevtoolsProjectSyncForm.class.php b/wcfsetup/install/files/lib/acp/form/DevtoolsProjectSyncForm.class.php
deleted file mode 100644 (file)
index 67149be..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-<?php
-namespace wcf\acp\form;
-use wcf\data\devtools\project\DevtoolsProject;
-use wcf\form\AbstractForm;
-use wcf\system\exception\IllegalLinkException;
-use wcf\system\WCF;
-
-/**
- * Shows the devtools project sync form.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2017 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @package    WoltLabSuite\Core\Acp\Form
- * @since       3.1
- */
-class DevtoolsProjectSyncForm extends AbstractForm {
-       /**
-        * @inheritDoc
-        */
-       public $activeMenuItem = 'wcf.acp.menu.link.devtools.project.list';
-       
-       /**
-        * @inheritDoc
-        */
-       public $neededModules = ['ENABLE_DEVELOPER_TOOLS'];
-       
-       /**
-        * @inheritDoc
-        */
-       public $neededPermissions = ['admin.configuration.package.canInstallPackage'];
-       
-       /**
-        * project id
-        * @var integer
-        */
-       public $objectID = 0;
-       
-       /**
-        * devtools project
-        * @var DevtoolsProject
-        */
-       public $object;
-       
-       /**
-        * @inheritDoc
-        */
-       public function readParameters() {
-               parent::readParameters();
-               
-               if (isset($_REQUEST['id'])) $this->objectID = intval($_REQUEST['id']);
-               $this->object = new DevtoolsProject($this->objectID);
-               if (!$this->object->projectID) {
-                       throw new IllegalLinkException();
-               }
-       }
-       
-       /**
-        * @inheritDoc
-        */
-       public function assignVariables() {
-               parent::assignVariables();
-               
-               WCF::getTPL()->assign([
-                       'objectID' => $this->objectID,
-                       'object' => $this->object
-               ]);
-       }
-}
diff --git a/wcfsetup/install/files/lib/acp/page/DevtoolsProjectSyncPage.class.php b/wcfsetup/install/files/lib/acp/page/DevtoolsProjectSyncPage.class.php
new file mode 100644 (file)
index 0000000..152702d
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+namespace wcf\acp\form;
+use wcf\data\devtools\project\DevtoolsProject;
+use wcf\page\AbstractPage;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\WCF;
+
+/**
+ * Shows the devtools project sync form.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2017 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Acp\Page
+ * @since       3.1
+ */
+class DevtoolsProjectSyncPage extends AbstractPage {
+       /**
+        * @inheritDoc
+        */
+       public $activeMenuItem = 'wcf.acp.menu.link.devtools.project.list';
+       
+       /**
+        * @inheritDoc
+        */
+       public $neededModules = ['ENABLE_DEVELOPER_TOOLS'];
+       
+       /**
+        * @inheritDoc
+        */
+       public $neededPermissions = ['admin.configuration.package.canInstallPackage'];
+       
+       /**
+        * project id
+        * @var integer
+        */
+       public $objectID = 0;
+       
+       /**
+        * devtools project
+        * @var DevtoolsProject
+        */
+       public $object;
+       
+       /**
+        * @inheritDoc
+        */
+       public function readParameters() {
+               parent::readParameters();
+               
+               if (isset($_REQUEST['id'])) $this->objectID = intval($_REQUEST['id']);
+               $this->object = new DevtoolsProject($this->objectID);
+               if (!$this->object->projectID) {
+                       throw new IllegalLinkException();
+               }
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function assignVariables() {
+               parent::assignVariables();
+               
+               WCF::getTPL()->assign([
+                       'objectID' => $this->objectID,
+                       'object' => $this->object
+               ]);
+       }
+}
index f8e5afc2ddef24fcc4c54e2815614edbaff1b021..c8f716e91c1f9f22cfd7121a7130980c5de57050 100644 (file)
@@ -38,6 +38,11 @@ class DevtoolsProject extends DatabaseObject {
         */
        protected $packageArchive;
        
+       /**
+        * Returns a list of decorated PIPs.
+        * 
+        * @return      DevtoolsPip[]
+        */
        public function getPips() {
                $pipList = new PackageInstallationPluginList();
                $pipList->sqlOrderBy = 'pluginName';
@@ -51,6 +56,12 @@ class DevtoolsProject extends DatabaseObject {
                return $pips;
        }
        
+       /**
+        * Validates the repository and returns the first error message, or
+        * an empty string on success.
+        * 
+        * @return      string
+        */
        public function validate() {
                $errorType = self::validatePath($this->path);
                if ($errorType !== '') {
@@ -60,6 +71,11 @@ class DevtoolsProject extends DatabaseObject {
                return $this->validatePackageXml();
        }
        
+       /**
+        * Returns true if this project appears to be `WoltLab Suite Core`.
+        * 
+        * @return      boolean
+        */
        public function isCore() {
                if ($this->isCore === null) {
                        $this->isCore = self::pathIsCore($this->path);
@@ -68,6 +84,11 @@ class DevtoolsProject extends DatabaseObject {
                return $this->isCore;
        }
        
+       /**
+        * Validates the package.xml and checks if the package is already installed.
+        * 
+        * @return      string
+        */
        public function validatePackageXml() {
                $packageXml = $this->path . ($this->isCore() ? 'com.woltlab.wcf/' : '') . 'package.xml';
                $this->packageArchive = new DevtoolsPackageArchive($packageXml);
@@ -124,6 +145,12 @@ class DevtoolsProject extends DatabaseObject {
                return '';
        }
        
+       /**
+        * Returns true if the path appears to point to `WoltLab Suite Core`.
+        * 
+        * @param       string          $path
+        * @return      boolean
+        */
        public static function pathIsCore($path) {
                return (is_dir($path . 'com.woltlab.wcf') && file_exists($path . 'com.woltlab.wcf/package.xml'));
        }
index 7c7beabb5188a8dc3ae0363ddbaa3fb6120cbdce..de704dcb5d4d97fb8bbc36f98bc41f24fc0f3c4b 100644 (file)
@@ -8,7 +8,6 @@ use wcf\system\devtools\pip\DevtoolsPip;
 use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
 use wcf\system\exception\PermissionDeniedException;
 use wcf\system\exception\UserInputException;
-use wcf\system\package\plugin\IPackageInstallationPlugin;
 use wcf\system\package\SplitNodeException;
 use wcf\system\search\SearchIndexManager;
 use wcf\system\version\VersionTracker;
@@ -85,6 +84,8 @@ class PackageInstallationPluginAction extends AbstractDatabaseObjectAction {
                        'value' => $this->devtoolsPip->getInstructionValue($this->project, $this->parameters['target'])
                ]);
                
+               $start = microtime(true);
+               
                try {
                        $pip->update();
                }
@@ -102,5 +103,12 @@ class PackageInstallationPluginAction extends AbstractDatabaseObjectAction {
                VersionTracker::getInstance()->createStorageTables();
                
                CacheHandler::getInstance()->flushAll();
+               
+               
+               return [
+                       'pluginName' => $this->packageInstallationPlugin->pluginName,
+                       'target' => $this->parameters['target'],
+                       'timeElapsed' => WCF::getLanguage()->getDynamicVariable('wcf.acp.devtools.sync.status.success', ['timeElapsed' => round(microtime(true) - $start, 3)])
+               ];
        }
 }
diff --git a/wcfsetup/install/files/lib/system/devtools/package/DevtoolsInstaller.class.php b/wcfsetup/install/files/lib/system/devtools/package/DevtoolsInstaller.class.php
new file mode 100644 (file)
index 0000000..f1a1389
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+namespace wcf\system\devtools\package;
+use wcf\data\devtools\project\DevtoolsProject;
+use wcf\system\setup\Installer;
+
+/**
+ * Specialized implementation to emulate a regular package installation.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2017 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Devtools\Package
+ * @since       3.1
+ */
+class DevtoolsInstaller extends Installer {
+       /**
+        * @var DevtoolsProject
+        */
+       protected $project;
+       
+       /**
+        * @inheritDoc
+        */
+       public function __construct(DevtoolsProject $project, $targetDir, $source, $fileHandler = null, $folder = '') {
+               $this->project = $project;
+               
+               parent::__construct($targetDir, $source, $fileHandler, $folder);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getTar($source) {
+               return $this->project->getPackageArchive()->getTar();
+       }
+}
\ No newline at end of file
index 1494becc2f4d3059a214ade85eaf699c63f5f44b..2a60f8a5143ea8694288880c016356a6c963028f 100644 (file)
@@ -3,19 +3,38 @@ namespace wcf\system\devtools\package;
 use wcf\system\package\PackageArchive;
 
 /**
- * @method             DevtoolsTar     getTar()
+ * Specialized implementation to emulate a regular package installation.
+ *
+ * @author     Alexander Ebert
+ * @copyright  2001-2017 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Devtools\Package
+ * @since       3.1
+ * 
+ * @method     DevtoolsTar     getTar()
  */
 class DevtoolsPackageArchive extends PackageArchive {
        protected $packageXmlPath = '';
        
-       /** @noinspection PhpMissingParentConstructorInspection */
+       /** @noinspection PhpMissingParentConstructorInspection @inheritDoc */
        public function __construct($packageXmlPath) {
                $this->packageXmlPath = $packageXmlPath;
        }
        
+       
+       /**
+        * @inheritDoc
+        */
        public function openArchive() {
                $this->tar = new DevtoolsTar(['package.xml' => $this->packageXmlPath]);
                
                $this->readPackageInfo();
        }
+       
+       /**
+        * @inheritDoc
+        */
+       public function extractTar($filename, $tempPrefix = 'package_') {
+               return $tempPrefix . $filename . '_dummy';
+       }
 }
index 31fe02184be861e29de44456d62f8ce057021936..50caa3a608c5e2536cf1b4922ca6145c4020a2ba 100644 (file)
@@ -2,22 +2,57 @@
 namespace wcf\system\devtools\package;
 use wcf\system\io\Tar;
 
+/**
+ * Specialized implementation to emulate a regular package installation.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2017 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Devtools\Package
+ * @since       3.1
+ */
 class DevtoolsTar extends Tar {
-       protected $files = '';
+       /**
+        * list of virtual files
+        * @var string[]
+        */
+       protected $files = [];
        
-       /** @noinspection PhpMissingParentConstructorInspection */
+       /** @noinspection PhpMissingParentConstructorInspection @inheritDoc */
        public function __construct(array $files) {
                $this->files = $files;
        }
        
+       /**
+        * Resets the internal file list for re-use, because the devtools use
+        * the same instance over and over to avoid some otherwise awkward
+        * changes to the code. 
+        */
+       public function reset() {
+               $this->contentList = $this->files = [];
+               $this->read = false;
+       }
+       
+       /**
+        * Registers a new file in the virtual file list.
+        * 
+        * @param       string          $filename
+        * @param       string          $fullPath
+        */
        public function registerFile($filename, $fullPath) {
                $this->files[$filename] = $fullPath;
        }
        
+       /**
+        * @inheritDoc
+        */
        public function getIndexByFilename($filename) {
                return (isset($this->files[$filename]) ? $filename : false);
        }
        
+       /**
+        * @inheritDoc
+        */
        public function extractToString($index) {
                if (!isset($this->files[$index])) {
                        throw new \RuntimeException("DevtoolsTar does not permit reading any files except for the explicitly registered ones.");
@@ -26,7 +61,38 @@ class DevtoolsTar extends Tar {
                return file_get_contents($this->files[$index]);
        }
        
+       /**
+        * @inheritDoc
+        */
        public function extract($index, $destination) {
                copy($this->files[$index], $destination);
        }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getContentList() {
+               if (!$this->read) {
+                       foreach ($this->files as $filename => $fullPath) {
+                               if (strpos($filename, '/') !== false) {
+                                       $directory = dirname($filename) . '/';
+                                       if (!isset($this->contentList[$directory])) {
+                                               $this->contentList[$directory] = [
+                                                       'filename' => $directory,
+                                                       'type' => 'folder'
+                                               ];
+                                       }
+                               }
+                               
+                               $this->contentList[$filename] = [
+                                       'filename' => $filename,
+                                       'type' => 'file'
+                               ];
+                       }
+                       
+                       $this->read = true;
+               }
+               
+               return $this->contentList;
+       }
 }
index 484f78ed15e1c517ca5b14d421f5279898f704b4..8c09c947c9f0db323a7e6e2bd2bedf98bf65921c 100644 (file)
@@ -1,30 +1,60 @@
 <?php
 namespace wcf\system\devtools\pip;
 use wcf\data\devtools\project\DevtoolsProject;
+use wcf\system\devtools\package\DevtoolsInstaller;
 use wcf\system\devtools\package\DevtoolsPackageArchive;
 use wcf\system\package\PackageInstallationDispatcher;
 
+/**
+ * Specialized implementation to emulate a regular package installation.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2017 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Devtools\Pip
+ * @since       3.1
+ */
 class DevtoolsPackageInstallationDispatcher extends PackageInstallationDispatcher {
        /**
         * @var DevtoolsPackageArchive
         */
        protected $project;
        
+       /**
+        * @inheritDoc
+        */
        public function __construct(DevtoolsProject $project) {
                parent::__construct(new DevtoolsPackageInstallationQueue($project));
                
                $this->project = $project;
        }
        
+       /**
+        * @inheritDoc
+        */
        public function getArchive() {
                return $this->project->getPackageArchive();
        }
        
+       /**
+        * @inheritDoc
+        */
        public function getPackageID() {
                return $this->project->getPackage()->packageID;
        }
        
+       /**
+        * @inheritDoc
+        */
        public function getPackageName() {
                return $this->project->getPackage()->getName();
        }
+       
+       /**
+        * @inheritDoc
+        */
+       public function extractFiles($targetDir, $sourceArchive, $fileHandler = null) {
+               /** @noinspection PhpParamsInspection */
+               return new DevtoolsInstaller($this->project, $targetDir, $sourceArchive, $fileHandler);
+       }
 }
index 6845c88522dd97e855b432ab74ce3510b61bbc18..31ff90d0b5f74a9acde114d501ae28829d664101 100644 (file)
@@ -4,7 +4,19 @@ use wcf\data\devtools\project\DevtoolsProject;
 use wcf\data\package\installation\queue\PackageInstallationQueue;
 use wcf\system\WCF;
 
+/**
+ * Specialized implementation to emulate a regular package installation.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2017 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Devtools\Pip
+ * @since       3.1
+ */
 class DevtoolsPackageInstallationQueue extends PackageInstallationQueue {
+       /**
+        * @inheritDoc
+        */
        public function __construct(DevtoolsProject $project) {
                parent::__construct(null, [
                        'queueID' => 0,
index 54e19a197441282a6307ee9705bd104515170b11..9d778ab80ca9c0a9ce854b065ae71476abb99c92 100644 (file)
@@ -4,8 +4,9 @@ use wcf\data\devtools\project\DevtoolsProject;
 use wcf\data\package\installation\plugin\PackageInstallationPlugin;
 use wcf\data\DatabaseObjectDecorator;
 use wcf\system\application\ApplicationHandler;
-use wcf\system\exception\NotImplementedException;
 use wcf\system\WCF;
+use wcf\util\FileUtil;
+use wcf\util\JSON;
 
 /**
  * Wrapper class for package installation plugins for use with the sync feature.
@@ -52,6 +53,10 @@ class DevtoolsPip extends DatabaseObjectDecorator {
                return call_user_func([$this->getDecoratedObject()->className, 'getDefaultFilename']);
        }
        
+       public function getEffectiveDefaultFilename() {
+               return './' . preg_replace('~\.tar$~', '/', $this->getDefaultFilename());
+       }
+       
        /**
         * Returns true if the PIP exists, has a default filename and is idempotent.
         * 
@@ -61,6 +66,12 @@ class DevtoolsPip extends DatabaseObjectDecorator {
                return $this->classExists() && $this->getDefaultFilename() && $this->isIdempotent();
        }
        
+       public function getSyncDependencies($toJson = true) {
+               $dependencies = call_user_func([$this->getDecoratedObject()->className, 'getSyncDependencies']);
+               
+               return ($toJson) ? JSON::encode($dependencies) : $dependencies;
+       }
+       
        /**
         * Returns the first validation error.
         * 
@@ -162,37 +173,132 @@ class DevtoolsPip extends DatabaseObjectDecorator {
                return $targets;
        }
        
+       /**
+        * Computes and prepares the instruction value for the provided target file.
+        * 
+        * @param       DevtoolsProject         $project
+        * @param       string                  $target
+        * @return      string
+        */
        public function getInstructionValue(DevtoolsProject $project, $target) {
                $defaultFilename = $this->getDefaultFilename();
+               $pluginName = $this->getDecoratedObject()->pluginName;
+               $tar = $project->getPackageArchive()->getTar();
+               $tar->reset();
                
                if ($project->isCore()) {
-                       switch ($this->getDecoratedObject()->pluginName) {
+                       switch ($pluginName) {
                                case 'acpTemplate':
                                case 'file':
                                case 'template':
-                                       throw new NotImplementedException();
-                                       break;
+                                       if ($pluginName === 'acpTemplate' || $pluginName === 'template') {
+                                               $path = ($pluginName === 'acpTemplate') ? 'wcfsetup/install/files/acp/templates/' : 'com.woltlab.wcf/templates/';
+                                               foreach (glob($project->path . $path . '*.tpl') as $template) {
+                                                       $tar->registerFile(basename($template), FileUtil::unifyDirSeparator($template));
+                                               }
+                                       }
+                                       else {
+                                               $path = 'wcfsetup/install/files/';
+                                               
+                                               $directory = new \RecursiveDirectoryIterator($project->path . $path);
+                                               $filter = new \RecursiveCallbackFilterIterator($directory, function ($current) {
+                                                       /** @var \SplFileInfo $current */
+                                                       $filename = $current->getFilename();
+                                                       if ($filename[0] === '.') {
+                                                               // ignore dot files and files/directories starting with a dot
+                                                               return false;
+                                                       }
+                                                       else if ($filename === 'templates') {
+                                                               // ignores both `templates` and `acp/templates`
+                                                               return false;
+                                                       }
+                                                       
+                                                       return true;
+                                               });
+                                               
+                                               $iterator = new \RecursiveIteratorIterator($filter, \RecursiveIteratorIterator::SELF_FIRST);
+                                               foreach ($iterator as $value => $item) {
+                                                       /** @var \SplFileInfo $item */
+                                                       $itemPath = $item->getRealPath();
+                                                       if (is_dir($itemPath)) continue;
+                                                       
+                                                       $tar->registerFile(
+                                                               FileUtil::getRelativePath($project->path . $path, $item->getPath()) . $item->getFilename(),
+                                                               $itemPath
+                                                       );
+                                               }
+                                       }
+                                       
+                                       return $defaultFilename;
                                
                                case 'language':
                                        $filename = "wcfsetup/install/lang/{$target}";
-                                       $project->getPackageArchive()->getTar()->registerFile($filename, $project->path . $filename);
+                                       $tar->registerFile($filename, $project->path . $filename);
                                        
                                        return $filename;
                                        
                                default:
                                        $filename = "com.woltlab.wcf/{$target}";
-                                       $project->getPackageArchive()->getTar()->registerFile($filename, $project->path . $filename);
+                                       $tar->registerFile($filename, $project->path . $filename);
                                        
                                        return $filename;
                        }
                }
                else {
-                       if (strpos($defaultFilename, '*') !== false) {
-                               $filename = str_replace('*', $target, $defaultFilename);
-                               $project->getPackageArchive()->getTar()->registerFile($filename, $project->path . $filename);
+                       switch ($pluginName) {
+                               case 'acpTemplate':
+                               case 'file':
+                               case 'template':
+                                       if ($pluginName === 'acpTemplate' || $pluginName === 'template') {
+                                               $path = ($pluginName === 'acpTemplate') ? 'acptemplates/' : 'templates/';
+                                               foreach (glob($project->path . $path . '*.tpl') as $template) {
+                                                       $tar->registerFile(basename($template), FileUtil::unifyDirSeparator($template));
+                                               }
+                                       }
+                                       else {
+                                               $path = 'files/';
+                                               
+                                               $directory = new \RecursiveDirectoryIterator($project->path . $path);
+                                               $filter = new \RecursiveCallbackFilterIterator($directory, function ($current) {
+                                                       /** @var \SplFileInfo $current */
+                                                       $filename = $current->getFilename();
+                                                       if ($filename[0] === '.') {
+                                                               // ignore dot files and files/directories starting with a dot
+                                                               return false;
+                                                       }
+                                                       
+                                                       return true;
+                                               });
+                                               
+                                               $iterator = new \RecursiveIteratorIterator($filter, \RecursiveIteratorIterator::SELF_FIRST);
+                                               foreach ($iterator as $value => $item) {
+                                                       /** @var \SplFileInfo $item */
+                                                       $itemPath = $item->getRealPath();
+                                                       if (is_dir($itemPath)) continue;
+                                                       
+                                                       $tar->registerFile(
+                                                               FileUtil::getRelativePath($project->path . $path, $item->getPath()) . $item->getFilename(),
+                                                               $itemPath
+                                                       );
+                                               }
+                                       }
+                                       
+                                       return $defaultFilename;
+                               
+                               default:
+                                       if (strpos($defaultFilename, '*') !== false) {
+                                               $filename = str_replace('*', $target, $defaultFilename);
+                                               $tar->registerFile($filename, $project->path . $filename);
+                                       }
+                                       else {
+                                               $filename = "com.woltlab.wcf/{$target}";
+                                               $tar->registerFile($filename, $project->path . $filename);
+                                       }
+                                       
+                                       return $filename;
                        }
+                       
+                       
                }
-               
-               throw new NotImplementedException();
        }
 }
\ No newline at end of file
index 9022c096dd902e701ff5b7c2fe9dd1825562484c..5ad65569fdb96544791c666728703c7511beedb1 100644 (file)
@@ -20,4 +20,15 @@ use wcf\system\package\plugin\IPackageInstallationPlugin;
  * @package    WoltLabSuite\Core\System\Devtools\Pip
  * @since       3.1
  */
-interface IIdempotentPackageInstallationPlugin extends IPackageInstallationPlugin {}
+interface IIdempotentPackageInstallationPlugin extends IPackageInstallationPlugin {
+       /**
+        * Returns a list of package installation plugins that need to be
+        * executed prior to a call to this PIP.
+        * 
+        * This method is only considered for the bulk sync in the developer
+        * tools and has no impact on the regular installation process.
+        * 
+        * @return      string[]
+        */
+       public static function getSyncDependencies();
+}
index c6b61b31f4a19ea6e42cd5a96a0d3da15b0e4d71..628ce07b4d9d59db57cbd893f718552ee7d049ba 100644 (file)
@@ -86,4 +86,11 @@ class ACPSearchProviderPackageInstallationPlugin extends AbstractXMLPackageInsta
        public static function getDefaultFilename() {
                return 'acpSearchProvider.xml';
        }
+       
+       /**
+        * @inheritDoc
+        */
+       public static function getSyncDependencies() {
+               return [];
+       }
 }
index 34871b578925ee9e92675f33a1c6e5a30d744193..a9828f7a4d8b030f947a42ebe68866f94f8798a6 100644 (file)
@@ -113,4 +113,11 @@ class ACPTemplatePackageInstallationPlugin extends AbstractPackageInstallationPl
                
                return false;
        }
+       
+       /**
+        * @inheritDoc
+        */
+       public static function getSyncDependencies() {
+               return [];
+       }
 }
index 9cf09cda94fe8709e6cd5c7db2885ffd0eb2ea1f..324bca4c5752686d7f31c218a1b388440709f580 100644 (file)
@@ -88,4 +88,11 @@ abstract class AbstractMenuPackageInstallationPlugin extends AbstractXMLPackageI
                        'parameters' => $parameters
                ];
        }
+       
+       /**
+        * @inheritDoc
+        */
+       public static function getSyncDependencies() {
+               return [];
+       }
 }
index ad9959f7704220cf4829949a66a6f866cbb9977d..46e5ee4bb49909dd98396d56fe8adfbd39e1eb05 100644 (file)
@@ -322,4 +322,11 @@ abstract class AbstractOptionPackageInstallationPlugin extends AbstractXMLPackag
         * @inheritDoc
         */
        protected function findExistingItem(array $data) { }
+       
+       /**
+        * @inheritDoc
+        */
+       public static function getSyncDependencies() {
+               return [];
+       }
 }
index 8f80ceaa62267f6174bfd58fc4f1a1f56b149615..faf2e3b04a67ae8156a513c5cbd8322782053bc2 100644 (file)
@@ -196,4 +196,11 @@ class BBCodePackageInstallationPlugin extends AbstractXMLPackageInstallationPlug
        public static function getDefaultFilename() {
                return 'bbcode.xml';
        }
+       
+       /**
+        * @inheritDoc
+        */
+       public static function getSyncDependencies() {
+               return [];
+       }
 }
index 5266bc02d81af265307aedfbfca1548a1f065468..fc93485d2ba5e55a97e1e8fc3d9afba58519d022 100644 (file)
@@ -391,4 +391,11 @@ class BoxPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin
                        }
                }
        }
+       
+       /**
+        * @inheritDoc
+        */
+       public static function getSyncDependencies() {
+               return ['language'];
+       }
 }
index 1f8bce012b24909d0c82982e58551bde7bcabeaa..4a5de19fb1c4d6fbf5f46678d16c498ab56dc4e9 100644 (file)
@@ -147,4 +147,11 @@ class ClipboardActionPackageInstallationPlugin extends AbstractXMLPackageInstall
                        }
                }
        }
+       
+       /**
+        * @inheritDoc
+        */
+       public static function getSyncDependencies() {
+               return [];
+       }
 }
index 1306e68363fc50d1941e965414d3f6d17a04de11..f29651f95bc22e70cfb4d4de7e25ad768a47160c 100644 (file)
@@ -69,4 +69,11 @@ class CoreObjectPackageInstallationPlugin extends AbstractXMLPackageInstallation
        protected function cleanup() {
                CoreObjectCacheBuilder::getInstance()->reset();
        }
+       
+       /**
+        * @inheritDoc
+        */
+       public static function getSyncDependencies() {
+               return [];
+       }
 }
index b83aefd1b4caf66d86c73605ec8204963d7b5b39..a3a24f6e895392523321dded124a101aaae87447 100644 (file)
@@ -149,4 +149,11 @@ class CronjobPackageInstallationPlugin extends AbstractXMLPackageInstallationPlu
                
                $data['nextExec'] = TIME_NOW;
        }
+       
+       /**
+        * @inheritDoc
+        */
+       public static function getSyncDependencies() {
+               return [];
+       }
 }
index ee1d778e05c7e76a09e8430b149da82a2e654bba..740cd912720ff9a61e7b3552100e3ae99cfc7188 100644 (file)
@@ -157,4 +157,12 @@ class EventListenerPackageInstallationPlugin extends AbstractXMLPackageInstallat
                // clear cache immediately
                EventListenerCacheBuilder::getInstance()->reset();
        }
+       
+       /**
+        * @inheritDoc
+        */
+       public static function getSyncDependencies() {
+               return [];
+       }
+       
 }
index a0501a74893955a35db61e45bf13ec706fbe9d0e..40da096c40cd7acfec9a3dbf0df5f3862ebc43ec 100644 (file)
@@ -137,4 +137,11 @@ class FilePackageInstallationPlugin extends AbstractPackageInstallationPlugin im
                
                return false;
        }
+       
+       /**
+        * @inheritDoc
+        */
+       public static function getSyncDependencies() {
+               return [];
+       }
 }
index 3813e55ed46f2425a08d59f59d05e07c13d60d57..ef9003f34fbc0f73a7f7502d03cd678a914ab303 100644 (file)
@@ -262,4 +262,11 @@ class LanguagePackageInstallationPlugin extends AbstractXMLPackageInstallationPl
        public static function isValid(PackageArchive $archive, $instruction) {
                return true;
        }
+       
+       /**
+        * @inheritDoc
+        */
+       public static function getSyncDependencies() {
+               return [];
+       }
 }
index b2e5099c0e7a88ec9883dc90ac95331af1e0bf93..2f6ad07e7dc7237826f843c78a1bc267b31795c9 100644 (file)
@@ -203,4 +203,11 @@ class MenuItemPackageInstallationPlugin extends AbstractXMLPackageInstallationPl
                
                return (!$row['showOrder']) ? 1 : $row['showOrder'] + 1;
        }
+       
+       /**
+        * @inheritDoc
+        */
+       public static function getSyncDependencies() {
+               return ['language'];
+       }
 }
index f2f1563a8ac3ed7d648d89d52494b7327923234e..a5d876270534f39c5028bb57bb27f95c334f2be6 100644 (file)
@@ -265,4 +265,11 @@ class MenuPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin
                        }
                }
        }
+       
+       /**
+        * @inheritDoc
+        */
+       public static function getSyncDependencies() {
+               return ['language'];
+       }
 }
index 6fa72676e1d950c8d5b3ed81d33ca7072a7d129e..87a33b9952470b952d55634c92ea616aee143da4 100644 (file)
@@ -64,4 +64,11 @@ class ObjectTypeDefinitionPackageInstallationPlugin extends AbstractXMLPackageIn
                        'parameters' => $parameters
                ];
        }
+       
+       /**
+        * @inheritDoc
+        */
+       public static function getSyncDependencies() {
+               return [];
+       }
 }
index f0597d50d6001a90c8e161fc1b1504b02226e3e9..527317f3955def3b8e1137828f3cd24dc701bca4 100644 (file)
@@ -104,4 +104,11 @@ class ObjectTypePackageInstallationPlugin extends AbstractXMLPackageInstallation
                        'parameters' => $parameters
                ];
        }
+       
+       /**
+        * @inheritDoc
+        */
+       public static function getSyncDependencies() {
+               return ['objectTypeDefinition'];
+       }
 }
index e033c3815906d88af6d4c9cd6aee5b0156ae6bdc..feef16a0def30bee61387d375733fa840599a2ed 100644 (file)
@@ -76,4 +76,11 @@ class PIPPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin
                        'parameters' => $parameters
                ];
        }
+       
+       /**
+        * @inheritDoc
+        */
+       public static function getSyncDependencies() {
+               return [];
+       }
 }
index 7daf6a3f327f4daf5997bf0f3bcfed4025317dfc..b954e02352798755e8bb40e3664cc3fe7b485334 100644 (file)
@@ -355,4 +355,11 @@ class PagePackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin
                        }
                }
        }
+       
+       /**
+        * @inheritDoc
+        */
+       public static function getSyncDependencies() {
+               return ['language'];
+       }
 }
index 596a6cf304ff8f5559d88d3e4eafb4570319d5e7..ceb5dae0e6b954964518a4aae89c81fbfec09888 100644 (file)
@@ -78,4 +78,11 @@ class SmileyPackageInstallationPlugin extends AbstractXMLPackageInstallationPlug
                        'parameters' => $parameters
                ];
        }
+       
+       /**
+        * @inheritDoc
+        */
+       public static function getSyncDependencies() {
+               return [];
+       }
 }
index a3b3d9fcac62770546d04521177ebec60174b131..151f2cde94ee1ee5761208e409865a65c1c237e5 100644 (file)
@@ -98,4 +98,11 @@ class TemplateListenerPackageInstallationPlugin extends AbstractXMLPackageInstal
                // clear cache immediately
                TemplateListenerCodeCacheBuilder::getInstance()->reset();
        }
+       
+       /**
+        * @inheritDoc
+        */
+       public static function getSyncDependencies() {
+               return [];
+       }
 }
index fcf275bfae2c6f4f9708f231183f9f5106ee0554..61302b2aeaa5aa59d0bc78ab6dea933683c1614f 100644 (file)
@@ -115,4 +115,11 @@ class TemplatePackageInstallationPlugin extends AbstractPackageInstallationPlugi
                
                return false;
        }
+       
+       /**
+        * @inheritDoc
+        */
+       public static function getSyncDependencies() {
+               return [];
+       }
 }
index ec90935e0021923a8e0757aa21842f2cb5e4770b..ad4934aaf323ed0f3ec0bd2c09babab13e31fa36 100644 (file)
@@ -131,6 +131,7 @@ class UserNotificationEventPackageInstallationPlugin extends AbstractXMLPackageI
         *
         * @param       string          $objectType
         * @return      integer
+        * @throws      SystemException
         */
        protected function getObjectTypeID($objectType) {
                // get object type id
@@ -148,4 +149,11 @@ class UserNotificationEventPackageInstallationPlugin extends AbstractXMLPackageI
                if (empty($row['objectTypeID'])) throw new SystemException("unknown notification object type '".$objectType."' given");
                return $row['objectTypeID'];
        }
+       
+       /**
+        * @inheritDoc
+        */
+       public static function getSyncDependencies() {
+               return [];
+       }
 }
index 23ba0ca631b6e8f289cda59290e8842864d2799f..cb94b961f65f74edf2b74a6dbe361ce3f636490b 100644 (file)
@@ -81,4 +81,11 @@ class UserProfileMenuPackageInstallationPlugin extends AbstractXMLPackageInstall
                        'parameters' => $parameters
                ];
        }
+       
+       /**
+        * @inheritDoc
+        */
+       public static function getSyncDependencies() {
+               return [];
+       }
 }
index 6e9bb02cd108bd28f814e1f426ad53da57a89e66..a399bac31c83a0262d1d9af001b7345775856d56 100644 (file)
@@ -118,7 +118,7 @@ class Installer {
                $this->createTargetDir();
                
                // open source archive
-               $tar = new Tar($this->source);
+               $tar = $this->getTar($this->source);
                
                // distinct directories and files
                $directories = [];
@@ -173,6 +173,16 @@ class Installer {
                $tar->close();
        }
        
+       /**
+        * Opens a new tar archive.
+        * 
+        * @param       string          $source
+        * @return      Tar
+        */
+       protected function getTar($source) {
+               return new Tar($source);
+       }
+       
        /**
         * Checks whether the given files overwriting locked existing files.
         * 
index dfeadd24f352ece89f1db674e281f946f6785b05..c016a0b1bf4c0dcb03d602beeb13cdf3ead338dc 100644 (file)
                <item name="wcf.acp.devtools.pip.showOnlyMatches.description"><![CDATA[Es werden nur PIPs angeboten, die für den wiederholten Import geeignet sind und für die es eine Entsprechung auf Basis des Standard-Dateinamens existiert.]]></item>
                <item name="wcf.acp.devtools.pip.target"><![CDATA[Übereinstimmungen]]></item>
                <item name="wcf.acp.devtools.pip.target.noMatches"><![CDATA[(Keine Treffer)]]></item>
+               <item name="wcf.acp.devtools.sync.status.failure"><![CDATA[Es ist ein Fehler aufgetreten.]]></item>
+               <item name="wcf.acp.devtools.sync.status.idle"><![CDATA[Bereit.]]></item>
+               <item name="wcf.acp.devtools.sync.status.success"><![CDATA[{$timeElapsed}s ({@TIME_NOW|time})]]></item>
+               <item name="wcf.acp.devtools.sync.syncAll"><![CDATA[Alles Abgleichen]]></item>
        </category>
        
        <category name="wcf.acp.exceptionLog">
                <item name="wcf.acp.menu.link.trophy.add"><![CDATA[Trophäe hinzufügen]]></item>
                <item name="wcf.acp.menu.link.trophy.edit"><![CDATA[Trophäe bearbeiten]]></item>
                <item name="wcf.acp.menu.link.devtools"><![CDATA[Entwickler-Werkzeuge]]></item>
-               <item name="wcf.acp.menu.link.devtools.project.list"><![CDATA[Projekte auflisten]]></item>
+               <item name="wcf.acp.menu.link.devtools.project.list"><![CDATA[Projekte]]></item>
        </category>
        
        <category name="wcf.acp.notice">
@@ -1333,7 +1337,7 @@ GmbH=Gesellschaft mit beschränkter Haftung]]></item>
                <item name="wcf.acp.option.module_contact_form"><![CDATA[Kontakt-Formular aktivieren]]></item>
                <item name="wcf.acp.option.module_contact_form.description"><![CDATA[Aktiviert das Kontakt-Formular, nach Aktivierung können Sie die <a href="{link controller='ContactSettings'}{/link}">Eingabefelder und Empfänger</a> individuell konfigurieren.]]></item>
                <item name="wcf.acp.option.module_trophy"><![CDATA[Trophäen]]></item>
-               <item name="wcf.acp.option.category.module.developer"><![CDATA[Entwickler]]></item>
+               <item name="wcf.acp.option.category.module.development"><![CDATA[Entwicklung]]></item>
                <item name="wcf.acp.option.enable_developer_tools"><![CDATA[Entwickler-Werkzeuge aktivieren]]></item>
                <item name="wcf.acp.option.enable_developer_tools.description"><![CDATA[Aktiviert spezielle Werkzeuge die für die Plugin-Entwicklung verwendet werden.]]></item>
        </category>
index 5d9b486b2aa0b75c5b3609dfb93ef6784bf3153c..8bc1bb061a5068c6856a400a3d74ccd4b5859625 100644 (file)
                <item name="wcf.acp.dataImport.started"><![CDATA[Import started.]]></item>
        </category>
        
+       <category name="wcf.acp.devtools">
+               <item name="wcf.acp.devtools.project.action"><![CDATA[Actions]]></item>
+               <item name="wcf.acp.devtools.project.add"><![CDATA[Add Project]]></item>
+               <item name="wcf.acp.devtools.project.edit"><![CDATA[Edit Project]]></item>
+               <item name="wcf.acp.devtools.project.list"><![CDATA[Projects]]></item>
+               <item name="wcf.acp.devtools.project.name"><![CDATA[Name]]></item>
+               <item name="wcf.acp.devtools.project.path"><![CDATA[Path]]></item>
+               <item name="wcf.acp.devtools.project.sync"><![CDATA[Sync Data]]></item>
+               <item name="wcf.acp.devtools.pip.defaultFilename"><![CDATA[Searchpattern]]></item>
+               <item name="wcf.acp.devtools.pip.error.notIdempotent"><![CDATA[This PIP does not support repeated imports and can only be processed in regular updates.]]></item>
+               <item name="wcf.acp.devtools.pip.notice"><![CDATA[Any existing instructions in the <kbd>package.xml</kbd> will be ignored; This allows the import of PIPs that have no specific instructions provided for them yet. Only the suggested default paths are recognized, with an additional support for application suffixes for <kbd>.tar</kbd>-archives (e. g. <kbd>files_wcf.tar</kbd>) are supported.]]></item>
+               <item name="wcf.acp.devtools.pip.pluginName"><![CDATA[PIP identifier]]></item>
+               <item name="wcf.acp.devtools.pip.showOnlyMatches"><![CDATA[Show valid PIPs only]]></item>
+               <item name="wcf.acp.devtools.pip.showOnlyMatches.description"><![CDATA[Shows only PIPs that match the default paths and are qualified for repeated imports.]]></item>
+               <item name="wcf.acp.devtools.pip.target"><![CDATA[Matches]]></item>
+               <item name="wcf.acp.devtools.pip.target.noMatches"><![CDATA[(No matches)]]></item>
+               <item name="wcf.acp.devtools.sync.status.failure"><![CDATA[An error has occurred.]]></item>
+               <item name="wcf.acp.devtools.sync.status.idle"><![CDATA[Ready.]]></item>
+               <item name="wcf.acp.devtools.sync.status.success"><![CDATA[{$timeElapsed}s ({@TIME_NOW|time})]]></item>
+               <item name="wcf.acp.devtools.sync.syncAll"><![CDATA[Sync All]]></item>
+       </category>
+       
        <category name="wcf.acp.exceptionLog">
                <item name="wcf.acp.exceptionLog"><![CDATA[Logged errors]]></item>
                <item name="wcf.acp.exceptionLog.exception.message"><![CDATA[Error Message]]></item>
                <item name="wcf.acp.menu.link.trophy.list"><![CDATA[Trophies]]></item>
                <item name="wcf.acp.menu.link.trophy.add"><![CDATA[Add Trophy]]></item>
                <item name="wcf.acp.menu.link.trophy.edit"><![CDATA[Edit Trophy]]></item>
+               <item name="wcf.acp.menu.link.devtools"><![CDATA[Developer Tools]]></item>
+               <item name="wcf.acp.menu.link.devtools.project.list"><![CDATA[Projects]]></item>
        </category>
        
        <category name="wcf.acp.notice">
                <item name="wcf.acp.option.module_contact_form"><![CDATA[Enable contact form]]></item>
                <item name="wcf.acp.option.module_contact_form.description"><![CDATA[Enables the contact form, once enabled you can customize the <a href="{link controller='ContactSettings'}{/link}">input fields and recipients</a> to better suit your needs.]]></item>
                <item name="wcf.acp.option.module_trophy"><![CDATA[Trophies]]></item>
+               <item name="wcf.acp.option.category.module.development"><![CDATA[Development]]></item>
+               <item name="wcf.acp.option.enable_developer_tools"><![CDATA[Enable developer tools]]></item>
+               <item name="wcf.acp.option.enable_developer_tools.description"><![CDATA[Enables a set of specialized tools that are used for plugin development.]]></item>
        </category>
        
        <category name="wcf.acp.customOption">