AclQueryFeatures.php 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807
  1. <?php
  2. Lib::loadClass('ACL');
  3. Lib::loadClass('SqlQueryWhereBuilder');
  4. Lib::loadClass('ParseOgcFilter');
  5. Lib::loadClass('TableAcl');
  6. // usage: (Acl class)::buildQuery($params): return new AclQueryFeatures($this, $params);
  7. // (view): $queryFeatures = $acl->buildQuery($params);
  8. // (view): $total = $queryFeatures->getTotal();
  9. // (view): $items = $queryFeatures->getItems();
  10. // example: @see TableAcl, TableAjax
  11. // Special Filter Access - btns visible only if user don't have super access perms. If has, then will always see all rows.
  12. class AclQueryFeatures {
  13. public $_params;
  14. public $_acl;
  15. public $_query;
  16. public $_total;
  17. public $_legacyMode;
  18. public $_selectLocalFields; // TODO: leave here or move to AclQueryBuilder? use join for perf?
  19. public $_selectRemote; // TODO: ...
  20. public $_dbgExecTime;
  21. public $_instanceID;
  22. public $_foundFeatures;
  23. public function __construct($acl, $params, $legacyMode = false) {
  24. $this->_acl = $acl;
  25. $this->_params = $params;
  26. $this->_query = null;
  27. $this->_total = null;
  28. $this->_legacyMode = $legacyMode;
  29. // TODO: _legacyMode = ($from instanceof simple schema or another programmed objects)
  30. $this->_selectLocalFields = null;
  31. $this->_selectRemote = null;
  32. $this->_foundFeatures = [];
  33. // 'skipFeaturesAsXlink' => $this->_foundFeatures
  34. if (array_key_exists('skipFeaturesAsXlink', $this->_params)) {
  35. $this->_foundFeatures = V::get('skipFeaturesAsXlink', [], $this->_params, 'array');
  36. unset($this->_params['skipFeaturesAsXlink']);
  37. }
  38. $DBG_LOG_TO_FILE = false; // TODO: read from ENV or REQUEST
  39. if ($DBG_LOG_TO_FILE) {
  40. Lib::loadClass('DebugExecutionTime');
  41. $this->_dbgExecTime = new DebugExecutionTime();
  42. $this->_dbgExecTime->setLogFile('/tmp/se-dbg-exec-time--AclQueryFeatures.csv', $format = 'csv'); // $format: 'csv', 'json'
  43. $this->_logToFile("from: '" . $this->_acl->getNamespace() . "'");
  44. }
  45. }
  46. public function _logToFile($msg) {
  47. if ($this->_dbgExecTime) {
  48. $idInstance = $this->_getInstanceID();
  49. $this->_dbgExecTime->logToFile("#{$idInstance}: {$msg}");
  50. }
  51. }
  52. public function _getInstanceID() {
  53. static $_INSTANCE_ID = 0;
  54. if (!$this->_instanceID) {
  55. $_INSTANCE_ID += 1;
  56. $this->_instanceID = $_INSTANCE_ID;
  57. }
  58. return $this->_instanceID;
  59. }
  60. public function parseQueryValue($fieldName, $searchQuery, $fieldType = 'xsd:string') {
  61. if ('!NULL' === $searchQuery) return ['is not null', null];
  62. if ('IS NOT NULL' === $searchQuery) return ['is not null', null];
  63. if ('NULL' === $searchQuery) return ['is null', null];
  64. if ('IS NULL' === $searchQuery) return ['is null', null];
  65. switch ($fieldType) {
  66. case 'p5Type:polygon':
  67. case 'p5Type:lineString':
  68. case 'p5Type:point':
  69. case 'gml:PolygonPropertyType':
  70. case 'gml:PointPropertyType':
  71. case 'gml:LineStringPropertyType':
  72. case 'gml:GeometryPropertyType': return $this->_parseGeomQuery($searchQuery);
  73. // $sqlFilter = $this->_sqlValueForGeomField($fldName, $v, 't');
  74. // if ('_CSV_NUM' == substr($fldName, -8)) { // if ($this->isCsvNumericField($fldName)) { // TODO: xsd type - p5:csv_num
  75. // $sqlFilter = $this->_sqlValueForCsvNumericField($fldName, $v, 't');
  76. // if ($sqlFilter) $sql_where_and[] = $sqlFilter;
  77. // continue;
  78. // }
  79. }
  80. switch (substr($searchQuery, 0, 1)) {
  81. case '=': return ['=', substr($searchQuery, 1)];
  82. case '>':
  83. switch (substr($searchQuery, 1, 1)) {
  84. case '=': return ['>=', substr($searchQuery, 2)];
  85. default: return ['>', substr($searchQuery, 1)];
  86. }
  87. case '<':
  88. switch (substr($searchQuery, 1, 1)) {
  89. case '=': return ['<=', substr($searchQuery, 2)];
  90. case '>': return ['!=', substr($searchQuery, 2)];
  91. default: return ['<', substr($searchQuery, 1)];
  92. }
  93. case '!':
  94. switch (substr($searchQuery, 1, 1)) {
  95. case '=': return ['!=', substr($searchQuery, 2)];
  96. default: return ['not like', substr($searchQuery, 1)];
  97. }
  98. default: {
  99. switch ($fieldType) {
  100. case 'xsd:long':
  101. case 'xsd:number':
  102. case 'xsd:int':
  103. case 'xsd:integer': {
  104. if (false !== strpos($searchQuery, '%')) return ['like', $searchQuery];
  105. return ['=', $searchQuery];
  106. }
  107. default: {
  108. if (false !== strpos($searchQuery, '%')) return ['like', $searchQuery];
  109. $queryWhereBuilder = new SqlQueryWhereBuilder();
  110. return ['and'
  111. , array_map(function ($word) use ($fieldName) {
  112. return [$fieldName, 'like', "%{$word}%"];
  113. }, $queryWhereBuilder->splitQueryToWords($searchQuery)
  114. )
  115. ];
  116. }
  117. }
  118. return ['=', $searchQuery];
  119. }
  120. }
  121. }
  122. public function _parseGeomQuery($searchQuery) { // _sqlValueForGeomField($fldName, $fltrValue, $tblPrefix = 't')
  123. // example: BBOX:54.40993961633866,18.583889010112824,54.337945760687454,18.397121431987586
  124. DBG::log($searchQuery, 'string', "\$searchQuery");
  125. if ('BBOX:' == substr($searchQuery, 0, 5)) {
  126. $valParts = explode(',', substr($searchQuery, 5));
  127. if (4 !== count($valParts)) throw new Exception("Wrong BBOX query");
  128. $valParts = array_filter($valParts, 'is_numeric');
  129. if (4 !== count($valParts)) throw new Exception("Wrong BBOX query - expected 4 numeric values");
  130. $bounds = "POLYGON((
  131. {$valParts[3]} {$valParts[2]},
  132. {$valParts[3]} {$valParts[0]},
  133. {$valParts[1]} {$valParts[0]},
  134. {$valParts[1]} {$valParts[2]},
  135. {$valParts[3]} {$valParts[2]}
  136. ))";
  137. // for mysql 5.6 use ST_Contains() @see http://dev.mysql.com/doc/refman/5.6/en/spatial-relation-functions.html
  138. return [ 'Intersects', $bounds ];
  139. }
  140. else if ('GeometryType=' == substr($fltrValue, 0, 13)) {
  141. return [ 'GeometryType', substr($fltrValue, 13) ];
  142. }
  143. throw new Exception("Not implemented geometry query string"); // TODO:? return null;
  144. }
  145. public function _sqlValueForCsvNumericField($fldName, $fltrValue, $tblPrefix = 't') {
  146. $sqlFilter = false;
  147. if (is_numeric($fltrValue)) {
  148. $sqlFilter = "FIND_IN_SET('{$fltrValue}', `{$fldName}`)>0";
  149. } else if (false !== strpos($fltrValue, ' ')) {
  150. $sqlGlue = " or ";
  151. $fltrValues = $fltrValue;
  152. if ('&' == substr($fltrValues, 0, 1)) {
  153. $fltrValues = substr($fltrValues, 1);
  154. $sqlGlue = " and ";
  155. }
  156. $fltrValues = explode(' ', $fltrValues);
  157. $sqlNumericValues = array();
  158. foreach ($fltrValues as $fltrVal) {
  159. if (is_numeric($fltrVal)) {
  160. $sqlNumericValues[] = "FIND_IN_SET('{$fltrVal}', `{$fldName}`)>0";
  161. }
  162. }
  163. if (!empty($sqlNumericValues)) {
  164. $sqlFilter = "(" . implode($sqlGlue, $sqlNumericValues) . ")";
  165. }
  166. }
  167. return $sqlFilter;
  168. }
  169. public function parseSpecialFilterMsgs($type) {
  170. $rootTableName = $this->_acl->getRootTableName();
  171. DBG::log($rootTableName, 'string', "parse SpecialFilter Msgs({$type}), \$rootTableName");
  172. $sqlHasFltrMsgs = "
  173. select 1
  174. from `CRM_UI_MSGS` m
  175. where m.`uiTargetName`=CONCAT('{$rootTableName}.', t.`ID`)
  176. and m.`uiTargetType`='default_db_table_record'
  177. and m.`A_STATUS` not in('DELETED')
  178. limit 1
  179. ";
  180. switch ($type) {
  181. case 'HAS_MSGS': return " ({$sqlHasFltrMsgs})=1 ";
  182. case 'NO_MSGS': return " ({$sqlHasFltrMsgs}) is null ";
  183. case 'NEW_MSGS': {
  184. $sqlNewFltrMsgs = "
  185. select 1
  186. from `CRM_UI_MSGS` m
  187. where m.`uiTargetName`=CONCAT('{$rootTableName}.', t.`ID`)
  188. and m.`uiTargetType`='default_db_table_record'
  189. and m.`A_STATUS` in('WAITING')
  190. limit 1
  191. ";
  192. return " ({$sqlNewFltrMsgs})=1 ";
  193. }
  194. }
  195. return null;
  196. }
  197. public function parseSpecialFilterProblemy($type) {
  198. DBG::log($type, 'string', "parse SpecialFilter Problemy");
  199. switch ($type) {
  200. case 'PROBLEM': return ['A_PROBLEM', '!=', ''];
  201. case 'WARNING': return ['A_PROBLEM', '=', 'WARNING'];
  202. case 'NORMAL': return ['A_PROBLEM', '=', 'NORMAL'];
  203. }
  204. return null;
  205. }
  206. public function parseSpecialFilterStatus($type) {
  207. DBG::log($type, 'string', "parse SpecialFilter Status");
  208. switch ($type) {
  209. case 'WAITING': return ['A_STATUS', '=', 'WAITING'];
  210. case 'AKTYWNI': return ['A_STATUS', 'or', [ // `A_STATUS` in('NORMAL', 'WARNING') ";
  211. ['A_STATUS', '=', 'NORMAL'],
  212. ['A_STATUS', '=', 'WARNING'],
  213. ] ];
  214. }
  215. return null;
  216. }
  217. public function parseSpecialFilterTrash($type) {
  218. DBG::log($type, 'string', "parse SpecialFilter Trash");
  219. switch ($type) {
  220. case 'HIDE': return ['A_STATUS', '!=', 'DELETED'];
  221. case 'TRASH': return ['A_STATUS', '=', 'DELETED'];
  222. }
  223. return null;
  224. }
  225. public function parseSpecialFilterSpotkania($type) {
  226. DBG::log($type, 'string', "parse SpecialFilter Spotkania");
  227. switch ($type) {
  228. case 'OLD': return ['L_APPOITMENT_DATE', 'and', [
  229. ['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_LESS_THAN_NOW'],
  230. ] ];
  231. // COALESCE(UNIX_TIMESTAMP(t.`L_APPOITMENT_DATE`), 0) < UNIX_TIMESTAMP()
  232. // and t.`L_APPOITMENT_DATE` != ''
  233. // and t.`L_APPOITMENT_DATE` != '0000-00-00 00:00:00'
  234. case 'NOW': return ['L_APPOITMENT_DATE', 'and', [
  235. [ 'L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_NOW_3600'],
  236. ] ];
  237. // COALESCE(UNIX_TIMESTAMP(t.`L_APPOITMENT_DATE`), 0) < UNIX_TIMESTAMP()+3600
  238. // and COALESCE(UNIX_TIMESTAMP(t.`L_APPOITMENT_DATE`), 0) > UNIX_TIMESTAMP()-3600
  239. case 'TODAY': return ['L_APPOITMENT_DATE', 'and', [
  240. ['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_GREATER_THAN', mktime(0,0,0, date("m"), date("d"), date("Y"))],
  241. ['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_LESS_THAN', mktime(0,0,0, date("m"), date("d") + 1, date("Y"))],
  242. ] ];
  243. // $start = mktime(0,0,0, date("m"), date("d"), date("Y"));
  244. // $end = mktime(0,0,0, date("m"), date("d") + 1, date("Y"));
  245. // $sqlFltr = "
  246. // COALESCE(UNIX_TIMESTAMP(t.`L_APPOITMENT_DATE`), 0) > '{$start}'
  247. // and COALESCE(UNIX_TIMESTAMP(t.`L_APPOITMENT_DATE`), 0) < '{$end}'
  248. // ";
  249. case 'TOMORROW': return ['L_APPOITMENT_DATE', 'and', [
  250. ['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_GREATER_THAN', mktime(0,0,0, date("m"), date("d") + 1, date("Y"))],
  251. ['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_LESS_THAN', mktime(0,0,0, date("m"), date("d") + 2, date("Y"))],
  252. ] ];
  253. case 'YESTERDAY': return ['L_APPOITMENT_DATE', 'and', [
  254. ['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_GREATER_THAN', mktime(0,0,0, date("m"), date("d") - 2, date("Y"))],
  255. ['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_LESS_THAN', mktime(0,0,0, date("m"), date("d") - 1, date("Y"))],
  256. ] ];
  257. case 'BRAK': return ['L_APPOITMENT_DATE', 'or', [
  258. ['L_APPOITMENT_DATE', '=', ''],
  259. ['L_APPOITMENT_DATE', '=', '0000-00-00 00:00:00'],
  260. ] ];
  261. }
  262. return null;
  263. }
  264. public function parseSpecialFilterAccess() {
  265. $userLogin = User::getLogin();
  266. $usrAclGroups = User::getLdapGroupsNames();
  267. DBG::log(['ns'=>$this->_acl->getNamespace(), 'login'=>$userLogin, 'hasWriteField'=>$this->_acl->hasWriteGroupField(), 'hasReadField'=>$this->_acl->hasReadGroupField(), 'hasOwnerField'=>$this->_acl->hasOwnerField(), 'groups'=>$usrAclGroups], 'array', "parse SpecialFilter Access");
  268. $orWhere = [];
  269. if ($this->_acl->hasWriteGroupField()) {
  270. $orWhere[] = ['A_ADM_COMPANY', '=', ''];// TODO: allow empty for everyone?
  271. foreach ($usrAclGroups as $group) $orWhere[] = ['A_ADM_COMPANY', '=', $group];
  272. }
  273. if ($this->_acl->hasReadGroupField()) {
  274. $orWhere[] = ['A_CLASSIFIED', '=', ''];// TODO: allow empty for everyone?
  275. foreach ($usrAclGroups as $group) $orWhere[] = ['A_CLASSIFIED', '=', $group];
  276. }
  277. if (!empty($orWhere) && $this->_acl->hasOwnerField()) {
  278. $orWhere[] = ['L_APPOITMENT_USER', '=', $userLogin];
  279. }
  280. return (!empty($orWhere)) ? [null, 'or', $orWhere] : null;
  281. }
  282. public function parseOgcFilter($ogcFilter) {
  283. $parser = new ParseOgcFilter();
  284. $parser->loadOgcFilter($ogcFilter);
  285. $queryWhereBuilder = $parser->convertToSqlQueryWhereBuilder();
  286. return $queryWhereBuilder->getQueryWhere('t'); // TODO: $this->_fromPrefix
  287. }
  288. public function getQuery() {
  289. if ($this->_query) return clone($this->_query);
  290. // $ds = $this->_acl->getDataSource(); // TODO: only for TableAcl // TODO: move _parseSqlWhere to this class
  291. $filtrIsInstance = []; // $filtrIsInstance = [ $this->_acl->getNamespace() ];
  292. $filtrIsNotInstance = [];
  293. if (!empty($this->_params['f_is_instance'])) $filtrIsInstance = $this->_params['f_is_instance'];
  294. if (!empty($this->_params['f_is_not_instance'])) $filtrIsNotInstance = $this->_params['f_is_not_instance'];
  295. $this->_query = ACL::query($this->_acl)
  296. ->isInstance($filtrIsInstance)
  297. ->isNotInstance($filtrIsNotInstance);
  298. // ->join($instanceTable, 'i', [ 'rawJoin' => "i.pk = t.{$sqlPrimaryKey} and i.idInstance = {$idInstance}" ])
  299. // $this->_query->where($ds->_parseSqlWhere($params))
  300. DBG::log($this->_params, 'array', "AclQueryFeatures::getQuery \$this->_params");
  301. foreach ($this->_params as $k => $v) {
  302. // DBG::log(['v'=>$v, 'is_numeric' => is_numeric($k), 'is_int' => is_int($k), 'is_array' => is_array($v)], 'array', "AclQueryFeatures::getQuery \$this->_params[{$k}]");
  303. if (is_int($k) && is_array($v)) {
  304. $this->_query->where($v); // TODO: check format [$fieldName, $comparisonSign, $value]
  305. } else if (is_int($k) && null === $v) { // skip NULL
  306. } else if ('f_is_instance' === $k) { // parsed before
  307. } else if ('f_is_not_instance' === $k) { // parsed before
  308. } else if ('@instances' === $k) { // skip - select
  309. } else if ('cols' === $k) { // skip - select
  310. } else if ('f_@selected' === $k) { // skip - f_@selected
  311. Lib::loadClass('FeatureAttrSelected');
  312. $userSelectedTableName = FeatureAttrSelected::getAttributeTableName($typeName = $this->_acl->getNamespace(), $idUser = User::getID());
  313. $pkFieldName = $this->_acl->getPrimaryKeyField();
  314. if ('YES' === $v) {
  315. $this->_query->join($userSelectedTableName, 't_selected', [ 'rawJoin' => "t.{$pkFieldName} = t_selected.primaryKey" ]);
  316. } else if ('NO' === $v) {
  317. $this->_query->leftJoin($userSelectedTableName, 't_selected', [ 'rawJoin' => "t.{$pkFieldName} = t_selected.primaryKey" ]);
  318. $this->_query->where(['t_selected.primaryKey', 'is null']);
  319. } else {
  320. throw new Exception("Not implemented @selected filter value '{$v}'");
  321. }
  322. } else if ('f_' === substr($k, 0, 2) && is_string($v) && strlen($k) > 3) {
  323. $fieldName = substr($k, 2);
  324. $fieldType = $this->_acl->getXsdFieldType($fieldName);
  325. list($comparisonSign, $value) = $this->parseQueryValue($fieldName, $v, $fieldType);
  326. DBG::log([ $fieldName, $comparisonSign, $value, $fieldType ], 'array', "parseQueryValue");
  327. $this->_query->where([$fieldName, $comparisonSign, $value]);
  328. } else if ('sf_' === substr($k, 0, 3) && is_string($v) && strlen($k) > 4) {
  329. switch (substr($k, 3)) {
  330. case 'Msgs': $this->_query->where($this->parseSpecialFilterMsgs($v)); break;
  331. case 'Problemy': $this->_query->where($this->parseSpecialFilterProblemy($v)); break;
  332. case 'Status': $this->_query->where($this->parseSpecialFilterStatus($v)); break;
  333. case 'Trash': $this->_query->where($this->parseSpecialFilterTrash($v)); break;
  334. case 'Przypomnienia': $this->_query->where($this->parseSpecialFilterSpotkania($v)); break;
  335. case 'Access': break; // SKIP - used below
  336. default: {
  337. if (method_exists($this->_acl, 'parseSpecialFilter')) {
  338. $this->_query->where(
  339. $this->_acl->parseSpecialFilter(substr($k, 3), $v)
  340. );
  341. } else {
  342. throw new Exception("Not Implemented special filter '".substr($k, 3)."'");
  343. }
  344. }
  345. }
  346. } else if ('ogc:Filter' === $k) {
  347. $this->_query->where($this->parseOgcFilter($v));
  348. } else if ('primaryKey' === $k) {
  349. $fieldName = $this->_acl->getPrimaryKeyField();
  350. $fieldType = $this->_acl->getXsdFieldType($fieldName);
  351. if (is_array($v)) {
  352. DBG::log([ $fieldName, $v, $fieldType ], 'array', "parseQueryValue :: primaryKey, value is array");
  353. $this->_query->where([ $fieldName, 'or', array_map(function ($pk) use ($fieldName) {
  354. return [ $fieldName, '=', $pk ];
  355. }, $v) ]);
  356. } else {
  357. list($comparisonSign, $value) = $this->parseQueryValue($fieldName, $v, $fieldType);
  358. DBG::log([ $fieldName, $comparisonSign, $value, $fieldType ], 'array', "parseQueryValue");
  359. $this->_query->where([$fieldName, $comparisonSign, $value]);
  360. }
  361. } else if ('__backRef' === $k) { // skip - parse below
  362. } else if ('__childRef' === $k) { // skip - parse below
  363. } else if ('limit' === $k) {
  364. } else if ('limitstart' === $k) {
  365. } else if ('order_by' === $k) {
  366. } else if ('order_dir' === $k) {
  367. } else if ('sortBy' === $k) {
  368. } else {
  369. throw new Exception("Not Implemented param '{$k}' = '{$v}'");
  370. }
  371. }
  372. // sf_Access: if 'SHOW' then show all rows, but data with ***
  373. if ('SHOW' !== V::get('sf_Access', '', $this->_params)) $this->_query->where($this->parseSpecialFilterAccess());
  374. if (array_key_exists('__backRef', $this->_params)) {
  375. $backRef = $this->_params['__backRef'];
  376. if (!is_array($backRef)) throw new Exception("Wrong back ref structure - expected array");
  377. if (empty($backRef['namespace'])) throw new Exception("Wrong back ref structure - missing namespace");
  378. if (empty($backRef['primaryKey'])) throw new Exception("Wrong back ref structure - missing primaryKey");
  379. if (empty($backRef['fieldName'])) throw new Exception("Wrong back ref structure - missing fieldName");
  380. // TODO: $this->_query->where([ '__backRef' ]); or $this->_query->join([ '__backRef' ]);
  381. $refAcl = ACL::getAclByNamespace($backRef['namespace']);
  382. if ($refAcl->getSourceName() !== $this->_acl->getSourceName()) throw new Exception("Not implemented join with different source");
  383. $refTable = ACL::getRefTable($refAcl->getNamespace(), $backRef['fieldName']);
  384. // TODO: 'in' operator? // $this->_query->where($pkField, 'in', "");
  385. $sqlPk = $this->getAclSqlPrimaryKeyField();
  386. $sqlBackRefPk = DB::getPDO()->quote($backRef['primaryKey']);
  387. $limit = V::get('limit', 10, $this->_params, 'int');
  388. $offset = V::get('limitstart', 0, $this->_params, 'int');
  389. DBG::log(['limit' => $limit, 'offset' => $offset], 'array', "DBG: limit with __backRef \$this->_params");
  390. $sqlLimit = "limit {$limit} offset {$offset}";
  391. // $this->_query->where("
  392. // t.{$sqlPk} in (
  393. // select refTable.REMOTE_PRIMARY_KEY
  394. // from `{$refTable}` refTable
  395. // where refTable.PRIMARY_KEY = {$sqlBackRefPk}
  396. // order by refTable.REMOTE_PRIMARY_KEY DESC -- TODO refTable.SORT_PRIO
  397. // -- {$sqlLimit} -- BUG MariaDB not support limit in subquery
  398. // )
  399. // ");
  400. // TODO: convert `where t.ID in ( ... )` into `join` and `order by`
  401. // join `CRM__#REF_TABLE__120_VIEW` refTable on (refTable.REMOTE_PRIMARY_KEY = t.ID and refTable.PRIMARY_KEY = '37' )
  402. DBG::log([
  403. 'ACL::getRefTable(...)' => [$refAcl->getNamespace(), $backRef['fieldName']],
  404. '$backRef' => $backRef,
  405. '$refTable' => $refTable
  406. ], 'array', "DBG: join \$join class(".get_class($join).")");
  407. $this->_query->join($refTable, 'refTable', [ 'rawJoin' => "t.{$sqlPk} = refTable.REMOTE_PRIMARY_KEY and refTable.PRIMARY_KEY = {$sqlBackRefPk}" ]);
  408. // moved to getItems: $this->_query->orderBy("refTable.REMOTE_PRIMARY_KEY DESC"); // TODO: order by refTable.SORT_PRIO
  409. }
  410. if (array_key_exists('__childRef', $this->_params)) {
  411. $childRef = $this->_params['__childRef'];
  412. if (!is_array($childRef)) throw new Exception("Wrong child ref structure - expected array");
  413. if (empty($childRef['namespace'])) throw new Exception("Wrong child ref structure - missing namespace");
  414. if (empty($childRef['primaryKey'])) throw new Exception("Wrong child ref structure - missing primaryKey");
  415. $refAcl = ACL::getAclByNamespace($childRef['namespace']);
  416. if ($refAcl->getSourceName() !== $this->_acl->getSourceName()) throw new Exception("Not implemented join with different source");
  417. $refTypeName = Api_WfsNs::typeName($childRef['namespace']);
  418. DBG::log("\$refTypeName({$refTypeName})");
  419. $refTable = ACL::getRefTable($this->_acl->getNamespace(), $refTypeName);
  420. // TODO: 'in' operator? // $this->_query->where($pkField, 'in', "");
  421. $sqlPk = $this->getAclSqlPrimaryKeyField();
  422. $sqlChildRefPk = DB::getPDO()->quote($childRef['primaryKey']);
  423. $this->_query->where("
  424. t.{$sqlPk} in (
  425. select refTable.PRIMARY_KEY
  426. from `{$refTable}` refTable
  427. where refTable.REMOTE_PRIMARY_KEY = {$sqlChildRefPk}
  428. )
  429. ");
  430. }
  431. DBG::log($this->_query, 'array', "TODO: optimize \$this->_query");
  432. $this->_USE_TEMPORARY_TABLE = false; // TODO: ...
  433. if ($this->_USE_TEMPORARY_TABLE) {
  434. $sortBy = ($this->hasParam('sortBy')) ? $this->getParam('sortBy') : null;
  435. if (!$sortBy) {
  436. $sortBy = $this->hasParam('order_by')
  437. ? ( $this->hasParam('order_dir')
  438. ? $this->getParam('order_by') . " " . $this->getParam('order_dir')
  439. : $this->getParam('order_by')
  440. )
  441. : '';
  442. }
  443. $TMP_TABLE_NAME = $this->_acl->getRootTableName() . "__#TEMPORARY";
  444. DBG::log("\$TMP_TABLE_NAME='{$TMP_TABLE_NAME}'");
  445. Lib::loadClass('AntAclBase');
  446. if ($this->_acl instanceof AntAclBase) {
  447. $baseSql = $this->_query->generateSql();
  448. DB::getPDO()->execSql("
  449. create temporary table `{$TMP_TABLE_NAME}`
  450. {$baseSql}
  451. ");
  452. DBG::log(DB::getPDO()->fetchValue("select count(*) as cnt from `{$TMP_TABLE_NAME}`"), 'array', "select count(*) cnt from '{$TMP_TABLE_NAME}'");
  453. $tmpAclFrom = clone($this->_acl);
  454. $tmpAclFrom->_rootTableName = $TMP_TABLE_NAME;
  455. // $tmpAclFrom = AntAclBase::buildInstance($this->_acl->getID(), [
  456. // 'name' => $this->_acl->getName(),
  457. // '_rootTableName' => $TMP_TABLE_NAME,
  458. // 'idDatabase' => $this->_acl->getDatabaseID(),
  459. // 'namespace' => $this->_acl->getNamespace(),
  460. // 'primaryKey' => $this->_acl->getPrimaryKeyField(),
  461. // 'field' => $this->_acl->getFields(),
  462. // ]);
  463. Lib::loadClass('AclQueryBuilder');
  464. $query = new AclQueryBuilder();
  465. $query->from($tmpAclFrom, $prefix = 't');
  466. DBG::log($query, 'array', "TODO: \$query by temporary table");
  467. // $this->_query = $query;
  468. // return $this->_query;
  469. }
  470. }
  471. return clone($this->_query);
  472. }
  473. public function getTotal() {
  474. $this->_beforeFetchData();
  475. if ($this->_legacyMode) return $this->_acl->getTotal($this->_params);
  476. if (null !== $this->_total) return $this->_total;
  477. $this->_total = $this->getQuery()->fetchTotal();
  478. return $this->_total;
  479. }
  480. public function hasParam($key) { return !empty($this->_params[$key]); }
  481. public function getParam($key) { return V::get($key, '', $this->_params); }
  482. public function getItems() {
  483. $this->_logToFile('getItems() ...');
  484. $this->_beforeFetchData();
  485. if ($this->_legacyMode) return $this->_acl->getItems($this->_params);// TODO: array_map( $r => (array)$r )
  486. // 'limit' => 10,
  487. // 'limitstart' => 0,
  488. // 'order_by' => 'ID',
  489. // 'order_dir' => 'desc',
  490. // TODO: sortBy from wfs query
  491. $sortBy = ($this->hasParam('sortBy')) ? $this->getParam('sortBy') : null;
  492. if (!$sortBy && array_key_exists('__backRef', $this->_params)) {
  493. $sortBy = "refTable.REMOTE_PRIMARY_KEY DESC"; // TODO: order by refTable.SORT_PRIO
  494. }
  495. if (!$sortBy) {
  496. $sortBy = $this->hasParam('order_by')
  497. ? ( $this->hasParam('order_dir')
  498. ? $this->getParam('order_by') . " " . $this->getParam('order_dir')
  499. : $this->getParam('order_by')
  500. )
  501. : '';
  502. }
  503. $limit = V::get('limit', 10, $this->_params, 'int');
  504. $offset = V::get('limitstart', 0, $this->_params, 'int');
  505. DBG::log(['params' => $this->_params, 'sortBy' => $sortBy, 'limit' => $limit, 'offset' => $offset], 'array', '$this->_params');
  506. $select = $this->prepareSelect();
  507. DBG::log($select, 'array', "\$select is(TableAcl)=(".($this->_acl instanceof TableAcl).")");
  508. // DBG::log($this->getQuery(), 'array', "\$select is(TableAcl)=(".($this->_acl instanceof TableAcl).") \$this->getQuery()");
  509. $items = $this->fetchRowsRefs(
  510. $this->getQuery()
  511. ->select(array_merge(['@primaryKey'], $select))
  512. ->limit($limit)
  513. ->offset($offset)
  514. ->orderBy($sortBy)
  515. ->fetchAll()
  516. );
  517. $this->_logToFile('getItems() fetched.');
  518. return $items;
  519. }
  520. public function getRawQueryPrimaryKeys() {
  521. if ($this->_legacyMode) throw new Exception("getRawQueryPrimaryKeys not supported in legacyMode");
  522. $sortBy = ($this->hasParam('sortBy')) ? $this->getParam('sortBy') : null;
  523. if (!$sortBy && array_key_exists('__backRef', $this->_params)) {
  524. $sortBy = "refTable.REMOTE_PRIMARY_KEY DESC"; // TODO: order by refTable.SORT_PRIO
  525. }
  526. if (!$sortBy) {
  527. $sortBy = $this->hasParam('order_by')
  528. ? ( $this->hasParam('order_dir')
  529. ? $this->getParam('order_by') . " " . $this->getParam('order_dir')
  530. : $this->getParam('order_by')
  531. )
  532. : '';
  533. }
  534. $limit = V::get('limit', 10, $this->_params, 'int');
  535. $offset = V::get('limitstart', 0, $this->_params, 'int');
  536. return $this->getQuery()
  537. ->select(['@primaryKey'])
  538. ->limit($limit)
  539. ->offset($offset)
  540. ->orderBy($sortBy)
  541. ->generateSql();
  542. }
  543. function _beforeFetchData() {
  544. if (method_exists($this->_acl, 'onBeforeFetchData')) $this->_acl->onBeforeFetchData();
  545. }
  546. public function getItem($primaryKey) { // TODO: throw exception if not found?
  547. $this->_beforeFetchData();
  548. if ($this->_legacyMode) return (array)$this->_acl->getItem($primaryKey, $this->_params);
  549. $select = $this->prepareSelect();
  550. $pkField = $this->_acl->getPrimaryKeyField();
  551. return $this->fetchRowRefs(
  552. $this->getQuery()
  553. ->select($select)
  554. ->where([$pkField, '=', $primaryKey])
  555. ->fetchFirst()
  556. );
  557. }
  558. public function prepareSelect() { // TODO: replace with getSelectLocal
  559. // TODO: select from params: 'cols' => [ fieldName, ... ]
  560. // TODO: select from params: '@instances' => 1
  561. // TODO: if no fields set, then '*'
  562. // TODO: select must contain primaryKey
  563. return $this->getSelectLocal();
  564. }
  565. public function getSelectLocal() { // @returns [ $fieldName, ... ]
  566. // TODO: rawSelect!
  567. if (null !== $this->_selectLocalFields) return $this->_selectLocalFields;
  568. $this->_selectLocalFields = [];
  569. $todoFetchAllCols = false; // select t.*
  570. if (!empty($this->_params['cols'])) {
  571. if (in_array('*', $this->_params['cols'])) $todoFetchAllCols = true;
  572. $acl = $this->_acl;
  573. $this->_selectLocalFields = array_filter($this->_params['cols'], function ($fieldQuery) use ($acl) {
  574. if ('*' === $fieldQuery) return false;
  575. list($fieldName, $subFieldQuery) = explode('/', $fieldQuery, 2);
  576. if (!empty($subFieldQuery)) return false;
  577. return $acl->isLocalField($fieldName);
  578. });
  579. }
  580. if (!empty($this->_params['@instances'])) $this->_selectLocalFields[] = '@instances';
  581. if (empty($this->_selectLocalFields)) $todoFetchAllCols = true;
  582. if (1 === count($this->_selectLocalFields) && in_array('@instances', $this->_selectLocalFields)) $todoFetchAllCols = true;
  583. // if ($this->_acl instanceof TableAcl) {
  584. // $rawSelect = $this->_acl->getDataSource()->_getSqlCols();
  585. // DBG::log($rawSelect, 'string', "DBG raw select");
  586. // if ('*' !== $rawSelect && 't.*' !== $rawSelect) {
  587. // $this->_selectLocalFields['rawSelect'] = $rawSelect;
  588. // }
  589. // }
  590. if (!empty($this->_params['cols'])) DBG::log($this->_params['cols'], 'array', "\$this->_params[cols] (" . ($this->_acl ? $this->_acl->getNamespace() : 'unknown') . ")");
  591. if ($todoFetchAllCols) {
  592. // $this->_selectLocalFields[] = '*'; // TODO: select all $this->from local fields
  593. DBG::log($this->_acl->getLocalFieldList(), 'array', "\$this->_acl->getLocalFieldList()");
  594. foreach ($this->_acl->getLocalFieldList() as $localFieldName) {
  595. $this->_selectLocalFields[] = $localFieldName;
  596. }
  597. }
  598. $primaryKeyField = $this->getAclSqlPrimaryKeyField();
  599. if (!in_array($primaryKeyField, $this->_selectLocalFields)) {
  600. $this->_selectLocalFields[] = $primaryKeyField;
  601. }
  602. // TODO: always add A_ADM_COMPANY, A_CLASSIFIED, L_APPOITMENT_USER ?
  603. DBG::log($this->_selectLocalFields, 'array', '$this->_selectLocalFields');
  604. return $this->_selectLocalFields;
  605. }
  606. public function getSelectRemote() { // @returns [ $fieldName => [ $fieldName, ... ] ]
  607. if (null !== $this->_selectRemote) return $this->_selectRemote;
  608. $this->_selectRemote = [];
  609. if (!empty($this->_params['cols'])) {
  610. $cols = $this->_params['cols'];
  611. if (in_array('*', $cols)) {
  612. $this->_selectLocalFields[] = '*';
  613. $cols = array_filter($cols, function ($fieldQuery) { return '*' !== $fieldQuery; });
  614. }
  615. $acl = $this->_acl;
  616. $cols = array_filter($cols, function ($fieldQuery) use ($acl) {
  617. list($fieldName, $subFieldQuery) = explode('/', $fieldQuery, 2);
  618. // if (empty($subFieldQuery)) return false;
  619. return !$acl->isLocalField($fieldName);
  620. });
  621. foreach ($cols as $fieldQuery) { // group by fieldName
  622. list($fieldName, $subFieldQuery) = explode('/', $fieldQuery, 2);
  623. if (!array_key_exists($fieldName, $this->_selectRemote)) $this->_selectRemote[$fieldName] = [];
  624. if (!empty($subFieldQuery)) $this->_selectRemote[$fieldName][] = $subFieldQuery;
  625. }
  626. }
  627. return $this->_selectRemote;
  628. }
  629. public function fetchRowsRefs($rows) {
  630. $pkField = $this->_acl->getPrimaryKeyField();
  631. $namespace = $this->_acl->getNamespace();
  632. $this->_foundFeatures = array_merge($this->_foundFeatures, array_map(function ($row) use ($namespace, $pkField) {
  633. return "{$namespace}." . V::get($pkField, '', $row);
  634. }, $rows));
  635. DBG::log($this->_foundFeatures, 'array', "_foundFeatures");
  636. return array_map([ $this, 'fetchRowRefs' ], $rows);
  637. }
  638. public function fetchRowRefs($row) {
  639. try {
  640. return $this->_fetchRowRefs($row);
  641. } catch (Exception $e) {
  642. DBG::log($e);
  643. }
  644. return $row;
  645. }
  646. public function _fetchRowRefs($row) {
  647. if (!$row) return $row;
  648. $sqlPk = $this->getAclSqlPrimaryKeyField();
  649. $primaryKey = $row[$sqlPk];
  650. DBG::log($row, 'array', "DBG primaryKey '{$primaryKey}'");
  651. if (!$primaryKey) throw new Exception("Missing primaryKey");
  652. $defaultRefLimit = 10; // TODO: get from $this->_params and pass to nested buildQuery
  653. $refLimitPlus1 = $defaultRefLimit + 1;
  654. foreach ($this->getSelectRemote() as $fieldName => $cols) {
  655. DBG::log($cols, 'array', "add select remote '{$fieldName}' \$cols");
  656. $xsdType = $this->_acl->getXsdFieldType($fieldName);
  657. if ('ref:' === substr($xsdType, 0, 4) && empty($cols)) {
  658. DBG::log("add remote xlink's '{$fieldName}' \$items[{$primaryKey}] ...");
  659. $refTable = ACL::getRefTable($this->_acl->getNamespace(), $fieldName);
  660. if (!$refTable) DBG::log("BUG: Missing refTable in add remote xlink's '{$fieldName}' \$items[{$primaryKey}]");
  661. if ($refTable) {
  662. $xlinks = DB::getPDO()->fetchAll("
  663. select r.REMOTE_PRIMARY_KEY
  664. from `{$refTable}` r
  665. where r.PRIMARY_KEY = '{$primaryKey}'
  666. order by r.REMOTE_PRIMARY_KEY DESC -- TODO r.SORT_PRIO
  667. limit {$refLimitPlus1}
  668. ");
  669. DBG::log($xlinks, 'array', "add remote xlink's for '{$fieldName}' \$items[{$primaryKey}]");
  670. $row[$fieldName] = array_map(function ($refInfo) use ($fieldName) {
  671. $ns = Core_AclHelper::parseTypeName($fieldName);
  672. return [
  673. // '_ns' => $ns,
  674. 'xlink' => "{$ns['url']}#{$ns['name']}.{$refInfo['REMOTE_PRIMARY_KEY']}",
  675. ];
  676. }, $xlinks);
  677. if (count($xlinks) > $defaultRefLimit) DBG::log('TODO: xlink FETCH MORE DATA...');
  678. if (count($xlinks) > $defaultRefLimit) $row[$fieldName][] = [ 'p5:links' => [
  679. 'p5:next' => [
  680. '@typeName' => $fieldName,
  681. '@backRefNS' => $this->_acl->getNamespace(),
  682. '@backRefPK' => $primaryKey,
  683. '@startIndex' => $defaultRefLimit,
  684. '@maxFeatures' => $defaultRefLimit,
  685. 'value' => Request::getPathUri() . "wfs-data.php/" . "?SERVICE=WFS&VERSION=1.0.0&TYPENAME={$fieldName}&REQUEST=GetFeature&backRefNS=".$this->_acl->getNamespace()."&backRefPK={$primaryKey}&backRefField={$fieldName}&maxFeatures={$defaultRefLimit}&startIndex={$defaultRefLimit}",
  686. ],
  687. ] ];
  688. DBG::log($row[$fieldName], 'array', "remote xlinks for \$items[{$primaryKey}][{$fieldName}]");
  689. }
  690. } else if ('ref:' === substr($xsdType, 0, 4) && !empty($cols)) {
  691. $refAcl = ACL::getAclByTypeName($fieldName);
  692. $refQuery = $refAcl->buildQuery([
  693. 'cols' => $cols,
  694. '__backRef' => [
  695. 'namespace' => $this->_acl->getNamespace(),
  696. 'primaryKey' => $primaryKey,
  697. 'fieldName' => $fieldName,
  698. ],
  699. 'limit' => $refLimitPlus1,
  700. 'skipFeaturesAsXlink' => $this->_foundFeatures
  701. // TODO: add $defaultRefLimit + 1
  702. ]);
  703. $totalRefs = $refQuery->getTotal();
  704. $items = $refQuery->getItems([ 'limit' => $defaultRefLimit ]);
  705. // TODO: if (count($items) > $defaultRefLimit) // TODO: add item for GUI - has more data + wfs link to fetch more (offset)
  706. DBG::log($items, 'array', "add remote items '{$fieldName}' \$items[{$primaryKey}][{$fieldName}] total({$totalRefs})");
  707. if ($totalRefs > $defaultRefLimit) {
  708. DBG::log('TODO: resolve recurse fetch more data link');
  709. array_pop($items); // remove last item
  710. $items[] = [ 'p5:links' => [
  711. 'p5:next' => [
  712. '@typeName' => $fieldName,
  713. '@backRefNS' => $this->_acl->getNamespace(),
  714. '@backRefPK' => $primaryKey,
  715. '@startIndex' => $defaultRefLimit,
  716. '@maxFeatures' => $defaultRefLimit,
  717. 'value' => Request::getPathUri() . "wfs-data.php/" . "?SERVICE=WFS&VERSION=1.0.0&TYPENAME={$fieldName}&REQUEST=GetFeature&backRefNS=".$this->_acl->getNamespace()."&backRefPK={$primaryKey}&backRefField={$fieldName}&maxFeatures={$defaultRefLimit}&startIndex={$defaultRefLimit}",
  718. ],
  719. ] ];
  720. }
  721. $refNs = $refAcl->getNamespace();
  722. $refPk = $refAcl->getPrimaryKeyField();
  723. $this__hasFeatureId = [ $this, 'hasFeatureId' ];
  724. $this__addFeatureId = [ $this, 'addFeatureId' ];
  725. $row[$fieldName] = array_map(function ($item) use ($fieldName, $refNs, $refPk, $this__hasFeatureId, $this__addFeatureId) {
  726. if (1 === count($item) && !empty($item['p5:links'])) return $item;
  727. $pk = V::get($refPk, '', $item);
  728. $featureId = "{$refNs}.{$pk}";
  729. if (!$this__hasFeatureId($featureId)) {
  730. $this__addFeatureId($featureId);
  731. return $item;
  732. } else {
  733. $ns = Core_AclHelper::parseTypeName($fieldName);
  734. return [
  735. 'xlink' => "{$ns['url']}#{$ns['name']}.{$pk}",
  736. ];
  737. }
  738. }, $items);
  739. } else { // TODO: field is not ref
  740. DBG::log($items, 'array', "NotImplemented - add remote items for non ref field \$items[{$primaryKey}][{$fieldName}]?");
  741. }
  742. }
  743. return $row;
  744. }
  745. public function hasFeatureId($featureId) {
  746. return in_array($featureId, $this->_foundFeatures);
  747. }
  748. public function addFeatureId($featureId) {
  749. $this->_foundFeatures[] = $featureId;
  750. DBG::log($this->_foundFeatures, 'array', "addFeatureId({$featureId})");
  751. }
  752. public function getAclSqlPrimaryKeyField() {
  753. return ($this->_acl instanceof Core_AclBase)
  754. ? $this->_acl->getSqlPrimaryKeyField()
  755. : 'ID';
  756. }
  757. }