1: <?php
2:
3: /*****************************************************************************************
4: * X2Engine Open Source Edition is a customer relationship management program developed by
5: * X2Engine, Inc. Copyright (C) 2011-2016 X2Engine Inc.
6: *
7: * This program is free software; you can redistribute it and/or modify it under
8: * the terms of the GNU Affero General Public License version 3 as published by the
9: * Free Software Foundation with the addition of the following permission added
10: * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
11: * IN WHICH THE COPYRIGHT IS OWNED BY X2ENGINE, X2ENGINE DISCLAIMS THE WARRANTY
12: * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
13: *
14: * This program is distributed in the hope that it will be useful, but WITHOUT
15: * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16: * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
17: * details.
18: *
19: * You should have received a copy of the GNU Affero General Public License along with
20: * this program; if not, see http://www.gnu.org/licenses or write to the Free
21: * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
22: * 02110-1301 USA.
23: *
24: * You can contact X2Engine, Inc. P.O. Box 66752, Scotts Valley,
25: * California 95067, USA. or at email address [email protected].
26: *
27: * The interactive user interfaces in modified source and object code versions
28: * of this program must display Appropriate Legal Notices, as required under
29: * Section 5 of the GNU Affero General Public License version 3.
30: *
31: * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
32: * these Appropriate Legal Notices must retain the display of the "Powered by
33: * X2Engine" logo. If the display of the logo is not reasonably feasible for
34: * technical reasons, the Appropriate Legal Notices must display the words
35: * "Powered by X2Engine".
36: *****************************************************************************************/
37:
38: /**
39: * Behavior to provide requisite methods for checking for potential duplicate
40: * records. Currently only implemented in Contacts and Accounts.
41: */
42: class X2DuplicateBehavior extends CActiveRecordBehavior {
43:
44: // Set constants so that we can change these in the future without issue
45: CONST DUPLICATE_FIELD = 'dupeCheck';
46: CONST DUPLICATE_LIMIT = 5;
47:
48: /**
49: * Returns whether or not any duplicate records exist in the database.
50: *
51: * Commonly used as a gate in an if statement for other duplicate
52: * checking functionality.
53: * @return boolean
54: */
55: public function checkForDuplicates() {
56: if ($this->owner->{X2DuplicateBehavior::DUPLICATE_FIELD} == 0) {
57: $criteria = $this->getDuplicateCheckCriteria();
58: return $this->owner->count($criteria) > 0;
59: }
60: return false;
61: }
62:
63: /**
64: * Return a list of potential duplicate records.
65: *
66: * Capts at 5 records unless a special parameter is provided so as to prevent
67: * possible server crashes from attempting to render large numbers of records.
68: * @param boolean $getAll Whether to return all records or just 5
69: * @return CActiveDataProvider
70: */
71: public function getDuplicates($getAll = false, $strict = false) {
72: $criteria = $this->getDuplicateCheckCriteria(false, 't', $strict);
73: if ($getAll && !empty($criteria->limit)) {
74: $criteria = $this->getDuplicateCheckCriteria(true, 't', $strict);
75: }
76: if (!$getAll) {
77: $criteria->limit = X2DuplicateBehavior::DUPLICATE_LIMIT;
78: }
79: return $this->owner->findAll($criteria);
80: }
81:
82: /**
83: * Returns the total number of duplicates found (unrestricted by the limit on
84: * getDuplicates)
85: * @return int
86: */
87: public function countDuplicates() {
88: $criteria = $this->getDuplicateCheckCriteria();
89: return $this->owner->count($criteria);
90: }
91:
92: /**
93: * Mark a record as a duplicate.
94: *
95: * Set all relevant fields to the proper values for marking a record as duplicate.
96: * A duplicate record is private and assigned to 'Anyone', and if there
97: * are options for "doNotCall" and "doNotEmail" they need to be turned on.
98: * Alternatively, the "delete" string can be passed to delete the record instead
99: * of hiding it. This functionality exists in case some future code requires
100: * more things to be done on deleting duplicates.
101: * @param string $action
102: */
103: public function markAsDuplicate($action = 'hide') {
104: if ($action === 'hide') {
105: if ($this->owner->hasAttribute('visibility')) {
106: $this->owner->visibility = 0;
107: }
108: if ($this->owner->hasAttribute('assignedTo')) {
109: $this->owner->assignedTo = 'Anyone';
110: }
111: if ($this->owner->hasAttribute('doNotCall')) {
112: $this->owner->doNotCall = 1;
113: }
114: if ($this->owner->hasAttribute('doNotEmail')) {
115: $this->owner->doNotEmail = 1;
116: }
117: $this->owner->{X2DuplicateBehavior::DUPLICATE_FIELD} = 1;
118: $this->owner->update();
119: } elseif ($action === 'delete') {
120: $this->owner->delete();
121: }
122: }
123:
124: /**
125: * Reset dupeCheck field if duplicate defining fields are changed.
126: *
127: * Records have a concept of "duplicate-defining fields" which are the fields
128: * that are checked when searching for duplicates (name, email, etc.). If one
129: * of those fields is changed in an update, the dupeCheck parameter needs to
130: * be reset and the record needs to be checked for possible duplicates again.
131: * @param CEvent $event
132: */
133: public function afterSave($event) {
134: if (!$this->owner->getIsNewRecord()) {
135: $dupeFields = $this->owner->duplicateFields();
136: $oldAttributes = $this->owner->getOldAttributes();
137: foreach ($dupeFields as $field) {
138: if (array_key_exists($field, $oldAttributes) &&
139: $oldAttributes[$field] !== $this->owner->$field) {
140: $this->resetDuplicateField();
141: break;
142: }
143: }
144: }
145: }
146:
147: /**
148: * Update the dupeCheck field to reflect that a record has been checked.
149: *
150: * Set the value in the current record and use updateByPk so that no validation
151: * or behaviors from afterSave are called.
152: */
153: public function duplicateChecked() {
154: if ($this->owner->{X2DuplicateBehavior::DUPLICATE_FIELD} == 0) {
155: $this->owner->{X2DuplicateBehavior::DUPLICATE_FIELD} = 1;
156: $this->owner->updateByPk($this->owner->id, array(X2DuplicateBehavior::DUPLICATE_FIELD => 1));
157: }
158: }
159:
160: /**
161: * Reset the dupeCheck field to its unchecked state.
162: */
163: public function resetDuplicateField() {
164: $this->owner->{X2DuplicateBehavior::DUPLICATE_FIELD} = 0;
165: $this->owner->updateByPk($this->owner->id, array(X2DuplicateBehavior::DUPLICATE_FIELD => 0));
166: }
167:
168: /**
169: * Hide all potential duplicate records.
170: *
171: * This is equivalent to a mass version of "markAsDuplicate" but it affects
172: * records other than the currenly loaded one.
173: */
174: public function hideDuplicates() {
175: $criteria = $this->getDuplicateCheckCriteria(false, null);
176: $attributes = array(
177: X2DuplicateBehavior::DUPLICATE_FIELD => 1,
178: );
179: if ($this->owner->hasAttribute('visibility')) {
180: $attributes['visibility'] = 0;
181: }
182: if ($this->owner->hasAttribute('assignedTo')) {
183: $attributes['assignedTo'] = 'Anyone';
184: }
185: if ($this->owner->hasAttribute('doNotCall')) {
186: $attributes['doNotCall'] = 1;
187: }
188: if ($this->owner->hasAttribute('doNotEmail')) {
189: $attributes['doNotEmail'] = 1;
190: }
191: $this->owner->updateAll($attributes, $criteria);
192: }
193:
194: /**
195: * Delete all potential duplicate records.
196: */
197: public function deleteDuplicates() {
198: $criteria = $this->getDuplicateCheckCriteria(false, null);
199: $this->owner->deleteAll($criteria);
200: }
201:
202: /**
203: * Private helper function to get the duplicate criteria.
204: *
205: * Caches criteria for later use.
206: * @param boolean $refresh Force refresh of cached criteria
207: * @return CDbCriteria
208: */
209: private $_duplicateCheckCriteria = array ();
210: private function getDuplicateCheckCriteria($refresh = false, $alias='t', $strict = false) {
211: if (!$refresh && isset($this->_duplicateCheckCriteria[$alias])) {
212: return $this->_duplicateCheckCriteria[$alias];
213: }
214: $dupeFields = $this->owner->duplicateFields();
215: $criteria = new CDbCriteria();
216: $criteria->order = 'createDate ASC';
217: foreach ($dupeFields as $fieldName) {
218: if (!empty($this->owner->$fieldName)) {
219: $criteria->compare($fieldName, $this->owner->$fieldName, false, $strict?"AND":"OR");
220: }
221: }
222: if (empty($criteria->condition)) {
223: $criteria->condition = "FALSE";
224: } else {
225: $criteria->compare('id', "<>" . $this->owner->id, false, "AND");
226: if ($this->owner->asa('permissions')) {
227: $criteria->mergeWith($this->owner->getAccessCriteria($alias));
228: }
229: }
230: $this->_duplicateCheckCriteria[$alias] = $criteria;
231: return $this->_duplicateCheckCriteria[$alias];
232: }
233:
234: }
235: