<?php
/*******************************************************************************
 * Copyright (C) 2007 Easter-eggs
 * https://ldapsaisie.org
 *
 * Author: See AUTHORS file in top-level directory.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version 2
 * as published by the Free Software Foundation.
 *
 * This program 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

******************************************************************************/

LSsession :: loadLSclass('LSlog_staticLoggerClass');

/**
 * Object LSsearch
 *
 * @property-read string $basedn
 * @property-read string $sortBy
 * @property-read string $sortDirection 'ASC' or 'DESC'
 * @property-read array<string> $attributes
 * @property-read string $LSobject
 * @property-read string $LSobject
 * @property-read string $label_objectName
 * @property-read string $label_level
 * @property-read string $label_actions
 * @property-read string $label_no_result
 * @property-read bool $sort
 * @property-read int $sortlimit
 * @property-read int $total
 * @property-read bool $displaySubDn
 * @property-read bool $canCopy
 * @property-read array $predefinedFilters
 * @property-read array $extraDisplayedColumns
 * @property-read array $visibleExtraDisplayedColumns
 * @property-read string $hash
 * @author Benjamin Renard <brenard@easter-eggs.com>
 */
class LSsearch extends LSlog_staticLoggerClass {

  /**
   * The LdapObject type of search
   * @var string|null
   */
  private $LSobject = NULL;

  /**
   * The configuration of search
   * LSobjects.<type>.LSsearch
   * @see self::loadConfig()
   * @var array
   */
  private $config;

  /**
   * The context of search
   * @see self::__construct()
   * @var string
   */
  private $context;

  /**
   * The parameters of the search
   * @see self::setParams()
   * @see self::setParam()
   * @see self::getParam()
   * @see self::loadParamsFromSession()
   * @see self::saveParamsInSession()
   * @see self::setParamsFromRequest()
   * @var array<string,mixed>
   */
  private $params = array (
    // Search params
    'filter' => NULL,
    'pattern' => NULL,
    'predefinedFilter' => false,
    'basedn' => NULL,
    'subDn' => NULL,
    'scope' => NULL,
    'sizelimit' => 0,
    'attronly' => false,    // If true, only attribute names are returned
    'approx' => false,
    'recursive' => false,
    'attributes' => array(),
    // Display params
    'onlyAccessible' => NULL,
    'sortDirection' => NULL,
    'sortBy' => NULL,
    'sortlimit' => 0,
    'displaySubDn' => NULL,
    'displayFormat' => NULL,
    'nbObjectsByPage' => NB_LSOBJECT_LIST,
    'nbObjectsByPageChoices' => NULL,
    'customInfos' => array(),
    'withoutCache' => false,
    'extraDisplayedColumns' => false,
  );

  /**
   * The cache of search parameters
   * @see self::generateSearchParams()
   * @see self::formatSearchParams()
   * @var array<string,mixed>
   */
  private $_searchParams = array();

  /**
   * The result of the search
   * @var array|null
   */
  private $result = NULL;

  /**
   * Cache about $this->canCopy
   * @see self::__get()
   * @var bool|null
   */
  private $_canCopy = NULL;

  /**
   * Constructor
   *
   * @param string $LSobject The LdapObject type of search
   * @param string $context Context of search (LSrelation / LSldapObject/ ...)
   * @param array|null $params Parameters of search
   * @param boolean $purgeParams If params in session have to be purged
   *
   **/
  public function __construct($LSobject,$context,$params=null,$purgeParams=false) {
    if (!LSsession :: loadLSobject($LSobject)) {
      return;
    }
    $this -> LSobject = $LSobject;

    $this -> loadConfig();

    if (isset($_REQUEST['LSsearchPurgeSession'])) {
      $this -> purgeSession();
    }

    $this -> context = $context;

    if (!$purgeParams) {
      if (! $this -> loadParamsFromSession()) {
        $this -> loadDefaultParameters();
      }
    }
    else {
      $this -> purgeParams($LSobject, $context);
      $this -> loadDefaultParameters();
    }

    if (is_array($params)) {
      $this -> setParams($params);
    }

  }

  /**
   * Allow conversion of LSsearch to string
   *
   * @return string The string representation of the LSsearch
   */
  public function __toString() {
    $search_params = $this -> formatSearchParams();
    return "<LSsearch of ".$this -> LSobject." (context=".$this -> context.")".($search_params?" $search_params":"").">";
  }

  /**
   * Load configuration from LSconfig
   *
   * @return void
   */
  private function loadConfig() {
    $this -> config = LSconfig::get("LSobjects.".$this -> LSobject.".LSsearch");
    if (isset($this -> config['predefinedFilters']) && is_array($this -> config['predefinedFilters'])) {
      foreach($this -> config['predefinedFilters'] as $filter => $label) {
        if(!LSldap::isValidFilter($filter)) {
          LSerror::addErrorCode('LSsearch_15',array('label' => $label, 'filter' => $filter, 'type' => $this -> LSobject));
          unset($this -> config['predefinedFilters'][$filter]);
        }
      }
    }
  }

  /**
   * Load default search parameters from configuration
   *
   * @return boolean True on success or False
   */
  private function loadDefaultParameters() {
    if (isset($this -> config['params']) && is_array($this -> config['params'])) {
      self :: log_debug('Load default parameters (from object type configuration)');
      return $this -> setParams($this -> config['params']);
    }
    else
      self :: log_trace('loadDefaultParameters(): no parameters found in object type configuration');
    return true;
  }

  /**
   * Load search parameters from session
   *
   * @return boolean True if params has been loaded from session or False
   */
  private function loadParamsFromSession() {
    if (
      isset($_SESSION['LSsession'])
      && isset($_SESSION['LSsession']['LSsearch'])
      && isset($_SESSION['LSsession']['LSsearch'][$this -> LSobject])
      && isset($_SESSION['LSsession']['LSsearch'][$this -> LSobject]['params'])
      && isset($_SESSION['LSsession']['LSsearch'][$this -> LSobject]['params'][$this -> context])
      && is_array($_SESSION['LSsession']['LSsearch'][$this -> LSobject]['params'][$this -> context])
    ) {
      self :: log_debug('Load params from session for context '.$this -> context);
      $params = $_SESSION['LSsession']['LSsearch'][$this -> LSobject]['params'][$this -> context];

      if ($params['filter']) {
        $params['filter'] = Net_LDAP2_Filter::parse($params['filter']);
      }

      $this -> params = $params;
      return true;
    }
    else
      self :: log_trace("loadParamsFromSession(): no params in session for context ".$this -> context);
    return false;
  }

  /**
   * Save search parameters in session
   *
   * @return void
   */
  private function saveParamsInSession() {
    self :: log_debug(
      'saveParamsInSession(".$this -> LSobject."): Save context params session '.$this -> context);
    $params = $this -> params;
    if ($params['filter'] instanceof Net_LDAP2_Filter) {
      $params['filter'] = $params['filter'] -> asString();
    }
    if (!isset($_SESSION['LSsession']))
      $_SESSION['LSsession'] = array();
    if (!isset($_SESSION['LSsession']['LSsearch']))
      $_SESSION['LSsession']['LSsearch'] = array();
    if (!isset($_SESSION['LSsession']['LSsearch'][$this -> LSobject]))
      $_SESSION['LSsession']['LSsearch'][$this -> LSobject] = array();
    if (!isset($_SESSION['LSsession']['LSsearch'][$this -> LSobject]['params']))
      $_SESSION['LSsession']['LSsearch'][$this -> LSobject]['params'] = array();
    if (!isset($_SESSION['LSsession']['LSsearch'][$this -> LSobject]['params'][$this -> context]))
      $_SESSION['LSsession']['LSsearch'][$this -> LSobject]['params'][$this -> context] = array();
    foreach ($params as $param => $value) {
      if (
        !isset($_SESSION['LSsession']['LSsearch'][$this -> LSobject]['params'][$this -> context][$param])
        || $_SESSION['LSsession']['LSsearch'][$this -> LSobject]['params'][$this -> context][$param] != $value
      ) {
        self :: log_trace("saveParamsInSession(".$this -> LSobject."): $param => ".varDump($value));
        $_SESSION['LSsession']['LSsearch'][$this -> LSobject]['params'][$this -> context][$param]=$value;
      }
    }
  }

  /**
   * Purge parameters in session
   *
   * @param string $LSobject The LdapObject type
   * @param string|null $context The context of the search (optional, default: null)
   *
   * @return void
   */
  public static function purgeParams($LSobject, $context=null) {
    if (!isset($_SESSION['LSsession'])) return;
    if (!isset($_SESSION['LSsession']['LSsearch'])) return;
    if (!isset($_SESSION['LSsession']['LSsearch'][$LSobject])) return;
    if (!isset($_SESSION['LSsession']['LSsearch'][$LSobject]['params'])) return;
    if ($context) {
      if (isset($_SESSION['LSsession']['LSsearch'][$LSobject]['params'][$context]))
        unset($_SESSION['LSsession']['LSsearch'][$LSobject]['params'][$context]);
    }
    else
      unset($_SESSION['LSsession']['LSsearch'][$LSobject]['params']);
  }

  /**
   * Purge cache
   *
   * @param string $LSobject The LdapObject type
   * @param string|null $hash The hash of the search (optional, default: null)
   *
   * @return void
   */
  public static function purgeCache($LSobject, $hash=null) {
    if (!isset($_SESSION['LSsession'])) return;
    if (!isset($_SESSION['LSsession']['LSsearch'])) return;
    if (!isset($_SESSION['LSsession']['LSsearch'][$LSobject])) return;
    if (!isset($_SESSION['LSsession']['LSsearch'][$LSobject]['cache'])) return;
    if ($hash) {
      if (isset($_SESSION['LSsession']['LSsearch'][$LSobject]['cache'][$hash]))
        unset($_SESSION['LSsession']['LSsearch'][$LSobject]['cache'][$hash]);
    }
    else
      unset($_SESSION['LSsession']['LSsearch'][$LSobject]['cache']);
  }

  /**
   * Purge session
   *
   * @param string $LSobject The LdapObject type
   *
   * @return void
   */
  private function purgeSession($LSobject=null) {
    if (!isset($_SESSION['LSsession'])) return;
    if (!isset($_SESSION['LSsession']['LSsearch'])) return;
    if ($LSobject) {
      if (isset($_SESSION['LSsession']['LSsearch'][$LSobject]))
        unset($_SESSION['LSsession']['LSsearch'][$LSobject]);
    }
    else
      unset($_SESSION['LSsession']['LSsearch']);
  }

  /**
   * Define one search parameter
   *
   * @param string $param The parameter name
   * @param string $value The parameter value
   *
   * @return boolean True on success or False
   */
  public function setParam($param,$value) {
    return $this -> setParams(array($param => $value));
  }

  /**
   * Define search parameters
   *
   * @param array $params Parameters of search
   *
   * @return boolean True on success or False
   */
  public function setParams($params) {
    $OK=true;

    // Filter
    if (isset($params['filter'])) {
      if (is_string($params['filter'])) {
        $filter = Net_LDAP2_Filter::parse($params['filter']);
        if (!LSerror::isLdapError($filter)) {
          $this -> params['filter'] = $filter;
        }
        else {
          LSerror :: addErrorCode('LSsearch_01',$params['filter']);
          $OK=false;
        }
      }
      elseif($params['filter'] instanceof Net_LDAP2_Filter) {
        $this -> params['filter'] =& $params['filter'];
      }
    }

    // Approx
    if (isset($params['approx'])) {
      if (is_bool($params['approx']) || $params['approx']==0 || $params['approx']==1) {
        $this -> params['approx'] = (bool)$params['approx'];
      }
      else {
        LSerror :: addErrorCode('LSsearch_05','approx');
        $OK=false;
      }
    }

    // Without Cache
    if (isset($params['withoutCache'])) {
      if (is_bool($params['withoutCache']) || $params['withoutCache']==0 || $params['withoutCache']==1) {
        $this -> params['withoutCache'] = (bool)$params['withoutCache'];
      }
      else {
        LSerror :: addErrorCode('LSsearch_05','withoutCache');
        $OK=false;
      }
    }

    // Patterm
    if (isset($params['pattern'])) {
      if ($params['pattern']=="") {
        $this -> params['pattern'] = NULL;
      }
      elseif ($this -> isValidPattern($params['pattern'])) {
        $this -> params['pattern'] = $params['pattern'];
      }
    }


    // BaseDN
    if (isset($params['basedn']) && is_string($params['basedn'])) {
      if (isCompatibleDNs(LSsession :: getRootDn(),$params['basedn'])) {
        $this -> params['basedn'] = $params['basedn'];
      }
      else {
        LSerror :: addErrorCode('LSsearch_02',$params['basedn']);
        $OK=false;
      }
    }

    // subDn
    if (isset($params['subDn']) && is_string($params['subDn'])) {
      if (LSsession :: validSubDnLdapServer($params['subDn'])) {
        $this -> params['subDn'] = $params['subDn'];
      }
      else {
        LSerror :: addErrorCode('LSsearch_03','subDn');
        $OK=false;
      }
    }

    // Scope
    if (isset($params['scope']) && is_string($params['scope'])) {
      if (in_array($params['scope'],array('sub','one','base'))) {
        $this -> params['scope'] = $params['scope'];
      }
      else {
        LSerror :: addErrorCode('LSsearch_03','scope');
        $OK=false;
      }
    }

    // nbObjectsByPage
    if (isset($params['nbObjectsByPage'])) {
      if (((int)$params['nbObjectsByPage'])>=1 ) {
        $this -> params['nbObjectsByPage'] = (int)$params['nbObjectsByPage'];
      }
      else {
        LSerror :: addErrorCode('LSsearch_03','nbObjectsByPage');
        $OK=false;
      }
    }

    // nbObjectsByPageChoices
    if (isset($params['nbObjectsByPageChoices'])) {
      if (is_array($params['nbObjectsByPageChoices'])) {
        $choices = array();
        $choiceError = false;
        foreach($params['nbObjectsByPageChoices'] as $choice) {
          if (is_int($choice) && !in_array($choice, $choices)) {
            $choices[] = $choice;
          }
          else {
            $choiceError = true;
            break;
          }
        }
        if (!empty($choices) && !$choiceError) {
          $this -> params['nbObjectsByPageChoices'] = $choices;
        }
        else {
          LSerror :: addErrorCode('LSsearch_03','nbObjectsByPageChoices');
          $OK = false;
        }
      }
      else {
        LSerror :: addErrorCode('LSsearch_03','nbObjectsByPageChoices');
        $OK = false;
      }
    }

    // Extra Columns
    if (isset($params['extraDisplayedColumns'])) {
      $this -> params['extraDisplayedColumns']=(bool)$params['extraDisplayedColumns'];
    }

    // Sort Limit
    if (isset($params['sortlimit'])) {
      if (is_int($params['sortlimit']) && $params['sortlimit']>=0 ) {
        $this -> params['sortlimit'] = $params['sortlimit'];
      }
      elseif ((int)$params['sortlimit'] > 0) {
        $this -> params['sortlimit'] = (int)$params['sortlimit'];
      }
      else {
        LSerror :: addErrorCode('LSsearch_03','sortlimit');
        $OK=false;
      }
    }

    // Sort Direction
    if (isset($params['sortDirection']) && is_string($params['sortDirection'])) {
      if (in_array($params['sortDirection'],array('ASC','DESC'))) {
        $this -> params['sortDirection'] = $params['sortDirection'];
      }
      else {
        LSerror :: addErrorCode('LSsearch_03','sortDirection');
        $OK=false;
      }
    }

    // Sort By
    if (isset($params['sortBy']) && is_string($params['sortBy'])) {
      if (in_array($params['sortBy'],array('displayName','subDn')) || ($this ->extraDisplayedColumns && isset($this ->extraDisplayedColumns[$params['sortBy']]))) {
        if ($this -> params['sortBy'] == $params['sortBy']) {
          $this -> toggleSortDirection();
        }
        else {
          $this -> params['sortBy'] = $params['sortBy'];
          if (!isset($params['sortDirection']) || !is_string($params['sortDirection'])) {
            $this -> params['sortDirection'] = 'ASC';
          }
        }
      }
      else {
        LSerror :: addErrorCode('LSsearch_03','sortBy');
        $OK=false;
      }
    }

    // Size Limit
    if (isset($params['sizelimit'])) {
      if (((int)$params['sizelimit']) >= 0) {
        $this -> params['sizelimit'] = $params['sizelimit'];
      }
      else {
        LSerror :: addErrorCode('LSsearch_04');
        $OK=false;
      }
    }

    // Attronly
    if (isset($params['attronly'])) {
      if (is_bool($params['attronly']) || $params['attronly']==0 || $params['attronly']==1) {
        $this -> params['attronly'] = (bool)$params['attronly'];
      }
      else {
        LSerror :: addErrorCode('LSsearch_05','attronly');
        $OK=false;
      }
    }

    // Recursive
    if (isset($params['recursive'])) {
      if (is_bool($params['recursive']) || $params['recursive']==0 || $params['recursive']==1) {
        $this -> params['recursive'] = (bool)$params['recursive'];
      }
      else {
        LSerror :: addErrorCode('LSsearch_05','recursive');
        $OK=false;
      }
    }

    // displaySubDn
    if (isset($params['displaySubDn'])) {
      if (! LSsession :: isSubDnLSobject($this -> LSobject) ) {
        if (is_bool($params['displaySubDn']) || $params['displaySubDn']==0 || $params['displaySubDn']==1) {
          $this -> params['displaySubDn'] = (bool)$params['displaySubDn'];
        }
        else {
          LSerror :: addErrorCode('LSsearch_05','displaySubDn');
          $OK=false;
        }
      }
    }

    // Attributes
    if (isset($params['attributes'])) {
      if (is_string($params['attributes'])) {
        $this -> params['attributes'] = array($params['attributes']);
      }
      elseif (is_array($params['attributes'])) {
        $this -> params['attributes']=array();
        foreach ($params['attributes'] as $attr) {
          if (is_string($attr)) {
            if (LSconfig::get("LSobjects.".$this -> LSobject.".attrs.$attr")) {;
              $this -> params['attributes'][] = $attr;
            }
            else {
              LSerror :: addErrorCode('LSsearch_11',$attr);
            }
          }
        }
      }
      else {
        LSerror :: addErrorCode('LSsearch_06');
        $OK=false;
      }
    }

    // predefinedFilter
    if (isset($params['predefinedFilter'])) {
      if (is_string($params['predefinedFilter'])) {
        if (empty($params['predefinedFilter'])) {
          $this->params['predefinedFilter']=false;
        }
        elseif(is_array($this -> config['predefinedFilters'])) {
          if(isset($this->config['predefinedFilters'][$params['predefinedFilter']])) {
            $this -> params['predefinedFilter'] = $params['predefinedFilter'];
          }
          else {
            LSerror :: addErrorCode('LSsearch_03','predefinedFilter');
            $OK=false;
          }
        }
      }
      else {
        LSerror :: addErrorCode('LSsearch_03','predefinedFilter');
        $OK=false;
      }
    }

    // Display Format
    if (isset($params['displayFormat']) && is_string($params['displayFormat'])) {
      $this -> params['displayFormat'] = $params['displayFormat'];
    }

    // Custom Infos
    if (isset($params['customInfos']) && is_array($params['customInfos'])) {
      foreach($params['customInfos'] as $name => $data) {
        if(is_array($data['function']) && is_string($data['function'][0])) {
          LSsession::loadLSclass($data['function'][0]);
        }
        if (is_callable($data['function'])) {
          $this -> params['customInfos'][$name] = array (
            'function' => &$data['function'],
            'args' => (isset($data['args'])?$data['args']:null),
            'cache' => (isset($data['cache'])?boolval($data['cache']):true),
          );
        }
        else {
          LSerror :: addErrorCode('LSsearch_14',$name);
        }
      }
    }

    // Only Accessible objects
    if (isset($params['onlyAccessible'])) {
      $this -> params['onlyAccessible'] = (bool)$params['onlyAccessible'];
    }

    $this -> saveParamsInSession();
    return $OK;
  }

  /**
   * Return true only if the form is submited
   *
   * @return boolean True only if the is submited
   **/
  private function formIsSubmited() {
    return isset($_REQUEST['LSsearch_submit']);
  }

  /**
   * Define search parameters by reading request data ($_REQUEST)
   *
   * @return boolean True if all parameters found in request data are handled, False otherwise
   */
  public function setParamsFromRequest() {
    $allowedParams = array(
      'pattern', 'approx',  'recursive', 'extraDisplayedColumns', 'nbObjectsByPage',
      'attributes', 'sortBy', 'sortDirection', 'withoutCache', 'predefinedFilter',
      // Following parameters are allowed from request but need additional checks
      'filter', 'basedn', 'subDn', 'scope', 'attributes', 'displayFormat',
    );
    $data = array();

    foreach($_REQUEST as $key => $value) {
      if (!in_array($key, $allowedParams))
        continue;
      switch($key) {
        case 'filter':
          // Parse filter to extract attribute name and check if user have read access on it
          // FIXME: Net_LDAP2_Filter could only permit to extract attribute name from a simple
          // filter (without sub-filters), so we currently only access this type of filter.
          $filter = Net_LDAP2_Filter::parse($value);
          if (Net_LDAP2::isError($filter)) {
            LSerror :: addErrorCode('LSsearch_03', 'filter');
            return false;
          }
          $filter_parts = $filter -> getComponents();
          if (!is_array($filter_parts) || !LSsession :: canAccess($this -> LSobject, null, 'r', $filter_parts[0])) {
            LSerror :: addErrorCode('LSsearch_03', 'filter');
            return false;
          }
          break;

        case 'basedn':
        case 'subDn':
        case 'scope':
          // These parameters could only be used if combined with onlyAccessible==True
          $data['onlyAccessible'] = True;
          break;

        case 'attributes':
          foreach(ensureIsArray($value) as $attr) {
            if (!is_string($attr)) {
              LSerror :: addErrorCode('LSsearch_06');
              return false;
            }
            if (!LSsession :: canAccess($this -> LSobject, null, 'r', $attr)) {
              LSerror :: addErrorCode('LSsearch_11', $attr);
              return false;
            }
          }
          break;

        case 'displayFormat':
          if (!LSsession :: canComputeLSformat($value, $this -> LSobject)) {
            LSerror :: addErrorCode('LSsearch_11', 'displayFormat');
            return false;
          }
          break;
      }
      $data[$key] = $value;
    }

    if (self::formIsSubmited()) {
      // Recursive
      if (is_null($data['recursive'])) {
        $data['recursive']=false;
      }
      else {
        $data['recursive']=true;
      }

      // Approx
      if (is_null($data['approx'])) {
        $data['approx']=false;
      }
      else {
        $data['approx']=true;
      }

      if (isset($data['ajax']) && !isset($data['pattern'])) {
        $data['pattern']="";
      }
    }

    return (empty($data)?true:$this -> setParams($data));
  }

  /**
   * Toggle the sort direction
   *
   * @return void
   **/
  private function toggleSortDirection() {
    if ($this -> params['sortDirection']=="ASC") {
      $this -> params['sortDirection'] = "DESC";
    }
    else {
      $this -> params['sortDirection'] = "ASC";
    }
  }

  /**
   * Make a filter object with a pattern of search
   *
   * @param string|null $pattern The pattern of search. If is null, the pattern in params will be used.
   *
   * @return Net_LDAP2_Filter|false Net_LDAP2_Filter on success or False
   */
  public function getFilterFromPattern($pattern=NULL) {
    if ($pattern==NULL) {
      $pattern=$this -> params['pattern'];
    }
    if ($this -> isValidPattern($pattern)) {
      $attrsConfig=LSconfig::get("LSobjects.".$this -> LSobject.".LSsearch.attrs");
      $attrsList=array();
      if (!is_array($attrsConfig)) {
        foreach(LSconfig::get("LSobjects.".$this -> LSobject.".attrs") as $attr => $config) {
          $attrsList[$attr]=array();
        }
      }
      else {
        foreach($attrsConfig as $key => $val) {
          if(is_int($key)) {
            $attrsList[$val]=array();
          }
          else {
            $attrsList[$key]=$val;
          }
        }
      }

      if (empty($attrsList)) {
        LSerror :: addErrorCode('LSsearch_07');
        return false;
      }

      $filters=array();
      foreach ($attrsList as $attr => $opts) {
        if ($this -> params['approx']) {
          if (isset($opts['approxLSformat'])) {
            $filter=Net_LDAP2_Filter::parse(getFData($opts['approxLSformat'],array('name'=>$attr,'pattern'=>$pattern)));
          }
          else {
            $filter=Net_LDAP2_Filter::create($attr,'approx',$pattern);
          }
        }
        else {
          if (isset($opts['searchLSformat'])) {
            $filter=Net_LDAP2_Filter::parse(getFData($opts['searchLSformat'],array('name'=>$attr,'pattern'=>$pattern)));
          }
          else {
            $filter=Net_LDAP2_Filter::create($attr,'contains',$pattern);
          }
        }

        if (!Net_LDAP2::isError($filter)) {
          $filters[]=$filter;
        }
        else {
          LSerror :: addErrorCode('LSsearch_08',array('attr' => $attr,'pattern' => $pattern));
          return false;
        }
      }
      if($filters) {
        $filter=LSldap::combineFilters('or',$filters);
        if ($filter) {
          return $filter;
        }
        else {
          LSerror :: addErrorCode('LSsearch_09');
        }
      }
    }
    else {
      LSerror :: addErrorCode('LSsearch_10');
    }
    return false;
  }

  /**
   * Check if search pattern is valid
   *
   * @param string $pattern The pattern
   *
   * @return boolean True if pattern is valid or False
   **/
  public function isValidPattern($pattern) {
    if (is_string($pattern) && $pattern!= "") {
      $regex = (isset($this -> config['validPatternRegex'])?$this -> config['validPatternRegex']:'/^[\w \-\_\\\'\"^\[\]\(\)\{\}\=\+\£\%\$\€\.\:\;\,\?\/\@]+$/iu');
      if (preg_match($regex, $pattern))
        return True;
    }
    LSerror :: addErrorCode('LSsearch_17');
    return False;
  }

  /**
   * Check if cache is enabled
   *
   * @return boolean True if cache is enabled or False
   **/
  public function cacheIsEnabled() {
    if (isset($this -> config['cache'])) {
      $conf=$this -> config['cache'];
      if (is_bool($conf) || $conf==0 || $conf==1) {
        return (bool)$conf;
      }
      else {
        LSerror :: addErrorCode('LSsearch_03','cache');
      }
    }
    return LSsession :: cacheSearch();
  }

  /**
   * Methode for parameters value access
   *
   * @param string $key The parameter name
   *
   * @return mixed The parameter value or NULL
   **/
  public function getParam($key) {
    if(in_array($key,array_keys($this -> params))) {
      if ($key == 'nbObjectsByPageChoices' && !is_array($this -> params['nbObjectsByPageChoices'])) {
        return (isset($GLOBALS['NB_LSOBJECT_LIST_CHOICES']) && is_array($GLOBALS['NB_LSOBJECT_LIST_CHOICES'])?$GLOBALS['NB_LSOBJECT_LIST_CHOICES']:range(NB_LSOBJECT_LIST, NB_LSOBJECT_LIST*4, NB_LSOBJECT_LIST));
      }
      return $this -> params[$key];
    }
    return NULL;
  }

  /**
   * Return hidden fileds to add in search form
   *
   * @return array The hield fields whith their values
   **/
  public function getHiddenFieldForm() {
    return array (
      'LSobject' => $this -> LSobject
    );
  }

  /**
   * Generate an array with search parameters, only parameters whitch have to be
   * passed to Net_LDAP2 for the LDAP search. This array will be store in
   * $this -> _searchParams private variable.
   *
   * @return void
   **/
  private function generateSearchParams() {
    // Base
    $retval = array(
      'filter' => $this -> params['filter'],
      'basedn' => $this -> params['basedn'],
      'scope' => $this -> params['scope'],
      'sizelimit' => $this -> params['sizelimit'],
      'attronly' => $this -> params['attronly'],
      'attributes' => $this -> params['attributes']
    );

    // Pattern
    if (!is_null($this -> params['pattern'])) {
      $filter=$this ->getFilterFromPattern();
      if (is_null($retval['filter'])) {
        $retval['filter']=$filter;
      }
      else {
        $retval['filter']=LSldap::combineFilters('and',array($retval['filter'],$filter));
      }
    }

    // predefinedFilter
    if (is_string($this -> params['predefinedFilter'])) {
      if (!is_null($retval['filter'])) {
        $filter=LSldap::combineFilters('and',array($this -> params['predefinedFilter'],$retval['filter']));
        if ($filter) {
          $retval['filter']=$filter;
        }
      }
      else {
        $retval['filter']=$this -> params['predefinedFilter'];
      }
    }

    // Filter
    $objFilter=LSldapObject::_getObjectFilter($this -> LSobject);
    if ($objFilter) {
      if (!is_null($retval['filter'])) {
        $filter=LSldap::combineFilters('and',array($objFilter,$retval['filter']));
        if ($filter) {
          $retval['filter']=$filter;
        }
      }
      else {
        $retval['filter']=$objFilter;
      }
    }

    // Recursive
    if (is_null($retval['basedn'])) {
      if (!is_null($this -> params['subDn'])) {
        if ($this -> params['recursive']) {
          $retval['basedn'] = $this -> params['subDn'];
        }
        else {
          $retval['basedn'] = LSconfig::get("LSobjects.".$this -> LSobject.".container_dn").','.$this -> params['subDn'];
        }
      }
      else {
        if ($this -> params['recursive']) {
          $retval['basedn'] = LSsession :: getTopDn();
        }
        else {
          $retval['basedn'] = LSconfig::get("LSobjects.".$this -> LSobject.".container_dn").','.LSsession :: getTopDn();
        }
      }
    }
    if ($this -> params['recursive'] || !isset($retval['scope'])) {
      $retval['scope'] = 'sub';
    }

    if (is_null($this -> params['displayFormat'])) {
      $this -> params['displayFormat']=LSconfig::get("LSobjects.".$this -> LSobject.".display_name_format");
    }

    // Display Format
    $attrs=getFieldInFormat($this -> params['displayFormat']);
    if(is_array($retval['attributes'])) {
      $retval['attributes']=array_merge($attrs,$retval['attributes']);
    }
    else {
      $retval['attributes']=$attrs;
    }

    // Extra Columns
    if ($this -> params['extraDisplayedColumns'] && is_array($this -> config['extraDisplayedColumns'])) {
      foreach ($this -> config['extraDisplayedColumns'] as $id => $conf) {
        $attrs=array();
        if (isset($conf['LSformat'])) {
          $attrs=getFieldInFormat($conf['LSformat']);
          if (isset($conf['alternativeLSformats'])) {
            if(is_array($conf['alternativeLSformats'])) {
              foreach ($conf['alternativeLSformats'] as $format) {
                $attrs = array_merge($attrs, getFieldInFormat($format));
              }
            }
            else {
              $attrs = array_merge($attrs, getFieldInFormat($conf['alternativeLSformats']));
            }
          }
          if(isset($conf['formaterLSformat'])) {
            $attrs=array_unique(array_merge($attrs,getFieldInFormat($conf['formaterLSformat'])));
            if(($key = array_search('val', $attrs)) !== false) {
              unset($attrs[$key]);
            }
          }
        }
        if(isset($conf['additionalAttrs'])) {
          $attrs=array_unique(array_merge($attrs,(is_array($conf['additionalAttrs'])?$conf['additionalAttrs']:array($conf['additionalAttrs']))));
        }
        if(is_array($retval['attributes'])) {
          $retval['attributes']=array_merge($attrs,$retval['attributes']);
        }
        else {
          $retval['attributes']=$attrs;
        }
      }
    }

    if (is_array($retval['attributes'])) {
      $retval['attributes']=array_unique($retval['attributes']);
    }

    $this -> _searchParams = $retval;
  }

  /**
   * Format search parameters for logging
   *
   * @return string|null Formated search parameters is defined, or null if not
   **/
  private function formatSearchParams() {
    if (!$this -> _searchParams)
      return null;
    if ($this -> _searchParams['filter'] instanceof Net_LDAP2_Filter)
      $return = "filter=".$this -> _searchParams['filter']->asString();
    else
      $return = "without filter";
    $return .= ", on basedn '".$this -> _searchParams['basedn']."'";
    $return .= " (scope: ".($this -> _searchParams['scope']?$this -> _searchParams['scope']:'default');
    if ($this -> _searchParams['attronly'])
      $return .= ", attrs only)";
    else
      $return .= ", attrs: ".(
        (is_array($this -> _searchParams['attributes']) && $this -> _searchParams['attributes'])?
        implode(',', $this -> _searchParams['attributes']):
        'all'
      ).")";
    return $return;
  }

  /**
   * Get search attributes
   *
   * @return array The attributes asked in this search
   **/
  public function getAttributes() {
    if (!$this -> _searchParams)
      $this -> generateSearchParams();
    return $this -> _searchParams['attributes'];
  }

  /**
   * Run the search
   *
   * @param boolean $cache Define if the cache can be used
   *
   * @return boolean True on success or False
   */
  public function run($cache=true) {
    $this -> generateSearchParams();
    self :: log_debug($this." -> run(".($cache?'with cache':'without cache').")");

    if( $cache && (!isset($_REQUEST['refresh'])) && (!$this -> params['withoutCache']) ) {
      self :: log_debug($this.' -> run(): Cache enabled');
      $this -> result = $this -> getResultFromCache();
      if ($this -> result)
        self :: log_debug($this.' -> run(): result retrieved from cache');
      else
        self :: log_debug($this.' -> run(): result not found in cache');
    }
    else {
      self :: log_debug($this.' -> run(): Cache disabled');
      $this -> setParam('withoutCache', false);
    }

    if (!$this -> result) {
      $this -> result=array(
        'sortBy' => NULL,
        'sortDirection' => NULL
      );

      // Search in LDAP
      $list = LSldap :: search(
        $this -> _searchParams['filter'],
        $this -> _searchParams['basedn'],
        $this -> _searchParams
      );

      // Check result
      if ($list === false) {
        LSerror :: addErrorCode('LSsearch_12');
        return false;
      }

      // Handle onlyAccessible parameter
      if ($this -> getParam('onlyAccessible') && LSsession :: getLSuserObjectDn()) {
        self :: log_debug($this.' -> run(): Filter on only accessible object');
        $this -> result['list'] = array();

        // Check user rights on objets
        foreach($list as $id => $obj) {
          if (LSsession :: canAccess($this -> LSobject, $obj['dn'])) {
            $this -> result['list'][] = $obj;
          }
        }
      }
      else
        $this -> result['list'] = $list;
      $this -> addResultToCache();
    }

    self :: log_debug($this.' -> run(): '.$this -> total. " object(s) found");

    $this -> doSort();

    return true;
  }

  /**
   * Return an hash corresponding to the parameters of the search
   *
   * @param array $searchParams An optional search params array
   * @param boolean $onlyAccessible An optional onlyAccessible boolean flag
   *
   * @return string|false The hash of the parameters of the search, or false in case of error
   **/
  public function getHash($searchParams=null, $onlyAccessible=null) {
    if (is_null($searchParams)) {
      $searchParams = $this -> _searchParams;
    }
    if (!$searchParams)
      return false;
    if ($searchParams['filter'] instanceof Net_LDAP2_Filter) {
      $searchParams['filter'] = $searchParams['filter'] -> asString();
    }
    $to_hash = print_r($searchParams, true);
    if (is_null($onlyAccessible)) {
      $onlyAccessible = ($this -> getParam('onlyAccessible') && LSsession :: getLSuserObjectDn());
    }
    $to_hash .= '-onlyAccessible='.intval($onlyAccessible);
    return hash('md5', print_r($searchParams, true));
  }

  /**
   * Add the result of the search to cache of the session
   *
   * @return void
   **/
  public function addResultToCache() {
    if ($this -> cacheIsEnabled()) {
      $hash = $this->getHash();
      self :: log_trace("addResultToCache(): Save result in cache with hash '$hash'.");
      if (!isset($_SESSION['LSsession']))
        $_SESSION['LSsession'] = array();
      if (!isset($_SESSION['LSsession']['LSsearch']))
        $_SESSION['LSsession']['LSsearch'] = array();
      if (!isset($_SESSION['LSsession']['LSsearch'][$this -> LSobject]))
        $_SESSION['LSsession']['LSsearch'][$this -> LSobject] = array();
      if (!isset($_SESSION['LSsession']['LSsearch'][$this -> LSobject]['cache']))
        $_SESSION['LSsession']['LSsearch'][$this -> LSobject]['cache'] = array();
      $_SESSION['LSsession']['LSsearch'][$this -> LSobject]['cache'][$hash] = $this->result;
    }
    else
      self :: log_trace('addResultToCache(): cache is disabled.');
  }

  /**
   * Get the result of the search from cache of the session
   *
   * @return array | False The array of the result of the search or False
   **/
  private function getResultFromCache() {
    if ($this -> cacheIsEnabled()) {
      $hash = $this->getHash();
      if (
        isset($_SESSION['LSsession'])
        && isset($_SESSION['LSsession']['LSsearch'])
        && isset($_SESSION['LSsession']['LSsearch'][$this -> LSobject])
        && isset($_SESSION['LSsession']['LSsearch'][$this -> LSobject]['cache'])
        && isset($_SESSION['LSsession']['LSsearch'][$this -> LSobject]['cache'][$hash])
      ) {
        self :: log_trace('getResultFromCache(): result found in cache.');
        return $_SESSION['LSsession']['LSsearch'][$this -> LSobject]['cache'][$hash];
      }
      self :: log_trace('getResultFromCache(): result not found in cache.');
    }
    else
      self :: log_trace('getResultFromCache(): cache is disabled.');
    return false;
  }

  /**
   * Get page informations to display
   *
   * @param int $page The number of the page
   *
   * @return array|false The information of the page, or false in case of error
   **/
  public function getPage($page=1) {
    if (!LSsession::loadLSclass('LSsearchEntry')) {
      LSerror::addErrorCode('LSsession_05',$this -> LSobject);
      return false;
    }
    $page = (int)$page;
    if ($page < 1)
      $page = 1;

    $retval = array(
      'nb' => $page,
      'nbPages' => 1,
      'list' => array(),
      'total' => $this -> total
    );

    if ($retval['total'] > 0) {
      if (!$this->params['nbObjectsByPage']) {
        $this->params['nbObjectsByPage'] = NB_LSOBJECT_LIST;
      }
      $retval['nbPages'] = ceil($retval['total'] / $this->params['nbObjectsByPage']);

      $sortTable=$this -> getSortTable();

      $list = array_slice(
        $sortTable,
        (($page - 1) * $this->params['nbObjectsByPage']),
        $this->params['nbObjectsByPage']
      );

      foreach ($list as $key => $id) {
        $retval['list'][] = new LSsearchEntry(
          $this,
          $this -> LSobject,
          $this -> params,
          $this -> result['list'],
          $id
        );
      }
    }
    return $retval;
  }

  /**
   * Get search entries
   *
   * @return array|false The entries, or false in case of error
   **/
  public function getSearchEntries() {
    if (!LSsession::loadLSclass('LSsearchEntry')) {
      LSerror::addErrorCode('LSsession_05',$this -> LSobject);
      return false;
    }
    $retval = array();
    if ($this -> total>0) {
      $sortTable=$this -> getSortTable();

      foreach ($sortTable as $key => $id) {
        $retval[] = new LSsearchEntry(
          $this,
          $this -> LSobject,
          $this -> params,
          $this -> result['list'],
          $id
        );
      }
    }
    return $retval;
  }

  /**
   * Access to information of this object
   *
   * @param string $key The key of the info
   *
   * @return mixed The info
   **/
  public function __get($key) {
    $params = array (
      'basedn',
      'sortBy',
      'sortDirection',
      'attributes',
    );
    if ($key=='LSobject') {
      return $this -> LSobject;
    }
    elseif (in_array($key,$params)) {
      return $this -> params[$key];
    }
    elseif ($key=='label_objectName') {
      return LSldapObject::getLabel($this -> LSobject);
    }
    elseif ($key=='label_level') {
      return LSsession :: getSubDnLabel();
    }
    elseif ($key=='label_actions') {
      return _('Actions');
    }
    elseif ($key=='label_no_result') {
      return _("This search didn't get any result.");
    }
    elseif ($key=='sort') {
      if (isset($this -> params['sortlimit']) && ($this -> params['sortlimit']>0)) {
        return ($this -> total < $this -> params['sortlimit']);
      }
      return true;
    }
    elseif ($key=='sortlimit') {
      return $this -> params['sortlimit'];
    }
    elseif ($key=='total') {
      return count($this -> result['list']);
    }
    elseif ($key=='label_total') {
      return $this -> total." ".$this -> label_objectName;
    }
    elseif ($key=='displaySubDn') {
      if (LSsession :: subDnIsEnabled()) {
        if (!is_null($this -> params[$key])) {
          return $this -> params[$key];
        }
        else {
          return (! LSsession :: isSubDnLSobject($this -> LSobject) );
        }
      }
      return false;
    }
    elseif ($key=='canCopy') {
      if (!is_null($this -> _canCopy))
        return $this -> _canCopy;
      $this -> _canCopy = LSsession :: canCreate($this -> LSobject);
      return $this -> _canCopy;
    }
    elseif ($key=='predefinedFilters') {
      $retval=array();
      if (is_array($this -> config['predefinedFilters'])) {
        foreach($this -> config['predefinedFilters'] as $filter => $label) {
          $retval[$filter]=__($label);
        }
      }
      return $retval;
    }
    elseif ($key=='extraDisplayedColumns') {
      if ($this->params['extraDisplayedColumns'] && is_array($this -> config['extraDisplayedColumns'])) {
        return $this -> config['extraDisplayedColumns'];
      }
      else {
        return False;
      }
    }
    elseif ($key=='visibleExtraDisplayedColumns') {
      if ($this->params['extraDisplayedColumns'] && is_array($this -> config['extraDisplayedColumns'])) {
        $ret=array();
        foreach($this->config['extraDisplayedColumns'] as $col => $conf) {
          if (isset($conf['visibleTo']) && !LSsession :: isLSprofiles($this -> basedn, $conf['visibleTo'])) {
            continue;
          }
          $ret[$col]=$conf;
        }
        return $ret;
      }
    }
    elseif ($key == 'hash') {
      return $this -> getHash();
    }
    else {
      throw new Exception("Incorrect property '$key'!");
    }
  }

  /**
   * Function use with uasort to sort two entry
   *
   * @param LSsearchEntry &$a One entry of result
   * @param LSsearchEntry &$b One entry of result
   *
   * @return int Value for uasort
   **/
  private function _sortTwoEntry(&$a,&$b) {
    $sortBy = $this -> params['sortBy'];
    $sortDirection = $this -> params['sortDirection'];
    if ($sortDirection=='ASC') {
      $dir = 1;
    }
    else {
      $dir = -1;
    }
    $oa = new LSsearchEntry($this, $this -> LSobject, $this -> params, $this -> result['list'], $a);
    $va = $oa->$sortBy;
    $ob = new LSsearchEntry($this, $this -> LSobject, $this -> params, $this -> result['list'], $b);
    $vb = $ob->$sortBy;

    if ($va == $vb) return 0;

    $val = strnatcmp(strtolower($va), strtolower($vb));
    return $val*$dir;
  }

  /**
   * Function to run after using the result. It's update the cache
   *
   * IT'S FUNCTION IS VERY IMPORTANT !!!
   *
   * @return void
   **/
  public function afterUsingResult() {
    $this -> addResultToCache();
  }

  /**
   * Redirect user to object view if the search have only one result
   *
   * @return boolean True only if user have been redirected
   **/
  public function redirectWhenOnlyOneResult() {
    if ($this -> total == 1 && $this -> result && self::formIsSubmited()) {
      LSurl :: redirect('object/'.$this -> LSobject.'/'.urlencode($this -> result['list'][0]['dn']));
      return true;
    }
    return false;
  }

  /**
   * Run the sort if it's enabled and if the result is not in the cache
   *
   * @return boolean True on success or false
   **/
  private function doSort() {
    if (!$this -> sort) {
      self :: log_debug('doSort(): sort is disabled');
      return true;
    }
    if (is_null($this -> params['sortBy'])) {
      return false;
    }
    if (is_null($this -> params['sortDirection'])) {
      $this -> params['sortDirection']='ASC';
    }

    if ($this->total==0) {
      return true;
    }

    if (isset($this -> result['sort'][$this -> params['sortBy']][$this -> params['sortDirection']])) {
      self :: log_debug('doSort(): from cache');
      return true;
    }

    self :: log_debug(
      'doSort(): sort by "'.$this -> params['sortBy'].
      '" (order: '.$this -> params['sortDirection'].")");

    $this -> result['sort'][$this -> params['sortBy']][$this -> params['sortDirection']]=range(0,($this -> total-1));

    if (!LSsession :: loadLSClass('LSsearchEntry')) {
      LSerror::addErrorCode('LSsession_05','LSsearchEntry');
      return false;
    }

    if (!uasort(
      $this -> result['sort'][$this -> params['sortBy']][$this -> params['sortDirection']],
      array($this,'_sortTwoEntry')
    )) {
      LSerror :: addErrorCode('LSsearch_13');
      return false;
    }

    return true;
  }

  /**
   * Returns the id of table rows in the result sorted according to criteria
   * defined in the parameters
   *
   * @return array The Table of id lines of results sorted
   **/
  public function getSortTable() {
    if (isset($this -> result['sort'][$this -> params['sortBy']][$this -> params['sortDirection']])) {
      return $this -> result['sort'][$this -> params['sortBy']][$this -> params['sortDirection']];
    }
    return range(0,($this -> total-1));
  }

  /**
   * List LSsearchEntry objects
   *
   * @return array|false DN associate with name, or false in case of error
   **/
  public function listEntries() {
    if (!LSsession::loadLSclass('LSsearchEntry')) {
      LSerror::addErrorCode('LSsession_05',$this -> LSobject);
      return false;
    }

    $retval=array();

    if ($this -> total>0) {
      $sortTable = $this -> getSortTable();

      foreach ($sortTable as $key => $id) {
        $entry = new LSsearchEntry(
          $this,
          $this -> LSobject,
          $this -> params,
          $this -> result['list'],
          $id
        );
        $retval[$entry -> dn] = $entry;
      }
    }

    return $retval;
  }

  /**
   * List objects name
   *
   * @return array|false DN associate with name, or false in case of error
   **/
  public function listObjectsName() {
    $entries = $this -> listEntries();
    if (!is_array($entries))
      return false;
    $retval = array();
    foreach($entries as $entry)
      $retval[$entry -> dn] = $entry -> displayName;
    return $retval;
  }

  /**
   * List LSldapObjects
   *
   * @return array of LSldapObjects
   **/
  public function listObjects() {
    $retval=array();

    if ($this -> total>0) {
      $sortTable=$this -> getSortTable();

      $c=0;
      foreach ($sortTable as $key => $id) {
        $retval[$c]=new $this -> LSobject();
        $retval[$c] -> loadData($this -> result['list'][$id]['dn']);
        $c++;
      }
    }

    return $retval;
  }

  /**
   * List objects dn
   *
   * @return array of DN
   **/
  public function listObjectsDn() {
    $retval=array();

    if ($this -> total>0) {
      $sortTable=$this -> getSortTable();

      $c=0;
      foreach ($sortTable as $key => $id) {
        $retval[$c] = $this -> result['list'][$id]['dn'];
        $c++;
      }
    }

    return $retval;
  }

  /**
   * CLI search command
   *
   * @param array $command_args Command arguments :
   *   - Positional arguments :
   *     - LSobject type
   *     - patterns
   *   - Optional arguments :
   *     - -f|--filter : LDAP filter string
   *     - -b|--basedn : LDAP base DN
   *     - -s|--scope : LDAP search scope (sub, one, base)
   *     - -l|--limit : search result size limit
   *     - -a|--approx : approximative search on provided pattern
   *     - -r|--recursive : recursive search
   *     - --sort-by : Sort by specific attribute/column
   *     - -R|--reverse : reverse search result
   *     - --sort-limit : Sort limit (in number of objects found)
   *     - --display-subdn : Display subDn in result
   *     - --display-format : Display format of objectName
   *     - -N|--nb-obj-by-page : number of object by page
   *     - -W|--without-cache : Disable cache
   *     - -e|--extra-columns : Display extra columns
   *     - -p|--page : page number to show (starting by 1, default: first one)
   *
   * @return boolean True on success, false otherwise
   **/
  public static function cli_search($command_args) {
    $objType = null;
    $patterns = array();
    $params = array(
      'sortDirection' => 'ASC',
      'extraDisplayedColumns' => false,
    );
    $page_nb = 1;
    $all = false;
    $json = false;
    $pretty = false;
    for ($i=0; $i < count($command_args); $i++) {
      switch ($command_args[$i]) {
        case '-f':
        case '--filter':
          $params['filter'] = $command_args[++$i];
          break;
        case '-b':
        case '--basedn':
          $params['basedn'] = $command_args[++$i];
          break;
        case '-s':
        case '--scope':
          $params['scope'] = $command_args[++$i];
          break;
        case '-s':
        case '--scope':
          $params['scope'] = $command_args[++$i];
          break;
        case '-l':
        case '--limit':
          $params['sizelimit'] = intval($command_args[++$i]);
          break;
        case '-a':
        case '--approx':
          $params['approx'] = true;
          break;
        case '-r':
        case '--recursive':
          $params['recursive'] = true;
          break;
        case '--sort-by':
          $params['sortBy'] = $command_args[++$i];
          break;
        case '-R':
        case '--reverse':
          $params['sortDirection'] = 'DESC';
          break;
        case '--sort-limit':
          $params['sortlimit'] = intval($command_args[++$i]);
          break;
        case '--sort-limit':
          $params['sortlimit'] = intval($command_args[++$i]);
          break;
        case '--display-subdn':
          $params['displaySubDn'] = true;
          break;
        case '--display-format':
          $params['displayFormat'] = boolval($command_args[++$i]);
          break;
        case '-N':
        case '--nb-obj-by-page':
          $params['nbObjectsByPage'] = intval($command_args[++$i]);
          break;
        case '-W':
        case '--without-cache':
          $params['withoutCache'] = True;
          break;
        case '-e':
        case '--extra-columns':
          $params['extraDisplayedColumns'] = True;
          break;
        case '-p':
        case '--page':
          $page_nb = intval($command_args[++$i]);
          break;
        case '--all':
          $all = true;
          break;
        case '-j':
        case '--json':
          $json = true;
          break;
        case '--pretty':
          $pretty = true;
          break;
        default:
          if (is_null($objType)) {
            $objType = $command_args[$i];
          }
          elseif (substr($command_args[$i], 0, 1) == '-') {
            LScli :: usage("Invalid parameter '".$command_args[$i]."'");
          }
          else {
            $patterns[] = $command_args[$i];
          }
      }
    }

    if (is_null($objType))
      LScli :: usage('You must provide LSobject type.');

    // Load Console Table lib
    $console_table_path = LSconfig :: get('ConsoleTable', 'Console/Table.php', 'string');
    if (!LSsession :: includeFile($console_table_path, true))
      self :: log_fatal('Fail to load ConsoleTable library.');

    if (!empty($patterns))
      $params['pattern'] = implode(' ', $patterns);

    $search = new LSsearch($objType, 'CLI', array(), true);

    // Set search params
    self :: log_debug('Search parameters : '.varDump($params));
    if (!$search -> setParams($params))
      self :: log_fatal('Fail to set search parameters.');

    // Run search
    if (!$search -> run())
      self :: log_fatal('Fail to run search.');


    if ($all) {
      $entries = $search -> listEntries();
      if (!is_array($entries))
        self :: log_fatal("Fail to retrieve search result");
    }
    else {
      // Retrieve page
      $page = $search -> getPage($page_nb);
      /*
       * $page = array(
       *   'nb' => $page,
       *   'nbPages' => 1,
       *   'list' => array(),
       *   'total' => $this -> total
       * );
       */

      // Check page
      if (!is_array($page) || $page_nb > $page['nbPages'])
        self :: log_fatal("Fail to retrieve page #$page_nb.");
    }

    // Handle JSON output
    if ($json) {
      $export = array(
        'objects' => array(),
        'total' => $search -> total,
      );
      if (!$all) {
        $export['page'] = $page['nb'];
        $export['nbPages'] = $page['nbPages'];
      }
      foreach(($all?$entries:$page['list']) as $obj) {
        $export['objects'][$obj -> dn] = array(
          'name' => $obj -> displayName,
        );
        if ($search -> displaySubDn)
          $export['objects'][$obj -> dn][$search -> label_level] = $obj -> subDn;
        if ($search -> extraDisplayedColumns) {
          foreach ($search -> visibleExtraDisplayedColumns as $cid => $conf) {
            $export['objects'][$obj -> dn][$conf['label']] = $obj -> $cid;
          }
        }
      }
      $output = json_encode($export, ($pretty?JSON_PRETTY_PRINT:0));
      if ($output === false) {
        self :: log_error("Fail to encode data as JSON");
        exit(1);
      }
      echo $output;
      exit(0);
    }

    if (empty($all?$entries:$page['list'])) {
      echo "No $objType object found.\n";
      exit(0);
    }

    // Create result table with its header
    $tbl = new Console_Table();
    $headers = array('DN', 'Name');
    if ($search -> displaySubDn)
      $headers[] = $search -> label_level;
    if ($search -> extraDisplayedColumns) {
      foreach ($search -> visibleExtraDisplayedColumns as $cid => $conf) {
        $headers[] = $conf['label'];
      }
    }
    $tbl->setHeaders($headers);

    // Add one line for each object found (in page)
    foreach($all?$entries:$page['list'] as $obj) {
      $row = array(
        $obj -> dn,
        $obj -> displayName,
      );
      if ($search -> displaySubDn)
        $row[] = $obj -> subDn;
      if ($search -> extraDisplayedColumns) {
        foreach ($search -> visibleExtraDisplayedColumns as $cid => $conf) {
          $row[] = $obj -> $cid;
        }
      }
      $tbl->addRow($row);
    }
    echo $tbl->getTable();
    if ($all)
      echo "Total: ".$search -> total."\n";
    else
      echo "Page ".($page['nb'])." on ".$page['nbPages']." / Total: ".$search -> total."\n";

    return true;
  }

  /**
   * Args autocompleter for CLI command search
   *
   * @param array<string> $command_args List of already typed words of the command
   * @param int $comp_word_num The command word number to autocomplete
   * @param string $comp_word The command word to autocomplete
   * @param array<string> $opts List of global available options
   *
   * @return array<string> List of available options for the word to autocomplete
   **/
  public static function cli_search_args_autocompleter($command_args, $comp_word_num, $comp_word, $opts) {
    $command_opts = array (
      '-f', '--filter',
      '-b', '--basedn',
      '-s', '--scope',
      '-l', '--limit',
      '-a', '--approx',
      '-r', '--recursive',
      '--sort-by',
      '-R', '--reverse',
      '--sort-limit',
      '--display-subdn',
      '--display-format',
      '-N', '--nb-obj-by-page',
      '-W', '--without-cache',
      '-e', '--extra-columns',
      '-p', '--page',
      '--all',
      '-j', '--json', '--pretty',
    );

    // Detect positional args
    $objType = null;
    $objType_arg_num = null;
    $patterns = array();
    $extra_columns = false;
    for ($i=0; $i < count($command_args); $i++) {
      if (!in_array($command_args[$i], $command_opts) || in_array($command_args[$i], $opts)) {
        // If object type not defined
        if (is_null($objType)) {
          // Check object type exists
          $objTypes = LScli :: autocomplete_LSobject_types($command_args[$i]);

          // Load it if exist and not trying to complete it
          if (in_array($command_args[$i], $objTypes) && $i != $comp_word_num) {
            LSsession :: loadLSobject($command_args[$i], false);
          }

          // Defined it
          $objType = $command_args[$i];
          $objType_arg_num = $i;
        }
        else
          $patterns[] = $command_args[$i];
      }
      else {
        switch ($command_args[$i]) {
          case '-s':
          case '--scope':
          case '-f':
          case '--filter':
          case '-b':
          case '--basedn':
          case '-l':
          case '--limit':
          case '--sort-limit':
          case '-N':
          case '--nb-obj-by-page':
          case '-p':
          case '--page':
          case '--sort-by':
            // This args accept options => increase $i
            $i++;
            break;
          case '-e':
          case '--extra-columns':
            $extra_columns = true;
            LSlog :: debug('Extra columns enabled');
            break;
        }
      }
    }

    // Handle completion of args value
    LSlog :: debug("Last complete word = '".$command_args[$comp_word_num-1]."'");
    switch ($command_args[$comp_word_num-1]) {
      case '-s':
      case '--scope':
        return LScli :: autocomplete_opts(array('sub', 'one', 'base'), $comp_word);
      case '-f':
      case '--filter':
      case '-b':
      case '--basedn':
        // This args need string value that can't be autocomplete: stop autocompletion
        return array();
      case '-l':
      case '--limit':
      case '--sort-limit':
      case '-N':
      case '--nb-obj-by-page':
      case '-p':
      case '--page':
        return LScli :: autocomplete_int($comp_word);
      case '--sort-by':
        $bys = array('displayName', 'subDn');
        if ($objType && $extra_columns) {
          $extraDisplayedColumns = LSconfig::get("LSobjects.$objType.LSsearch.extraDisplayedColumns", array());
          if (is_array($extraDisplayedColumns))
            $bys = array_merge($bys, array_keys($extraDisplayedColumns));
        }
        LSlog :: debug('Available sort-bys clauses: '.implode(', ', $bys));
        return LScli :: autocomplete_opts($bys, $comp_word);
    }
    $opts = array_merge($opts, $command_opts);

    // If objType not already choiced (or currently autocomplete), add LSobject types to available options
    if (!$objType || $objType_arg_num == $comp_word_num)
      $opts = array_merge($opts, LScli :: autocomplete_LSobject_types($comp_word));

    return LScli :: autocomplete_opts($opts, $comp_word);
  }
}

/**
 * Error Codes
 **/
LSerror :: defineError('LSsearch_01',
___("LSsearch : Invalid filter : %{filter}.")
);
LSerror :: defineError('LSsearch_02',
___("LSsearch : Invalid basedn (%{basedn}).")
);
LSerror :: defineError('LSsearch_03',
___("LSsearch : Invalid value for %{param} parameter.")
);
LSerror :: defineError('LSsearch_04',
___("LSsearch : Invalid size limit. Must be an integer greater or equal to 0.")
);
LSerror :: defineError('LSsearch_05',
___("LSsearch : Invalid parameter %{param}. Must be an boolean.")
);
LSerror :: defineError('LSsearch_06',
___("LSsearch : Invalid parameter attributes. Must be an string or an array of strings.")
);
LSerror :: defineError('LSsearch_07',
___("LSsearch : Can't build attributes list for make filter.")
);
LSerror :: defineError('LSsearch_08',
___("LSsearch : Error building filter with attribute '%{attr}' and pattern '%{pattern}'")
);
LSerror :: defineError('LSsearch_09',
___("LSsearch : Error combining filters.")
);
LSerror :: defineError('LSsearch_10',
___("LSsearch : Invalid pattern.")
);
LSerror :: defineError('LSsearch_11',
___("LSsearch : Invalid attribute %{attr} in parameters.")
);
LSerror :: defineError('LSsearch_12',
___("LSsearch : Error during the search.")
);
LSerror :: defineError('LSsearch_13',
___("LSsearch : Error sorting the search.")
);
LSerror :: defineError('LSsearch_14',
___("LSsearch : The function of the custum information %{name} is not callable.")
);
LSerror :: defineError('LSsearch_15',
___("LSsearch : Invalid predefinedFilter for LSobject type %{type} : %{label} (filter : %{filter}).")
);
LSerror :: defineError('LSsearch_16',
___("LSsearch : Error during execution of the custom action %{customAction}.")
);
LSerror :: defineError('LSsearch_17',
___("LSsearch : Invalid search pattern.")
);

// LScli
LScli :: add_command(
    'search',
    array('LSsearch', 'cli_search'),
    'Search LSobject',
    '[object type] [pattern1] [pattern2 ...]',
    array(
    '   - Positional arguments :',
    '     - LSobject type',
    '     - patterns',
    '',
    '   - Optional arguments :',
    '     - -f|--filter : LDAP filter string',
    '     - -b|--basedn : LDAP base DN',
    '     - -s|--scope : LDAP search scope (sub, one, base)',
    '     - -l|--limit : search result size limit',
    '     - -a|--approx : approximative search on provided pattern',
    '     - -r|--recursive : recursive search',
    '     - --sort-by : Sort by specific attribute/column',
    '     - -R|--reverse : reverse search result',
    '     - --sort-limit : Sort limit (in number of objects found)',
    '     - --display-subdn : Display subDn in result',
    '     - --display-format : Display format of objectName',
    '     - -N|--nb-obj-by-page : number of object by page',
    '     - -W|--without-cache : Disable cache',
    '     - -e|--extra-columns : Display extra columns',
    '     - -p|--page : page number to show (starting by 1, default: first one)',
    '     - --all : list all matching objects (no pagination)',
    '     - -j|--json : JSON output',
    '     - --pretty : prettify the JSON output',
  ),
  true,
  array('LSsearch', 'cli_search_args_autocompleter')
);
