Merged com.woltlab.wcf.tagging into WCF
authorMarcel Werk <burntime@woltlab.com>
Mon, 20 May 2013 22:52:02 +0000 (00:52 +0200)
committerMarcel Werk <burntime@woltlab.com>
Mon, 20 May 2013 22:52:02 +0000 (00:52 +0200)
31 files changed:
com.woltlab.wcf/acpMenu.xml
com.woltlab.wcf/objectTypeDefinition.xml
com.woltlab.wcf/option.xml
com.woltlab.wcf/template/tagCloudBox.tpl [new file with mode: 0644]
com.woltlab.wcf/template/tagInput.tpl [new file with mode: 0644]
com.woltlab.wcf/template/tagged.tpl [new file with mode: 0644]
com.woltlab.wcf/userGroupOption.xml
wcfsetup/install/files/acp/templates/tagAdd.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/tagList.tpl [new file with mode: 0644]
wcfsetup/install/files/js/WCF.Tagging.js [new file with mode: 0644]
wcfsetup/install/files/js/WCF.Tagging.min.js [new file with mode: 0644]
wcfsetup/install/files/lib/acp/form/TagAddForm.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/acp/form/TagEditForm.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/acp/page/TagListPage.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/tag/Tag.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/tag/TagAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/tag/TagCloudTag.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/tag/TagEditor.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/tag/TagList.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/page/TaggedPage.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/cache/builder/TagCloudCacheBuilder.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/cache/builder/TypedTagCloudCacheBuilder.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/tagging/ITaggable.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/tagging/ITagged.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/tagging/TagCloud.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/tagging/TagEngine.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/tagging/TypedTagCloud.class.php [new file with mode: 0644]
wcfsetup/install/files/style/tagging.less [new file with mode: 0644]
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml
wcfsetup/setup/db/install.sql

index 2f042a83f96487781fcd87fa39b349788a44a8be..431929bd4681d58f570b72b3a2f26e484326726f 100644 (file)
                        <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>
index 5498a02015769386769b2e7458fb991efa9611d9..2596592e8c0e3dba0753689dca510f3ee2a5adcf 100644 (file)
                        <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>
index d3ffe14cf6d306a30e0050104387ed7d2794469f..e9a602c1877411dd51cfce67402c58d4d1ca88f0 100644 (file)
                                <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>
@@ -1007,6 +1013,16 @@ DESC:wcf.global.sortOrder.descending]]></selectoptions>
                                <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>
diff --git a/com.woltlab.wcf/template/tagCloudBox.tpl b/com.woltlab.wcf/template/tagCloudBox.tpl
new file mode 100644 (file)
index 0000000..ded4835
--- /dev/null
@@ -0,0 +1,9 @@
+{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
diff --git a/com.woltlab.wcf/template/tagInput.tpl b/com.woltlab.wcf/template/tagInput.tpl
new file mode 100644 (file)
index 0000000..910eb81
--- /dev/null
@@ -0,0 +1,20 @@
+<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
diff --git a/com.woltlab.wcf/template/tagged.tpl b/com.woltlab.wcf/template/tagged.tpl
new file mode 100644 (file)
index 0000000..1c8a3cc
--- /dev/null
@@ -0,0 +1,88 @@
+{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
index 9fb1123cfbfc868639c1cd5d8a58889978b1ef18..9964a11b270365cf8a630a39153ebc1761e2d3f2 100644 (file)
@@ -95,6 +95,9 @@
                        <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>
@@ -545,6 +548,13 @@ png]]></defaultvalue>
                                <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>
diff --git a/wcfsetup/install/files/acp/templates/tagAdd.tpl b/wcfsetup/install/files/acp/templates/tagAdd.tpl
new file mode 100644 (file)
index 0000000..ae76dce
--- /dev/null
@@ -0,0 +1,120 @@
+{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
diff --git a/wcfsetup/install/files/acp/templates/tagList.tpl b/wcfsetup/install/files/acp/templates/tagList.tpl
new file mode 100644 (file)
index 0000000..eda77f5
--- /dev/null
@@ -0,0 +1,104 @@
+{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'}
diff --git a/wcfsetup/install/files/js/WCF.Tagging.js b/wcfsetup/install/files/js/WCF.Tagging.js
new file mode 100644 (file)
index 0000000..6521c8a
--- /dev/null
@@ -0,0 +1,152 @@
+/**
+ * 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
diff --git a/wcfsetup/install/files/js/WCF.Tagging.min.js b/wcfsetup/install/files/js/WCF.Tagging.min.js
new file mode 100644 (file)
index 0000000..7bb0770
--- /dev/null
@@ -0,0 +1 @@
+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
diff --git a/wcfsetup/install/files/lib/acp/form/TagAddForm.class.php b/wcfsetup/install/files/lib/acp/form/TagAddForm.class.php
new file mode 100644 (file)
index 0000000..b5654de
--- /dev/null
@@ -0,0 +1,194 @@
+<?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
+               ));
+       }
+}
diff --git a/wcfsetup/install/files/lib/acp/form/TagEditForm.class.php b/wcfsetup/install/files/lib/acp/form/TagEditForm.class.php
new file mode 100644 (file)
index 0000000..6d63236
--- /dev/null
@@ -0,0 +1,140 @@
+<?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'
+               ));
+       }
+}
diff --git a/wcfsetup/install/files/lib/acp/page/TagListPage.class.php b/wcfsetup/install/files/lib/acp/page/TagListPage.class.php
new file mode 100644 (file)
index 0000000..42a7829
--- /dev/null
@@ -0,0 +1,86 @@
+<?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.'%'));
+               }
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/tag/Tag.class.php b/wcfsetup/install/files/lib/data/tag/Tag.class.php
new file mode 100644 (file)
index 0000000..b3c40af
--- /dev/null
@@ -0,0 +1,92 @@
+<?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();
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/tag/TagAction.class.php b/wcfsetup/install/files/lib/data/tag/TagAction.class.php
new file mode 100644 (file)
index 0000000..973f4af
--- /dev/null
@@ -0,0 +1,82 @@
+<?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;
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/tag/TagCloudTag.class.php b/wcfsetup/install/files/lib/data/tag/TagCloudTag.class.php
new file mode 100644 (file)
index 0000000..27956b0
--- /dev/null
@@ -0,0 +1,44 @@
+<?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;
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/tag/TagEditor.class.php b/wcfsetup/install/files/lib/data/tag/TagEditor.class.php
new file mode 100644 (file)
index 0000000..1615e7b
--- /dev/null
@@ -0,0 +1,71 @@
+<?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);
+               }
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/tag/TagList.class.php b/wcfsetup/install/files/lib/data/tag/TagList.class.php
new file mode 100644 (file)
index 0000000..ef0bec9
--- /dev/null
@@ -0,0 +1,20 @@
+<?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';
+}
diff --git a/wcfsetup/install/files/lib/page/TaggedPage.class.php b/wcfsetup/install/files/lib/page/TaggedPage.class.php
new file mode 100644 (file)
index 0000000..f3f8004
--- /dev/null
@@ -0,0 +1,103 @@
+<?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
+               ));
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/cache/builder/TagCloudCacheBuilder.class.php b/wcfsetup/install/files/lib/system/cache/builder/TagCloudCacheBuilder.class.php
new file mode 100644 (file)
index 0000000..e7e8131
--- /dev/null
@@ -0,0 +1,128 @@
+<?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;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/cache/builder/TypedTagCloudCacheBuilder.class.php b/wcfsetup/install/files/lib/system/cache/builder/TypedTagCloudCacheBuilder.class.php
new file mode 100644 (file)
index 0000000..9e78549
--- /dev/null
@@ -0,0 +1,27 @@
+<?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;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/tagging/ITaggable.class.php b/wcfsetup/install/files/lib/system/tagging/ITaggable.class.php
new file mode 100644 (file)
index 0000000..9fc0bfd
--- /dev/null
@@ -0,0 +1,37 @@
+<?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();
+}
diff --git a/wcfsetup/install/files/lib/system/tagging/ITagged.class.php b/wcfsetup/install/files/lib/system/tagging/ITagged.class.php
new file mode 100644 (file)
index 0000000..035051b
--- /dev/null
@@ -0,0 +1,28 @@
+<?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();
+}
diff --git a/wcfsetup/install/files/lib/system/tagging/TagCloud.class.php b/wcfsetup/install/files/lib/system/tagging/TagCloud.class.php
new file mode 100644 (file)
index 0000000..4827073
--- /dev/null
@@ -0,0 +1,117 @@
+<?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;
+               }
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/tagging/TagEngine.class.php b/wcfsetup/install/files/lib/system/tagging/TagEngine.class.php
new file mode 100644 (file)
index 0000000..ab4e63e
--- /dev/null
@@ -0,0 +1,189 @@
+<?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;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/tagging/TypedTagCloud.class.php b/wcfsetup/install/files/lib/system/tagging/TypedTagCloud.class.php
new file mode 100644 (file)
index 0000000..0e432e8
--- /dev/null
@@ -0,0 +1,45 @@
+<?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
+               ));
+       }
+}
diff --git a/wcfsetup/install/files/style/tagging.less b/wcfsetup/install/files/style/tagging.less
new file mode 100644 (file)
index 0000000..b6c8665
--- /dev/null
@@ -0,0 +1,59 @@
+.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
index 5826fad39403a14a880b06c976ea115b23314e0d..ecdd50dd59790f4bf54641081a3f8bb5ffa2ee25 100644 (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>
@@ -1694,6 +1718,14 @@ Erlaubte Dateiendungen: {', '|implode:$attachmentHandler->getAllowedExtensions()
                <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>
index 193ba8c37ce03e52a95607db5112db11eccfae35..8e58246be10fc3eeb0181ee9d279e522a6894c3f 100644 (file)
@@ -310,6 +310,8 @@ Examples for medium ID detection:
                <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">
@@ -484,6 +486,9 @@ Examples for medium ID detection:
                <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">
@@ -757,6 +762,9 @@ Examples for medium ID detection:
                <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">
@@ -1013,6 +1021,22 @@ Examples for medium ID detection:
                <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>
@@ -1692,6 +1716,14 @@ Allowed extensions: {', '|implode:$attachmentHandler->getAllowedExtensions()}]]>
                <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>
index 038e6d2e95649e473f56b1e5dbd2105791fbf551..ed7b63cb482578e2476678149e5ad2569ec57f21 100644 (file)
@@ -798,6 +798,26 @@ CREATE TABLE wcf1_style_variable_value (
        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,
@@ -1424,6 +1444,12 @@ ALTER TABLE wcf1_label_group_to_object ADD FOREIGN KEY (objectTypeID) REFERENCES
 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