ssp.class.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. <?php
  2. /* Copyright (C) 2017-2021 Regis Houssin <regis.houssin@inodbox.com>
  3. *
  4. * This program is free software; you can redistribute it and/or modify
  5. * it under the terms of the GNU General Public License as published by
  6. * the Free Software Foundation; either version 3 of the License, or
  7. * (at your option) any later version.
  8. *
  9. * This program is distributed in the hope that it will be useful,
  10. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. * GNU General Public License for more details.
  13. *
  14. * You should have received a copy of the GNU General Public License
  15. * along with this program; if not, write to the Free Software
  16. * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  17. */
  18. /**
  19. * \file /multicompany/class/ssp.class.php
  20. * \ingroup multicompany
  21. * \brief File of class to manage DataTables server-side processing
  22. */
  23. /**
  24. * \class SSP
  25. * \brief DataTables server-side processing class
  26. */
  27. class SSP
  28. {
  29. /**
  30. * Create the data output array for the DataTables rows
  31. *
  32. * @param array $columns Column information array
  33. * @param array $data Data from the SQL get
  34. * @return array Formatted data in a row based format
  35. */
  36. static function data_output($columns, $data)
  37. {
  38. $out = array();
  39. for ($i=0, $ien=count($data); $i<$ien; $i++) {
  40. $row = array();
  41. for ($j=0, $jen=count($columns); $j<$jen; $j++) {
  42. $column = $columns[$j];
  43. // Is there a formatter?
  44. if (isset( $column['formatter'])) {
  45. $row[$column['dt']] = $column['formatter']($data[$i][$column['db']], $data[$i]);
  46. } else {
  47. $row[$column['dt']] = $data[$i][$columns[$j]['db']];
  48. }
  49. }
  50. $out[] = $row;
  51. }
  52. return $out;
  53. }
  54. /**
  55. * Paging
  56. *
  57. * Construct the LIMIT clause for server-side processing SQL query
  58. *
  59. * @param array $request Data sent to server by DataTables
  60. * @param array $columns Column information array
  61. * @return string SQL limit clause
  62. */
  63. static function limit($request, $columns, $db)
  64. {
  65. $limit = '';
  66. if (isset($request['start']) && $request['length'] != -1) {
  67. $limit = $db->plimit(intval($request['length']), intval($request['start']));
  68. }
  69. return $limit;
  70. }
  71. /**
  72. * Ordering
  73. *
  74. * Construct the ORDER BY clause for server-side processing SQL query
  75. *
  76. * @param array $request Data sent to server by DataTables
  77. * @param array $columns Column information array
  78. * @return string SQL order by clause
  79. */
  80. static function order($request, $columns, $db)
  81. {
  82. $order = '';
  83. $sortfield = array();
  84. $sortorder = array();
  85. if (isset($request['order']) && count($request['order'])) {
  86. $dtColumns = self::pluck( $columns, 'dt' );
  87. for ($i=0, $ien=count($request['order']); $i<$ien; $i++) {
  88. // Convert the column index into the column data property
  89. $columnIdx = intval($request['order'][$i]['column']);
  90. $requestColumn = $request['columns'][$columnIdx];
  91. $columnIdx = array_search( $requestColumn['data'], $dtColumns );
  92. $column = $columns[ $columnIdx ];
  93. if ($requestColumn['orderable'] == 'true') {
  94. $dir = $request['order'][$i]['dir'] === 'asc' ? 'ASC' : 'DESC';
  95. $sortfield[] = $column['db'];
  96. $sortorder[] = $dir;
  97. }
  98. }
  99. $order = $db->order(implode(', ', $sortfield), implode(', ', $sortorder));
  100. }
  101. return $order;
  102. }
  103. /**
  104. * Searching / Filtering
  105. *
  106. * Construct the WHERE clause for server-side processing SQL query.
  107. *
  108. * NOTE this does not match the built-in DataTables filtering which does it
  109. * word by word on any field. It's possible to do here performance on large
  110. * databases would be very poor
  111. *
  112. * @param array $request Data sent to server by DataTables
  113. * @param array $columns Column information array
  114. * @param array $bindings Array of values for PDO bindings, used in the
  115. * sql_exec() function
  116. * @return string SQL where clause
  117. */
  118. static function filter($request, $columns, $db)
  119. {
  120. $globalSearch = array();
  121. $columnSearch = array();
  122. $dtColumns = self::pluck( $columns, 'dt' );
  123. if (isset($request['search']) && $request['search']['value'] != '') {
  124. $str = $request['search']['value'];
  125. for ($i=0, $ien=count($request['columns']); $i<$ien; $i++) {
  126. $requestColumn = $request['columns'][$i];
  127. $columnIdx = array_search( $requestColumn['data'], $dtColumns );
  128. $column = $columns[ $columnIdx ];
  129. if ($requestColumn['searchable'] == 'true') {
  130. $globalSearch[] = $column['db']." LIKE '%" . $db->escape($str)."%'";
  131. }
  132. }
  133. }
  134. // Individual column filtering
  135. if (isset( $request['columns'])) {
  136. for ($i=0, $ien=count($request['columns']); $i<$ien; $i++) {
  137. $requestColumn = $request['columns'][$i];
  138. $columnIdx = array_search( $requestColumn['data'], $dtColumns );
  139. $column = $columns[$columnIdx];
  140. $str = $requestColumn['search']['value'];
  141. if ($requestColumn['searchable'] == 'true' && $str != '') {
  142. $columnSearch[] = $column['db']." LIKE '%" . $db->escape($str)."%'";
  143. }
  144. }
  145. }
  146. // Combine the filters into a single string
  147. $where = '';
  148. if (count( $globalSearch)) {
  149. $where = '('.implode(' OR ', $globalSearch).')';
  150. }
  151. if (count( $columnSearch)) {
  152. $where = $where === '' ?
  153. implode(' AND ', $columnSearch) :
  154. $where .' AND '. implode(' AND ', $columnSearch);
  155. }
  156. if ($where !== '') {
  157. $where = 'WHERE '.$where;
  158. }
  159. return $where;
  160. }
  161. /**
  162. * Perform the SQL queries needed for an server-side processing requested,
  163. * utilising the helper functions of this class, limit(), order() and
  164. * filter() among others. The returned array is ready to be encoded as JSON
  165. * in response to an SSP request, or can be modified if needed before
  166. * sending back to the client.
  167. *
  168. * @param array $request Data sent to server by DataTables
  169. * @param string $table SQL table to query
  170. * @param string $primaryKey Primary key of the table
  171. * @param array $columns Column information array
  172. * @return array Server-side processing response array
  173. */
  174. static function simple($request, $db, $table, $primaryKey, $columns)
  175. {
  176. // Build the SQL query string from the request
  177. $limit = self::limit( $request, $columns, $db );
  178. $order = self::order( $request, $columns, $db );
  179. $where = self::filter( $request, $columns, $db );
  180. // Main query to actually get the data
  181. $sql =
  182. "SELECT ".implode(", ", self::pluck($columns, 'db'))."
  183. FROM $table
  184. $where
  185. $order
  186. $limit";
  187. $data = self::sql_exec($db, $sql);
  188. //print_r($data);
  189. // Data set length after filtering
  190. $resFilterLength = self::sql_exec($db,
  191. "SELECT COUNT({$primaryKey})
  192. FROM $table
  193. $where"
  194. );
  195. $recordsFiltered = $resFilterLength[0][0];
  196. // Total data set length
  197. $resTotalLength = self::sql_exec($db,
  198. "SELECT COUNT({$primaryKey})
  199. FROM $table"
  200. );
  201. $recordsTotal = $resTotalLength[0][0];
  202. /*
  203. * Output
  204. */
  205. return array(
  206. "draw" => (isset ( $request['draw'] ) ? intval( $request['draw'] ) : 0),
  207. "recordsTotal" => intval( $recordsTotal ),
  208. "recordsFiltered" => intval( $recordsFiltered ),
  209. "data" => self::data_output( $columns, $data )
  210. );
  211. }
  212. /**
  213. * The difference between this method and the `simple` one, is that you can
  214. * apply additional `where` conditions to the SQL queries. These can be in
  215. * one of two forms:
  216. *
  217. * * 'Result condition' - This is applied to the result set, but not the
  218. * overall paging information query - i.e. it will not effect the number
  219. * of records that a user sees they can have access to. This should be
  220. * used when you want apply a filtering condition that the user has sent.
  221. * * 'All condition' - This is applied to all queries that are made and
  222. * reduces the number of records that the user can access. This should be
  223. * used in conditions where you don't want the user to ever have access to
  224. * particular records (for example, restricting by a login id).
  225. *
  226. * @param array $request Data sent to server by DataTables
  227. * @param string $table SQL table to query
  228. * @param string $primaryKey Primary key of the table
  229. * @param array $columns Column information array
  230. * @param string $whereResult WHERE condition to apply to the result set
  231. * @param string $whereAll WHERE condition to apply to all queries
  232. * @return array Server-side processing response array
  233. */
  234. static function complex($request, $db, $table, $primaryKey, $columns, $whereResult=null, $whereAll=null)
  235. {
  236. $whereAllSql = '';
  237. // Build the SQL query string from the request
  238. $limit = self::limit( $request, $columns, $db );
  239. $order = self::order( $request, $columns, $db );
  240. $where = self::filter( $request, $columns, $db );
  241. $whereResult = self::_flatten( $whereResult );
  242. $whereAll = self::_flatten( $whereAll );
  243. if (!empty($whereResult)) {
  244. $where = $where ? $where .' AND '.$whereResult : 'WHERE '.$whereResult;
  245. }
  246. if (!empty($whereAll)) {
  247. $where = $where ? $where .' AND '.$whereAll : 'WHERE '.$whereAll;
  248. $whereAllSql = 'WHERE '.$whereAll;
  249. }
  250. // Main query to actually get the data
  251. $data = self::sql_exec($db,
  252. "SELECT ".implode(", ", self::pluck($columns, 'db'))."
  253. FROM $table $where $order $limit"
  254. );
  255. // Data set length after filtering
  256. $resFilterLength = self::sql_exec($db,
  257. "SELECT COUNT({$primaryKey})
  258. FROM $table $where"
  259. );
  260. $recordsFiltered = $resFilterLength[0][0];
  261. // Total data set length
  262. $resTotalLength = self::sql_exec($db,
  263. "SELECT COUNT({$primaryKey})
  264. FROM $table ".$whereAllSql
  265. );
  266. $recordsTotal = $resTotalLength[0][0];
  267. /*
  268. * Output
  269. */
  270. return array(
  271. "draw" => (isset($request['draw']) ? intval($request['draw']) : 0),
  272. "recordsTotal" => intval($recordsTotal),
  273. "recordsFiltered" => intval($recordsFiltered),
  274. "data" => self::data_output($columns, $data)
  275. );
  276. }
  277. /**
  278. * Execute an SQL query on the database
  279. *
  280. * @param string $sql SQL query to execute.
  281. * @return array Result from the query (all rows)
  282. */
  283. static function sql_exec($db, $sql)
  284. {
  285. $out=array();
  286. $resql = $db->query($sql);
  287. if (!empty($resql)) {
  288. $i = 0;
  289. $num_rows = $db->num_rows($resql);
  290. while ($i < $num_rows) {
  291. $array = $db->fetch_array($resql);
  292. array_push($out, $array);
  293. $i++;
  294. }
  295. }
  296. return $out;
  297. }
  298. /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
  299. * Internal methods
  300. */
  301. /**
  302. * Pull a particular property from each assoc. array in a numeric array,
  303. * returning and array of the property values from each item.
  304. *
  305. * @param array $a Array to get data from
  306. * @param string $prop Property to read
  307. * @return array Array of property values
  308. */
  309. static function pluck($a, $prop)
  310. {
  311. $out = array();
  312. for ($i=0, $len=count($a); $i<$len; $i++) {
  313. $out[] = $a[$i][$prop];
  314. }
  315. return $out;
  316. }
  317. /**
  318. * Return a string from an array or a string
  319. *
  320. * @param array|string $a Array to join
  321. * @param string $join Glue for the concatenation
  322. * @return string Joined string
  323. */
  324. static function _flatten($a, $join = ' AND ')
  325. {
  326. if (empty($a)) {
  327. return '';
  328. } else if (!empty($a) && is_array($a)) {
  329. return implode( $join, $a );
  330. }
  331. return $a;
  332. }
  333. }