1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35:
36:
37: 38: 39: 40: 41:
42: abstract class X2FlowTrigger extends X2FlowItem {
43:
44: 45: 46:
47: public $notifType = '';
48: 49: 50:
51: public $eventType = '';
52:
53: 54: 55:
56: public static function getFieldComparisonOptions () {
57: return array(
58: '=' => Yii::t('app','equals'),
59: '>' => Yii::t('app','greater than'),
60: '<' => Yii::t('app','less than'),
61: '>=' => Yii::t('app','greater than or equal to'),
62: '<=' => Yii::t('app','less than or equal to'),
63: '<>' => Yii::t('app','not equal to'),
64: 'list' => Yii::t('app','in list'),
65: 'notList' => Yii::t('app','not in list'),
66: 'empty' => Yii::t('app','empty'),
67: 'notEmpty' => Yii::t('app','not empty'),
68: 'contains' => Yii::t('app','contains'),
69: 'noContains' => Yii::t('app','does not contain'),
70: 'changed' => Yii::t('app','changed'),
71: 'before' => Yii::t('app','before'),
72: 'after' => Yii::t('app','after'),
73: );
74: }
75:
76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89:
90:
91: public static $genericConditions = array(
92: 'attribute' => 'Compare Attribute',
93: 'workflow_status' => 'Process Status',
94: 'current_user' => 'Current User',
95: 'month' => 'Current Month',
96: 'day_of_week' => 'Day of Week',
97: 'day_of_month' => 'Day of Month',
98: 'time_of_day' => 'Time of Day',
99: 'current_time' => 'Current Time',
100: 'user_active' => 'User Logged In',
101: 'on_list' => 'On List',
102: 'has_tags' => 'Has Tags',
103: 'email_open' => 'Email Opened',
104:
105:
106: );
107:
108: public static function getGenericConditions() {
109: return array_map(function($term){
110: return Yii::t('studio',$term);
111: },self::$genericConditions);
112: }
113:
114: public static function getGenericCondition($type) {
115: switch($type) {
116: case 'current_user':
117: return array(
118: 'name' => 'user',
119: 'label' => Yii::t('studio','Current User'),
120: 'type' => 'dropdown',
121: 'multiple' => 1,
122: 'options' => X2Model::getAssignmentOptions(false,false),
123: 'operators'=>array('=','<>','list','notList')
124: );
125:
126: case 'month':
127: return array(
128: 'label'=>Yii::t('studio','Current Month'),
129: 'type' => 'dropdown',
130: 'multiple' => 1,
131: 'options' => Yii::app()->locale->monthNames,
132: 'operators'=>array('=','<>','list','notList')
133: );
134:
135: case 'day_of_week':
136: return array(
137: 'label' => Yii::t('studio','Day of Week'),
138: 'type' => 'dropdown',
139: 'multiple' => 1,
140: 'options' => Yii::app()->locale->weekDayNames,
141: 'operators'=>array('=','<>','list','notList')
142: );
143:
144: case 'day_of_month':
145: $days = array_keys(array_fill(1,31,1));
146: return array(
147: 'label' => Yii::t('studio','Day of Month'),
148: 'type' => 'dropdown',
149: 'multiple' => 1,
150: 'options' => array_combine($days,$days),
151: 'operators'=>array('=','<>','list','notList')
152: );
153:
154: case 'time_of_day':
155: return array(
156: 'label' => Yii::t('studio','Time of Day'),
157: 'type' => 'time',
158: 'operators'=>array('before','after')
159: );
160:
161:
162:
163: case 'current_time':
164: return array(
165: 'label' => Yii::t('studio','Current Time'),
166: 'type' => 'dateTime',
167: 'operators'=>array('before','after')
168: );
169:
170: case 'user_active':
171: return array(
172: 'label' => Yii::t('studio','User Logged In'),
173: 'type' => 'dropdown',
174: 'options' => X2Model::getAssignmentOptions(false,false)
175: );
176:
177: case 'on_list':
178: return array(
179: 'label' => Yii::t('studio','On List'),
180: 'type' => 'link',
181: 'linkType'=>'X2List',
182: 'linkSource'=>Yii::app()->controller->createUrl(
183: CActiveRecord::model('X2List')->autoCompleteSource)
184: );
185: case 'has_tags':
186: return array(
187: 'label' => Yii::t('studio','Has Tags'),
188: 'type' => 'tags',
189: );
190: case 'email_open':
191: return array(
192: 'label' => Yii::t('studio', 'Email Opened'),
193: 'type' => 'dropdown',
194: 'options' => array(),
195: );
196: default:
197: return false;
198:
199:
200:
201:
202:
203:
204:
205:
206:
207:
208:
209:
210: }
211: }
212:
213: 214: 215:
216: public function getDefaultReturnVal ($flowId) { return null; }
217:
218: 219: 220:
221: public function afterValidate (&$params, $defaultErrMsg='', $flowId) {
222: return array (false, Yii::t('studio', $defaultErrMsg));
223: }
224:
225: 226: 227:
228: public function validate(&$params=array(), $flowId=null) {
229: $paramRules = $this->paramRules();
230: if(!isset($paramRules['options'],$this->config['options'])) {
231: return $this->afterValidate (
232: $params, YII_DEBUG ?
233: 'invalid rules/params: trigger passed options when it specifies none' :
234: 'invalid rules/params', $flowId);
235: }
236: $config = &$this->config['options'];
237:
238: if(isset($paramRules['modelClass'])) {
239: $modelClass = $paramRules['modelClass'];
240: if($modelClass === 'modelClass') {
241: if(isset($config['modelClass'],$config['modelClass']['value'])) {
242: $modelClass = $config['modelClass']['value'];
243: } else {
244: return $this->afterValidate (
245: $params, YII_DEBUG ?
246: 'invalid rules/params: '.
247: 'trigger requires model class option but given none' :
248: 'invalid rules/params', $flowId);
249: }
250: }
251: if(!isset($params['model'])) {
252: return $this->afterValidate (
253: $params, YII_DEBUG ?
254: 'invalid rules/params: trigger requires a model but passed none' :
255: 'invalid rules/params', $flowId);
256: }
257: if($modelClass !== get_class($params['model'])) {
258: return $this->afterValidate (
259: $params, YII_DEBUG ?
260: 'invalid rules/params: required model class does not match model passed ' .
261: 'to trigger' :
262: 'invalid rules/params', $flowId);
263: }
264: }
265: return $this->validateOptions($paramRules,$params);
266: }
267:
268: 269: 270: 271: 272:
273: public function check(&$params) {
274: foreach($this->config['options'] as $name => &$option) {
275:
276: if($name === 'modelClass')
277: continue;
278:
279:
280: if($option['optional'] && ($option['value'] === null || $option['value'] === ''))
281: continue;
282:
283: $value = $option['value'];
284:
285: if(isset($option['type']))
286: $value = X2Flow::parseValue($value,$option['type'],$params);
287:
288: if (isset ($option['comparison']) && !$option['comparison']) {
289: continue;
290: }
291:
292: if(!static::evalComparison($params[$name], $option['operator'], $value)) {
293: if (is_string ($value) && is_string ($params[$name]) &&
294: is_string ($option['operator'])) {
295:
296: return array (
297: false,
298: Yii::t('studio', 'The following condition did not pass: ' .
299: '{name} {operator} {value}', array (
300: '{name}' => $params[$name],
301: '{operator}' => $option['operator'],
302: '{value}' => (string) $value,
303: ))
304: );
305: } else {
306: return array (
307: false,
308: Yii::t('studio', 'Condition failed')
309: );
310: }
311: }
312: }
313:
314: return $this->checkConditions($params);
315: }
316: 317: 318: 319:
320: public function checkConditions(&$params) {
321: if(isset($this->config['conditions'])){
322: foreach($this->config['conditions'] as &$condition) {
323: if(!isset($condition['type']))
324: $condition['type'] = '';
325:
326: $required = isset($condition['required']) && $condition['required'];
327:
328:
329: if(isset($condition['name']) && $required && !isset($params[$condition['name']])) {
330: if (YII_DEBUG) {
331: return array (false, Yii::t('studio', 'a required parameter is missing'));
332: } else {
333: return array (false, Yii::t('studio', 'conditions not passed'));
334: }
335: }
336:
337: if(array_key_exists($condition['type'],self::$genericConditions)) {
338: if(!self::checkCondition($condition,$params))
339: return array (
340: false,
341: Yii::t('studio', 'conditions not passed')
342: );
343: }
344: }
345: }
346: return array (true, '');
347: }
348:
349: 350: 351: 352: 353: 354:
355: public static function checkWorkflowStatusCondition ($condition, &$params) {
356: if (!isset ($params['model']) ||
357: !isset ($condition['workflowId']) ||
358: !isset ($condition['stageNumber']) ||
359: !isset ($condition['stageState'])) {
360:
361: return false;
362: }
363:
364: $model = $params['model'];
365: $workflowId = $condition['workflowId'];
366: $stageNumber = $condition['stageNumber'];
367: $stageState = $condition['stageState'];
368: $modelId = $model->id;
369: $type = lcfirst (X2Model::getModuleName (get_class ($model)));
370:
371: $workflowStatus = Workflow::getWorkflowStatus($workflowId,$modelId,$type);
372: $stages = $workflowStatus['stages'];
373: if (!isset ($workflowStatus['stages'][$stageNumber])) return false;
374:
375: $stage = $workflowStatus['stages'][$stageNumber];
376:
377: $passed = false;
378: switch ($stageState) {
379: case 'completed':
380: $passed = Workflow::isCompleted ($workflowStatus, $stageNumber);
381: break;
382: case 'started':
383: $passed = Workflow::isStarted ($workflowStatus, $stageNumber);
384: break;
385: case 'notCompleted':
386: $passed = !Workflow::isCompleted ($workflowStatus, $stageNumber);
387: break;
388: case 'notStarted':
389: $passed = !Workflow::isStarted ($workflowStatus, $stageNumber);
390: break;
391: default:
392: return false;
393: }
394: return $passed;
395: }
396:
397: 398: 399: 400: 401:
402: public static function checkCondition($condition,&$params) {
403: if ($condition['type'] === 'workflow_status')
404: return self::checkWorkflowStatusCondition ($condition, $params);
405:
406: $model = isset($params['model'])? $params['model'] : null;
407: $operator = isset($condition['operator'])? $condition['operator'] : '=';
408:
409: $value = isset($condition['value'])? $condition['value'] : null;
410:
411:
412: if(isset($condition['name']) && $condition['type'] === '') {
413: if(!isset($params[$condition['name']]))
414: return false;
415:
416: return self::evalComparison($params[$condition['name']],$operator,$value);
417: }
418:
419: switch($condition['type']) {
420: case 'attribute':
421: if(!isset($condition['name'],$model))
422: return false;
423: $attr = &$condition['name'];
424: if(null === $field = $model->getField($attr))
425: return false;
426:
427: if($operator === 'changed') {
428: return $model->attributeChanged ($attr);
429: }
430:
431: if ($field->type === 'link') {
432: list ($attrVal, $id) = Fields::nameAndId ($model->getAttribute ($attr));
433: } else {
434: $attrVal = $model->getAttribute ($attr);
435: }
436:
437: return self::evalComparison(
438: $attrVal,$operator, X2Flow::parseValue($value,$field->type,$params), $field);
439:
440: case 'current_user':
441: return self::evalComparison(
442: Yii::app()->user->getName(),$operator,
443: X2Flow::parseValue($value,'assignment',$params));
444:
445: case 'month':
446: return self::evalComparison((int)date('n'),$operator,$value);
447:
448: case 'day_of_month':
449: return self::evalComparison((int)date('j'),$operator,$value);
450:
451: case 'day_of_week':
452: return self::evalComparison((int)date('N'),$operator,$value);
453:
454: case 'time_of_day':
455: return self::evalComparison(time(),$operator,X2Flow::parseValue($value,'time',$params));
456:
457:
458:
459: case 'current_time':
460: return self::evalComparison(time(),$operator,X2Flow::parseValue($value,'dateTime',$params));
461:
462: case 'user_active':
463: return CActiveRecord::model('Session')->exists('user=:user AND status=1',array(':user'=>X2Flow::parseValue($value,'assignment',$params)));
464:
465:
466: case 'on_list':
467: if(!isset($model,$value))
468: return false;
469: $value = X2Flow::parseValue ($value, 'link');
470:
471:
472: if(is_numeric($value)){
473: $list = CActiveRecord::model('X2List')->findByPk($value);
474: }else{
475: $list = CActiveRecord::model('X2List')->findByAttributes(
476: array('name'=>$value));
477: }
478:
479: return ($list !== null && $list->hasRecord ($model));
480: case 'has_tags':
481: if(!isset($model,$value))
482: return false;
483: $tags = X2Flow::parseValue ($value, 'tags');
484: return $model->hasTags ($tags, 'AND');
485: case 'workflow_status':
486: if(!isset($model,$condition['workflowId'],$condition['stageNumber']))
487: return false;
488:
489: switch($operator) {
490: case 'started_workflow':
491: return CActiveRecord::model('Actions')->exists(
492: 'associationType=:type AND associationId=:modelId AND type="workflow" AND workflowId=:workflow',array(
493: ':type' => get_class($model),
494: ':modelId' => $model->id,
495: ':workflow' => $condition['workflowId'],
496: ));
497: case 'started_stage':
498: return CActiveRecord::model('Actions')->exists(
499: 'associationType=:type AND associationId=:modelId AND type="workflow" AND workflowId=:workflow AND stageNumber=:stage AND (completeDate IS NULL OR completeDate=0)',array(
500: ':type' => get_class($model),
501: ':modelId' => $model->id,
502: ':workflow' => $condition['workflowId'],
503: ':stageNumber' => $condition['stageNumber'],
504: ));
505: case 'completed_stage':
506: return CActiveRecord::model('Actions')->exists(
507: 'associationType=:type AND associationId=:modelId AND type="workflow" AND workflowId=:workflow AND stageNumber=:stage AND completeDate > 0',array(
508: ':type' => get_class($model),
509: ':modelId' => $model->id,
510: ':workflow' => $condition['workflowId'],
511: ':stageNumber' => $condition['stageNumber'],
512: ));
513: case 'completed_workflow':
514: $stageCount = CActiveRecord::model('WorkflowStage')->count('workflowId=:id',array(':id'=>$condition['workflowId']));
515: $actionCount = CActiveRecord::model('Actions')->count(
516: 'associationType=:type AND associationId=:modelId AND type="workflow" AND workflowId=:workflow',array(
517: ':type' => get_class($model),
518: ':modelId' => $model->id,
519: ':workflow' => $condition['workflowId'],
520: ));
521: return $actionCount >= $stageCount;
522: }
523: return false;
524: case 'email_open':
525: if (isset($params['sentEmails'], $params['sentEmails'][$value])) {
526: $trackEmail = TrackEmail::model()->findByAttributes(array('uniqueId' => $params['sentEmails'][$value]));
527: return $trackEmail && !is_null($trackEmail->opened);
528: }
529: return false;
530: }
531: return false;
532:
533:
534:
535:
536:
537:
538:
539:
540:
541:
542:
543:
544:
545: }
546:
547: protected static function parseArray ($operator, $value) {
548: $expectsArray = array ('list', 'notList', 'between');
549:
550:
551: if(in_array($operator, $expectsArray, true) && !is_array($value)) {
552: $value = explode(',',$value);
553:
554: $len = count($value);
555: for($i=0;$i<$len; $i++) {
556:
557: if(($value[$i] = trim($value[$i])) === '')
558: unset($value[$i]);
559: }
560: }
561: return $value;
562: }
563:
564: 565: 566: 567: 568: 569: 570:
571: public static function evalComparison($subject,$operator,$value=null, Fields $field = null) {
572: $value = self::parseArray ($operator, $value);
573:
574:
575:
576:
577:
578:
579:
580:
581: switch($operator) {
582: case '=':
583:
584: if ($field && $field->type === 'dropdown') {
585: $dropdown = $field->getDropdown ();
586: if ($dropdown && $dropdown->multi) {
587: $subject = StringUtil::jsonDecode ($subject, false);
588: AuxLib::coerceToArray ($subject);
589: AuxLib::coerceToArray ($value);
590: return $subject === $value;
591: }
592:
593: } else if ($field && $field->type === 'assignment' &&
594: $field->linkType === 'multiple') {
595:
596: $subject = explode (Fields::MULTI_ASSIGNMENT_DELIM, $subject);
597: AuxLib::coerceToArray ($subject);
598: AuxLib::coerceToArray ($value);
599: return $subject === $value;
600: }
601:
602:
603:
604:
605: if (is_array ($value)) {
606: AuxLib::coerceToArray ($subject);
607: }
608: return $subject == $value;
609:
610: case '>':
611: return $subject > $value;
612:
613: case '<':
614: return $subject < $value;
615:
616: case '>=':
617: return $subject >= $value;
618:
619: case '<=':
620: return $subject <= $value;
621:
622: case 'between':
623: if(count($value) !== 2)
624: return false;
625: return $subject >= min($value) && $subject <= max($value);
626:
627: case '<>':
628: case '!=':
629: return $subject != $value;
630:
631: case 'notEmpty':
632: return $subject !== null && $subject !== '';
633:
634: case 'empty':
635: return $subject === null || trim($subject) === '';
636:
637: case 'list':
638: if(count($value) === 0)
639: return false;
640: foreach($value as &$val)
641: if($subject == $val)
642: return true;
643:
644: return false;
645:
646: case 'notList':
647: if(count($value) === 0)
648: return true;
649: foreach($value as &$val)
650: if($subject == $val)
651: return false;
652:
653: return true;
654:
655: case 'noContains':
656: return stripos($subject,$value) === false;
657:
658: case 'contains':
659: default:
660: return stripos($subject,$value) !== false;
661: }
662: }
663:
664:
665:
666:
667:
668:
669:
670:
671:
672:
673:
674:
675:
676:
677:
678: protected static $_tokenChars = array(
679: ',' => 'COMMA',
680: '{' => 'OPEN_BRACKET',
681: '}' => 'CLOSE_BRACKET',
682: '+' => 'ADD',
683: '-' => 'SUBTRACT',
684: '*' => 'MULTIPLY',
685: '/' => 'DIVIDE',
686: '%' => 'MOD',
687:
688:
689: );
690: protected static $_tokenRegex = array(
691: '\d+\.\d+\b|^\.?\d+\b' => 'NUMBER',
692: '[a-zA-Z]\w*\.[a-zA-Z]\w*' => 'VAR_COMPLEX',
693: '[a-zA-Z]\w*' => 'VAR',
694: '\s+' => 'SPACE',
695: '.' => 'UNKNOWN',
696: );
697:
698: 699: 700: 701: 702: 703:
704: protected static function tokenize($str) {
705: $tokens = array();
706: $offset = 0;
707: while($offset < mb_strlen($str)) {
708: $token = array();
709:
710: $substr = mb_substr($str,$offset);
711:
712: foreach(self::$_tokenChars as $char => &$name) {
713: if(mb_substr($substr,0,1) === $char) {
714: $tokens[] = array($name);
715: $offset++;
716: continue 2;
717: }
718: }
719: foreach(self::$_tokenRegex as $regex => &$name) {
720: $matches = array();
721: if(preg_match('/^'.$regex.'/u',$substr,$matches) === 1) {
722: $tokens[] = array($name,$matches[0]);
723: $offset += mb_strlen($matches[0]);
724: continue 2;
725: }
726: }
727: $offset++;
728: }
729: return $tokens;
730: }
731:
732: 733: 734: 735: 736: 737:
738: protected static function addNode(&$tree,$nodePath,$value) {
739: if(count($nodePath) > 0)
740: return self::addNode($tree[array_shift($nodePath)],$nodePath,$value);
741:
742: $tree[] = $value;
743: return count($tree) - 1;
744: }
745:
746: 747: 748: 749: 750:
751: protected static function simplifyNode(&$tree,$nodePath) {
752: if(count($nodePath) > 0)
753: return self::simplifyNode($tree[array_shift($nodePath)],$nodePath);
754:
755: $last = count($tree) - 1;
756:
757: if(empty($tree[$last][1]))
758: array_pop($tree);
759: elseif(count($tree[$last][1]) === 1)
760: $tree[$last] = $tree[$last][1][0];
761: }
762:
763: 764: 765: 766: 767: 768:
769: 770: 771: 772: 773: 774: 775: 776: 777: 778: 779: 780: 781: 782: 783: 784: 785: 786: 787: 788: 789: 790: 791: 792: 793: 794: 795: 796: 797: 798: 799: 800: 801: 802: 803: 804: 805: 806: 807: 808: 809: 810: 811: 812: 813: 814: 815: 816: 817: 818: 819: 820: 821: 822: 823: 824: 825: 826: 827: 828: 829:
830:
831: 832: 833: 834: 835:
836: public static function parseExpressionTree($str) {
837:
838: $tokens = self::tokenize($str);
839:
840: $tree = array();
841: $nodePath = array();
842: $error = false;
843:
844: for($i=0;$i<count($tokens);$i++) {
845: switch($tokens[$i][0]) {
846: case 'OPEN_BRACKET':
847: $nodePath[] = self::addNode($tree,$nodePath,array('EXPRESSION',array()));
848: $nodePath[] = 1;
849: break;
850: case 'CLOSE_BRACKET':
851: if(count($nodePath) > 1) {
852: $nodePath = array_slice($nodePath,0,-2);
853: self::simplifyNode($tree,$nodePath);
854:
855: } else {
856: $error = 'unbalanced brackets';
857: }
858: break;
859:
860: case 'SPACE': break;
861: default:
862: self::addNode($tree,$nodePath,$tokens[$i]);
863: }
864: }
865:
866: if(count($nodePath) !== 0)
867: $error = 'unbalanced brackets';
868:
869: if($error !== false)
870: return 'ERROR: '.$error;
871: else
872: return $tree;
873: }
874:
875: 876: 877: 878: 879: 880: 881:
882: public static function getTriggerTypes($queryProperty = False,$queryValue = False) {
883: $types = array();
884: foreach(self::getTriggerInstances() as $class) {
885: $include = true;
886: if($queryProperty)
887: $include = $class->$queryProperty == $queryValue;
888: if($include)
889: $types[get_class($class)] = Yii::t('studio',$class->title);
890: }
891: return $types;
892: }
893:
894: 895: 896: 897: 898: 899:
900: public static function getTriggerTitle ($triggerType) {
901: foreach(self::getTriggerInstances() as $class) {
902: if (get_class ($class) === $triggerType) {
903: return Yii::t('studio', $class->title);
904: }
905: }
906: return '';
907: }
908:
909: public static function getTriggerInstances(){
910: return self::getInstances('triggers',array(__CLASS__,'X2FlowSwitch','X2FlowSplitter','BaseTagTrigger','BaseWorkflowStageTrigger', 'BaseWorkflowTrigger', 'BaseUserTrigger', 'MultiChildNode'));
911: }
912: }
913: