--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<xs:schema id="data" targetNamespace="http://www.woltlab.com" xmlns="http://www.woltlab.com" xmlns:xs="http://www.w3.org/2001/XMLSchema" attributeFormDefault="qualified" elementFormDefault="qualified">
+ <xs:element name="action">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="actionclassname" type="xs:string" minOccurs="0" />
+ <xs:element name="showorder" type="xs:string" minOccurs="0" />
+ <xs:element name="pages" minOccurs="0" maxOccurs="unbounded">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="page" nillable="true" minOccurs="0" maxOccurs="unbounded">
+ <xs:complexType>
+ <xs:simpleContent>
+ <xs:extension base="xs:string">
+ </xs:extension>
+ </xs:simpleContent>
+ </xs:complexType>
+ </xs:element>
+ </xs:sequence>
+ </xs:complexType>
+ </xs:element>
+ </xs:sequence>
+ <xs:attribute name="name" form="unqualified" type="xs:string" />
+ </xs:complexType>
+ </xs:element>
+ <xs:element name="data">
+ <xs:complexType>
+ <xs:choice minOccurs="0" maxOccurs="unbounded">
+ <xs:element ref="action" />
+ <xs:element name="import">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element ref="action" minOccurs="0" maxOccurs="unbounded" />
+ </xs:sequence>
+ </xs:complexType>
+ </xs:element>
+ <xs:element name="delete">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element ref="action" minOccurs="0" maxOccurs="unbounded" />
+ </xs:sequence>
+ </xs:complexType>
+ </xs:element>
+ </xs:choice>
+ </xs:complexType>
+ </xs:element>
+</xs:schema>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<xs:schema id="data" targetNamespace="http://www.woltlab.com" xmlns="http://www.woltlab.com" xmlns:xs="http://www.w3.org/2001/XMLSchema" attributeFormDefault="qualified" elementFormDefault="qualified">
+ <xs:element name="type">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="listclassname" type="xs:string" minOccurs="0" />
+ </xs:sequence>
+ <xs:attribute name="name" form="unqualified" type="xs:string" />
+ </xs:complexType>
+ </xs:element>
+ <xs:element name="data">
+ <xs:complexType>
+ <xs:choice minOccurs="0" maxOccurs="unbounded">
+ <xs:element ref="type" />
+ <xs:element name="import">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element ref="type" minOccurs="0" maxOccurs="unbounded" />
+ </xs:sequence>
+ </xs:complexType>
+ </xs:element>
+ <xs:element name="delete">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element ref="type" minOccurs="0" maxOccurs="unbounded" />
+ </xs:sequence>
+ </xs:complexType>
+ </xs:element>
+ </xs:choice>
+ </xs:complexType>
+ </xs:element>
+</xs:schema>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/maelstrom/clipboardaction.xsd">
+ <import>
+ <action name="assignToGroup">
+ <actionclassname><![CDATA[wcf\system\clipboard\action\UserClipboardAction]]></actionclassname>
+ <showorder>1</showorder>
+ <pages>
+ <page><![CDATA[wcf\acp\page\UserListPage]]></page>
+ </pages>
+ </action>
+
+ <action name="sendMail">
+ <actionclassname><![CDATA[wcf\system\clipboard\action\UserClipboardAction]]></actionclassname>
+ <showorder>2</showorder>
+ <pages>
+ <page><![CDATA[wcf\acp\page\UserListPage]]></page>
+ </pages>
+ </action>
+
+ <action name="delete">
+ <actionclassname><![CDATA[wcf\system\clipboard\action\UserClipboardAction]]></actionclassname>
+ <showorder>3</showorder>
+ <pages>
+ <page><![CDATA[wcf\acp\page\UserListPage]]></page>
+ </pages>
+ </action>
+
+ <action name="exportMailAdress">
+ <actionclassname><![CDATA[wcf\system\clipboard\action\UserClipboardAction]]></actionclassname>
+ <showorder>4</showorder>
+ <pages>
+ <page><![CDATA[wcf\acp\page\UserListPage]]></page>
+ </pages>
+ </action>
+ </import>
+</data>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/maelstrom/clipboarditemtype.xsd">
+ <import>
+ <type name="com.woltlab.wcf.user">
+ <listclassname><![CDATA[wcf\data\user\UserList]]></listclassname>
+ </type>
+ </import>
+</data>
}
}
}
+};
+
+/**
+ * Namespace for WCF.ACP.User
+ */
+WCF.ACP.User = {};
+
+/**
+ * UserList clipboard API
+ */
+WCF.ACP.User.List = function() { this.init(); };
+WCF.ACP.User.List.prototype = {
+ /**
+ * Initializes the UserList clipboard API.
+ */
+ init: function() {
+ $('body').bind('clipboardAction', $.proxy(this.handleClipboardEvent, this));
+ },
+
+ /**
+ * Event handler for clipboard editor item actions.
+ */
+ handleClipboardEvent: function(event, type, actionName) {
+ // ignore unrelated events
+ if ((type != 'com.woltlab.wcf.user') || (actionName != 'user.delete')) return;
+
+ var $item = $(event.target);
+ this._delete($item);
+ },
+
+ /**
+ * Handle delete action.
+ *
+ * @param jQuery item
+ */
+ _delete: function(item) {
+ var $confirmMessage = item.data('internalData')['confirmMessage'];
+ if (confirm($confirmMessage)) {
+ WCF.Clipboard.sendRequest(item);
+ }
+ }
};
\ No newline at end of file
}
});
+/**
+ * Clipboard API
+ */
+WCF.Clipboard = {
+ /**
+ * action proxy object
+ * @var WCF.Action.Proxy
+ */
+ _actionProxy: null,
+
+ /**
+ * current page
+ * @var string
+ */
+ _page: '',
+
+ /**
+ * proxy object
+ * @var WCF.Action.Proxy
+ */
+ _proxy: null,
+
+ /**
+ * Initializes the clipboard API.
+ */
+ init: function(page) {
+ this._page = page;
+
+ this._actionProxy = new WCF.Action.Proxy({
+ success: $.proxy(this._actionSuccess, this),
+ url: 'index.php?action=ClipboardProxy&t=' + SECURITY_TOKEN + SID_ARG_2ND
+ });
+
+ this._proxy = new WCF.Action.Proxy({
+ success: $.proxy(this._success, this),
+ url: 'index.php?action=Clipboard&t=' + SECURITY_TOKEN + SID_ARG_2ND
+ });
+
+ // init containers first
+ $('.clipboardContainer').each($.proxy(function(index, container) {
+ this._initContainer(container);
+ }, this));
+ },
+
+ /**
+ * Initializes a clipboard container.
+ *
+ * @param object container
+ */
+ _initContainer: function(container) {
+ var $container = $(container);
+
+ // fetch id or assign a random one if none found
+ var $id = $container.attr('id');
+ if (!$id) {
+ $id = WCF.getRandomID();
+ $container.attr('id', $id);
+ }
+
+ // bind mark all checkboxes
+ $container.find('.clipboardMarkAll').each($.proxy(function(index, item) {
+ $(item).data('hasContainer', $id).click($.proxy(this._markAll, this));
+ }, this));
+
+ // bind item checkboxes
+ $container.find('input.clipboardItem').each($.proxy(function(index, item) {
+ $(item).data('hasContainer', $id).click($.proxy(this._click, this));
+ }, this));
+ },
+
+ /**
+ * Processes change checkbox state.
+ *
+ * @param object event
+ */
+ _click: function(event) {
+ var $item = $(event.target);
+ var $objectID = $item.data('objectID');
+ var $isMarked = ($item.attr('checked')) ? true : false;
+ var $objectIDs = [ $objectID ];
+
+ // item is part of a container
+ if ($item.data('hasContainer')) {
+ var $container = $('#' + $item.data('hasContainer'));
+ var $type = $container.data('type');
+
+ // check if all items are marked
+ var $markedAll = true;
+ $container.find('input.clipboardItem').each(function(index, containerItem) {
+ var $containerItem = $(containerItem);
+ if (!$containerItem.attr('checked')) {
+ $markedAll = false;
+ }
+ });
+
+ // simulate a ticked 'markAll' checkbox
+ $container.find('.clipboardMarkAll').each(function(index, markAll) {
+ if ($markedAll) {
+ $(markAll).attr('checked', 'checked');
+ }
+ else {
+ $(markAll).removeAttr('checked');
+ }
+ });
+ }
+ else {
+ // standalone item
+ var $type = $item.data('type');
+ }
+
+ this._saveState($type, $objectIDs, $isMarked);
+ },
+
+ /**
+ * Marks all associated clipboard items as checked.
+ *
+ * @param object event
+ */
+ _markAll: function(event) {
+ var $item = $(event.target);
+ var $objectIDs = [ ];
+ var $isMarked = true;
+
+ // if markAll object is a checkbox, allow toggling
+ if ($item.getTagName() == 'input') {
+ $isMarked = $item.attr('checked');
+ }
+
+ // handle item containers
+ if ($item.data('hasContainer')) {
+ var $container = $('#' + $item.data('hasContainer'));
+ var $type = $container.data('type');
+
+ // toggle state for all associated items
+ $container.find('input.clipboardItem').each(function(index, containerItem) {
+ var $containerItem = $(containerItem);
+ if ($isMarked) {
+ if (!$containerItem.attr('checked')) {
+ $containerItem.attr('checked', 'checked');
+ $objectIDs.push($containerItem.data('objectID'));
+ }
+ }
+ else {
+ if ($containerItem.attr('checked')) {
+ $containerItem.removeAttr('checked');
+ $objectIDs.push($containerItem.data('objectID'));
+ }
+ }
+ });
+ }
+
+ // save new status
+ this._saveState($type, $objectIDs, $isMarked);
+ },
+
+ /**
+ * Saves clipboard item state.
+ *
+ * @param string type
+ * @param array objectIDs
+ * @param boolean isMarked
+ */
+ _saveState: function(type, objectIDs, isMarked) {
+ this._proxy.setOption('data', {
+ action: (isMarked) ? 'mark' : 'unmark',
+ objectIDs: objectIDs,
+ pageClassName: this._page,
+ type: type
+ })
+ this._proxy.sendRequest();
+ },
+
+ /**
+ * Updates editor options.
+ *
+ * @param object data
+ * @param string textStatus
+ * @param jQuery jqXHR
+ */
+ _success: function(data, textStatus, jqXHR) {
+ // clear all editors first
+ var $containers = {};
+ $('.clipboardEditor').each(function(index, container) {
+ var $container = $(container);
+ var $typeName = $container.data('type');
+ if ($typeName) {
+ $containers[$typeName] = $container;
+ }
+
+ $container.empty();
+ });
+
+ // do not buid new editors
+ if (!data.items) return;
+
+ // rebuild editors
+ for (var $typeName in data.items) {
+ if (!$containers[$typeName]) {
+ continue;
+ }
+
+ // create container
+ var $container = $containers[$typeName];
+ var $editor = data.items[$typeName];
+ var $label = $('<span>' + $editor.label + '</span>').appendTo($container).click(function(event) {
+ var $span = $(event.target);
+ $span.next().toggle();
+ });
+ var $list = $('<ol></ol>').appendTo($container).hide();
+
+ // create editor items
+ for (var $itemIndex in $editor.items) {
+ var $item = $editor.items[$itemIndex];
+ var $listItem = $('<li>' + $item.label + '</li>').appendTo($list);
+ $listItem.data('actionName', $item.actionName).data('parameters', $item.parameters);
+ $listItem.data('internalData', $item.internalData).data('url', $item.url).data('type', $typeName);
+
+ // bind event
+ $listItem.click($.proxy(this._executeAction, this));
+ }
+ }
+ },
+
+ /**
+ * Executes a clipboard editor item action.
+ *
+ * @param object event
+ */
+ _executeAction: function(event) {
+ var $listItem = $(event.target);
+ var $url = $listItem.data('url');
+ if ($url) {
+ window.location.href = $url;
+ }
+
+ // fire event
+ $listItem.trigger('clipboardAction', [ $listItem.data('type'), $listItem.data('actionName') ]);
+ },
+
+ /**
+ * Sends a clipboard proxy request.
+ *
+ * @param object item
+ */
+ sendRequest: function(item) {
+ var $item = $(item);
+
+ this._actionProxy.setOption('data', {
+ parameters: $item.data('parameters'),
+ typeName: $item.data('type')
+ });
+ this._actionProxy.sendRequest();
+ }
+};
+
/**
* Provides a simple call for periodical executed functions. Based upon
* ideas by Prototype's PeriodicalExecuter.
--- /dev/null
+<?php
+namespace wcf\data\clipboard\action;
+use wcf\data\DatabaseObject;
+
+/**
+ * Represents a clipboard action.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2011 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.clipboard.action
+ * @category Community Framework
+ */
+class ClipboardAction extends DatabaseObject {
+ /**
+ * @see wcf\data\DatabaseObject::$databaseTableName
+ */
+ protected static $databaseTableName = 'clipboard_action';
+
+ /**
+ * @see wcf\data\DatabaseObject::$databaseTableIndexName
+ */
+ protected static $databaseTableIndexName = 'actionID';
+}
--- /dev/null
+<?php
+namespace wcf\data\clipboard\action;
+use wcf\data\AbstractDatabaseObjectAction;
+
+/**
+ * Executes clipboard action-related actions.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2011 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.clipboard.action
+ * @category Community Framework
+ */
+class ClipboardActionAction extends AbstractDatabaseObjectAction {
+ /**
+ * @see wcf\data\AbstractDatabaseObjectAction::$className
+ */
+ protected $className = 'wcf\data\clipboard\action\ClipboardActionEditor';
+}
--- /dev/null
+<?php
+namespace wcf\data\clipboard\action;
+use wcf\data\DatabaseObjectEditor;
+
+/**
+ * Provides functions to edit clipboard actions.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2011 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.clipboard.action
+ * @category Community Framework
+ */
+class ClipboardActionEditor extends DatabaseObjectEditor {
+ /**
+ * @see wcf\data\DatabaseObjectDecorator::$baseClass
+ */
+ protected static $baseClass = 'wcf\data\clipboard\action\ClipboardAction';
+}
--- /dev/null
+<?php
+namespace wcf\data\clipboard\action;
+use wcf\data\DatabaseObjectList;
+
+/**
+ * Represents a list of clipboard actions.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2011 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.clipboard.action
+ * @category Community Framework
+ */
+class ClipboardActionList extends DatabaseObjectList {
+ /**
+ * @see wcf\data\DatabaseObjectList::$className
+ */
+ public $className = 'wcf\data\clipboard\action\ClipboardAction';
+}
--- /dev/null
+<?php
+namespace wcf\data\clipboard\item\type;
+use wcf\data\DatabaseObject;
+
+/**
+ * Represents a clipboard item type.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2011 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.clipboard.item.type
+ * @category Community Framework
+ */
+class ClipboardItemType extends DatabaseObject {
+ /**
+ * @see wcf\data\DatabaseObject::$databaseTableName
+ */
+ protected static $databaseTableName = 'clipboard_item_type';
+
+ /**
+ * @see wcf\data\DatabaseObject::$databaseTableIndexName
+ */
+ protected static $databaseTableIndexName = 'typeID';
+}
--- /dev/null
+<?php
+namespace wcf\data\clipboard\item\type;
+use wcf\data\AbstractDatabaseObjectAction;
+
+/**
+ * Executes clipboard item type-related actions.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2011 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.clipboard.item.type
+ * @category Community Framework
+ */
+class ClipboardItemTypeAction extends AbstractDatabaseObjectAction {
+ /**
+ * @see wcf\data\AbstractDatabaseObjectAction::$className
+ */
+ protected $className = 'wcf\data\clipboard\item\type\ClipboardItemTypeEditor';
+}
--- /dev/null
+<?php
+namespace wcf\data\clipboard\item\type;
+use wcf\data\DatabaseObjectEditor;
+
+/**
+ * Provides functions to edit clipboard item types.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2011 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.clipboard.item.type
+ * @category Community Framework
+ */
+class ClipboardItemTypeEditor extends DatabaseObjectEditor {
+ /**
+ * @see wcf\data\DatabaseObjectDecorator::$baseClass
+ */
+ protected static $baseClass = 'wcf\data\clipboard\item\type\ClipboardItemType';
+}
--- /dev/null
+<?php
+namespace wcf\data\clipboard\item\type;
+use wcf\data\DatabaseObjectList;
+
+/**
+ * Represents a list of clipboard item types.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2011 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data.clipboard.item.type
+ * @category Community Framework
+ */
+class ClipboardItemTypeList extends DatabaseObjectList {
+ /**
+ * @see wcf\data\DatabaseObjectList::$className
+ */
+ public $className = 'wcf\data\clipboard\item\type\ClipboardItemType';
+}
--- /dev/null
+<?php
+namespace wcf\system\clipboard;
+use wcf\system\SystemException;
+use wcf\util\StringUtil;
+
+/**
+ * Represents a clipboard item for inline editing.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2011 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage system.clipboard
+ * @category Community Framework
+ */
+final class ClipboardEditorItem {
+ /**
+ * internal data
+ * @var array
+ */
+ protected $internalData = array();
+
+ /**
+ * item name
+ * @var string
+ */
+ protected $name = '';
+
+ /**
+ * list of parameters passed to ClipboardProxyAction
+ * @var array
+ */
+ protected $parameters = array();
+
+ /**
+ * redirect url
+ * @var string
+ */
+ protected $url = '';
+
+ /**
+ * Returns internal data.
+ *
+ * @return array
+ */
+ public function getInternalData() {
+ return $this->internalData;
+ }
+
+ /**
+ * Returns item name.
+ *
+ * @return string
+ */
+ public function getName() {
+ return $this->name;
+ }
+
+ /**
+ * Returns parameters passed to ClipboardProxyAction.
+ *
+ * @return array
+ */
+ public function getParameters() {
+ return $this->parameters;
+ }
+
+ /**
+ * Returns redirect url.
+ *
+ * @return string
+ */
+ public function getURL() {
+ return $this->url;
+ }
+
+ /**
+ * Adds internal data, values will be left untouched by clipboard API.
+ *
+ * @param string $name
+ * @param mixed $value
+ */
+ public function addInternalData($name, $value) {
+ if (!preg_match('~^[a-zA-Z]+$~', $name)) {
+ throw new SystemException("internal data name '".$name."' is invalid");
+ }
+
+ if (in_array($name, $this->internalData)) {
+ throw new SystemException("internal data name '".$name."' is not unique");
+ }
+
+ $this->internalData[$name] = $value;
+ }
+
+ /**
+ * Adds an parameter passed to ClipboardProxyAction.
+ *
+ * @param string $name
+ * @param mixed $value
+ */
+ public function addParameter($name, $value) {
+ if (!preg_match('~^[a-zA-Z]+$~', $name)) {
+ throw new SystemException("parameter name '".$name."' is invalid");
+ }
+
+ if (in_array($name, $this->parameters)) {
+ throw new SystemException("parameter name '".$name."' is not unique");
+ }
+
+ $this->parameters[$name] = $value;
+ }
+
+ /**
+ * Sets item name.
+ *
+ * @param string $name
+ */
+ public function setName($name) {
+ if (!preg_match('~^[a-zA-Z0-9\.]+$~', $name)) {
+ throw new SystemException("item name '".$name."' is invalid");
+ }
+
+ $this->name = $name;
+ }
+
+ /**
+ * Sets redirect url, session id will be appended.
+ *
+ * @param string $url
+ */
+ public function setURL($url) {
+ if (StringUtil::indexOf($url, '?') === false) {
+ $url .= SID_ARG_1ST;
+ }
+ else {
+ $url .= SID_ARG_2ND_NOT_ENCODED;
+ }
+
+ $this->url = $url;
+ }
+}
--- /dev/null
+<?php
+namespace wcf\system\clipboard;
+use wcf\system\cache\CacheHandler;
+use wcf\system\database\util\PreparedStatementConditionBuilder;
+use wcf\system\exception\SystemException;
+use wcf\system\SingletonFactory;
+use wcf\system\WCF;
+use wcf\util\ClassUtil;
+
+/**
+ * Handles clipboard-related actions.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2011 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage system.clipboard
+ * @category Community Framework
+ */
+class ClipboardHandler extends SingletonFactory {
+ /**
+ * cached list of actions
+ * @var array
+ */
+ protected $actionCache = null;
+
+ /**
+ * cached list of page actions
+ * @var array
+ */
+ protected $pageCache = null;
+
+ /**
+ * cached list of clipboard item types
+ * @var array<array>
+ */
+ protected $typeCache = null;
+
+ /**
+ * @see wcf\system\SingletonFactory::init()
+ */
+ protected function init() {
+ CacheHandler::getInstance()->addResource(
+ 'clipboard-itemType-'.PACKAGE_ID,
+ WCF_DIR.'cache/cache.clipboard-itemType-'.PACKAGE_ID.'.php',
+ 'wcf\system\cache\builder\ClipboardItemTypeCacheBuilder'
+ );
+ $this->typeCache = CacheHandler::getInstance()->get('clipboard-itemType-'.PACKAGE_ID);
+
+ CacheHandler::getInstance()->addResource(
+ 'clipboard-page-'.PACKAGE_ID,
+ WCF_DIR.'cache/cache.clipboard-page-'.PACKAGE_ID.'.php',
+ 'wcf\system\cache\builder\ClipboardPageCacheBuilder'
+ );
+ $this->pageCache = CacheHandler::getInstance()->get('clipboard-page-'.PACKAGE_ID);
+ }
+
+ /**
+ * Loads action cache.
+ */
+ protected function loadActionCache() {
+ if ($this->actionCache !== null) return;
+
+ CacheHandler::getInstance()->addResource(
+ 'clipboard-action-'.PACKAGE_ID,
+ WCF_DIR.'cache/cache.clipboard-action-'.PACKAGE_ID.'.php',
+ 'wcf\system\cache\builder\ClipboardActionCacheBuilder'
+ );
+ $this->actionCache = CacheHandler::getInstance()->get('clipboard-action-'.PACKAGE_ID);
+ }
+
+ /**
+ * Marks objects as marked.
+ *
+ * @param array $objectIDs
+ * @param integer $typeID
+ */
+ public function mark(array $objectIDs, $typeID) {
+ // remove existing entries first, prevents conflict with INSERT
+ $this->unmark($objectIDs, $typeID);
+
+ $sql = "INSERT INTO wcf".WCF_N."_clipboard_item
+ (typeID, userID, objectID)
+ VALUES (?, ?, ?)";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ foreach ($objectIDs as $objectID) {
+ $statement->execute(array(
+ $typeID,
+ WCF::getUser()->userID,
+ $objectID
+ ));
+ }
+ }
+
+ /**
+ * Removes an object marking.
+ *
+ * @param array $objectIDs
+ * @param integer $typeID
+ */
+ public function unmark(array $objectIDs, $typeID) {
+ $conditions = new PreparedStatementConditionBuilder();
+ $conditions->add("typeID = ?", array($typeID));
+ $conditions->add("objectID IN (?)", array($objectIDs));
+ $conditions->add("userID = ?", array(WCF::getUser()->userID));
+
+ $sql = "DELETE FROM wcf".WCF_N."_clipboard_item
+ ".$conditions;
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute($conditions->getParameters());
+ }
+
+ /**
+ * Returns a type id by name.
+ *
+ * @param string $typeName
+ * @return integer
+ */
+ public function getTypeID($typeName) {
+ if (isset($this->typeCache['typeNames'][$typeName])) {
+ return $this->typeCache['typeNames'][$typeName];
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns a type by type id.
+ *
+ * @param integer $typeID
+ * @return wcf\data\clipboard\item\type\ClipboardItemType
+ */
+ public function getType($typeID) {
+ if (isset($this->typeCache['types'][$typeID])) {
+ return $this->typeCache['types'][$typeID];
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns a list of marked items grouped by type name.
+ *
+ * @param integer $typeID
+ * @return array<array>
+ */
+ public function getMarkedItems($typeID = null) {
+ $conditions = new PreparedStatementConditionBuilder();
+ $conditions->add("userID = ?", array(WCF::getUser()->userID));
+ if ($typeID !== null) $conditions->add("typeID = ?", array($typeID));
+
+ // fetch object ids
+ $sql = "SELECT typeID, objectID
+ FROM wcf".WCF_N."_clipboard_item
+ ".$conditions;
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute($conditions->getParameters());
+
+ // group object ids by type name
+ $data = array();
+ while ($row = $statement->fetchArray()) {
+ $type = $this->getType($row['typeID']);
+ if ($type === null) {
+ continue;
+ }
+
+ if (!isset($data[$type->typeName])) {
+ $data[$type->typeName] = array(
+ 'className' => $type->className,
+ 'objectIDs' => array()
+ );
+ }
+
+ $data[$type->typeName]['objectIDs'][] = $row['objectID'];
+ }
+
+ // read objects
+ $objects = array();
+ foreach ($data as $typeName => $objectData) {
+ $objectList = new $objectData['className']();
+ $objectList->getConditionBuilder()->add($objectList->getDatabaseTableAlias() . "." . $objectList->getDatabaseTableIndexName() . " IN (?)", array($objectData['objectIDs']));
+ $objectList->sqlLimit = 0;
+ $objectList->readObjects();
+
+ $objects[$typeName] = $objectList->getObjects();
+ }
+
+ return $objects;
+ }
+
+ /**
+ * Returns items for clipboard editor.
+ *
+ * @param string $page
+ * @return array<array>
+ */
+ public function getEditorItems($page) {
+ // ignore unknown pages
+ if (!isset($this->pageCache[$page])) die('page '.$page.' is unknown');
+ #return null;
+
+ // get objects
+ $objects = $this->getMarkedItems();
+ if (!count($objects)) return null;
+
+ // fetch action ids
+ $this->loadActionCache();
+ $actionIDs = array();
+ foreach ($this->pageCache[$page] as $actionID) {
+ if (isset($this->actionCache[$actionID])) {
+ $actionIDs[] = $actionID;
+ }
+ }
+ $actionIDs = array_unique($actionIDs);
+
+ // load actions
+ $actions = array();
+ foreach ($actionIDs as $actionID) {
+ $actionClassName = $this->actionCache[$actionID]->actionClassName;
+ $actionName = $this->actionCache[$actionID]->actionName;
+ if (!isset($actions[$actionClassName])) {
+ // validate class
+ if (!ClassUtil::isInstanceOf($actionClassName, 'wcf\system\clipboard\action\IClipboardAction')) {
+ throw new SystemException("class '".$actionClassName."' does not implement the interface 'wcf\system\clipboard\action\IClipboardAction'.");
+ }
+
+ $actions[$actionClassName] = array(
+ 'actions' => array(),
+ 'object' => new $actionClassName()
+ );
+ }
+
+ $actions[$actionClassName]['actions'][] = $actionName;
+ }
+
+ // execute actions
+ $editorData = array();
+ foreach ($actions as $actionData) {
+ // get accepted objects
+ $typeName = $actionData['object']->getTypeName();
+ if (!isset($objects[$typeName])) continue;
+
+ $editorData[$typeName] = array(
+ 'label' => $actionData['object']->getEditorLabel($objects[$typeName]),
+ 'items' => array()
+ );
+
+ foreach ($actionData['actions'] as $action) {
+ $data = $actionData['object']->execute($objects[$typeName], $action);
+ if ($data === null) {
+ continue;
+ }
+
+ $editorData[$typeName]['items'][$action] = $data;
+ }
+ }
+
+ return $editorData;
+ }
+}
--- /dev/null
+<?php
+namespace wcf\system\clipboard\action;
+
+/**
+ * Basic interface for all clipboard editor actions.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2011 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage system.clipboard.action
+ * @category Community Framework
+ */
+interface IClipboardAction {
+ /**
+ * Returns type name identifier.
+ *
+ * @return string
+ */
+ public function getTypeName();
+
+ /**
+ * Returns action data, return NULL if action is not applicable.
+ *
+ * @param array $objects
+ * @param string $actionName
+ * @return wcf\system\clipboard\ClipboardEditorItem
+ */
+ public function execute(array $objects, $actionName);
+
+ /**
+ * Returns label for item editor.
+ *
+ * @param array $objects
+ * @return string
+ */
+ public function getEditorLabel(array $objects);
+}
--- /dev/null
+<?php
+namespace wcf\system\clipboard\action;
+use wcf\system\clipboard\ClipboardEditorItem;
+use wcf\system\database\util\PreparedStatementConditionBuilder;
+use wcf\system\exception\SystemException;
+use wcf\system\WCF;
+
+/**
+ * Prepares clipboard editor items for user objects.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2011 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage system.clipboard.action
+ * @category Community Framework
+ */
+class UserClipboardAction implements IClipboardAction {
+ /**
+ * @see wcf\system\clipboard\action\IClipboardAction::getTypeName()
+ */
+ public function getTypeName() {
+ return 'com.woltlab.wcf.user';
+ }
+
+ /**
+ * @see wcf\system\clipboard\action\IClipboardAction::execute()
+ */
+ public function execute(array $objects, $actionName) {
+ $item = new ClipboardEditorItem();
+
+ // handle actions
+ switch ($actionName) {
+ case 'assignToGroup':
+ $item->setName('user.assignToGroup');
+ $item->setURL('index.php?form=UserAssignToGroup');
+ break;
+
+ case 'delete':
+ $count = $this->validateDelete($objects);
+ if (!$count) {
+ return null;
+ }
+
+ // TODO: use language variable
+ $item->addInternalData('confirmMessage', 'Delete '.$count.' users?');
+ $item->addParameter('actionName', 'delete');
+ $item->addParameter('className', 'wcf\data\user\UserAction');
+ $item->setName('user.delete');
+ break;
+
+ case 'exportMailAddress':
+ $item->setName('user.exportMailAddress');
+ $item->setURL('index.php?form=UserEmailAddressExport');
+ break;
+
+ case 'sendMail':
+ $item->setName('user.sendMail');
+ $item->setURL('index.php?form=UserMail');
+ break;
+
+ default:
+ throw new SystemException("action '".$actionName."' is invalid");
+ break;
+ }
+
+ return $item;
+ }
+
+ /**
+ * Returns number of users which can be deleted.
+ *
+ * @param array<wcf\data\user\User> $objects
+ * @return integer
+ */
+ protected function validateDelete(array $objects) {
+ // check permissions
+ if (!WCF::getSession()->getPermission('admin.user.canDeleteUser')) {
+ return 0;
+ }
+
+ // user cannot delete itself
+ $count = count($objects);
+ $userIDs = array_keys($objects);
+ foreach ($userIDs as $index => $userID) {
+ if ($userID == WCF::getUser()->userID) {
+ $count--;
+ unset($objects[$userID]);
+ unset($userIDs[$index]);
+ }
+ }
+
+ // no valid users found
+ if (!$count) return 0;
+
+ // fetch user to group associations
+ $conditions = new PreparedStatementConditionBuilder();
+ $conditions->add("userID IN (?)", array($userIDs));
+
+ $sql = "SELECT userID, groupID
+ FROM wcf".WCF_N."_user_to_group
+ ".$conditions;
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute($conditions->getParameters());
+
+ $userToGroup = array();
+ while ($row = $statement->fetchArray()) {
+ if (!isset($userToGroup[$row['userID']])) {
+ $userToGroup[$row['userID']] = array();
+ }
+
+ $userToGroup[$row['userID']][] = $row['groupID'];
+ }
+
+ // validate if user's group is accessible for current user
+ $count = count($objects);
+ foreach ($userIDs as $index => $userID) {
+ if (!isset($userToGroup[$userID])) {
+ $count--;
+ continue;
+ }
+
+ if (!UserGroup::isAccessibleGroup($userToGroup[$userID])) {
+ $count--;
+ }
+ }
+
+ return $count;
+ }
+
+ /**
+ * @see wcf\system\clipboard\action\IClipboardAction::getEditorLabel()
+ * @todo use language variable
+ */
+ public function getEditorLabel(array $objects) {
+ $count = count($objects);
+ if ($count == 1) {
+ return 'One user marked';
+ }
+ else {
+ return $count . ' users marked';
+ }
+ }
+}
UNIQUE KEY (className, packageID)
);
+DROP TABLE IF EXISTS wcf1_clipboard_action;
+CREATE TABLE wcf1_clipboard_action (
+ actionID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ packageID INT(10) NOT NULL DEFAULT 0,
+ actionName VARCHAR(50) NOT NULL DEFAULT '',
+ actionClassName VARCHAR(200) NOT NULL DEFAULT '',
+ sortOrder INT(10) NOT NULL DEFAULT 0
+);
+
+DROP TABLE IF EXISTS wcf1_clipboard_item;
+CREATE TABLE wcf1_clipboard_item (
+ typeID INT(10) NOT NULL DEFAULT 0,
+ userID INT(10) NOT NULL DEFAULT 0,
+ objectID INT(10) NOT NULL DEFAULT 0
+);
+
+DROP TABLE IF EXISTS wcf1_clipboard_item_type;
+CREATE TABLE wcf1_clipboard_item_type (
+ typeID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ packageID INT(10) NOT NULL DEFAULT 0,
+ typeName VARCHAR(80) NOT NULL DEFAULT '',
+ className VARCHAR(200) NOT NULL DEFAULT ''
+);
+
+DROP TABLE IF EXISTS wcf1_clipboard_page;
+CREATE TABLE wcf1_clipboard_page (
+ pageClassName VARCHAR(80) NOT NULL DEFAULT '',
+ packageID INT(10) NOT NULL DEFAULT 0,
+ actionID INT(10) NOT NULL DEFAULT 0
+);
+
DROP TABLE IF EXISTS wcf1_cleanup_log;
CREATE TABLE wcf1_cleanup_log (
packageID INT(10) NOT NULL DEFAULT 0,