* `permissions` and `options` support for event listeners.
* `name` attribute for event listener PIP (`listenerName` for event listener objects).
* `permissions` and `options` support for template listeners.
-* `wcf\data\TDatabaseObjectOptions` and `wcf\data\TDatabaseObjectPermissions` for database object-bound options and permissions validation.
* `wcf\system\cache\builder\EventListenerCacheBuilder` returns `wcf\data\event\listener\EventListener` objects instead of data arrays.
* `wcf\system\clipboard\action\UserExtendedClipboardAction` removed.
* `wcf\system\event\listener\PreParserAtUserListener` removed.
* Ported the PHP-BBCode parser, massively improves accuracy and ensures validity
* Show error message if poll options are given but not question instead of discarding poll options.
* `parentObjectID` column added to `modification_log` and `wcf\system\log\modification\AbstractModificationLogHandler` introduced as a replacement for `wcf\system\log\modification\ModificationLogHandler`.
-* `wcf\data\TUserContent` added.
+
+#### New Traits
+
+* `wcf\data\TDatabaseObjectOptions` for database object-bound options validation.
+* `wcf\data\TDatabaseObjectOptions` for database object-bound permissions validation.
+* `wcf\data\TMultiCategoryObject` provides category-related methods for objects with multiple categories.
+* `wcf\data\TUserContent` provides default implementations of the (non-inherited) methods of the IUserContent interface.
--- /dev/null
+<?php
+namespace wcf\data;
+use wcf\data\category\AbstractDecoratedCategory;
+use wcf\system\exception\SystemException;
+use wcf\system\WCF;
+
+/**
+ * Provides category-related methods for an object with multiple categories.
+ *
+ * @author Matthias Schmidt, Marcel Werk
+ * @copyright 2001-2015 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage data
+ * @category Community Framework
+ */
+trait TMultiCategoryObject {
+ /**
+ * list of the object's categories
+ * @var AbstractDecoratedCategory[]
+ */
+ protected $categories = null;
+
+ /**
+ * ids of the object's categories
+ * @var integer[]
+ */
+ protected $categoryIDs = array();
+
+ /**
+ * list of the object's leaf categories
+ * @var AbstractDecoratedCategory[]
+ */
+ protected $leafCategories = null;
+
+ /**
+ * Returns the list of category ids.
+ *
+ * @return integer[]
+ */
+ public function getCategoryIDs() {
+ return $this->categoryIDs;
+ }
+
+ /**
+ * Returns the categories of the object.
+ *
+ * @return CalendarCategory[]
+ */
+ public function getCategories() {
+ if ($this->categories === null) {
+ $this->categories = array();
+
+ $className = static::getCategoryClassName();
+ if (!is_subclass_of($className, AbstractDecoratedCategory::class)) {
+ throw new SystemException("'".$className."' does not extend '".AbstractDecoratedCategory::class."'.");
+ }
+
+ if (!empty($this->categoryIDs)) {
+ foreach ($this->categoryIDs as $categoryID) {
+ $this->categories[$categoryID] = $className::getCategory($categoryID);
+ }
+ }
+ else {
+ $sql = "SELECT categoryID
+ FROM wcf".WCF_N."_category
+ WHERE categoryID IN (
+ SELECT categoryID
+ FROM ".static::getCategoryMappingDatabaseTableName()."
+ WHERE ".static::getDatabaseTableIndexName()." = ?
+ )
+ ORDER BY parentCategoryID, showOrder";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute(array($this->getObjectID()));
+ while ($categoryID = $statement->fetchColumn()) {
+ $this->categories[$categoryID] = $className::getCategory($categoryID);
+ }
+ }
+ }
+
+ return $this->categories;
+ }
+
+ /**
+ * Returns the list of all selected categories unless a child category is selected.
+ *
+ * @return AbstractDecoratedCategory[]
+ */
+ public function getLeafCategories() {
+ if ($this->leafCategories === null) {
+ $this->leafCategories = $categories = $this->getCategories();
+
+ foreach ($categories as $category) {
+ if ($category->parentCategoryID && isset($this->leafCategories[$category->parentCategoryID])) {
+ unset($this->leafCategories[$category->parentCategoryID]);
+ }
+ }
+ }
+
+ return $this->leafCategories;
+ }
+
+ /**
+ * @see DatabaseObject::getObjectID()
+ */
+ abstract public function getObjectID();
+
+ /**
+ * Sets a category id.
+ *
+ * @param integer $categoryID
+ */
+ public function setCategoryID($categoryID) {
+ $this->categoryIDs[] = $categoryID;
+ }
+
+ /**
+ * Sets a category ids.
+ *
+ * @param integer[] $categoryIDs
+ */
+ public function setCategoryIDs(array $categoryIDs) {
+ $this->categoryIDs= $categoryIDs;
+ }
+
+ /**
+ * Returns the name of the database table containing the mapping of the objects to their categories.
+ *
+ * @return string
+ */
+ abstract public static function getCategoryMappingDatabaseTableName();
+
+ /**
+ * Returns the name of the used AbstractDecoratedCategory class.
+ *
+ * @return string
+ */
+ abstract public static function getCategoryClassName();
+
+ /**
+ * @see IStorableObject::getDatabaseTableIndexName()
+ */
+ abstract public static function getDatabaseTableIndexName();
+}