<showorder>4</showorder>
</acpmenuitem>
+ <acpmenuitem name="wcf.acp.menu.link.tag">
+ <parent>wcf.acp.menu.link.content</parent>
+ </acpmenuitem>
+
+ <acpmenuitem name="wcf.acp.menu.link.tag.list">
+ <controller><![CDATA[wcf\acp\page\TagListPage]]></controller>
+ <parent>wcf.acp.menu.link.tag</parent>
+ <permissions>admin.content.tag.canManageTag</permissions>
+ <showorder>1</showorder>
+ </acpmenuitem>
+
+ <acpmenuitem name="wcf.acp.menu.link.tag.add">
+ <controller><![CDATA[wcf\acp\form\TagAddForm]]></controller>
+ <parent>wcf.acp.menu.link.tag</parent>
+ <permissions>admin.content.tag.canManageTag</permissions>
+ <showorder>2</showorder>
+ </acpmenuitem>
+
<acpmenuitem name="wcf.acp.menu.link.community">
<showorder>5</showorder>
</acpmenuitem>
<name>com.woltlab.wcf.label.objectType</name>
<interfacename>wcf\system\label\object\type\ILabelObjectTypeHandler</interfacename>
</definition>
+
+ <definition>
+ <name>com.woltlab.wcf.tagging.taggableObject</name>
+ <interfacename>wcf\system\tagging\ITaggable</interfacename>
+ </definition>
</import>
</data>
<defaultvalue>1</defaultvalue>
</option>
+ <option name="module_tagging">
+ <categoryname>module.content</categoryname>
+ <optiontype>boolean</optiontype>
+ <defaultvalue>1</defaultvalue>
+ </option>
+
<!-- general.page -->
<option name="page_title">
<categoryname>general.page</categoryname>
<optiontype>useroptions</optiontype>
<defaultvalue></defaultvalue>
</option>
+
+ <!-- message.general -->
+ <option name="tagging_max_tag_length">
+ <categoryname>message.general</categoryname>
+ <optiontype>integer</optiontype>
+ <defaultvalue>30</defaultvalue>
+ <minvalue>1</minvalue>
+ <maxvalue>255</maxvalue>
+ </option>
+ <!-- /message.general -->
</options>
</import>
</data>
--- /dev/null
+{hascontent}
+ <ul class="tagList">
+ {content}
+ {foreach from=$tags item=tag}
+ <li><a href="{link controller='Tagged' object=$tag}{/link}" rel="tag" style="font-size: {@$tag->getSize()}%;">{$tag->name}</a></li>
+ {/foreach}
+ {/content}
+ </ul>
+{/hascontent}
\ No newline at end of file
--- /dev/null
+<dl class="jsOnly">
+ <dt><label for="tagSearchInput{if $tagInputSuffix|isset}{@$tagInputSuffix}{/if}">{lang}wcf.tagging.tags{/lang}</label></dt>
+ <dd>
+ <div id="tagList{if $tagInputSuffix|isset}{@$tagInputSuffix}{/if}" class="editableItemList"></div>
+ <input id="tagSearchInput{if $tagInputSuffix|isset}{@$tagInputSuffix}{/if}" type="text" value="" class="long" />
+ <small>{lang}wcf.tagging.tags.description{/lang}</small>
+ </dd>
+</dl>
+<script type="text/javascript" src="{@$__wcf->getPath()}js/WCF.Tagging{if !ENABLE_DEBUG_MODE}.min{/if}.js"></script>
+<script type="text/javascript">
+ //<![CDATA[
+ $(function() {
+ var $tagList = new WCF.Tagging.TagList('#tagList{if $tagInputSuffix|isset}{@$tagInputSuffix}{/if}', '#tagSearchInput{if $tagInputSuffix|isset}{@$tagInputSuffix}{/if}', {@TAGGING_MAX_TAG_LENGTH});
+
+ {if $tags|isset && $tags|count}
+ $tagList.load([ {implode from=$tags item=tag}'{$tag}'{/implode} ]);
+ {/if}
+ });
+ //]]>
+</script>
\ No newline at end of file
--- /dev/null
+{include file='documentHeader'}
+
+<head>
+ <title>{lang}wcf.tagging.taggedObjects.{@$objectType}{/lang} - {PAGE_TITLE|language}</title>
+
+ {include file='headInclude'}
+
+ {if $pageNo < $pages}
+ <link rel="next" href="{link controller='Tagged' object=$tag}objectType={@$objectType}&pageNo={@$pageNo+1}{/link}" />
+ {/if}
+ {if $pageNo > 1}
+ <link rel="prev" href="{link controller='Tagged' object=$tag}objectType={@$objectType}{if $pageNo > 2}&pageNo={@$pageNo-1}{/if}{/link}" />
+ {/if}
+ <link rel="canonical" href="{link controller='Tagged' object=$tag}objectType={@$objectType}{if $pageNo > 1}&pageNo={@$pageNo}{/if}{/link}" />
+</head>
+
+<body id="tpl{$templateName|ucfirst}">
+
+{capture assign='sidebar'}
+ <fieldset>
+ <legend>{lang}wcf.tagging.objectTypes{/lang}</legend>
+
+ <nav>
+ <ul>
+ {foreach from=$availableObjectTypes item=availableObjectType}
+ <li{if $objectType == $availableObjectType->objectType} class="active"{/if}><a href="{link controller='Tagged' object=$tag}objectType={@$availableObjectType->objectType}{/link}">{lang}wcf.tagging.objectType.{@$availableObjectType->objectType}{/lang}</a></li>
+ {/foreach}
+ </ul>
+ </nav>
+ </fieldset>
+
+ <fieldset>
+ <legend>{lang}wcf.tagging.tags{/lang}</legend>
+
+ <ul class="tagList">
+ {foreach from=$tags item=__tag}
+ <li><a href="{link controller='Tagged' object=$__tag}objectType={@$objectType}{/link}" rel="tag" style="font-size: {@$__tag->getSize()}%;">{$__tag->name}</a></li>
+ {/foreach}
+ </ul>
+ </fieldset>
+{/capture}
+
+{include file='header' sidebarOrientation='left'}
+
+<header class="boxHeadline">
+ <h1>{lang}wcf.tagging.taggedObjects.{@$objectType}{/lang}</h1>
+</header>
+
+{include file='userNotice'}
+
+<div class="contentNavigation">
+ {pages print=true assign=pagesLinks controller='Tagged' object=$tag link="objectType=$objectType&pageNo=%d"}
+
+ {hascontent}
+ <nav>
+ <ul>
+ {content}
+ {event name='contentNavigationButtonsTop'}
+ {/content}
+ </ul>
+ </nav>
+ {/hascontent}
+</div>
+
+{if $items}
+ {include file=$resultListTemplateName application=$resultListApplication}
+{else}
+ <p class="info">{lang}wcf.tagging.taggedObjects.noResults{/lang}</p>
+{/if}
+
+<div class="contentNavigation">
+ {@$pagesLinks}
+
+ {hascontent}
+ <nav>
+ <ul>
+ {content}
+ {event name='contentNavigationButtonsBottom'}
+ {/content}
+ </ul>
+ </nav>
+ {/hascontent}
+</div>
+
+{include file='footer'}
+
+</body>
+</html>
\ No newline at end of file
<category name="admin.content.label">
<parent>admin.content</parent>
</category>
+ <category name="admin.content.tag">
+ <parent>admin.content</parent>
+ </category>
<category name="admin.community">
<parent>admin</parent>
<defaultvalue>0</defaultvalue>
<admindefaultvalue>1</admindefaultvalue>
</option>
+
+ <option name="admin.content.tag.canManageTag">
+ <categoryname>admin.content.tag</categoryname>
+ <optiontype>boolean</optiontype>
+ <defaultvalue>0</defaultvalue>
+ <admindefaultvalue>1</admindefaultvalue>
+ </option>
</options>
</import>
</data>
--- /dev/null
+{include file='header' pageTitle='wcf.acp.tag.'|concat:$action}
+
+<header class="boxHeadline">
+ <h1>{lang}wcf.acp.tag.{$action}{/lang}</h1>
+</header>
+
+{if $errorField}
+ <p class="error">{lang}wcf.global.form.error{/lang}</p>
+{/if}
+
+{if $success|isset}
+ <p class="success">{lang}wcf.global.success.{$action}{/lang}</p>
+{/if}
+
+<div class="contentNavigation">
+ {hascontent}
+ <nav>
+ <ul>
+ {content}
+ {if $__wcf->session->getPermission('admin.tag.canDeleteTag') || $__wcf->session->getPermission('admin.tag.canEditTag')}
+ <li><a href="{link controller='TagList'}{/link}" class="button"><span class="icon icon16 icon-list"></span> <span>{lang}wcf.acp.menu.link.tag.list{/lang}</span></a></li>
+ {/if}
+
+ {event name='contentNavigationButtons'}
+ {/content}
+ </ul>
+ </nav>
+ {/hascontent}
+</div>
+
+<form method="post" action="{if $action == 'add'}{link controller='TagAdd'}{/link}{else}{link controller='TagEdit' object=$tagObj}{/link}{/if}">
+ <div class="container containerPadding marginTop">
+ <fieldset>
+ <legend>{lang}wcf.global.form.data{/lang}</legend>
+
+ <dl{if $errorField == 'name'} class="formError"{/if}>
+ <dt><label for="name">{lang}wcf.global.name{/lang}</label></dt>
+ <dd>
+ <input type="text" id="name" name="name" value="{$name}" required="required" autofocus="autofocus" class="medium" />
+ {if $errorField == 'name'}
+ <small class="innerError">
+ {if $errorType == 'empty'}
+ {lang}wcf.global.form.error.empty{/lang}
+ {elseif $errorType == 'duplicate'}
+ {lang}wcf.acp.tag.error.name.duplicate{/lang}
+ {/if}
+ </small>
+ {/if}
+ </dd>
+ </dl>
+
+ {hascontent}
+ <dl{if $errorField == 'languageID' || $action == 'edit'} class="{if $action == 'edit'}disabled{else}formError{/if}"{/if}>
+ <dt><label for="languageID">{lang}wcf.acp.tag.languageID{/lang}</label></dt>
+ <dd>
+ <select id="languageID" name="languageID"{if $action == 'edit'} disabled="disabled"{/if}>
+ {content}
+ {foreach from=$availableLanguages item=language}
+ <option value="{@$language->languageID}"{if $languageID == $language->languageID} selected="selected"{/if}>{$language->languageName} ({$language->languageCode})</option>
+ {/foreach}
+ {/content}
+ </select>
+ {if $errorField == 'languageID'}
+ <small class="innerError">
+ {lang}wcf.acp.tag.error.languageID.{$errorType}{/lang}
+ </small>
+ {/if}
+ </dd>
+ </dl>
+ {/hascontent}
+
+ {if !$tagObj|isset || $tagObj->synonymFor === null}
+ <dl>
+ <dt><label for="synonyms">{lang}wcf.acp.tag.synonyms{/lang}</label></dt>
+ <dd id="synonymList" class="editableItemList"></dd>
+ <dd>
+ <input id="synonyms" type="text" value="" class="long" />
+ {if $errorField == 'synonyms'}
+ <small class="innerError">
+ {if $errorType == 'duplicate'}
+ {lang}wcf.acp.tag.error.synonym.duplicate{/lang}
+ {/if}
+ </small>
+ {/if}
+ </dd>
+ </dl>
+
+ <script type="text/javascript" src="{@$__wcf->getPath()}js/WCF.Tagging{if !ENABLE_DEBUG_MODE}.min{/if}.js"></script>
+ <script type="text/javascript">
+ //<![CDATA[
+ $(function() {
+ var $tagList = new WCF.Tagging.TagList('#synonymList', '#synonyms');
+
+ {if $synonyms|isset && $synonyms|count}
+ $tagList.load([ {implode from=$synonyms item='synonym'}'{$synonym}'{/implode} ]);
+ {/if}
+ });
+ //]]>
+ </script>
+ {elseif $tagObj|isset}
+ <dl>
+ <dt><label for="synonyms">{lang}wcf.acp.tag.synonyms{/lang}</label></dt>
+ <dd>
+ <a href="{link controller='TagEdit' id=$tagObj->synonymFor}{/link}">{lang}wcf.acp.tag.synonyms.isSynonym{/lang}</a>
+ </dd>
+ </dl>
+ {/if}
+
+ {event name='dataFields'}
+ </fieldset>
+
+ {event name='fieldsets'}
+ </div>
+
+ <div class="formSubmit">
+ <input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s" />
+ </div>
+</form>
+
+{include file='footer'}
\ No newline at end of file
--- /dev/null
+{include file='header' pageTitle='wcf.acp.tag.list'}
+
+<script type="text/javascript">
+ //<![CDATA[
+ $(function() {
+ new WCF.Action.Delete('wcf\\data\\tag\\TagAction', '.jsTagRow');
+ });
+ //]]>
+</script>
+
+<header class="boxHeadline">
+ <h1>{lang}wcf.acp.tag.list{/lang}</h1>
+</header>
+
+{if $items}
+ <form action="{link controller='TagList'}{/link}">
+ <div class="container containerPadding marginTop">
+ <fieldset><legend>{lang}wcf.acp.tag.list.search{/lang}</legend>
+ <dl>
+ <dt><label for="search">{lang}wcf.acp.tag.list.search.query{/lang}</label></dt>
+ <dd>
+ <input type="search" id="search" name="search" value="{$search}" autofocus="autofocus" class="medium" />
+ </dd>
+ </dl>
+ </fieldset>
+
+ <div class="formSubmit">
+ <input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s" />
+ {@SID_INPUT_TAG}
+ </div>
+ </div>
+ </form>
+{/if}
+
+<div class="contentNavigation">
+ {pages print=true assign=pagesLinks controller="TagList" link="pageNo=%d&sortField=$sortField&sortOrder=$sortOrder&search=$search"}
+
+ <nav>
+ <ul>
+ <li><a href="{link controller='TagAdd'}{/link}" class="button"><span class="icon icon16 icon-plus"></span> <span>{lang}wcf.acp.tag.add{/lang}</span></a></li>
+
+ {event name='contentNavigationButtonsTop'}
+ </ul>
+ </nav>
+</div>
+
+{if $objects|count}
+ <div class="tabularBox tabularBoxTitle marginTop">
+ <header>
+ <h2>{lang}wcf.acp.tag.list{/lang} <span class="badge badgeInverse">{#$items}</span></h2>
+ </header>
+
+ <table class="table">
+ <thead>
+ <tr>
+ <th class="columnID columnTagID{if $sortField == 'tagID'} active {@$sortOrder}{/if}" colspan="2"><a href="{link controller='TagList'}pageNo={@$pageNo}&sortField=tagID&sortOrder={if $sortField == 'tagID' && $sortOrder == 'ASC'}DESC{else}ASC{/if}&search={@$search|rawurlencode}{/link}">{lang}wcf.global.objectID{/lang}</a></th>
+ <th class="columnTitle columnName{if $sortField == 'name'} active {@$sortOrder}{/if}"><a href="{link controller='TagList'}pageNo={@$pageNo}&sortField=name&sortOrder={if $sortField == 'name' && $sortOrder == 'ASC'}DESC{else}ASC{/if}&search={@$search|rawurlencode}{/link}">{lang}wcf.acp.tag.name{/lang}</a></th>
+ <th class="columnNumber columnUsageCount{if $sortField == 'usageCount'} active {@$sortOrder}{/if}"><a href="{link controller='TagList'}pageNo={@$pageNo}&sortField=usageCount&sortOrder={if $sortField == 'usageCount' && $sortOrder == 'ASC'}DESC{else}ASC{/if}&search={@$search|rawurlencode}{/link}">{lang}wcf.acp.tag.usageCount{/lang}</a></th>
+ <th class="columnText columnLanguage{if $sortField == 'language'} active {@$sortOrder}{/if}"><a href="{link controller='TagList'}pageNo={@$pageNo}&sortField=language&sortOrder={if $sortField == 'language' && $sortOrder == 'ASC'}DESC{else}ASC{/if}&search={@$search|rawurlencode}{/link}">{lang}wcf.acp.tag.languageID{/lang}</a></th>
+ <th class="columnText columnSynonymFor">{lang}wcf.acp.tag.synonymFor{/lang}</th>
+
+ {event name='columnHeads'}
+ </tr>
+ </thead>
+
+ <tbody>
+ {foreach from=$objects item=tag}
+ <tr class="jsTagRow">
+ <td class="columnIcon">
+ <a href="{link controller='TagEdit' object=$tag}{/link}" title="{lang}wcf.global.button.edit{/lang}" class="jsTooltip"><span class="icon icon16 icon-pencil"></span></a>
+ <span class="icon icon16 icon-remove jsDeleteButton jsTooltip pointer" title="{lang}wcf.global.button.delete{/lang}" data-object-id="{@$tag->tagID}" data-confirm-message="{lang}wcf.acp.tag.delete.sure{/lang}"></span>
+
+ {event name='rowButtons'}
+ </td>
+ <td class="columnID">{#$tag->tagID}</td>
+ <td class="columnTitle columnName"><a href="{link controller='TagEdit' object=$tag}{/link}" class="badge">{$tag->name}</a></td>
+ <td class="columnNumber columnUsageCount">{if $tag->synonymFor === null}{#$tag->usageCount}{/if}</td>
+ <td class="columnText columnLanguage">{if $tag->languageName !== null}{$tag->languageName} ({$tag->languageCode}){/if}</td>
+ <td class="columnText columnSynonymFor">{if $tag->synonymFor !== null}<a href="{link controller='TagList'}search={@$tag->synonymName|rawurlencode}{/link}" class="badge">{$tag->synonymName}</a>{/if}</td>
+
+ {event name='columns'}
+ </tr>
+ {/foreach}
+ </tbody>
+ </table>
+
+ </div>
+
+ <div class="contentNavigation">
+ {@$pagesLinks}
+
+ <nav>
+ <ul>
+ <li><a href="{link controller='TagAdd'}{/link}" class="button"><span class="icon icon16 icon-plus"></span> <span>{lang}wcf.acp.tag.add{/lang}</span></a></li>
+
+ {event name='contentNavigationButtonsBottom'}
+ </ul>
+ </nav>
+ </div>
+{else}
+ <p class="info">{lang}wcf.acp.tag.noneAvailable{/lang}</p>
+{/if}
+
+{include file='footer'}
--- /dev/null
+/**
+ * Tagging System for WCF
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2013 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ */
+
+/**
+ * Namespace for tagging related functions.
+ */
+WCF.Tagging = {};
+
+/**
+ * Editable tag list.
+ *
+ * @see WCF.EditableItemList
+ */
+WCF.Tagging.TagList = WCF.EditableItemList.extend({
+ /**
+ * @see WCF.EditableItemList._className
+ */
+ _className: 'wcf\\data\\tag\\TagAction',
+
+ /**
+ * maximum tag length
+ * @var integer
+ */
+ _maxLength: 0,
+
+ /**
+ * @see WCF.EditableItemList.init()
+ */
+ init: function(itemListSelector, searchInputSelector, maxLength) {
+ this._allowCustomInput = true;
+ this._maxLength = maxLength;
+
+ this._super(itemListSelector, searchInputSelector);
+
+ this._data = [ ];
+ this._search = new WCF.Tagging.TagSearch(this._searchInput, $.proxy(this.addItem, this));
+ this._itemList.addClass('tagList');
+ },
+
+ /**
+ * @see WCF.EditableItemList._keyDown()
+ */
+ _keyDown: function(event) {
+ if (this._super(event)) {
+ // ignore submit event
+ if (event === null) {
+ return true;
+ }
+
+ var $keyCode = event.which;
+ // allow [backspace], [escape], [enter] and [delete]
+ if ($keyCode === 8 || $keyCode === 27 || $keyCode === 13 || $keyCode === 46) {
+ return true;
+ }
+ else if ($keyCode > 36 && $keyCode < 41) {
+ // allow arrow keys (37-40)
+ return true;
+ }
+
+ if (this._searchInput.val().length >= this._maxLength) {
+ return false;
+ }
+
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * @see WCF.EditableItemList._submit()
+ */
+ _submit: function() {
+ this._super();
+
+ for (var $i = 0, $length = this._data.length; $i < $length; $i++) {
+ // deleting items leaves crappy indices
+ if (this._data[$i]) {
+ $('<input type="hidden" name="tags[]" value="' + this._data[$i] + '" />').appendTo(this._form);
+ }
+ };
+ },
+
+ /**
+ * @see WCF.EditableItemList.addItem()
+ */
+ addItem: function(data) {
+ // enforce max length by trimming values
+ if (!data.objectID && data.label.length > this._maxLength) {
+ data.label = data.label.substr(0, this._maxLength);
+ }
+
+ var result = this._super(data);
+ $(this._itemList).find('.badge:not(tag)').addClass('tag');
+
+ return result;
+ },
+
+ /**
+ * @see WCF.EditableItemList._addItem()
+ */
+ _addItem: function(objectID, label) {
+ this._data.push(label);
+ },
+
+ /**
+ * @see WCF.EditableItemList._removeItem()
+ */
+ _removeItem: function(objectID, label) {
+ for (var $i = 0, $length = this._data.length; $i < $length; $i++) {
+ if (this._data[$i] === label) {
+ delete this._data[$i];
+ return;
+ }
+ }
+ },
+
+ /**
+ * @see WCF.EditableItemList.load()
+ */
+ load: function(data) {
+ if (data && data.length) {
+ for (var $i = 0, $length = data.length; $i < $length; $i++) {
+ this.addItem({ objectID: 0, label: data[$i] });
+ }
+ }
+ }
+});
+
+/**
+ * Search handler for tags.
+ *
+ * @see WCF.Search.Base
+ */
+WCF.Tagging.TagSearch = WCF.Search.Base.extend({
+ /**
+ * @see WCF.Search.Base._className
+ */
+ _className: 'wcf\\data\\tag\\TagAction',
+
+ /**
+ * @see WCF.Search.Base.init()
+ */
+ init: function(searchInput, callback, excludedSearchValues, commaSeperated) {
+ this._super(searchInput, callback, excludedSearchValues, commaSeperated, false);
+ }
+});
\ No newline at end of file
--- /dev/null
+WCF.Tagging={};WCF.Tagging.TagList=WCF.EditableItemList.extend({_className:"wcf\\data\\tag\\TagAction",_maxLength:0,init:function(c,a,b){this._allowCustomInput=true;this._maxLength=b;this._super(c,a);this._data=[];this._search=new WCF.Tagging.TagSearch(this._searchInput,$.proxy(this.addItem,this));this._itemList.addClass("tagList")},_keyDown:function(b){if(this._super(b)){if(b===null){return true}var a=b.which;if(a===8||a===27||a===13||a===46){return true}else{if(a>36&&a<41){return true}}if(this._searchInput.val().length>=this._maxLength){return false}return true}return false},_submit:function(){this._super();for(var b=0,a=this._data.length;b<a;b++){if(this._data[b]){$('<input type="hidden" name="tags[]" value="'+this._data[b]+'" />').appendTo(this._form)}}},addItem:function(b){if(!b.objectID&&b.label.length>this._maxLength){b.label=b.label.substr(0,this._maxLength)}var a=this._super(b);$(this._itemList).find(".badge:not(tag)").addClass("tag");return a},_addItem:function(b,a){this._data.push(a)},_removeItem:function(d,a){for(var c=0,b=this._data.length;c<b;c++){if(this._data[c]===a){delete this._data[c];return}}},load:function(a){if(a&&a.length){for(var c=0,b=a.length;c<b;c++){this.addItem({objectID:0,label:a[c]})}}}});WCF.Tagging.TagSearch=WCF.Search.Base.extend({_className:"wcf\\data\\tag\\TagAction",init:function(b,d,a,c){this._super(b,d,a,c,false)}});
\ No newline at end of file
--- /dev/null
+<?php
+namespace wcf\acp\form;
+use wcf\data\tag\Tag;
+use wcf\data\tag\TagAction;
+use wcf\data\tag\TagEditor;
+use wcf\form\AbstractForm;
+use wcf\system\exception\UserInputException;
+use wcf\system\language\LanguageFactory;
+use wcf\system\WCF;
+use wcf\util\ArrayUtil;
+use wcf\util\StringUtil;
+
+/**
+ * Shows the tag add form.
+ *
+ * @author Tim Duesterhus
+ * @copyright 2001-2013 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf.tagging
+ * @subpackage acp.form
+ * @category Community Framework
+ */
+class TagAddForm extends AbstractForm {
+ /**
+ * @see wcf\page\AbstractPage::$activeMenuItem
+ */
+ public $activeMenuItem = 'wcf.acp.menu.link.tag.add';
+
+ /**
+ * @see wcf\page\AbstractPage::$neededPermissions
+ */
+ public $neededPermissions = array('admin.content.tag.canManageTag');
+
+ /**
+ * list of available languages
+ * @var array
+ */
+ public $availableLanguages = array();
+
+ /**
+ * name value
+ * @var string
+ */
+ public $name = '';
+
+ /**
+ * language value
+ * @var string
+ */
+ public $languageID = 0;
+
+ /**
+ * synonyms
+ * @var array<string>
+ */
+ public $synonyms = array();
+
+ /**
+ * @see wcf\page\IPage::readParameters()
+ */
+ public function readParameters() {
+ parent::readParameters();
+
+ $this->availableLanguages = LanguageFactory::getInstance()->getContentLanguages();
+ }
+
+ /**
+ * @see wcf\form\IForm::readFormParameters()
+ */
+ public function readFormParameters() {
+ parent::readFormParameters();
+
+ if (isset($_POST['name'])) $this->name = StringUtil::trim($_POST['name']);
+ if (isset($_POST['languageID'])) $this->languageID = intval($_POST['languageID']);
+
+ // actually these are synonyms
+ if (isset($_POST['tags']) && is_array($_POST['tags'])) $this->synonyms = ArrayUtil::trim($_POST['tags']);
+ }
+
+ /**
+ * @see wcf\form\IForm::validate()
+ */
+ public function validate() {
+ parent::validate();
+
+ if (empty($this->name)) {
+ throw new UserInputException('name');
+ }
+
+ // validate language
+ if (empty($this->availableLanguages)) {
+ // force default language id
+ $this->languageID = LanguageFactory::getInstance()->getDefaultLanguageID();
+ }
+ else {
+ if (!isset($this->availableLanguages[$this->languageID])) {
+ throw new UserInputException('languageID', 'notFound');
+ }
+ }
+
+ // check for duplicates
+ $tag = Tag::getTag($this->name, $this->languageID);
+ if ($tag !== null && (!isset($this->tagObj) || $tag->tagID != $this->tagObj->tagID)) {
+ throw new UserInputException('name', 'duplicate');
+ }
+
+ // validate synonyms
+ foreach ($this->synonyms as $synonym) {
+ if (StringUtil::toLowerCase($synonym) == StringUtil::toLowerCase($this->name)) throw new UserInputException('synonyms', 'duplicate');
+ }
+ }
+
+ /**
+ * @see wcf\page\IPage::readData()
+ */
+ public function readData() {
+ parent::readData();
+
+ if (empty($_POST)) {
+ // pre-select default language id
+ if (!empty($this->availableLanguages)) {
+ $this->languageID = LanguageFactory::getInstance()->getDefaultLanguageID();
+ if (!isset($this->availableLanguages[$this->languageID])) {
+ // language id is not within content languages, try user's language instead
+ $this->languageID = WCF::getUser()->languageID;
+ if (!isset($this->availableLanguages[$this->languageID])) {
+ // this installation is weird, just select nothing
+ $this->languageID = 0;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * @see wcf\form\IForm::save()
+ */
+ public function save() {
+ parent::save();
+
+ // save tag
+ $this->objectAction = new TagAction(array(), 'create', array('data' => array(
+ 'name' => $this->name,
+ 'languageID' => $this->languageID
+ )));
+ $this->objectAction->executeAction();
+ $returnValues = $this->objectAction->getReturnValues();
+ $editor = new TagEditor($returnValues['returnValues']);
+
+ foreach ($this->synonyms as $synonym) {
+ if (empty($synonym)) continue;
+
+ // find existing tag
+ $synonymObj = Tag::getTag($synonym, $this->languageID);
+ if ($synonymObj === null) {
+ $synonymAction = new TagAction(array(), 'create', array('data' => array(
+ 'name' => $synonym,
+ 'languageID' => $this->languageID,
+ 'synonymFor' => $editor->tagID
+ )));
+ $synonymAction->executeAction();
+ }
+ else {
+ $editor->addSynonym($synonymObj);
+ }
+ }
+
+ $this->saved();
+
+ // reset values
+ $this->name = '';
+ $this->synonyms = array();
+
+ // show success
+ WCF::getTPL()->assign(array(
+ 'success' => true
+ ));
+ }
+
+ /**
+ * @see wcf\page\IPage::assignVariables()
+ */
+ public function assignVariables() {
+ parent::assignVariables();
+
+ WCF::getTPL()->assign(array(
+ 'action' => 'add',
+ 'availableLanguages' => $this->availableLanguages,
+ 'name' => $this->name,
+ 'languageID' => $this->languageID,
+ 'synonyms' => $this->synonyms
+ ));
+ }
+}
--- /dev/null
+<?php
+namespace wcf\acp\form;
+use wcf\data\tag\Tag;
+use wcf\data\tag\TagAction;
+use wcf\data\tag\TagEditor;
+use wcf\data\tag\TagList;
+use wcf\form\AbstractForm;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\WCF;
+
+/**
+ * Shows the tag edit form.
+ *
+ * @author Tim Duesterhus
+ * @copyright 2001-2013 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf.tagging
+ * @subpackage acp.form
+ * @category Community Framework
+ */
+class TagEditForm extends TagAddForm {
+ /**
+ * @see wcf\page\AbstractPage::$activeMenuItem
+ */
+ public $activeMenuItem = 'wcf.acp.menu.link.tag';
+
+ /**
+ * @see wcf\page\AbstractPage::$neededPermissions
+ */
+ public $neededPermissions = array('admin.content.tag.canManageTag');
+
+ /**
+ * tag id
+ * @var integer
+ */
+ public $tagID = 0;
+
+ /**
+ * tag object
+ * @var wcf\data\tag\Tag
+ */
+ public $tagObj = null;
+
+ /**
+ * @see wcf\page\IPage::readParameters()
+ */
+ public function readParameters() {
+ parent::readParameters();
+
+ if (isset($_REQUEST['id'])) $this->tagID = intval($_REQUEST['id']);
+ $this->tagObj = new Tag($this->tagID);
+ if (!$this->tagObj->tagID) {
+ throw new IllegalLinkException();
+ }
+ }
+
+ /**
+ * @see wcf\form\IForm::save()
+ */
+ public function save() {
+ AbstractForm::save();
+
+ // update tag
+ $this->objectAction = new TagAction(array($this->tagID), 'update', array('data' => array(
+ 'name' => $this->name
+ )));
+ $this->objectAction->executeAction();
+
+ if ($this->tagObj->synonymFor === null) {
+ // remove synonyms first
+ $sql = "UPDATE wcf".WCF_N."_tag
+ SET synonymFor = ?
+ WHERE synonymFor = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute(array(
+ null,
+ $this->tagID
+ ));
+
+ $editor = new TagEditor($this->tagObj);
+ foreach ($this->synonyms as $synonym) {
+ if (empty($synonym)) continue;
+
+ // find existing tag
+ $synonymObj = Tag::getTag($synonym, $this->languageID);
+ if ($synonymObj === null) {
+ $synonymAction = new TagAction(array(), 'create', array('data' => array(
+ 'name' => $synonym,
+ 'languageID' => $this->languageID,
+ 'synonymFor' => $this->tagID
+ )));
+ $synonymAction->executeAction();
+ }
+ else {
+ $editor->addSynonym($synonymObj);
+ }
+ }
+ }
+
+ $this->saved();
+
+ // show success
+ WCF::getTPL()->assign(array(
+ 'success' => true
+ ));
+ }
+
+ /**
+ * @see wcf\page\IPage::readData()
+ */
+ public function readData() {
+ parent::readData();
+
+ if (empty($_POST)) {
+ $this->name = $this->tagObj->name;
+ }
+
+ $this->languageID = $this->tagObj->languageID;
+
+ $synonymList = new TagList();
+ $synonymList->getConditionBuilder()->add('synonymFor = ?', array($this->tagObj->tagID));
+ $synonymList->readObjects();
+ $this->synonyms = array();
+ foreach ($synonymList as $synonym) {
+ $this->synonyms[] = $synonym->name;
+ }
+ }
+
+ /**
+ * @see wcf\page\IPage::assignVariables()
+ */
+ public function assignVariables() {
+ parent::assignVariables();
+
+ WCF::getTPL()->assign(array(
+ 'tagObj' => $this->tagObj,
+ 'action' => 'edit'
+ ));
+ }
+}
--- /dev/null
+<?php
+namespace wcf\acp\page;
+use wcf\page\SortablePage;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Shows a list of tags.
+ *
+ * @author Tim Duesterhus
+ * @copyright 2001-2013 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf.tagging
+ * @subpackage acp.page
+ * @category Community Framework
+ */
+class TagListPage extends SortablePage {
+ /**
+ * @see wcf\page\AbstractPage::$activeMenuItem
+ */
+ public $activeMenuItem = 'wcf.acp.menu.link.tag.list';
+
+ /**
+ * @see wcf\page\AbstractPage::$neededPermissions
+ */
+ public $neededPermissions = array('admin.content.tag.canManageTag');
+
+ /**
+ * @see wcf\page\SortablePage::$defaultSortField
+ */
+ public $defaultSortField = 'name';
+
+ /**
+ * @see wcf\page\SortablePage::$validSortFields
+ */
+ public $validSortFields = array('tagID', 'languageID', 'name', 'usageCount');
+
+ /**
+ * @see wcf\page\MultipleLinkPage::$objectListClassName
+ */
+ public $objectListClassName = 'wcf\data\tag\TagList';
+
+ /**
+ * search-query
+ * @var string
+ */
+ public $search = '';
+
+ /**
+ * @see wcf\page\IPage::assignVariables()
+ */
+ public function assignVariables() {
+ parent::assignVariables();
+
+ WCF::getTPL()->assign(array(
+ 'search' => $this->search
+ ));
+ }
+
+ /**
+ * @see wcf\page\IPage::readParameters()
+ */
+ public function readParameters() {
+ parent::readParameters();
+
+ if (isset($_REQUEST['search'])) $this->search = StringUtil::trim($_REQUEST['search']);
+ }
+
+ /**
+ * @see wcf\page\MultipleLinkPage::initObjectList()
+ */
+ protected function initObjectList() {
+ parent::initObjectList();
+
+ $this->objectList->sqlSelects = "(SELECT COUNT(*) FROM wcf".WCF_N."_tag_to_object t2o WHERE t2o.tagID = tag.tagID) AS usageCount";
+ $this->objectList->sqlSelects .= ", language.languageName, language.languageCode";
+ $this->objectList->sqlSelects .= ", synonym.name AS synonymName";
+
+ $this->objectList->sqlJoins = "LEFT JOIN wcf".WCF_N."_language language ON tag.languageID = language.languageID";
+ $this->objectList->sqlJoins .= " LEFT JOIN wcf".WCF_N."_tag synonym ON tag.synonymFor = synonym.tagID";
+
+ if ($this->search !== '') {
+ $this->objectList->getConditionBuilder()->add('tag.name LIKE ?', array($this->search.'%'));
+ }
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\tag;
+use wcf\data\DatabaseObject;
+use wcf\system\request\IRouteController;
+use wcf\system\WCF;
+use wcf\util\ArrayUtil;
+
+/**
+ * Represents a tag.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2013 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf.tagging
+ * @subpackage data.tag
+ * @category Community Framework
+ */
+class Tag extends DatabaseObject implements IRouteController {
+ /**
+ * @see wcf\data\DatabaseObject::$databaseTableName
+ */
+ protected static $databaseTableName = 'tag';
+
+ /**
+ * @see wcf\data\DatabaseObject::$databaseIndexName
+ */
+ protected static $databaseTableIndexName = 'tagID';
+
+ /**
+ * Return the tag with the given name or null of no such tag exists.
+ *
+ * @param string $name
+ * @param integer $languageID
+ * @return mixed
+ */
+ public static function getTag($name, $languageID = 0) {
+ $sql = "SELECT *
+ FROM wcf".WCF_N."_tag
+ WHERE languageID = ?
+ AND name = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute(array($languageID, $name));
+ $row = $statement->fetchArray();
+ if ($row !== false) return new Tag(null, $row);
+
+ return null;
+ }
+
+ /**
+ * Takes a string of comma separated tags and splits it into an array.
+ *
+ * @param string $tags
+ * @param string $separators
+ * @return array<string>
+ */
+ public static function splitString($tags, $separators = ',;') {
+ return array_unique(ArrayUtil::trim(preg_split('/['.preg_quote($separators).']/', $tags)));
+ }
+
+ /**
+ * Takes a list of tags and builds a comma separated string from it.
+ *
+ * @param array<mixed> $tags
+ * @param string $separator
+ * @return string
+ */
+ public static function buildString(array $tags, $separator = ', ') {
+ $string = '';
+ foreach ($tags as $tag) {
+ if (!empty($string)) $string .= $separator;
+ $string .= (is_object($tag) ? $tag->__toString() : $tag);
+ }
+
+ return $string;
+ }
+
+ /**
+ * @see wcf\data\ITitledObject::getTitle()
+ */
+ public function getTitle() {
+ return $this->name;
+ }
+
+ /**
+ * Returns the name of this tag.
+ *
+ * @return string
+ */
+ public function __toString() {
+ return $this->getTitle();
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\tag;
+use wcf\data\AbstractDatabaseObjectAction;
+use wcf\data\ISearchAction;
+use wcf\system\database\util\PreparedStatementConditionBuilder;
+use wcf\system\exception\UserInputException;
+use wcf\system\WCF;
+
+/**
+ * Executes tagging-related actions.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2013 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf.tagging
+ * @subpackage data.tag
+ * @category Community Framework
+ */
+class TagAction extends AbstractDatabaseObjectAction implements ISearchAction {
+ /**
+ * @see wcf\data\AbstractDatabaseObjectAction
+ */
+ protected $allowGuestAccess = array('getSearchResultList');
+
+ /**
+ * @see wcf\data\AbstractDatabaseObjectAction::$className
+ */
+ protected $className = 'wcf\data\tag\TagEditor';
+
+ /**
+ * @see \wcf\data\AbstractDatabaseObjectAction::$permissionsDelete
+ */
+ protected $permissionsDelete = array('admin.content.tag.canManageTag');
+
+ /**
+ * @see \wcf\data\AbstractDatabaseObjectAction::$permissionsUpdate
+ */
+ protected $permissionsUpdate = array('admin.content.tag.canManageTag');
+
+ /**
+ * @see wcf\data\ISearchAction::validateGetSearchResultList()
+ */
+ public function validateGetSearchResultList() {
+ $this->readString('searchString', false, 'data');
+
+ if (isset($this->parameters['data']['excludedSearchValues']) && !is_array($this->parameters['data']['excludedSearchValues'])) {
+ throw new UserInputException('excludedSearchValues');
+ }
+ }
+
+ /**
+ * @see wcf\data\ISearchAction::getSearchResultList()
+ */
+ public function getSearchResultList() {
+ $excludedSearchValues = array();
+ if (isset($this->parameters['data']['excludedSearchValues'])) {
+ $excludedSearchValues = $this->parameters['data']['excludedSearchValues'];
+ }
+ $list = array();
+
+ $conditionBuilder = new PreparedStatementConditionBuilder();
+ $conditionBuilder->add("name LIKE ?", array($this->parameters['data']['searchString'].'%'));
+ if (!empty($excludedSearchValues)) {
+ $conditionBuilder->add("name NOT IN (?)", array($excludedSearchValues));
+ }
+
+ // find tags
+ $sql = "SELECT tagID, name
+ FROM wcf".WCF_N."_tag
+ ".$conditionBuilder;
+ $statement = WCF::getDB()->prepareStatement($sql, 5);
+ $statement->execute($conditionBuilder->getParameters());
+ while ($row = $statement->fetchArray()) {
+ $list[] = array(
+ 'label' => $row['name'],
+ 'objectID' => $row['tagID']
+ );
+ }
+
+ return $list;
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\tag;
+use wcf\data\DatabaseObjectDecorator;
+
+/**
+ * Represents a tag in a tag cloud.
+ *
+ * @author Marcel Werk
+ * @copyright 2009-2013 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf.tagging
+ * @subpackage data.tag
+ * @category Community Framework
+ */
+class TagCloudTag extends DatabaseObjectDecorator {
+ /**
+ * @see wcf\data\DatabaseObjectDecorator::$baseClass
+ */
+ protected static $baseClass = 'wcf\data\tag\Tag';
+
+ /**
+ * size of the tag in a weighted list
+ * @var double
+ */
+ protected $size = 0.0;
+
+ /**
+ * Sets the size of the tag.
+ *
+ * @param double $size
+ */
+ public function setSize($size) {
+ $this->size = $size;
+ }
+
+ /**
+ * Returns the size of the tag.
+ *
+ * @return double
+ */
+ public function getSize() {
+ return $this->size;
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\tag;
+use wcf\data\DatabaseObjectEditor;
+use wcf\system\WCF;
+
+/**
+ * Provides functions to edit tags.
+ *
+ * @author Tim Duesterhus, Marcel Werk
+ * @copyright 2001-2013 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf.tagging
+ * @subpackage data.tag
+ * @category Community Framework
+ */
+class TagEditor extends DatabaseObjectEditor {
+ /**
+ * @see wcf\data\DatabaseObjectEditor::$baseClass
+ */
+ protected static $baseClass = 'wcf\data\tag\Tag';
+
+ /**
+ * Adds the given tag, and all of it's synonyms as a synonym.
+ *
+ * @param wcf\data\tag\Tag $synonym
+ */
+ public function addSynonym(Tag $synonym) {
+ // clear up objects with both tags: the target and the synonym
+ // TODO: Optimize this!
+ $sql = "SELECT (objectTypeID || '-' || languageID || '-' || objectID || '-' || tagID) AS hash
+ FROM wcf".WCF_N."_tag_to_object
+ WHERE tagID = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute(array(
+ $this->tagID
+ ));
+ $parameters = array($this->tagID, $synonym->tagID, $this->tagID, ' ');
+ $notIn = '?';
+ while ($row = $statement->fetchArray()) {
+ $parameters[] = $row['hash'];
+ $notIn .= ', ?';
+ }
+
+ $sql = "UPDATE wcf".WCF_N."_tag_to_object
+ SET tagID = ?
+ WHERE tagID = ?
+ AND ".str_replace('tagID', '?', $concat)." NOT IN (".$notIn.")";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute($parameters);
+
+ $sql = "DELETE FROM wcf".WCF_N."_tag_to_object
+ WHERE tagID = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute(array(
+ $synonym->tagID,
+ ));
+
+ $editor = new TagEditor($synonym);
+ $editor->update(array(
+ 'synonymFor' => $this->tagID
+ ));
+
+ $synonymList = new TagList();
+ $synonymList->getConditionBuilder()->add('synonymFor = ?', array($synonym->tagID));
+ $synonymList->readObjects();
+
+ foreach ($synonymList as $synonym) {
+ $this->addSynonym($synonym);
+ }
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\tag;
+use wcf\data\DatabaseObjectList;
+
+/**
+ * Represents a list of tags.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2012 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf.tagging
+ * @subpackage data.tag
+ * @category Community Framework
+ */
+class TagList extends DatabaseObjectList {
+ /**
+ * @see wcf\data\DatabaseObjectList::$className
+ */
+ public $className = 'wcf\data\tag\Tag';
+}
--- /dev/null
+<?php
+namespace wcf\page;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\data\tag\Tag;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\tagging\TypedTagCloud;
+use wcf\system\WCF;
+
+/**
+ * Shows the a list of tagged objects.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2013 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf.tagging
+ * @subpackage page
+ * @category Community Framework
+ */
+class TaggedPage extends MultipleLinkPage {
+ /**
+ * tag id
+ * @var integer
+ */
+ public $tagID = 0;
+
+ /**
+ * tag object
+ * @var wcf\data\tag\Tag
+ */
+ public $tag = null;
+
+ /**
+ * object type object
+ * @var wcf\data\object\type\ObjectType
+ */
+ public $objectType = null;
+
+ /**
+ * tag cloud
+ * @var wcf\system\tagging\TypedTagCloud
+ */
+ public $tagCloud = null;
+
+ /**
+ * @see wcf\page\IPage::readParameters()
+ */
+ public function readParameters() {
+ parent::readParameters();
+
+ // get tag id
+ if (isset($_REQUEST['id'])) $this->tagID = intval($_REQUEST['id']);
+ $this->tag = new Tag($this->tagID);
+ if (!$this->tag->tagID) {
+ throw new IllegalLinkException();
+ }
+
+ // get object type
+ if (isset($_REQUEST['objectType'])) {
+ $this->objectType = ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.tagging.taggableObject', $_REQUEST['objectType']);
+ if ($this->objectType === null) {
+ throw new IllegalLinkException();
+ }
+ }
+ else {
+ // use first object type
+ $objectTypes = ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.tagging.taggableObject');
+ $this->objectType = reset($objectTypes);
+ }
+ }
+
+ /**
+ * @see wcf\page\MultipleLinkPage::readParameters()
+ */
+ protected function initObjectList() {
+ $this->objectList = $this->objectType->getProcessor()->getObjectList($this->tag);
+ }
+
+ /**
+ * @see wcf\page\IPage::readData()
+ */
+ public function readData() {
+ parent::readData();
+
+ $this->tagCloud = new TypedTagCloud($this->objectType->objectType);
+ }
+
+ /**
+ * @see wcf\page\IPage::assignVariables()
+ */
+ public function assignVariables() {
+ parent::assignVariables();
+
+ WCF::getTPL()->assign(array(
+ 'tag' => $this->tag,
+ 'tags' => $this->tagCloud->getTags(100),
+ 'availableObjectTypes' => ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.tagging.taggableObject'),
+ 'objectType' => $this->objectType->objectType,
+ 'resultListTemplateName' => $this->objectType->getProcessor()->getTemplateName(),
+ 'resultListApplication' => $this->objectType->getProcessor()->getApplication(),
+ 'allowSpidersToIndexThisPage' => true
+ ));
+ }
+}
--- /dev/null
+<?php
+namespace wcf\system\cache\builder;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\data\tag\Tag;
+use wcf\data\tag\TagCloudTag;
+use wcf\system\database\util\PreparedStatementConditionBuilder;
+use wcf\system\WCF;
+
+/**
+ * Caches the tag cloud.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2013 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf.tagging
+ * @subpackage system.cache.builder
+ * @category Community Framework
+ */
+class TagCloudCacheBuilder extends AbstractCacheBuilder {
+ /**
+ * list of tags
+ * @var array<wcf\data\tag\TagCloudTag>
+ */
+ protected $tags = array();
+
+ /**
+ * language ids
+ * @var integer
+ */
+ protected $languageIDs = array();
+
+ /**
+ * @see wcf\system\cache\builder\AbstractCacheBuilder::$maxLifetime
+ */
+ protected $maxLifetime = 3600;
+
+ /**
+ * object type ids
+ * @var integer
+ */
+ protected $objectTypeIDs = array();
+
+ /**
+ * @see wcf\system\cache\builder\AbstractCacheBuilder::rebuild()
+ */
+ protected function rebuild(array $parameters) {
+ $this->languageIDs = $this->parseLanguageIDs($parameters);
+
+ // get all taggable types
+ $objectTypes = ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.tagging.taggableObject');
+ foreach ($objectTypes as $objectType) {
+ $this->objectTypeIDs[] = $objectType->objectTypeID;
+ }
+
+ // get tags
+ $this->getTags();
+
+ return $this->tags;
+ }
+
+ /**
+ * Parses a list of language ids. If one given language id evaluates to '0' all ids will be discarded.
+ *
+ * @param array<integer> $parameters
+ * @return array<integer>
+ */
+ protected function parseLanguageIDs(array $parameters) {
+ // handle special '0' value
+ if (in_array(0, $parameters)) {
+ // discard all language ids
+ $parameters = array();
+ }
+
+ return $parameters;
+ }
+
+ /**
+ * Reads associated tags.
+ */
+ protected function getTags() {
+ if (!empty($this->objectTypeIDs)) {
+ // get tag ids
+ $tagIDs = array();
+ $conditionBuilder = new PreparedStatementConditionBuilder();
+ $conditionBuilder->add('object.objectTypeID IN (?)', array($this->objectTypeIDs));
+ $conditionBuilder->add('object.languageID IN (?)', array($this->languageIDs));
+ $sql = "SELECT COUNT(*) AS counter, object.tagID
+ FROM wcf".WCF_N."_tag_to_object object
+ ".$conditionBuilder->__toString()."
+ GROUP BY object.tagID
+ ORDER BY counter DESC";
+ $statement = WCF::getDB()->prepareStatement($sql, 500);
+ $statement->execute($conditionBuilder->getParameters());
+ while ($row = $statement->fetchArray()) {
+ $tagIDs[$row['tagID']] = $row['counter'];
+ }
+
+ // get tags
+ if (!empty($tagIDs)) {
+ $sql = "SELECT *
+ FROM wcf".WCF_N."_tag
+ WHERE tagID IN (?".(count($tagIDs) > 1 ? str_repeat(',?', count($tagIDs) - 1) : '').")";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute(array_keys($tagIDs));
+ while ($row = $statement->fetchArray()) {
+ $row['counter'] = $tagIDs[$row['tagID']];
+ $this->tags[$row['name']] = new TagCloudTag(new Tag(null, $row));
+ }
+
+ // sort by counter
+ uasort($this->tags, array('self', 'compareTags'));
+ }
+ }
+ }
+
+ /**
+ * Compares the weight between two tags.
+ *
+ * @param wcf\data\tag\TagCloudTag $tagA
+ * @param wcf\data\tag\TagCloudTag $tagB
+ * @return integer
+ */
+ protected static function compareTags($tagA, $tagB) {
+ if ($tagA->counter > $tagB->counter) return -1;
+ if ($tagA->counter < $tagB->counter) return 1;
+ return 0;
+ }
+}
--- /dev/null
+<?php
+namespace wcf\system\cache\builder;
+
+/**
+ * Caches the typed tag cloud.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2013 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf.tagging
+ * @subpackage system.cache.builder
+ * @category Community Framework
+ */
+class TypedTagCloudCacheBuilder extends TagCloudCacheBuilder {
+ /**
+ * @see wcf\system\cache\builder\AbstractCacheBuilder::rebuild()
+ */
+ protected function rebuild(array $parameters) {
+ $this->objectTypeIDs = $parameters['objectTypeIDs'];
+ $this->languageIDs = $parameters['languageIDs'];
+
+ // get tags
+ $this->getTags();
+
+ return $this->tags;
+ }
+}
--- /dev/null
+<?php
+namespace wcf\system\tagging;
+use wcf\data\tag\Tag;
+
+/**
+ * Any object type that is taggable, can implement this interface.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2012 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf.tagging
+ * @subpackage system.tagging
+ * @category Community Framework
+ */
+interface ITaggable {
+ /**
+ * Returns a list of tagged objects.
+ *
+ * @param wcf\data\tag\Tag $tag
+ * @return wcf\data\DatabaseObjectList
+ */
+ public function getObjectList(Tag $tag);
+
+ /**
+ * Returns the template name for the result output.
+ *
+ * @return string
+ */
+ public function getTemplateName();
+
+ /**
+ * Returns the application of the result template.
+ *
+ * @return string
+ */
+ public function getApplication();
+}
--- /dev/null
+<?php
+namespace wcf\system\tagging;
+
+/**
+ * Any tagged object has to implement this interface.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2013 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf.tagging
+ * @subpackage system.tagging
+ * @category Community Framework
+ */
+interface ITagged {
+ /**
+ * Gets the id of the tagged object.
+ *
+ * @return integer the id to get
+ */
+ public function getObjectID();
+
+ /**
+ * Gets the taggable type of this tagged object.
+ *
+ * @return wcf\system\tagging\ITaggable
+ */
+ public function getTaggable();
+}
--- /dev/null
+<?php
+namespace wcf\system\tagging;
+use wcf\system\cache\builder\TagCloudCacheBuilder;
+use wcf\system\language\LanguageFactory;
+
+/**
+ * This class holds a list of tags that can be used for creating a tag cloud.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2013 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf.tagging
+ * @subpackage system.tagging
+ * @category Community Framework
+ */
+class TagCloud {
+ /**
+ * max font size
+ * @var integer
+ */
+ const MAX_FONT_SIZE = 170;
+
+ /**
+ * min font size
+ * @var integer
+ */
+ const MIN_FONT_SIZE = 85;
+
+ /**
+ * list of tags
+ * @var array<wcf\data\tag\TagCloudTag>
+ */
+ protected $tags = array();
+
+ /**
+ * max value of tag counter
+ * @var integer
+ */
+ protected $maxCounter = 0;
+
+ /**
+ * min value of tag counter
+ * @var integer
+ */
+ protected $minCounter = 4294967295;
+
+ /**
+ * active language ids
+ * @var array<integer>
+ */
+ protected $languageIDs = array();
+
+ /**
+ * Contructs a new TagCloud object.
+ *
+ * @param array<integer> $languageIDs
+ */
+ public function __construct(array $languageIDs = array()) {
+ $this->languageIDs = $languageIDs;
+ if (empty($this->languageIDs)) {
+ $this->languageIDs = array_keys(LanguageFactory::getInstance()->getLanguages());
+ }
+
+ // init cache
+ $this->loadCache();
+ }
+
+ /**
+ * Loads the tag cloud cache.
+ */
+ protected function loadCache() {
+ $this->tags = TagCloudCacheBuilder::getInstance()->getData($this->languageIDs);
+ }
+
+ /**
+ * Gets a list of weighted tags.
+ *
+ * @param integer $slice
+ * @return array<wcf\data\tag\TagCloudTag> the tags to get
+ */
+ public function getTags($slice = 50) {
+ // slice list
+ $tags = array_slice($this->tags, 0, min($slice, count($this->tags)));
+
+ // get min / max counter
+ foreach ($tags as $tag) {
+ if ($tag->counter > $this->maxCounter) $this->maxCounter = $tag->counter;
+ if ($tag->counter < $this->minCounter) $this->minCounter = $tag->counter;
+ }
+
+ // assign sizes
+ foreach ($tags as $tag) {
+ $tag->setSize($this->calculateSize($tag->counter));
+ }
+
+ // sort alphabetically
+ ksort($tags);
+
+ // return tags
+ return $tags;
+ }
+
+ /**
+ * Returns the size of a tag with given number of uses for a weighted list.
+ *
+ * @param integer $counter
+ * @return double
+ */
+ private function calculateSize($counter) {
+ if ($this->maxCounter == $this->minCounter) {
+ return 100;
+ }
+ else {
+ return (self::MAX_FONT_SIZE - self::MIN_FONT_SIZE) / ($this->maxCounter - $this->minCounter) * $counter + self::MIN_FONT_SIZE - ((self::MAX_FONT_SIZE - self::MIN_FONT_SIZE) / ($this->maxCounter - $this->minCounter)) * $this->minCounter;
+ }
+ }
+}
--- /dev/null
+<?php
+namespace wcf\system\tagging;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\data\tag\Tag;
+use wcf\data\tag\TagAction;
+use wcf\system\database\util\PreparedStatementConditionBuilder;
+use wcf\system\exception\SystemException;
+use wcf\system\SingletonFactory;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Manages the tagging of objects.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2013 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf.tagging
+ * @subpackage system.tagging
+ * @category Community Framework
+ */
+class TagEngine extends SingletonFactory {
+ /**
+ * Adds tags to a tagged object.
+ *
+ * @param string $objectType
+ * @param integer $objectID
+ * @param array $tags
+ * @param integer $languageID
+ * @param boolean $replace
+ */
+ public function addObjectTags($objectType, $objectID, array $tags, $languageID, $replace = true) {
+ $objectTypeID = $this->getObjectTypeID($objectType);
+ $tags = array_unique($tags);
+
+ // remove tags prior to apply the new ones (prevents duplicate entries)
+ if ($replace) {
+ $sql = "DELETE FROM wcf".WCF_N."_tag_to_object
+ WHERE objectTypeID = ?
+ AND objectID = ?
+ AND languageID = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute(array(
+ $objectTypeID,
+ $objectID,
+ $languageID
+ ));
+ }
+
+ // get tag ids
+ $tagIDs = array();
+ foreach ($tags as $tag) {
+ if (empty($tag)) continue;
+
+ // find existing tag
+ $tagObj = Tag::getTag($tag, $languageID);
+ if ($tagObj === null) {
+ // enforce max length
+ if (StringUtil::length($tag) > TAGGING_MAX_TAG_LENGTH) {
+ $tag = StringUtil::substring($tag, 0, TAGGING_MAX_TAG_LENGTH);
+ }
+
+ // create new tag
+ $tagAction = new TagAction(array(), 'create', array('data' => array(
+ 'name' => $tag,
+ 'languageID' => $languageID
+ )));
+
+ $tagAction->executeAction();
+ $returnValues = $tagAction->getReturnValues();
+ $tagObj = $returnValues['returnValues'];
+ }
+
+ if ($tagObj->synonymFor !== null) $tagIDs[$tagObj->synonymFor] = $tagObj->synonymFor;
+ else $tagIDs[$tagObj->tagID] = $tagObj->tagID;
+ }
+
+ // save tags
+ $sql = "INSERT INTO wcf".WCF_N."_tag_to_object
+ (objectID, tagID, objectTypeID, languageID)
+ VALUES (?, ?, ?, ?)";
+ WCF::getDB()->beginTransaction();
+ $statement = WCF::getDB()->prepareStatement($sql);
+ foreach ($tagIDs as $tagID) {
+ $statement->execute(array($objectID, $tagID, $objectTypeID, $languageID));
+ }
+ WCF::getDB()->commitTransaction();
+ }
+
+ /**
+ * Deletes all tags assigned to given tagged object.
+ *
+ * @param string $objectType
+ * @param integer $objectID
+ * @param integer $languageID
+ */
+ public function deleteObjectTags($objectType, $objectID, $languageID = null) {
+ $objectTypeID = $this->getObjectTypeID($objectType);
+
+ $sql = "DELETE FROM wcf".WCF_N."_tag_to_object
+ WHERE objectTypeID = ?
+ AND objectID = ?
+ ".($languageID !== null ? "AND languageID = ?" : "");
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $parameters = array(
+ $objectTypeID,
+ $objectID
+ );
+ if ($languageID !== null) $parameters[] = $languageID;
+ $statement->execute($parameters);
+ }
+
+ /**
+ * Deletes all tags assigned to given tagged objects.
+ *
+ * @param string $objectType
+ * @param array<integer> $objectIDs
+ */
+ public function deleteObjects($objectType, array $objectIDs) {
+ $objectTypeID = $this->getObjectTypeID($objectType);
+
+ $conditionsBuilder = new PreparedStatementConditionBuilder();
+ $conditionsBuilder->add('objectTypeID = ?', array($objectTypeID));
+ $conditionsBuilder->add('objectID IN (?)', array($objectIDs));
+
+ $sql = "DELETE FROM wcf".WCF_N."_tag_to_object
+ ".$conditionsBuilder;
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute($conditionsBuilder->getParameters());
+ }
+
+ /**
+ * Returns all tags set for given object.
+ *
+ * @param string $objectType
+ * @param integer $objectID
+ * @param array<integer> $languageIDs
+ * @return array<wcf\data\tag\Tag>
+ */
+ public function getObjectTags($objectType, $objectID, array $languageIDs = array()) {
+ $objectTypeID = $this->getObjectTypeID($objectType);
+
+ // get tags
+ $conditions = new PreparedStatementConditionBuilder();
+ $conditions->add("tag_to_object.objectTypeID = ?", array($objectTypeID));
+ $conditions->add("tag_to_object.objectID = ?", array($objectID));
+ if (!empty($languageIDs)) {
+ foreach ($languageIDs as $index => $languageID) {
+ if (!$languageID) unset($languageIDs[$index]);
+ }
+
+ if (!empty($languageIDs)) {
+ $conditions->add("tag_to_object.languageID IN (?)", array($languageIDs));
+ }
+ }
+
+ $sql = "SELECT tag.*
+ FROM wcf".WCF_N."_tag_to_object tag_to_object
+ LEFT JOIN wcf".WCF_N."_tag tag
+ ON (tag.tagID = tag_to_object.tagID)
+ ".$conditions;
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute($conditions->getParameters());
+
+ $tags = array();
+
+ while ($row = $statement->fetchArray()) {
+ $tags[$row['tagID']] = new Tag(null, $row);
+ }
+
+ return $tags;
+ }
+
+ /**
+ * Returns id of the object type with the given name.
+ *
+ * @param string $objectType
+ * @return integer
+ */
+ public function getObjectTypeID($objectType) {
+ // get object type
+ $objectTypeObj = ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.tagging.taggableObject', $objectType);
+ if ($objectTypeObj === null) {
+ throw new SystemException("Object type '".$objectType."' is not valid for definition 'com.woltlab.wcf.tagging.taggableObject'");
+ }
+
+ return $objectTypeObj->objectTypeID;
+ }
+}
--- /dev/null
+<?php
+namespace wcf\system\tagging;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\system\cache\builder\TypedTagCloudCacheBuilder;
+
+/**
+ * This class provides the function to filter the tag cloud by object types.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2013 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf.tagging
+ * @subpackage system.tagging
+ * @category Community Framework
+ */
+class TypedTagCloud extends TagCloud {
+ /**
+ * object type ids
+ * @var array<integer>
+ */
+ protected $objectTypeIDs = array();
+
+ /**
+ * Contructs a new TypedTagCloud object.
+ *
+ * @param string $objectType
+ * @param array<integer> $languageIDs
+ */
+ public function __construct($objectType, array $languageIDs = array()) {
+ $objectTypeObj = ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.tagging.taggableObject', $objectType);
+ $this->objectTypeIDs[] = $objectTypeObj->objectTypeID;
+
+ parent::__construct($languageIDs);
+ }
+
+ /**
+ * Loads the tag cloud cache.
+ */
+ protected function loadCache() {
+ $this->tags = TypedTagCloudCacheBuilder::getInstance()->getData(array(
+ 'languageIDs' => $this->languageIDs,
+ 'objectTypeIDs' => $this->objectTypeIDs
+ ));
+ }
+}
--- /dev/null
+.tagList {
+ > li {
+ display: inline-block;
+ }
+}
+
+.tag {
+ font-weight: normal;
+ height: 13px;
+ margin-left: 6px;
+ padding-bottom: 2px;
+ padding-left: 10px;
+
+ .borderRadius(0, 4px, 4px, 0);
+
+ &:before{
+ border-color: transparent @wcfColor transparent transparent;
+ border-style: inset solid inset inset;
+ border-width: 8px 8px 8px 0;
+ clip: rect(auto auto auto 2px);
+ content: "";
+ height: 0;
+ left: -8px;
+ position: absolute;
+ top: 0;
+ width: 0;
+ }
+
+ &:after{
+ background: @wcfContentBackgroundColor;
+ content: "";
+ height: 4px;
+ left: -2px;
+ position: absolute;
+ top: 6px;
+ width: 4px;
+
+ .borderRadius(2px);
+ .boxShadow(0, -1px, rgba(0, 0, 0, .2), 2px);
+ }
+
+ &:hover {
+ background-color: @wcfTabularBoxBackgroundColor;
+ color: @wcfTabularBoxColor;
+
+ &:before{
+ border-right-color: @wcfTabularBoxBackgroundColor;
+ }
+ }
+}
+
+.editableItemList li.tag {
+ margin-bottom: 11px;
+ margin-left: 10px;
+}
+
+.editableItemList li.tag:first-child {
+ margin-left: 6px;
+}
\ No newline at end of file
<item name="wcf.acp.group.option.category.mod.profileComment"><![CDATA[Benutzerprofil-Pinnwand]]></item>
<item name="wcf.acp.group.option.admin.content.label.canManageLabel"><![CDATA[Kann Labels verwalten]]></item>
<item name="wcf.acp.group.option.category.admin.content.label"><![CDATA[Labels]]></item>
+ <item name="wcf.acp.group.option.category.admin.content.tag"><![CDATA[Tags]]></item>
+ <item name="wcf.acp.group.option.admin.content.tag.canManageTag"><![CDATA[Kann Tags verwalten]]></item>
</category>
<category name="wcf.acp.index">
<item name="wcf.acp.menu.link.label.list"><![CDATA[Labels auflisten]]></item>
<item name="wcf.acp.menu.link.label.group.add"><![CDATA[Labelgruppe hinzufügen]]></item>
<item name="wcf.acp.menu.link.label.group.list"><![CDATA[Labelgruppen auflisten]]></item>
+ <item name="wcf.acp.menu.link.tag"><![CDATA[Tags]]></item>
+ <item name="wcf.acp.menu.link.tag.add"><![CDATA[Tag hinzufügen]]></item>
+ <item name="wcf.acp.menu.link.tag.list"><![CDATA[Tags auflisten]]></item>
</category>
<category name="wcf.acp.option">
<item name="wcf.acp.option.message_sidebar_enable_likes_received"><![CDATA[Anzahl der erhaltenen Likes der Autoren anzeigen]]></item>
<item name="wcf.acp.option.message_sidebar_enable_activity_points"><![CDATA[Aktivitätspunkte der Autoren anzeigen]]></item>
<item name="wcf.acp.option.message_sidebar_user_options"><![CDATA[Ausgewählte Profilfelder der Autoren anzeigen]]></item>
+ <item name="wcf.acp.option.module_tagging"><![CDATA[Tagging]]></item>
+ <item name="wcf.acp.option.module_tagging.description"><![CDATA[Aktiviert die Funktion für das Taggen von Inhalten.]]></item>
+ <item name="wcf.acp.option.tagging_max_tag_length"><![CDATA[Maximale Tag-Länge]]></item>
</category>
<category name="wcf.acp.package">
<item name="wcf.acp.style.users"><![CDATA[Benutzer]]></item>
</category>
+ <category name="wcf.acp.tag">
+ <item name="wcf.acp.tag.add"><![CDATA[Tag hinzufügen]]></item>
+ <item name="wcf.acp.tag.edit"><![CDATA[Tag bearbeiten]]></item>
+ <item name="wcf.acp.tag.delete.sure"><![CDATA[Wollen Sie den Tag „{$tag}“ wirklich löschen?]]></item>
+ <item name="wcf.acp.tag.error.languageID.notFound"><![CDATA[Die gewählte Sprache ist ungültig]]></item>
+ <item name="wcf.acp.tag.languageID"><![CDATA[Sprache]]></item>
+ <item name="wcf.acp.tag.list"><![CDATA[Tags auflisten]]></item>
+ <item name="wcf.acp.tag.list.search"><![CDATA[Tags suchen]]></item>
+ <item name="wcf.acp.tag.list.search.query"><![CDATA[Suchbegriff]]></item>
+ <item name="wcf.acp.tag.noneAvailable"><![CDATA[Es sind noch keine Tags vorhanden.]]></item>
+ <item name="wcf.acp.tag.name"><![CDATA[Name]]></item>
+ <item name="wcf.acp.tag.synonyms"><![CDATA[Synonyme]]></item>
+ <item name="wcf.acp.tag.synonymFor"><![CDATA[Synonym für]]></item>
+ <item name="wcf.acp.tag.usageCount"><![CDATA[Verwendungen]]></item>
+ </category>
+
<category name="wcf.acp.template">
<item name="wcf.acp.template.list"><![CDATA[Templates]]></item>
<item name="wcf.acp.template.group"><![CDATA[Templategruppe]]></item>
<item name="wcf.style.currentStyle"><![CDATA[Aktiver Stil]]></item>
</category>
+ <category name="wcf.tagging">
+ <item name="wcf.tagging.tags"><![CDATA[Tags]]></item>
+ <item name="wcf.tagging.tags.add"><![CDATA[Tags]]></item>
+ <item name="wcf.tagging.tags.description"><![CDATA[Mehrere Tags müssen durch ein Komma getrennt werden.]]></item>
+ <item name="wcf.tagging.objectTypes"><![CDATA[Inhalte]]></item>
+ <item name="wcf.tagging.taggedObjects.noResults"><![CDATA[Es wurden keine Einträge mit diesem Tag gefunden.]]></item>
+ </category>
+
<category name="wcf.user">
<item name="wcf.user.confirmEmail"><![CDATA[E-Mail-Adresse wiederholen]]></item>
<item name="wcf.user.confirmPassword"><![CDATA[Kennwort wiederholen]]></item>
<item name="wcf.acp.group.option.category.mod.profileComment"><![CDATA[User Profile Wall]]></item>
<item name="wcf.acp.group.option.admin.content.label.canManageLabel"><![CDATA[Can manage labels]]></item>
<item name="wcf.acp.group.option.category.admin.content.label"><![CDATA[Labels]]></item>
+ <item name="wcf.acp.group.option.category.admin.content.tag"><![CDATA[Tags]]></item>
+ <item name="wcf.acp.group.option.admin.content.tag.canManageTag"><![CDATA[Can manage tags]]></item>
</category>
<category name="wcf.acp.index">
<item name="wcf.acp.menu.link.label.list"><![CDATA[List Labels]]></item>
<item name="wcf.acp.menu.link.label.group.add"><![CDATA[Add Label Group]]></item>
<item name="wcf.acp.menu.link.label.group.list"><![CDATA[List Label Groups]]></item>
+ <item name="wcf.acp.menu.link.tag"><![CDATA[Tags]]></item>
+ <item name="wcf.acp.menu.link.tag.add"><![CDATA[Add Tag]]></item>
+ <item name="wcf.acp.menu.link.tag.list"><![CDATA[List Tags]]></item>
</category>
<category name="wcf.acp.option">
<item name="wcf.acp.option.message_sidebar_enable_likes_received"><![CDATA[Show author’s likes received]]></item>
<item name="wcf.acp.option.message_sidebar_enable_activity_points"><![CDATA[Show author’s activity points]]></item>
<item name="wcf.acp.option.message_sidebar_user_options"><![CDATA[Show selected author profile fields]]></item>
+ <item name="wcf.acp.option.module_tagging"><![CDATA[Tagging]]></item>
+ <item name="wcf.acp.option.module_tagging.description"><![CDATA[Enables the tagging of content.]]></item>
+ <item name="wcf.acp.option.tagging_max_tag_length"><![CDATA[Maximum Length of a Tag]]></item>
</category>
<category name="wcf.acp.package">
<item name="wcf.acp.style.users"><![CDATA[Users]]></item>
</category>
+ <category name="wcf.acp.tag">
+ <item name="wcf.acp.tag.add"><![CDATA[Add Tag]]></item>
+ <item name="wcf.acp.tag.edit"><![CDATA[Edit Tag]]></item>
+ <item name="wcf.acp.tag.delete.sure"><![CDATA[Do you really want to delete the tag “{$tag}”?]]></item>
+ <item name="wcf.acp.tag.error.languageID.notFound"><![CDATA[Selected language is invalid]]></item>
+ <item name="wcf.acp.tag.languageID"><![CDATA[Language]]></item>
+ <item name="wcf.acp.tag.list"><![CDATA[List Tags]]></item>
+ <item name="wcf.acp.tag.list.search"><![CDATA[Search Tags]]></item>
+ <item name="wcf.acp.tag.list.search.query"><![CDATA[Search Term]]></item>
+ <item name="wcf.acp.tag.noneAvailable"><![CDATA[No tag have been added yet.]]></item>
+ <item name="wcf.acp.tag.name"><![CDATA[Name]]></item>
+ <item name="wcf.acp.tag.synonyms"><![CDATA[Synonyms]]></item>
+ <item name="wcf.acp.tag.synonymFor"><![CDATA[Synonym for]]></item>
+ <item name="wcf.acp.tag.usageCount"><![CDATA[Usages]]></item>
+ </category>
+
<category name="wcf.acp.template">
<item name="wcf.acp.template.list"><![CDATA[Templates]]></item>
<item name="wcf.acp.template.group"><![CDATA[Template Group]]></item>
<item name="wcf.style.currentStyle"><![CDATA[Current Style]]></item>
</category>
+ <category name="wcf.tagging">
+ <item name="wcf.tagging.tags"><![CDATA[Tags]]></item>
+ <item name="wcf.tagging.tags.add"><![CDATA[Tags]]></item>
+ <item name="wcf.tagging.tags.description"><![CDATA[Add multiple tags by separating them with a comma.]]></item>
+ <item name="wcf.tagging.objectTypes"><![CDATA[Content]]></item>
+ <item name="wcf.tagging.taggedObjects.noResults"><![CDATA[No items matched this tag.]]></item>
+ </category>
+
<category name="wcf.user">
<item name="wcf.user.confirmEmail"><![CDATA[Confirm Email]]></item>
<item name="wcf.user.confirmPassword"><![CDATA[Confirm Password]]></item>
UNIQUE KEY (styleID, variableID)
);
+DROP TABLE IF EXISTS wcf1_tag;
+CREATE TABLE wcf1_tag (
+ tagID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ languageID INT(10) NOT NULL DEFAULT 0,
+ name VARCHAR(255) NOT NULL,
+ synonymFor INT(10),
+ UNIQUE KEY (languageID, name)
+);
+
+DROP TABLE IF EXISTS wcf1_tag_to_object;
+CREATE TABLE wcf1_tag_to_object (
+ objectID INT(10) NOT NULL,
+ tagID INT(10) NOT NULL,
+ objectTypeID INT(10) NOT NULL,
+ languageID INT(10) NOT NULL,
+ UNIQUE KEY (objectTypeID, languageID, objectID, tagID),
+ KEY (objectTypeID, languageID, tagID),
+ KEY (tagID, objectTypeID)
+);
+
DROP TABLE IF EXISTS wcf1_template;
CREATE TABLE wcf1_template (
templateID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
ALTER TABLE wcf1_label_object ADD FOREIGN KEY (labelID) REFERENCES wcf1_label (labelID) ON DELETE CASCADE;
ALTER TABLE wcf1_label_object ADD FOREIGN KEY (objectTypeID) REFERENCES wcf1_object_type (objectTypeID) ON DELETE CASCADE;
+ALTER TABLE wcf1_tag ADD FOREIGN KEY (synonymFor) REFERENCES wcf1_tag (tagID) ON DELETE CASCADE;
+
+ALTER TABLE wcf1_tag_to_object ADD FOREIGN KEY (tagID) REFERENCES wcf1_tag (tagID) ON DELETE CASCADE;
+ALTER TABLE wcf1_tag_to_object ADD FOREIGN KEY (languageID) REFERENCES wcf1_language (languageID) ON DELETE CASCADE;
+ALTER TABLE wcf1_tag_to_object ADD FOREIGN KEY (objectTypeID) REFERENCES wcf1_object_type (objectTypeID) ON DELETE CASCADE;
+
/* default inserts */
-- default user groups