You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1247 lines
48 KiB

<?php
// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
//
// All Rights Reserved. See copyright.txt for details and a complete list of authors.
// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
// $Id$
function wikiplugin_customsearch_info()
{
return [
'name' => tra('Custom Search'),
'documentation' => 'PluginCustomSearch',
'description' => tra('Create a custom search form for searching or listing items on the site'),
'prefs' => ['wikiplugin_customsearch', 'wikiplugin_list', 'feature_search'],
'body' => tra('LIST plugin configuration information'),
'filter' => 'wikicontent',
'profile_reference' => 'search_plugin_content',
'iconname' => 'search',
'introduced' => 8,
'tags' => ['advanced'],
'params' => [
'wiki' => [
'required' => false,
'name' => tra('Template wiki page'),
'description' => tra('Wiki page where search user interface template is found'),
'since' => '8.0',
'filter' => 'pagename',
'default' => '',
'profile_reference' => 'wiki_page',
],
'tpl' => [
'required' => false,
'name' => tra('Template file'),
'description' => tra('Smarty template (.tpl) file where search user interface template is found'),
'since' => '12.2',
'default' => '',
],
'id' => [
'required' => false,
'name' => tra('Search Id'),
'description' => tra('A unique identifier to distinguish custom searches for storing of previous search
criteria entered by users'),
'since' => '8.0',
'filter' => 'alnum',
'default' => 0,
],
'autosearchdelay' => [
'required' => false,
'name' => tra('Search Delay'),
'description' => tr('Delay in milliseconds before automatically triggering search after change
(%00%1 disables and is the default)', '<code>', '</code>'),
'since' => '8.0',
'filter' => 'digits',
'default' => 0,
],
'searchfadediv' => [
'required' => false,
'name' => tra('Fade DIV Id'),
'description' => tra('The specific ID of the specific div to fade out when AJAX search is in progress,
if not set will attempt to fade the whole area or if failing simply show the spinner'),
'since' => '8.0',
'filter' => 'text',
'default' => '',
],
'recalllastsearch' => [
'required' => false,
'name' => tra('Recall Last Search'),
'description' => tra('In the same session, return users to same search parameters on coming back to the
search page after leaving'),
'since' => '8.0',
'options' => [
['text' => tra(''), 'value' => ''],
['text' => tra('No'), 'value' => '0'],
['text' => tra('Yes'), 'value' => '1'],
],
'filter' => 'digits',
'default' => 0,
],
'callbackscript' => [
'required' => false,
'name' => tra('Custom JavaScript Page'),
'description' => tra('The wiki page on which custom JavaScript is to be executed on return of Ajax results'),
'since' => '8.0',
'filter' => 'pagename',
'default' => '',
],
'destdiv' => [
'required' => false,
'name' => tra('Destination Div'),
'description' => tra('The id of an existing div to contain the search results'),
'since' => '9.0',
'filter' => 'text',
'default' => '',
],
'searchonload' => [
'required' => false,
'name' => tra('Search On Load'),
'description' => tra('Execute the search when the page loads (default: Yes)'),
'since' => '9.0',
'options' => [
['text' => tra(''), 'value' => ''],
['text' => tra('No'), 'value' => '0'],
['text' => tra('Yes'), 'value' => '1'],
],
'filter' => 'digits',
'default' => 1,
],
'requireinput' => [
'required' => false,
'name' => tra('Require Input'),
'description' => tra('Require first input field to be filled for search to trigger'),
'since' => '12.0',
'options' => [
['text' => tra(''), 'value' => ''],
['text' => tra('No'), 'value' => '0'],
['text' => tra('Yes'), 'value' => '1'],
],
'filter' => 'digits',
'default' => 0,
],
'forcesortmode' => [
'required' => false,
'name' => tra('Force Sort'),
'description' => tra('Force the use of specified sort mode in place of search relevance even when there is a text search query'),
'since' => '13.0',
'options' => [
['text' => tra(''), 'value' => ''],
['text' => tra('No'), 'value' => '0'],
['text' => tra('Yes'), 'value' => '1'],
],
'filter' => 'digits',
'default' => 1,
],
'trimlinefeeds' => [
'required' => false,
'name' => tra('Trim Linefeeds'),
'description' => tra('Remove the linefeeds added after each input which casues the wiki parser to add extra paragraphs.'),
'since' => '14.1',
'options' => [
['text' => tra(''), 'value' => ''],
['text' => tra('No'), 'value' => '0'],
['text' => tra('Yes'), 'value' => '1'],
],
'filter' => 'digits',
'default' => 0,
],
'searchable_only' => [
'required' => false,
'name' => tra('Searchable Only Results'),
'description' => tra('Only include results marked as searchable in the index.'),
'since' => '14.1',
'options' => [
['text' => tra(''), 'value' => ''],
['text' => tra('No'), 'value' => '0'],
['text' => tra('Yes'), 'value' => '1'],
],
'filter' => 'digits',
'default' => 1,
],
'customsearchjs' => [
'required' => false,
'name' => tra('Use custom search JavaScript file'),
'description' => tra('Mainly keeps the search state on the URL hash, but also adds some helper functions like easier sorting and page size.'),
'since' => '14.1',
'options' => [
['text' => tra(''), 'value' => ''],
['text' => tra('No'), 'value' => '0'],
['text' => tra('Yes'), 'value' => '1'],
],
'filter' => 'digits',
'default' => 0,
],
'noajaxforbots' => [
'required' => false,
'name' => tra('No AJAX for bots'),
'description' => tra('Renders the default search results when being crawled by a bot.'),
'since' => '24.1',
'options' => [
['text' => tra(''), 'value' => ''],
['text' => tra('No'), 'value' => '0'],
['text' => tra('Yes'), 'value' => '1'],
],
'filter' => 'digits',
'default' => 0,
],
],
];
}
function wikiplugin_customsearch($data, $params)
{
global $prefs;
if ($prefs['javascript_enabled'] !== 'y') {
require_once('lib/wiki-plugins/wikiplugin_list.php');
$smarty = TikiLib::lib('smarty');
$smarty->loadPlugin('smarty_block_remarksbox');
$repeat = false;
$out = smarty_block_remarksbox(
[
'type' => 'warning',
'title' => tr('JavaScript disabled'),
],
tr('JavaScript is required for this search feature'),
$smarty,
$repeat
);
return '~np~' . $out . '~/np~' . wikiplugin_list($data, []);
}
static $instance_id = null;
if (empty($params['wiki']) && empty($params['tpl'])) {
$params['tpl'] = 'templates/search_customsearch/default_form.tpl';
} elseif (! empty($params['wiki']) && ! TikiLib::lib('tiki')->page_exists($params['wiki'])) {
$link = new WikiParser_OutputLink();
$link->setIdentifier($params['wiki']);
return tra('Template page not found') . ' ' . $link->getHtml();
}
if (isset($params['id'])) {
$id = TikiLib::remove_non_word_characters_and_accents($params['id']);
} else {
if ($instance_id === null) {
$instance_id = 0;
} else {
$instance_id++;
}
$id = (string) $instance_id;
}
if (isset($params['recalllastsearch']) && $params['recalllastsearch'] == 1 && (! isset($_REQUEST['forgetlastsearch']) || $_REQUEST['forgetlastsearch'] != 'y')) {
$recalllastsearch = 1;
} else {
$recalllastsearch = 0;
}
$defaults = [];
$plugininfo = wikiplugin_customsearch_info();
foreach ($plugininfo['params'] as $key => $param) {
$defaults["$key"] = $param['default'] ?? null;
}
$params = array_merge($defaults, $params);
if (! isset($_REQUEST["offset"])) {
$offset = 0;
} else {
$offset = (int) $_REQUEST["offset"];
}
if (isset($_REQUEST['maxRecords'])) {
$maxRecords = (int) $_REQUEST['maxRecords'];
} elseif ($recalllastsearch && ! empty($_SESSION["customsearch_$id"]['maxRecords'])) {
$maxRecords = (int) $_SESSION["customsearch_$id"]['maxRecords'];
} else {
$maxRecords = (int) $prefs['maxRecords'];
$maxDefault = true;
}
if (! empty($_REQUEST['sort_mode'])) {
$sort_mode = $_REQUEST['sort_mode'];
} elseif ($recalllastsearch && ! empty($_SESSION["customsearch_$id"]['sort_mode'])) {
$sort_mode = $_SESSION["customsearch_$id"]['sort_mode'];
} else {
$sort_mode = '';
}
$definitionKey = md5($data);
$matches = WikiParser_PluginMatcher::match($data);
$query = new Search_Query();
if (! isset($params['searchable_only']) || $params['searchable_only'] == 1) {
$query->filterIdentifier('y', 'searchable');
}
$builder = new Search_Query_WikiBuilder($query);
$builder->apply($matches);
$tsret = $builder->applyTablesorter($matches);
if (! empty($tsret['max']) || ! empty($_GET['numrows'])) {
$max = ! empty($_GET['numrows']) ? $_GET['numrows'] : $tsret['max'];
$builder->wpquery_pagination_max($query, $max);
}
$paginationArguments = $builder->getPaginationArguments();
// Use maxRecords set in LIST parameters rather then global default if set.
if (isset($maxDefault) && $maxDefault) {
if (! empty($paginationArguments['max'])) {
$maxRecords = $paginationArguments['max'];
}
}
// setup AJAX pagination
$paginationArguments['offset_jsvar'] = "customsearch_$id.offset";
$paginationArguments['sort_jsvar'] = "customsearch_$id.sort_mode";
$paginationArguments['_onclick'] = "$('#customsearch_$id').submit();return false;";
$builder = new Search_Formatter_Builder();
$builder->setId('wpcs-' . $id);
$builder->setPaginationArguments($paginationArguments);
$builder->setTsOn($tsret['tsOn']);
$facets = new Search_Query_FacetWikiBuilder();
$facets->apply($matches);
$cachelib = TikiLib::lib('cache');
$cachelib->cacheItem(
$definitionKey,
serialize(
[
'query' => $query,
'data' => $data,
'builder' => $builder,
'facets' => $facets,
'tsret' => $tsret,
]
),
'customsearch'
);
if (! empty($params['wiki'])) {
$wikitpl = "tplwiki:" . $params['wiki'];
} else {
$wikitpl = $params['tpl'];
}
$wikicontent = TikiLib::lib('smarty')->fetch($wikitpl);
TikiLib::lib('parser')->parse_wiki_argvariable($wikicontent);
$matches = WikiParser_PluginMatcher::match($wikicontent);
$fingerprint = md5($wikicontent);
$sessionprint = "customsearch_{$id}_$fingerprint";
if (isset($_SESSION[$sessionprint]) && $_SESSION[$sessionprint] != $fingerprint) {
unset($_SESSION["customsearch_$id"]);
}
$_SESSION[$sessionprint] = $fingerprint;
// important that offset from session is set after fingerprint check otherwise blank page might show
if ($recalllastsearch && ! isset($_REQUEST['offset']) && ! empty($_SESSION["customsearch_$id"]["offset"])) {
$offset = (int) $_SESSION["customsearch_$id"]["offset"];
}
$options = [
'searchfadetext' => tr('Loading...'),
'searchfadediv' => $params['searchfadediv'],
'results' => empty($params['destdiv']) ? "#customsearch_{$id}_results" : "#{$params['destdiv']}",
'autosearchdelay' => ! empty($params['autosearchdelay']) ? max(1500, (int) $params['autosearchdelay']) : 0,
'searchonload' => (int) $params['searchonload'],
'requireinput' => (bool) $params['requireinput'],
'origrequireinput' => (bool) $params['requireinput'],
'forcesortmode' => (bool) $params['forcesortmode'],
];
/**
* NOTES: Search Execution
*
* There is a global delay on execution of 1 second. This makes sure
* multiple submissions will never trigger multiple requests.
*
* There is an additional autosearchdelay configuration that can trigger the search
* on field change rather than explicit request. Explicit requests will still work.
*/
$script = "
var customsearch$id = {
options: " . json_encode($options) . ",
id: " . json_encode($id) . ",
offset: 0,
searchdata: {},
definition: " . json_encode((string) $definitionKey) . ",
autoTimeout: null,
add: function (fieldId, filter) {
this.searchdata[fieldId] = filter;
this.auto();
},
remove: function (fieldId) {
delete this.searchdata[fieldId];
this.auto();
},
load: function () {
this._executor(this);
},
auto: function () {
},
_executor: delayedExecutor(1000, function (cs) {
var selector = '#' + cs.options.searchfadediv;
if (cs.options.searchfadediv.length <= 1 && $(selector).length === 0) {
selector = '#customsearch_' + cs.id;
}
$(selector).tikiModal(cs.options.searchfadetext);
if ($(cs.options.results).length) {
var resultsTop = $(cs.options.results).offset().top;
if( resultsTop && $(window).scrollTop() > resultsTop ) {
$('html, body').animate({scrollTop: resultsTop + 'px'}, 'fast');
}
}
cs._load(function (data) {
$(selector).tikiModal();
$(cs.options.results).html(data);
$(document).trigger('pageSearchReady');
});
cs.store_query = '';
}),
init: function () {
var that = this;
if (that.options.searchonload) {
that.load();
}
if (that.options.autosearchdelay) {
that.auto = delayedExecutor(that.options.autosearchdelay, function () {
if (that.options.requireinput && (!$('#customsearch_$id').find(':text').val() || $('#customsearch_$id').find(':text').val().indexOf('...') > 0)) {
return false;
}
that.load();
});
}
}
};
$('#customsearch_$id').click(function() {
customsearch$id.offset = 0;
});
$('#customsearch_$id').submit(function() {
if (customsearch$id.options.requireinput && (!$(this).find(':text').val() || $(this).find(':text').val().indexOf('...') > 0)) {
alert(tr('Please enter a search query'));
return false;
}
if (customsearch$id.options.origrequireinput != customsearch$id.options.requireinput) {
customsearch$id.options.requireinput = customsearch$id.options.origrequireinput;
}
customsearch$id.load();
return false;
});
window.customsearch_$id = customsearch$id;
";
if (isset($_REQUEST['default']) && is_array($_REQUEST['default'])) {
$defaultRequest = $_REQUEST['default'];
} else {
$defaultRequest = [];
}
$parser = new WikiParser_PluginArgumentParser();
$dr = 0;
$configs = []; // to collect field data for noajaxforbots
foreach ($matches as $match) {
$name = $match->getName();
$arguments = $parser->parse($match->getArguments());
$key = $match->getInitialStart();
$fieldid = "customsearch_{$id}_$key";
if (isset($arguments['id'])) {
$fieldid = $arguments['id'];
}
if ($name == 'sort' && ! empty($arguments['mode']) && empty($sort_mode)) {
$sort_mode = $arguments['mode'];
$match->replaceWith('');
continue;
}
if ($defaultRequest) {
foreach ($defaultRequest as $key => $value) {
if (! empty($arguments['id']) && $key === $arguments['id']) {
$default = $value;
unset($defaultRequest[$key]);
break;
} elseif (! empty($arguments['_field']) && $key === $arguments['_field']) {
$default = $value;
unset($defaultRequest[$key]);
break;
} elseif (empty($arguments['id']) && empty($arguments['_field']) && $key === $arguments['_filter']) {
$default = $value;
unset($defaultRequest[$key]);
break;
} elseif (! empty($arguments['_group']) && $key === $arguments['_group'] && $arguments['type'] === 'radio' && $arguments['_value'] === $value) {
$default = $value;
unset($defaultRequest[$key]);
break;
} elseif (
! empty($arguments['id']) && (
! empty($defaultRequest["{$arguments['id']}_from"]) ||
! empty($defaultRequest["{$arguments['id']}_to"]) ||
! empty($defaultRequest["{$arguments['id']}_gap"])
)
) {
// defaults for date ranges with the id
$default = csGetRangeDefaults($defaultRequest, $arguments['id']);
} elseif (
! empty($arguments['_field']) && (
! empty($defaultRequest["{$arguments['_field']}_from"]) ||
! empty($defaultRequest["{$arguments['_field']}_to"]) ||
! empty($defaultRequest["{$arguments['_field']}_gap"])
)
) {
// defaults for date ranges with the field name
$default = csGetRangeDefaults($defaultRequest, $arguments['_field']);
}
}
} elseif ($recalllastsearch && isset($_SESSION["customsearch_$id"][$fieldid])) {
$default = $_SESSION["customsearch_$id"][$fieldid];
} elseif (! empty($arguments['_default'])) {
if (strpos($arguments['_default'], ',') !== false) {
$default = explode(',', $arguments['_default']);
} else {
$default = $arguments['_default'];
}
} else {
$default = '';
}
if ($name == 'categories') {
$parent = $arguments['_parent'];
if (! empty($_REQUEST['defaultcat'][$parent])) {
$default = $_REQUEST['defaultcat'][$parent];
}
}
$function = "cs_design_{$name}";
if (function_exists($function)) {
if (isset($arguments['_group'])) {
$fieldname = "customsearch_{$id}_gr" . $arguments['_group'];
} elseif (isset($arguments['_textrange'])) {
$fieldname = "customsearch_{$id}_textrange" . $arguments['_textrange'];
} elseif (isset($arguments['_daterange'])) {
$fieldname = "customsearch_{$id}_daterange" . $arguments['_daterange'];
} else {
$fieldname = $fieldid;
}
$html = $function($id, $fieldname, $fieldid, $arguments, $default, $script);
if ($params['trimlinefeeds']) {
$html = trim($html);
}
$match->replaceWith($html);
if (! empty($params['noajaxforbots'])) {
$configs[$fieldname] = [
'config' => $arguments,
'name' => $name,
];
}
}
if ($name == 'daterange') {
$dr++;
}
}
$callbackScript = null;
if (! empty($params['callbackscript']) && TikiLib::lib('tiki')->page_exists($params['callbackscript'])) {
$callbackscript_tpl = "wiki:" . $params['callbackscript'];
$callbackScript = TikiLib::lib('smarty')->fetch($callbackscript_tpl);
}
//get iconset icon if daterange is one of the fields
if ($dr) {
$smarty = TikiLib::lib('smarty');
$smarty->loadPlugin('smarty_function_js_insert_icon');
$iconinsert = smarty_function_js_insert_icon(['type' => 'jscalendar', 'return' => 'y'], $smarty->getEmptyInternalTemplate());
} else {
$iconinsert = '';
}
global $page;
$script .= "$('.icon-pdf').parent().click(function(){storeSortTable('#customsearch_" . $id . "_results',$('#customsearch_" . $id . "_results').html())});
customsearch$id._load = function (receive) {
var datamap = {
definition: this.definition,
adddata: $.toJSON(this.searchdata),
searchid: this.id,
offset: customsearch$id.offset,
maxRecords: this.maxRecords,
store_query: this.store_query,
page: " . json_encode($page) . ",
recalllastsearch: $recalllastsearch
};
if (!customsearch$id.options.forcesortmode && $('#customsearch_$id').find(':text').val() && $('#customsearch_$id').find(':text').val().indexOf('...') <= 0) {
customsearch$id.sort_mode = 'score_desc';
}
if (customsearch$id.sort_mode) {
// blank sort_mode is not allowed by Tiki input filter
datamap.sort_mode = customsearch$id.sort_mode;
}
$.ajax({
type: 'POST',
url: $.service('search_customsearch', 'customsearch'),
data: datamap,
dataType: 'html',
success: function(data) {
receive(data);
$('[data-bs-toggle=\'popover\']').attr('data-html', true);
$('[data-bs-toggle=\'popover\']').popover();
$callbackScript;
},
error: function ( jqXHR, textStatus, errorThrown ) {
var selector = '#' + customsearch$id.options.searchfadediv;
if (customsearch$id.options.searchfadediv.length <= 1 && $(selector).length === 0) {
selector = '#customsearch_$id';
}
$(selector).tikiModal();
$('#customsearch_$id').showError(jqXHR)
}
});
};
customsearch$id.sort_mode = " . json_encode($sort_mode) . ";
customsearch$id.offset = $offset;
customsearch$id.maxRecords = $maxRecords;
customsearch$id.store_query ='';
customsearch$id.init();
$iconinsert;
$(document).trigger('formSearchReady');
";
$out = '<div id="customsearch_' . $id . '_form"><form id="customsearch_' . $id . '" class="customsearch_form">' . $matches->getText() . '</form></div>';
if (empty($params['destdiv'])) {
$out .= '<div id="customsearch_' . $id . '_results" class="customsearch_results"></div>';
}
if (! empty($params['noajaxforbots'])) {
// bot list from https://stackoverflow.com/a/60055115/2459703 (TODO better and refactor?)
if (
isset($_REQUEST['debugbot']) ||
preg_match('/abacho|accona|AddThis|AdsBot|ahoy|AhrefsBot|AISearchBot|alexa|altavista|anthill|appie|applebot|arale|araneo|AraybOt|ariadne|arks|aspseek|ATN_Worldwide|Atomz|baiduspider|baidu|bbot|bingbot|bing|Bjaaland|BlackWidow|BotLink|bot|boxseabot|bspider|calif|CCBot|ChinaClaw|christcrawler|CMC\/0\.01|combine|confuzzledbot|contaxe|CoolBot|cosmos|crawler|crawlpaper|crawl|curl|cusco|cyberspyder|cydralspider|dataprovider|digger|DIIbot|DotBot|downloadexpress|DragonBot|DuckDuckBot|dwcp|EasouSpider|ebiness|ecollector|elfinbot|esculapio|ESI|esther|eStyle|Ezooms|facebookexternalhit|facebook|facebot|fastcrawler|FatBot|FDSE|FELIX IDE|fetch|fido|find|Firefly|fouineur|Freecrawl|froogle|gammaSpider|gazz|gcreep|geona|Getterrobo-Plus|get|girafabot|golem|googlebot|\-google|grabber|GrabNet|griffon|Gromit|gulliver|gulper|hambot|havIndex|hotwired|htdig|HTTrack|ia_archiver|iajabot|IDBot|Informant|InfoSeek|InfoSpiders|INGRID\/0\.1|inktomi|inspectorwww|Internet Cruiser Robot|irobot|Iron33|JBot|jcrawler|Jeeves|jobo|KDD\-Explorer|KIT\-Fireball|ko_yappo_robot|label\-grabber|larbin|legs|libwww-perl|linkedin|Linkidator|linkwalker|Lockon|logo_gif_crawler|Lycos|m2e|majesticsEO|marvin|mattie|mediafox|mediapartners|MerzScope|MindCrawler|MJ12bot|mod_pagespeed|moget|Motor|msnbot|muncher|muninn|MuscatFerret|MwdSearch|NationalDirectory|naverbot|NEC\-MeshExplorer|NetcraftSurveyAgent|NetScoop|NetSeer|newscan\-online|nil|none|Nutch|ObjectsSearch|Occam|openstat.ru\/Bot|packrat|pageboy|ParaSite|patric|pegasus|perlcrawler|phpdig|piltdownman|Pimptrain|pingdom|pinterest|pjspider|PlumtreeWebAccessor|PortalBSpider|psbot|rambler|Raven|RHCS|RixBot|roadrunner|Robbie|robi|RoboCrawl|robofox|Scooter|Scrubby|Search\-AU|searchprocess|search|SemrushBot|Senrigan|seznambot|Shagseeker|sharp\-info\-agent|sift|SimBot|Site Valet|SiteSucker|skymob|SLCrawler\/2\.0|slurp|snooper|solbot|speedy|spider_monkey|SpiderBot\/1\.0|spiderline|spider|suke|tach_bw|TechBOT|TechnoratiSnoop|templeton|teoma|titin|topiclink|twitterbot|twitter|UdmSearch|Ukonline|UnwindFetchor|URL_Spider_SQL|urlck|urlresolver|Valkyrie libwww\-perl|verticrawl|Victoria|void\-bot|Voyager|VWbot_K|wapspider|WebBandit\/1\.0|webcatcher|WebCopier|WebFindBot|WebLeacher|WebMechanic|WebMoose|webquest|webreaper|webspider|webs|WebWalker|WebZip|wget|whowhere|winona|wlm|WOLP|woriobot|WWWC|XGET|xing|yahoo|YandexBot|YandexMobileBot|yandex|yeti|Zeus/i', $_SERVER['HTTP_USER_AGENT'])
) {
$servicelib = TikiLib::lib('service');
// $out contains the html for the form
$doc = new DOMDocument();
$doc->loadHTML("<html lang='en'><body>$out</body></html>");
$adddata = [];
$inputs = $doc->getElementsByTagName('*');
for ($i = $inputs->length; --$i >= 0; ) {
$item = $inputs->item($i);
$value = $item->getAttribute('value');
if (in_array($item->nodeName, ['input', 'select']) && $value) {
$name = $item->getAttribute('name');
// deal with range inputs, e.g. `thing_from` and `thing_to`
$parts = explode('_', $name);
if (count($parts) === 2) {
$name = $parts[0];
if ($configs[$name]['name'] === 'daterange') {
$rangeValue = $adddata[$name]['value'] ?? '';
if ($parts[1] === 'to' && $rangeValue) {
$value .= ',' . $rangeValue;
} else if ($parts[1] === 'from') {
$value = $rangeValue . ',' . $value;
}
}
}
if ($configs[$name]['config']['type'] !== 'submit') {
if (! isset($adddata[$name])) {
$adddata[$name] = $configs[$name];
}
$adddata[$name]['value'] = $value;
}
}
}
$request = array_replace([
'searchid' => $id,
'definition' => $definitionKey,
], $_REQUEST);
$request['adddata'] = json_encode($adddata);
$out .= $servicelib->render('search_customsearch', 'customsearch', $request);
// prevent ajax autoload etc
$script = '';
}
}
TikiLib::lib('header')->add_jq_onready($script);
if ($params['customsearchjs']) {
TikiLib::lib('header')->add_jsfile('lib/jquery_tiki/customsearch.js');
}
if (! empty($params['wiki'])) {
return $out;
} else {
// If using smarty tpl should assume it's all HTML
$out = str_replace('~np~', '', $out);
$out = str_replace('~/np~', '', $out);
return '~np~' . $out . '~/np~';
}
}
/**
* @param array $defaultRequest The default array passed from the URL
* @param string $key Current id or fieldname being processed
*
* @return string[]
*/
function csGetRangeDefaults(array &$defaultRequest, string $key): array
{
$default = [];
foreach (['from', 'to', 'gap'] as $str) {
if (isset($defaultRequest["{$key}_{$str}"])) {
$default[$str] = $defaultRequest["{$key}_{$str}"];
if (is_numeric($default[$str])) {
$default['from'] = strtotime($default[$str]);
}
unset($defaultRequest["{$key}_{$str}"]);
}
}
return $default;
}
function cs_design_setbasic($element, $fieldid, $fieldname, $arguments)
{
$element->setAttribute('id', $fieldid);
$element->setAttribute('name', $fieldname);
foreach ($arguments as $k => $v) {
if (substr($k, 0, 1) != '_') {
$element->setAttribute($k, $v);
}
}
}
function cs_design_input($id, $fieldname, $fieldid, $arguments, $default, &$script)
{
$document = new DOMDocument();
$element = $document->createElement('input');
cs_design_setbasic($element, $fieldid, $fieldname, $arguments);
if (! empty($default)) {
$arguments['default'] = $default;
}
$script .= "
(function (id, config, fieldname) {
var field = $('#' + id);
field.change(function() {
var partial = '';
var value = $(this).val();
if (config['_partial'] == 'y') {
// Partial only makes sense for values not empty and for single words
if (value != '' && value.indexOf(' ') == -1) {
partial = '*';
}
}
var filter = {
config: config,
name: 'input',
value: value + partial
};
if ($(this).is(':checkbox, :radio')) {
filter.value = $(this).is(':checked');
}
if ($(this).is(':radio')) {
$(this).closest('form').find(':radio')
.filter(function () {
return $(this).attr('name') == fieldname
})
.each(function() {
customsearch$id.remove($(this).attr('id'));
});
}
customsearch$id.add($(this).attr('id'), filter);
customsearch$id.offset = 0;
});
if (config['default'] || $(field).attr('type') === 'hidden') {
field.change();
}
})('$fieldid', " . json_encode($arguments) . ", " . json_encode($fieldname) . ");
";
$arguments = new JitFilter($arguments);
$default = $arguments->default->text();
$type = $arguments->type->word();
if ($default && $type != "hidden") {
if ((string) $default != 'n' && $type == 'checkbox') {
$element->setAttribute('checked', 'checked');
} else if ($type == 'radio' && (string) $default === $arguments->_value->text()) {
$element->setAttribute('checked', 'checked');
} else {
$element->setAttribute('value', $default);
}
}
$document->appendChild($element);
return html_entity_decode($document->saveHTML());
}
function cs_design_categories($id, $fieldname, $fieldid, $arguments, $default, &$script)
{
$document = new DOMDocument();
extract($arguments, EXTR_SKIP);
if (! isset($_style)) {
$_style = 'select';
}
if (empty($_group) && ($_style == 'checkbox' || $_style == 'radio')) {
return tr("_group is needed to be set if _style is checkbox or radio");
}
$showSubcategories = isset($_showdeep) && $_showdeep != 'n';
if (isset($_parent) && ctype_digit($_parent) && $_parent > 0) {
$filter = ['identifier' => $_parent, 'type' => $showSubcategories ? 'descendants' : 'children'];
} else {
$filter = ['type' => $showSubcategories ? 'all' : 'roots'];
}
if (! isset($_categpath)) {
$_categpath = false;
} else {
$_categpath = ($_categpath === 'y');
}
$cats = TikiLib::lib('categ')->getCategories($filter);
if ($_style == 'checkbox' || $_style == 'radio') {
$currentlevel = 0;
$orig_fieldid = $fieldid;
foreach ($cats as $c) {
$categId = $c['categId'];
$fieldid = $orig_fieldid . "_cat$categId";
$level = count($c['tepath']);
if ($level > $currentlevel) {
$ul[$level] = $document->createElement('ul');
if ($currentlevel) {
$ul[$currentlevel]->appendChild($ul[$level]);
} else {
$document->appendChild($ul[$level]);
}
$currentlevel = $level;
} elseif ($level < $currentlevel) {
$currentlevel = $level;
}
$li = $document->createElement('li');
$li->setAttribute('class', $_style);
$ul[$currentlevel]->appendChild($li);
$input = $document->createElement('input');
$input->setAttribute('type', $_style);
cs_design_setbasic($input, $fieldid, $fieldname, $arguments);
$input->setAttribute('value', $categId);
$labelElement = $document->createElement('label');
$label = $document->createTextNode($_categpath ? $c['relativePathString'] : $c['name']);
$labelElement->appendChild($input);
$labelElement->appendChild($label);
$li->appendChild($labelElement);
if ($_style == 'radio') {
$radioreset = "$('input[type=radio][name=$fieldname]').each(function() {
customsearch$id.remove($(this).attr('id'));
});"
;
} else {
$radioreset = '';
}
$script .= "
$('#$fieldid').change(function() {
if ($(this).is(':checked')) {
var filter = {
config : " . json_encode($arguments) . ",
name : 'categories',
value : $(this).val()
}
$radioreset
customsearch$id.add('$fieldid', filter);
} else {
customsearch$id.remove('$fieldid', filter);
}
});
";
if ($default && in_array($c['categId'], (array) $default)) {
$input->setAttribute('checked', 'checked');
$script .= "
$('#$fieldid').trigger('change');
";
}
}
} elseif ($_style == 'select') {
$element = $document->createElement('select');
cs_design_setbasic($element, $fieldid, $fieldname, $arguments);
$document->appendChild($element);
// leave a blank one in the front
if (! isset($arguments['multiple']) && ! isset($arguments['size']) || isset($arguments['_firstlabel'])) {
if (! empty($arguments['_firstlabel'])) {
$label = $arguments['_firstlabel'];
} else {
$label = '';
}
$option = $document->createElement('option', $label);
$option->setAttribute('value', '');
$element->appendChild($option);
}
$script .= "
$('#$fieldid').change(function() {
customsearch$id.add('$fieldid', {
config: " . json_encode($arguments) . ",
name: 'categories',
value: $(this).val()
});
});
";
foreach ($cats as $c) {
/** @var DOMElement $option */
$option = $document->createElement('option');
$option->setAttribute('value', $c['categId']);
$option->appendChild($document->createTextNode($_categpath ? $c['relativePathString'] : $c['name']));
$element->appendChild($option);
if ($default && in_array($c['categId'], (array) $default)) {
$option->setAttribute('selected', 'selected');
$script .= "
$('#$fieldid').trigger('change');
";
}
}
}
return '~np~' . $document->saveHTML() . '~/np~';
}
function cs_design_select($id, $fieldname, $fieldid, $arguments, $default, &$script)
{
$document = new DOMDocument();
$element = $document->createElement('select');
cs_design_setbasic($element, $fieldid, $fieldname, $arguments);
$document->appendChild($element);
if (isset($arguments['_labels'])) {
$labels = explode(',', $arguments['_labels']);
} else {
$labels = [];
}
if (isset($arguments['_options'])) {
$options = explode(',', $arguments['_options']);
} else {
$options = [];
}
// get the options for an ItemLink field - needs _trackerId and _field set in the {select} plugin
if (
empty($options) && empty($labels) && isset($arguments['_field']) &&
strpos($arguments['_field'], 'tracker_field_') === 0 && ! empty($arguments['_trackerId'])
) {
$definition = Tracker_Definition::get($arguments['_trackerId']);
$field = $definition->getFieldFromPermName(str_replace('tracker_field_', '', $arguments['_field']));
$handler = TikiLib::lib('trk')->get_field_handler($field);
if ($field['type'] === 'r') { // Item Link
$labels = $handler->getItemList();
$options = array_keys($labels);
$labels = array_values($labels);
} elseif ($field['type'] === 't') { // Text field so get all values up to a sensible(?) amount
global $prefs;
// turns out using a straight "old fashioned" DISTINCT MySQL query here is the most efficient
$result = TikiLib::lib('tiki')->query(
'SELECT DISTINCT `value` FROM `tiki_tracker_item_fields` WHERE `fieldId` = ? ORDER BY `value` LIMIT 1000;',
$field['fieldId']
);
while ($row = $result->fetchRow()) {
$label = $row['value'];
if (! in_array($label, $labels)) {
$labels[] = $label;
$label = explode(' ', $label);
foreach ($label as & $word) {
if ($prefs['unified_engine'] !== 'mysql') {
if (in_array($word, $prefs['unified_stopwords'])) {
$word = '';
}
} else {
if (strlen($word) < 4) { // default mysql fulltext minimum word length TODO find current value for ft_min_word_len
$word = '';
}
}
}
$options[] = implode(' AND ', array_filter($label));
}
}
} elseif ($field['type'] === 'u') { // User Selector (only when in dropdown list mode)
$html = $handler->renderInput();
$fieldid = 'user_selector_' . $field['fieldId'];
$script .= "
$('#$fieldid').change(function() {
customsearch$id.add('$fieldid', {
config: " . json_encode($arguments) . ",
name: 'select',
value: $(this).val()
});
});
";
return $html;
} elseif (in_array($field['type'], ['d','D','R','M'])) { // or types - dropdown, dropdown with other, radio button. multiselect
$data = $handler->getFieldData();
$options = array_keys($data['possibilities']);
$labels = array_values($data['possibilities']);
}
}
if (isset($arguments['_mandatory']) && $arguments['_mandatory'] == 'y') {
$mandatory = true;
} else {
$mandatory = false;
}
// leave a blank one in the front
if (! $mandatory && ! isset($arguments['multiple']) && ! isset($arguments['size']) || isset($arguments['_firstlabel'])) {
if (! empty($arguments['_firstlabel'])) {
$label = $arguments['_firstlabel'];
} else {
$label = '';
}
$option = $document->createElement('option', $label);
$option->setAttribute('value', '');
$element->appendChild($option);
}
$script .= "
$('#$fieldid').change(function() {
customsearch$id.add('$fieldid', {
config: " . json_encode($arguments) . ",
name: 'select',
value: $(this).val()
});
});
";
foreach ($options as $k => $opt) {
if (empty($labels[$k]) && is_numeric($labels[$k]) === false) {
$body = $opt;
} else {
$body = $labels[$k];
}
$option = $document->createElement('option', $body);
if ((empty($arguments['_unquoted']) || $arguments['_unquoted'] !== 'y') && strpos($opt, ' ') !== false) {
// quote values with spaces in
$opt = '"' . $opt . '"';
}
$option->setAttribute('value', $opt);
if ($default && in_array($opt, (array) $default)) {
$option->setAttribute('selected', 'selected');
$script .= "
$('#$fieldid').trigger('change');
";
}
$element->appendChild($option);
}
return '~np~' . $document->saveHTML() . '~/np~';
}
function cs_design_daterange($id, $fieldname, $fieldid, $arguments, $default, &$script)
{
extract($arguments, EXTR_SKIP);
$smarty = TikiLib::lib('smarty');
$smarty->loadPlugin('smarty_function_jscalendar');
if (! empty($default['from'])) {
$_from = $default['from'];
}
if (! empty($default['to'])) {
$_to = $default['to'];
}
if (! empty($default['gap'])) {
$_gap = $default['gap'];
}
$params_from = [];
$params_to = [];
if (! empty($_showtime) && $_showtime == 'y') {
$params_from['showtime'] = 'y';
$params_to['showtime'] = 'y';
} else {
$params_from['showtime'] = 'n';
$params_to['showtime'] = 'n';
}
$params_from['fieldname'] = $fieldname . '_from';
$params_to['fieldname'] = $fieldname . '_to';
$params_from['id'] = $fieldid_from = $fieldid . '_from';
$params_to['id'] = $fieldid_to = $fieldid . '_to';
if (isset($_from) && ! is_numeric($_from)) {
$_from = strtotime($_from);
if (! $_from) {
Feedback::error(tr('_from parameter not valid: "%0"', $arguments['_from']));
}
}
if (isset($_to) && ! is_numeric($_to)) {
$_to = strtotime($_to);
if (! $_to) {
Feedback::error(tr('_to parameter not valid: "%0"', $arguments['_to']));
}
}
if (isset($_gap) && ! is_numeric($_gap)) {
$_gap = strtotime($_gap);
if (! $_gap) {
Feedback::error(tr('_gap parameter not valid: "%0"', $arguments['_gap']));
} else {
$_gap -= time();
}
}
$startEmpty = (empty($_from) && empty($_to) && empty($_gap));
if (! empty($_from)) {
if ($_from == 'now') {
$params_from['date'] = TikiLib::lib('tiki')->now;
} else {
$params_from['date'] = $_from;
}
if (empty($_to)) {
if (empty($_gap)) {
$_gap = 365 * 24 * 3600;
}
$params_to['date'] = $params_from['date'] + $_gap;
}
} else {
$params_from['date'] = $startEmpty ? '' : TikiLib::lib('tiki')->now;
}
if (! empty($_to)) {
if ($_to == 'now') {
$params_to['date'] = TikiLib::lib('tiki')->now;
} else {
$params_to['date'] = $_to;
}
if (empty($_from)) {
if (empty($_gap)) {
$_gap = 365 * 24 * 3600;
}
$params_from['date'] = $params_to['date'] - $_gap;
}
} elseif (empty($params_to['date'])) {
$params_to['date'] = $startEmpty ? '' : TikiLib::lib('tiki')->now + 365 * 24 * 3600;
}
$picker = '';
$picker .= '<div class="col-sm-6">' . smarty_function_jscalendar($params_from, $smarty->getEmptyInternalTemplate()) . '</div>';
$picker .= '<div class="col-sm-6">' . smarty_function_jscalendar($params_to, $smarty->getEmptyInternalTemplate()) . '</div>';
$script .= "
$('#{$fieldid_from},#{$fieldid_to}').change(function() {
updateDateRange_$fieldid();
});
function updateDateRange_$fieldid() {
var from = $('#$fieldid_from').val();
var to = $('#$fieldid_to').val();
var val = (from && to) ? from + ',' + to : '';
customsearch$id.add('$fieldid', {
config: " . json_encode($arguments) . ",
name: 'daterange',
value: val
});
}
updateDateRange_$fieldid();
";
return '<div class="daterange row">' . $picker . '</div>';
}
function cs_design_distance($id, $fieldname, $fieldid, $arguments, $default, &$script)
{
$document = new DOMDocument();
$distanceElement = $document->createElement('input');
if (! empty($arguments['id'])) {
$arguments['id'] = $fieldid . '_dist';
}
cs_design_setbasic($distanceElement, $fieldid . '_dist', $fieldname, $arguments);
$latElement = $document->createElement('input');
if (! empty($arguments['id'])) {
$arguments['id'] = $fieldid . '_lat';
}
cs_design_setbasic($latElement, $fieldid . '_lat', $fieldname, $arguments);
$lonElement = $document->createElement('input');
if (! empty($arguments['id'])) {
$arguments['id'] = $fieldid . '_lon';
}
cs_design_setbasic($lonElement, $fieldid . '_lon', $fieldname, $arguments);
if (! empty($default)) {
$arguments['default'] = $default;
}
$script .= "
(function (id, config, fieldname) {
var fields = $('input[name=$fieldname]');
fields.change(function() {
var filter = {
config: config,
name: 'distance',
value: fields.map(function () {return $(this).val();}).get().join()
};
customsearch$id.add($(this).attr('name'), filter);
}).change();
})('$fieldid', " . json_encode($arguments) . ", " . json_encode($fieldname) . ");
";
$arguments = new JitFilter($arguments);
$distanceElement->setAttribute('value', $arguments->_distance->text());
$latElement->setAttribute('value', $arguments->_lat->text());
$lonElement->setAttribute('value', $arguments->_lon->text());
$document->appendChild($distanceElement);
$document->appendChild($latElement);
$document->appendChild($lonElement);
return $document->saveHTML();
}
function cs_design_store($id, $fieldname, $fieldid, $arguments, $default, &$script)
{
global $prefs;
if ($prefs['storedsearch_enabled'] != 'y') {
return;
}
$document = new DOMDocument();
$element = $document->createElement('input');
$element->setAttribute('type', 'submit');
cs_design_setbasic($element, $fieldid, $fieldname, $arguments);
$document->appendChild($element);
$script .= "
$('#$fieldid').click(function() {
$(this).serviceDialog({
title: $(this).val(),
controller: 'search_stored',
action: 'select',
success: function (data) {
customsearch$id.store_query = data.queryId;
customsearch$id.load();
}
});
return false;
});
";
return $document->saveHTML();
}