<name>com.woltlab.wcf.tagging.taggableObject</name>
<interfacename>wcf\system\tagging\ITaggable</interfacename>
</definition>
+
+ <definition>
+ <name>com.woltlab.wcf.searchableObjectType</name>
+ <interfacename>wcf\system\search\ISearchableObjectType</interfacename>
+ </definition>
</import>
</data>
<category name="message.sidebar">
<parent>message</parent>
</category>
+
+ <category name="message.search">
+ <parent>message</parent>
+ </category>
<!-- /message -->
<category name="dashboard">
<maxvalue>255</maxvalue>
</option>
<!-- /message.general -->
+
+ <option name="search_results_per_page">
+ <categoryname>message.search</categoryname>
+ <optiontype>integer</optiontype>
+ <defaultvalue>20</defaultvalue>
+ <minvalue>5</minvalue>
+ <maxvalue>40</maxvalue>
+ </option>
+ <option name="search_use_captcha">
+ <categoryname>security.antispam</categoryname>
+ <optiontype>boolean</optiontype>
+ </option>
+
+ <option name="search_default_sort_field">
+ <categoryname>message.search</categoryname>
+ <optiontype>select</optiontype>
+ <selectoptions><![CDATA[relevance:wcf.search.sortBy.relevance
+subject:wcf.global.subject
+time:wcf.search.sortBy.time
+username:wcf.search.sortBy.username]]></selectoptions>
+ <defaultvalue><![CDATA[relevance]]></defaultvalue>
+ </option>
+ <option name="search_default_sort_order">
+ <categoryname>message.search</categoryname>
+ <optiontype>select</optiontype>
+ <defaultvalue>DESC</defaultvalue>
+ <selectoptions><![CDATA[ASC:wcf.global.sortOrder.ascending
+DESC:wcf.global.sortOrder.descending]]></selectoptions>
+ </option>
</options>
</import>
</data>
</ul>
{/hascontent}
- {event name='searchArea'}
+ {include file='searchArea'}
</div>
</nav>
--- /dev/null
+{include file='documentHeader'}
+
+<head>
+ <title>{lang}wcf.search.title{/lang} - {PAGE_TITLE|language}</title>
+
+ {include file='headInclude'}
+</head>
+
+<body id="tpl{$templateName|ucfirst}">
+
+{include file='header'}
+
+<header class="boxHeadline">
+ <h1>{lang}wcf.search.title{/lang}</h1>
+</header>
+
+{include file='userNotice'}
+
+{if $errorField}
+ <p class="error">{lang}wcf.global.form.error{/lang}</p>
+{/if}
+
+{if $errorMessage|isset}
+ <p class="error">{@$errorMessage}</p>
+{/if}
+
+<form method="post" action="{link controller='Search'}{/link}">
+ <div class="container containerPadding marginTop">
+ <fieldset>
+ <legend>{lang}wcf.search.general{/lang}</legend>
+
+ <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="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="checked"{/if} /> {lang}wcf.search.subjectOnly{/lang}</label>
+ {event name='queryOptions'}
+
+ <p><small>{lang}wcf.search.query.description{/lang}</small></p>
+ </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="long" maxlength="255" autocomplete="off" />
+ <label><input type="checkbox" name="nameExactly" value="1"{if $nameExactly == 1} checked="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}" placeholder="{lang}wcf.date.period.start{/lang}" />
+ <input type="date" id="endDate" name="endDate" value="{$endDate}" 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="selected"{/if}>{lang}wcf.search.sortBy.relevance{/lang}</option>
+ <option value="subject"{if $sortField == 'subject'} selected="selected"{/if}>{lang}wcf.search.sortBy.subject{/lang}</option>
+ <option value="time"{if $sortField == 'time'} selected="selected"{/if}>{lang}wcf.search.sortBy.time{/lang}</option>
+ <option value="username"{if $sortField == 'username'} selected="selected"{/if}>{lang}wcf.search.sortBy.username{/lang}</option>
+ </select>
+
+ <select name="sortOrder">
+ <option value="ASC"{if $sortOrder == 'ASC'} selected="selected"{/if}>{lang}wcf.global.sortOrder.ascending{/lang}</option>
+ <option value="DESC"{if $sortOrder == 'DESC'} selected="selected"{/if}>{lang}wcf.global.sortOrder.descending{/lang}</option>
+ </select>
+ {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="checked"{/if} /> {lang}wcf.search.type.{@$objectTypeName}{/lang}</label>
+ {/if}
+ {/foreach}
+ </dd>
+ </dl>
+ </fieldset>
+
+ {event name='fieldsets'}
+ {if $useCaptcha}{include file='recaptcha'}{/if}
+
+ {foreach from=$objectTypes key=objectTypeName item=objectType}
+ {if $objectType->isAccessible() && $objectType->getFormTemplateName()}
+ {assign var='__jsID' value='.'|str_replace:'_':$objectTypeName}
+ <fieldset id="{@$__jsID}Form">
+ <legend>{lang}wcf.search.type.{@$objectTypeName}{/lang}</legend>
+
+ {include file=$objectType->getFormTemplateName() application=$objectType->getApplication()}
+
+ <script type="text/javascript">
+ //<![CDATA[
+ $(function() {
+ $('#{@$__jsID}').click(function() {
+ if (this.checked) $('#{@$__jsID}Form').wcfFadeIn();
+ else $('#{@$__jsID}Form').wcfFadeOut();
+ });
+ {if !$objectTypeName|in_array:$selectedObjectTypes}$('#{@$__jsID}Form').hide();{/if}
+ });
+ //]]>
+ </script>
+ </fieldset>
+ {/if}
+ {/foreach}
+ </div>
+
+ <div class="formSubmit">
+ <input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s" />
+ </div>
+</form>
+
+{include file='footer'}
+
+<script type="text/javascript">
+ //<![CDATA[
+ $(function() {
+ new WCF.Search.User($('#searchAuthor'), function(data) {
+ $('#searchAuthor').val(data.label);//.focus();
+ });
+ });
+ //]]>
+</script>
+
+</body>
+</html>
\ No newline at end of file
--- /dev/null
+{capture assign='__searchFormLink'}{link controller='Search'}{/link}{/capture}
+{capture assign='__searchInputPlaceholder'}{lang}wcf.global.search.enterSearchTerm{/lang}{/capture}
+{capture assign='__searchDropdownOptions'}<label><input type="checkbox" name="subjectOnly" value="1" /> {lang}wcf.search.subjectOnly{/lang}</label>{/capture}
+{assign var='__searchHiddenInputFields' value=''}
+
+{event name='settings'}
+
+<aside id="search" class="searchBar dropdown">
+ <form method="post" action="{@$__searchFormLink}">
+ <input type="search" name="q" placeholder="{@$__searchInputPlaceholder}" autocomplete="off" required="required" value="" class="dropdownToggle" data-toggle="search" />
+
+ <ul class="dropdownMenu">
+ {hascontent}
+ <li class="dropdownText">
+ {content}
+ {@$__searchDropdownOptions}
+ {/content}
+ </li>
+ <li class="dropdownDivider"></li>
+ {/hascontent}
+ <li><a href="{@$__searchFormLink}">{lang}wcf.search.extended{/lang}</a></li>
+ </ul>
+
+ {@$__searchHiddenInputFields}
+ </form>
+</aside>
+
+<script type="text/javascript" src="{@$__wcf->getPath('wcf')}js/WCF.Search.Message{if !ENABLE_DEBUG_MODE}.min{/if}.js"></script>
+<script type="text/javascript">
+ //<![CDATA[
+ $(function() {
+ new WCF.Search.Message.SearchArea($('#search'));
+ });
+ //]]>
+</script>
\ No newline at end of file
--- /dev/null
+{include file='documentHeader'}
+
+<head>
+ <title>{lang}wcf.search.results{/lang} - {PAGE_TITLE|language}</title>
+
+ {include file='headInclude'}
+</head>
+
+<body id="tpl{$templateName|ucfirst}">
+
+{include file='header'}
+
+<header class="boxHeadline">
+ <h1>{if $query}<a href="{link controller='Search'}q={$query|urlencode}{/link}">{lang}wcf.search.results{/lang}</a>{else}{lang}wcf.search.results{/lang}{/if}</h1>
+ <p>{lang}wcf.search.results.description{/lang}</p>
+</header>
+
+{include file='userNotice'}
+
+<div class="contentNavigation">
+ {assign var=encodedHighlight value=$highlight|urlencode}
+ {pages print=true assign=pagesLinks controller='SearchResult' id=$searchID link="pageNo=%d&highlight=$encodedHighlight"}
+
+ {hascontent}
+ <nav>
+ <ul>
+ {content}
+ {if $alterable}
+ <li><a href="{link controller='Search'}modify={@$searchID}{/link}" class="button"><span class="icon icon16 icon-search"></span> <span>{lang}wcf.search.results.change{/lang}</span></a></li>
+ {/if}
+ {event name='contentNavigationButtonsTop'}
+ {/content}
+ </ul>
+ </nav>
+ {/hascontent}
+</div>
+
+{include file=$resultListTemplateName application=$resultListApplication}
+
+<div class="contentNavigation">
+ {@$pagesLinks}
+
+ {hascontent}
+ <nav>
+ <ul>
+ {content}
+ {if $alterable}
+ <li><a href="{link controller='Search'}modify={@$searchID}{/link}" class="button"><span class="icon icon16 icon-search"></span> <span>{lang}wcf.search.results.change{/lang}</span></a></li>
+ {/if}
+ {event name='contentNavigationButtonsBottom'}
+ {/content}
+ </ul>
+ </nav>
+ {/hascontent}
+</div>
+
+{include file='footer'}
+
+</body>
+</html>
\ No newline at end of file
--- /dev/null
+<div class="container marginTop">
+ <ul class="containerList messageSearchResultList">
+ {foreach from=$objects item=message}
+ <li>
+ <div class="box48">
+ <a href="{link controller='User' object=$message->getUserProfile()}{/link}" title="{$message->getUserProfile()->username}" class="framed">{@$message->getUserProfile()->getAvatar()->getImageTag(48)}</a>
+
+ <div>
+ <div class="containerHeadline">
+ <h3><a href="{$message->getLink($query)}">{$message->getSubject()}</a></h3>
+ <p>
+ <a href="{link controller='User' object=$message->getUserProfile()}{/link}" class="userLink" data-user-id="{@$message->getUserProfile()->userID}">{$message->getUserProfile()->username}</a>
+ <small>- {@$message->getTime()|time}</small>
+ {if $message->getContainerTitle()}<small>- <a href="{$message->getContainerLink()}">{$message->getContainerTitle()}</a></small>{/if}
+ </p>
+ <small class="containerContentType">{lang}wcf.search.object.{@$message->getObjectTypeName()}{/lang}</small>
+ </div>
+
+ <p>{@$message->getFormattedMessage()}</p>
+ </div>
+ </div>
+ </li>
+ {/foreach}
+ </ul>
+</div>
\ No newline at end of file
--- /dev/null
+/**
+ * Namespace
+ */
+WCF.Search.Message = {};
+
+/**
+ * Provides quick search for search keywords.
+ *
+ * @see WCF.Search.Base
+ */
+WCF.Search.Message.KeywordList = WCF.Search.Base.extend({
+ /**
+ * @see WCF.Search.Base._className
+ */
+ _className: 'wcf\\data\\search\\keyword\\SearchKeywordAction',
+
+ /**
+ * dropdown divider
+ * @var jQuery
+ */
+ _divider: null,
+
+ /**
+ * true, if submit should be forced
+ * @var boolean
+ */
+ _forceSubmit: false,
+
+ /**
+ * @see WCF.Search.Base.init()
+ */
+ init: function(searchInput, callback, excludedSearchValues) {
+ if (!$.isFunction(callback)) {
+ console.debug("[WCF.Search.Message.KeywordList] The given callback is invalid, aborting.");
+ return;
+ }
+
+ this._callback = callback;
+ this._excludedSearchValues = [];
+ if (excludedSearchValues) {
+ this._excludedSearchValues = excludedSearchValues;
+ }
+ this._searchInput = $(searchInput).keyup($.proxy(this._keyUp, this)).keydown($.proxy(function(event) {
+ // block form submit
+ if (event.which === 13) {
+ // ... unless there are no suggestions
+ if (this._itemCount) {
+ event.preventDefault();
+ }
+ }
+ }, this));
+
+ var $dropdownMenu = this._searchInput.next('.dropdownMenu');
+ var $lastDivider = $dropdownMenu.find('li.dropdownDivider').last();
+ this._divider = $('<li class="dropdownDivider" />').hide().insertBefore($lastDivider);
+ this._list = $('<li class="dropdownList" />').hide().insertBefore($lastDivider);
+
+ // supress clicks on checkboxes
+ $dropdownMenu.find('input, label').on('click', function(event) { event.stopPropagation(); });
+
+ this._proxy = new WCF.Action.Proxy({
+ showLoadingOverlay: false,
+ success: $.proxy(this._success, this)
+ });
+ },
+
+ /**
+ * @see WCF.Search.Base._createListItem()
+ */
+ _createListItem: function(item) {
+ this._divider.show();
+ this._list.show();
+
+ this._super(item);
+ },
+
+ /**
+ * @see WCF.Search.Base._clearList()
+ */
+ _clearList: function(clearSearchInput) {
+ if (clearSearchInput) {
+ this._searchInput.val('');
+ }
+
+ this._divider.hide();
+ this._list.hide().empty();
+
+ WCF.CloseOverlayHandler.removeCallback('WCF.Search.Base');
+
+ // reset item navigation
+ this._itemCount = 0;
+ this._itemIndex = -1;
+ }
+});
+
+/**
+ *
+ */
+WCF.Search.Message.SearchArea = Class.extend({
+ _searchArea: null,
+
+ init: function(searchArea) {
+ this._searchArea = searchArea;
+
+ new WCF.Search.Message.KeywordList(this._searchArea.find('input[type=search]'), $.proxy(this._callback, this));
+
+ // forward clicks on the search icon to input field
+ var self = this;
+ var $input = this._searchArea.find('input[type=search]');
+ this._searchArea.click(function(event) {
+ // only forward clicks if the search element itself is the target
+ if (event.target == self._searchArea[0]) {
+ $input.focus().trigger('click');
+ return false;
+ }
+ });
+ },
+
+ _callback: function(data) {
+ this._searchArea.find('input[type=search]').val(data.label);
+ this._searchArea.find('input[type=search]').focus();
+ return false;
+ }
+});
\ No newline at end of file
--- /dev/null
+WCF.Search.Message={};WCF.Search.Message.KeywordList=WCF.Search.Base.extend({_className:"wcf\\data\\search\\keyword\\SearchKeywordAction",_divider:null,_forceSubmit:false,init:function(c,e,b){if(!$.isFunction(e)){console.debug("[WCF.Search.Message.KeywordList] The given callback is invalid, aborting.");return}this._callback=e;this._excludedSearchValues=[];if(b){this._excludedSearchValues=b}this._searchInput=$(c).keyup($.proxy(this._keyUp,this)).keydown($.proxy(function(f){if(f.which===13){if(this._itemCount){f.preventDefault()}}},this));var a=this._searchInput.next(".dropdownMenu");var d=a.find("li.dropdownDivider").last();this._divider=$('<li class="dropdownDivider" />').hide().insertBefore(d);this._list=$('<li class="dropdownList" />').hide().insertBefore(d);a.find("input, label").on("click",function(f){f.stopPropagation()});this._proxy=new WCF.Action.Proxy({showLoadingOverlay:false,success:$.proxy(this._success,this)})},_createListItem:function(a){this._divider.show();this._list.show();this._super(a)},_clearList:function(a){if(a){this._searchInput.val("")}this._divider.hide();this._list.hide().empty();WCF.CloseOverlayHandler.removeCallback("WCF.Search.Base");this._itemCount=0;this._itemIndex=-1}});WCF.Search.Message.SearchArea=Class.extend({_searchArea:null,init:function(b){this._searchArea=b;new WCF.Search.Message.KeywordList(this._searchArea.find("input[type=search]"),$.proxy(this._callback,this));var a=this;var c=this._searchArea.find("input[type=search]");this._searchArea.click(function(d){if(d.target==a._searchArea[0]){c.focus().trigger("click");return false}})},_callback:function(a){this._searchArea.find("input[type=search]").val(a.label);this._searchArea.find("input[type=search]").focus();return false}});
\ No newline at end of file
--- /dev/null
+<?php
+namespace wcf\data\search;
+
+/**
+ * All search result objects should 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.search
+ * @subpackage data.search
+ * @category Community Framework
+ */
+interface ISearchResultObject {
+ /**
+ * Returns author's user profile.
+ *
+ * @return wcf\data\user\UserProfile
+ */
+ public function getUserProfile();
+
+ /**
+ * Returns the subject of this object.
+ *
+ * @return string
+ */
+ public function getSubject();
+
+ /**
+ * Returns the creation time.
+ *
+ * @return integer
+ */
+ public function getTime();
+
+ /**
+ * Returns the link to this object.
+ *
+ * @param string $query search query
+ * @return string
+ */
+ public function getLink($query = '');
+
+ /**
+ * Returns the object type name.
+ *
+ * @return string
+ */
+ public function getObjectTypeName();
+
+ /**
+ * Returns the message text.
+ *
+ * @return string
+ */
+ public function getFormattedMessage();
+
+ /**
+ * Returns the title of object's container. Returns empty string if there
+ * is no container.
+ *
+ * @return string
+ */
+ public function getContainerTitle();
+
+ /**
+ * Returns the link to object's container. Returns empty string if there
+ * is no container.
+ *
+ * @return string
+ */
+ public function getContainerLink();
+}
--- /dev/null
+<?php
+namespace wcf\data\search\keyword;
+use wcf\data\DatabaseObject;
+
+/**
+ * Represents a search keyword.
+ *
+ * @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.search
+ * @subpackage data.search.keyword
+ * @category Community Framework
+ */
+class SearchKeyword extends DatabaseObject {
+ /**
+ * @see wcf\data\DatabaseObject::$databaseTableName
+ */
+ protected static $databaseTableName = 'search_keyword';
+
+ /**
+ * @see wcf\data\DatabaseObject::$databaseTableIndexName
+ */
+ protected static $databaseTableIndexName = 'keywordID';
+}
--- /dev/null
+<?php
+namespace wcf\data\search\keyword;
+use wcf\data\AbstractDatabaseObjectAction;
+use wcf\data\ISearchAction;
+use wcf\system\exception\UserInputException;
+use wcf\system\WCF;
+
+/**
+ * Executes keyword-related actions.
+ *
+ * @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.search
+ * @subpackage data.search.keyword
+ * @category Community Framework
+ */
+class SearchKeywordAction extends AbstractDatabaseObjectAction implements ISearchAction {
+ /**
+ * @see wcf\data\AbstractDatabaseObjectAction::$className
+ */
+ protected $className = 'wcf\data\search\keyword\SearchKeywordEditor';
+
+ /**
+ * @see wcf\data\AbstractDatabaseObjectAction::$allowGuestAccess
+ */
+ protected $allowGuestAccess = array('getSearchResultList');
+
+ /**
+ * @see wcf\data\ISearchAction::validateGetSearchResultList()
+ */
+ public function validateGetSearchResultList() {
+ $this->readString('searchString', false, 'data');
+ }
+
+ /**
+ * @see wcf\data\ISearchAction::getSearchResultList()
+ */
+ public function getSearchResultList() {
+ $list = array();
+
+ // find users
+ $sql = "SELECT *
+ FROM wcf".WCF_N."_search_keyword
+ WHERE keyword LIKE ?
+ ORDER BY searches DESC";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute(array($this->parameters['data']['searchString'].'%'));
+ while ($row = $statement->fetchArray()) {
+ $list[] = array(
+ 'label' => $row['keyword'],
+ 'objectID' => $row['keywordID']
+ );
+ }
+
+ return $list;
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\search\keyword;
+use wcf\data\DatabaseObjectEditor;
+
+/**
+ * Provides functions to edit keywords.
+ *
+ * @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.search
+ * @subpackage data.search.keyword
+ * @category Community Framework
+ */
+class SearchKeywordEditor extends DatabaseObjectEditor {
+ /**
+ * @see wcf\data\DatabaseObjectDecorator::$baseClass
+ */
+ protected static $baseClass = 'wcf\data\search\keyword\SearchKeyword';
+}
--- /dev/null
+<?php
+namespace wcf\data\search\keyword;
+use wcf\data\DatabaseObjectList;
+
+/**
+ * Represents a list of keywords.
+ *
+ * @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.search
+ * @subpackage data.search.keyword
+ * @category Community Framework
+ */
+class SearchKeywordList extends DatabaseObjectList {
+ /**
+ * @see wcf\data\DatabaseObjectList::$className
+ */
+ public $className = 'wcf\data\search\keyword\SearchKeyword';
+}
--- /dev/null
+<?php
+namespace wcf\form;
+use wcf\data\search\Search;
+use wcf\data\search\SearchAction;
+use wcf\system\database\util\PreparedStatementConditionBuilder;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\exception\NamedUserException;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\exception\SystemException;
+use wcf\system\exception\UserInputException;
+use wcf\system\language\LanguageFactory;
+use wcf\system\request\LinkHandler;
+use wcf\system\search\SearchEngine;
+use wcf\system\search\SearchKeywordManager;
+use wcf\system\WCF;
+use wcf\util\HeaderUtil;
+use wcf\util\StringUtil;
+
+/**
+ * Shows the search form.
+ *
+ * @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.search
+ * @subpackage form
+ * @category Community Framework
+ */
+class SearchForm extends RecaptchaForm {
+ /**
+ * list of additional conditions
+ * @var array<string>
+ */
+ public $additionalConditions = array();
+
+ /**
+ * end date
+ * @var integer
+ */
+ public $endDate = '';
+
+ /**
+ * true, if search should be modified
+ * @var boolean
+ */
+ public $modifySearch = null;
+
+ /**
+ * search id used for modification
+ * @var integer
+ */
+ public $modifySearchID = 0;
+
+ /**
+ * require exact matches
+ * @var integer
+ */
+ public $nameExactly = 1;
+
+ /**
+ * search query
+ * @var string
+ */
+ public $query = '';
+
+ /**
+ * list of search results
+ * @var array
+ */
+ public $results = array();
+
+ /**
+ * @see wcf\page\SortablePage::$sortField
+ */
+ public $sortField = SEARCH_DEFAULT_SORT_FIELD;
+
+ /**
+ * @see wcf\page\SortablePage::$sortOrder
+ */
+ public $sortOrder = SEARCH_DEFAULT_SORT_ORDER;
+
+ /**
+ * user id
+ * @var integer
+ */
+ public $userID = 0;
+
+ /**
+ * username
+ * @var string
+ */
+ public $username = '';
+
+ /**
+ * @see wcf\form\RecaptchaForm::$useCaptcha
+ */
+ public $useCaptcha = SEARCH_USE_CAPTCHA;
+
+ /**
+ * parameters used for previous search
+ * @var array
+ */
+ public $searchData = array();
+
+ /**
+ * search id
+ * @var integer
+ */
+ public $searchID = 0;
+
+ /**
+ * PreparedStatementConditionBuilder object
+ * @var wcf\system\database\util\PreparedStatementConditionBuilder
+ */
+ public $searchIndexCondition = null;
+
+ /**
+ * search hash to modify existing search
+ * @var string
+ */
+ public $searchHash = '';
+
+ /**
+ * selected object types
+ * @var array<string>
+ */
+ public $selectedObjectTypes = array();
+
+ /**
+ * start date
+ * @var integer
+ */
+ public $startDate = '';
+
+ /**
+ * search for subject only
+ * @var integer
+ */
+ public $subjectOnly = 0;
+
+ /**
+ * mark as submitted form if modifying search
+ * @var boolean
+ */
+ public $submit = false;
+
+ /**
+ * @see wcf\page\IPage::readParameters()
+ */
+ public function readParameters() {
+ parent::readParameters();
+
+ if (isset($_REQUEST['q'])) $this->query = StringUtil::trim($_REQUEST['q']);
+ if (isset($_REQUEST['username'])) $this->username = StringUtil::trim($_REQUEST['username']);
+ if (isset($_REQUEST['userID'])) $this->userID = intval($_REQUEST['userID']);
+ if (isset($_REQUEST['types']) && is_array($_REQUEST['types'])) $this->selectedObjectTypes = $_REQUEST['types'];
+ $this->submit = (!empty($_POST) || !empty($this->query) || !empty($this->username) || $this->userID);
+
+ if (isset($_REQUEST['modify'])) {
+ $this->modifySearchID = intval($_REQUEST['modify']);
+ $this->modifySearch = new Search($this->modifySearchID);
+
+ if (!$this->modifySearch->searchID || ($this->modifySearch->userID && $this->modifySearch->userID != WCF::getUser()->userID)) {
+ throw new IllegalLinkException();
+ }
+
+ $this->searchData = unserialize($this->modifySearch->searchData);
+ if (empty($this->searchData['alterable'])) {
+ throw new IllegalLinkException();
+ }
+ $this->query = $this->searchData['query'];
+ $this->sortOrder = $this->searchData['sortOrder'];
+ $this->sortField = $this->searchData['sortField'];
+ $this->nameExactly = $this->searchData['nameExactly'];
+ $this->subjectOnly = $this->searchData['subjectOnly'];
+ $this->startDate = $this->searchData['startDate'];
+ $this->endDate = $this->searchData['endDate'];
+ $this->username = $this->searchData['username'];
+ $this->userID = $this->searchData['userID'];
+ $this->selectedObjectTypes = $this->searchData['selectedObjectTypes'];
+
+ if (!empty($_POST)) {
+ $this->submit = true;
+ }
+ }
+
+ // sort order
+ if (isset($_REQUEST['sortField'])) {
+ $this->sortField = $_REQUEST['sortField'];
+ }
+
+ switch ($this->sortField) {
+ case 'subject':
+ case 'time':
+ case 'username': break;
+ case 'relevance': if (!$this->submit || !empty($this->query)) break;
+ default:
+ if (!$this->submit || !empty($this->query)) $this->sortField = 'relevance';
+ else $this->sortField = 'time';
+ }
+
+ if (isset($_REQUEST['sortOrder'])) {
+ $this->sortOrder = $_REQUEST['sortOrder'];
+ switch ($this->sortOrder) {
+ case 'ASC':
+ case 'DESC': break;
+ default: $this->sortOrder = 'DESC';
+ }
+ }
+ }
+
+ /**
+ * @see wcf\form\IForm::readFormParameters()
+ */
+ public function readFormParameters() {
+ parent::readFormParameters();
+
+ $this->nameExactly = 0;
+ if (isset($_POST['nameExactly'])) $this->nameExactly = intval($_POST['nameExactly']);
+ if (isset($_POST['subjectOnly'])) $this->subjectOnly = intval($_POST['subjectOnly']);
+ if (isset($_POST['startDate'])) $this->startDate = $_POST['startDate'];
+ if (isset($_POST['endDate'])) $this->endDate = $_POST['endDate'];
+ }
+
+ /**
+ * @see wcf\form\IForm::validate()
+ */
+ public function validate() {
+ parent::validate();
+
+ // get search conditions
+ $this->getConditions();
+
+ // check query and author
+ if (empty($this->query) && empty($this->username) && !$this->userID) {
+ throw new UserInputException('q');
+ }
+
+ // build search hash
+ $this->searchHash = StringUtil::getHash(serialize(array($this->query, $this->selectedObjectTypes, !$this->subjectOnly, $this->searchIndexCondition, $this->additionalConditions, $this->sortField.' '.$this->sortOrder, PACKAGE_ID)));
+
+ // check search hash
+ if (!empty($this->query)) {
+ $parameters = array($this->searchHash, 'messages', TIME_NOW - 1800);
+ if (WCF::getUser()->userID) $parameters[] = WCF::getUser()->userID;
+
+ $sql = "SELECT searchID
+ FROM wcf".WCF_N."_search
+ WHERE searchHash = ?
+ AND searchType = ?
+ AND searchTime > ?
+ ".(WCF::getUser()->userID ? 'AND userID = ?' : 'AND userID IS NULL');
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute($parameters);
+ $row = $statement->fetchArray();
+ if ($row !== false) {
+ HeaderUtil::redirect(LinkHandler::getInstance()->getLink('SearchResult', array('id' => $row['searchID']), 'highlight='.urlencode($this->query)));
+ exit;
+ }
+ }
+
+ // do search
+ $this->results = SearchEngine::getInstance()->search($this->query, $this->selectedObjectTypes, $this->subjectOnly, $this->searchIndexCondition, $this->additionalConditions, $this->sortField.' '.$this->sortOrder);
+
+ // result is empty
+ if (empty($this->results)) {
+ $this->throwNoMatchesException();
+ }
+ }
+
+ /**
+ * Throws a NamedUserException on search failure.
+ */
+ public function throwNoMatchesException() {
+ if (empty($this->query)) {
+ throw new NamedUserException(WCF::getLanguage()->get('wcf.search.error.user.noMatches'));
+ }
+ else {
+ throw new NamedUserException(WCF::getLanguage()->getDynamicVariable('wcf.search.error.noMatches', array('query' => $this->query)));
+ }
+ }
+
+ /**
+ * @see wcf\form\IForm::save()
+ */
+ public function save() {
+ parent::save();
+
+ // get additional data
+ $additionalData = array();
+ foreach (SearchEngine::getInstance()->getAvailableObjectTypes() as $objectTypeName => $objectType) {
+ if (($data = $objectType->getAdditionalData()) !== null) {
+ $additionalData[$objectTypeName] = $data;
+ }
+ }
+
+ // save result in database
+ $this->searchData = array(
+ 'packageID' => PACKAGE_ID,
+ 'query' => $this->query,
+ 'results' => $this->results,
+ 'additionalData' => $additionalData,
+ 'sortField' => $this->sortField,
+ 'sortOrder' => $this->sortOrder,
+ 'nameExactly' => $this->nameExactly,
+ 'subjectOnly' => $this->subjectOnly,
+ 'startDate' => $this->startDate,
+ 'endDate' => $this->endDate,
+ 'username' => $this->username,
+ 'userID' => $this->userID,
+ 'selectedObjectTypes' => $this->selectedObjectTypes,
+ 'alterable' => (!$this->userID ? 1 : 0)
+ );
+ if ($this->modifySearchID) {
+ $this->objectAction = new SearchAction(array($this->modifySearchID), 'update', array('data' => array(
+ 'searchData' => serialize($this->searchData),
+ 'searchTime' => TIME_NOW,
+ 'searchType' => 'messages',
+ 'searchHash' => $this->searchHash
+ )));
+ $this->objectAction->executeAction();
+ }
+ else {
+ $this->objectAction = new SearchAction(array(), 'create', array('data' => array(
+ 'userID' => (WCF::getUser()->userID ?: null),
+ 'searchData' => serialize($this->searchData),
+ 'searchTime' => TIME_NOW,
+ 'searchType' => 'messages',
+ 'searchHash' => $this->searchHash
+ )));
+ $resultValues = $this->objectAction->executeAction();
+ $this->searchID = $resultValues['returnValues']->searchID;
+ }
+ // save keyword
+ if (!empty($this->query)) {
+ SearchKeywordManager::getInstance()->add($this->query);
+ }
+ $this->saved();
+
+ // forward to result page
+ HeaderUtil::redirect(LinkHandler::getInstance()->getLink('SearchResult', array('id' => $this->searchID), 'highlight='.urlencode($this->query)));
+ exit;
+ }
+
+ /**
+ * @see wcf\page\IPage::assignVariables()
+ */
+ public function assignVariables() {
+ parent::assignVariables();
+
+ // init form
+ foreach (SearchEngine::getInstance()->getAvailableObjectTypes() as $objectType) $objectType->show($this);
+
+ WCF::getTPL()->assign(array(
+ 'query' => $this->query,
+ 'subjectOnly' => $this->subjectOnly,
+ 'username' => $this->username,
+ 'nameExactly' => $this->nameExactly,
+ 'startDate' => $this->startDate,
+ 'endDate' => $this->endDate,
+ 'sortField' => $this->sortField,
+ 'sortOrder' => $this->sortOrder,
+ 'selectedObjectTypes' => $this->selectedObjectTypes,
+ 'objectTypes' => SearchEngine::getInstance()->getAvailableObjectTypes()
+ ));
+ }
+
+ /**
+ * @see wcf\page\IPage::show()
+ */
+ public function show() {
+ if (empty($_POST) && $this->submit) {
+ if ($this->userID) $this->useCaptcha = false;
+ $this->submit();
+ }
+
+ parent::show();
+ }
+
+ /**
+ * Gets the conditions for a search in the table of the selected object types.
+ */
+ protected function getConditions() {
+ if (empty($this->selectedObjectTypes)) {
+ $this->selectedObjectTypes = array_keys(SearchEngine::getInstance()->getAvailableObjectTypes());
+ }
+
+ // default conditions
+ $userIDs = $this->getUserIDs();
+ $this->searchIndexCondition = new PreparedStatementConditionBuilder(false);
+
+ // user ids
+ if (!empty($userIDs)) {
+ $this->searchIndexCondition->add('userID IN (?)', array($userIDs));
+ }
+
+ // dates
+ if (($startDate = @strtotime($this->startDate)) && ($endDate = @strtotime($this->endDate))) {
+ $this->searchIndexCondition->add('time BETWEEN ? AND ?', array($startDate, $endDate));
+ }
+
+ // language
+ if (LanguageFactory::getInstance()->multilingualismEnabled() && count(WCF::getUser()->getLanguageIDs())) {
+ $this->searchIndexCondition->add('(languageID IN (?) OR languageID IS NULL)', array(WCF::getUser()->getLanguageIDs()));
+ }
+
+ foreach ($this->selectedObjectTypes as $key => $objectTypeName) {
+ $objectType = SearchEngine::getInstance()->getObjectType($objectTypeName);
+ if ($objectType === null) {
+ throw new SystemException('unknown search object type '.$objectTypeName);
+ }
+
+ try {
+ if (!$objectType->isAccessible()) {
+ throw new PermissionDeniedException();
+ }
+
+ // special conditions
+ if (($conditionBuilder = $objectType->getConditions($this)) !== null) {
+ $this->additionalConditions[$objectTypeName] = $conditionBuilder;
+ }
+ }
+ catch (PermissionDeniedException $e) {
+ unset($this->selectedObjectTypes[$key]);
+ continue;
+ }
+ }
+
+ if (empty($this->selectedObjectTypes)) {
+ $this->throwNoMatchesException();
+ }
+ }
+
+ /**
+ * Returns user ids.
+ *
+ * @return array<integer>
+ */
+ public function getUserIDs() {
+ $userIDs = array();
+
+ // username
+ if (!empty($this->username)) {
+ $sql = "SELECT userID
+ FROM wcf".WCF_N."_user
+ WHERE username ".($this->nameExactly ? "= ?" : "LIKE ?");
+ $statement = WCF::getDB()->prepareStatement($sql, 100);
+ $statement->execute(array($this->username.(!$this->nameExactly ? '%' : '')));
+ while ($row = $statement->fetchArray()) {
+ $userIDs[] = $row['userID'];
+ }
+
+ if (empty($userIDs)) {
+ $this->throwNoMatchesException();
+ }
+ }
+
+ // userID
+ if ($this->userID) {
+ $userIDs[] = $this->userID;
+ }
+
+ return $userIDs;
+ }
+}
--- /dev/null
+<?php
+namespace wcf\page;
+use wcf\data\search\Search;
+use wcf\system\event\EventHandler;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\search\SearchEngine;
+use wcf\system\WCF;
+
+/**
+ * Shows the result of a search request.
+ *
+ * @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.search
+ * @subpackage page
+ * @category Community Framework
+ */
+class SearchResultPage extends MultipleLinkPage {
+ /**
+ * @see wcf\page\MultipleLinkPage::$itemsPerPage
+ */
+ public $itemsPerPage = SEARCH_RESULTS_PER_PAGE;
+
+ /**
+ * highlight string
+ * @var string
+ */
+ public $highlight = '';
+
+ /**
+ * search id
+ * @var integer
+ */
+ public $searchID = 0;
+
+ /**
+ * search object
+ * @var wcf\data\search\Search
+ */
+ public $search = null;
+
+ /**
+ * messages
+ * @var array
+ */
+ public $messages = array();
+
+ /**
+ * search data
+ * @var array
+ */
+ public $searchData = null;
+
+ /**
+ * result list template
+ * @var string
+ */
+ public $resultListTemplateName = 'searchResultList';
+
+ /**
+ * result list template's application
+ * @var string
+ */
+ public $resultListApplication = 'wcf';
+
+ /**
+ * @see wcf\page\IPage::readParameters()
+ */
+ public function readParameters() {
+ parent::readParameters();
+
+ if (isset($_REQUEST['highlight'])) $this->highlight = $_REQUEST['highlight'];
+ if (isset($_REQUEST['id'])) $this->searchID = intval($_REQUEST['id']);
+ $this->search = new Search($this->searchID);
+ if (!$this->search->searchID || $this->search->searchType != 'messages') {
+ throw new IllegalLinkException();
+ }
+ if ($this->search->userID && $this->search->userID != WCF::getUser()->userID) {
+ throw new IllegalLinkException();
+ }
+
+ // get search data
+ $this->searchData = unserialize($this->search->searchData);
+
+ // check package id of this search
+ if (!empty($this->searchData['packageID']) && $this->searchData['packageID'] != PACKAGE_ID) {
+ throw new IllegalLinkException();
+ }
+ }
+
+ /**
+ * @see wcf\page\IPage::readData()
+ */
+ public function readData() {
+ parent::readData();
+
+ // cache message data
+ $this->cacheMessageData();
+
+ // get messages
+ $this->readMessages();
+ }
+
+ /**
+ * Caches the message data.
+ */
+ protected function cacheMessageData() {
+ $types = array();
+
+ // group object id by object type
+ for ($i = $this->startIndex - 1; $i < $this->endIndex; $i++) {
+ $type = $this->searchData['results'][$i]['objectType'];
+ $objectID = $this->searchData['results'][$i]['objectID'];
+
+ if (!isset($types[$type])) $types[$type] = array();
+ $types[$type][] = $objectID;
+ }
+
+ foreach ($types as $type => $objectIDs) {
+ $objectType = SearchEngine::getInstance()->getObjectType($type);
+ $objectType->cacheObjects($objectIDs, (isset($this->searchData['additionalData'][$type]) ? $this->searchData['additionalData'][$type] : null));
+ }
+ }
+
+ /**
+ * Gets the data of the messages.
+ */
+ protected function readMessages() {
+ for ($i = $this->startIndex - 1; $i < $this->endIndex; $i++) {
+ $type = $this->searchData['results'][$i]['objectType'];
+ $objectID = $this->searchData['results'][$i]['objectID'];
+
+ $objectType = SearchEngine::getInstance()->getObjectType($type);
+ if (($message = $objectType->getObject($objectID)) !== null) {
+ $this->messages[] = $message;
+ }
+ }
+ }
+
+ /**
+ * @see wcf\page\IPage::assignVariables()
+ */
+ public function assignVariables() {
+ parent::assignVariables();
+
+ WCF::getTPL()->assign(array(
+ 'query' => $this->searchData['query'],
+ 'objects' => $this->messages,
+ 'searchData' => $this->searchData,
+ 'searchID' => $this->searchID,
+ 'highlight' => $this->highlight,
+ 'sortField' => $this->searchData['sortField'],
+ 'sortOrder' => $this->searchData['sortOrder'],
+ 'alterable' => (!empty($this->searchData['alterable']) ? 1 : 0),
+ 'objectTypes' => SearchEngine::getInstance()->getAvailableObjectTypes(),
+ 'resultListTemplateName' => $this->resultListTemplateName,
+ 'resultListApplication' => $this->resultListApplication
+ ));
+ }
+
+ /**
+ * @see wcf\page\MultipleLinkPage::countItems()
+ */
+ public function countItems() {
+ // call countItems event
+ EventHandler::getInstance()->fireAction($this, 'countItems');
+
+ return count($this->searchData['results']);
+ }
+
+ /**
+ * @see wcf\page\MultipleLinkPage::initObjectList()
+ */
+ protected function initObjectList() { }
+
+ /**
+ * @see wcf\page\MultipleLinkPage::readObjects()
+ */
+ protected function readObjects() { }
+}
public function execute(Cronjob $cronjob) {
parent::execute($cronjob);
+ // clean up search keywords
+ $sql = "SELECT AVG(searches) AS searches
+ FROM wcf".WCF_N."_search_keyword";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute();
+ if (($row = $statement->fetchArray()) !== false) {
+ $sql = "DELETE FROM wcf".WCF_N."_search_keyword
+ WHERE searches <= ?
+ AND lastSearchTime < ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute(array(
+ floor($row['searches'] / 4),
+ (TIME_NOW - 86400 * 30)
+ ));
+ }
+
// clean up notifications
$sql = "DELETE FROM wcf".WCF_N."_user_notification
WHERE time < ?";
--- /dev/null
+<?php
+namespace wcf\system\search;
+use wcf\data\object\type\AbstractObjectTypeProcessor;
+use wcf\form\IForm;
+use wcf\system\database\util\PreparedStatementConditionBuilder;
+
+/**
+ * This class provides default implementations for the ISearchableObjectType 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.search
+ * @subpackage system.search
+ * @category Community Framework
+ */
+abstract class AbstractSearchableObjectType extends AbstractObjectTypeProcessor implements ISearchableObjectType {
+ /**
+ * @see wcf\system\search\ISearchableObjectType::show()
+ */
+ public function show(IForm $form = null) {}
+
+ /**
+ * @see wcf\system\search\ISearchableObjectType::getApplication()
+ */
+ public function getApplication() {
+ return 'wcf';
+ }
+
+ /**
+ * @see wcf\system\search\ISearchableObjectType::getConditions()
+ */
+ public function getConditions(IForm $form = null) {
+ return null;
+ }
+
+ /**
+ * @see wcf\system\search\ISearchableObjectType::getJoins()
+ */
+ public function getJoins() {
+ return '';
+ }
+
+ /**
+ * @see wcf\system\search\ISearchableObjectType::getSubjectFieldName()
+ */
+ public function getSubjectFieldName() {
+ return $this->getTableName().'.subject';
+ }
+
+ /**
+ * @see wcf\system\search\ISearchableObjectType::getUsernameFieldName()
+ */
+ public function getUsernameFieldName() {
+ return $this->getTableName().'.username';
+ }
+
+ /**
+ * @see wcf\system\search\ISearchableObjectType::getTimeFieldName()
+ */
+ public function getTimeFieldName() {
+ return $this->getTableName().'.time';
+ }
+
+ /**
+ * @see wcf\system\search\ISearchableObjectType::getAdditionalData()
+ */
+ public function getAdditionalData() {
+ return null;
+ }
+
+ /**
+ * @see wcf\system\search\ISearchableObjectType::isAccessible()
+ */
+ public function isAccessible() {
+ return true;
+ }
+
+ /**
+ * @see wcf\system\search\ISearchableObjectType::getFormTemplateName()
+ */
+ public function getFormTemplateName() {
+ return '';
+ }
+
+ /**
+ * @see wcf\system\search\ISearchableObjectType::getSpecialSQLQuery()
+ */
+ public function getSpecialSQLQuery(PreparedStatementConditionBuilder $fulltextCondition = null, PreparedStatementConditionBuilder $searchIndexConditions = null, PreparedStatementConditionBuilder $additionalConditions = null, $orderBy = 'time DESC') {
+ return '';
+ }
+}
--- /dev/null
+<?php
+namespace wcf\system\search;
+use wcf\form\IForm;
+use wcf\system\database\util\PreparedStatementConditionBuilder;
+
+/**
+ * All searchable object types should 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.search
+ * @subpackage system.search
+ * @category Community Framework
+ */
+interface ISearchableObjectType {
+ /**
+ * Caches the data for the given object ids.
+ *
+ * @param array $objectIDs
+ * @param array $additionalData
+ */
+ public function cacheObjects(array $objectIDs, array $additionalData = null);
+
+ /**
+ * Returns the object with the given object id.
+ *
+ * @param integer $objectID
+ * @return wcf\data\search\ISearchResultObject
+ */
+ public function getObject($objectID);
+
+ /**
+ * Shows the form part of this object type.
+ *
+ * @param wcf\form\IForm $form instance of the form class where the search has taken place
+ */
+ public function show(IForm $form = null);
+
+ /**
+ * Returns the application abbreviation.
+ *
+ * @return string
+ */
+ public function getApplication();
+
+ /**
+ * Returns the search conditions of this message type.
+ *
+ * @param wcf\form\IForm $form
+ * @return wcf\system\database\util\PreparedStatementConditionBuilder
+ */
+ public function getConditions(IForm $form = null);
+
+ /**
+ * Provides the ability to add additional joins to sql search query.
+ *
+ * @return string
+ */
+ public function getJoins();
+
+ /**
+ * Returns the database table name of this message.
+ *
+ * @return string
+ */
+ public function getTableName();
+
+ /**
+ * Returns the database field name of the message id.
+ *
+ * @return string
+ */
+ public function getIDFieldName();
+
+ /**
+ * Returns the database field name of the subject field.
+ *
+ * @return string
+ */
+ public function getSubjectFieldName();
+
+ /**
+ * Returns the database field name of the username.
+ *
+ * @return string
+ */
+ public function getUsernameFieldName();
+
+ /**
+ * Returns the database field name of the time.
+ *
+ * @return string
+ */
+ public function getTimeFieldName();
+
+ /**
+ * Returns additional search information.
+ *
+ * @return mixed
+ */
+ public function getAdditionalData();
+
+ /**
+ * Returns true if the current user can use this searchable object type.
+ *
+ * @return boolean
+ */
+ public function isAccessible();
+
+ /**
+ * Returns the name of the form template for this object type.
+ *
+ * @return string
+ */
+ public function getFormTemplateName();
+
+ /**
+ * Provides the option to replace the default search index SQL query by an own version.
+ *
+ * @param wcf\system\database\util\PreparedStatementConditionBuilder $fulltextCondition
+ * @param wcf\system\database\util\PreparedStatementConditionBuilder $searchIndexConditions
+ * @param wcf\system\database\util\PreparedStatementConditionBuilder $additionalConditions
+ * @param string $orderBy
+ * @return string
+ */
+ public function getSpecialSQLQuery(PreparedStatementConditionBuilder $fulltextCondition = null, PreparedStatementConditionBuilder $searchIndexConditions = null, PreparedStatementConditionBuilder $additionalConditions = null, $orderBy = 'time DESC');
+}
--- /dev/null
+<?php
+namespace wcf\system\search;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\system\database\util\PreparedStatementConditionBuilder;
+use wcf\system\exception\SystemException;
+use wcf\system\SingletonFactory;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * SearchEngine searches for given query in the selected object types.
+ *
+ * @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.search
+ * @subpackage system.search
+ * @category Community Framework
+ */
+class SearchEngine extends SingletonFactory {
+ /**
+ * list of available object types
+ * @var array
+ */
+ protected $availableObjectTypes = array();
+
+ /**
+ * @see wcf\system\SingletonFactory::init()
+ */
+ protected function init() {
+ // get available object types
+ $this->availableObjectTypes = ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.searchableObjectType');
+
+ // get processors
+ foreach ($this->availableObjectTypes as &$objectType) {
+ $objectType = $objectType->getProcessor();
+ }
+ }
+
+ /**
+ * Returns a list of available object types.
+ *
+ * @return array
+ */
+ public function getAvailableObjectTypes() {
+ return $this->availableObjectTypes;
+ }
+
+ /**
+ * Returns the object type with the given name.
+ *
+ * @param string $objectTypeName
+ * @return wcf\data\object\type\ObjectType
+ */
+ public function getObjectType($objectTypeName) {
+ if (isset($this->availableObjectTypes[$objectTypeName])) {
+ return $this->availableObjectTypes[$objectTypeName];
+ }
+
+ return null;
+ }
+
+ /**
+ * Searches for the given string and returns the data of the found messages.
+ *
+ * @param string $q
+ * @param array $objectTypes
+ * @param boolean $subjectOnly
+ * @param wcf\system\database\util\PreparedStatementConditionBuilder $searchIndexCondition
+ * @param array $additionalConditions
+ * @param string $orderBy
+ * @param integer $limit
+ * @return array
+ */
+ public function search($q, array $objectTypes, $subjectOnly = false, PreparedStatementConditionBuilder $searchIndexCondition = null, array $additionalConditions = array(), $orderBy = 'time DESC', $limit = 1000) {
+ // handle sql types
+ $fulltextCondition = null;
+ $relevanceCalc = '';
+ if (!empty($q)) {
+ // expand search terms with a * unless they're encapsulated with quotes
+ $inQuotes = false;
+ $tmp = '';
+ $controlCharacterOrSpace = false;
+ $chars = array('+', '-', '*');
+ for ($i = 0, $length = StringUtil::length($q); $i < $length; $i++) {
+ $char = $q[$i];
+
+ if ($inQuotes) {
+ if ($char == '"') {
+ $inQuotes = false;
+ }
+ }
+ else {
+ if ($char == '"') {
+ $inQuotes = true;
+ }
+ else {
+ if ($char == ' ' && !$controlCharacterOrSpace) {
+ $controlCharacterOrSpace = true;
+ $tmp .= '*';
+ }
+ else if (in_array($char, $chars)) {
+ $controlCharacterOrSpace = true;
+ }
+ else {
+ $controlCharacterOrSpace = false;
+ }
+ }
+ }
+
+ $tmp .= $char;
+ }
+
+ // handle last char
+ if (!$inQuotes && !$controlCharacterOrSpace) {
+ $tmp .= '*';
+ }
+ $q = $tmp;
+
+ $fulltextCondition = new PreparedStatementConditionBuilder(false);
+ switch (WCF::getDB()->getDBType()) {
+ case 'wcf\system\database\MySQLDatabase':
+ $fulltextCondition->add("MATCH (subject".(!$subjectOnly ? ', message, metaData' : '').") AGAINST (? IN BOOLEAN MODE)", array($q));
+ break;
+
+ case 'wcf\system\database\PostgreSQLDatabase':
+ // replace * with :*
+ $q = StringUtil::replace('*', ':*', $q);
+
+ $fulltextCondition->add("fulltextIndex".($subjectOnly ? "SubjectOnly" : '')." @@ to_tsquery(?)", array($q));
+ break;
+
+ default:
+ throw new SystemException("your database type doesn't support fulltext search");
+ }
+
+ if ($orderBy == 'relevance ASC' || $orderBy == 'relevance DESC') {
+ switch (WCF::getDB()->getDBType()) {
+ case 'wcf\system\database\MySQLDatabase':
+ $relevanceCalc = "MATCH (subject".(!$subjectOnly ? ', message, metaData' : '').") AGAINST ('".escapeString($q)."') + (5 / (1 + POW(LN(1 + (".TIME_NOW." - time) / 2592000), 2))) AS relevance";
+ break;
+
+ case 'wcf\system\database\PostgreSQLDatabase':
+ $relevanceCalc = "ts_rank_cd(fulltextIndex".($subjectOnly ? "SubjectOnly" : '').", '".escapeString($q)."') AS relevance";
+ break;
+ }
+ }
+ }
+
+ // build search query
+ $sql = '';
+ $parameters = array();
+ foreach ($objectTypes as $objectTypeName) {
+ $objectType = $this->getObjectType($objectTypeName);
+ if (!empty($sql)) $sql .= "\nUNION\n";
+ if (($specialSQL = $objectType->getSpecialSQLQuery($fulltextCondition, $searchIndexCondition, (isset($additionalConditions[$objectTypeName]) ? $additionalConditions[$objectTypeName] : null), $orderBy))) {
+ $sql .= "(".$specialSQL.")";
+ }
+ else {
+ $sql .= "(
+ SELECT ".$objectType->getIDFieldName()." AS objectID,
+ ".$objectType->getSubjectFieldName()." AS subject,
+ ".$objectType->getTimeFieldName()." AS time,
+ ".$objectType->getUsernameFieldName()." AS username,
+ '".$objectTypeName."' AS objectType
+ ".($relevanceCalc ? ',search_index.relevance' : '')."
+ FROM ".$objectType->getTableName()."
+ INNER JOIN (
+ SELECT objectID
+ ".($relevanceCalc ? ','.$relevanceCalc : '')."
+ FROM wcf".WCF_N."_search_index
+ WHERE ".($fulltextCondition !== null ? $fulltextCondition : '')."
+ ".(($searchIndexCondition !== null && $searchIndexCondition->__toString()) ? ($fulltextCondition !== null ? "AND " : '').$searchIndexCondition : '')."
+ AND objectTypeID = ".$objectType->objectTypeID."
+ ".(!empty($orderBy) && $fulltextCondition === null ? 'ORDER BY '.$orderBy : '')."
+ LIMIT 1000
+ ) search_index
+ ON (".$objectType->getIDFieldName()." = search_index.objectID)
+ ".$objectType->getJoins()."
+ ".(isset($additionalConditions[$objectTypeName]) ? $additionalConditions[$objectTypeName] : '')."
+ )";
+ }
+
+ if ($fulltextCondition !== null) $parameters = array_merge($parameters, $fulltextCondition->getParameters());
+ if ($searchIndexCondition !== null) $parameters = array_merge($parameters, $searchIndexCondition->getParameters());
+ if (isset($additionalConditions[$objectTypeName])) $parameters = array_merge($parameters, $additionalConditions[$objectTypeName]->getParameters());
+ }
+ if (empty($sql)) {
+ throw new SystemException('no object types given');
+ }
+
+ if (!empty($orderBy)) {
+ $sql .= " ORDER BY " . $orderBy;
+ }
+
+ // send search query
+ $messages = array();
+ $statement = WCF::getDB()->prepareStatement($sql, $limit);
+ $statement->execute($parameters);
+ while ($row = $statement->fetchArray()) {
+ $messages[] = array(
+ 'objectID' => $row['objectID'],
+ 'objectType' => $row['objectType']
+ );
+ }
+
+ return $messages;
+ }
+}
--- /dev/null
+<?php
+namespace wcf\system\search;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\system\exception\SystemException;
+use wcf\system\SingletonFactory;
+use wcf\system\WCF;
+
+/**
+ * Manages the search index.
+ *
+ * @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.search
+ * @subpackage system.search
+ * @category Community Framework
+ */
+class SearchIndexManager extends SingletonFactory {
+ /**
+ * list of available object types
+ * @var array
+ */
+ protected $availableObjectTypes = array();
+
+ /**
+ * @see wcf\system\SingletonFactory::init()
+ */
+ protected function init() {
+ // get available object types
+ $this->availableObjectTypes = ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.searchableObjectType');
+ }
+
+ /**
+ * Returns the id of the object type with the given name.
+ *
+ * @param string $objectType
+ * @return integer
+ */
+ public function getObjectTypeID($objectType) {
+ if (!isset($this->availableObjectTypes[$objectType])) {
+ throw new SystemException("unknown object type '".$objectType."'");
+ }
+
+ return $this->availableObjectTypes[$objectType]->objectTypeID;
+ }
+
+ /**
+ * Adds a new entry.
+ *
+ * @param string $objectType
+ * @param integer $objectID
+ * @param string $message
+ * @param string $subject
+ * @param integer $time
+ * @param integer $userID
+ * @param string $username
+ * @param integer $languageID
+ * @param string $metaData
+ */
+ public function add($objectType, $objectID, $message, $subject, $time, $userID, $username, $languageID = null, $metaData = '') {
+ if ($languageID === null) $languageID = 0;
+
+ // save new entry
+ $sql = "INSERT INTO wcf".WCF_N."_search_index
+ (objectTypeID, objectID, subject, message, time, userID, username, languageID, metaData)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute(array($this->getObjectTypeID($objectType), $objectID, $subject, $message, $time, $userID, $username, $languageID, $metaData));
+ }
+
+ /**
+ * Updates the search index.
+ *
+ * @param string $objectType
+ * @param integer $objectID
+ * @param string $message
+ * @param string $subject
+ * @param integer $time
+ * @param integer $userID
+ * @param string $username
+ * @param integer $languageID
+ * @param string $metaData
+ */
+ public function update($objectType, $objectID, $message, $subject, $time, $userID, $username, $languageID = null, $metaData = '') {
+ // delete existing entry
+ $this->delete($objectType, array($objectID), ($languageID === null ? 0 : $languageID));
+
+ // save new entry
+ $this->add($objectType, $objectID, $message, $subject, $time, $userID, $username, $languageID, $metaData);
+ }
+
+ /**
+ * Deletes search index entries.
+ *
+ * @param string $objectType
+ * @param array<integer> $objectIDs
+ */
+ public function delete($objectType, array $objectIDs) {
+ $objectTypeID = $this->getObjectTypeID($objectType);
+
+ $sql = "DELETE FROM wcf".WCF_N."_search_index
+ WHERE objectTypeID = ?
+ AND objectID = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ WCF::getDB()->beginTransaction();
+ foreach ($objectIDs as $objectID) {
+ $parameters = array($objectTypeID, $objectID);
+
+ $statement->execute($parameters);
+ }
+ WCF::getDB()->commitTransaction();
+ }
+}
--- /dev/null
+<?php
+namespace wcf\system\search;
+use wcf\data\search\keyword\SearchKeywordAction;
+use wcf\system\SingletonFactory;
+use wcf\system\WCF;
+
+/**
+ * Manages the search keywords.
+ *
+ * @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.search
+ * @subpackage system.search
+ * @category Community Framework
+ */
+class SearchKeywordManager extends SingletonFactory {
+ /**
+ * Adds the given keyword.
+ *
+ * @param string $keyword
+ */
+ public function add($keyword) {
+ $keyword = static::simplifyKeyword($keyword);
+
+ // search existing entry
+ $sql = "SELECT *
+ FROM wcf".WCF_N."_search_keyword
+ WHERE keyword = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute(array($keyword));
+ if (($object = $statement->fetchObject('wcf\data\search\keyword\SearchKeyword')) !== null) {
+ $action = new SearchKeywordAction(array($object), 'update', array('data' => array(
+ 'searches' => $object->searches + 1,
+ 'lastSearchTime' => TIME_NOW
+ )));
+ $action->executeAction();
+ }
+ else {
+ $action = new SearchKeywordAction(array(), 'create', array('data' => array(
+ 'keyword' => $keyword,
+ 'searches' => 1,
+ 'lastSearchTime' => TIME_NOW
+ )));
+ $action->executeAction();
+ }
+ }
+
+ /**
+ * Returns simplified version of the given keyword.
+ *
+ * @param string $keyword
+ * @return string
+ */
+ public static function simplifyKeyword($keyword) {
+ // TODO: do something useful
+
+ return $keyword;
+ }
+}
--- /dev/null
+<?php
+namespace wcf\system\search;
+use wcf\system\bbcode\KeywordHighlighter;
+use wcf\system\Regex;
+use wcf\system\SingletonFactory;
+use wcf\util\ArrayUtil;
+use wcf\util\StringUtil;
+
+/**
+ * Formats messages for search result output.
+ *
+ * @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.search
+ * @subpackage system.search
+ * @category Community Framework
+ */
+class SearchResultTextParser extends SingletonFactory {
+ /**
+ * max length for message abstract
+ * @var integer
+ */
+ const MAX_LENGTH = 500;
+
+ /**
+ * highlight query
+ * @var mixed
+ */
+ protected $searchQuery = '';
+
+ /**
+ * @see wcf\system\SingletonFactory::init()
+ */
+ protected function init() {
+ if (isset($_GET['highlight'])) {
+ $keywordString = $_GET['highlight'];
+
+ // remove search operators
+ $keywordString = preg_replace('/[\+\-><()~\*]+/', '', $keywordString);
+
+ if (StringUtil::substring($keywordString, 0, 1) == '"' && StringUtil::substring($keywordString, -1) == '"') {
+ // phrases search
+ $keywordString = StringUtil::trim(StringUtil::substring($keywordString, 1, -1));
+
+ if (!empty($keywordString)) {
+ $this->searchQuery = $keywordString;
+ }
+ }
+ else {
+ $this->searchQuery = ArrayUtil::trim(explode(' ', $keywordString));
+ if (empty($this->searchQuery)) {
+ $this->searchQuery = false;
+ }
+ else if (count($this->searchQuery) == 1) {
+ $this->searchQuery = reset($this->searchQuery);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns an abstract of the given message.
+ * Uses search keywords to shift the start and end position of the abstract (like Google).
+ *
+ * @param string $text
+ * @return string
+ */
+ protected function getMessageAbstract($text) {
+ // replace newlines with spaces
+ $text = Regex::compile("\s+")->replace($text, ' ');
+
+ if (StringUtil::length($text) > static::MAX_LENGTH) {
+ if ($this->searchQuery) {
+ // phrase search
+ if (!is_array($this->searchQuery)) {
+ $start = StringUtil::indexOfIgnoreCase($text, $this->searchQuery);
+ if ($start !== false) {
+ $end = $start + StringUtil::length($this->searchQuery);
+ $shiftStartBy = $shiftEndBy = round((static::MAX_LENGTH - StringUtil::length($this->searchQuery)) / 2);
+
+ // shiftStartBy is negative when search query length is over max length
+ if ($shiftStartBy < 0) {
+ $shiftEndBy += $shiftStartBy;
+ $shiftStartBy = 0;
+ }
+
+ // shift abstract start
+ if ($start - $shiftStartBy < 0) {
+ $shiftEndBy += $shiftStartBy - $start;
+ $start = 0;
+ }
+ else {
+ $start -= $shiftStartBy;
+ }
+
+ // shift abstract end
+ if ($end + $shiftEndBy > StringUtil::length($text) - 1) {
+ $shiftStartBy = $end + $shiftEndBy - StringUtil::length($text) - 1;
+ $shiftEndBy = 0;
+ if ($shiftStartBy > $start) {
+ $start = 0;
+ }
+ else {
+ $start -= $shiftStartBy;
+ }
+ }
+ else {
+ $end += $shiftEndBy;
+ }
+
+ $newText = '';
+ if ($start > 0) $newText .= StringUtil::HELLIP;
+ $newText .= StringUtil::substring($text, $start, $end - $start);
+ if ($end < StringUtil::length($text) - 1) $newText .= StringUtil::HELLIP;
+ return $newText;
+ }
+ }
+ else {
+ $matches = array();
+ $shiftLength = static::MAX_LENGTH;
+ // find first match of each keyword
+ foreach ($this->searchQuery as $keyword) {
+ $start = StringUtil::indexOfIgnoreCase($text, $keyword);
+ if ($start !== false) {
+ $shiftLength -= StringUtil::length($keyword);
+ $matches[$keyword] = array('start' => $start, 'end' => $start + StringUtil::length($keyword));
+ }
+ }
+
+ // shift match position
+ $shiftBy = round(($shiftLength / count($this->searchQuery)) / 2);
+ foreach ($matches as $keyword => $position) {
+ $position['start'] -= $shiftBy;
+ $position['end'] += $shiftBy;
+ $matches[$keyword] = $position;
+ }
+
+ $start = 0;
+ $end = StringUtil::length($text) - 1;
+ $newText = '';
+ $i = 0;
+ $length = count($matches);
+ foreach ($matches as $keyword => $position) {
+ if ($position['start'] < $start) {
+ $position['end'] += $start - $position['start'];
+ $position['start'] = $start;
+ }
+
+ if ($position['end'] > $end) {
+ if ($position['start'] > $start) {
+ $shiftStartBy = $position['end'] - $end;
+ if ($position['start'] - $shiftStartBy < $start) {
+ $shiftStartBy = $position['start'] - $start;
+ }
+
+ $position['start'] -= $shiftStartBy;
+ }
+
+ $position['end'] = $end;
+ }
+
+ if ($position['start'] > $start) $newText .= StringUtil::HELLIP;
+ $newText .= StringUtil::substring($text, $position['start'], $position['end'] - $position['start']);
+ if ($i == $length - 1 && $position['end'] < $end) $newText .= StringUtil::HELLIP;
+
+ $start = $position['end'];
+ $i++;
+ }
+
+ if (!empty($newText)) return $newText;
+ }
+ }
+
+ // no search query or no matches
+ return StringUtil::substring($text, 0, static::MAX_LENGTH) . StringUtil::HELLIP;
+ }
+
+ return $text;
+ }
+
+ /**
+ * Formats a message for search result output.
+ *
+ * @param string $text
+ * @return string
+ */
+ public function parse($text) {
+ // remove nonessentials
+ $text = Regex::compile('<!-- begin:parser_nonessential -->.*?<!-- end:parser_nonessential -->', Regex::DOT_ALL)->replace($text, '');
+
+ // remove html codes
+ $text = StringUtil::stripHTML($text);
+
+ // decode html
+ $text = StringUtil::decodeHTML($text);
+
+ // get abstract
+ $text = $this->getMessageAbstract($text);
+
+ // encode html
+ $text = StringUtil::encodeHTML($text);
+
+ // do highlighting
+ return KeywordHighlighter::getInstance()->doHighlight($text);
+ }
+}
--- /dev/null
+.messageSearchResultList {
+ > li > .box48 > div > .containerHeadline > h3 {
+ padding-right: 100px;
+ }
+}
\ No newline at end of file
<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>
+ <item name="wcf.acp.option.category.message.search"><![CDATA[Suchfunktion]]></item>
+ <item name="wcf.acp.option.search_results_per_page"><![CDATA[Ergebnisse pro Seite]]></item>
+ <item name="wcf.acp.option.search_default_sort_field"><![CDATA[Standardsortierung]]></item>
+ <item name="wcf.acp.option.search_default_sort_order"><![CDATA[Standardreihenfolge]]></item>
+ <item name="wcf.acp.option.search_use_captcha"><![CDATA[reCAPTCHA in Suchfunktion aktivieren]]></item>
</category>
<category name="wcf.acp.package">
<item name="wcf.recaptcha.error.recaptchaString.false"><![CDATA[Sie haben leider nicht die korrekten Zeichen eingegeben. Bitte versuchen Sie es erneut!]]></item>
</category>
+ <category name="wcf.search">
+ <item name="wcf.search.author"><![CDATA[Suche nach Autor]]></item>
+ <item name="wcf.search.extended"><![CDATA[Erweiterte Suche]]></item>
+ <item name="wcf.search.general"><![CDATA[Allgemeine Suchangaben]]></item>
+ <item name="wcf.search.matchExactly"><![CDATA[Exakter Treffer]]></item>
+ <item name="wcf.search.period"><![CDATA[Im Zeitraum]]></item>
+ <item name="wcf.search.query"><![CDATA[Suche nach Begriff]]></item>
+ <item name="wcf.search.query.description"><![CDATA[Geben Sie einen oder mehrere Begriffe ein. Ein Begriff muss mindestens vier Zeichen lang sein.]]></item>
+ <item name="wcf.search.results"><![CDATA[Suchergebnisse]]></item>
+ <item name="wcf.search.results.change"><![CDATA[Suche ändern]]></item>
+ <item name="wcf.search.results.description"><![CDATA[Suchergebnisse {#$startIndex}-{#$endIndex} von insgesamt {#$items}{if $query} für „<strong>{$query}</strong>“{/if}.{if $items == 1000} Es gibt noch weitere Suchergebnisse, bitte verfeinern Sie Ihre Suche.{/if}{if $highlight}<br />Diese Suchbegriffe wurden hervorgehoben: <span class="highlight">{$highlight}</span>{/if}]]></item>
+ <item name="wcf.search.sortBy"><![CDATA[Sortierung]]></item>
+ <item name="wcf.search.sortBy.relevance"><![CDATA[Relevanz]]></item>
+ <item name="wcf.search.sortBy.time"><![CDATA[Datum]]></item>
+ <item name="wcf.search.sortBy.username"><![CDATA[Autor]]></item>
+ <item name="wcf.search.subjectOnly"><![CDATA[Nur Betreff durchsuchen]]></item>
+ <item name="wcf.search.title"><![CDATA[Suche]]></item>
+ <item name="wcf.search.type"><![CDATA[Suche in]]></item>
+ <item name="wcf.search.error.noMatches"><![CDATA[Es wurde kein{if !$query|empty} mit Ihrer Suchanfrage „{$query}“ übereinstimmender{/if} Eintrag gefunden.]]></item>
+ <item name="wcf.search.error.user.noMatches"><![CDATA[Es wurde kein Eintrag von diesem Autor gefunden.]]></item>
+ </category>
+
<category name="wcf.style">
<item name="wcf.style.changeStyle"><![CDATA[Stil ändern]]></item>
<item name="wcf.style.colorPicker"><![CDATA[Farbwähler]]></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>
+ <item name="wcf.acp.option.category.message.search"><![CDATA[Search]]></item>
+ <item name="wcf.acp.option.search_results_per_page"><![CDATA[Results per Page]]></item>
+ <item name="wcf.acp.option.search_default_sort_field"><![CDATA[Default Sort Field]]></item>
+ <item name="wcf.acp.option.search_default_sort_order"><![CDATA[Default Sort Order]]></item>
+ <item name="wcf.acp.option.search_use_captcha"><![CDATA[Enable reCAPTCHA protection for search]]></item>
</category>
<category name="wcf.acp.package">
<item name="wcf.recaptcha.error.recaptchaString.false"><![CDATA[You have entered the security code incorrectly. Please try again.]]></item>
</category>
+ <category name="wcf.search">
+ <item name="wcf.search.author"><![CDATA[Search By Author]]></item>
+ <item name="wcf.search.extended"><![CDATA[More Options]]></item>
+ <item name="wcf.search.general"><![CDATA[General Search Terms]]></item>
+ <item name="wcf.search.matchExactly"><![CDATA[Exact match]]></item>
+ <item name="wcf.search.period"><![CDATA[Timeframe]]></item>
+ <item name="wcf.search.query"><![CDATA[Search for Term]]></item>
+ <item name="wcf.search.query.description"><![CDATA[Type in one or more search terms, each must be at least 4 characters.]]></item>
+ <item name="wcf.search.results"><![CDATA[Search Results]]></item>
+ <item name="wcf.search.results.change"><![CDATA[Change Search]]></item>
+ <item name="wcf.search.results.description"><![CDATA[Search results {#$startIndex}-{#$endIndex} of {#$items}{if $query} for “<strong>{$query}</strong>”{/if}.{if $items == 1000} There are more results available, please enhance your search parameters.{/if}{if $highlight}<br />These terms were highlighted: <span class="highlight">{$highlight}</span>{/if}]]></item>
+ <item name="wcf.search.sortBy"><![CDATA[Sort By]]></item>
+ <item name="wcf.search.sortBy.relevance"><![CDATA[Relevance]]></item>
+ <item name="wcf.search.sortBy.time"><![CDATA[Time]]></item>
+ <item name="wcf.search.sortBy.username"><![CDATA[Author]]></item>
+ <item name="wcf.search.subjectOnly"><![CDATA[Search subject only]]></item>
+ <item name="wcf.search.title"><![CDATA[Search]]></item>
+ <item name="wcf.search.type"><![CDATA[Search in]]></item>
+ <item name="wcf.search.error.noMatches"><![CDATA[No items matched your search{if !$query|empty} terms: “{$query}”{/if}.]]></item>
+ <item name="wcf.search.error.user.noMatches"><![CDATA[There were no items matching this author.]]></item>
+ </category>
+
<category name="wcf.style">
<item name="wcf.style.changeStyle"><![CDATA[Change Style]]></item>
<item name="wcf.style.colorPicker"><![CDATA[Color Picker]]></item>
KEY searchHash (searchHash)
);
+DROP TABLE IF EXISTS wcf1_search_index;
+CREATE TABLE wcf1_search_index (
+ objectTypeID INT(10) NOT NULL,
+ objectID INT(10) NOT NULL,
+ subject VARCHAR(255) NOT NULL DEFAULT '',
+ message MEDIUMTEXT,
+ metaData MEDIUMTEXT,
+ time INT(10) NOT NULL DEFAULT 0,
+ userID INT(10),
+ username VARCHAR(255) NOT NULL DEFAULT '',
+ languageID INT(10),
+ UNIQUE KEY (objectTypeID, objectID),
+ FULLTEXT INDEX fulltextIndex (subject, message, metaData),
+ FULLTEXT INDEX fulltextIndexSubjectOnly (subject),
+ KEY (userID, objectTypeID, time)
+);
+
+DROP TABLE IF EXISTS wcf1_search_keyword;
+CREATE TABLE wcf1_search_keyword (
+ keywordID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ keyword VARCHAR(255) NOT NULL,
+ searches INT(10) NOT NULL DEFAULT 0,
+ lastSearchTime INT(10) NOT NULL DEFAULT 0,
+ UNIQUE KEY (keyword),
+ KEY (searches, lastSearchTime)
+);
+
DROP TABLE IF EXISTS wcf1_session;
CREATE TABLE wcf1_session (
sessionID CHAR(40) NOT NULL PRIMARY KEY,
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;
+ALTER TABLE wcf1_search_index ADD FOREIGN KEY (objectTypeID) REFERENCES wcf1_object_type (objectTypeID) ON DELETE CASCADE;
+ALTER TABLE wcf1_search_index ADD FOREIGN KEY (languageID) REFERENCES wcf1_language (languageID) ON DELETE SET NULL;
/* default inserts */
-- default user groups