Alternate search mode using tags
authorAlexander Ebert <ebert@woltlab.com>
Wed, 2 Jan 2019 17:28:18 +0000 (18:28 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Wed, 2 Jan 2019 17:28:18 +0000 (18:28 +0100)
com.woltlab.wcf/templates/search.tpl
com.woltlab.wcf/templates/tagSearch.tpl [new file with mode: 0644]
wcfsetup/install/files/lib/data/article/TaggedArticleList.class.php
wcfsetup/install/files/lib/form/TagSearchForm.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/page/CombinedTaggedPage.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/tagging/AbstractCombinedTaggable.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/tagging/ICombinedTaggable.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/tagging/ITaggable.class.php
wcfsetup/install/files/lib/system/tagging/TagEngine.class.php
wcfsetup/install/files/lib/system/tagging/TaggableArticle.class.php

index fe309937f401fe41de39a347850e5e7df609c2b6..20472ee81c8a02fefc0d4a9f666003c53335aa9b 100644 (file)
 {/if}
 
 <form method="post" action="{link controller='Search'}{/link}">
-       <div class="section">
-               <dl{if $errorField == 'q'} class="formError"{/if}>
-                       <dt><label for="searchTerm">{lang}wcf.search.query{/lang}</label></dt>
-                       <dd>
-                               <input type="text" id="searchTerm" name="q" value="{$query}" class="long" maxlength="255" autofocus>
-                               {if $errorField == 'q'}
-                                       <small class="innerError">
-                                               {if $errorType == 'empty'}
-                                                       {lang}wcf.global.form.error.empty{/lang}
-                                               {else}
-                                                       {lang}wcf.search.query.error.{@$errorType}{/lang}
-                                               {/if}
-                                       </small>
-                               {/if}
-                               <label><input type="checkbox" name="subjectOnly" value="1"{if $subjectOnly == 1} checked{/if}> {lang}wcf.search.subjectOnly{/lang}</label>
-                               {event name='queryOptions'}
+       <div class="section tabMenuContainer staticTabMenuContainer">
+               <nav class="tabMenu">
+                       <ul>
+                               <li class="active"><a href="{link controller='Search'}{/link}">{lang}wcf.search.type.keywords{/lang}</a></li>
+                               <li><a href="{link controller='TagSearch'}{/link}">{lang}wcf.search.type.tags{/lang}</a></li>
                                
-                               <small>{lang}wcf.search.query.description{/lang}</small>
-                       </dd>
-               </dl>
-               
-               <dl>
-                       <dt><label for="searchAuthor">{lang}wcf.search.author{/lang}</label></dt>
-                       <dd>
-                               <input type="text" id="searchAuthor" name="username" value="{$username}" class="medium" maxlength="255" autocomplete="off">
-                               <label><input type="checkbox" name="nameExactly" value="1"{if $nameExactly == 1} checked{/if}> {lang}wcf.search.matchExactly{/lang}</label>
-                               {event name='authorOptions'}
-                       </dd>
-               </dl>
-               
-               <dl>
-                       <dt><label for="startDate">{lang}wcf.search.period{/lang}</label></dt>
-                       <dd>
-                               <input type="date" id="startDate" name="startDate" value="{$startDate}" data-placeholder="{lang}wcf.date.period.start{/lang}">
-                               <input type="date" id="endDate" name="endDate" value="{$endDate}" data-placeholder="{lang}wcf.date.period.end{/lang}">
-                               {event name='periodOptions'}
-                       </dd>
-               </dl>
+                               {event name='tabMenuTabs'}
+                       </ul>
+               </nav>
                
-               <dl>
-                       <dt><label for="sortField">{lang}wcf.search.sortBy{/lang}</label></dt>
-                       <dd>
-                               <select id="sortField" name="sortField">
-                                       <option value="relevance"{if $sortField == 'relevance'} selected{/if}>{lang}wcf.search.sortBy.relevance{/lang}</option>
-                                       <option value="subject"{if $sortField == 'subject'} selected{/if}>{lang}wcf.global.subject{/lang}</option>
-                                       <option value="time"{if $sortField == 'time'} selected{/if}>{lang}wcf.search.sortBy.time{/lang}</option>
-                                       <option value="username"{if $sortField == 'username'} selected{/if}>{lang}wcf.search.sortBy.username{/lang}</option>
-                               </select>
+               <div class="tabMenuContent">
+                       <div class="section">
+                               <dl{if $errorField == 'q'} class="formError"{/if}>
+                                       <dt><label for="searchTerm">{lang}wcf.search.query{/lang}</label></dt>
+                                       <dd>
+                                               <input type="text" id="searchTerm" name="q" value="{$query}" class="long" maxlength="255" autofocus>
+                                               {if $errorField == 'q'}
+                                                       <small class="innerError">
+                                                               {if $errorType == 'empty'}
+                                                                       {lang}wcf.global.form.error.empty{/lang}
+                                                               {else}
+                                                                       {lang}wcf.search.query.error.{@$errorType}{/lang}
+                                                               {/if}
+                                                       </small>
+                                               {/if}
+                                               <label><input type="checkbox" name="subjectOnly" value="1"{if $subjectOnly == 1} checked{/if}> {lang}wcf.search.subjectOnly{/lang}</label>
+                                               {event name='queryOptions'}
+                                               
+                                               <small>{lang}wcf.search.query.description{/lang}</small>
+                                       </dd>
+                               </dl>
                                
-                               <select name="sortOrder">
-                                       <option value="ASC"{if $sortOrder == 'ASC'} selected{/if}>{lang}wcf.global.sortOrder.ascending{/lang}</option>
-                                       <option value="DESC"{if $sortOrder == 'DESC'} selected{/if}>{lang}wcf.global.sortOrder.descending{/lang}</option>
-                               </select>
+                               <dl>
+                                       <dt><label for="searchAuthor">{lang}wcf.search.author{/lang}</label></dt>
+                                       <dd>
+                                               <input type="text" id="searchAuthor" name="username" value="{$username}" class="medium" maxlength="255" autocomplete="off">
+                                               <label><input type="checkbox" name="nameExactly" value="1"{if $nameExactly == 1} checked{/if}> {lang}wcf.search.matchExactly{/lang}</label>
+                                               {event name='authorOptions'}
+                                       </dd>
+                               </dl>
                                
-                               {event name='displayOptions'}
-                       </dd>
-               </dl>
-               
-               {event name='generalFields'}
-               
-               <dl>
-                       <dt>{lang}wcf.search.type{/lang}</dt>
-                       <dd class="floated">
-                               {foreach from=$objectTypes key=objectTypeName item=objectType}
-                                       {if $objectType->isAccessible()}
-                                               <label><input id="{@'.'|str_replace:'_':$objectTypeName}" type="checkbox" name="types[]" value="{@$objectTypeName}"{if $objectTypeName|in_array:$selectedObjectTypes} checked{/if}> {lang}wcf.search.type.{@$objectTypeName}{/lang}</label>
-                                       {/if}
-                               {/foreach}
-                       </dd>
-               </dl>
-       </div>
-               
-       {event name='sections'}
-               
-       {foreach from=$objectTypes key=objectTypeName item=objectType}
-               {if $objectType->isAccessible() && $objectType->getFormTemplateName()}
-                       {assign var='__jsID' value='.'|str_replace:'_':$objectTypeName}
-                       <section class="section" id="{@$__jsID}Form">
-                               <h2 class="sectionTitle">{lang}wcf.search.type.{@$objectTypeName}{/lang}</h2>
+                               <dl>
+                                       <dt><label for="startDate">{lang}wcf.search.period{/lang}</label></dt>
+                                       <dd>
+                                               <input type="date" id="startDate" name="startDate" value="{$startDate}" data-placeholder="{lang}wcf.date.period.start{/lang}">
+                                               <input type="date" id="endDate" name="endDate" value="{$endDate}" data-placeholder="{lang}wcf.date.period.end{/lang}">
+                                               {event name='periodOptions'}
+                                       </dd>
+                               </dl>
+                               
+                               <dl>
+                                       <dt><label for="sortField">{lang}wcf.search.sortBy{/lang}</label></dt>
+                                       <dd>
+                                               <select id="sortField" name="sortField">
+                                                       <option value="relevance"{if $sortField == 'relevance'} selected{/if}>{lang}wcf.search.sortBy.relevance{/lang}</option>
+                                                       <option value="subject"{if $sortField == 'subject'} selected{/if}>{lang}wcf.global.subject{/lang}</option>
+                                                       <option value="time"{if $sortField == 'time'} selected{/if}>{lang}wcf.search.sortBy.time{/lang}</option>
+                                                       <option value="username"{if $sortField == 'username'} selected{/if}>{lang}wcf.search.sortBy.username{/lang}</option>
+                                               </select>
+                                               
+                                               <select name="sortOrder">
+                                                       <option value="ASC"{if $sortOrder == 'ASC'} selected{/if}>{lang}wcf.global.sortOrder.ascending{/lang}</option>
+                                                       <option value="DESC"{if $sortOrder == 'DESC'} selected{/if}>{lang}wcf.global.sortOrder.descending{/lang}</option>
+                                               </select>
+                                               
+                                               {event name='displayOptions'}
+                                       </dd>
+                               </dl>
                                
-                               {include file=$objectType->getFormTemplateName() application=$objectType->getApplication()}
+                               {event name='generalFields'}
                                
-                               <script data-relocate="true">
-                                       $(function() {
-                                               $('#{@$__jsID}').click(function() {
-                                                       if (this.checked) $('#{@$__jsID}Form').wcfFadeIn();
-                                                       else $('#{@$__jsID}Form').wcfFadeOut();
-                                               });
-                                               {if !$objectTypeName|in_array:$selectedObjectTypes}$('#{@$__jsID}Form').hide();{/if}
-                                       });
-                               </script>
-                       </section>
-               {/if}
-       {/foreach}
-       
-       {include file='captcha' supportsAsyncCaptcha=true}
-       
-       <div class="formSubmit">
-               <input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s">
-               {@SECURITY_TOKEN_INPUT_TAG}
+                               <dl>
+                                       <dt>{lang}wcf.search.type{/lang}</dt>
+                                       <dd class="floated">
+                                               {foreach from=$objectTypes key=objectTypeName item=objectType}
+                                                       {if $objectType->isAccessible()}
+                                                               <label><input id="{@'.'|str_replace:'_':$objectTypeName}" type="checkbox" name="types[]" value="{@$objectTypeName}"{if $objectTypeName|in_array:$selectedObjectTypes} checked{/if}> {lang}wcf.search.type.{@$objectTypeName}{/lang}</label>
+                                                       {/if}
+                                               {/foreach}
+                                       </dd>
+                               </dl>
+                       </div>
+                               
+                       {event name='sections'}
+                               
+                       {foreach from=$objectTypes key=objectTypeName item=objectType}
+                               {if $objectType->isAccessible() && $objectType->getFormTemplateName()}
+                                       {assign var='__jsID' value='.'|str_replace:'_':$objectTypeName}
+                                       <section class="section" id="{@$__jsID}Form">
+                                               <h2 class="sectionTitle">{lang}wcf.search.type.{@$objectTypeName}{/lang}</h2>
+                                               
+                                               {include file=$objectType->getFormTemplateName() application=$objectType->getApplication()}
+                                               
+                                               <script data-relocate="true">
+                                                       $(function() {
+                                                               $('#{@$__jsID}').click(function() {
+                                                                       if (this.checked) $('#{@$__jsID}Form').wcfFadeIn();
+                                                                       else $('#{@$__jsID}Form').wcfFadeOut();
+                                                               });
+                                                               {if !$objectTypeName|in_array:$selectedObjectTypes}$('#{@$__jsID}Form').hide();{/if}
+                                                       });
+                                               </script>
+                                       </section>
+                               {/if}
+                       {/foreach}
+                       
+                       {include file='captcha' supportsAsyncCaptcha=true}
+                       
+                       <div class="formSubmit">
+                               <input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s">
+                               {@SECURITY_TOKEN_INPUT_TAG}
+                       </div>
+               </div>
        </div>
 </form>
 
diff --git a/com.woltlab.wcf/templates/tagSearch.tpl b/com.woltlab.wcf/templates/tagSearch.tpl
new file mode 100644 (file)
index 0000000..b824483
--- /dev/null
@@ -0,0 +1,78 @@
+{include file='header' __disableAds=true}
+
+{include file='formError'}
+
+{if $errorMessage|isset}
+       <p class="error">{@$errorMessage}</p>
+{/if}
+
+<form method="post" action="{link controller='TagSearch'}{/link}">
+       <div class="section tabMenuContainer staticTabMenuContainer">
+               <nav class="tabMenu">
+                       <ul>
+                               <li><a href="{link controller='Search'}{/link}">{lang}wcf.search.type.keywords{/lang}</a></li>
+                               <li class="active"><a href="{link controller='TagSearch'}{/link}">{lang}wcf.search.type.tags{/lang}</a></li>
+                               
+                               {event name='tabMenuTabs'}
+                       </ul>
+               </nav>
+               
+               <div class="tabMenuContent">
+                       <div class="section jsOnly">
+                               {include file='messageFormMultilingualism'}
+                               
+                               <dl>
+                                       <dt><label for="tagSearchInput">{lang}wcf.tagging.tags{/lang}</label></dt>
+                                       <dd>
+                                               <input id="tagSearchInput" type="text" value="" class="long">
+                                               <small>{lang}wcf.tagging.tags.description{/lang}</small>
+                                       </dd>
+                               </dl>
+                               
+                               <script data-relocate="true">
+                                       require(['WoltLabSuite/Core/Language/Chooser', 'WoltLabSuite/Core/Ui/ItemList'], function(LanguageChooser, UiItemList) {
+                                               UiItemList.init(
+                                                       'tagSearchInput',
+                                                       [{if !$tagNames|empty}{implode from=$tagNames item=tagName}'{@$tagName|encodeJS}'{/implode}{/if}],
+                                                       {
+                                                               ajax: {
+                                                                       className: 'wcf\\data\\tag\\TagAction'
+                                                               },
+                                                               maxItems: {@SEARCH_MAX_COMBINED_TAGS},
+                                                               restricted: true,
+                                                               submitFieldName: 'tagNames[]'
+                                                       }
+                                               );
+                                               
+                                               var languageId = {@$languageID};
+                                               LanguageChooser.getChooser('languageID').callback = function (listItem) {
+                                                       var newLanguageId = parseInt(elData(listItem, 'language-id'), 10);
+                                                       if (newLanguageId !== languageId) {
+                                                               languageId = newLanguageId;
+                                                       }
+                                               };
+                                       });
+                               </script>
+                       </div>
+                       
+                       {if !$tags|empty}
+                               <section class="section">
+                                       <h2 class="sectionTitle">{lang}wcf.search.type.tags.popular{/lang}</h2>
+                                       
+                                       {include file='tagCloudBox'}
+                               </section>
+                       {/if}
+                       
+                       {event name='sections'}
+                       
+                       {include file='captcha' supportsAsyncCaptcha=true}
+                       
+                       <div class="formSubmit">
+                               <input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s">
+                               {@SECURITY_TOKEN_INPUT_TAG}
+                       </div>
+               </div>
+       </div>
+</form>
+
+{include file='footer' __disableAds=true}
index fd212f59fe8d1acd1e23eb5172d84f71b77a2d79..8d02089e1a50682be1e5a74e6ab428fff56d7a29 100644 (file)
@@ -16,12 +16,14 @@ class TaggedArticleList extends AccessibleArticleList {
        /**
         * Creates a new CategoryArticleList object.
         *
-        * @param       Tag     $tag
+        * @param       Tag|Tag[]       $tag
         */
-       public function __construct(Tag $tag) {
+       public function __construct($tag) {
                parent::__construct();
                
                $this->sqlOrderBy = 'article.time ' . ARTICLE_SORT_ORDER;
-               $this->getConditionBuilder()->add("article.articleID IN (SELECT articleID FROM wcf".WCF_N."_article_content WHERE articleContentID IN (SELECT objectID FROM wcf".WCF_N."_tag_to_object WHERE objectTypeID = ? AND languageID = ? AND tagID = ?))", [TagEngine::getInstance()->getObjectTypeID('com.woltlab.wcf.article'), $tag->languageID, $tag->tagID]);
+               
+               $innerSql = TagEngine::getInstance()->getSqlForObjectsByTags('com.woltlab.wcf.article', $tag);
+               $this->getConditionBuilder()->add("article.articleID IN (SELECT articleID FROM wcf".WCF_N."_article_content WHERE articleContentID IN (".$innerSql['sql']."))", [$innerSql['parameters']]);
        }
 }
diff --git a/wcfsetup/install/files/lib/form/TagSearchForm.class.php b/wcfsetup/install/files/lib/form/TagSearchForm.class.php
new file mode 100644 (file)
index 0000000..e4aef35
--- /dev/null
@@ -0,0 +1,125 @@
+<?php
+namespace wcf\form;
+use wcf\data\language\Language;
+use wcf\data\tag\Tag;
+use wcf\system\exception\UserInputException;
+use wcf\system\language\LanguageFactory;
+use wcf\system\request\LinkHandler;
+use wcf\system\tagging\TagCloud;
+use wcf\system\tagging\TagEngine;
+use wcf\system\WCF;
+use wcf\util\ArrayUtil;
+use wcf\util\HeaderUtil;
+
+/**
+ * Shows the tag search form.
+ * 
+ * @author      Alexander Ebert
+ * @copyright   2001-2018 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package     WoltLabSuite\Core\Form
+ * @since       3.2
+ */
+class TagSearchForm extends AbstractCaptchaForm {
+       /**
+        * @var Language[]
+        */
+       public $availableContentLanguages = [];
+       
+       /**
+        * @var int
+        */
+       public $languageID;
+       
+       /**
+        * @var TagCloud
+        */
+       public $tagCloud;
+       
+       /**
+        * @var string[]
+        */
+       public $tagNames;
+       
+       /**
+        * @var Tag[]
+        */
+       public $tags;
+       
+       /**
+        * @inheritDoc
+        */
+       public function readParameters() {
+               parent::readParameters();
+               
+               $this->availableContentLanguages = LanguageFactory::getInstance()->getContentLanguages();
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function readFormParameters() {
+               parent::readFormParameters();
+               
+               if (isset($_POST['languageID'])) $this->languageID = intval($_POST['languageID']);
+               if (isset($_POST['tagNames']) && is_array($_POST['tagNames'])) $this->tagNames = ArrayUtil::trim($_POST['tagNames']);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function validate() {
+               parent::validate();
+               
+               if ($this->languageID !== null) {
+                       if (!in_array($this->languageID, LanguageFactory::getInstance()->getContentLanguageIDs())) {
+                               throw new UserInputException('languageID');
+                       }
+               }
+               
+               $this->tags = TagEngine::getInstance()->getTagsByName($this->tagNames, $this->languageID);
+               if (empty($this->tags)) {
+                       throw new UserInputException('tags');
+               }
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function readData() {
+               if (empty($_POST)) {
+                       $this->languageID = WCF::getLanguage()->languageID;
+               }
+               
+               parent::readData();
+               
+               $this->tagCloud = new TagCloud();
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function save() {
+               parent::save();
+               
+               HeaderUtil::redirect(
+                       LinkHandler::getInstance()->getLink('CombinedTagged', ['tagIDs' => array_keys($this->tags)]),
+                       true,
+                       true
+               );
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function assignVariables() {
+               parent::assignVariables();
+               
+               WCF::getTPL()->assign([
+                       'availableContentLanguages' => $this->availableContentLanguages,
+                       'languageID' => $this->languageID ?: 0,
+                       'tags' => $this->tagCloud->getTags(),
+                       'tagNames' => $this->tagNames,
+               ]);
+       }
+}
diff --git a/wcfsetup/install/files/lib/page/CombinedTaggedPage.class.php b/wcfsetup/install/files/lib/page/CombinedTaggedPage.class.php
new file mode 100644 (file)
index 0000000..992a472
--- /dev/null
@@ -0,0 +1,108 @@
+<?php
+namespace wcf\page;
+use wcf\data\object\type\ObjectType;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\data\tag\Tag;
+use wcf\data\tag\TagList;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\tagging\TypedTagCloud;
+use wcf\system\WCF;
+use wcf\util\ArrayUtil;
+use wcf\util\StringUtil;
+
+/**
+ * Shows the a list of tagged objects.
+ * 
+ * @author     Marcel Werk
+ * @copyright  2001-2018 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Page
+ */
+class CombinedTaggedPage extends TaggedPage {
+       public $tags = [];
+       public $tagIDs = [];
+       
+       /**
+        * @inheritDoc
+        */
+       public function readParameters() {
+               MultipleLinkPage::readParameters();
+               
+               if (isset($_GET['tagIDs']) && is_array($this->tagIDs)) $this->tagIDs = ArrayUtil::toIntegerArray($_GET['tagIDs']);
+               if (empty($this->tagIDs)) {
+                       throw new IllegalLinkException();
+               }
+               else if (count($this->tagIDs) > SEARCH_MAX_COMBINED_TAGS) {
+                       throw new PermissionDeniedException();
+               }
+               
+               $tagList = new TagList();
+               $tagList->getConditionBuilder()->add('tagID IN (?)', [$this->tagIDs]);
+               $tagList->readObjects();
+               
+               $this->tags = $tagList->getObjects();
+               if (empty($this->tags)) {
+                       throw new IllegalLinkException();
+               }
+               
+               $this->availableObjectTypes = ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.tagging.taggableObject');
+               foreach ($this->availableObjectTypes as $key => $objectType) {
+                       if (!$objectType->validateOptions() || !$objectType->validatePermissions()) {
+                               unset($this->availableObjectTypes[$key]);
+                       }
+               }
+               
+               if (empty($this->availableObjectTypes)) {
+                       throw new IllegalLinkException();
+               }
+               
+               if (isset($_REQUEST['objectType'])) {
+                       $objectType = StringUtil::trim($_REQUEST['objectType']);
+                       if (!isset($this->availableObjectTypes[$objectType])) {
+                               throw new IllegalLinkException();
+                       }
+                       $this->objectType = $this->availableObjectTypes[$objectType];
+               }
+               else {
+                       // No object type provided, use the first object type.
+                       $this->objectType = reset($this->availableObjectTypes);
+               }
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       protected function initObjectList() {
+               $this->objectList = $this->objectType->getProcessor()->getObjectListFor($this->tags);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function readData() {
+               parent::readData();
+               
+               $this->tagCloud = new TypedTagCloud($this->objectType->objectType);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function assignVariables() {
+               parent::assignVariables();
+               
+               WCF::getTPL()->assign([
+                       'tag' => $this->tag,
+                       'tags' => $this->tagCloud->getTags(100),
+                       'availableObjectTypes' => $this->availableObjectTypes,
+                       'objectType' => $this->objectType->objectType,
+                       'resultListTemplateName' => $this->objectType->getProcessor()->getTemplateName(),
+                       'resultListApplication' => $this->objectType->getProcessor()->getApplication()
+               ]);
+               
+               if (count($this->objectList) === 0) {
+                       @header('HTTP/1.1 404 Not Found');
+               }
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/tagging/AbstractCombinedTaggable.class.php b/wcfsetup/install/files/lib/system/tagging/AbstractCombinedTaggable.class.php
new file mode 100644 (file)
index 0000000..3c2c29a
--- /dev/null
@@ -0,0 +1,28 @@
+<?php
+namespace wcf\system\tagging;
+use wcf\data\tag\Tag;
+
+/**
+ * Abstract implementation of a taggable with support for searches with multiple tags.
+ * 
+ * @author      Alexander Ebert
+ * @copyright   2001-2018 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package     WoltLabSuite\Core\System\Tagging
+ * @since       3.2
+ */
+abstract class AbstractCombinedTaggable extends AbstractTaggable implements ICombinedTaggable {
+       /**
+        * @inheritDoc
+        */
+       public function getObjectList(Tag $tag) {
+               return $this->getObjectListFor([$tag]);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getObjectListFor(array $tags) {
+               return null;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/tagging/ICombinedTaggable.class.php b/wcfsetup/install/files/lib/system/tagging/ICombinedTaggable.class.php
new file mode 100644 (file)
index 0000000..221e8f8
--- /dev/null
@@ -0,0 +1,25 @@
+<?php
+namespace wcf\system\tagging;
+use wcf\data\DatabaseObjectList;
+use wcf\data\tag\Tag;
+
+/**
+ * Extended interface for taggable objects that support searches for objects
+ * that match multiple tags.
+ * 
+ * @author      Alexander Ebert
+ * @copyright   2001-2018 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package     WoltLabSuite\Core\System\Tagging
+ * @since       3.2
+ */
+interface ICombinedTaggable extends ITaggable {
+       /**
+        * Returns a list of tagged objects that match all provided tags.
+        *
+        * @param Tag[] $tags
+        * @return DatabaseObjectList
+        * @since 3.2
+        */
+       public function getObjectListFor(array $tags);
+}
index e92108470355cf4219ad980def9ff13c8dcd8b98..38ead2d90b95299fb263159049163bb91f859d77 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 namespace wcf\system\tagging;
+use wcf\data\DatabaseObjectList;
 use wcf\data\tag\Tag;
 
 /**
@@ -15,7 +16,8 @@ interface ITaggable {
         * Returns a list of tagged objects.
         * 
         * @param       Tag     $tag
-        * @return      \wcf\data\DatabaseObjectList
+        * @return      DatabaseObjectList
+        * @deprecated 3.2
         */
        public function getObjectList(Tag $tag);
        
index 4579c59d05ac0e12ce30df716fff7474daee0c29..6745df00b0beca0e430ac7e41046f7c30d49ef17 100644 (file)
@@ -3,8 +3,10 @@ namespace wcf\system\tagging;
 use wcf\data\object\type\ObjectTypeCache;
 use wcf\data\tag\Tag;
 use wcf\data\tag\TagAction;
+use wcf\data\tag\TagList;
 use wcf\system\database\util\PreparedStatementConditionBuilder;
 use wcf\system\exception\InvalidObjectTypeException;
+use wcf\system\language\LanguageFactory;
 use wcf\system\SingletonFactory;
 use wcf\system\WCF;
 
@@ -229,4 +231,52 @@ class TagEngine extends SingletonFactory {
                
                return key($languageIDs);
        }
+       
+       /**
+        * Generates the inner SQL statement to fetch object ids that have all listed
+        * tags assigned to them.
+        * 
+        * @param string $objectType
+        * @param Tag[] $tags
+        * @return array
+        * @since 3.2
+        */
+       public function getSqlForObjectsByTags($objectType, array $tags) {
+               $parameters = [$this->getObjectTypeID($objectType)];
+               $tagIDs = implode(',', array_map(function(Tag $tag) {
+                       $parameters[] = $tag->tagID;
+                       
+                       return '?';
+               }, $tags));
+               $parameters[] = count($tags);
+               
+               $sql = "SELECT          objectID
+                       FROM            wcf".WCF_N."_tag_to_object
+                       WHERE           objectTypeID = ?
+                                       AND tagID IN (".$tagIDs.")
+                       GROUP BY        objectID
+                       HAVING          COUNT(objectID) = ?";
+               
+               return [
+                       'sql' => $sql,
+                       'parameters' => $parameters,
+               ];
+       }
+       
+       /**
+        * Returns the matching tags by name.
+        * 
+        * @param string[] $names
+        * @param int $languageID
+        * @return Tag[]
+        * @since 3.2
+        */
+       public function getTagsByName(array $names, $languageID) {
+               $tagList = new TagList();
+               $tagList->getConditionBuilder()->add('name IN (?)', [$names]);
+               $tagList->getConditionBuilder()->add('languageID = ?', [$languageID ?: WCF::getLanguage()->languageID]);
+               $tagList->readObjects();
+               
+               return $tagList->getObjects();
+       }
 }
index 0b8d7a0cf5d20dd637f4de08246ba628e396d554..0df69e645d26c5aecf625cc325db51cad3296006 100644 (file)
@@ -1,7 +1,6 @@
 <?php
 namespace wcf\system\tagging;
 use wcf\data\article\TaggedArticleList;
-use wcf\data\tag\Tag;
 
 /**
  * Implementation of ITaggable for tagging of cms articles.
@@ -12,12 +11,12 @@ use wcf\data\tag\Tag;
  * @package    WoltLabSuite\Core\System\Tagging
  * @since      3.0
  */
-class TaggableArticle extends AbstractTaggable {
+class TaggableArticle extends AbstractCombinedTaggable {
        /**
         * @inheritDoc
         */
-       public function getObjectList(Tag $tag) {
-               return new TaggedArticleList($tag);
+       public function getObjectListFor(array $tags) {
+               return new TaggedArticleList($tags);
        }
        
        /**