Commit 3f119a48 authored by Carsten  Rose's avatar Carsten Rose
Browse files

typeAhead: second implementation of typeAhead - user can customize LDAP queries.

TypeAhead.php: Reformat of various constants. Hard coded LDAP query replaced by one submitted via SIP.
KeyValueStringParser.php: Implemented explodeWrapped() - behaves like 'explode()' but will respect strings wrapped in quotes as no explodeable.
Ldap.php: New class to manage LDAP access.
AbstractBuildForm.php: Add attributes DATA_TYPEAHEAD_LIMIT, DATA_TYPEAHEAD_MINLENGTH. Parsing of FormElement.parameter.typeahead* parameter.
parent be7d22aa
......@@ -727,10 +727,13 @@ abstract class AbstractBuildForm {
$attribute = '';
$class = 'form-control';
if (isset($formElement[FE_TYPE_AHEAD_SQL])) {
$class .= ' qfq-type-ahead';
$dataSip = $this->sip->queryStringToSip(TYPE_AHEAD_SELECT . '=' . $formElement[FE_TYPE_AHEAD_SQL], RETURN_SIP);
$attribute .= Support::doAttribute(DATA_SIP, $dataSip);
$typeAheadUrlParam = $this->typeAheadBuildParam($formElement);
if ($typeAheadUrlParam != '') {
$class .= ' ' . CLASS_TYPEAHEAD;
$dataSip = $this->sip->queryStringToSip($typeAheadUrlParam, RETURN_SIP);
$attribute .= Support::doAttribute(DATA_TYPEAHEAD_SIP, $dataSip);
$attribute .= Support::doAttribute(DATA_TYPEAHEAD_LIMIT, $formElement[FE_TYPEAHEAD_LIMIT]);
$attribute .= Support::doAttribute(DATA_TYPEAHEAD_MINLENGTH, $formElement[FE_TYPEAHEAD_MINLENGTH]);
}
$attribute .= Support::doAttribute('id', $formElement[FE_HTML_ID]);
......@@ -781,6 +784,47 @@ abstract class AbstractBuildForm {
}
/**
* Check $formElement for FE_TYPE_AHEAD_SQL or FE_TYPE_AHEAD_LDAP_SERVER.
* If one of them is given: fill $urlParam.
*
* @param array $formElement
* @return string
*/
private function typeAheadBuildParam(array $formElement) {
$urlParam = '';
if (isset($formElement[FE_TYPEAHEAD_SQL])) {
$urlParam = FE_TYPEAHEAD_SQL . '=' . $formElement[FE_TYPEAHEAD_SQL];
} elseif (isset($formElement[FE_TYPEAHEAD_LDAP_SERVER])) {
$formElement[FE_TYPEAHEAD_LDAP_SERVER] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LDAP_SERVER);
$formElement[FE_TYPEAHEAD_LDAP_BASE_DN] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LDAP_BASE_DN);
$formElement[FE_TYPEAHEAD_LDAP_SEARCH] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LDAP_SEARCH);
$formElement[FE_TYPEAHEAD_LDAP_VALUE_PRINTF] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LDAP_VALUE_PRINTF);
$formElement[FE_TYPEAHEAD_LDAP_KEY_PRINTF] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LDAP_KEY_PRINTF);
$formElement[FE_TYPEAHEAD_LIMIT] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LIMIT, TYPEAHEAD_DEFAULT_LIMIT);
$arr = [
FE_TYPEAHEAD_LDAP_SERVER => $formElement[FE_TYPEAHEAD_LDAP_SERVER],
FE_TYPEAHEAD_LDAP_BASE_DN => $formElement[FE_TYPEAHEAD_LDAP_BASE_DN],
FE_TYPEAHEAD_LDAP_SEARCH => $formElement[FE_TYPEAHEAD_LDAP_SEARCH],
FE_TYPEAHEAD_LDAP_VALUE_PRINTF => $formElement[FE_TYPEAHEAD_LDAP_VALUE_PRINTF],
FE_TYPEAHEAD_LDAP_KEY_PRINTF => $formElement[FE_TYPEAHEAD_LDAP_KEY_PRINTF],
FE_TYPEAHEAD_LIMIT => $formElement[FE_TYPEAHEAD_LIMIT],
];
$urlParam = OnArray::toString($arr);
}
if ($urlParam != '') {
$formElement[FE_TYPEAHEAD_LIMIT] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_LIMIT, TYPEAHEAD_DEFAULT_LIMIT);
$formElement[FE_TYPEAHEAD_MINLENGTH] = Support::setIfNotSet($formElement, FE_TYPEAHEAD_MINLENGTH, 2);
}
return $urlParam;
}
/**
* Calculates the maxlength of an input field, based on formElement type, formElement user definition and table.field definition.
*
......
......@@ -162,6 +162,7 @@ const ERROR_OVERWRITE_RECORD_ID = 1073;
const ERROR_MISSING_SLAVE_ID_DEFINITION = 1074;
const ERROR_MISSING_INTL = 1075;
const ERROR_HTML_TOKEN_TOO_SHORT = 1076;
const ERROR_MISSING_PRINTF_ARGUMENTS = 1077;
// Subrecord
const ERROR_SUBRECORD_MISSING_COLUMN_ID = 1100;
......@@ -198,6 +199,8 @@ const ERROR_UPLOAD = 1500;
const ERROR_UNKNOWN_ACTION = 1502;
const ERROR_NO_TARGET_PATH_FILE_NAME = 1503;
const ERROR_LDAP_CONNECT = 1600;
// KeyValueParser
const ERROR_KVP_VALUE_HAS_NO_KEY = 1900;
......@@ -374,15 +377,17 @@ const VAR_SLAVE_ID = ACTION_KEYWORD_SLAVE_ID;
const VAR_FILENAME = 'filename'; // Original filename of an uploaded file.
// Class DB can operate in these modes
// PHP class DB can operate in these modes
const MODE_DB_REGULAR = 'regular';
const MODE_DB_NO_LOG = 'noLog';
// CLASS TypeAhead
const TYPE_AHEAD_API_QUERY = 'query'; // Name of parameter in API call of typeahead.php?query=...&s=...
const TYPE_AHEAD_API_SIP = 'sip'; // Name of parameter in API call of typeahead.php?query=...&s=...
const TYPE_AHEAD_SELECT = 'select'; // Value of FE_TYPE_AHEAD_SQL, stored in a SIP under the name TYPE_AHEAD_SELECT.
const TYPE_AHEAD_LDAP = 'ldap'; // Value of FE_TYPE_AHEAD_LDAP, stored in a SIP under the name TYPE_AHEAD_LDAP.
// PHPO class Typeahead
const TYPEAHEAD_API_QUERY = 'query'; // Name of parameter in API call of typeahead.php?query=...&s=... - See also FE_TYPE_AHEAD_SQL
const TYPEAHEAD_API_SIP = 'sip'; // Name of parameter in API call of typeahead.php?query=...&s=...
const TYPEAHEAD_DEFAULT_LIMIT = 20;
const SINGLE_TICK = "'";
const DOUBLE_TICK = '"';
// TOKEN evaluate
const TOKEN_ESCAPE_SINGLE_TICK = 's';
......@@ -440,18 +445,25 @@ const API_JSON_HIDDEN = 'hidden';
const API_JSON_DISABLED = 'disabled';
const API_JSON_REQUIRED = 'required';
const DATA_HIDDEN = 'data-hidden';
const DATA_DISABLED = 'data-disabled';
const DATA_REQUIRED = 'data-required';
const DATA_SIP = 'data-sip'; // Used for typeAhead
const API_ANSWER_STATUS_SUCCESS = 'success';
const API_ANSWER_STATUS_ERROR = 'error';
const API_ANSWER_REDIRECT_CLIENT = 'client';
const API_ANSWER_REDIRECT_NO = 'no';
const API_ANSWER_REDIRECT_URL = 'url';
const API_TYPEAHEAD_KEY = 'key';
const API_TYPEAHEAD_VALUE = 'value';
const DATA_HIDDEN = 'data-hidden';
const DATA_DISABLED = 'data-disabled';
const DATA_REQUIRED = 'data-required';
const DATA_TYPEAHEAD_SIP = 'data-typeahead-sip'; // Used for typeAhead
const DATA_TYPEAHEAD_LIMIT = 'data-typeahead-limit';
const DATA_TYPEAHEAD_MINLENGTH = 'data-typeahead-minlength';
const CLASS_TYPEAHEAD = 'qfq-typeahead';
// BuildForm
const SYMBOL_NEW = 'new';
const SYMBOL_EDIT = 'edit';
......@@ -591,12 +603,18 @@ const FE_TEMPLATE_GROUP_CLASS = 'tgClass';
const FE_TEMPLATE_GROUP_DEFAULT_MAX_LENGTH = 5;
const FE_TEMPLATE_GROUP_NAME_PATTERN = '%d';
const FE_BUTTON_CLASS = 'buttonClass';
const FE_TYPE_AHEAD_SQL = 'typeAheadSql';
const FE_TYPEAHEAD_LIMIT = 'typeaheadLimit';
const FE_TYPEAHEAD_MINLENGTH = 'typeaheadMinLength';
const FE_TYPEAHEAD_SQL = 'typeAheadSql';
const FE_TYPEAHEAD_LDAP_SERVER = 'typeAheadLdapServer';
const FE_TYPEAHEAD_LDAP_BASE_DN = 'typeAheadLdapBaseDn';
const FE_TYPEAHEAD_LDAP_SEARCH = 'typeAheadLdapSearch';
const FE_TYPEAHEAD_LDAP_VALUE_PRINTF = 'typeAheadLdapValuePrintf';
const FE_TYPEAHEAD_LDAP_KEY_PRINTF = 'typeAheadLdapKeyPrintf';
const RETYPE_FE_NAME_EXTENSION = 'RETYPE';
const FE_HTML_ID = 'htmlId'; // Will be dynamically computed during runtime.
// FormElement Types
const FE_TYPE_UPLOAD = 'upload';
const FE_TYPE_EXTRA = 'extra';
......
......@@ -8,12 +8,15 @@
namespace qfq;
use TYPO3\CMS\Core\FormProtection\Exception;
require_once(__DIR__ . '/../store/Sip.php');
//require_once(__DIR__ . '/store/FillStoreForm.php');
require_once(__DIR__ . '/../store/Session.php');
require_once(__DIR__ . '/../Constants.php');
//require_once(__DIR__ . '/Save.php');
//require_once(__DIR__ . '/helper/KeyValueStringParser.php');
require_once(__DIR__ . '/../helper/Ldap.php');
//require_once(__DIR__ . '/helper/HelperFormElement.php');
//require_once(__DIR__ . '/exceptions/UserFormException.php');
//require_once(__DIR__ . '/exceptions/CodeException.php');
......@@ -47,12 +50,12 @@ class TypeAhead {
*/
public function __construct($phpUnit = false) {
if (!isset($_GET[TYPE_AHEAD_API_QUERY]) || !isset($_GET[TYPE_AHEAD_API_SIP])) {
throw new CodeException('Missing GET parameter "' . TYPE_AHEAD_API_SIP . '" or "' . TYPE_AHEAD_API_QUERY . '"');
if (!isset($_GET[TYPEAHEAD_API_QUERY]) || !isset($_GET[TYPEAHEAD_API_SIP])) {
throw new CodeException('Missing GET parameter "' . TYPEAHEAD_API_SIP . '" or "' . TYPEAHEAD_API_QUERY . '"');
}
$this->vars[TYPE_AHEAD_API_QUERY] = $_GET[TYPE_AHEAD_API_QUERY];
$this->vars[TYPE_AHEAD_API_SIP] = $_GET[TYPE_AHEAD_API_SIP];
$this->vars[TYPEAHEAD_API_QUERY] = $_GET[TYPEAHEAD_API_QUERY];
$this->vars[TYPEAHEAD_API_SIP] = $_GET[TYPEAHEAD_API_SIP];
$session = Session::getInstance($phpUnit);
......@@ -67,25 +70,41 @@ class TypeAhead {
* @throws UserFormException
*/
public function process() {
//test
$arr = array();
$values = array();
$sipClass = new Sip();
$sipVars = $sipClass->getVarsFromSip($this->vars[TYPE_AHEAD_API_SIP]);
$sipVars = $sipClass->getVarsFromSip($this->vars[TYPEAHEAD_API_SIP]);
$query = '%' . $this->vars[TYPE_AHEAD_API_QUERY] . '%';
$cnt = substr_count($sipVars[TYPE_AHEAD_SELECT], '?');
for ($ii = 0; $ii < $cnt; $ii++) {
$values[] = $query;
if (isset($sipVars[FE_TYPEAHEAD_SQL])) {
$arr = typeAheadSql($sipVars, $this->vars[TYPEAHEAD_API_QUERY]);
} elseif (isset($sipVars[FE_TYPEAHEAD_LDAP_SERVER])) {
$ldap = new Ldap();
$arr = $ldap->process($sipVars, $this->vars[TYPEAHEAD_API_QUERY]);
}
if (isset($sipVars[TYPE_AHEAD_SELECT])) {
$arr = $this->db->sql($sipVars[TYPE_AHEAD_SELECT], ROW_REGULAR, $values);
return $arr;
}
/**
* @param array $config
* @param $query
* @return array|int
* @throws CodeException
* @throws DbException
*/
private function typeAheadSql(array $config, $query) {
$values = array();
$query = '%' . $query . '%';
$cnt = substr_count($config[FE_TYPEAHEAD_SQL], '?');
for ($ii = 0; $ii < $cnt; $ii++) {
$values[] = $query;
}
return $arr;
return $this->db->sql($config[FE_TYPEAHEAD_SQL], ROW_REGULAR, $values);
}
}
\ No newline at end of file
......@@ -147,7 +147,89 @@ class KeyValueStringParser {
if (in_array($string[0], $quotes) === true && self::isFirstAndLastCharacterIdentical($string)) {
return substr($string, 1, strlen($string) - 2);
}
return $string;
}
/**
* Works like PHP 'explode()', but respects $delimeter wrapped in ticks (single or double): those are not interpreted as delimiter.
*
* E.g.: "a,b,'c,d',e" with delimiter ',' will result in [ 'a', 'b', 'c,d', 'e' ]
*
* @param $delimeter
* @param $str
* @param int $limit
* @return array|bool
* @throws CodeException
*/
public static function explodeWrapped($delimeter, $str, $limit=PHP_INT_MAX ) {
if($delimeter=='') {
return false;
}
if($limit<0) {
throw new CodeException("Not Implemented: limit<0", ERROR_NOT_IMPLEMENTED);
}
if($limit==0) {
$limit = 1;
}
$final = array();
$startToken='';
$onHold = '';
$cnt = 0;
$arr = explode($delimeter, $str, PHP_INT_MAX);
foreach($arr as $value) {
$trimmed=trim($value);
if($value=='' && $startToken=='') {
if($cnt<$limit) {
$final[] = '';
$cnt++;
}
continue;
}
if($startToken=='') {
switch ($trimmed[0]) {
case SINGLE_TICK:
case DOUBLE_TICK:
if($trimmed[0] == substr($trimmed, -1)) {
break; // In case start and end token is in one exploded item
}
$startToken = $trimmed[0];
$onHold = $value;
continue 2;
default:
break;
}
if($cnt>=$limit) {
$final[$cnt-1] .= $delimeter . $value;
} else {
$final[] = $value;
$cnt++;
}
continue;
} else {
$onHold .= $delimeter . $value;
$lastChar = substr($trimmed,-1);
if($startToken == $lastChar) {
if($cnt>=$limit) {
$final[$cnt-1] .= $delimeter . $onHold;
} else {
$final[] = $onHold;
$cnt++;
}
$startToken = '';
$onHold = '';
}
}
}
return $final;
}
}
<?php
/**
* Created by PhpStorm.
* User: crose
* Date: 3/14/17
* Time: 11:32 PM
*/
namespace qfq;
use qfq;
require_once(__DIR__ . '/KeyValueStringParser.php');
class Ldap {
/**
* @param $query
* @return array
* @throws UserFormException
*/
public function process($config, $query) {
$arr = array();
$ldapServer = $config[FE_TYPEAHEAD_LDAP_SERVER];
$ldapBaseDn = $config[FE_TYPEAHEAD_LDAP_BASE_DN];
$ldapSearch = $config[FE_TYPEAHEAD_LDAP_SEARCH];
$ldapSearch = str_replace('?', $query, $ldapSearch);
$ldapLimit = $config[FE_TYPEAHEAD_LIMIT];
$ds = ldap_connect($ldapServer); // must be a valid LDAP server!
if (!$ds) {
throw new UserFormException("Unable to connect to LDAP server: $ldapServer", ERROR_LDAP_CONNECT);
}
$sr = ldap_search($ds, $ldapBaseDn, $ldapSearch, null, null, $ldapLimit);
$info = ldap_get_entries($ds, $sr);
for ($i = 0; $i < $info["count"]; $i++) {
$key = $this->printfResult($config[FE_TYPEAHEAD_LDAP_KEY_PRINTF], $info[$i]);
$value = $this->printfResult($config[FE_TYPEAHEAD_LDAP_VALUE_PRINTF], $info[$i]);
$arr[] = [API_TYPEAHEAD_KEY => $key, API_TYPEAHEAD_VALUE => $value];
}
ldap_close($ds);
return $arr;
}
/**
* Collects values from $inforElelement and sprintf them as described in $format.
*
* @param $format
* @param $infoElement
* @return string output of sprintf
* @throws CodeException
* @throws UserFormException
*/
private function printfResult($format, $infoElement) {
// $format: "'%s / %s / %s', cn, mail, telephonenumber".
// keyArr[0]: printf format string
// keyArr[1..x]: columnnames
$keyArr = KeyValueStringParser::explodeWrapped(',', $format);
if (count($keyArr) <= 2) {
throw new UserFormException("Expect a sprintf compatible format string with a least one argument. Got: '" . $format . "'", ERROR_MISSING_PRINTF_ARGUMENTS);
}
$args = array();
$args[] = $keyArr[0];
$keyArr = array_shift($keyArr);
foreach ($keyArr as $arg) {
$keyName = trim($arg);
$args[] = $infoElement[$keyName][0];
}
// if(isset($info[$i]["mail"][0])) {
// $key = $info[$i]["mail"][0];
// if($key===false || $key===null || $key=='') {
// continue;
// }
// $arr[] = [ 'key' => $info[$i]["mail"][0], 'value' => $info[$i]["cn"][0] . ' / ' . $info[$i]["mail"][0] . ' / ' . $info[$i]["telephonenumber"][0]];
// }
return call_user_func_array('sprintf', $args);
}
}
\ No newline at end of file
......@@ -29,6 +29,7 @@ class OnArray {
* @param array $dataArray
* @param string $keyValueGlue
* @param string $rowGlue
* @param string $encloseValue - char (or string) to enclose the value with.
* @return string
*/
public static function toString(array $dataArray, $keyValueGlue = '=', $rowGlue = '&', $encloseValue = '') {
......
......@@ -625,6 +625,9 @@ class Support {
}
/**
* Looks in $arr if there is an element $index. If not, set it to $value.
* If $overwriteThis!=false, replace the the original value with $value, if $arr[$index]==$overwriteThis.
*
* @param array $arr
* @param string $index
* @param string $value
......@@ -692,6 +695,8 @@ class Support {
}
/**
* Convert 'false' and 'empty' to '0'.
*
* @param $val
* @return string
*/
......
......@@ -169,4 +169,85 @@ class KeyValueStringParserTest extends \PHPUnit_Framework_TestCase {
$this->assertEquals($expected, $actual);
$this->assertCount(2, $actual);
}
public function testExplodeContent() {
$actual = keyValueStringParser::explodeWrapped('', '');
$this->assertEquals(false, $actual);
$actual = keyValueStringParser::explodeWrapped(':', '');
$this->assertEquals([''], $actual);
$actual = keyValueStringParser::explodeWrapped(':', 'a,b,c');
$this->assertEquals(['a,b,c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', 'a,b,c');
$this->assertEquals(['a' , 'b' ,'c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', ' a,b,c');
$this->assertEquals([' a' , 'b' ,'c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', 'a ,b,c');
$this->assertEquals(['a ' , 'b' ,'c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', ' a ,b,c');
$this->assertEquals([' a ' , 'b' ,'c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', 'a, b,c');
$this->assertEquals(['a' , ' b' ,'c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', 'a,b ,c');
$this->assertEquals(['a' , 'b ' ,'c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', 'a, b ,c');
$this->assertEquals(['a' , ' b ' ,'c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', 'a,b, c');
$this->assertEquals(['a' , 'b' ,' c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', 'a,b,c ');
$this->assertEquals(['a' , 'b' ,'c '], $actual);
$actual = keyValueStringParser::explodeWrapped(',', 'a,b, c ');
$this->assertEquals(['a' , 'b' ,' c '], $actual);
$actual = keyValueStringParser::explodeWrapped(',', 'a,"b",c');
$this->assertEquals(['a' , '"b"','c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', 'a,"b,b",c');
$this->assertEquals(['a' , '"b,b"','c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', "a,'b',c");
$this->assertEquals(['a' , "'b'",'c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', "a,'b,b',c");
$this->assertEquals(['a' , "'b,b'",'c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', "a,'b,b,b',c");
$this->assertEquals(['a' , "'b,b,b'",'c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', "'a,a,a,a','b','c,c,c,c,c'");
$this->assertEquals(["'a,a,a,a'" , "'b'","'c,c,c,c,c'"], $actual);
$actual = keyValueStringParser::explodeWrapped(',', " 'a,a,a' , 'b' , 'c,c' ");
$this->assertEquals([" 'a,a,a' " , " 'b' "," 'c,c' "], $actual);
$actual = keyValueStringParser::explodeWrapped(',', 'a,b,c', 2);
$this->assertEquals(['a' , 'b,c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', "'a,a',b,c", 2);
$this->assertEquals(["'a,a'" , 'b,c'], $actual);
$actual = keyValueStringParser::explodeWrapped(',', "a,'b,b',c", 2);
$this->assertEquals(['a' , "'b,b',c" ], $actual);
$actual = keyValueStringParser::explodeWrapped(',', "a,b,'c,c'", 2);
$this->assertEquals(['a' , "b,'c,c'" ], $actual);
$actual = keyValueStringParser::explodeWrapped(',', "a,b,c", 0);
$this->assertEquals(['a,b,c' ], $actual);
$actual = keyValueStringParser::explodeWrapped(',', "a,b,c", 1);
$this->assertEquals(['a,b,c' ], $actual);
$actual = keyValueStringParser::explodeWrapped(',', "'a,b',c", 1);
$this->assertEquals(["'a,b',c" ], $actual);
$actual = keyValueStringParser::explodeWrapped(',', "a,'b,c'", 1);
$this->assertEquals(["a,'b,c'" ], $actual);
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment