<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>
{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>
--- /dev/null
+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
/**
* @exports WoltLabSuite/Core/Ajax
*/
- var Ajax = {
+ return {
/**
* Shorthand function to perform a request against the WCF-API with overrides
* for success and failure callbacks.
}
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;
});
}
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, {
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.
*
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();
+++ /dev/null
-<?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
- ]);
- }
-}
--- /dev/null
+<?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
+ ]);
+ }
+}
*/
protected $packageArchive;
+ /**
+ * Returns a list of decorated PIPs.
+ *
+ * @return DevtoolsPip[]
+ */
public function getPips() {
$pipList = new PackageInstallationPluginList();
$pipList->sqlOrderBy = 'pluginName';
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 !== '') {
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);
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);
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'));
}
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;
'value' => $this->devtoolsPip->getInstructionValue($this->project, $this->parameters['target'])
]);
+ $start = microtime(true);
+
try {
$pip->update();
}
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)])
+ ];
}
}
--- /dev/null
+<?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
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';
+ }
}
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.");
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;
+ }
}
<?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);
+ }
}
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,
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.
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.
*
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.
*
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
* @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();
+}
public static function getDefaultFilename() {
return 'acpSearchProvider.xml';
}
+
+ /**
+ * @inheritDoc
+ */
+ public static function getSyncDependencies() {
+ return [];
+ }
}
return false;
}
+
+ /**
+ * @inheritDoc
+ */
+ public static function getSyncDependencies() {
+ return [];
+ }
}
'parameters' => $parameters
];
}
+
+ /**
+ * @inheritDoc
+ */
+ public static function getSyncDependencies() {
+ return [];
+ }
}
* @inheritDoc
*/
protected function findExistingItem(array $data) { }
+
+ /**
+ * @inheritDoc
+ */
+ public static function getSyncDependencies() {
+ return [];
+ }
}
public static function getDefaultFilename() {
return 'bbcode.xml';
}
+
+ /**
+ * @inheritDoc
+ */
+ public static function getSyncDependencies() {
+ return [];
+ }
}
}
}
}
+
+ /**
+ * @inheritDoc
+ */
+ public static function getSyncDependencies() {
+ return ['language'];
+ }
}
}
}
}
+
+ /**
+ * @inheritDoc
+ */
+ public static function getSyncDependencies() {
+ return [];
+ }
}
protected function cleanup() {
CoreObjectCacheBuilder::getInstance()->reset();
}
+
+ /**
+ * @inheritDoc
+ */
+ public static function getSyncDependencies() {
+ return [];
+ }
}
$data['nextExec'] = TIME_NOW;
}
+
+ /**
+ * @inheritDoc
+ */
+ public static function getSyncDependencies() {
+ return [];
+ }
}
// clear cache immediately
EventListenerCacheBuilder::getInstance()->reset();
}
+
+ /**
+ * @inheritDoc
+ */
+ public static function getSyncDependencies() {
+ return [];
+ }
+
}
return false;
}
+
+ /**
+ * @inheritDoc
+ */
+ public static function getSyncDependencies() {
+ return [];
+ }
}
public static function isValid(PackageArchive $archive, $instruction) {
return true;
}
+
+ /**
+ * @inheritDoc
+ */
+ public static function getSyncDependencies() {
+ return [];
+ }
}
return (!$row['showOrder']) ? 1 : $row['showOrder'] + 1;
}
+
+ /**
+ * @inheritDoc
+ */
+ public static function getSyncDependencies() {
+ return ['language'];
+ }
}
}
}
}
+
+ /**
+ * @inheritDoc
+ */
+ public static function getSyncDependencies() {
+ return ['language'];
+ }
}
'parameters' => $parameters
];
}
+
+ /**
+ * @inheritDoc
+ */
+ public static function getSyncDependencies() {
+ return [];
+ }
}
'parameters' => $parameters
];
}
+
+ /**
+ * @inheritDoc
+ */
+ public static function getSyncDependencies() {
+ return ['objectTypeDefinition'];
+ }
}
'parameters' => $parameters
];
}
+
+ /**
+ * @inheritDoc
+ */
+ public static function getSyncDependencies() {
+ return [];
+ }
}
}
}
}
+
+ /**
+ * @inheritDoc
+ */
+ public static function getSyncDependencies() {
+ return ['language'];
+ }
}
'parameters' => $parameters
];
}
+
+ /**
+ * @inheritDoc
+ */
+ public static function getSyncDependencies() {
+ return [];
+ }
}
// clear cache immediately
TemplateListenerCodeCacheBuilder::getInstance()->reset();
}
+
+ /**
+ * @inheritDoc
+ */
+ public static function getSyncDependencies() {
+ return [];
+ }
}
return false;
}
+
+ /**
+ * @inheritDoc
+ */
+ public static function getSyncDependencies() {
+ return [];
+ }
}
*
* @param string $objectType
* @return integer
+ * @throws SystemException
*/
protected function getObjectTypeID($objectType) {
// get object type id
if (empty($row['objectTypeID'])) throw new SystemException("unknown notification object type '".$objectType."' given");
return $row['objectTypeID'];
}
+
+ /**
+ * @inheritDoc
+ */
+ public static function getSyncDependencies() {
+ return [];
+ }
}
'parameters' => $parameters
];
}
+
+ /**
+ * @inheritDoc
+ */
+ public static function getSyncDependencies() {
+ return [];
+ }
}
$this->createTargetDir();
// open source archive
- $tar = new Tar($this->source);
+ $tar = $this->getTar($this->source);
// distinct directories and files
$directories = [];
$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.
*
<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">
<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>
<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">