From 1bcacda867167701bdcf82e9f7814f99de9afde1 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Wed, 2 Jan 2019 18:28:18 +0100 Subject: [PATCH] Alternate search mode using tags --- com.woltlab.wcf/templates/search.tpl | 201 ++++++++++-------- com.woltlab.wcf/templates/tagSearch.tpl | 78 +++++++ .../data/article/TaggedArticleList.class.php | 8 +- .../files/lib/form/TagSearchForm.class.php | 125 +++++++++++ .../lib/page/CombinedTaggedPage.class.php | 108 ++++++++++ .../AbstractCombinedTaggable.class.php | 28 +++ .../tagging/ICombinedTaggable.class.php | 25 +++ .../lib/system/tagging/ITaggable.class.php | 4 +- .../lib/system/tagging/TagEngine.class.php | 50 +++++ .../system/tagging/TaggableArticle.class.php | 7 +- 10 files changed, 532 insertions(+), 102 deletions(-) create mode 100644 com.woltlab.wcf/templates/tagSearch.tpl create mode 100644 wcfsetup/install/files/lib/form/TagSearchForm.class.php create mode 100644 wcfsetup/install/files/lib/page/CombinedTaggedPage.class.php create mode 100644 wcfsetup/install/files/lib/system/tagging/AbstractCombinedTaggable.class.php create mode 100644 wcfsetup/install/files/lib/system/tagging/ICombinedTaggable.class.php diff --git a/com.woltlab.wcf/templates/search.tpl b/com.woltlab.wcf/templates/search.tpl index fe309937f4..20472ee81c 100644 --- a/com.woltlab.wcf/templates/search.tpl +++ b/com.woltlab.wcf/templates/search.tpl @@ -7,106 +7,119 @@ {/if}
-
- -
-
- - {if $errorField == 'q'} - - {if $errorType == 'empty'} - {lang}wcf.global.form.error.empty{/lang} - {else} - {lang}wcf.search.query.error.{@$errorType}{/lang} - {/if} - - {/if} - - {event name='queryOptions'} +
+
- - -
-
-
- - - {event name='authorOptions'} -
-
- -
-
-
- - - {event name='periodOptions'} -
-
+ {event name='tabMenuTabs'} + + -
-
-
- +
+
+ +
+
+ + {if $errorField == 'q'} + + {if $errorType == 'empty'} + {lang}wcf.global.form.error.empty{/lang} + {else} + {lang}wcf.search.query.error.{@$errorType}{/lang} + {/if} + + {/if} + + {event name='queryOptions'} + + {lang}wcf.search.query.description{/lang} +
+
- +
+
+
+ + + {event name='authorOptions'} +
+
- {event name='displayOptions'} - - - - {event name='generalFields'} - -
-
{lang}wcf.search.type{/lang}
-
- {foreach from=$objectTypes key=objectTypeName item=objectType} - {if $objectType->isAccessible()} - - {/if} - {/foreach} -
-
-
- - {event name='sections'} - - {foreach from=$objectTypes key=objectTypeName item=objectType} - {if $objectType->isAccessible() && $objectType->getFormTemplateName()} - {assign var='__jsID' value='.'|str_replace:'_':$objectTypeName} -
-

{lang}wcf.search.type.{@$objectTypeName}{/lang}

+
+
+
+ + + {event name='periodOptions'} +
+
+ +
+
+
+ + + + + {event name='displayOptions'} +
+
- {include file=$objectType->getFormTemplateName() application=$objectType->getApplication()} + {event name='generalFields'} - -
- {/if} - {/foreach} - - {include file='captcha' supportsAsyncCaptcha=true} - -
- - {@SECURITY_TOKEN_INPUT_TAG} +
+
{lang}wcf.search.type{/lang}
+
+ {foreach from=$objectTypes key=objectTypeName item=objectType} + {if $objectType->isAccessible()} + + {/if} + {/foreach} +
+
+
+ + {event name='sections'} + + {foreach from=$objectTypes key=objectTypeName item=objectType} + {if $objectType->isAccessible() && $objectType->getFormTemplateName()} + {assign var='__jsID' value='.'|str_replace:'_':$objectTypeName} +
+

{lang}wcf.search.type.{@$objectTypeName}{/lang}

+ + {include file=$objectType->getFormTemplateName() application=$objectType->getApplication()} + + +
+ {/if} + {/foreach} + + {include file='captcha' supportsAsyncCaptcha=true} + +
+ + {@SECURITY_TOKEN_INPUT_TAG} +
+
diff --git a/com.woltlab.wcf/templates/tagSearch.tpl b/com.woltlab.wcf/templates/tagSearch.tpl new file mode 100644 index 0000000000..b824483ca2 --- /dev/null +++ b/com.woltlab.wcf/templates/tagSearch.tpl @@ -0,0 +1,78 @@ +{include file='header' __disableAds=true} + +{include file='formError'} + +{if $errorMessage|isset} +

{@$errorMessage}

+{/if} + +
+
+ + +
+
+ {include file='messageFormMultilingualism'} + +
+
+
+ + {lang}wcf.tagging.tags.description{/lang} +
+
+ + +
+ + {if !$tags|empty} +
+

{lang}wcf.search.type.tags.popular{/lang}

+ + {include file='tagCloudBox'} +
+ {/if} + + {event name='sections'} + + {include file='captcha' supportsAsyncCaptcha=true} + +
+ + {@SECURITY_TOKEN_INPUT_TAG} +
+
+
+
+ +{include file='footer' __disableAds=true} diff --git a/wcfsetup/install/files/lib/data/article/TaggedArticleList.class.php b/wcfsetup/install/files/lib/data/article/TaggedArticleList.class.php index fd212f59fe..8d02089e1a 100644 --- a/wcfsetup/install/files/lib/data/article/TaggedArticleList.class.php +++ b/wcfsetup/install/files/lib/data/article/TaggedArticleList.class.php @@ -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 index 0000000000..e4aef35b2c --- /dev/null +++ b/wcfsetup/install/files/lib/form/TagSearchForm.class.php @@ -0,0 +1,125 @@ + + * @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 index 0000000000..992a472052 --- /dev/null +++ b/wcfsetup/install/files/lib/page/CombinedTaggedPage.class.php @@ -0,0 +1,108 @@ + + * @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 index 0000000000..3c2c29a489 --- /dev/null +++ b/wcfsetup/install/files/lib/system/tagging/AbstractCombinedTaggable.class.php @@ -0,0 +1,28 @@ + + * @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 index 0000000000..221e8f8d37 --- /dev/null +++ b/wcfsetup/install/files/lib/system/tagging/ICombinedTaggable.class.php @@ -0,0 +1,25 @@ + + * @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); +} diff --git a/wcfsetup/install/files/lib/system/tagging/ITaggable.class.php b/wcfsetup/install/files/lib/system/tagging/ITaggable.class.php index e921084703..38ead2d90b 100644 --- a/wcfsetup/install/files/lib/system/tagging/ITaggable.class.php +++ b/wcfsetup/install/files/lib/system/tagging/ITaggable.class.php @@ -1,5 +1,6 @@ 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(); + } } diff --git a/wcfsetup/install/files/lib/system/tagging/TaggableArticle.class.php b/wcfsetup/install/files/lib/system/tagging/TaggableArticle.class.php index 0b8d7a0cf5..0df69e645d 100644 --- a/wcfsetup/install/files/lib/system/tagging/TaggableArticle.class.php +++ b/wcfsetup/install/files/lib/system/tagging/TaggableArticle.class.php @@ -1,7 +1,6 @@