Fix Bug #985487: Auto-suggestion for the search interface

This commit is contained in:
Sebastian Meyer 2012-08-03 14:26:38 +02:00
commit 91a7039392
10 changed files with 349 additions and 22 deletions

View File

@ -45,6 +45,24 @@ class tx_dlf_helper {
*/
public static $extKey = 'dlf';
/**
* The encryption key
* @see encrypt() / decrypt()
*
* @var string
* @access private
*/
private static $ENCRYPTION_KEY = 'b8b311560d3e6f8dea0aa445995b1b2b';
/**
* The initialization vector for encryption
* @see encrypt() / decrypt()
*
* @var string
* @access private
*/
private static $ENCRYPTION_IV = '381416de30a5c970f8f486aa6d5cc932';
/**
* Searches the array recursively for a given value and returns the corresponding key if successful
* @see http://php.net/array_search
@ -195,6 +213,87 @@ class tx_dlf_helper {
}
/**
* Decrypt encrypted value with given control hash
* @see http://yavkata.co.uk/weblog/php/securing-html-hidden-input-fields-using-encryption-and-hashing/
*
* @access public
*
* @param string $encrypted: The encrypted value to decrypt
* @param string $hash: The control hash for decrypting
*
* @return mixed The decrypted value or NULL on error
*/
public static function decrypt($encrypted, $hash) {
$decrypted = NULL;
// Check for PHP extension "mcrypt".
if (!extension_loaded('mcrypt')) {
trigger_error('PHP extension "mcrypt" not available', E_USER_WARNING);
return NULL;
}
if (empty($encrypted) || empty($hash)) {
return NULL;
}
$iv = substr(self::$ENCRYPTION_IV, 0, mcrypt_get_iv_size(MCRYPT_BLOWFISH, MCRYPT_MODE_CFB));
$decrypted = mcrypt_decrypt(MCRYPT_BLOWFISH, self::$ENCRYPTION_KEY, base64_decode($encrypted), MCRYPT_MODE_CFB, $iv);
$salt = substr($hash, 0, 10);
$hashed = $salt.substr(sha1($salt.$decrypted), -10);
if ($hashed !== $hash) {
return NULL;
}
return $decrypted;
}
/**
* Encrypt the given string
* @see http://yavkata.co.uk/weblog/php/securing-html-hidden-input-fields-using-encryption-and-hashing/
*
* @access public
*
* @param string $string: The string to encrypt
*
* @return array Array with encrypted string and control hash
*/
public static function encrypt($string) {
// Check for PHP extension "mcrypt".
if (!extension_loaded('mcrypt')) {
trigger_error('PHP extension "mcrypt" not available', E_USER_WARNING);
return NULL;
}
$iv = substr(self::$ENCRYPTION_IV, 0, mcrypt_get_iv_size(MCRYPT_BLOWFISH, MCRYPT_MODE_CFB));
$encrypted = base64_encode(mcrypt_encrypt(MCRYPT_BLOWFISH, self::$ENCRYPTION_KEY, $string, MCRYPT_MODE_CFB, $iv));
$salt = substr(md5(uniqid(rand(), TRUE)), 0, 10);
$hash = $salt.substr(sha1($salt.$string), -10);
return array ('encrypted' => $encrypted, 'hash' => $hash);
}
/**
* Get a backend user object (even in frontend mode)
*
@ -236,28 +335,36 @@ class tx_dlf_helper {
*
* @param integer $uid: The UID of the record
* @param string $table: Get the "index_name" from this table
* @param string $pid: Get the "index_name" from this page
* @param integer $pid: Get the "index_name" from this page
*
* @return string "index_name" for the given UID
*/
public static function getIndexName($uid, $table, $pid) {
public static function getIndexName($uid, $table, $pid = -1) {
$uid = max(intval($uid), 0);
$pid = max(intval($pid), 0);
if (!$uid || !in_array($table, array ('tx_dlf_collections', 'tx_dlf_libraries', 'tx_dlf_metadata', 'tx_dlf_structures', 'tx_dlf_solrcores'))) {
if (!$uid || !$pid || !in_array($table, array ('tx_dlf_collections', 'tx_dlf_libraries', 'tx_dlf_metadata', 'tx_dlf_structures'))) {
trigger_error('At least one argument is not valid: UID='.$uid.' PID='.$pid.' TABLE='.$table, E_USER_WARNING);
trigger_error('At least one argument is not valid: UID='.$uid.' or TABLE='.$table, E_USER_WARNING);
return '';
}
$where = '';
if ($pid !== -1) {
$pid = max(intval($pid), 0);
$where = ' AND '.$table.'.pid='.$pid;
}
$_result = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
$table.'.index_name AS index_name',
$table,
$table.'.uid='.$uid.' AND '.$table.'.pid='.$pid.self::whereClause($table),
$table.'.uid='.$uid.$where.self::whereClause($table),
'',
'',
'1'

View File

@ -21,7 +21,7 @@
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
// TODO: Clean up and reduce code duplication. Consider switching to Solarium.
/**
* [CLASS/FUNCTION INDEX of SCRIPT]
*/
@ -30,6 +30,7 @@
* Solr class 'tx_dlf_solr' for the 'dlf' extension.
*
* @author Sebastian Meyer <sebastian.meyer@slub-dresden.de>
* @author Henrik Lochmann <dev@mentalmotive.com>
* @copyright Copyright (c) 2011, Sebastian Meyer, SLUB Dresden
* @package TYPO3
* @subpackage tx_dlf
@ -45,6 +46,41 @@ class tx_dlf_solr {
*/
public static $extKey = 'dlf';
/**
* Returns the request URL for a specific Solr core
*
* @access public
*
* @param string $core: Name of the core to load
*
* @return string The request URL for a specific Solr core
*/
public static function getSolrUrl($core = '') {
// Extract extension configuration.
$conf = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'][self::$extKey]);
// Derive Solr host name.
$host = ($conf['solrHost'] ? $conf['solrHost'] : 'localhost');
// Prepend username and password to hostname.
if ($conf['solrUser'] && $conf['solrPass']) {
$host = $conf['solrUser'].':'.$conf['solrPass'].'@'.$host;
}
// Set port if not set.
$port = t3lib_div::intInRange($conf['solrPort'], 1, 65535, 8180);
// Append core name to path.
$path = trim($conf['solrPath'], '/').'/'.$core;
// Return entire request URL.
return 'http://'.$host.':'.$port.'/'.$path;
}
/**
* Get SolrPhpClient service object and establish connection to Solr server
* @see EXT:dlf/lib/SolrPhpClient/Apache/Solr/Service.php
@ -82,20 +118,9 @@ class tx_dlf_solr {
// Get core name if UID is given.
if (t3lib_div::testInt($core)) {
$result = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
'tx_dlf_solrcores.index_name AS index_name',
'tx_dlf_solrcores',
'tx_dlf_solrcores.uid='.intval($core).tx_dlf_helper::whereClause('tx_dlf_solrcores'),
'',
'',
'1'
);
$core = tx_dlf_helper::getIndexName($core, 'tx_dlf_solrcores');
if ($GLOBALS['TYPO3_DB']->sql_num_rows($result)) {
list ($core) = $GLOBALS['TYPO3_DB']->sql_fetch_row($result);
} else {
if (empty($core)) {
trigger_error('Could not find Solr core with UID '.$core, E_USER_NOTICE);

View File

@ -49,6 +49,7 @@ return array (
'tx_dlf_oai' => $extensionPath.'plugins/oai/class.tx_dlf_oai.php',
'tx_dlf_pageview' => $extensionPath.'plugins/pageview/class.tx_dlf_pageview.php',
'tx_dlf_search' => $extensionPath.'plugins/search/class.tx_dlf_search.php',
'tx_dlf_search_suggest' => $extensionPath.'plugins/search/class.tx_dlf_search_suggest.php',
'tx_dlf_statistics' => $extensionPath.'plugins/statistics/class.tx_dlf_statistics.php',
'tx_dlf_toc' => $extensionPath.'plugins/toc/class.tx_dlf_toc.php',
'tx_dlf_toolbox' => $extensionPath.'plugins/toolbox/class.tx_dlf_toolbox.php',

View File

@ -49,6 +49,7 @@ $EM_CONF[$_EXTKEY] = array(
),
'suggests' => array(
'realurl' => '',
't3jquery' => '2.1.2-',
),
),
'_md5_values_when_last_written' => '',

View File

@ -66,4 +66,6 @@ $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['dlf/common/class.tx_dlf_document.php'
// Register command line scripts.
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['cliKeys'][$_EXTKEY] = array ('EXT:'.$_EXTKEY.'/cli/class.tx_dlf_cli.php', '_CLI_dlf');
// Register AJAX eID handlers.
$GLOBALS['TYPO3_CONF_VARS']['FE']['eID_include']['tx_dlf_search_suggest'] = 'EXT:'.$_EXTKEY.'/plugins/search/class.tx_dlf_search_suggest.php';
?>

View File

@ -30,6 +30,7 @@
* Plugin 'DLF: Search' for the 'dlf' extension.
*
* @author Sebastian Meyer <sebastian.meyer@slub-dresden.de>
* @author Henrik Lochmann <dev@mentalmotive.com>
* @copyright Copyright (c) 2011, Sebastian Meyer, SLUB Dresden
* @package TYPO3
* @subpackage tx_dlf
@ -39,6 +40,74 @@ class tx_dlf_search extends tx_dlf_plugin {
public $scriptRelPath = 'plugins/search/class.tx_dlf_search.php';
/**
* Adds the JS files necessary for autocompletion
*
* @access protected
*
* @return boolean TRUE on success or FALSE on error
*/
protected function addAutocompleteJS() {
// Ensure extension "t3jquery" is available.
if (t3lib_extMgm::isLoaded('t3jquery')) {
require_once(t3lib_extMgm::extPath('t3jquery').'class.tx_t3jquery.php');
}
// Is "t3jquery" loaded and the custom library created?
if (T3JQUERY === TRUE) {
tx_t3jquery::addJqJS();
$GLOBALS['TSFE']->additionalHeaderData[$this->prefixId.'_search_suggest'] = ' <script type="text/javascript" src="'.t3lib_extMgm::siteRelPath($this->extKey).'plugins/search/tx_dlf_search_suggest.js"></script>';
return TRUE;
} else {
// No autocompletion available!
return FALSE;
}
}
/**
* Adds the encrypted Solr core name to the search form
*
* @access protected
*
* @param integer $core: UID of the core
*
* @return string HTML input fields with encrypted core name and hash
*/
protected function addEncryptedCoreName($core) {
// Get core name.
$name = tx_dlf_helper::getIndexName($core, 'tx_dlf_solrcores');
// Encrypt core name.
if (!empty($name)) {
$name = tx_dlf_helper::encrypt($name);
}
// Add encrypted fields to search form.
if (is_array($name)) {
return '<input type="hidden" name="'.$this->prefixId.'"[encrypted]" value="'.$name['encrypted'].'" /><input type="hidden" name="'.$this->prefixId.'[hashed]" value="'.$name['hash'].'" />';
} else {
return '';
}
}
/**
* The main method of the PlugIn
*
@ -67,6 +136,9 @@ class tx_dlf_search extends tx_dlf_plugin {
if (empty($this->piVars['query'])) {
// Add javascript for autocompletion if available.
$autocomplete = $this->addAutocompleteJS();
// Load template file.
if (!empty($this->conf['templateFile'])) {
@ -96,8 +168,16 @@ class tx_dlf_search extends tx_dlf_plugin {
'###LABEL_SUBMIT###' => $this->pi_getLL('label.submit'),
'###FIELD_QUERY###' => $this->prefixId.'[query]',
'###QUERY###' => htmlspecialchars($lastQuery),
'###ADDITIONAL_INPUTS###' => '',
);
// Encrypt Solr core name and add as hidden input field to the search form.
if ($autocomplete) {
$markerArray['###ADDITIONAL_INPUTS###'] = $this->addEncryptedCoreName($this->conf['solrcore']);
}
// Display search form.
$content .= $this->cObj->substituteMarkerArray($this->template, $markerArray);

View File

@ -0,0 +1,82 @@
<?php
/***************************************************************
* Copyright notice
*
* (c) 2012 Henrik Lochmann <dev@mentalmotive.com>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
require_once (PATH_tslib.'class.tslib_pibase.php');
/**
* Autocompletion for the search plugin of the 'dlf' extension.
*
* @author Henrik Lochmann <dev@mentalmotive.com>
* @copyright Copyright (c) 2012, Zeutschel GmbH
* @package TYPO3
* @subpackage tx_dlf
* @access public
*/
class tx_dlf_search_suggest extends tx_dlf_plugin {
public $scriptRelPath = 'plugins/search/class.tx_dlf_search_suggest.php';
/**
* The main method of the PlugIn
*
* @access public
*
* @param string $content: The PlugIn content
* @param array $conf: The PlugIn configuration
*
* @return void
*/
public function main($content = '', $conf = array ()) {
if (!empty($this->piVars['encrypted']) && !empty($this->piVars['hashed'])) {
$core = tx_dlf_helper::decrypt($this->piVars['encrypted'], $this->piVars['hashed']);
}
if (!empty($core)) {
$url = trim(tx_dlf_solr::getSolrUrl($core), '/').'/suggest/?q='.t3lib_div::_POST('q');
if ($stream = fopen($url, 'r')) {
$content .= stream_get_contents($stream);
fclose($stream);
}
}
echo $content;
}
}
$cObj = t3lib_div::makeInstance('tx_dlf_search_suggest');
$cObj->main();
?>

View File

@ -0,0 +1,27 @@
$(
function(){
// jQuery autocomplete integration
$(".autocomplete").autocomplete({
source: function(request, response) {
return $.post(
'/',
{
eID: "tx_dlf_search_suggest",
q: escape(request.term),
encrypted: $("input[name='tx_dlf[encrypted]']").val(),
hashed: $("input[name='tx_dlf[hashed]']").val()
},
function(xmlData) {
var result = new array();
$('arr[name="suggestion"] str', xmlData).each(function(i) {
if ($(this).text().indexOf(request.term) == 0) {
result.push($(this).text());
}
});
return response(result);
},
'xml');
}
});
}
);

View File

@ -1,7 +1,8 @@
<!-- ###TEMPLATE### -->
<form class="tx-dlf-search-form" action="###ACTION_URL###" method="post" enctype="multipart/form-data">
<label for="###FIELD_QUERY###">###LABEL_QUERY###</label>
<input type="text" id="###FIELD_QUERY###" name="###FIELD_QUERY###" value="###QUERY###" />
<input type="text" id="###FIELD_QUERY###" name="###FIELD_QUERY###" value="###QUERY###" class="autocomplete" autocomplete="off" role="textbox" aria-autocomplete="list" aria-haspopup="true">
<input type="submit" value="###LABEL_SUBMIT###" />
###ADDITIONAL_INPUTS###
</form>
<!-- ###TEMPLATE### -->

1
dlf/t3jquery.txt Normal file
View File

@ -0,0 +1 @@
components=jQuery,Core,Position,Autocomplete