1 <?php
2 /*
3 * This code is part of GOsa (http://www.gosa-project.org)
4 * Copyright (C) 2003-2008 GONICUS GmbH
5 *
6 * ID: $$Id$$
7 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21 */
23 class filter {
25 var $xmlData;
26 var $elements= array();
27 var $elementValues= array();
28 var $alphabetElements= array();
29 var $autocompleter= array();
30 var $category= "";
31 var $objectStorage= array();
32 var $base= "";
33 var $scope= "";
34 var $query;
35 var $initial= false;
36 var $scopeMode= "auto";
37 var $alphabet= null;
38 var $converter= array();
39 var $pid;
42 function filter($filename)
43 {
44 global $config;
46 // Load eventually passed filename
47 if (!$this->load($filename)) {
48 die("Cannot parse $filename!");
49 }
51 $this->pid= preg_replace("/[^0-9]/", "", microtime(TRUE));
52 }
55 function load($filename)
56 {
57 $contents = file_get_contents($filename);
58 $this->xmlData= xml::xml2array($contents, 1);
60 if (!isset($this->xmlData['filterdef'])) {
61 return false;
62 }
64 $this->xmlData= $this->xmlData["filterdef"];
66 // Load filter
67 if (isset($this->xmlData['search'])) {
68 if (!isset($this->xmlData['search']['query'][0])){
69 $this->xmlData['search']['query']= array($this->xmlData['search']['query']);
70 }
72 // Move information
73 $entry= $this->xmlData['search'];
74 $this->scopeMode= $entry['scope'];
75 if ($entry['scope'] == "auto") {
76 $this->scope= "one";
77 } else {
78 $this->scope= $entry['scope'];
79 }
80 $this->query= $entry['query'];
81 } else {
82 return false;
83 }
85 // Transfer initial value
86 if (isset($this->xmlData['definition']['initial']) && $this->xmlData['definition']['initial'] == "true"){
87 $this->initial= true;
88 }
90 // Transfer category
91 if (isset($this->xmlData['definition']['category'])){
92 $this->category= $this->xmlData['definition']['category'];
93 }
95 // Generate formular data
96 if (isset($this->xmlData['element'])) {
97 if (!isset($this->xmlData['element'][0])){
98 $this->xmlData['element']= array($this->xmlData['element']);
99 }
100 foreach ($this->xmlData['element'] as $element) {
102 // Ignore elements without type
103 if (!isset($element['type']) || !isset($element['tag'])) {
104 next;
105 }
107 $tag= $element['tag'];
109 // Fix arrays
110 if (isset($element['value']) && !isset($element['value'][0])) {
111 $element['value']= array($element['value']);
112 }
114 // Store element for quick access
115 $this->elements[$tag] = $element;
117 // Preset elementValues with default values if exist
118 if (isset($element['default']) && !is_array($element['default'])) {
119 $this->elementValues[$tag] = $element['default'];
120 } else {
121 $this->elementValues[$tag] = "";
122 }
124 // Does this element react on alphabet links?
125 if (isset($element['alphabet']) && $element['alphabet'] == "true") {
126 $this->alphabetElements[]= $tag;
127 }
128 }
130 uasort($this->elements, 'strlenSort');
131 $this->elements= array_reverse($this->elements);
133 }
135 return true;
136 }
139 function getTextfield($element)
140 {
141 $tag= $element['tag'];
142 $size= 30;
143 if (isset($element['size'])){
144 $size= $element['size'];
145 }
146 $maxlength= 30;
147 if (isset($element['maxlength'])){
148 $maxlength= $element['maxlength'];
149 }
150 $result= "<input class='filter_textfield' id='$tag' name='$tag' type='text' size='$size' maxlength='{$maxlength}' value='".$this->elementValues[$tag]."'>";
151 if (isset($element['autocomplete'])) {
152 $frequency= "0.5";
153 $characters= "1";
154 if (isset($element['autocomplete']['frequency'])) {
155 $frequency= $element['autocomplete']['frequency'];
156 }
157 if (isset($element['autocomplete']['characters'])) {
158 $characters= $element['autocomplete']['characters'];
159 }
160 $result.= "<div id='autocomplete$tag' class='autocomplete'></div>".
161 "<script type='text/javascript'>".
162 "new Ajax.Autocompleter('$tag', 'autocomplete$tag', 'autocomplete.php', { minChars: $characters, frequency: $frequency });".
163 "</script>";
165 $this->autocompleters[$tag]= $element['autocomplete'];
166 }
167 return $result;
168 }
171 function getCheckbox($element)
172 {
173 $tag= $element['tag'];
174 $checked= "";
175 if ($this->elementValues[$tag] == "true") {
176 $checked= " checked";
177 }
179 $result= "<input class='filter_checkbox' id='$tag' name='$tag' type='checkbox' onClick='document.mainform.submit();' value='true'$checked>";
180 return $result;
181 }
184 function getCombobox($element)
185 {
186 $result= "<select name='".$element['tag']."' size='1' onChange='document.mainform.submit();'>";
188 // Fill with presets
189 foreach ($element['value'] as $value) {
190 $selected= "";
191 if ($this->elementValues[$element['tag']] == $value['key']) {
192 $selected= " selected";
193 }
195 // Handle translations
196 $result.= "<option value='".$value['key']."'$selected>"._($value['label'])."</option>";
197 }
199 // Use autocompleter for additional data
200 if (isset($element['autocomplete'])) {
201 $list= $this->getCompletitionList($element['autocomplete'], $element['tag']);
202 foreach ($list as $value) {
203 $selected= "";
204 if ($this->elementValues[$element['tag']] == $value) {
205 $selected= " selected";
206 }
207 $result.= "<option value='$value'$selected>$value</option>";
208 }
209 }
211 $result.= "</select>";
213 return $result;
214 }
217 function setComboBoxOptions($tag, $options)
218 {
219 if (isset($this->elements[$tag]) && $this->elements[$tag]['type'] == "combobox") {
221 $this->elements[$tag]['value']= array();
222 foreach ($options as $key => $label) {
223 $this->elements[$tag]['value'][]= array('label' => $label, 'key' => $key);
224 }
225 }
226 }
229 function getCurrentBase()
230 {
231 if (isset($this->search->base) && (string)$this->search->scope != "auto") {
232 return false;
233 }
235 return $this->base;
236 }
239 function getCurrentScope()
240 {
241 if (isset($this->search->scope) && (string)$this->search->scope != "auto") {
242 return (string)$this->search->scope;
243 }
245 return $this->scope;
246 }
249 function setConverter($field, $hook)
250 {
251 $this->converter[$field]= $hook;
252 }
255 function setObjectStorage($storage)
256 {
257 $this->objectStorage= $storage;
258 }
261 function setBase($base)
262 {
263 $this->base= $base;
264 }
267 function setCurrentScope($scope)
268 {
269 $this->scope= $scope;
270 }
273 function renderAlphabet($columns= 10)
274 {
275 // Return pre-rendered alphabet if available
276 if ($this->alphabet) {
277 return ($this->alphabet);
278 }
280 $characters= _("*ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");
281 $alphabet= "";
282 $c= 0;
284 /* Fill cells with charaters */
285 for ($i= 0, $l= mb_strlen($characters, 'UTF8'); $i<$l; $i++){
286 if ($c == 0){
287 $alphabet.= "<tr>";
288 }
290 $ch = mb_substr($characters, $i, 1, "UTF8");
291 $alphabet.= "<td><a class=\"alphaselect\" href=\"main.php?plug=".
292 validate($_GET['plug'])."&filter=".$ch."\"> ".$ch." </a></td>";
294 if ($c++ == $columns){
295 $alphabet.= "</tr>";
296 $c= 0;
297 }
298 }
300 /* Fill remaining cells */
301 while ($c++ <= $columns){
302 $alphabet.= "<td> </td>";
303 }
305 /* Save alphabet */
306 $this->alphabet= "<table width='100%'>$alphabet</table>";
308 return ($this->alphabet);
309 }
312 function renderApply()
313 {
314 return ("<input type='submit' name='apply' value='"._("Apply filter")."'>");
315 }
318 function renderScope()
319 {
320 $checked= $this->scope == "sub"?" checked":"";
321 return "<input type='checkbox' id='SCOPE' name='SCOPE' value='1' onClick='document.mainform.submit();'$checked> <LABEL for='SCOPE'>"._("Search in subtrees")."</LABEL>";
322 }
325 function render()
326 {
327 $smarty= get_smarty();
328 $smarty->assign("ALPHABET", $this->renderAlphabet());
329 $smarty->assign("APPLY", $this->renderApply());
330 $smarty->assign("SCOPE", $this->renderScope());
332 // Load template and replace elementsHtml[]
333 foreach ($this->elements as $tag => $element) {
334 $htmlCode= "";
335 switch ($element['type']) {
336 case "textfield":
337 $htmlCode = $this->getTextfield($element);
338 break;
340 case "checkbox":
341 $htmlCode = $this->getCheckbox($element);
342 break;
344 case "combobox":
345 $htmlCode = $this->getCombobox($element);
346 break;
348 default:
349 die ("Unknown element type specified!");
350 }
351 $smarty->assign("$tag", $htmlCode);
352 }
354 // Try to load template from plugin the folder first...
355 $file = get_template_path($this->xmlData['definition']['template'], true);
357 // ... if this fails, try to load the file from the theme folder.
358 if(!file_exists($file)){
359 $file = get_template_path($this->xmlData['definition']['template']);
360 }
362 // Load template
363 return ("<input type='hidden' name='FILTER_PID' value='".$this->pid."'>".$smarty->fetch($file));
364 }
367 function query()
368 {
369 global $class_mapping;
370 $result= array();
372 // Return empty list if initial is not set
373 if (!$this->initial) {
374 $this->initial= true;
375 return $result;
376 }
378 // Go thru all queries and merge results
379 foreach ($this->query as $query) {
380 if (!isset($query['backend']) || !isset($query['filter']) || !isset($query['attribute'])) {
381 die("No backend specified in search config.");
382 }
384 // Is backend available?
385 $backend= "filter".$query['backend'];
386 if (!isset($class_mapping["$backend"])) {
387 die("Invalid backend specified in search config.");
388 }
390 // Load filter and attributes
391 $filter= $query['filter'];
392 $attributes= $query['attribute'];
394 // Generate final filter
395 foreach ($this->elements as $tag => $element) {
396 if (!isset($element['set']) || !isset($element['unset'])) {
397 continue;
398 }
400 // Handle converters if present
401 if (isset($this->converter[$tag])) {
402 preg_match('/([^:]+)::(.*)$/', $this->converter[$tag], $m);
403 $e_set= call_user_func(array($m[1], $m[2]), preg_replace('/\$/', $this->elementValues[$tag], is_array($element['set'])?"":$element['set']));
404 $e_unset= call_user_func(array($m[1], $m[2]), preg_replace('/\$/', $this->elementValues[$tag], is_array($element['unset'])?"":$element['unset']));
405 } else {
406 $e_set= is_array($element['set'])?"":$element['set'];
407 $e_unset= is_array($element['unset'])?"":$element['unset'];
408 }
410 // Do not replace escaped \$ - This is required to be able to search for e.g. windows machines.
411 if ($this->elementValues[$tag] == "") {
412 $e_unset= preg_replace('/([^\\\\])\$/', '${1}'.normalizeLdap($this->elementValues[$tag]), $e_unset);
413 $e_unset= preg_replace('/\\\\\$/','$', $e_unset);
414 $filter= preg_replace("/\\$$tag/", $e_unset, $filter);
415 } else {
416 $e_set= preg_replace('/([^\\\\])\$/', '${1}'.normalizeLdap($this->elementValues[$tag]), $e_set);
417 $e_set= preg_replace('/\\\\\$/','$', $e_set);
418 $filter= preg_replace("/\\$$tag/", $e_set, $filter);
419 }
420 }
422 $result= array_merge($result, call_user_func(array($backend, 'query'), $this->base, $this->scope, $filter, $attributes, $this->category, $this->objectStorage));
423 }
425 return ($result);
426 }
429 function isValid()
430 {
431 foreach ($this->elements as $tag => $element) {
432 if (isset($element->regex)){
433 if (!preg_match('/'.(string)$element->regex.'/', $this->elementValues[$tag])){
434 return false;
435 }
436 }
437 }
438 return true;
439 }
442 function update()
443 {
444 /* React on alphabet links if needed */
445 if (isset($_GET['filter'])){
446 $s= mb_substr(validate($_GET['filter']), 0, 1, "UTF8");
447 foreach ($this->alphabetElements as $tag) {
448 $this->elementValues[$tag]= $s;
449 }
450 }
452 if (isset($_POST['FILTER_PID']) && $_POST['FILTER_PID'] == $this->pid) {
453 // Load post values and adapt filter, base and scope accordingly - but
454 // only if we didn't get a _GET
455 foreach ($this->elements as $tag => $element) {
456 if (isset($_POST[$tag])){
457 $this->elementValues[$tag]= validate($_POST[$tag]);
458 } else {
459 $this->elementValues[$tag]= "";
460 }
461 }
463 // Save scope if needed
464 if ($this->scopeMode == "auto") {
465 $this->scope= isset($_POST['SCOPE'])?"sub":"one";
466 }
467 }
469 }
472 function getCompletitionList($config, $tag, $value="*")
473 {
474 global $class_mapping;
475 $res= array();
477 // Is backend available?
478 $backend= "filter".$config['backend'];
479 if (!isset($class_mapping["$backend"])) {
480 die("Invalid backend specified in search config.");
481 }
483 // Load filter and attributes
484 $filter= $config['filter'];
485 $attributes= $config['attribute'];
486 if (!is_array($attributes)) {
487 $attributes= array($attributes);
488 }
490 // Make filter
491 $filter= preg_replace("/\\$$tag/", normalizeLdap($value), $filter);
492 if (isset($config['base']) && isset($config['scope']) && isset($config['category'])) {
493 $result= call_user_func(array($backend, 'query'), $config['base'], $config['scope'], $filter, $attributes,
494 $config["category"], $config["objectStorage"]);
495 } else {
496 $result= call_user_func(array($backend, 'query'), $this->base, $this->scope, $filter, $attributes,
497 $this->category, $this->objectStorage);
498 }
500 foreach ($result as $entry) {
501 foreach ($attributes as $attribute) {
502 if (is_array($entry[$attribute])) {
503 for ($i= 0; $i<$entry[$attribute]['count']; $i++) {
504 if (mb_stristr($entry[$attribute][$i], $value)) {
505 $res[]= $entry[$attribute][$i];
506 }
507 }
508 } else {
509 $res[]= $entry[$attribute];
510 }
511 }
512 }
514 return $res;
515 }
518 function processAutocomplete()
519 {
520 global $class_mapping;
521 $result= array();
523 // Introduce maximum number of entries
524 $max= 25;
526 foreach ($this->autocompleters as $tag => $config) {
527 if(isset($_POST[$tag])){
528 $result= $this->getCompletitionList($config, $tag, $_POST[$tag]);
529 $result= array_unique($result);
530 asort($result);
532 echo '<ul>';
533 foreach ($result as $entry) {
534 echo '<li>'.mark($_POST[$tag], $entry).'</li>';
535 if ($max-- == 0) {
536 break;
537 }
538 }
540 echo '</ul>';
541 }
542 }
543 }
546 function getObjectBase($dn)
547 {
548 global $config;
549 $base= "";
551 // Try every object storage
552 $storage= $this->objectStorage;
553 if (!is_array($storage)){
554 $storage= array($storage);
555 }
556 foreach ($storage as $location) {
557 $pattern= "/^[^,]+,".preg_quote($location, '/')."/i";
558 $base= preg_replace($pattern, '', $dn);
559 }
561 /* Set to base, if we're not on a correct subtree */
562 if (!isset($config->idepartments[$base])){
563 $base= $config->current['BASE'];
564 }
566 return $base;
567 }
570 }
572 // Sort elements for element length to allow proper replacing later on
573 function strlenSort($a, $b) {
574 if (strlen($a['tag']) == strlen($b['tag'])) {
575 return 0;
576 }
577 return (strlen($a['tag']) < strlen($b['tag']) ? -1 : 1);
578 }
580 ?>