<icon>fa-plus</icon>
</acpmenuitem>
<!-- /other -->
+
+ <!-- devtools -->
+ <acpmenuitem name="wcf.acp.menu.link.devtools">
+ <parent>wcf.acp.menu.link.configuration</parent>
+ <showorder>99</showorder>
+ <options>enable_developer_tools</options>
+ </acpmenuitem>
+
+ <acpmenuitem name="wcf.acp.menu.link.devtools.project.list">
+ <parent>wcf.acp.menu.link.devtools</parent>
+ <controller>wcf\acp\page\DevtoolsProjectListPage</controller>
+ <permissions>admin.configuration.package.canInstallPackage</permissions>
+ </acpmenuitem>
+ <acpmenuitem name="wcf.acp.menu.link.devtools.project.add">
+ <parent>wcf.acp.menu.link.devtools.project.list</parent>
+ <controller>wcf\acp\form\DevtoolsProjectAddForm</controller>
+ <permissions>admin.configuration.package.canInstallPackage</permissions>
+ <icon>fa-plus</icon>
+ </acpmenuitem>
+ <!-- /devtools -->
<!-- /configuration -->
<category name="module.community">
<parent>module</parent>
</category>
+ <category name="module.developer">
+ <parent>module</parent>
+ </category>
<!-- /modules -->
<!-- general -->
<defaultvalue>0</defaultvalue>
</option>
- <option name="enable_debug_mode">
- <categoryname>module.system</categoryname>
- <optiontype>boolean</optiontype>
- <defaultvalue>0</defaultvalue>
- </option>
-
- <option name="enable_benchmark">
- <categoryname>module.system</categoryname>
- <optiontype>boolean</optiontype>
- <defaultvalue>0</defaultvalue>
- </option>
-
<option name="visitor_use_tiny_build">
<categoryname>module.system</categoryname>
<optiontype>boolean</optiontype>
<defaultvalue>1</defaultvalue>
</option>
- <!--
- <option name="enable_pluginstore_widget">
- <categoryname>module.system</categoryname>
- <optiontype>boolean</optiontype>
- <defaultvalue>0</defaultvalue>
- </option>
- -->
-
<option name="module_system_recaptcha"><!-- @deprecated -->
<categoryname>module.system</categoryname>
<optiontype>boolean</optiontype>
<defaultvalue>0</defaultvalue>
</option>
+ <option name="enable_debug_mode">
+ <categoryname>module.developer</categoryname>
+ <optiontype>boolean</optiontype>
+ <defaultvalue>0</defaultvalue>
+ </option>
+ <option name="enable_benchmark">
+ <categoryname>module.developer</categoryname>
+ <optiontype>boolean</optiontype>
+ <defaultvalue>0</defaultvalue>
+ </option>
+ <option name="enable_developer_tools">
+ <categoryname>module.developer</categoryname>
+ <optiontype>boolean</optiontype>
+ <defaultvalue>0</defaultvalue>
+ </option>
+
<!-- general.page -->
<option name="page_title">
<categoryname>general.page</categoryname>
define('MODULE_CONTACT_FORM', 0);
define('SITEMAP_INDEX_TIME_FRAME', 365);
define('MODULE_TROPHY', 1);
+define('ENABLE_DEVELOPER_TOOLS', 0);
--- /dev/null
+{include file='header' pageTitle='wcf.acp.devtools.project.'|concat:$action}
+
+<header class="contentHeader">
+ <div class="contentHeaderTitle">
+ <h1 class="contentTitle">{lang}wcf.acp.devtools.project.{$action}{/lang}</h1>
+ </div>
+
+ <nav class="contentHeaderNavigation">
+ <ul>
+ <li><a href="{link controller='DevtoolsProjectList'}{/link}" class="button"><span class="icon icon16 fa-list"></span> <span>{lang}wcf.acp.menu.link.devtools.project.list{/lang}</span></a></li>
+
+ {event name='contentHeaderNavigation'}
+ </ul>
+ </nav>
+</header>
+
+{include file='formError'}
+
+{if $success|isset}
+ <p class="success">{lang}wcf.global.success.{$action}{/lang}</p>
+{/if}
+
+<form method="post" action="{if $action == 'add'}{link controller='DevtoolsProjectAdd'}{/link}{else}{link controller='DevtoolsProjectEdit' id=$objectID}{/link}{/if}">
+ <div class="section">
+ <dl{if $errorField == 'name'} class="formError"{/if}>
+ <dt><label for="name">{lang}wcf.acp.devtools.project.name{/lang}</label></dt>
+ <dd>
+ <input type="text" id="name" name="name" value="{$name}" required autofocus class="long">
+ {if $errorField == 'name'}
+ <small class="innerError">
+ {if $errorType == 'empty'}
+ {lang}wcf.global.form.error.empty{/lang}
+ {else}
+ {lang}wcf.acp.devtools.project.name.error.{@$errorType}{/lang}
+ {/if}
+ </small>
+ {/if}
+ </dd>
+ </dl>
+
+ <dl{if $errorField == 'path'} class="formError"{/if}>
+ <dt><label for="path">{lang}wcf.acp.devtools.project.path{/lang}</label></dt>
+ <dd>
+ <input type="text" id="path" name="path" value="{$path}" class="long">
+ {if $errorField == 'path'}
+ <small class="innerError">
+ {if $errorType == 'empty'}
+ {lang}wcf.global.form.error.{@$errorType}{/lang}
+ {else}
+ {lang}wcf.acp.devtools.project.path.error.{@$errorType}{/lang}
+ {/if}
+ </small>
+ {/if}
+ </dd>
+ </dl>
+
+ {event name='dataFields'}
+ </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>
+
+{include file='footer'}
--- /dev/null
+{include file='header' pageTitle='wcf.acp.devtools.project.list'}
+
+<script data-relocate="true">
+ $(function() {
+ new WCF.Action.Delete('wcf\\data\\cronjob\\CronjobAction', '.jsObjectRow');
+ });
+</script>
+
+<header class="contentHeader">
+ <div class="contentHeaderTitle">
+ <h1 class="contentTitle">{lang}wcf.acp.devtools.project.list{/lang}</h1>
+ </div>
+
+ <nav class="contentHeaderNavigation">
+ <ul>
+ <li><a href="{link controller='DevtoolsProjectAdd'}{/link}" class="button"><span class="icon icon16 fa-plus"></span> <span>{lang}wcf.acp.devtools.project.add{/lang}</span></a></li>
+
+ {event name='contentHeaderNavigation'}
+ </ul>
+ </nav>
+</header>
+
+{hascontent}
+ <div class="section tabularBox">
+ <table class="table">
+ <thead>
+ <tr>
+ <th class="columnID" colspan="2">{lang}wcf.global.objectID{/lang}</th>
+ <th class="columnText">{lang}wcf.acp.devtools.project.name{/lang}</th>
+ <th class="columnText">{lang}wcf.acp.devtools.project.path{/lang}</th>
+ <th class="columnIcon">{lang}wcf.acp.devtools.project.action{/lang}</th>
+
+ {event name='columnHeads'}
+ </tr>
+ </thead>
+
+ <tbody>
+ {content}
+ {foreach from=$objects item=object}
+ <tr class="jsObjectRow">
+ <td class="columnIcon">
+ <a href="{link controller='DevtoolsProjectEdit' id=$object->getObjectID()}{/link}" title="{lang}wcf.global.button.edit{/lang}" class="jsTooltip"><span class="icon icon16 fa-pencil"></span></a>
+ <span class="icon icon16 fa-times jsDeleteButton jsTooltip pointer" title="{lang}wcf.global.button.delete{/lang}" data-object-id="{@$object->getObjectID()}" data-confirm-message-html="{lang __encode=true}wcf.acp.devtools.project.delete.confirmMessage{/lang}"></span>
+ </td>
+ <td class="columnID">{@$object->getObjectID()}</td>
+ <td class="columnText"><a href="{link controller='DevtoolsProjectEdit' id=$object->getObjectID()}{/link}">{$object->name}</a></td>
+ <td class="columnText"><small>{$object->path}</small></td>
+ <td class="columnIcon">
+ <a href="{link controller='DevtoolsProjectSync' id=$object->getObjectID()}{/link}" class="button small">{lang}wcf.acp.devtools.project.sync{/lang}</a>
+ </td>
+ </tr>
+ {/foreach}
+ {/content}
+ </tbody>
+ </table>
+ </div>
+{hascontentelse}
+ <p class="info">{lang}wcf.global.noItems{/lang}</p>
+{/hascontent}
+
+<footer class="contentFooter">
+ <nav class="contentFooterNavigation">
+ <ul>
+ <li><a href="{link controller='DevtoolsProjectAdd'}{/link}" class="button"><span class="icon icon16 fa-plus"></span> <span>{lang}wcf.acp.devtools.project.add{/lang}</span></a></li>
+
+ {event name='contentFooterNavigation'}
+ </ul>
+ </nav>
+</footer>
+
+{include file='footer'}
--- /dev/null
+{include file='header' pageTitle='wcf.acp.devtools.project.sync'}
+
+<header class="contentHeader">
+ <div class="contentHeaderTitle">
+ <h1 class="contentTitle">{lang}wcf.acp.devtools.project.sync{/lang}</h1>
+ <p class="contentHeaderDescription">{$object->name}</p>
+ </div>
+
+ <nav class="contentHeaderNavigation">
+ <ul>
+ <li><a href="{link controller='DevtoolsProjectList'}{/link}" class="button"><span class="icon icon16 fa-list"></span> <span>{lang}wcf.acp.menu.link.devtools.project.list{/lang}</span></a></li>
+
+ {event name='contentHeaderNavigation'}
+ </ul>
+ </nav>
+</header>
+
+{include file='formError'}
+
+{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>
+ {else}
+ <td class="columnText" colspan="2">{$pip->getFirstError()}</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>
+
+ <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')
+ }
+ });
+ });
+ });
+ });
+ </script>
+
+ <style>
+ #syncPipMatches.jsShowOnlyMatches tbody > tr:not(.jsHasPipTargets) {
+ display: none;
+ }
+ </style>
+{else}
+ <p class="error">{$object->validate()}</p>
+{/if}
+
+{include file='footer'}
--- /dev/null
+<?php
+namespace wcf\acp\form;
+use wcf\data\devtools\project\DevtoolsProject;
+use wcf\data\devtools\project\DevtoolsProjectAction;
+use wcf\form\AbstractForm;
+use wcf\system\exception\UserInputException;
+use wcf\system\WCF;
+use wcf\util\FileUtil;
+use wcf\util\StringUtil;
+
+/**
+ * Shows the devtools project add 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 DevtoolsProjectAddForm extends AbstractForm {
+ /**
+ * @inheritDoc
+ */
+ public $activeMenuItem = 'wcf.acp.menu.link.devtools.project.add';
+
+ /**
+ * @inheritDoc
+ */
+ public $neededModules = ['ENABLE_DEVELOPER_TOOLS'];
+
+ /**
+ * @inheritDoc
+ */
+ public $neededPermissions = ['admin.configuration.package.canInstallPackage'];
+
+ /**
+ * cronjob class name
+ * @var string
+ */
+ public $name = '';
+
+ /**
+ * cronjob path
+ * @var string
+ */
+ public $path = '';
+
+ /**
+ * @inheritDoc
+ */
+ public function readFormParameters() {
+ parent::readFormParameters();
+
+ if (isset($_POST['name'])) $this->name = StringUtil::trim($_POST['name']);
+ if (isset($_POST['path'])) $this->path = StringUtil::trim($_POST['path']);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function validate() {
+ parent::validate();
+
+ // validate name
+ if (empty($this->name)) {
+ throw new UserInputException('name');
+ }
+ else {
+ $sql = "SELECT COUNT(*) AS count
+ FROM wcf".WCF_N."_devtools_project
+ WHERE name = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute([$this->name]);
+ $count = $statement->fetchColumn();
+ if ($count > 0) {
+ throw new UserInputException('name', 'notUnique');
+ }
+ }
+
+ // validate path
+ if (empty($this->path)) {
+ throw new UserInputException('path');
+ }
+ else {
+ $path = FileUtil::addTrailingSlash(FileUtil::unifyDirSeparator($this->path));
+ $errorType = DevtoolsProject::validatePath($path);
+ if ($errorType !== '') {
+ throw new UserInputException('path', $errorType);
+ }
+
+ $this->path = $path;
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function save() {
+ parent::save();
+
+ // save cronjob
+ $data = array_merge($this->additionalFields, [
+ 'name' => $this->name,
+ 'path' => $this->path
+ ]);
+
+ $this->objectAction = new DevtoolsProjectAction([], 'create', ['data' => $data]);
+ $this->objectAction->executeAction();
+
+ $this->saved();
+
+ // reset values
+ $this->name = $this->path = '';
+
+ // show success message
+ WCF::getTPL()->assign('success', true);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function assignVariables() {
+ parent::assignVariables();
+
+ WCF::getTPL()->assign([
+ 'name' => $this->name,
+ 'path' => $this->path,
+ 'action' => 'add'
+ ]);
+ }
+}
--- /dev/null
+<?php
+namespace wcf\acp\form;
+use wcf\data\devtools\project\DevtoolsProject;
+use wcf\data\devtools\project\DevtoolsProjectAction;
+use wcf\form\AbstractForm;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\WCF;
+
+/**
+ * Shows the devtools project edit 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 DevtoolsProjectEditForm extends DevtoolsProjectAddForm {
+ /**
+ * @inheritDoc
+ */
+ public $activeMenuItem = 'wcf.acp.menu.link.devtools.project.list';
+
+ /**
+ * 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 readData() {
+ parent::readData();
+
+ if (empty($_POST)) {
+ $this->name = $this->object->name;
+ $this->path = $this->object->path;
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function save() {
+ AbstractForm::save();
+
+ // update cronjob
+ $data = array_merge($this->additionalFields, [
+ 'name' => $this->name,
+ 'path' => $this->path
+ ]);
+
+ $this->objectAction = new DevtoolsProjectAction([$this->objectID], 'update', ['data' => $data]);
+ $this->objectAction->executeAction();
+
+ $this->saved();
+
+ // show success message
+ WCF::getTPL()->assign('success', true);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function assignVariables() {
+ parent::assignVariables();
+
+ WCF::getTPL()->assign([
+ 'objectID' => $this->objectID,
+ 'action' => 'edit'
+ ]);
+ }
+}
--- /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\page;
+use wcf\data\devtools\project\DevtoolsProjectList;
+use wcf\page\AbstractPage;
+use wcf\system\WCF;
+
+/**
+ * Shows a list of devtools projects.
+ *
+ * @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 DevtoolsProjectListPage 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'];
+
+ /**
+ * @var DevtoolsProjectList
+ */
+ public $objectList;
+
+ /**
+ * @inheritDoc
+ */
+ public function readData() {
+ parent::readData();
+
+ $this->objectList = new DevtoolsProjectList();
+ $this->objectList->readObjects();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function assignVariables() {
+ parent::assignVariables();
+
+ WCF::getTPL()->assign([
+ 'objects' => $this->objectList
+ ]);
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\devtools\project;
+use wcf\data\DatabaseObject;
+use wcf\data\package\installation\plugin\PackageInstallationPluginList;
+use wcf\data\package\Package;
+use wcf\data\package\PackageCache;
+use wcf\system\devtools\package\DevtoolsPackageArchive;
+use wcf\system\devtools\pip\DevtoolsPip;
+use wcf\system\package\validation\PackageValidationException;
+use wcf\system\WCF;
+
+/**
+ * Represents a devtools project.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2017 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Devtools\Project
+ * @since 3.1
+ *
+ * @property-read integer $projectID unique id of the project
+ * @property-read string $name internal name for display inside the ACP
+ * @property-read string $path file system path
+ */
+class DevtoolsProject extends DatabaseObject {
+ /**
+ * @var boolean
+ */
+ protected $isCore;
+
+ /**
+ * @var Package
+ */
+ protected $package;
+
+ /**
+ * @var DevtoolsPackageArchive
+ */
+ protected $packageArchive;
+
+ public function getPips() {
+ $pipList = new PackageInstallationPluginList();
+ $pipList->sqlOrderBy = 'pluginName';
+ $pipList->readObjects();
+
+ $pips = [];
+ foreach ($pipList as $pip) {
+ $pips[] = new DevtoolsPip($pip);
+ }
+
+ return $pips;
+ }
+
+ public function validate() {
+ $errorType = self::validatePath($this->path);
+ if ($errorType !== '') {
+ return WCF::getLanguage()->get('wcf.acp.devtools.project.path.error.' . $errorType);
+ }
+
+ return $this->validatePackageXml();
+ }
+
+ public function isCore() {
+ if ($this->isCore === null) {
+ $this->isCore = self::pathIsCore($this->path);
+ }
+
+ return $this->isCore;
+ }
+
+ public function validatePackageXml() {
+ $packageXml = $this->path . ($this->isCore() ? 'com.woltlab.wcf/' : '') . 'package.xml';
+ $this->packageArchive = new DevtoolsPackageArchive($packageXml);
+ try {
+ $this->packageArchive->openArchive();
+ }
+ catch (PackageValidationException $e) {
+ return $e->getErrorMessage();
+ }
+
+ $this->package = PackageCache::getInstance()->getPackageByIdentifier($this->packageArchive->getPackageInfo('name'));
+ if ($this->package === null) {
+ return WCF::getLanguage()->getDynamicVariable('wcf.acp.devtools.project.path.error.notInstalled', [
+ 'package' => $this->packageArchive->getPackageInfo('name')
+ ]);
+ }
+
+ return '';
+ }
+
+ /**
+ * @return Package
+ */
+ public function getPackage() {
+ return $this->package;
+ }
+
+ /**
+ * @return DevtoolsPackageArchive
+ */
+ public function getPackageArchive() {
+ return $this->packageArchive;
+ }
+
+ /**
+ * Validates the provided path and returns an error code
+ * if the path does not exist (`notFound`) or if there is
+ * no package.xml (`packageXml`).
+ *
+ * @param string $path
+ * @return string
+ */
+ public static function validatePath($path) {
+ if (!is_dir($path)) {
+ return 'notFound';
+ }
+ else if (!file_exists($path . 'package.xml')) {
+ // check if this is `com.woltlab.wcf`
+ if (!self::pathIsCore($path)) {
+ return 'packageXml';
+ }
+ }
+
+ return '';
+ }
+
+ public static function pathIsCore($path) {
+ return (is_dir($path . 'com.woltlab.wcf') && file_exists($path . 'com.woltlab.wcf/package.xml'));
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\devtools\project;
+use wcf\data\AbstractDatabaseObjectAction;
+
+/**
+ * Executes devtools project related actions.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2017 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Data\Devtools\Project
+ * @since 3.1
+ *
+ * @method DevtoolsProjectEditor[] getObjects()
+ * @method DevtoolsProjectEditor getSingleObject()
+ */
+class DevtoolsProjectAction extends AbstractDatabaseObjectAction {
+ /**
+ * @inheritDoc
+ */
+ protected $className = DevtoolsProjectEditor::class;
+}
--- /dev/null
+<?php
+namespace wcf\data\devtools\project;
+use wcf\data\DatabaseObjectEditor;
+
+/**
+ * Provides functions to edit devtool projects.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2017 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Data\Devtools\Project
+ * @since 3.1
+ *
+ * @method static DevtoolsProject create(array $parameters = [])
+ * @method DevtoolsProject getDecoratedObject()
+ * @mixin DevtoolsProject
+ */
+class DevtoolsProjectEditor extends DatabaseObjectEditor {
+ /**
+ * @inheritDoc
+ */
+ protected static $baseClass = DevtoolsProject::class;
+}
--- /dev/null
+<?php
+namespace wcf\data\devtools\project;
+use wcf\data\DatabaseObjectList;
+
+/**
+ * Represents a list of devtools projects.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2017 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Data\Devtools\Project
+ * @since 3.1
+ *
+ * @method DevtoolsProject current()
+ * @method DevtoolsProject[] getObjects()
+ * @method DevtoolsProject|null search($objectID)
+ * @property DevtoolsProject[] $objects
+ */
+class DevtoolsProjectList extends DatabaseObjectList {
+ /**
+ * @inheritDoc
+ */
+ public $className = DevtoolsProject::class;
+}
<?php
namespace wcf\data\package\installation\plugin;
use wcf\data\AbstractDatabaseObjectAction;
+use wcf\data\devtools\project\DevtoolsProject;
+use wcf\system\cache\CacheHandler;
+use wcf\system\devtools\pip\DevtoolsPackageInstallationDispatcher;
+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;
+use wcf\system\WCF;
/**
* Executes package installation plugin-related actions.
* @inheritDoc
*/
protected $className = PackageInstallationPluginEditor::class;
+
+ /**
+ * @inheritDoc
+ */
+ protected $requireACP = ['invoke'];
+
+ /**
+ * @var DevtoolsPip
+ */
+ public $devtoolsPip;
+
+ /**
+ * @var PackageInstallationPlugin
+ */
+ public $packageInstallationPlugin;
+
+ /**
+ * @var DevtoolsProject
+ */
+ public $project;
+
+ public function validateInvoke() {
+ if (!ENABLE_DEVELOPER_TOOLS || !WCF::getSession()->getPermission('admin.configuration.package.canInstallPackage')) {
+ throw new PermissionDeniedException();
+ }
+
+ $this->readString('pluginName');
+ $this->readInteger('projectID');
+ $this->readString('target');
+
+ $this->project = new DevtoolsProject($this->parameters['projectID']);
+ if (!$this->project->projectID || $this->project->validate() !== '') {
+ throw new UserInputException('projectID');
+ }
+
+ $this->packageInstallationPlugin = new PackageInstallationPlugin($this->parameters['pluginName']);
+ if (!$this->packageInstallationPlugin->pluginName) {
+ throw new UserInputException('pluginName');
+ }
+
+ $this->devtoolsPip = new DevtoolsPip($this->packageInstallationPlugin);
+ $targets = $this->devtoolsPip->getTargets($this->project);
+ if (!in_array($this->parameters['target'], $targets)) {
+ throw new UserInputException('target');
+ }
+ }
+
+ public function invoke() {
+ $dispatcher = new DevtoolsPackageInstallationDispatcher($this->project);
+ /** @var IIdempotentPackageInstallationPlugin $pip */
+ $pip = new $this->packageInstallationPlugin->className($dispatcher, [
+ 'value' => $this->devtoolsPip->getInstructionValue($this->project, $this->parameters['target'])
+ ]);
+
+ try {
+ $pip->update();
+ }
+ catch (SplitNodeException $e) {
+ throw new \RuntimeException("PIP '{$this->packageInstallationPlugin->pluginName}' is not allowed to throw a 'SplitNodeException'.");
+ }
+
+ // clear cache
+
+ // TODO: use a central method instead!
+
+ // create search index tables
+ SearchIndexManager::getInstance()->createSearchIndices();
+
+ VersionTracker::getInstance()->createStorageTables();
+
+ CacheHandler::getInstance()->flushAll();
+ }
}
class ApplicationHandler extends SingletonFactory {
/**
* application cache
- * @var Application[]
+ * @var mixed[][]
*/
protected $cache;
return null;
}
+ /**
+ * Returns the list of application abbreviations.
+ *
+ * @return string[]
+ * @since 3.1
+ */
+ public function getAbbreviations() {
+ return array_keys($this->cache['abbreviation']);
+ }
+
/**
* Returns true if given $url is an internal URL.
*
--- /dev/null
+<?php
+namespace wcf\system\devtools\package;
+use wcf\system\package\PackageArchive;
+
+/**
+ * @method DevtoolsTar getTar()
+ */
+class DevtoolsPackageArchive extends PackageArchive {
+ protected $packageXmlPath = '';
+
+ /** @noinspection PhpMissingParentConstructorInspection */
+ public function __construct($packageXmlPath) {
+ $this->packageXmlPath = $packageXmlPath;
+ }
+
+ public function openArchive() {
+ $this->tar = new DevtoolsTar(['package.xml' => $this->packageXmlPath]);
+
+ $this->readPackageInfo();
+ }
+}
--- /dev/null
+<?php
+namespace wcf\system\devtools\package;
+use wcf\system\io\Tar;
+
+class DevtoolsTar extends Tar {
+ protected $files = '';
+
+ /** @noinspection PhpMissingParentConstructorInspection */
+ public function __construct(array $files) {
+ $this->files = $files;
+ }
+
+ public function registerFile($filename, $fullPath) {
+ $this->files[$filename] = $fullPath;
+ }
+
+ public function getIndexByFilename($filename) {
+ return (isset($this->files[$filename]) ? $filename : false);
+ }
+
+ 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]);
+ }
+
+ public function extract($index, $destination) {
+ copy($this->files[$index], $destination);
+ }
+}
--- /dev/null
+<?php
+namespace wcf\system\devtools\pip;
+use wcf\data\devtools\project\DevtoolsProject;
+use wcf\system\devtools\package\DevtoolsPackageArchive;
+use wcf\system\package\PackageInstallationDispatcher;
+
+class DevtoolsPackageInstallationDispatcher extends PackageInstallationDispatcher {
+ /**
+ * @var DevtoolsPackageArchive
+ */
+ protected $project;
+
+ public function __construct(DevtoolsProject $project) {
+ parent::__construct(new DevtoolsPackageInstallationQueue($project));
+
+ $this->project = $project;
+ }
+
+ public function getArchive() {
+ return $this->project->getPackageArchive();
+ }
+
+ public function getPackageID() {
+ return $this->project->getPackage()->packageID;
+ }
+
+ public function getPackageName() {
+ return $this->project->getPackage()->getName();
+ }
+}
--- /dev/null
+<?php
+namespace wcf\system\devtools\pip;
+use wcf\data\devtools\project\DevtoolsProject;
+use wcf\data\package\installation\queue\PackageInstallationQueue;
+use wcf\system\WCF;
+
+class DevtoolsPackageInstallationQueue extends PackageInstallationQueue {
+ public function __construct(DevtoolsProject $project) {
+ parent::__construct(null, [
+ 'queueID' => 0,
+ 'parentQueueID' => 0,
+ 'processNo' => 0,
+ 'userID' => WCF::getUser()->userID,
+ 'package' => $project->getPackage()->package,
+ 'packageName' => $project->getPackage()->getName(),
+ 'archive' => '',
+ 'action' => 'update',
+ 'done' => 0,
+ 'isApplication' => $project->getPackage()->isApplication
+ ]);
+ }
+}
\ No newline at end of file
--- /dev/null
+<?php
+namespace wcf\system\devtools\pip;
+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;
+
+/**
+ * Wrapper class for package installation plugins for use with the sync feature.
+ *
+ * @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
+ *
+ * @method PackageInstallationPlugin getDecoratedObject()
+ * @mixin PackageInstallationPlugin
+ */
+class DevtoolsPip extends DatabaseObjectDecorator {
+ /**
+ * @inheritDoc
+ */
+ protected static $baseClass = PackageInstallationPlugin::class;
+
+ /**
+ * Returns true if the PIP class can be found.
+ *
+ * @return boolean
+ */
+ public function classExists() {
+ return class_exists($this->getDecoratedObject()->className);
+ }
+
+ /**
+ * Returns true if the PIP is expected to be idempotent.
+ *
+ * @return boolean
+ */
+ public function isIdempotent() {
+ return is_subclass_of($this->getDecoratedObject()->className, IIdempotentPackageInstallationPlugin::class);
+ }
+
+ /**
+ * Returns the default filename of this PIP.
+ *
+ * @return string
+ */
+ public function getDefaultFilename() {
+ return call_user_func([$this->getDecoratedObject()->className, 'getDefaultFilename']);
+ }
+
+ /**
+ * Returns true if the PIP exists, has a default filename and is idempotent.
+ *
+ * @return boolean
+ */
+ public function isSupported() {
+ return $this->classExists() && $this->getDefaultFilename() && $this->isIdempotent();
+ }
+
+ /**
+ * Returns the first validation error.
+ *
+ * @return string
+ */
+ public function getFirstError() {
+ if (!$this->classExists()) {
+ return WCF::getLanguage()->getDynamicVariable('wcf.acp.devtools.pip.error.className', ['className' => $this->getDecoratedObject()->className]);
+ }
+ else if (!$this->isIdempotent()) {
+ return WCF::getLanguage()->get('wcf.acp.devtools.pip.error.notIdempotent');
+ }
+ else if (!$this->getDefaultFilename()) {
+ return WCF::getLanguage()->get('wcf.acp.devtools.pip.error.defaultFilename');
+ }
+
+ throw new \LogicException("Please call `isSupported()` to check for potential errors.");
+ }
+
+ /**
+ * Returns the list of valid targets for this pip.
+ *
+ * @param DevtoolsProject $project
+ * @return string[]
+ */
+ public function getTargets(DevtoolsProject $project) {
+ if (!$this->isSupported()) {
+ return [];
+ }
+
+ $path = $project->path;
+ $defaultFilename = $this->getDefaultFilename();
+ $targets = [];
+
+ // the core uses a significantly different file layout
+ if ($project->isCore()) {
+ switch ($this->getDecoratedObject()->pluginName) {
+ case 'acpTemplate':
+ case 'file':
+ case 'template':
+ // these pips are satisfied by definition
+ return [$defaultFilename];
+
+ case 'language':
+ foreach (glob($path . 'wcfsetup/install/lang/*.xml') as $file) {
+ $targets[] = basename($file);
+ }
+
+ // `glob()` returns files in an arbitrary order
+ sort($targets, SORT_NATURAL);
+
+ return $targets;
+ }
+
+ if (strpos($defaultFilename, '*') !== false) {
+ foreach (glob($path . 'com.woltlab.wcf/' . $defaultFilename) as $file) {
+ $targets[] = basename($file);
+ }
+
+ // `glob()` returns files in an arbitrary order
+ sort($targets, SORT_NATURAL);
+ }
+ else {
+ if (file_exists($path . 'com.woltlab.wcf/' . $defaultFilename)) {
+ $targets[] = $defaultFilename;
+ }
+ }
+ }
+ else {
+ if (preg_match('~^(?<filename>.*)\.tar$~', $defaultFilename, $match)) {
+ if (is_dir($path . $match['filename'])) {
+ $targets[] = $defaultFilename;
+ }
+
+ // check for application-specific pips too
+ foreach (ApplicationHandler::getInstance()->getAbbreviations() as $abbreviation) {
+ if (is_dir($path . $match['filename'] . '_' . $abbreviation)) {
+ $targets[] = $match['filename'] . "_{$abbreviation}.tar";
+ }
+ }
+ }
+ else {
+ if (strpos($defaultFilename, '*') !== false) {
+ foreach (glob($path . $defaultFilename) as $file) {
+ $targets[] = basename($file);
+ }
+
+ // `glob()` returns files in an arbitrary order
+ sort($targets, SORT_NATURAL);
+ }
+ else {
+ if (file_exists($path . $defaultFilename)) {
+ $targets[] = $defaultFilename;
+ }
+ }
+ }
+ }
+
+ return $targets;
+ }
+
+ public function getInstructionValue(DevtoolsProject $project, $target) {
+ $defaultFilename = $this->getDefaultFilename();
+
+ if ($project->isCore()) {
+ switch ($this->getDecoratedObject()->pluginName) {
+ case 'acpTemplate':
+ case 'file':
+ case 'template':
+ throw new NotImplementedException();
+ break;
+
+ case 'language':
+ $filename = "wcfsetup/install/lang/{$target}";
+ $project->getPackageArchive()->getTar()->registerFile($filename, $project->path . $filename);
+
+ return $filename;
+
+ default:
+ $filename = "com.woltlab.wcf/{$target}";
+ $project->getPackageArchive()->getTar()->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);
+ }
+ }
+
+ throw new NotImplementedException();
+ }
+}
\ No newline at end of file
--- /dev/null
+<?php
+namespace wcf\system\devtools\pip;
+use wcf\system\package\plugin\IPackageInstallationPlugin;
+
+/**
+ * Default interface for package installation plugins that indicate to have
+ * no side-effects when run more than once. Furthermore such plugins may not
+ * throw a `SplitNodeException` during its normal operation.
+ *
+ * This is especially important for the developer tools that rely on PIPs
+ * being invokable at any point, regardless if they have been ran before.
+ *
+ * Classic examples include the menu-PIPs that will at most update existing
+ * data, but not cause items to be duplicated when evaluated once more. The
+ * opposite is the sql-PIP that (usually) contains non-repeatable instructions.
+ *
+ * @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
+ */
+interface IIdempotentPackageInstallationPlugin extends IPackageInstallationPlugin {}
'debug',
'mail_send_method'
]);
+ $statement->execute([
+ 1,
+ 'enable_developer_tools'
+ ]);
}
// update options.inc.php
<?php
namespace wcf\system\package\plugin;
use wcf\data\acl\option\ACLOptionEditor;
+use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
use wcf\system\exception\SystemException;
use wcf\system\WCF;
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @package WoltLabSuite\Core\System\Package\Plugin
*/
-class ACLOptionPackageInstallationPlugin extends AbstractOptionPackageInstallationPlugin {
+class ACLOptionPackageInstallationPlugin extends AbstractOptionPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
/**
* @inheritDoc
*/
namespace wcf\system\package\plugin;
use wcf\data\acp\search\provider\ACPSearchProviderEditor;
use wcf\system\cache\builder\ACPSearchProviderCacheBuilder;
+use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
use wcf\system\WCF;
/**
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @package WoltLabSuite\Core\System\Package\Plugin
*/
-class ACPSearchProviderPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin {
+class ACPSearchProviderPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
/**
* @inheritDoc
*/
namespace wcf\system\package\plugin;
use wcf\data\application\Application;
use wcf\data\package\Package;
+use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
use wcf\system\exception\SystemException;
use wcf\system\package\ACPTemplatesFileHandler;
use wcf\system\package\PackageArchive;
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @package WoltLabSuite\Core\System\Package\Plugin
*/
-class ACPTemplatePackageInstallationPlugin extends AbstractPackageInstallationPlugin {
+class ACPTemplatePackageInstallationPlugin extends AbstractPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
/**
* @inheritDoc
*/
<?php
namespace wcf\system\package\plugin;
+use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
use wcf\system\exception\SystemException;
use wcf\system\WCF;
use wcf\util\StringUtil;
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @package WoltLabSuite\Core\System\Package\Plugin
*/
-abstract class AbstractMenuPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin {
+abstract class AbstractMenuPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
/**
* @inheritDoc
*/
<?php
namespace wcf\system\package\plugin;
use wcf\data\package\Package;
+use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
use wcf\system\exception\SystemException;
use wcf\system\WCF;
use wcf\util\StringUtil;
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @package WoltLabSuite\Core\System\Package\Plugin
*/
-abstract class AbstractOptionPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin {
+abstract class AbstractOptionPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
/**
* @inheritDoc
*/
use wcf\data\bbcode\BBCodeEditor;
use wcf\data\package\PackageCache;
use wcf\system\database\util\PreparedStatementConditionBuilder;
+use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
use wcf\system\exception\SystemException;
use wcf\system\WCF;
use wcf\util\StringUtil;
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @package WoltLabSuite\Core\Acp\Package\Plugin
*/
-class BBCodePackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin {
+class BBCodePackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
/**
* @inheritDoc
*/
use wcf\data\box\Box;
use wcf\data\box\BoxEditor;
use wcf\system\database\util\PreparedStatementConditionBuilder;
+use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
use wcf\system\exception\SystemException;
use wcf\system\language\LanguageFactory;
use wcf\system\WCF;
* @package WoltLabSuite\Core\Acp\Package\Plugin
* @since 3.0
*/
-class BoxPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin {
+class BoxPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
/**
* @inheritDoc
*/
namespace wcf\system\package\plugin;
use wcf\data\clipboard\action\ClipboardAction;
use wcf\data\clipboard\action\ClipboardActionEditor;
+use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
use wcf\system\WCF;
/**
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @package WoltLabSuite\Core\Acp\Package\Plugin
*/
-class ClipboardActionPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin {
+class ClipboardActionPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
/**
* @inheritDoc
*/
namespace wcf\system\package\plugin;
use wcf\data\core\object\CoreObjectEditor;
use wcf\system\cache\builder\CoreObjectCacheBuilder;
+use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
use wcf\system\WCF;
/**
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @package WoltLabSuite\Core\System\Package\Plugin
*/
-class CoreObjectPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin {
+class CoreObjectPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
/**
* @inheritDoc
*/
namespace wcf\system\package\plugin;
use wcf\data\cronjob\Cronjob;
use wcf\data\cronjob\CronjobEditor;
+use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
use wcf\system\WCF;
use wcf\util\CronjobUtil;
use wcf\util\StringUtil;
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @package WoltLabSuite\Core\Acp\Package\Plugin
*/
-class CronjobPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin {
+class CronjobPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
/**
* @inheritDoc
*/
use wcf\data\event\listener\EventListener;
use wcf\data\event\listener\EventListenerEditor;
use wcf\system\cache\builder\EventListenerCacheBuilder;
+use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
use wcf\system\WCF;
use wcf\util\StringUtil;
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @package WoltLabSuite\Core\System\Package\Plugin
*/
-class EventListenerPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin {
+class EventListenerPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
/**
* @inheritDoc
*/
namespace wcf\system\package\plugin;
use wcf\data\application\Application;
use wcf\data\package\Package;
+use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
use wcf\system\exception\SystemException;
use wcf\system\package\FilesFileHandler;
use wcf\system\package\PackageArchive;
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @package WoltLabSuite\Core\System\Package\Plugin
*/
-class FilePackageInstallationPlugin extends AbstractPackageInstallationPlugin {
+class FilePackageInstallationPlugin extends AbstractPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
/**
* @inheritDoc
*/
use wcf\data\language\Language;
use wcf\data\language\LanguageEditor;
use wcf\system\database\util\PreparedStatementConditionBuilder;
+use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
use wcf\system\exception\SystemException;
use wcf\system\package\PackageArchive;
use wcf\system\WCF;
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @package WoltLabSuite\Core\System\Package\Plugin
*/
-class LanguagePackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin {
+class LanguagePackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
/**
* @inheritDoc
*/
namespace wcf\system\package\plugin;
use wcf\data\menu\item\MenuItem;
use wcf\data\menu\item\MenuItemEditor;
+use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
use wcf\system\exception\SystemException;
use wcf\system\WCF;
* @package WoltLabSuite\Core\Acp\Package\Plugin
* @since 3.0
*/
-class MenuItemPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin {
+class MenuItemPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
/**
* @inheritDoc
*/
use wcf\data\menu\MenuEditor;
use wcf\data\menu\MenuList;
use wcf\system\database\util\PreparedStatementConditionBuilder;
+use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
use wcf\system\exception\SystemException;
use wcf\system\WCF;
* @package WoltLabSuite\Core\Acp\Package\Plugin
* @since 3.0
*/
-class MenuPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin {
+class MenuPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
/**
* box meta data per menu
* @var string[]
<?php
namespace wcf\system\package\plugin;
use wcf\data\object\type\definition\ObjectTypeDefinitionEditor;
+use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
use wcf\system\WCF;
/**
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @package WoltLabSuite\Core\Acp\Package\Plugin
*/
-class ObjectTypeDefinitionPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin {
+class ObjectTypeDefinitionPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
/**
* @inheritDoc
*/
<?php
namespace wcf\system\package\plugin;
use wcf\data\object\type\ObjectTypeEditor;
+use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
use wcf\system\exception\SystemException;
use wcf\system\WCF;
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @package WoltLabSuite\Core\Acp\Package\Plugin
*/
-class ObjectTypePackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin {
+class ObjectTypePackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
/**
* @inheritDoc
*/
use wcf\data\option\Option;
use wcf\data\option\OptionEditor;
use wcf\data\package\Package;
+use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
use wcf\system\exception\SystemException;
use wcf\system\WCF;
use wcf\util\StringUtil;
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @package WoltLabSuite\Core\System\Package\Plugin
*/
-class OptionPackageInstallationPlugin extends AbstractOptionPackageInstallationPlugin {
+class OptionPackageInstallationPlugin extends AbstractOptionPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
/**
* @inheritDoc
*/
<?php
namespace wcf\system\package\plugin;
use wcf\data\package\installation\plugin\PackageInstallationPluginEditor;
+use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
use wcf\system\WCF;
/**
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @package WoltLabSuite\Core\System\Package\Plugin
*/
-class PIPPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin {
+class PIPPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
/**
* @inheritDoc
*/
use wcf\data\package\PackageCache;
use wcf\data\page\Page;
use wcf\data\page\PageEditor;
+use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
use wcf\system\exception\SystemException;
use wcf\system\language\LanguageFactory;
use wcf\system\request\RouteHandler;
* @package WoltLabSuite\Core\Acp\Package\Plugin
* @since 3.0
*/
-class PagePackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin {
+class PagePackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
/**
* @inheritDoc
*/
<?php
namespace wcf\system\package\plugin;
use wcf\data\smiley\SmileyEditor;
+use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
use wcf\system\WCF;
/**
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @package WoltLabSuite\Core\Acp\Package\Plugin
*/
-class SmileyPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin {
+class SmileyPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
/**
* @inheritDoc
*/
namespace wcf\system\package\plugin;
use wcf\data\template\listener\TemplateListenerEditor;
use wcf\system\cache\builder\TemplateListenerCodeCacheBuilder;
+use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
use wcf\system\WCF;
use wcf\util\StringUtil;
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @package WoltLabSuite\Core\System\Package\Plugin
*/
-class TemplateListenerPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin {
+class TemplateListenerPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
/**
* @inheritDoc
*/
namespace wcf\system\package\plugin;
use wcf\data\application\Application;
use wcf\data\package\Package;
+use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
use wcf\system\exception\SystemException;
use wcf\system\package\PackageArchive;
use wcf\system\package\TemplatesFileHandler;
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @package WoltLabSuite\Core\System\Package\Plugin
*/
-class TemplatePackageInstallationPlugin extends AbstractPackageInstallationPlugin {
+class TemplatePackageInstallationPlugin extends AbstractPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
/**
* @inheritDoc
*/
use wcf\data\user\group\option\UserGroupOption;
use wcf\data\user\group\option\UserGroupOptionEditor;
use wcf\data\user\group\UserGroup;
+use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
use wcf\system\WCF;
use wcf\util\StringUtil;
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @package WoltLabSuite\Core\System\Package\Plugin
*/
-class UserGroupOptionPackageInstallationPlugin extends AbstractOptionPackageInstallationPlugin {
+class UserGroupOptionPackageInstallationPlugin extends AbstractOptionPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
/**
* list of group ids by type
* @var integer[][]
namespace wcf\system\package\plugin;
use wcf\data\user\notification\event\UserNotificationEvent;
use wcf\data\user\notification\event\UserNotificationEventEditor;
+use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
use wcf\system\exception\SystemException;
use wcf\system\WCF;
use wcf\util\StringUtil;
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @package WoltLabSuite\Core\System\Package\Plugin
*/
-class UserNotificationEventPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin {
+class UserNotificationEventPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
/**
* @inheritDoc
*/
use wcf\data\user\option\category\UserOptionCategoryEditor;
use wcf\data\user\option\UserOption;
use wcf\data\user\option\UserOptionEditor;
+use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
use wcf\system\exception\SystemException;
use wcf\system\WCF;
use wcf\util\StringUtil;
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @package WoltLabSuite\Core\System\Package\Plugin
*/
-class UserOptionPackageInstallationPlugin extends AbstractOptionPackageInstallationPlugin {
+class UserOptionPackageInstallationPlugin extends AbstractOptionPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
/**
* @inheritDoc
*/
<?php
namespace wcf\system\package\plugin;
use wcf\data\user\profile\menu\item\UserProfileMenuItemEditor;
+use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
use wcf\system\WCF;
use wcf\util\StringUtil;
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @package WoltLabSuite\Core\System\Package\Plugin
*/
-class UserProfileMenuPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin {
+class UserProfileMenuPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
/**
* @inheritDoc
*/
<item name="wcf.acp.dataImport.started"><![CDATA[Import begonnen.]]></item>
</category>
+ <category name="wcf.acp.devtools">
+ <item name="wcf.acp.devtools.project.action"><![CDATA[Aktionen]]></item>
+ <item name="wcf.acp.devtools.project.add"><![CDATA[Projekt hinzufügen]]></item>
+ <item name="wcf.acp.devtools.project.edit"><![CDATA[Projekt bearbeiten]]></item>
+ <item name="wcf.acp.devtools.project.list"><![CDATA[Projekte]]></item>
+ <item name="wcf.acp.devtools.project.name"><![CDATA[Name]]></item>
+ <item name="wcf.acp.devtools.project.path"><![CDATA[Pfad]]></item>
+ <item name="wcf.acp.devtools.project.sync"><![CDATA[Daten-Abgleich]]></item>
+ <item name="wcf.acp.devtools.pip.defaultFilename"><![CDATA[Suchmuster]]></item>
+ <item name="wcf.acp.devtools.pip.error.notIdempotent"><![CDATA[Das PIP unterstützt keinen wiederholten Import und kann nur bei einem Update verarbeitet werden.]]></item>
+ <item name="wcf.acp.devtools.pip.notice"><![CDATA[Bestehende Anweisungen in der <kbd>package.xml</kbd> werden nicht berücksichtigt; es können somit auch PIPs importiert werden, für die noch keine Anweisungen hinterlegt worden sind. Es werden nur die Standardpfade bei der Suche verwendet, zusätzlich werden anwendungsbezogene Suffixe (z. B. <kbd>files_wcf.tar</kbd>) für <kbd>.tar</kbd>-basierte PIPs unterstützt.]]></item>
+ <item name="wcf.acp.devtools.pip.pluginName"><![CDATA[PIP-Bezeichner]]></item>
+ <item name="wcf.acp.devtools.pip.showOnlyMatches"><![CDATA[Zeige nur übereinstimmende PIPs an]]></item>
+ <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>
+ </category>
+
<category name="wcf.acp.exceptionLog">
<item name="wcf.acp.exceptionLog"><![CDATA[Protokollierte Fehler]]></item>
<item name="wcf.acp.exceptionLog.exception.message"><![CDATA[Fehlermeldung]]></item>
<item name="wcf.acp.menu.link.trophy.list"><![CDATA[Trophäen]]></item>
<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>
</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.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>
<category name="wcf.acp.customOption">
error TEXT
);
+DROP TABLE IF EXISTS wcf1_devtools_project;
+CREATE TABLE wcf1_devtools_project (
+ projectID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(191) NOT NULL,
+ path TEXT
+);
+
DROP TABLE IF EXISTS wcf1_edit_history_entry;
CREATE TABLE wcf1_edit_history_entry (
entryID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,