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: 43: 44: 45: 46: 47: 48: 49: 50:
51: Yii::import('application.components.x2flow.X2FlowItem');
52: Yii::import('application.components.x2flow.X2FlowFormatter');
53: Yii::import('application.components.x2flow.actions.*');
54: Yii::import('application.components.x2flow.triggers.*');
55: Yii::import('application.models.ApiHook');
56:
57: class X2Flow extends X2ActiveRecord {
58:
59: 60: 61:
62: const MAX_TRIGGER_DEPTH = 0;
63:
64: 65: 66: 67: 68:
69: public static function model($className = __CLASS__){
70: return parent::model($className);
71: }
72:
73: 74: 75:
76: private static $_triggerDepth = 0;
77:
78: 79: 80:
81: public function tableName(){
82: return 'x2_flows';
83: }
84:
85: 86: 87:
88: public function rules(){
89: return array(
90: array('name, createDate, lastUpdated, triggerType, flow', 'required'),
91: array('createDate, lastUpdated', 'numerical', 'integerOnly' => true),
92: array('active', 'boolean'),
93: array('name', 'length', 'max' => 100),
94: array ('flow', 'validateFlow'),
95: array('triggerType, modelClass', 'length', 'max' => 40),
96:
97:
98: array('id, active, name, createDate, lastUpdated', 'safe', 'on' => 'search'),
99: );
100: }
101:
102: private $_flow;
103: public function getFlow ($refresh=false) {
104: if (!isset ($this->_flow) || $refresh) {
105: $this->_flow = CJSON::decode ($this->flow);
106: }
107: return $this->_flow;
108: }
109:
110: public function setFlow (array $flow) {
111: $this->_flow = $flow;
112: $this->flow = CJSON::encode ($flow);
113: }
114:
115: 116: 117:
118: public function validateFlow ($attribute) {
119: $flow = CJSON::decode ($this->$attribute);
120:
121: if(isset($flow['trigger']) && isset ($flow['items'])) {
122: if (!$this->validateFlowItem ($flow['trigger'])) {
123: return false;
124: }
125: if ($this->validateFlowPrime ($flow['items'])) {
126: return true;
127: }
128: } else {
129: $this->addError ($attribute, Yii::t('studio', 'Invalid flow'));
130: return false;
131: }
132: }
133:
134: public function afterSave () {
135: $flow = CJSON::decode ($this->flow);
136: $triggerClass = $flow['trigger']['type'];
137: if ($triggerClass === 'PeriodicTrigger') {
138: $trigger = X2FlowTrigger::create ($flow['trigger']);
139: $trigger->afterFlowSave ($this);
140: }
141: parent::afterSave ();
142: }
143:
144: 145: 146: 147:
148: public function behaviors(){
149: return array(
150: 'X2TimestampBehavior' => array('class' => 'X2TimestampBehavior'),
151: );
152: }
153:
154: 155: 156:
157: public function attributeLabels(){
158: return array(
159: 'id' => Yii::t('admin', 'ID'),
160: 'active' => Yii::t('admin', 'Active'),
161: 'name' => Yii::t('admin', 'Name'),
162: 'triggerType' => Yii::t('admin', 'Trigger'),
163: 'modelClass' => Yii::t('admin', 'Type'),
164: 'name' => Yii::t('admin', 'Name'),
165: 'createDate' => Yii::t('admin', 'Create Date'),
166: 'lastUpdated' => Yii::t('admin', 'Last Updated'),
167: 'description' => Yii::t('admin', 'Description'),
168: );
169: }
170:
171: 172: 173: 174: 175:
176: public function search(){
177: $criteria = new CDbCriteria;
178:
179: $criteria->compare('id', $this->id);
180: $criteria->compare('active', $this->active);
181: $criteria->compare('name', $this->name, true);
182: $criteria->compare('createDate', $this->createDate, true);
183: $criteria->compare('lastUpdated', $this->lastUpdated, true);
184:
185: return new CActiveDataProvider($this, array(
186: 'criteria' => $criteria,
187: 'pagination' => array (
188: 'pageSize' => Profile::getResultsPerPage(),
189: ),
190: ));
191: }
192:
193: 194: 195: 196: 197:
198: public function beforeValidate(){
199: $flowData = CJSON::decode($this->flow);
200:
201: if($flowData === false){
202: $this->addError('flow', Yii::t('studio', 'Flow configuration data appears to be '.
203: 'corrupt.'));
204: return false;
205: }
206: if(isset($flowData['trigger']['type'])){
207: $this->triggerType = $flowData['trigger']['type'];
208: if(isset($flowData['trigger']['modelClass']))
209: $this->modelClass = $flowData['trigger']['modelClass'];
210: } else{
211:
212: }
213: if(!isset($flowData['items']) || empty($flowData['items'])){
214: $this->addError('flow', Yii::t('studio', 'There must be at least one action in the '.
215: 'flow.'));
216: }
217:
218: $this->lastUpdated = time();
219: if($this->isNewRecord)
220: $this->createDate = $this->lastUpdated;
221: return parent::beforeValidate();
222: }
223:
224: 225: 226: 227:
228: public static function triggerDepth(){
229: return self::$_triggerDepth;
230: }
231:
232: 233: 234: 235: 236: 237: 238: 239: 240:
241: public static function trigger($triggerName, $params = array()){
242: if(self::$_triggerDepth > self::MAX_TRIGGER_DEPTH)
243: return;
244:
245: $triggeredAt = time ();
246:
247: if(isset($params['model']) &&
248: (!is_object($params['model']) || !($params['model'] instanceof X2Model))) {
249:
250: return false;
251: }
252:
253:
254: ApiHook::runAll($triggerName,$params);
255:
256:
257: self::$_triggerDepth++;
258:
259: $flowAttributes = array('triggerType' => $triggerName, 'active' => 1);
260:
261: if(isset($params['model'])){
262: $flowAttributes['modelClass'] = get_class($params['model']);
263: $params['modelClass'] = get_class($params['model']);
264: }
265:
266:
267: if (isset ($params['flowId'])) $flowAttributes['id'] = $params['flowId'];
268:
269: $flows = CActiveRecord::model('X2Flow')->findAllByAttributes($flowAttributes);
270:
271:
272: $triggerInfo = array (
273: 'triggerName' => Yii::t('studio', X2FlowItem::getTitle ($triggerName))
274: );
275: if (isset ($params['model']) && is_subclass_of($params['model'],'X2Model') &&
276: $params['model']->asa ('X2LinkableBehavior')) {
277: $triggerInfo['modelLink'] =
278: Yii::t('studio', 'View record: ').$params['model']->getLink ();
279: }
280:
281:
282:
283: $triggerLog;
284: $flowTrace;
285: $flowRetVal = null;
286: foreach($flows as &$flow) {
287: $triggerLog = new TriggerLog;
288: $triggerLog->triggeredAt = $triggeredAt;
289: $triggerLog->flowId = $flow->id;
290: $triggerLog->save ();
291:
292: $flowRetArr = self::_executeFlow($flow, $params, null, $triggerLog->id);
293: $flowTrace = $flowRetArr['trace'];
294: $flowRetVal = (isset ($flowRetArr['retVal'])) ? $flowRetArr['retVal'] : null;
295: $flowRetVal = self::extractRetValFromTrace ($flowTrace);
296:
297:
298: $triggerLog->triggerLog =
299: CJSON::encode (array_merge (array ($triggerInfo), array ($flowTrace)));
300: $triggerLog->save ();
301: }
302:
303: self::$_triggerDepth--;
304: return $flowRetVal;
305: }
306:
307: 308: 309: 310: 311: 312: 313:
314: public static function ($flowTrace) {
315:
316:
317: if (sizeof ($flowTrace) === 3 && $flowTrace[0] && !is_array ($flowTrace[1])) {
318: return $flowTrace[2];
319: }
320:
321:
322: if (sizeof ($flowTrace) < 2 || !$flowTrace[0] || !is_array ($flowTrace[1])) {
323: return null;
324: }
325:
326:
327: $startOfBranchExecution = $flowTrace[1];
328: $lastAction = $startOfBranchExecution[sizeof ($startOfBranchExecution) - 1];
329: while (true) {
330: if (is_subclass_of ($lastAction[0], 'MultiChildNode')){
331: $startOfBranchExecution = $lastAction[2];
332: if (sizeof ($startOfBranchExecution) > 0) {
333: $lastAction = $startOfBranchExecution[sizeof ($startOfBranchExecution) - 1];
334: } else {
335: return null;
336: }
337: } else {
338: break;
339: }
340: }
341:
342:
343: if (sizeof ($lastAction[1]) === 3) return $lastAction[1][2];
344: }
345:
346: 347: 348: 349: 350: 351: 352:
353: public static function resumeFlowExecution (
354: &$flow, &$params, $actionId = null, $triggerLogId=null){
355:
356: if(self::$_triggerDepth > self::MAX_TRIGGER_DEPTH)
357: return;
358:
359: if(isset($params['model']) &&
360: (!is_object($params['model']) || !($params['model'] instanceof X2Model))) {
361:
362: return false;
363: }
364:
365:
366: self::$_triggerDepth++;
367: $result = self::_executeFlow ($flow, $params, $actionId, $triggerLogId);
368: self::$_triggerDepth--;
369: return $result;
370: }
371:
372: 373: 374:
375: public static function executeFlow(&$flow, &$params, $actionId = null){
376: if(self::$_triggerDepth > self::MAX_TRIGGER_DEPTH)
377: return;
378:
379: self::$_triggerDepth++;
380:
381: $triggerInfo = array(
382: 'triggerName' => Yii::t('studio', X2FlowItem::getTitle('MacroTrigger'))
383: );
384: if (isset($params['model']) && is_subclass_of($params['model'], 'X2Model') &&
385: $params['model']->asa('X2LinkableBehavior')) {
386: $triggerInfo['modelLink'] = Yii::t('studio', 'View record: ') .
387: $params['model']->getLink();
388: }
389:
390: $triggerLog = new TriggerLog;
391: $triggerLog->triggeredAt = time();
392: $triggerLog->flowId = $flow->id;
393: $triggerLog->save();
394: $flowRetArr = self::_executeFlow ($flow, $params, $actionId, $triggerLog->id);
395: $flowTrace = $flowRetArr['trace'];
396:
397:
398: $triggerLog->triggerLog = CJSON::encode(
399: array_merge(array($triggerInfo), array($flowTrace)));
400: $triggerLog->save();
401:
402: self::$_triggerDepth--;
403: return $flowRetArr;
404: }
405:
406: 407: 408: 409: 410: 411: 412: 413: 414: 415: 416: 417: 418:
419: private function traverseLegacy (
420: $flowPath, &$flowItems, &$params, $pathIndex = 0, $triggerLogId=null) {
421:
422:
423: if(is_bool($flowPath[$pathIndex])){
424: foreach($flowItems as &$item){
425: if(is_subclass_of ($item['type'], 'MultiChildNode')){
426: $nodeClass = $item['type'];
427: if($flowPath[$pathIndex] && isset($item[$nodeClass::getRightChildName ()])){
428: if(isset($flowPath[$pathIndex + 1])) {
429: return $this->traverseLegacy(
430: $flowPath, $item[$nodeClass::getRightChildName ()], $params,
431: $pathIndex + 1, $triggerLogId);
432: } else {
433: return array(
434: $item['type'], true,
435: $this->executeBranch(
436: $item[$nodeClass::getRightChildName], $params, $triggerLogId)
437: );
438: }
439: } elseif(!$flowPath[$pathIndex] &&
440: isset($item[$nodeClass::getLeftChildName ()])){
441:
442: if(isset($flowPath[$pathIndex + 1])) {
443: return $this->traverseLegacy(
444: $flowPath, $item[$nodeClass::getLeftChildName ()], $params,
445: $pathIndex + 1, $triggerLogId);
446: } else {
447: return array(
448: $item['type'], false,
449: $this->executeBranch(
450: $item[$nodeClass::getLeftChildName ()], $params, $triggerLogId)
451: );
452: }
453: }
454: }
455: }
456: return false;
457: } else {
458: if(isset($flowPath[$pathIndex])) {
459: $sliced = array_slice ($flowItems, $flowPath[$pathIndex]);
460: return $this->executeBranch($sliced, $params, $triggerLogId);
461: }
462: }
463: }
464:
465: 466: 467: 468: 469: 470:
471: private function traverse ($actionId, &$flowItems, &$params, $triggerLogId=null){
472: if (is_array ($actionId)) {
473: return $this->traverseLegacy (
474: $actionId, $flowItems, $params, 0, $triggerLogId);
475: } else {
476: $i = 0;
477:
478: foreach ($flowItems as $item) {
479: if ($item['id'] === $actionId) {
480: $sliced = array_slice ($flowItems, $i + 1);
481: return $this->executeBranch ($sliced, $params, $triggerLogId);
482: }
483: if (is_subclass_of ($item['type'], 'MultiChildNode')) {
484: $nodeClass = $item['type'];
485: if (isset ($item[$nodeClass::getLeftChildName ()])) {
486: $ret = $this->traverse (
487: $actionId, $item[$nodeClass::getLeftChildName ()], $params,
488: $triggerLogId);
489: if ($ret) return $ret;
490: }
491: if (isset ($item[$nodeClass::getRightChildName ()])) {
492: $ret = $this->traverse (
493: $actionId, $item[$nodeClass::getRightChildName ()], $params,
494: $triggerLogId);
495: if ($ret) return $ret;
496: }
497: }
498: $i++;
499: }
500: }
501: return false;
502: }
503:
504: 505: 506: 507: 508: 509: 510: 511:
512: public function executeBranch(&$flowItems, &$params, $triggerLogId=null){
513: $results = array();
514:
515: for($i = 0; $i < count($flowItems); $i++){
516: $item = &$flowItems[$i];
517: if(!isset($item['type']) || !class_exists($item['type']))
518: continue;
519:
520: $node = X2FlowItem::create($item);
521: if($item['type'] === 'X2FlowSwitch'){
522: $validateRetArr = $node->validate($params, $this->id);
523: if($validateRetArr[0]){
524:
525: $checkRetArr = $node->check($params);
526: if($checkRetArr[0] && isset($item['trueBranch'])){
527: $results[] = array(
528: $item['type'], true,
529: $this->executeBranch(
530: $item['trueBranch'], $params, $triggerLogId)
531: );
532: }elseif(isset($item['falseBranch'])){
533: $results[] = array(
534: $item['type'], false,
535: $this->executeBranch(
536: $item['falseBranch'], $params, $triggerLogId)
537: );
538: }
539: }
540: }elseif($item['type'] === 'X2FlowSplitter'){
541: $validateRetArr = $node->validate($params, $this->id);
542: if($validateRetArr[0]){
543:
544: $branchVal = true;
545: if(isset($item[X2FlowSplitter::getRightChildName ()])){
546: $results[] = array(
547: $item['type'], true,
548: $this->executeBranch(
549: $item[X2FlowSplitter::getRightChildName ()], $params, $triggerLogId)
550: );
551: }
552: if(isset($item[X2FlowSplitter::getLeftChildName ()])){
553: $results[] = array(
554: $item['type'], false,
555: $this->executeBranch(
556: $item[X2FlowSplitter::getLeftChildName ()], $params, $triggerLogId)
557: );
558: }
559: }
560: }else{
561: $flowAction = X2FlowAction::create($item);
562: if($item['type'] === 'X2FlowWait'){
563: $node->flowId = $this->id;
564: $results[] = $this->validateAndExecute (
565: $item, $node, $params, $triggerLogId);
566: break;
567: }else{
568: $results[] = $this->validateAndExecute (
569: $item, $node, $params, $triggerLogId);
570: }
571: }
572: }
573: return $results;
574: }
575:
576: public function validateAndExecute ($item, $flowAction, &$params, $triggerLogId=null) {
577: $logEntry;
578: $validationRetStatus = $flowAction->validate($params, $this->id);
579: if ($validationRetStatus[0] === true) {
580: $logEntry = array ($item['type'], $flowAction->execute ($params, $triggerLogId, $this));
581: } else {
582: $logEntry = array ($item['type'], $validationRetStatus);
583: }
584: return $logEntry;
585: }
586:
587: 588: 589: 590: 591: 592: 593:
594: public static function parseValue($value, $type, &$params = null, $renderFlag=true){
595: if (is_string ($value)){
596: if (strpos ($value, '=') === 0){
597:
598: $evald = X2FlowFormatter::parseFormula($value, $params);
599:
600:
601:
602: $value = '';
603: if($evald[0])
604: $value = $evald[1];
605: } else {
606:
607: $value = X2FlowFormatter::replaceVariables(
608: $value, $params, $type, $renderFlag, true);
609: }
610: }
611:
612: switch($type){
613: case 'boolean':
614: return (bool) $value;
615: case 'time':
616: case 'date':
617: case 'dateTime':
618: if(ctype_digit((string) $value))
619: return $value;
620: if($type === 'date')
621: $value = Formatter::parseDate($value);
622: elseif($type === 'dateTime')
623: $value = Formatter::parseDateTime($value);
624: else
625: $value = strtotime($value);
626: return $value === false ? null : $value;
627: case 'link':
628: $pieces = explode('_', $value);
629: if(count($pieces) > 1)
630: return $pieces[0];
631: return $value;
632: case 'tags':
633: return Tags::parseTags ($value);
634: default:
635: return $value;
636: }
637: }
638:
639:
640: public static function getModelTypes($assoc=false) {
641: return array_diff_key (
642: X2Model::getModelTypes ($assoc),
643: array_flip (array ('Fingerprint', 'Charts','EmailInboxes')));
644: }
645:
646: 647: 648: 649: 650: 651: 652: 653: 654: 655:
656: private static function _executeFlow(&$flow, &$params, $actionId = null, $triggerLogId=null){
657: $error = '';
658:
659: $flowData = $flow->getFlow ();
660:
661: if($flowData !== false &&
662: isset($flowData['trigger']['type'], $flowData['items'][0]['type'])){
663:
664: if($actionId === null){
665: $trigger = X2FlowTrigger::create($flowData['trigger']);
666: assert ($trigger !== null);
667: if($trigger === null) {
668: $error = array (
669: 'trace' => array (false, 'failed to load trigger class'));
670: }
671: $validateRetArr = $trigger->validate($params, $flow->id);
672: if (!$validateRetArr[0]) {
673: $error = $validateRetArr;
674: return array ('trace' => $error);
675: } else if (sizeof ($validateRetArr) === 3) {
676: return array (
677: 'trace' => $validateRetArr,
678: 'retVal' => $validateRetArr[2]
679: );
680: }
681: $checkRetArr = $trigger->check($params);
682: if (!$checkRetArr[0]) {
683: $error = $checkRetArr;
684: }
685:
686: if(empty($error)){
687: try{
688: $flowTrace = array (true, $flow->executeBranch (
689: $flowData['items'], $params, $triggerLogId));
690: $flowRetVal = self::extractRetValFromTrace ($flowTrace);
691: if (!$flowRetVal) {
692: $flowRetVal = $trigger->getDefaultReturnVal ($flow->id);
693: }
694: return array (
695: 'trace' => $flowTrace,
696: 'retVal' => $flowRetVal,
697: );
698: }catch(Exception $e){
699: return array ('trace' => array (false, $e->getMessage()));
700:
701: }
702: } else {
703: return array ('trace' => $error);
704: }
705: }else{
706: try{
707: return array (
708: 'trace' => array (
709: true, $flow->traverse(
710: $actionId, $flowData['items'], $params, $triggerLogId)));
711: } catch(Exception $e) {
712: return array (
713: 'trace' => array (false, $e->getMessage()));
714: }
715: }
716: }else{
717: return array ('trace' => array (false, 'invalid flow data'));
718: }
719: }
720:
721: 722: 723: 724: 725:
726: private function validateFlowPrime ($items) {
727: $valid = true;
728:
729: foreach ($items as $item) {
730: if(!isset($item['type']) || !class_exists($item['type'])) {
731: continue;
732: }
733: if(is_subclass_of ($item['type'], 'MultiChildNode')){
734: $nodeClass = $item['type'];
735: if (isset ($item['type'][$nodeClass::getLeftChildName ()])) {
736: if (!$this->validateFlowPrime ($item[$nodeClass::getLeftChildName ()])) {
737: $valid = false;
738: break;
739: }
740: }
741: if (isset ($item['type'][$nodeClass::getRightChildName ()])) {
742: if (!$this->validateFlowPrime ($item[$nodeClass::getRightChildName ()])) {
743: $valid = false;
744: break;
745: }
746: }
747: } else {
748: $valid = $this->validateFlowItem ($item);
749: if (!$valid) {
750: break;
751: }
752: }
753: }
754: return $valid;
755: }
756:
757: 758: 759: 760:
761: private function validateFlowItem ($config, $action=true) {
762: $class = $action ? 'X2FlowAction' : 'X2FlowTrigger';
763: $flowItem = $class::create ($config);
764: $paramRules = $flowItem->paramRules ();
765: list ($success, $message) = $flowItem->validateOptions ($paramRules, null, true);
766: if ($success === false) {
767: $this->addError ('flow', $flowItem->title.': '.$message);
768: return false;
769: } else if ($success === X2FlowItem::VALIDATION_WARNING) {
770: Yii::app()->user->setFlash (
771: 'notice', Yii::t('studio', $message));
772: }
773: return true;
774: }
775:
776: }
777: