Added clipboard-API (draft)
authorAlexander Ebert <ebert@woltlab.com>
Tue, 6 Sep 2011 14:07:02 +0000 (16:07 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Tue, 6 Sep 2011 14:25:05 +0000 (16:25 +0200)
19 files changed:
XSD/clipboardaction.xsd [new file with mode: 0644]
XSD/clipboarditemtype.xsd [new file with mode: 0644]
com.woltlab.wcf/clipboardaction.xml [new file with mode: 0644]
com.woltlab.wcf/clipboarditemtype.xml [new file with mode: 0644]
wcfsetup/install/files/acp/js/WCF.ACP.js
wcfsetup/install/files/js/WCF.js
wcfsetup/install/files/lib/data/clipboard/action/ClipboardAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/clipboard/action/ClipboardActionAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/clipboard/action/ClipboardActionEditor.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/clipboard/action/ClipboardActionList.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/clipboard/item/type/ClipboardItemType.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/clipboard/item/type/ClipboardItemTypeAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/clipboard/item/type/ClipboardItemTypeEditor.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/clipboard/item/type/ClipboardItemTypeList.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/clipboard/ClipboardEditorItem.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/clipboard/ClipboardHandler.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/clipboard/action/IClipboardAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/clipboard/action/UserClipboardAction.class.php [new file with mode: 0644]
wcfsetup/setup/db/install.sql

diff --git a/XSD/clipboardaction.xsd b/XSD/clipboardaction.xsd
new file mode 100644 (file)
index 0000000..09e2147
--- /dev/null
@@ -0,0 +1,47 @@
+<?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
diff --git a/XSD/clipboarditemtype.xsd b/XSD/clipboarditemtype.xsd
new file mode 100644 (file)
index 0000000..55a781c
--- /dev/null
@@ -0,0 +1,32 @@
+<?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
diff --git a/com.woltlab.wcf/clipboardaction.xml b/com.woltlab.wcf/clipboardaction.xml
new file mode 100644 (file)
index 0000000..ced15c1
--- /dev/null
@@ -0,0 +1,36 @@
+<?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>
diff --git a/com.woltlab.wcf/clipboarditemtype.xml b/com.woltlab.wcf/clipboarditemtype.xml
new file mode 100644 (file)
index 0000000..b51e7d4
--- /dev/null
@@ -0,0 +1,8 @@
+<?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>
index c8690e004230b53e5173ce76c8d9bd0fd12750dc..51ad383f02a30ce907277f25ba4bad09af579d77 100644 (file)
@@ -487,4 +487,45 @@ WCF.ACP.Options.prototype = {
                        }
                }
        }
+};
+
+/**
+ * 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
index 00903afa4ff52622173cf85bc5608e30c0efa437..a8f93aa49798ee964999385fa148088432eb4a5c 100644 (file)
@@ -407,6 +407,261 @@ $.extend(WCF, {
        }
 });
 
+/**
+ * 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.
diff --git a/wcfsetup/install/files/lib/data/clipboard/action/ClipboardAction.class.php b/wcfsetup/install/files/lib/data/clipboard/action/ClipboardAction.class.php
new file mode 100644 (file)
index 0000000..9df8338
--- /dev/null
@@ -0,0 +1,25 @@
+<?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';
+}
diff --git a/wcfsetup/install/files/lib/data/clipboard/action/ClipboardActionAction.class.php b/wcfsetup/install/files/lib/data/clipboard/action/ClipboardActionAction.class.php
new file mode 100644 (file)
index 0000000..917a1a0
--- /dev/null
@@ -0,0 +1,20 @@
+<?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';
+}
diff --git a/wcfsetup/install/files/lib/data/clipboard/action/ClipboardActionEditor.class.php b/wcfsetup/install/files/lib/data/clipboard/action/ClipboardActionEditor.class.php
new file mode 100644 (file)
index 0000000..fbdced4
--- /dev/null
@@ -0,0 +1,20 @@
+<?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';
+}
diff --git a/wcfsetup/install/files/lib/data/clipboard/action/ClipboardActionList.class.php b/wcfsetup/install/files/lib/data/clipboard/action/ClipboardActionList.class.php
new file mode 100644 (file)
index 0000000..a499a68
--- /dev/null
@@ -0,0 +1,20 @@
+<?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';
+}
diff --git a/wcfsetup/install/files/lib/data/clipboard/item/type/ClipboardItemType.class.php b/wcfsetup/install/files/lib/data/clipboard/item/type/ClipboardItemType.class.php
new file mode 100644 (file)
index 0000000..a9fa4af
--- /dev/null
@@ -0,0 +1,25 @@
+<?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';
+}
diff --git a/wcfsetup/install/files/lib/data/clipboard/item/type/ClipboardItemTypeAction.class.php b/wcfsetup/install/files/lib/data/clipboard/item/type/ClipboardItemTypeAction.class.php
new file mode 100644 (file)
index 0000000..088a3ba
--- /dev/null
@@ -0,0 +1,20 @@
+<?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';
+}
diff --git a/wcfsetup/install/files/lib/data/clipboard/item/type/ClipboardItemTypeEditor.class.php b/wcfsetup/install/files/lib/data/clipboard/item/type/ClipboardItemTypeEditor.class.php
new file mode 100644 (file)
index 0000000..498b543
--- /dev/null
@@ -0,0 +1,20 @@
+<?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';
+}
diff --git a/wcfsetup/install/files/lib/data/clipboard/item/type/ClipboardItemTypeList.class.php b/wcfsetup/install/files/lib/data/clipboard/item/type/ClipboardItemTypeList.class.php
new file mode 100644 (file)
index 0000000..3adc7bd
--- /dev/null
@@ -0,0 +1,20 @@
+<?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';
+}
diff --git a/wcfsetup/install/files/lib/system/clipboard/ClipboardEditorItem.class.php b/wcfsetup/install/files/lib/system/clipboard/ClipboardEditorItem.class.php
new file mode 100644 (file)
index 0000000..0bd933f
--- /dev/null
@@ -0,0 +1,141 @@
+<?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;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/clipboard/ClipboardHandler.class.php b/wcfsetup/install/files/lib/system/clipboard/ClipboardHandler.class.php
new file mode 100644 (file)
index 0000000..0cfa703
--- /dev/null
@@ -0,0 +1,260 @@
+<?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;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/clipboard/action/IClipboardAction.class.php b/wcfsetup/install/files/lib/system/clipboard/action/IClipboardAction.class.php
new file mode 100644 (file)
index 0000000..8ef8b1f
--- /dev/null
@@ -0,0 +1,38 @@
+<?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);
+}
diff --git a/wcfsetup/install/files/lib/system/clipboard/action/UserClipboardAction.class.php b/wcfsetup/install/files/lib/system/clipboard/action/UserClipboardAction.class.php
new file mode 100644 (file)
index 0000000..361327a
--- /dev/null
@@ -0,0 +1,144 @@
+<?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';
+               }
+       }
+}
index 15a1d156aaeec4d1ae3e19580c238b1a9c9049a9..70378768409505e3e1db5fc71a0353240bbfe9cf 100644 (file)
@@ -96,6 +96,37 @@ CREATE TABLE wcf1_cleanup_listener (
        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,