1: <?php
2: /*****************************************************************************************
3: * X2Engine Open Source Edition is a customer relationship management program developed by
4: * X2Engine, Inc. Copyright (C) 2011-2016 X2Engine Inc.
5: *
6: * This program is free software; you can redistribute it and/or modify it under
7: * the terms of the GNU Affero General Public License version 3 as published by the
8: * Free Software Foundation with the addition of the following permission added
9: * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
10: * IN WHICH THE COPYRIGHT IS OWNED BY X2ENGINE, X2ENGINE DISCLAIMS THE WARRANTY
11: * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
12: *
13: * This program is distributed in the hope that it will be useful, but WITHOUT
14: * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15: * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
16: * details.
17: *
18: * You should have received a copy of the GNU Affero General Public License along with
19: * this program; if not, see http://www.gnu.org/licenses or write to the Free
20: * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21: * 02110-1301 USA.
22: *
23: * You can contact X2Engine, Inc. P.O. Box 66752, Scotts Valley,
24: * California 95067, USA. or at email address [email protected].
25: *
26: * The interactive user interfaces in modified source and object code versions
27: * of this program must display Appropriate Legal Notices, as required under
28: * Section 5 of the GNU Affero General Public License version 3.
29: *
30: * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
31: * these Appropriate Legal Notices must retain the display of the "Powered by
32: * X2Engine" logo. If the display of the logo is not reasonably feasible for
33: * technical reasons, the Appropriate Legal Notices must display the words
34: * "Powered by X2Engine".
35: *****************************************************************************************/
36:
37: /**
38: * Changelog recording behavior class.
39: *
40: * X2ChangeLogBehavior is a CActiveRecordBehavior which automatically saves changelog
41: * data when a record is saved. It also looks up any applicable notification criteria
42: * and takes the appropriate action (create a notification, create a new action,
43: * reassign the record, etc.)
44: *
45: * @package application.components
46: * @property string $autoCompleteSource The action to user for autocomplete data
47: * @property string $baseRoute The default module/controller this model "belongs" to
48: * @property string $editingUsername Username of the user who is performing the save operation.
49: * @property string $viewRoute The default action to view this model
50: */
51: class X2ChangeLogBehavior extends CActiveRecordBehavior {
52:
53: public function events() {
54: return array_merge(parent::events(),array(
55: 'onAfterCreate'=>'afterCreate',
56: 'onAfterUpdate'=>'afterUpdate',
57: ));
58: }
59:
60: private $_editingUsername;
61: public $createEvent = true;
62: protected $validated = false;
63:
64: /**
65: * Magic getter for {@link editingUsername} that returns a username regardless of context.
66: *
67: * {@link editingUsername} should be used in place of Yii::app()->user->name
68: * in order for X2Model to work in console commands and API calls (where
69: * there is no user session).
70: *
71: * @return type
72: */
73: public function getEditingUsername() {
74: if (!isset($this->_editingUsername))
75: $this->_editingUsername = Yii::app()->getSuName();
76: return $this->_editingUsername;
77: }
78:
79: /**
80: * Sets username fields of a model
81: */
82: public static function usernameFieldsSet(CActiveRecord $model,$username){
83: if($model->hasAttribute('updatedBy')){
84: $model->updatedBy = $username;
85: }
86: if($model->hasAttribute('createdBy') && $model->isNewRecord)
87: $model->createdBy = $username;
88: }
89:
90: public function beforeSave($event){
91: $model=$this->getOwner();
92: self::usernameFieldsSet($model,$this->editingUsername);
93: return parent::beforeSave($event);
94: }
95:
96: public function afterCreate($event) {
97:
98: $model = $this->getOwner();
99:
100: //$api = 0; // FIX THIS
101:
102: if($this->createEvent){
103: $event = new Events;
104: $event->visibility = $model->hasAttribute('visibility')? $model->visibility : 1;
105: $event->associationType = get_class($model);
106: $event->associationId = $model->id;
107: $event->user = $this->editingUsername;
108: $event->type = 'record_create';
109:
110: // Event creation already handled by web lead.
111: // if(!$model instanceof Contacts || $api==0)
112:
113: $event->save();
114: }
115:
116: if($model->hasAttribute('assignedTo')) {
117: if(!empty($model->assignedTo) && $model->assignedTo != $this->editingUsername &&
118: $model->assignedTo != 'Anyone') {
119:
120: $notif = new Notification;
121: $notif->user = $model->assignedTo;
122: //$notif->createdBy = ($api == 1) ? 'API' : $this->editingUsername;
123: $notif->createdBy = $this->editingUsername;
124: $notif->createDate = time();
125: $notif->type = 'create';
126: $notif->modelType = get_class($model);
127: $notif->modelId = $model->id;
128: $notif->save();
129: }
130: }
131: }
132:
133: /**
134: * Marks the record as validated, so we know somebody called CActiveRecord::save() rather than CActiveRecord::update() on it
135: */
136: public function afterValidate($event) {
137: $this->validated = true;
138: }
139:
140: /**
141: * Triggers record_updated, runs changelog calculations and checks notification criteria (soon to be removed)
142: */
143: public function afterUpdate($event) {
144: if($this->validated) {
145: $changes = $this->getChanges();
146: $this->updateChangelog($changes);
147: }
148: $this->validated = false; // reset in case CActiveRecord::update() is called after CActiveRecord::save()
149: }
150:
151: /**
152: * Logs the deletion of the model
153: */
154: public function afterDelete($event) {
155: $modelClass = get_class($this->getOwner());
156: if($modelClass === 'Actions' && $this->getOwner()->workflowId !== null) // no deletion events for workflow actions, that's somebody else's problem
157: return;
158: if($this->createEvent){
159: $event = new Events();
160: $event->type='record_deleted';
161: $event->associationType = $modelClass;
162: $event->associationId = $this->getOwner()->id;
163: if($this->getOwner()->hasAttribute('visibility')){
164: $event->visibility=$this->getOwner()->visibility;
165: }
166: $event->text = $this->getOwner()->name;
167: $event->user = $this->editingUsername;
168: $event->save();
169: }
170:
171: $log = new Changelog;
172: $log->type = $modelClass;
173: $log->itemId = $this->getOwner()->id;
174: $log->recordName = $this->getOwner()->name;
175: $log->changed = 'delete';
176:
177: $log->changedBy = $this->editingUsername;
178: $log->timestamp = time();
179:
180: $log->save();
181:
182: X2Flow::trigger('RecordDeleteTrigger',array(
183: 'model'=>$this->getOwner()
184: ));
185: }
186:
187: /**
188: * Finds attributes that were changed and generates an array of changes.
189: *
190: * @return array a 2-dimensional array of changes, with the format $fieldName => array($old,$new)
191: */
192: public function getChanges() {
193: $changes = array();
194:
195: // $this->_oldAttributes
196: $oldAttributes = $this->getOwner()->getOldAttributes();
197: $newAttributes = $this->getOwner()->getAttributes();
198:
199: // compare old and new
200: foreach($newAttributes as $fieldName => $new) {
201: if(isset($oldAttributes[$fieldName])) {
202: $old = $oldAttributes[$fieldName];
203: if(is_array($old))
204: $old = implode(', ',$old); // convert arrays to a string with commas in it (for example multiple assignedTo)
205:
206: if($new != $old)
207: $changes[$fieldName] = array($old,$new);
208: }elseif(!is_null($new)){
209: $changes[$fieldName]=array(null,$new);
210: }
211: }
212:
213: return $changes;
214: }
215:
216:
217: /* public function writeChangelog($changes) {
218: for($i=0;$i<count($changes); $i++) {
219: $old = &$changes[$i][0];
220: $new = &$changes[$i][1];
221:
222: if($new != $old) {
223: $log = new Changelog;
224: $log->type = get_class($this->getOwner());
225:
226: $log->itemId = $this->getOwner()->id;
227: $log->changedBy = $this->editingUsername;
228: $log->fieldName = $field;
229: // $log->oldValue = $old;
230: $log->timestamp = time();
231:
232: if(empty($old)) {
233: $log->diff = false;
234: $log->newValue = $new;
235: } else {
236: $diff = FineDiff::getDiffOpcodes($old,$new,FineDiff::$wordGranularity);
237:
238: $log->diff = strlen($diff) > strlen($old);
239: $log->newValue = $log->diff? $diff : $new;
240: }
241:
242: $log->save();
243: }
244: }
245: } */
246:
247: /**
248: * Writes field changes to the changelog. Calls {@link checkNotificationCriteria()} for each change
249: * @param array $changes the changes array, calls {@link getChanges()} if not provided
250: */
251: public function updateChangelog($changes = null) {
252: $model = $this->getOwner();
253:
254: if($changes === null)
255: $changes = $this->getChanges();
256:
257: // $model->lastUpdated = time();
258: // $model->updatedBy = Yii::app()->user->getName();
259: // $model->save();
260: $type = get_class($model);
261:
262: // Handle special types
263: $pluralize = array('Quote', 'Product');
264: if (in_array($type, $pluralize))
265: $type .= "s";
266: else if ($type == 'Campaign')
267: $type = "Marketing";
268: else if ($type == 'bugreports')
269: $type = 'BugReports';
270:
271: $excludeFields=array(
272: 'lastUpdated',
273: 'createDate',
274: 'lastActivity',
275: 'updatedBy',
276: 'trackingKey',
277: );
278: if(is_array($changes)) {
279:
280: foreach($changes as $fieldName => $change){
281: if(!in_array($fieldName,$excludeFields)){
282: $changelog = new Changelog;
283: $changelog->type = $type;
284: if (!isset($model->id)) {
285: if ($model->save()) {
286:
287: }
288: }
289: $changelog->itemId = $model->id;
290: if ($model->hasAttribute('name')) {
291: $changelog->recordName=$model->name;
292: } else {
293: $changelog->recordName=$type;
294: }
295: $changelog->changedBy = $this->editingUsername;
296: $changelog->fieldName = $fieldName;
297: $changelog->oldValue = $change[0];
298: $changelog->newValue = $change[1];
299: $changelog->timestamp = time();
300:
301: $changelog->save();
302:
303: $this->checkNotificationCriteria($fieldName,$change[0],$change[1]);
304: }
305: }
306: }
307: // } elseif($changes == 'Create' || $changes == 'Edited') {
308: // if($model instanceof Contacts)
309: // $change = $model->backgroundInfo;
310: // else if($model instanceof Actions)
311: // $change = $model->actionDescription;
312: // else if($model instanceof Docs)
313: // $change = $model->text;
314: // else
315: // $change = $model->name;
316: // } elseif($changes != '' && $changes != 'Completed') {
317: // $pieces = explode("<br />", $change);
318: // foreach($pieces as $piece) {
319: // $newPieces = explode("TO:", $piece);
320: // $forDeletion = $newPieces[0];
321: // if(isset($newPieces[1]) && preg_match('/<b>' . Yii::t('actions', 'color') . '<\/b>/', $piece) == false) {
322: // $changes[] = $newPieces[1];
323: // }
324: // }
325: // }
326: }
327:
328: /**
329: * Looks up notification criteria in x2_criteria relevant to this model
330: * and field and performs the specified operation.
331: * Soon to be eliminated in wake of x2flow automation system.
332: *
333: * @param string $fieldName the name of the current field
334: * @param string $old the old value
335: * @param string $new the new value
336: */
337: public function checkNotificationCriteria($fieldName,$old,$new) {
338:
339: $model = $this->getOwner();
340: $modelClass = get_class($model);
341:
342: $allCriteria = Criteria::model()->findAllByAttributes(array('modelType' => $modelClass, 'modelField' => $fieldName));
343: foreach ($allCriteria as $criteria) {
344: if (($criteria->comparisonOperator == "=" && $new == $criteria->modelValue)
345: || ($criteria->comparisonOperator == ">" && $new >= $criteria->modelValue)
346: || ($criteria->comparisonOperator == "<" && $new <= $criteria->modelValue)
347: || ($criteria->comparisonOperator == "change" && $new != $old)) {
348:
349: $users = preg_split('/[\s,]+/',$criteria->users,null,PREG_SPLIT_NO_EMPTY);
350:
351: if($criteria->type == 'notification') {
352: foreach($users as $user) {
353: $event=new Events;
354: $event->user=$user;
355: $event->associationType='Notification';
356: $event->type='notif';
357:
358: $notif = new Notification;
359: $notif->type = 'change';
360: $notif->fieldName = $fieldName;
361: $notif->modelType = get_class($model);
362: $notif->modelId = $model->id;
363:
364: if($criteria->comparisonOperator == 'change') {
365: $notif->comparison = 'change'; // if the criteria is just 'changed'
366: $notif->value = $new; // record the new value
367: } else {
368: $notif->comparison = $criteria->comparisonOperator; // otherwise record the operator type
369: $notif->value = mb_substr($criteria->modelValue, 0, 250, 'UTF-8'); // and the comparison value
370: }
371: $notif->user = $user;
372: $notif->createdBy = $this->editingUsername;
373: $notif->createDate = time();
374:
375: if($notif->save()) {
376: $event->associationId = $notif->id;
377: $event->save();
378: }
379: }
380: } elseif($criteria->type == 'action') {
381: foreach($users as $user) {
382: $action = new Actions;
383: $action->assignedTo = $user;
384: if ($criteria->comparisonOperator == "=") {
385: $action->actionDescription = "A record of type " . $modelClass . " has been modified to meet $criteria->modelField $criteria->comparisonOperator $criteria->modelValue" . " by " . $this->editingUsername;
386: } else if ($criteria->comparisonOperator == ">") {
387: $action->actionDescription = "A record of type " . $modelClass . " has been modified to meet $criteria->modelField $criteria->comparisonOperator $criteria->modelValue" . " by " . $this->editingUsername;
388: } else if ($criteria->comparisonOperator == "<") {
389: $action->actionDescription = "A record of type " . $modelClass . " has been modified to meet $criteria->modelField $criteria->comparisonOperator $criteria->modelValue" . " by " . $this->editingUsername;
390: } else if ($criteria->comparisonOperator == "change") {
391: $action->actionDescription = "A record of type " . $modelClass . " has had its $criteria->modelField field changed from ".$old.' to '.$new.' by '.$this->editingUsername;
392: }
393: $action->dueDate = mktime('23', '59', '59');
394: $action->createDate = time();
395: $action->lastUpdated = time();
396: $action->updatedBy = 'admin';
397: $action->visibility = 1;
398: $action->associationType = lcfirst($modelClass);
399: $action->associationId = $model->id;
400: $action->associationName = $model->name;
401: $action->save();
402: }
403: } elseif ($criteria->type == 'assignment') {
404: $model->assignedTo = $criteria->users;
405:
406: if ($model->save()) {
407: $event=new Events;
408: $event->type='notif';
409: $event->user=$model->assignedTo;
410: $event->associationType='Notification';
411:
412: $notif = new Notification;
413: $notif->user = $model->assignedTo;
414: $notif->createDate = time();
415: $notif->type = 'assignment';
416: $notif->modelType = $modelClass;
417: $notif->modelId = $model->id;
418: if($notif->save()){
419: $event->associationId = $notif->id;
420: if($this->createEvent){
421: $event->save();
422: }
423: }
424: }
425: }
426: }
427: }
428: }
429:
430: }
431: