<?php abstract class jqGrid { /** @var $DB jqGrid_DB */ protected $DB; /** @var $Loader jqGridLoader */ protected $Loader; protected $grid_id; protected $base_url; protected $input; protected $page; protected $limit; protected $sidx; protected $sord; protected $out = 'json'; protected $json_mode; protected $offset; protected $count; protected $total; protected $where = array(); protected $where_glue = ' AND '; protected $where_empty = 'true'; protected $table; protected $query; protected $query_agg; protected $primary_key = array(); protected $primary_key_glue = '_'; protected $primary_key_auto_increment = null; protected $do_agg = true; protected $do_search = true; protected $do_search_advanced = true; protected $do_sort = true; protected $do_limit = true; protected $treegrid = false; //'adjacency' or 'nested' protected $options = array(); protected $nav = array(); protected $cols = array(); protected $rows = array(); protected $agg = array(); protected $userdata = array(); protected $debug = array(); protected $response = array(); #Local column default protected $cols_default = array(); #Base defaults protected $default = array( 'cols' => array( 'label' => '', 'db' => '', 'db_agg' => '', 'unset' => false, 'replace' => null, 'formatter' => null, 'manual' => false, 'hidden' => false, 'editable' => false, 'search' => true, 'search_op' => 'auto', 'classes' => '', 'align' => 'left', 'null' => null, 'encode' => true, ), 'nav' => array( 'add' => false, 'edit' => false, 'del' => false, 'refresh' => true, 'search' => false, 'view' => false, ), 'options' => array( 'datatype' => 'json', 'mtype' => 'POST', 'loadui' => 'block', 'prmNames' => array( 'id' => '_id', ), ), 'reserved_col_names' => array('page', 'sidx', 'sord', 'nd', 'oper', 'filters', 'rd'), ); protected $reserved_col_names; protected $internal_col_prop = array('db', 'db_agg', 'unset', 'manual', 'search_op'); protected $query_placeholders = array('fields' => '{fields}', 'where' => '{where}'); protected $render_data = array(); protected $render_extend = 'opts'; protected $render_suffix_col = null; protected $render_filter_toolbar = false; /** * Class constructor, initializes basic properties * * @param jqGridLoader $loader */ public function __construct(jqGridLoader $loader) { //------------------ // Globals //------------------ $this->grid_id = get_class($this); $this->Loader = $loader; $this->input = $this->getInput(); $this->DB = $loader->loadDB(); //---------------- // Init //---------------- $this->beforeInit(); $this->init(); $this->afterInit(); //---------------- // Primary key //---------------- reset($this->cols); $this->primary_key = $this->primary_key ? (array)$this->primary_key : array(key($this->cols)); if(is_null($this->primary_key_auto_increment)) { $this->primary_key_auto_increment = count($this->primary_key) == 1; } //---------------- // Prepare columns //---------------- $this->reserved_col_names = $this->getReservedColNames(); foreach($this->cols as $k => &$c) { $c = $this->initColumn($k, $c); } } /** * Abstract function for setting grid properties * * @abstract * @return void */ abstract protected function init(); /** * MAIN ACTION (1): Output data * * @throws jqGrid_Exception * @return void */ public function output() { //---------------- // Setup essential vars //---------------- $this->getOutputParams(); //---------------- // Input to search //---------------- if($this->do_search) { $this->search(); } if($this->do_search_advanced) { $this->searchAdvanced(); } //---------------- // Try to guess the basic query //---------------- if(!$this->query) { $this->query = $this->getDefaultQuery(); } //---------------- // Get agg data //---------------- if($this->do_agg) { $this->getDataAgg(); } //---------------- // Get rows data //---------------- $this->getDataRows(); //---------------- // Build userdata //---------------- $this->userdata = $this->getDataUser($this->userdata); //---------------- // Set count automatically //---------------- if(is_null($this->count)) { $this->setRowCount(); } //---------------- // Do output //---------------- $callback = array($this, jqGrid_Utils::uscore2camel('out', $this->out)); if(!is_callable($callback)) { throw new jqGrid_Exception("Output type '{$this->out}' is not defined"); } call_user_func($callback); } /** * MAIN ACTION (2): Perform operation to change data is any way * * @param $oper - operation name * @return void */ public function oper($oper) { $id = $this->input('_id'); $oper = strval($oper); switch($oper) { case 'add': $data = array_intersect_key($this->input, $this->cols); $data = $this->operData($data); $id = $this->opAdd($data); #Not auto increment -> build new_id from data if(empty($this->primary_key_auto_increment)) { $id = $this->implodePrimaryKey($data); } $this->response['new_id'] = $id; $this->operAfterAddEdit($id); break; case 'edit': $data = array_intersect_key($this->input, $this->cols); $data = $this->operData($data); $this->opEdit($id, $data); $this->operAfterAddEdit($id); break; case 'del': $this->opDel($id); break; default: $callback = array($this, jqGrid_Utils::uscore2camel('op', $oper)); if(is_callable($callback)) { call_user_func($callback); } else { throw new jqGrid_Exception("Oper $oper is not defined"); } break; } $this->response = array_merge(array('success' => 1), $this->response); $this->operComplete($oper); //---------------- // Output result //---------------- $this->json($this->response); } /** * MAIN ACTION (3): Render grid * * $jq_loader->render('jq_example'); * * @param array $render_data * @return string */ public function render($render_data = array()) { if(!is_array($render_data)) { throw new jqGrid_Exception_Render('Render data must be an array'); } $this->render_data = $render_data; //------------------ // Basic data //------------------ $data = array(); $data['extend'] = $this->render_extend; $data['suffix'] = $this->renderGridSuffix($render_data); //------------------ // Render ids //------------------ $data['id'] = $this->grid_id . $data['suffix']; $data['pager_id'] = $this->grid_id . $data['suffix'] . '_p'; //----------------- // Render colModel //----------------- foreach($this->cols as $k => $c) { if(isset($c['unset']) and $c['unset']) continue; #Remove internal column properties $c = array_diff_key($c, array_flip($this->internal_col_prop)); $colModel[] = $this->renderColumn($c); } //----------------- // Render options //----------------- $options = array( 'colModel' => $colModel, 'pager' => '#' . $data['pager_id'], ); #URL's $options['url'] = $options['editurl'] = $options['cellurl'] = $this->renderGridUrl(); #Any postData? if($post_data = $this->renderPostData()) { $options['postData'] = $post_data; } $data['options'] = $this->renderOptions(array_merge($this->default['options'], $options, $this->options)); //----------------- // Render navigator //----------------- if(is_array($this->nav)) { $data['nav'] = $this->renderNav(array_merge($this->default['nav'], $this->nav)); } //------------------ // Render base html //------------------ $data['html'] = $this->renderHtml($data); //----------------- // Compile the final string //----------------- return $this->renderComplete($data); } /** * All exceptions comes here * Override this method for custom exception handling * * @param jqGrid_Exception $e * @return mixed */ public function catchException(jqGrid_Exception $e) { #More output types will be added switch($e->getOutputType()) { case 'json': $r = array( 'error' => 1, 'error_msg' => $e->getMessage(), 'error_code' => $e->getCode(), 'error_data' => $e->getData(), 'error_type' => $e->getExceptionType(), ); if($this->Loader->get('debug_output')) { $r['error_string'] = (string)$e; } else { if($e instanceof jqGrid_Exception_DB) { unset($r['error_data']['query']); } } $this->json($r); break; case 'trigger_error': trigger_error($e->getMessage(), E_USER_ERROR); break; } return $e; } /** * (Output) Add new row to result set * And also do related stuff * * @param array $row - raw data from database * @return void */ protected function addRow($row) { #Allow modification of result row without annoying overloading of 'addRow' $row = $this->parseRow($row); $id = $this->implodePrimaryKey($row); $cell = array(); //---------------- // Fill cells //---------------- foreach($this->cols as $k => $c) { #Discard this column in output? if($c['unset']) continue; #Parse each cell $cell[] = $this->addRowCell($c, $row); } #Prase whole row $cell = $this->addRowComplete($id, $cell, $row); $this->rows[] = array('id' => $id, 'cell' => $cell); } /** * (Output) Populate cell * * @param array $c - column options * @param array $row - data row * @return mixed - final cell value */ protected function addRowCell($c, $row) { $val = isset($row[$c['name']]) ? $row[$c['name']] : null; #Handle nulls if($val === null and $c['null'] !== null) $val = $c['null']; #Easy replace values if($c['replace']) $val = isset($c['replace'][$val]) ? $c['replace'][$val] : $val; #Encode before output if($c['encode']) $val = $this->outputEncodeValue($c, $val); return $val; } /** * (Output) Per-row result modification * * @param integer $id row_id * @param array $cell - parsed cells * @param array $row - data row * @return array - modified $cell */ protected function addRowComplete($id, $cell, $row) { //---------------- // TreeGrid handling //---------------- if($this->treegrid == 'adjacency') { $cell[] = $row['level']; $cell[] = $row['parent']; $cell[] = $row['isLeaf']; $cell[] = $row['expanded']; $cell[] = isset($row['loaded']) ? $row['loaded'] : false; } if($this->treegrid == 'nested') { $cell[] = $row['level']; $cell[] = $row['lft']; $cell[] = $row['rgt']; $cell[] = $row['isLeaf']; $cell[] = $row['expanded']; $cell[] = isset($row['loaded']) ? $row['loaded'] : false; } //---------------- // Send all the vars beginning with '_' to userdata! //---------------- foreach($row as $k => $v) { if(strpos($k, '_') === 0) { $this->userdata[$k][$id] = $v; } } return $cell; } /** * Custom actions after 'init', but before columns init * You may also overload __construct to add something in the very end */ protected function afterInit() { //empty } /** * Custom actions before 'init' * Place your singletons init here */ protected function beforeInit() { //empty } //----------------- // Query builder //----------------- /** * (Output) Build complete AGG-query * * @param string $q - base query * @return string - final query */ protected function buildQueryAgg($q) { //----------------- // Placeholders //----------------- $replace = array( $this->query_placeholders['where'] => $this->buildWhere($this->where, $this->where_glue, 'agg'), $this->query_placeholders['fields'] => $this->buildFieldsAgg($this->cols), ); $q = strtr($q, $replace); return $q; } /** * (Output) Build complete ROWS-query * * @param string $q - base query * @return string - final query */ protected function buildQueryRows($q) { //----------------- // Placeholders //----------------- $replace = array( $this->query_placeholders['where'] => $this->buildWhere($this->where, $this->where_glue, 'rows'), $this->query_placeholders['fields'] => $this->buildFields($this->cols), ); $q = strtr($q, $replace); //----------------- // ORDER BY, LIMIT, OFFSET //----------------- if($this->do_sort) $q .= $this->buildOrderBy($this->sidx, $this->sord) . "\n"; if($this->do_limit) $q .= $this->buildLimitOffset($this->limit, $this->page) . "\n"; return $q; } /** * (Output) Implode {fields} for ROWS-query using 'db' property * * @param array $cols - grid columns * @return string - imploded list */ protected function buildFields($cols) { $fields = array(); foreach($cols as $k => &$c) { if($c['manual']) continue; $fields[] = ($k == $c['db']) ? $c['db'] : ($c['db'] . ' AS ' . $k); } return implode(', ', $fields); } /** * (Output) Implode {fields} for AGG-query using 'db_agg' property * * @param array $cols - grid columns * @return string - imploded list */ protected function buildFieldsAgg($cols) { #Count should always be here! $fields = array('count(*) AS _count'); foreach($cols as $k => $c) { if(!$c['db_agg']) continue; switch($c['db_agg']) { #Common case 'sum': case 'avg': case 'count': case 'min': case 'max': $fields[] = $c['db_agg'] . '(' . $c['db'] . ') AS ' . $k; break; #Custom default: $fields[] = $c['db_agg'] . ' AS ' . $k; break; } } return implode(', ', $fields); } /** * (Output) Builds paging for ROWS-query * LIMIT x OFFSET y * * Alter this if your database requires other syntax * * @param integer $limit * @param integer $page * @return string */ protected function buildLimitOffset($limit, $page) { $limit = intval($limit); $page = intval($page); if($limit > 0 and $page > 0) { $offset = ($page * $limit) - $limit; return "LIMIT $limit OFFSET $offset"; } return ''; } /** * (Output) Builds sorting for ROWS-query * Overload it to introduce more complex * * Input is checked in 'getOutputParams', so we trust it * * @param string $sidx - field name * @param string $sord - order (asc, desc) * @return string */ protected function buildOrderBy($sidx, $sord) { if($sidx and $sord) { return "ORDER BY $sidx $sord"; } return ''; } /** * (Output) Builds {where} both for ROWS and AGG queries * * @param array $where - array with conditions * @param string $glue - glue string (' AND ', ' OR ') * @param string $type - query type ('agg', 'rows') * @return string - imploded condition string */ protected function buildWhere($where, $glue, $type) { return $where ? implode($glue, $where) : $this->where_empty; } /** * (Output) Generate default query if only $this->table was set * * @return string */ protected function getDefaultQuery() { if(empty($this->table)) { return ''; } return " SELECT {fields} FROM {$this->table} WHERE {where} "; } /** * (Output) Get and validate params needed for output */ protected function getOutputParams() { $this->page = max(1, intval($this->input('page', 1))); $this->limit = max(-1, intval($this->input['rows'])); $this->sidx = $this->input('sidx') ? jqGrid_Utils::checkAlphanum($this->input('sidx')) : $this->primary_key[0]; $this->sord = in_array($this->input('sord'), array('asc', 'desc')) ? $this->input('sord') : 'asc'; $this->out = $this->input('_out', 'json'); } /** * (Output) Get AGG data - over the whole result set * Save it to $this->agg */ protected function getDataAgg() { $query = $this->buildQueryAgg($this->query_agg ? $this->query_agg : $this->query); $result = $this->DB->query($query); $this->agg = $this->DB->fetch($result); $this->debug['query_agg'] = $query; } /** * (Output) Get ROWS data - the current page * Save it to $this->rows via 'addRow' */ protected function getDataRows() { $query = $this->buildQueryRows($this->query); $result = $this->DB->query($query); while($r = $this->DB->fetch($result)) { $this->addRow($r); } $this->debug['query_rows'] = $query; } /** * (Output) Build 'userdata' * * @param array $userdata - orignal value * @return array - final value */ protected function getDataUser($userdata) { if($this->agg) { $userdata['agg'] = $this->agg; } return $userdata; } /** * It is the ONLY entry-point for external data * * @return array */ protected function getInput() { $req = array_merge($_GET, $_POST); #Ajax input is always utf-8 -> convert it if($this->Loader->get('encoding') != 'utf-8' and isset($_SERVER['HTTP_X_REQUESTED_WITH'])) { $req = jqGrid_Utils::arrayIconv($req, 'utf-8', $this->Loader->get('encoding')); } return $req; } /** * Get reserved column names specific to this grid * * @return array - array of names */ protected function getReservedColNames() { $names = $this->default['reserved_col_names']; if($this->treegrid == 'adjacency') { $names = array_merge($names, array('parent', 'level', 'isLeaf', 'expanded', 'loaded')); } if($this->treegrid == 'nested') { $names = array_merge($names, array('level', 'lft', 'rgt', 'isLeaf', 'expanded', 'loaded')); } return $names; } /** * Get row from DB * * @param $primary_key * @return array */ protected function getRowByPrimaryKey($primary_key) { $where = array(); foreach($this->explodePrimaryKey($primary_key) as $k => $v) { $where[] = "$k = " . $this->DB->quote($v); } $result = $this->DB->query("SELECT * FROM {$this->table} WHERE " . implode(' AND ', $where)); return $this->DB->fetch($result); } /** * Init one column * Apply defaults, check name etc. * * @param string $k - column name * @param array $c - non-default column params * @return array - complete column */ protected function initColumn($k, $c) { #Check reserved keys if(in_array($k, $this->reserved_col_names)) { throw new jqGrid_Exception("Column name '$k' reserved for internal usage!"); } if(strpos($k, '_') === 0) { throw new jqGrid_Exception("Column name '$k' must NOT begin with underscore!"); } #Name and index always match the array key $c['name'] = $k; $c['index'] = $k; #Label = column key if not set if(!isset($c['label'])) $c['label'] = $k; #Merge with defaults $c = array_merge($this->default['cols'], $this->cols_default, $c); #DB = column key if not set $c['db'] = $c['db'] ? $c['db'] : $k; return $c; } //---------------- // OPERATIONS PART //---------------- /** * (Oper) Insert * * Please note: this is the only "Oper" function, which must return new row id * * @param array $ins - form data * @return integer - new_id */ protected function opAdd($ins) { if(empty($this->table)) { throw new jqGrid_Exception('Table is not defined'); } return $this->DB->insert($this->table, $ins, true); } /** * (Oper) Update * * @param integer $id - id to update * @param array $upd - form data * @return void */ protected function opEdit($id, $upd) { if(empty($this->table)) { throw new jqGrid_Exception('Table is not defined'); } $this->DB->update($this->table, $upd, $this->explodePrimaryKey($id)); } /** * (Oper) Delete * * @param integer|string $id - one or multiple id's to delete * @return void */ protected function opDel($id) { if(empty($this->table)) { throw new jqGrid_Exception('Table is not defined'); } $where = array(); foreach(explode(',', $id) as $pk) { $wh = array(); foreach($this->explodePrimaryKey($pk) as $kk => $vv) { $wh[] = "$kk = " . $this->DB->quote($vv); } $where[] = '(' . implode(' AND ', $wh) . ')'; } $this->DB->delete($this->table, implode(' OR ', $where)); } //---------------- // OPER HOOKS PART //---------------- /** * (Oper) Modify form data for opAdd and opEdit only * These operations usually need the same modifications, so i made a stand-alone hook for them * * @param array $r - form data * @return array - modified form data */ protected function operData($r) { return $r; } /** * (Oper) Hook after opAdd and opEdit * Useful for uploading images after processing other data etc. * * @param $id - id of updated or inserted row * @return void */ protected function operAfterAddEdit($id) { return; } /** * (Oper) Hook after ALL oper's * Useful for cleanup and dropping caches * * @param $oper - operation name * @return void */ protected function operComplete($oper) { return; } //---------------- // OUTPUT PART //---------------- /** * Output json */ protected function outJson() { $data = array( 'page' => $this->page, 'total' => $this->total, 'records' => $this->count, 'rows' => $this->rows, 'userdata' => $this->userdata, ); if($this->Loader->get('debug_output')) { $data['debug'] = $this->debug; } $this->json($data); } /** * Output XML ... not supported */ protected function outXml() { //unsupported } /** * Export data using plugin */ protected function outExport() { $type = jqGrid_Utils::checkAlphanum($this->input('export')); $class = 'jqGrid_Export_' . ucfirst($type); if(!class_exists($class)) { throw new jqGrid_Exception("Export type $type does not exist"); } #Weird >__< $lib = new $class($this->Loader); $this->setExportData($lib); $lib->doExport(); } /** * (Output) Encode each row value before output * * @param $c - column * @param $val - value * @return string - encoded value */ protected function outputEncodeValue($c, $val) { return htmlspecialchars($val, ENT_QUOTES); } /** * (Output) Modify row data before output * * @param array $r - row data * @return array - modified row data */ protected function parseRow($r) { return $r; } //---------------- // RENDER PART //---------------- /** * (Render) Render single column * * @param array $c - col properties * @return array - modified col properties */ protected function renderColumn($c) { return $c; } /** * Get grid_id suffix when multiple instances are used on one page * * @param $render_data * @return string */ protected function renderGridSuffix($render_data) { if(isset($this->render_suffix_col) and isset($render_data[$this->render_suffix_col])) { return jqGrid_Utils::checkAlphanum($render_data[$this->render_suffix_col]); } return ''; } /** * (Render) Base url is used into 'url', 'editurl', 'cellurl' options * * @return string */ protected function renderGridUrl() { $params[$this->Loader->get('input_grid')] = $this->grid_id; foreach($this->render_data as $k => $v) { $params[$k] = $v; } return $this->base_url . '?' . http_build_query($params); } /** * (Render) There are few ways to create basic html markup for jqGrid * You may alter this if you want yours * * @param $data - render data * @return string - string to be added before .jqGrid call */ protected function renderHtml($data) { return ' </script> <table id="' . $data['id'] . '"></table> <div id="' . $data['pager_id'] . '"></div> <script> '; } /** * (Render) Alter 'nav' * * @param $nav original nav * @return array modified nav */ protected function renderNav($nav) { return $nav; } /** * (Render) Alter 'options' * * @param array $opts original options * @return array modified options */ protected function renderOptions($opts) { return $opts; } /** * (Render) Special hook for setting option 'postData' * Oftenly used to set explicit invisible filters * * @return array postData */ protected function renderPostData() { return array(); } /** * (Render) Takes all previously generated parts and combine them into general output string * You can completely override the rendering strategy here * * @param array $data * @return string */ protected function renderComplete($data) { $data['extend'] = $data['extend'] ? $data['extend'] : '{}'; //prevent errors on empty 'extend' $code = $data['html'] . ' var pager = "#' . $data['pager_id'] . '"; var $grid = $("#' . $data['id'] . '"); var $' . $data['id'] . ' = $grid; $grid.jqGrid($.extend(' . jqGrid_Utils::jsonEncode($data['options']) . ', typeof(' . $data['extend'] . ') == "undefined" ? {} : ' . $data['extend'] . "));\n"; #Event binder $code .= "\$grid.jqGrid('extBindEvents');\n"; #NavGrid if(isset($data['nav'])) { $nav_special = array('prmEdit', 'prmAdd', 'prmDel', 'prmSearch', 'prmView'); $code .= "\$grid.jqGrid('navGrid', pager, " . jqGrid_Utils::jsonEncode(array_diff_key($data['nav'], array_flip($nav_special))); #Respect the argument order foreach($nav_special as $k) { if(isset($data['nav'][$k])) { $code .= ', ' . jqGrid_Utils::jsonEncode($data['nav'][$k]); } else { $code .= ', null'; } } $code .= ");\n"; #Excel button if(isset($data['nav']['excel']) and $data['nav']['excel']) { $code .= "\$grid.jqGrid('navButtonAdd', pager, {caption: '{$data['nav']['exceltext']}', title: '{$data['nav']['exceltext']}', icon: 'ui-extlink', onClickButton: function(){ \$(this).jqGrid('extExport', {'export' : 'ExcelHtml', 'rows': -1}); }});\n"; } } #Filter toolbar if($this->render_filter_toolbar) { $code .= "\$grid.jqGrid('filterToolbar');\n"; } return $code; } //---------------- // SEARCH OPERATORS PART //---------------- /** * (Output) Perform searching based on input * Populates $this->where with SQL-expressions * * @return void */ protected function search() { foreach($this->cols as $k => $c) { if(!isset($this->input[$k]) or $this->input[$k] === '') { continue; } #Preserve original input value $val = $this->input[$k]; if(is_array($val)) { foreach($val as $kk => $vv) { jqGrid_Utils::checkAlphanum($kk); $val[$kk] = $this->searchCleanVal($vv); } } else { $val = $this->searchCleanVal($val); } //------------------ // Apply search operator //------------------ $callback = array($this, jqGrid_Utils::uscore2camel('searchOp', $c['search_op'])); if(!is_callable($callback)) { throw new jqGrid_Exception('Search operation ' . $c['search_op'] . ' is not defined'); } $wh = call_user_func($callback, $c, $val); if($wh) $this->where[] = $wh; } } /** * (Output) Perform searching based on input json-encoded variable 'filters' * Populates $this->where with SQL-expressions * * @return void */ protected function searchAdvanced() { $filters = $this->input('filters'); if(empty($filters)) { return; } $filters = json_decode($filters, true); if(empty($filters)) { return; } $this->where[] = $this->searchAdvancedGroup($filters); } /** * (Output) Recursive processor for each search group * * @param $row * @return string */ protected function searchAdvancedGroup($row) { static $base = array( 'groupOp' => 'AND', 'rules' => array(), 'groups' => array(), ); static $basic_ops = array( 'eq' => '=', 'ne' => '!=', 'lt' => '<', 'le' => '<=', 'gt' => '>', 'ge' => '>=', ); static $like_ops = array( 'bw' => "LIKE '{data}%'", 'bn' => "NOT LIKE '{data}%'", 'ew' => "LIKE '%{data}'", 'en' => "NOT LIKE '%{data}'", 'cn' => "LIKE '%{data}%'", 'nc' => "NOT LIKE '%{data}%'", ); $row = array_merge($base, $row); $row['groupOp'] = in_array($row['groupOp'], array('AND', 'OR')) ? $row['groupOp'] : 'AND'; $wh = array(); //------------ // Process rules //------------ foreach($row['rules'] as $r) { if(!array_key_exists($r['field'], $this->cols)) { continue; } $op = $r['op']; $c = $this->cols[$r['field']]; $data = $this->searchCleanVal($r['data']); //------------- // Empty data? Skip this rule! //------------- if(empty($data) and !in_array($op, array('nu', 'nn'))) { continue; } //------------- // Customer search op //------------- if($c['search_op'] and $c['search_op'] != 'auto') { $callback = array($this, jqGrid_Utils::uscore2camel('searchOp', $c['search_op'])); if(!is_callable($callback)) { throw new jqGrid_Exception('Search operation ' . $c['search_op'] . ' is not defined'); } $wh[] = call_user_func($callback, $c, $data); continue; } //------------- // Common search op's //------------- if(array_key_exists($op, $basic_ops)) { $wh[] = $c['db'] . ' ' . $basic_ops[$op] . " '$data'"; } elseif(array_key_exists($op, $like_ops)) { $wh[] = $c['db'] . ' ' . str_replace('{data}', addcslashes($data, '%_'), $like_ops[$op]); } else { switch($op) { case 'nu': $wh[] = $c['db'] . ' IS NULL'; break; case 'nn': $wh[] = $c['db'] . ' IS NOT NULL'; break; case 'in': $wh[] = $c['db'] . " IN ('" . implode("','", array_map('trim', explode(',', $data))) . "')"; break; case 'ni': $wh[] = $c['db'] . " NOT IN ('" . implode("','", array_map('trim', explode(',', $data))) . "')"; break; } } } //------------ // Process sub-groups recursively //------------ foreach($row['groups'] as $g) { $wh[] = $this->searchAdvancedGroup($g); } //------------ // Implode rules //------------ $wh = array_filter($wh); return $wh ? ('(' . implode(' ' . $row['groupOp'] . ' ', $wh) . ')') : $this->where_empty; } /** * (Output) Clean each search value before sending it to other functions * Returns clean, but UNQUOTED value!! * * @param $val - value * @return string - clean value */ protected function searchCleanVal($val) { $val = trim($val); $val = $this->DB->quote($val); #Strip quotes for easier values handling $start = (strpos($val, "E'") === 0) ? 2 : 1; $val = substr($val, $start, -1); return $val; } /** * (Output) Auto detect of searchOp * * @param $c - column settings * @param $val - "clean" value. If you need original value - use $this->input * @return string - SQL-expression -> goes directly to WHERE. Set false to skip search. */ protected function searchOpAuto($c, $val) { #Search type - select? if(isset($c['stype']) and $c['stype'] == 'select') { return self::searchOpEqual($c, $val); } #Numeric by formatter? if(isset($c['formatter']) and in_array($c['formatter'], array('integer', 'numeric', 'currency'))) { return self::searchOpNumeric($c, $val); } #Numeric by value? if(preg_match('#^([<>=!]{1,2})?\d+$#', $val)) { return self::searchOpNumeric($c, $val); } #Seems to be 'like' return self::searchOpLike($c, $val); } /** * (Output) Disable search * * @param $c - column * @param $val - value * @return bool */ protected function searchOpIgnore($c, $val) { return false; } /** * (Output) Look for EXACT match * * @param $c - column * @param $val - value * @return string */ protected function searchOpEqual($c, $val) { return $c['db'] . " = '$val'"; } /** * (Output) Look for similar text * * @param $c - column * @param $val - value * @return string */ protected function searchOpLike($c, $val) { #Escape wildcards $val = addcslashes($val, '%_'); $op = ($this->DB->getType() == 'postgresql') ? 'ILIKE' : 'LIKE'; return $c['db'] . " $op '%$val%'"; } /** * (Output) Look for list of values * * @param $c - column * @param $val - value * @return string */ protected function searchOpIn($c, $val) { $hash = array_map('trim', explode(',', $val)); return $c['db'] . " IN ('" . implode("','", $hash) . "')"; } /** * (Output) Look for numeric value * Supports prefixes to search for range of numeric values * * @param $c - column * @param $val - value * @return string */ protected function searchOpNumeric($c, $val) { static $prefix = array('<=', '>=', '<>', '!=', '<', '>', '='); foreach($prefix as $p) { if(strpos($val, $p) === 0) { $op = $p; $val = substr($val, strlen($p)); break; } } $op = isset($op) ? $op : '='; $val = floatval(trim($val)); return $c['db'] . " $op '$val'"; } /** * (Output) Send data to export library * * @param jqGrid_Export $lib * @return void */ protected function setExportData(jqGrid_Export $lib) { $lib->grid_id = $this->grid_id; $lib->input = $this->input; $lib->DB = $this->DB; $lib->cols = $this->cols; $lib->rows = $this->rows; $lib->userdata = $this->userdata; $lib->page = $this->page; $lib->total = $this->total; } /** * (Output) Set total row count and calc related vars * * @throws jqGrid_Exception * @param integer $count - set this argument to override auto-detection * @return void */ protected function setRowCount($count = null) { if(is_null($count)) { #Retrieve count from agg data if(isset($this->agg['_count'])) { $count = $this->agg['_count']; } #Simply count 'rows' else { $count = count($this->rows); } } $count = intval($count); if($count < 0) { throw new jqGrid_Exception('Invalid count value'); } if(!$count) { $this->count = 0; $this->page = 0; $this->total = 0; $this->offset = 0; return; } $this->count = $count; if($this->limit == -1) { $this->total = 1; $this->page = 1; $this->offset = 0; } elseif($this->limit) { $this->total = ($this->count > 0) ? ceil($this->count / $this->limit) : 0; $this->page = ($this->page <= $this->total) ? $this->page : $this->total; $this->offset = $this->limit * $this->page - $this->limit; } } //---------------- // HELPER PART //---------------- /** * Get input var(s) by key(s) * Nice shortcut against repeating "isset" everywhere * * @param string|array $key - single key or set of keys * @param mixed $default - default value in case key does not exist * @return mixed */ protected function input($key, $default = null) { if(is_array($key)) { $ret = array(); foreach($key as $k) { $ret[$k] = $this->input($k, $default); } return $ret; } return isset($this->input[$key]) ? $this->input[$key] : $default; } /** * Send JSON to browser * Please set $this->json_mode for special output * * TODO: add jsonp support * * @param $obj object to send * @return void */ protected function json($obj) { #Mode preset if($this->json_mode) { $mode = $this->json_mode; } #Common jQuery request elseif(isset($_SERVER['HTTP_X_REQUESTED_WITH'])) { $mode = 'json'; } #Probably ajaxForm iframe else { $mode = 'ajaxForm'; } switch($mode) { case 'ajaxForm': header("Content-type: text/html; charset={$this->Loader->get('encoding')};"); //echo '<textarea>' . jqGrid_Utils::jsonEncode($obj) . '</textarea>'; echo jqGrid_Utils::jsonEncode($obj); break; default: header("Content-type: application/json; charset={$this->Loader->get('encoding')};"); echo jqGrid_Utils::jsonEncode($obj); break; } } /** * Extract primary key from string * * @param string $primary_key * @return array * @throws jqGrid_Exception */ protected function explodePrimaryKey($primary_key) { $pk_parts = explode($this->primary_key_glue, $primary_key); if(count($this->primary_key) != count($pk_parts)) { throw new jqGrid_Exception('Incorrect format of primary key'); } return array_combine($this->primary_key, $pk_parts); } /** * Build primary key string * * @param array $row * @return string */ protected function implodePrimaryKey($row) { foreach($this->primary_key as $k) { $pk_parts[] = $row[$k]; } return implode($this->primary_key_glue, $pk_parts); } }