PageRenderTime 40ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 0ms

/wa-system/contact/waContactsCollection.class.php

http://github.com/webasyst/webasyst-framework
PHP | 1383 lines | 1152 code | 111 blank | 120 comment | 150 complexity | c765367dd89892a6551678a924d055bd MD5 | raw file
Possible License(s): LGPL-3.0, CC-BY-3.0, BSD-3-Clause, MIT

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. class waContactsCollection
  3. {
  4. protected $hash;
  5. protected $fields = array();
  6. protected $order_by = '';
  7. protected $group_by;
  8. protected $having = array();
  9. protected $where;
  10. protected $where_fields = array();
  11. protected $joins;
  12. protected $join_index = array();
  13. protected $info = array();
  14. protected $title = '';
  15. protected $count;
  16. protected $options = array(
  17. );
  18. protected $update_count;
  19. protected $post_fields;
  20. protected $prepared;
  21. protected $alias_index = array();
  22. protected $models;
  23. protected $left_joins = array();
  24. /**
  25. * Constructor for collection of contacts
  26. *
  27. * @param string $hash - search hash
  28. * @param array $options
  29. *
  30. * - string $options['transform_phone_prefix'] - String, that say what transformation phone prefix rule(s) apply when search by phone prefix (search/phone*=<phone_prefix> OR search/phone@*=<phone_prefix>)
  31. * Variants of available values of string
  32. * - 'all_domains' - apply transformation prefix rules of all domains. Need for search contacts in backend
  33. * - 'current_domain' - apply transformation prefix rule of current domain. May come useful for frontend search
  34. * - other string, that supposed to be domain - apply transformation prefix rule of concrete domain. May come useful for tests
  35. *
  36. *
  37. *
  38. * @example
  39. * All contacts where name contains John
  40. * $collection = new waContactsCollection('/search/name*=John/');
  41. *
  42. * All contacts in the list with id 100500
  43. * $collection = new waContactsCollection('/list/100500/');
  44. *
  45. * Contacts with ids from list
  46. * $collection = new waContactsCollection('/id/1,10,100,500/');
  47. * or
  48. * $collection = new waContactsCollection('/search/id=1,10,100,500/');
  49. *
  50. * All contacts
  51. * $collection = new waContactsCollection();
  52. */
  53. public function __construct($hash = '', $options = array())
  54. {
  55. foreach ($options as $k => $v) {
  56. $this->options[$k] = $v;
  57. }
  58. $this->setHash($hash);
  59. }
  60. public function setHash($hash)
  61. {
  62. if (is_array($hash)) {
  63. $hash = '/id/'.implode(',', $hash);
  64. }
  65. if (substr($hash, 0, 1) == '#') {
  66. $hash = substr($hash, 1);
  67. }
  68. $this->hash = trim($hash, '/');
  69. if (substr($this->hash, 0 ,9) == 'contacts/') {
  70. $this->hash = substr($this->hash, 9);
  71. }
  72. if ($this->hash == 'all') {
  73. $this->hash = '';
  74. }
  75. $this->hash = explode('/', $this->hash, 2);
  76. if (isset($this->hash[1])) {
  77. $escapedS = 'ESCAPED_BACKSLASH';
  78. while(FALSE !== strpos($this->hash[1], $escapedS)) {
  79. $escapedS .= rand(0, 9);
  80. }
  81. $this->hash[1] = str_replace('\/', $escapedS, $this->hash[1]);
  82. }
  83. if ($this->hash[0] !== 'search' && isset($this->hash[1]) && strpos($this->hash[1], '/')) {
  84. $this->hash[1] = substr($this->hash[1], 0, strpos($this->hash[1], '/'));
  85. }
  86. if (isset($this->hash[1])) {
  87. $this->hash[1] = str_replace($escapedS, '/', $this->hash[1]);
  88. }
  89. }
  90. /**
  91. * Returns count of the all contacts in collection
  92. *
  93. * @return int
  94. */
  95. public function count()
  96. {
  97. if ($this->count === null) {
  98. $sql = $this->getSQL();
  99. if ($this->getHaving()) {
  100. $sql .= $this->getGroupBy();
  101. $sql .= $this->getHaving();
  102. $sql = "SELECT COUNT(t.id) FROM (SELECT c.id {$sql}) t";
  103. } else {
  104. $sql = "SELECT COUNT(".($this->joins ? 'DISTINCT ' : '')."c.id) ".$sql;
  105. }
  106. //header("X-SQL-COUNT:". $sql);
  107. $this->count = (int)$this->getModel()->query($sql)->fetchField();
  108. if ($this->update_count && $this->count != $this->update_count['count']) {
  109. $this->update_count['model']->updateCount($this->update_count['id'], $this->count);
  110. }
  111. }
  112. return $this->count;
  113. }
  114. public function getTitle()
  115. {
  116. if ($this->title === null) {
  117. $this->prepare();
  118. }
  119. return $this->title;
  120. }
  121. public function addField($field, $alias)
  122. {
  123. $this->fields[$alias] = $field;
  124. }
  125. protected function getFields($fields)
  126. {
  127. $contact_model = $this->getModel();
  128. if (substr($fields, 0, 1) == '*') {
  129. $extra_fileds = substr($fields, 2);
  130. $fields = $contact_model->getMetadata();
  131. unset($fields['password']);
  132. $fields = array_keys($fields);
  133. foreach ($fields as &$f) {
  134. $f = 'c.'.$f;
  135. }
  136. unset($f);
  137. $this->post_fields['_internal'] = array('_online_status');
  138. $this->post_fields['email'] = array('email');
  139. $this->post_fields['data'] = array();
  140. if ($extra_fileds) {
  141. $extra_fileds = $this->getFields($extra_fileds);
  142. $fields = array_merge($fields, explode(",", $extra_fileds));
  143. $fields = array_unique($fields);
  144. }
  145. $result = implode(",", $fields);
  146. foreach($this->fields as $alias => $expr) {
  147. $result .= ",".$expr.' AS '.$alias;
  148. }
  149. return $result;
  150. }
  151. $required_fields = array('id' => 'c', 'is_company' => 'c'); // field => table, to be added later in any case
  152. if (!is_array($fields)) {
  153. $fields = explode(",", $fields);
  154. }
  155. // Add required fields to select and delete fields for getting data after query
  156. foreach ($fields as $i => $f) {
  157. if (!$contact_model->fieldExists($f)) {
  158. if ($f === 'email' || substr($f, 0, 6) === 'email.') {
  159. if ($f === 'email') {
  160. $this->post_fields['email'][] = $f; // OLD style behavior
  161. } elseif ($f === 'email.*') {
  162. $this->post_fields['email'][] = '*';
  163. } else {
  164. $this->post_fields['email'][] = substr($f, 6);
  165. }
  166. } elseif ($f == '_online_status') {
  167. $required_fields['last_datetime'] = 'c';
  168. $required_fields['login'] = 'c';
  169. $this->post_fields['_internal'][] = $f;
  170. } elseif ($f == '_access') {
  171. $this->post_fields['_internal'][] = $f;
  172. } elseif ($f == 'photo_url' || substr($f, 0, 10) == 'photo_url_') {
  173. $required_fields['photo'] = 'c';
  174. $this->post_fields['_internal'][] = $f;
  175. } elseif ($f == '_event') {
  176. $this->post_fields['_internal'][] = $f;
  177. } else {
  178. $this->post_fields['data'][] = $f;
  179. }
  180. unset($fields[$i]);
  181. continue;
  182. }
  183. if (isset($required_fields[$f])) {
  184. $fields[$i] = ($required_fields[$f] ? $required_fields[$f]."." : '').$f;
  185. unset($required_fields[$f]);
  186. } elseif ($contact_model->fieldExists($f)) {
  187. $fields[$i] = 'c.' . $f;
  188. }
  189. }
  190. foreach ($required_fields as $field => $table) {
  191. $fields[] = ($table ? $table."." : '').$field;
  192. }
  193. foreach($this->fields as $alias => $expr) {
  194. $fields[] = $expr.' AS '.$alias;
  195. }
  196. return implode(",", $fields);
  197. }
  198. /**
  199. * Get data for contacts in this collection.
  200. * @param string|array $fields
  201. *
  202. * If need extract other columns of email row, NOT just email value use dot notation
  203. * Like this
  204. * email.status, email.email
  205. *
  206. * You can use even '*"
  207. * Like this
  208. * email.*
  209. *
  210. * @example
  211. * $col->getContacts('*,email.email,email.status')
  212. * OR
  213. * $col->getContacts('*,email.*')
  214. *
  215. * If use OLD notation - without dot (.) collection extract only email values
  216. * $col->getContacts('*') (email values will be extracted - case we skip 'email' in fields list)
  217. * $col->getContacts('*,email') (email values will be extracted - same as we skipped 'email')
  218. * $col->getContacts('*,email.email') (email values will be extracted - same as previous )
  219. *
  220. * APPLICABLE only for email, cause we need save just 'email' notation for backward compatibility
  221. *
  222. * For other data fields we extract raw data from DB
  223. *
  224. * @param int $offset
  225. * @param int $limit
  226. * @return array [contact_id][field] = field value in appropriate field format
  227. * @throws waException
  228. */
  229. public function getContacts($fields = "id", $offset = 0, $limit = 50)
  230. {
  231. $sql = "SELECT ".$this->getFields($fields)."\n".$this->getSQL();
  232. $sql .= "\n".$this->getGroupBy();
  233. $sql .= "\n".$this->getHaving();
  234. $sql .= "\n".$this->getOrderBy();
  235. $sql .= "\nLIMIT ".($offset ? $offset.',' : '').(int)$limit;
  236. //header("X-SQL-". mt_rand() . ": ". str_replace("\n", " ", $sql));
  237. $data = $this->getModel()->query($sql)->fetchAll('id');
  238. $ids = array_keys($data);
  239. // Update group and category count, if needed
  240. if ($offset == 0 && $this->update_count && $limit > count($data) && count($data) != $this->update_count['count']) {
  241. $this->update_count['model']->updateCount($this->update_count['id'], count($data));
  242. }
  243. //
  244. // Load fields from other storages
  245. //
  246. if ($ids && $this->post_fields) {
  247. // $fill[table][field] = null
  248. // needed for all rows to always contain all apropriate keys
  249. // in case when we're asked to load all fields from that table
  250. $fill = array_fill_keys(array_keys($this->post_fields), array());
  251. foreach (waContactFields::getAll('enabled') as $fid => $field) {
  252. /**
  253. * @var waContactField $field
  254. */
  255. $fill[$field->getStorage(true)][$fid] = false;
  256. }
  257. foreach ($this->post_fields as $table => $fields) {
  258. if ($table == '_internal') {
  259. foreach (array_unique($fields) as $f) {
  260. /**
  261. * @var $f string
  262. */
  263. if ($f == 'photo_url' || substr($f, 0, 10) == 'photo_url_') {
  264. if ($f == 'photo_url') {
  265. $size = null;
  266. } else {
  267. $size = substr($f, 10);
  268. }
  269. $retina = isset($this->options['photo_url_2x']) ? $this->options['photo_url_2x'] : null;
  270. foreach ($data as $id => &$v) {
  271. $v[$f] = waContact::getPhotoUrl($id, $v['photo'], $size, $size, $v['is_company'] ? 'company' : 'person', $retina);
  272. }
  273. unset($v);
  274. } else {
  275. switch($f) {
  276. case '_online_status':
  277. $llm = new waLoginLogModel();
  278. $timeout = waUser::getOption('online_timeout');
  279. $contact_ids_map = $llm->select('DISTINCT contact_id')
  280. ->where('contact_id IN (?)', array($ids))
  281. ->where('datetime_out IS NULL')
  282. ->fetchAll('contact_id');
  283. $contacts_idle = (new waContactSettingsModel())->getByField([
  284. 'contact_id' => array_keys($contact_ids_map),
  285. 'app_id' => 'webasyst',
  286. 'name' => 'idle_since'
  287. ], 'contact_id');
  288. foreach($data as &$v) {
  289. $v['_online_status'] = 'offline';
  290. // Ever logged in?
  291. if (isset($v['last_datetime']) && $v['last_datetime'] && $v['last_datetime'] != '0000-00-00 00:00:00') {
  292. // Were active in the last 5 minutes?
  293. if (time() - strtotime($v['last_datetime']) < $timeout) {
  294. // Make sure user didn't log out
  295. if (isset($contact_ids_map[$v['id']])) {
  296. $v['_online_status'] = 'online';
  297. if (isset($contacts_idle[$v['id']])) {
  298. $v['_online_status'] = 'idle';
  299. }
  300. }
  301. }
  302. }
  303. }
  304. unset($v);
  305. break;
  306. case '_access':
  307. $rm = new waContactRightsModel();
  308. $accessStatus = $rm->getAccessStatus($ids);
  309. foreach($data as $id => &$v) {
  310. if (!isset($accessStatus[$id])) {
  311. $v['_access'] = '';
  312. continue;
  313. }
  314. $v['_access'] = $accessStatus[$id];
  315. }
  316. unset($v);
  317. break;
  318. case '_event':
  319. $cem = new waContactEventsModel();
  320. $events = $cem->getEventByContact($ids);
  321. $events_by_contacts = array();
  322. foreach ($events as $id=>$e) {
  323. if (empty($events_by_contacts[$e['contact_id']])) {
  324. $events_by_contacts[$e['contact_id']] = $e;
  325. }
  326. }
  327. foreach($data as $id => &$v) {
  328. if (!isset($events_by_contacts[$id])) {
  329. $v['_event'] = '';
  330. continue;
  331. }
  332. $v['_event'] = $events_by_contacts[$id];
  333. }
  334. unset($v);
  335. break;
  336. default:
  337. throw new waException('Unknown internal field: '.$f);
  338. }
  339. }
  340. }
  341. continue;
  342. }
  343. if ($table === 'email') {
  344. $model = $this->getModel('email');
  345. $columns = array();
  346. foreach ($fields as $field) {
  347. if ($field === '*') {
  348. $columns = array_keys($model->getMetadata());
  349. break;
  350. }
  351. if ($model->fieldExists($field)) {
  352. $columns[] = $field;
  353. }
  354. }
  355. // always present, cause it is important field
  356. $columns[] = 'email';
  357. $columns = array_unique($columns);
  358. $all_emails = $model->getByField('contact_id', $ids, true);
  359. // fill each contact by empty 'email'
  360. foreach ($data as $contact_id => &$contact) {
  361. $contact['email'] = array();
  362. }
  363. unset($contact);
  364. // merge into contacts info about emails AND take into account columns array
  365. foreach ($all_emails as $email_row) {
  366. if (isset($data[$email_row['contact_id']])) {
  367. if ($columns === array('email')) {
  368. // OLD style behavior case
  369. $data[$email_row['contact_id']]['email'][$email_row['sort']] = $email_row['email'];
  370. } else {
  371. $email_info = array();
  372. foreach ($columns as $column) {
  373. if ($column != 'id' && $column != 'contact_id' && $column != 'sort') {
  374. $email_info[$column] = $email_row[$column];
  375. }
  376. }
  377. // just in case if some contact field formatter consume only 'value'
  378. // see for example
  379. $email_info['value'] = $email_info['email'];
  380. $data[$email_row['contact_id']]['email'][$email_row['sort']] = $email_info;
  381. }
  382. }
  383. }
  384. // array_values just in case
  385. foreach ($data as $contact_id => &$contact) {
  386. // ensure that emails for contact in order of sort field
  387. ksort($contact['email'], SORT_NUMERIC);
  388. // ensure 0 .. n indexing
  389. $contact['email'] = array_values($contact['email']);
  390. }
  391. unset($contact);
  392. continue;
  393. }
  394. $data_fields = array_unique($fields);
  395. foreach ($data_fields as $k => $field_id) {
  396. $f = waContactFields::get($field_id);
  397. if ($f && $f instanceof waContactCompositeField) {
  398. unset($data_fields[$k]);
  399. $data_fields = array_merge($data_fields, $f->getField());
  400. }
  401. }
  402. $model = $this->getModel($table);
  403. $post_data = $model->getData($ids, $data_fields);
  404. foreach ($post_data as $contact_id => $contact_data) {
  405. foreach ($contact_data as $field_id => $value) {
  406. if (!($f = waContactFields::get($field_id))) {
  407. continue;
  408. }
  409. if (!empty($value[0]) && !$f->isMulti()) {
  410. $post_data[$contact_id][$field_id] = isset($value[0]['data']) ? $value[0]['data'] :
  411. (is_array($value[0]) ? $value[0]['value'] : $value[0]);
  412. }
  413. }
  414. }
  415. if ($fields) {
  416. $fill[$table] = array_fill_keys($fields, '');
  417. } else if (!isset($fill[$table])) {
  418. $fill[$table] = array();
  419. }
  420. foreach ($data as $contact_id => $v) {
  421. if (isset($post_data[$contact_id])) {
  422. $data[$contact_id] += $post_data[$contact_id];
  423. }
  424. $data[$contact_id] += $fill[$table];
  425. }
  426. }
  427. }
  428. return $data;
  429. }
  430. public function prepare($new = false, $auto_title = true)
  431. {
  432. if (!$this->prepared || $new) {
  433. $type = $this->hash[0];
  434. if ($type) {
  435. $method = strtolower($type).'Prepare';
  436. if (method_exists($this, $method)) {
  437. $this->$method(isset($this->hash[1]) ? $this->hash[1] : '', $auto_title);
  438. } else {
  439. $params = array(
  440. 'collection' => $this,
  441. 'auto_title' => $auto_title,
  442. 'new' => $new,
  443. );
  444. /**
  445. * @event contacts_collection
  446. * @param array [string]mixed $params
  447. * @param array [string]waContactsCollection $params['collection']
  448. * @param array [string]boolean $params['auto_title']
  449. * @param array [string]boolean $params['new']
  450. * @return bool null if ignored, true when something changed in the collection
  451. */
  452. $processed = array_filter(wa()->event(array('contacts', 'contacts_collection'), $params));
  453. if (!$processed) {
  454. $this->where[] = 0;
  455. }
  456. }
  457. } elseif ($auto_title) {
  458. $this->addTitle(_ws('All contacts'));
  459. }
  460. if ($this->prepared) {
  461. return;
  462. }
  463. $this->prepared = true;
  464. }
  465. }
  466. protected function idPrepare($ids)
  467. {
  468. $ids = explode(',', $ids);
  469. foreach ($ids as $k => $v) {
  470. $v = (int)$v;
  471. if ($v) {
  472. $ids[$k] = $v;
  473. } else {
  474. unset($ids[$k]);
  475. }
  476. }
  477. if ($ids) {
  478. $this->where[] = "c.id IN (".implode(",", $ids).")";
  479. } else {
  480. $this->where[] = "0=1";
  481. }
  482. }
  483. protected function searchPrepare($query, $auto_title = true)
  484. {
  485. if ($auto_title || !isset($this->alias_index['data'])) {
  486. $this->alias_index['data'] = 0;
  487. }
  488. //$query = urldecode($query); // sometime this urldecode broke query, better make urldecode (if needed) outside the searchPrepare
  489. // `&` can be escaped in search request. Need to split by not escaped ones only.
  490. $escapedBS = 'ESCAPED_BACKSLASH';
  491. //If the user added to the request "ESCAPED_BACKSLASH" need to make it unique
  492. while(FALSE !== strpos($query, $escapedBS)) {
  493. $escapedBS .= rand(0, 9);
  494. }
  495. $escapedAmp = 'ESCAPED_AMPERSAND';
  496. while(FALSE !== strpos($query, $escapedAmp)) {
  497. $escapedAmp .= rand(0, 9);
  498. }
  499. //Replace escaped ampersand and backslash to text 'ESCAPED_AMPERSAND' and 'ESCAPED_BACKSLASH'
  500. $query = str_replace('\\&', $escapedAmp, str_replace('\\\\', $escapedBS, $query));
  501. $query = explode('&', $query);
  502. $model = $this->getModel();
  503. $title = array();
  504. foreach ($query as $part) {
  505. if (! ( $part = trim($part))) {
  506. continue;
  507. }
  508. //Return backslash and ampersand to query part
  509. $part = str_replace(array($escapedBS, $escapedAmp), array('\\', '&'), $part);
  510. $pattern = self::getConditionOperations();
  511. $parts = preg_split($pattern, $part, 2, PREG_SPLIT_DELIM_CAPTURE);
  512. if ($parts) {
  513. if ($parts[0] === 'name' && $parts[1] === '*=') {
  514. $t_a = preg_split("/\s+/", $parts[2]);
  515. $cond = array();
  516. foreach ($t_a as $t) {
  517. $t = trim($t);
  518. if (strlen($t) > 0) {
  519. $t = $model->escape($t, 'like');
  520. $cond[] = "c.name LIKE '%{$t}%'";
  521. }
  522. }
  523. if ($cond) {
  524. $this->addWhere(implode(" AND ", $cond));
  525. $title[] = _ws('Name').$parts[1].$parts[2];
  526. }
  527. } else if ($parts[0] == 'email') {
  528. if (!isset($this->joins['email'])) {
  529. $this->joins['email'] = array(
  530. 'table' => 'wa_contact_emails',
  531. 'alias' => 'e'
  532. );
  533. }
  534. $title[] = waContactFields::get($parts[0])->getName().$parts[1].$parts[2];
  535. $this->where[] = 'e.email'.$this->getExpression($parts[1], $parts[2]);
  536. } else if ($model->fieldExists($parts[0])) {
  537. if ($f = waContactFields::get($parts[0])) {
  538. $title[] = $f->getName().$parts[1].$parts[2];
  539. } else {
  540. $title[] = $parts[0].$parts[1].$parts[2];
  541. }
  542. $this->where[] = 'c.'.$parts[0].$this->getExpression($parts[1], $parts[2]);
  543. } else if ($parts[0] == 'category') {
  544. if (!isset($this->joins['categories'])) {
  545. $this->joins['categories'] = array(
  546. 'table' => 'wa_contact_categories',
  547. 'alias' => 'cc'
  548. );
  549. }
  550. $title[] = _ws('Category').$parts[1].$parts[2];
  551. $this->where[] = 'cc.category_id'.$this->getExpression($parts[1], $parts[2]);
  552. } else {
  553. $field_parts = explode('.', $parts[0]);
  554. $f = $field_parts[0];
  555. if ($fo = waContactFields::get($f)) {
  556. $title[] = $fo->getName().$parts[1].$parts[2];
  557. }
  558. $ext = isset($field_parts[1]) ? $field_parts[1] : null;
  559. $on = ":table.contact_id = c.id AND :table.field = '".$model->escape($f)."'";
  560. $this->where_fields[] = $f;
  561. $op = $parts[1];
  562. $term = $parts[2];
  563. if ($f === 'address:country') {
  564. $al1 = $this->addJoin('wa_contact_data', $on);
  565. $whr = "{$al1}.value ".$this->getExpression($op, $term);
  566. if ($ext !== null) {
  567. $whr .= " AND {$al1}.ext = '".$model->escape($ext)."'";
  568. $whr = "({$whr})";
  569. }
  570. // search by l18n name of countries
  571. if ($op === '*=') {
  572. if (wa()->getLocale() === 'en_US') {
  573. $al2 = $this->addLeftJoin('wa_country', ":table.iso3letter = {$al1}.value");
  574. $whr .= " OR {$al2}.name ".$this->getExpression($parts[1], $parts[2]);
  575. } else if (wa()->getLocale() !== 'en_US') {
  576. $iso3letters = array();
  577. $country_model = new waCountryModel();
  578. $countries = $country_model->all();
  579. $term = mb_strtolower($term);
  580. foreach ($countries as &$cntr) {
  581. if (mb_strpos(mb_strtolower($cntr['name']), $term) === 0) {
  582. $iso3letters[] = $cntr['iso3letter'];
  583. }
  584. }
  585. unset($cntr);
  586. if ($iso3letters) {
  587. $al2 = $this->addLeftJoin('wa_country', ":table.iso3letter = {$al1}.value");
  588. $whr .= " OR {$al2}.iso3letter IN ('".implode("','", $iso3letters)."')";
  589. }
  590. }
  591. }
  592. $this->addWhere($whr);
  593. } else if ($f === 'address:region') {
  594. if (strpos($term, ":") !== false) {
  595. // country_code : region_code - search by country code AND region code AND only in wa_region
  596. $term = explode(":", $term);
  597. $country_iso3 = $model->escape($term[0]);
  598. $code = $model->escape($term[1]);
  599. $al1 = $this->addJoin('wa_contact_data', $on);
  600. $whr = array();
  601. if ($ext !== null) {
  602. $whr[] = "{$al1}.ext = '".$model->escape($ext)."'";
  603. }
  604. $al2 = $this->addJoin('wa_contact_data', ":table.contact_id = c.id AND :table.field = 'address:country'");
  605. $al3 = $this->addJoin('wa_region', ":table.code = {$al1}.value AND :table.country_iso3 = {$al2}.value");
  606. $whr[] = "{$al3}.country_iso3 = '{$country_iso3}'";
  607. $whr[] = "{$al3}.code = '{$code}'";
  608. $whr = implode(" AND ", $whr);
  609. } else {
  610. $al1 = $this->addJoin('wa_contact_data', $on);
  611. $whr = "{$al1}.value".$this->getExpression($op, $term);
  612. if ($ext !== null) {
  613. $whr .= " AND {$al1}.ext = '".$model->escape($ext)."'";
  614. $whr = "({$whr})";
  615. }
  616. if ($op === "*=") {
  617. // if search by like, search by wa_region.name but taking into account country
  618. $al2 = $this->addJoin('wa_contact_data', ":table.contact_id = c.id AND :table.field = 'address:country'");
  619. $al3 = $this->addLeftJoin('wa_region', ":table.code = {$al1}.value AND :table.country_iso3 = {$al2}.value");
  620. $whr .= " OR {$al3}.name ".$this->getExpression($op, $term);
  621. }
  622. }
  623. $this->addWhere($whr);
  624. } else {
  625. $is_transform_phone_prefix_search = false;
  626. if ($f === 'phone') {
  627. $is_op_applicable = in_array($op, array('=', '==', '@=', '^=', '@^=', '*=', '@*='));
  628. $is_transform_phone_prefix_search = !empty($this->options['transform_phone_prefix']) && $is_op_applicable;
  629. }
  630. // search by phone with taking into account prefix transformation
  631. if ($is_transform_phone_prefix_search) {
  632. // normalize '=' op, cause we don't have '@==' pair ("multiple" version)
  633. $op = $op === '=' || $op === '==' ? '=' : $op;
  634. $is_single_term_search = substr($op, 0, 1) !== '@';
  635. $is_equals_search = $op === '=' || $op === '@=';
  636. if ($is_single_term_search) {
  637. $input_terms = array($term);
  638. $multi_op = '@' . $op; // "multiple" version of original op
  639. } else {
  640. $input_terms = explode(',', $term);
  641. $multi_op = $op; // "multiple" version of original op is original op itself
  642. }
  643. // result list of terms that need to participate in search
  644. // indexed by search operation
  645. $result_terms = array();
  646. foreach ($input_terms as $phone_term) {
  647. $phone_term = trim($phone_term);
  648. // is international phone (or phone prefix) is passed
  649. $is_international = substr($phone_term, 0, 1) === '+';
  650. // original phone query search by multiple version of original search operation
  651. $result_terms[$multi_op][] = waContactPhoneField::cleanPhoneNumber($term);
  652. // for what domains apply transformations
  653. if ($this->options['transform_phone_prefix'] === 'all_domains') {
  654. $domains = null;
  655. } elseif ($this->options['transform_phone_prefix'] == 'current_domain') {
  656. $domains = wa()->getRouting()->getDomain(null, true, false);
  657. } else {
  658. $domains = $this->options['transform_phone_prefix'];
  659. }
  660. $transform_results = waDomainAuthConfig::transformPhonePrefixForDomains($phone_term, $is_international, $domains);
  661. foreach ($transform_results as $transform_result) {
  662. if ($transform_result['status']) {
  663. if ($is_equals_search) {
  664. // if search by "equal", transformed phone also must be search by "equal" op
  665. $result_terms[$multi_op][] = $transform_result['phone'];
  666. } else {
  667. // otherwise search as prefix
  668. $result_terms['@^='][] = $transform_result['phone'];
  669. }
  670. }
  671. }
  672. }
  673. $table_alias = $this->addJoin('wa_contact_data', $on);
  674. $where_conditions = array();
  675. foreach ($result_terms as $search_op => $search_terms) {
  676. // in $search_terms always at least one item, so no need to check for empty array
  677. $search_terms = array_unique($search_terms);
  678. $search_terms_str = join(',', $search_terms);
  679. $field_name = $table_alias . '.value';
  680. $where_condition = $this->getFullExpression($field_name, $search_op, $search_terms_str, false);
  681. $where_conditions[] = $where_condition;
  682. }
  683. $where_conditions_str = '(' . join(' OR ', $where_conditions) . ')';
  684. if ($ext !== null) {
  685. $where_conditions_str .= " AND {$table_alias}.ext = '".$model->escape($ext)."'";
  686. }
  687. $this->addWhere($where_conditions_str);
  688. } else {
  689. // just simple search by wa_contact_data.value
  690. $on .= ' AND ' . $this->getFullExpression(':table.value', $op, $term);
  691. if ($ext !== null) {
  692. $on .= " AND :table.ext = '" . $model->escape($ext) . "'";
  693. }
  694. $this->addJoin('wa_contact_data', $on);
  695. }
  696. }
  697. }
  698. }
  699. }
  700. if ($title) {
  701. $title = implode(', ', $title);
  702. // Strip slashes from search title.
  703. $bs = '\\\\';
  704. $title = preg_replace("~{$bs}(_|%|&|{$bs})~", '\1', $title);
  705. }
  706. if ($auto_title && $title) {
  707. $this->addTitle($title, ' ');
  708. }
  709. }
  710. public function addTitle($title, $delim = ', ')
  711. {
  712. if (!$title && $title !== '0' && $title !== 0) {
  713. return;
  714. }
  715. if ($this->title) {
  716. $this->title .= $delim;
  717. }
  718. $this->title .= $title;
  719. }
  720. public function setTitle($title)
  721. {
  722. $this->title = $title;
  723. }
  724. public function getWhereFields()
  725. {
  726. return $this->where_fields;
  727. }
  728. protected function categoryPrepare($id, $auto_title = false)
  729. {
  730. $category_model = new waContactCategoryModel();
  731. $category = $category_model->getById($id);
  732. if ($category) {
  733. if ($auto_title) {
  734. $this->title = $category['name'];
  735. }
  736. $this->update_count = array(
  737. 'model' => $category_model,
  738. 'id' => $id,
  739. 'count' => isset($category['cnt']) ? $category['cnt'] : 0
  740. );
  741. }
  742. $this->addJoin('wa_contact_categories', null, ':table.category_id = '.(int)$id);
  743. }
  744. protected function usersPrepare($params, $auto_title = true)
  745. {
  746. $this->where[] = 'c.login IS NOT NULL';
  747. if ($params == 'banned') {
  748. $this->where[] = 'c.is_user = -1';
  749. } else if ($params == 'active_and_banned') {
  750. $this->where[] = 'c.is_user <> 0';
  751. } else {
  752. $this->where[] = 'c.is_user = 1';
  753. }
  754. if ($auto_title) {
  755. $this->addTitle(_ws('All users'));
  756. }
  757. }
  758. protected function companyPrepare($params, $auto_title = true)
  759. {
  760. $params = array_filter(array_map('intval', explode(',', $params)));
  761. if ($params) {
  762. $this->where[] = "c.company_contact_id IN ('".join("','", $params)."')";
  763. } else {
  764. $this->where[] = '0';
  765. }
  766. if ($auto_title) {
  767. $this->addTitle(_ws('Company'));
  768. }
  769. }
  770. /**
  771. * Add joins and conditions for hash /group/$group_id
  772. * @param int $id
  773. */
  774. protected function groupPrepare($id)
  775. {
  776. $group_model = new waGroupModel();
  777. $group = $group_model->getById($id);
  778. if ($group) {
  779. $this->title = $group['name'];
  780. $this->update_count = array(
  781. 'model' => $group_model,
  782. 'id' => $id,
  783. 'count' => isset($group['cnt']) ? $group['cnt'] : 0
  784. );
  785. }
  786. $this->where[] = "cg.group_id = ".(int)$id;
  787. $this->where[] = "c.is_user > 0";
  788. $this->joins[] = array(
  789. 'table' => 'wa_user_groups',
  790. 'alias' => 'cg',
  791. );
  792. }
  793. /**
  794. * Returns ORDER BY clause
  795. * @return string
  796. */
  797. protected function getOrderBy()
  798. {
  799. if ($this->order_by) {
  800. return " ORDER BY ".$this->order_by;
  801. } else {
  802. return "";
  803. }
  804. }
  805. /**
  806. * Returns GROUP BY clause
  807. * @return string
  808. */
  809. protected function getGroupBy()
  810. {
  811. if ($this->group_by) {
  812. return " GROUP BY ".$this->group_by;
  813. } else {
  814. return "";
  815. }
  816. }
  817. protected function getHaving()
  818. {
  819. if ($this->having) {
  820. return " HAVING " . implode(' AND ', $this->having);
  821. } else {
  822. return "";
  823. }
  824. }
  825. /**
  826. * Returns contacts model
  827. *
  828. * @param string $type
  829. * @return waContactModel|waContactDataModel|waContactEmailsModel
  830. */
  831. protected function getModel($type = null)
  832. {
  833. switch ($type) {
  834. case 'data':
  835. if (!isset($this->models[$type])) {
  836. $this->models[$type] = new waContactDataModel();
  837. }
  838. return $this->models[$type];
  839. case 'email':
  840. if (!isset($this->models[$type])) {
  841. $this->models[$type] = new waContactEmailsModel();
  842. }
  843. return $this->models[$type];
  844. default:
  845. $type = 'default';
  846. if (!isset($this->models[$type])) {
  847. $this->models[$type] = new waContactModel();
  848. }
  849. return $this->models[$type];
  850. }
  851. }
  852. /**
  853. * Returns expression for SQL
  854. *
  855. * @param string $op - operand ==, >=, etc
  856. * @param string $value - value
  857. * @return string
  858. */
  859. protected function getExpression($op, $value)
  860. {
  861. $model = $this->getModel();
  862. switch ($op) {
  863. case '>':
  864. case '>=':
  865. case '<':
  866. case '<=':
  867. case '!=':
  868. return " ".$op." '".$model->escape($value)."'";
  869. case "^=":
  870. return " LIKE '".$model->escape($value, 'like')."%'";
  871. case "$=":
  872. return " LIKE '%".$model->escape($value, 'like')."'";
  873. case "*=":
  874. return " LIKE '%".$model->escape($value, 'like')."%'";
  875. case '@=':
  876. $values = array();
  877. foreach (explode(',', $value) as $v) {
  878. $values[] = "'".$model->escape($v)."'";
  879. }
  880. return ' IN ('.implode(',', $values).')';
  881. case "==":
  882. case "=";
  883. default:
  884. return " = '".$model->escape($value)."'";
  885. }
  886. }
  887. /**
  888. * Returns expression for SQL with field name inside it
  889. * Supports advanced operations: @^=, @$=, @*=
  890. * @param string $field
  891. * @param string $op
  892. * @param string $value
  893. * @param bool $wrap_in_parentheses
  894. * @return mixed
  895. */
  896. protected function getFullExpression($field, $op, $value, $wrap_in_parentheses = true)
  897. {
  898. $model = $this->getModel();
  899. switch ($op) {
  900. case '@^=':
  901. $condition = array();
  902. foreach (explode(',', $value) as $v) {
  903. $condition[] = ":field LIKE '".$model->escape($v, 'like')."%'";
  904. }
  905. $expr = join(' OR ', $condition);
  906. break;
  907. case '@$=':
  908. $condition = array();
  909. foreach (explode(',', $value) as $v) {
  910. $condition[] = ":field LIKE '%".$model->escape($v, 'like')."'";
  911. }
  912. $expr = join(' OR ', $condition);
  913. break;
  914. case '@*=':
  915. $condition = array();
  916. foreach (explode(',', $value) as $v) {
  917. $condition[] = ":field LIKE '%".$model->escape($v, 'like')."%'";
  918. }
  919. $expr = join(' OR ', $condition);
  920. break;
  921. default:
  922. $expr = ':field' . $this->getExpression($op, $value);
  923. break;
  924. }
  925. $expr = str_replace(':field', $field, $expr);
  926. if ($wrap_in_parentheses) {
  927. $expr = '(' . $expr . ')';
  928. }
  929. return $expr;
  930. }
  931. public function getSQL($with_primary_email = false)
  932. {
  933. $this->prepare();
  934. $sql = "FROM wa_contact c";
  935. if ($this->joins) {
  936. foreach ($this->joins as $join) {
  937. $alias = isset($join['alias']) ? $join['alias'] : '';
  938. if (isset($join['on'])) {
  939. $on = $join['on'];
  940. } else {
  941. $on = "c.id = ".($alias ? $alias : $join['table']).".contact_id";
  942. }
  943. $sql .= (isset($join['type']) ? " ".$join['type'] : '')." JOIN ".$join['table']." ".$alias;
  944. if (isset($join['force_index'])) {
  945. $sql .= ' FORCE INDEX ('.$join['force_index'].')';
  946. }
  947. $sql .= " ON ".$on;
  948. }
  949. }
  950. if ($with_primary_email) {
  951. $sql .= " JOIN wa_contact_emails _e ON c.id = _e.contact_id";
  952. }
  953. if ($this->left_joins) {
  954. foreach ($this->left_joins as $join) {
  955. $alias = isset($join['alias']) ? $join['alias'] : '';
  956. if (isset($join['on'])) {
  957. $on = $join['on'];
  958. } else {
  959. $on = "c.id = ".($alias ? $alias : $join['table']).".contact_id";
  960. }
  961. $sql .= (isset($join['type']) ? " ".$join['type'] : '')." LEFT JOIN ".$join['table']." ".$alias;
  962. if (isset($join['force_index'])) {
  963. $sql .= ' FORCE INDEX ('.$join['force_index'].')';
  964. }
  965. $sql .= " ON ".$on;
  966. }
  967. }
  968. if ($this->where) {
  969. $where = $this->where;
  970. if ($with_primary_email) {
  971. $where[] = "_e.sort = 0";
  972. }
  973. if (!empty($where['_or'])) {
  974. $where['_or'] = "(" . implode(" OR ", $where['_or']) . ")";
  975. }
  976. $sql .= "\nWHERE ".implode(" AND ", $where);
  977. }
  978. return $sql;
  979. }
  980. /**
  981. * Save requested fields of the collection in temporary table
  982. *
  983. * @param string $table - name of the temporary table
  984. * @param string $fields - fields for select
  985. * @param bool $ignore
  986. * @return bool - result
  987. */
  988. public function saveToTable($table, $fields = 'id', $ignore = false)
  989. {
  990. if (!is_array($fields)) {
  991. $fields = explode(",", $fields);
  992. $fields = array_map("trim", $fields);
  993. }
  994. $primary_email = false;
  995. $insert_fields = $select_fields = array();
  996. foreach ($fields as $k => $v) {
  997. if (is_numeric($k)) {
  998. $insert_fields[] = $v;
  999. $select_fields[] = "c.".$v;
  1000. } else {
  1001. $insert_fields[] = $k;
  1002. if (strpos($v, '.') !== false || is_numeric($v)) {
  1003. $select_fields[] = $v;
  1004. } else {
  1005. if ($v == '_email') {
  1006. $select_fields[] = "_e.email";
  1007. $primary_email = true;
  1008. } else {
  1009. $select_fields[] = "c.".$v;
  1010. }
  1011. }
  1012. }
  1013. }
  1014. $sql = "INSERT ".($ignore ? "IGNORE " : "")."INTO ".$table." (".implode(",", $insert_fields).")
  1015. SELECT DISTINCT ".implode(",", $select_fields)." ".$this->getSQL($primary_email);
  1016. $sql .= $this->getGroupBy();
  1017. $sql .= $this->getHaving();
  1018. $sql .= $this->getOrderBy();
  1019. return $this->getModel()->exec($sql);
  1020. }
  1021. /**
  1022. * Set order by clause for select
  1023. *
  1024. * @param string $field
  1025. * @param string $order
  1026. * @return string
  1027. */
  1028. public function orderBy($field, $order = 'ASC')
  1029. {
  1030. if (strtolower(trim($order)) == 'desc') {
  1031. $order = 'DESC';
  1032. } else {
  1033. $order = 'ASC';
  1034. }
  1035. $field = trim($field);
  1036. if ($field == '~data') {
  1037. $this->fields['data_count'] = 'count(*)';
  1038. $this->joins[] = array(
  1039. 'table' => 'wa_contact_data',
  1040. 'alias' => 'd',
  1041. 'type' => 'LEFT'
  1042. );
  1043. $this->group_by = 'c.id';
  1044. $this->order_by = 'data_count '.$order;
  1045. } else if ($field) {
  1046. $field = trim($field);
  1047. if (substr($field, 0, 2) == 'c.') {
  1048. $field = substr($field, 2);
  1049. }
  1050. if (!$this->getModel()->fieldExists($field)) {
  1051. $field = 'id';
  1052. }
  1053. $this->order_by = 'c.'.$field." ".$order;
  1054. }
  1055. return $this->order_by;
  1056. }
  1057. public function addJoin($table, $on = null, $where = null, $options = array())
  1058. {
  1059. $type = '';
  1060. if (is_array($table)) {
  1061. if (isset($table['on'])) {
  1062. $on = $table['on'];
  1063. }
  1064. if (isset($table['where'])) {
  1065. $where = $table['where'];
  1066. }
  1067. if (isset($table['type'])) {
  1068. $type = $table['type'];
  1069. }
  1070. $table = $table['table'];
  1071. }
  1072. $alias = $this->getTableAlias($table);
  1073. if (!isset($this->join_index[$alias])) {
  1074. $this->join_index[$alias] = 1;
  1075. } else {
  1076. $this->join_index[$alias]++;
  1077. }
  1078. $alias .= $this->join_index[$alias];
  1079. $join = array(
  1080. 'table' => $table,
  1081. 'alias' => $alias,
  1082. 'type' => $type
  1083. );
  1084. if (!empty($options['force_index'])) {
  1085. $join['force_index'] = $options['force_index'];
  1086. }
  1087. if ($on) {
  1088. $join['on'] = str_replace(':table', $alias, $on);
  1089. }
  1090. $this->joins[] = $join;
  1091. if ($where) {
  1092. $this->addWhere(str_replace(':table', $alias, $where));
  1093. }
  1094. return $alias;
  1095. }
  1096. public function addLeftJoin($table, $on = null, $where = null, $options = array())
  1097. {
  1098. $type = '';
  1099. if (is_array($table)) {
  1100. if (isset($table['on'])) {
  1101. $on = $table['on'];
  1102. }
  1103. if (isset($table['where'])) {
  1104. $where = $table['where'];
  1105. }
  1106. if (isset($table['type'])) {
  1107. $type = $table['type'];
  1108. }
  1109. $table = $table['table'];
  1110. }
  1111. $alias = $this->getTableAlias($table);
  1112. if (!isset($this->join_index[$alias])) {
  1113. $this->join_index[$alias] = 1;
  1114. } else {
  1115. $this->join_index[$alias]++;
  1116. }
  1117. $alias .= $this->join_index[$alias];
  1118. $join = array(
  1119. 'table' => $table,
  1120. 'alias' => $alias,
  1121. 'type' => $type
  1122. );
  1123. if (!empty($options['force_index'])) {
  1124. $join['force_index'] = $options['force_index'];
  1125. }
  1126. if ($on) {
  1127. $join['on'] = str_replace(':table', $alias, $on);
  1128. }
  1129. $this->left_joins[] = $join;
  1130. if ($where) {
  1131. $this->addWhere(str_replace(':table', $alias, $where));
  1132. }
  1133. return $alias;
  1134. }
  1135. public fun

Large files files are truncated, but you can click here to view the full file