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: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77:
78: class Api2Controller extends CController {
79:
80: const ENABLED = true;
81: const MAX_PAGE_SIZE = 1000;
82:
83: const FIND_DELIM = ';';
84: const FIND_EQUAL = '=';
85:
86: 87: 88: 89:
90: private $_jpost;
91:
92: 93: 94: 95:
96: private $_model;
97:
98: 99: 100: 101:
102: private ;
103:
104: 105: 106: 107: 108: 109:
110: private $_staticModel;
111:
112: 113: 114: 115:
116: private $_user;
117:
118:
119:
120:
121:
122:
123:
124: 125: 126: 127: 128:
129: public function actionAppInfo() {
130: $this->response['message'] = "Welcome to the X2Engine REST API!";
131: $this->response['name'] = Yii::app()->settings->appName;
132: $this->response['version'] = Yii::app()->params->version;
133: $this->response['edition'] = Yii::app()->editionLabel;
134: $this->response['buildDate'] = Yii::app()->params->buildDate;
135: $this->response['clientAddress'] = Yii::app()->request->userHostAddress;
136: $this->response['serverName'] = $_SERVER['SERVER_NAME'];
137: }
138:
139: 140: 141: 142: 143: 144:
145: public function actionCount($_class,$_findBy=null) {
146: $staticModel = $this->getStaticModel();
147:
148: if(!empty($_findBy)) {
149:
150: $attributeConditions = $this->findConditions(
151: $_findBy,
152: $staticModel->attributes
153: );
154: $criteria = new CDbCriteria ();
155: foreach ($attributeConditions as $field => $value)
156: $criteria->compare ($field, $value);
157: $count = $this->getDataProvider(null, $criteria)->getTotalItemCount();
158: } else {
159:
160: $count = $this->getDataProvider()->getTotalItemCount();
161: }
162: $this->responseBody = $count;
163: }
164:
165: 166: 167: 168: 169:
170: public function actionDropdowns($_id=null) {
171: if($_id !== null){
172:
173: if(!(($dropdown = Dropdowns::model()->findByPk($_id)) instanceof Dropdowns))
174: $this->send(404);
175: $dropdown->options = json_decode($dropdown->options, 1);
176: $this->responseBody = $dropdown;
177: } else {
178:
179: $this->responseBody = array_map(function($d){
180: $d->options = json_decode($d->options, 1);
181: return $d;
182: }, $this->getDataProvider('Dropdowns')->getData());
183: }
184: }
185:
186: 187: 188:
189: public function actionFieldPermissions($_class) {
190: $this->responseBody = $this->staticModel->fieldPermissions;
191: }
192:
193: 194: 195: 196: 197: 198:
199: public function actionFields($_class,$_fieldName=null) {
200: $c = new CDbCriteria;
201: $c->compare('modelName',$_class);
202: if(!empty($_fieldName))
203: $c->compare('fieldName',$_fieldName);
204: $dp = $this->getDataProvider('Fields',$c);
205: $dp->pagination = false;
206: $fields = $dp->getData();
207: if(!empty($_fieldName)) {
208: if(count($fields) === 0 && $dp->pagination->pageSize !== 0)
209: $this->send(404,"Model $_class does not have a field named "
210: . "\"$_fieldName\"");
211: $this->responseBody = reset($fields);
212: } else
213: $this->responseBody = $fields;
214: }
215:
216: 217: 218: 219: 220: 221:
222: public function actionHooks($_id=null,$_class=null) {
223: $method = Yii::app()->request->getRequestType();
224: if($method=='DELETE') {
225: $hook = ApiHook::model()->findByPk($_id);
226: if(!$hook instanceof ApiHook)
227: $this->send(404,'"Hook not found." -Smee');
228: elseif(!$hook->userId != Yii::app()->getSuId())
229: $this->send(403,'You cannot delete other API users\' hooks in X2Engine.');
230: $hook->setScenario('delete.remote');
231: if($hook->delete()) {
232: $this->sendEmpty("Successfully unsubscribed from hook with "
233: . "return URL {$hook->target_url}.");
234: }
235: } else {
236: if($_id !== null)
237: $this->send(405,'Cannot manipulate preexisting hooks with POST.');
238: $hook = new ApiHook;
239: $hook->attributes = $this->getJpost();
240: $hook->userId = Yii::app()->getSuId();
241: if(!empty($_class))
242: $hook->modelName = get_class($this->staticModel);
243: if(!$hook->validate('event')) {
244: $this->send(429, "The maximum number of hooks ({$maximum}) has "
245: . "been reached for events of this type.");
246: }
247: if(!$hook->validate()) {
248: $this->response['errors'] = $hook->errors;
249: $this->send(422);
250: }
251: if($hook->save()) {
252: $this->response->httpHeader['Location'] =
253: $this->createAbsoluteUrl('/api2/hooks',array(
254: '_id' => $hook->id
255: ));
256: $this->responseBody = $hook;
257: $this->send(201);
258: } else {
259: $this->send(500,'Could not save hook due to unexpected '
260: . 'internal server error.');
261: }
262: }
263: }
264:
265: 266: 267: 268: 269: 270: 271:
272: public function actionModel($_class,$_id=null,$_findBy=null) {
273: $method = Yii::app()->request->getRequestType();
274:
275:
276: $this->kludgesForActions();
277:
278: switch($method) {
279: case 'GET':
280: if((!empty($_id) && ctype_digit((string) $_id)) ||
281: !empty($_findBy)) {
282:
283:
284: $this->responseBody = $this->model;
285: } else {
286:
287:
288: $this->responseBody = $this->getDataProvider()->getData();
289: }
290: break;
291: case 'PATCH':
292: case 'POST':
293: case 'PUT':
294:
295: if($method == 'POST') {
296: if(!(empty($_id) && empty($_findBy)))
297: $this->send(400,'POST should be used for creating new '
298: . 'records and cannot be used to update an '
299: . 'existing model. PUT or PATCH should be used '
300: . 'instead.');
301:
302:
303: $class = get_class($this->getStaticModel());
304: if (!isset ($this->_model)) $this->model = new $class;
305: }
306:
307:
308: $this->setModelAttributes();
309:
310:
311: $saved = false;
312: if($method == 'POST') {
313:
314: $saved = $this->model->save();
315: } else {
316:
317: $attributes = array_intersect(array_keys($this->jpost),
318: $this->staticModel->attributeNames());
319: if($this->model->validate($attributes)) {
320:
321: if ($this->model->asa('X2FlowTriggerBehavior') &&
322: $this->model->asa('X2FlowTriggerBehavior')->enabled) {
323: $this->model->enableUpdateTrigger();
324: }
325: $saved = $this->model->update($attributes);
326: if ($this->model->asa('X2FlowTriggerBehavior') &&
327: $this->model->asa('X2FlowTriggerBehavior')->enabled) {
328:
329: $this->model->disableUpdateTrigger();
330: }
331: }
332: }
333:
334:
335: if($this->model->hasErrors()) {
336: $this->response['errors'] = $this->model->errors;
337: $this->send(422,"Model failed validation.");
338: } else if(!$saved) {
339: $this->send(500,"Model passed validation but still could not "
340: . "be saved due to an unexpected internal server error.");
341: }
342:
343:
344: $this->responseBody = $this->model;
345:
346:
347:
348: if($method == 'POST') {
349: $this->response->httpHeader['Location'] = $this->createAbsoluteUrl('/api2/model', array(
350: '_class' => $class,
351: '_id' => $this->model->id
352: ));
353: $this->send(201,"Model of class \"$class\" created successfully.");
354: }
355: break;
356: case 'DELETE':
357: if($this->model->delete()) {
358: $this->sendEmpty("Model of class \"$_class\" with id=$_id "
359: . "deleted successfully.");
360: }
361: else
362: $this->send(500);
363: break;
364: }
365: }
366:
367: 368: 369: 370: 371: 372:
373: public function actionModels($partialSupport=1) {
374:
375: $modelNames = X2Model::getModelNames();
376:
377: $partial = array(
378: 'Actions'=>Yii::t('app','Actions'),
379: 'Docs'=>Yii::t('app','Docs'),
380: 'Groups'=>Yii::t('app','Groups'),
381: 'Media'=>Yii::t('app','Media'),
382: 'Quote'=>Yii::t('app','Quotes'),
383: 'X2List'=>Yii::t('app','Contact Lists')
384: );
385: if((boolean) (integer) $partialSupport) {
386: $modelNames = array_unique(array_merge($modelNames,$partial));
387: } else {
388: $modelNames = array_diff($modelNames,$partial);
389: }
390: asort($modelNames);
391:
392: $models = array();
393: foreach($modelNames as $modelName => $title) {
394: $attributes = X2Model::model($modelName)->attributeNames();
395: $models[] = compact('modelName','title','attributes');
396: }
397:
398: $this->responseBody = $models;
399: }
400:
401: 402: 403: 404: 405: 406: 407:
408: public function actionRelationships($_class=null,$_id=null,$_relatedId=null) {
409: $method = Yii::app()->request->requestType;
410:
411: $relationship = null;
412: if($_relatedId !== null) {
413: $relationship = Relationships::model()->findByPk($_relatedId);
414: if(!($relationship instanceof Relationships)) {
415: $this->send(404,"Relationship with id=$_relatedId not found.");
416: }
417:
418: if($_class !== null
419: && $_id !== null
420: && $relationship->firstId != $this->model->id
421: && $relationship->secondId != $this->model->id
422: && $relationship->firstType != $_class
423: && $relationship->secondType != $_class) {
424: $this->response->httpHeader['Location'] = $this->createAbsoluteUrl('/api2/relationships', array(
425: '_class' => $relationship->firstType,
426: '_id' => $relationship->firstId,
427: '_relatedId' => $relationship->id
428: ));
429: $this->send(303,"Specified relationship does not correspond "
430: . "to $_class record $_id.");
431: }
432: }
433:
434: switch($method) {
435: case 'GET':
436: if($relationship !== null) {
437:
438:
439: $which = $relationship->firstId == $_id
440: && $relationship->firstType == $_class
441: ? 'second' : 'first';
442: $relId = $which.'Id';
443: $relType = $which.'Type';
444: $this->response->httpHeader['Location'] = $this->createAbsoluteUrl('/api2/model',array(
445: '_class' => $relationship->$relType,
446: '_id' => $relationship->$relId
447: ));
448: $this->responseBody = $relationship;
449: } else {
450:
451: $criteria = null;
452: if(!($relationship instanceof Relationships)) {
453:
454: $from = new CDbCriteria;
455: $to = new CDbCriteria;
456: $from->compare('firstType',$_class);
457: $from->compare('firstId',$_id);
458: $to->compare('secondType',$_class);
459: $to->compare('secondId',$_id);
460: $criteria = new CDbCriteria;
461: $criteria->mergeWith($from,'OR');
462: $criteria->mergeWith($to,'OR');
463: }
464: $this->responseBody = $this
465: ->getDataProvider('Relationships',$criteria)
466: ->getData();
467: }
468: break;
469: case 'PATCH':
470: case 'POST':
471: case 'PUT':
472: if(!$relationship instanceof Relationships) {
473: if($method !== 'POST') {
474:
475: $this->send(405,"Method \"POST\" is required to create new relationships.");
476: }
477: $relationship = new Relationships;
478: }
479:
480:
481:
482: $relationship->setScenario('api');
483: $relationship->setAttributes($this->jpost);
484:
485: if(empty($relationship->firstType)) {
486: $relationship->firstType = $_class;
487: $relationship->firstId = $_id;
488: } elseif (empty($relationship->secondType)) {
489: $relationship->secondType = $_class;
490: $relationship->secondId = $_id;
491: }
492: if(!$relationship->save()) {
493:
494: $this->response['errors'] = $relationship->errors;
495: $this->send(422);
496: } else {
497: $this->responseBody = $relationship;
498: if($method === 'POST'){
499:
500: $this->response->httpHeader['Location'] = $this->createAbsoluteUrl('/api2/relationships', array(
501: '_class' => $_class,
502: '_id' => $_id,
503: '_relatedId' => $_relatedId
504: ));
505: $this->send(201,"Relationship created successfully");
506: }
507: }
508: break;
509: case 'DELETE':
510: if(!($relationship instanceof Relationships)) {
511: $this->send(400,"Cannot delete relationships without specifying which one to delete.");
512: }
513: if($relationship->delete()) {
514: $this->sendEmpty("Relationship $_relatedId deleted successfully.");
515: } else {
516: $this->send(500,"Failed to delete relationship #$_relatedId. It may have been deleted already.");
517: }
518: break;
519: }
520: }
521:
522: 523: 524: 525: 526: 527: 528: 529: 530: 531:
532: public function actionTags($_class=null,$_id=null,$_tagName=null) {
533: $method = Yii::app()->request->requestType;
534:
535:
536: $tag = null;
537: if($_class !== null && $_id !== null && $_tagName != null){
538:
539:
540: $tag = Tags::model()->findByAttributes(array(
541: 'type' => $_class,
542: 'itemId' => $this->model->id,
543: 'tag' => '#'.ltrim($_tagName, '#')
544: ));
545: if(!($tag instanceof Tags))
546: $this->send(404,"Tag \"$_tagName\" not found on $_class id=$_id.");
547: }
548:
549: switch($method){
550: case 'GET':
551: if(!($tag instanceof Tags)){
552:
553:
554: $criteria = new CDbCriteria();
555: if($_class !== null && !isset($_GET['type']))
556: $criteria->compare('type',$_class);
557: if($_id !== null && !isset($_GET['itemId']))
558: $criteria->compare('itemId',$_id);
559: if($_tagName !== null && !isset($_GET['tag']))
560: $criteria->compare('tag','#'.ltrim($_tagName, '#'));
561: $this->responseBody = $this
562: ->getDataProvider('Tags',$criteria)
563: ->getData();
564: $this->send(200);
565: }else{
566:
567: $this->responseBody = $tag;
568: }
569: break;
570: case 'POST':
571: if($tag instanceof Tags) {
572:
573: $this->send(405,"Tags cannot be individually modified.");
574: }
575:
576: $this->model->addTags($this->jpost);
577: $this->response['message'] = 'Tags added successfully.';
578: break;
579: case 'DELETE':
580: if(!($tag instanceof Tags)) {
581: $this->send(400,"Tag name must be specified when deleting a tag.");
582: }
583: if($this->model->removeTags('#'.ltrim($_tagName,'#'))) {
584: $this->sendEmpty("Tag #$_tagName deleted from $_class id=$_id.");
585: } else {
586: $this->send(500);
587: }
588: break;
589: }
590: }
591:
592: 593: 594:
595: public function actionTeapot(){
596: $this->send(418, "I'm a teapot.");
597: }
598:
599: 600: 601: 602:
603: public function actionUsers($_id=null) {
604: if($_id !== null) {
605: if((bool) ($user=User::model()->findByPk($_id))) {
606: $this->responseBody = $user;
607: } else {
608: $this->send(404,"User with specified ID $_id not found.");
609: }
610: } else {
611: $this->responseBody = $this->getDataProvider('User')->getData();
612: }
613: }
614:
615: 616: 617: 618: 619: 620:
621: public function actionZapierFields($_class,$_permissionLevel=1) {
622: $fieldModels = $this->staticModel->fields;
623: $fieldPermissions = $this->staticModel->fieldPermissions;
624: $fields = array();
625: $typeMapping = array(
626: 'assignment' => 'unicode',
627: 'boolean' => 'bool',
628: 'credentials' => 'int',
629: 'currency' => 'unicode',
630: 'date' => 'datetime',
631: 'dateTime' => 'datetime',
632: 'dropdown' => 'unicode',
633: 'email' => 'unicode',
634: 'integer' => 'int',
635: 'optionalAssignment' => 'unicode',
636: 'percentage' => 'float',
637: 'phone' => 'unicode',
638: 'rating' => 'int',
639: 'text' => 'text',
640: 'url' => 'unicode',
641: 'varchar' => 'unicode',
642: 'visibility' => 'int',
643: '' => 'unicode'
644: );
645: foreach($fieldModels as $field) {
646: if($fieldPermissions[$field->fieldName] < $_permissionLevel)
647: continue;
648: $fieldOut = array(
649: 'type' => isset($typeMapping[$field->type])
650: ? $typeMapping[$field->type]
651: : 'unicode',
652: 'key' => $field->fieldName,
653: 'required' => (boolean) (integer) $field->required,
654: 'label' => $this->staticModel->getAttributeLabel($field->fieldName),
655: );
656:
657:
658: $options = $this->fieldOptions($field);
659: if(!empty($options))
660: $fieldOut['choices'] = $options;
661:
662: $fields[] = $fieldOut;
663: }
664: $this->responseBody = $fields;
665: }
666:
667: 668: 669: 670: 671: 672: 673: 674: 675: 676: 677: 678:
679: public function afterAction($action){
680: if(isset($this->response->body) || count($this->response) > 0)
681: $this->send();
682: else
683: $this->sendEmpty();
684: }
685:
686: 687: 688: 689: 690:
691: public function attributesOf(CActiveRecord $model){
692: if($model instanceof X2Model){
693: $attributes = $model->getAttributes($model->getReadableAttributeNames());
694:
695:
696: if($model instanceof Actions && $model->fieldPermissions['actionDescription'] >=1) {
697: $attributes['actionDescription'] = $model->getActionDescription();
698: }
699: return $attributes;
700: }elseif($model instanceof User){
701: $excludeAttributes = array_fill_keys(array('password','userKey','googleRefreshToken'),'');
702: $attributes = array_diff_key(array_merge($model->attributes,
703: $model->profile->attributes),$excludeAttributes);
704: $uid = Yii::app()->getSuId();
705: if(!Yii::app()->authManager->checkAccess('UsersAdmin',$uid)
706: && $model->id != $uid) {
707:
708: $attributes = array_intersect_key($attributes,array_fill_keys(array(
709: 'id','firstName','lastName','emailAddress','username',
710: 'userAlias','fullName'
711: ),''));
712: }
713: return $attributes;
714: }else{
715: return $model->attributes;
716: }
717: }
718:
719: 720: 721: 722: 723:
724: public function authFail($message){
725:
726: $this->response->httpHeader['WWW-Authenticate'] =
727: 'Basic realm="X2Engine API v2"';
728:
729:
730:
731: $this->send(401, $message);
732: }
733:
734: 735: 736: 737: 738: 739: 740:
741: public function behaviors() {
742: set_exception_handler(array($this,'handleException'));
743: return array(
744: 'ResponseBehavior' => array(
745: 'class' => 'application.components.ResponseBehavior',
746: 'isConsole' => false,
747: 'exitNonFatal' => false,
748: 'longErrorTrace' => false,
749: 'handleErrors' => true,
750: 'handleExceptions' => false,
751: 'errorCode' => 500
752: )
753: );
754: }
755:
756: 757: 758: 759: 760: 761: 762: 763: 764: 765:
766: public function fieldOptions(Fields $field) {
767: switch($field->type){
768: case 'assignment':
769: return X2Model::getAssignmentOptions(true, true, false);
770: case 'credentials':
771: $typeArr = explode(':',$field->linkType);
772: $type = $typeArr[0];
773: if(count($typeAlias) > 1){
774: $uid = Credentials::$sysUseId[$typeAlias[1]];
775: }else{
776: $uid = Yii::app()->getSuId();
777: }
778: if(count($typeArr>0))
779: $uid = $typeArr[1];
780: $config = Credentials::getCredentialOptions($this->staticModel,
781: $field->fieldName, $type, $uid);
782: return $config['credentials'];
783: case 'dropdown':
784:
785: $dropdown = Dropdowns::model()->findByPk($field->linkType);
786: if($dropdown instanceof Dropdowns){
787: return json_decode($dropdown->options, 1);
788: }
789: break;
790: case 'optionalAssignment':
791: $options = X2Model::getAssignmentOptions(true, true, false);
792: unset($options['Anyone']);
793: $options[''] = '';
794: return $options;
795: case 'rating':
796: return range(Fields::RATING_MIN,Fields::RATING_MAX);
797: case 'varchar':
798:
799: if($field->modelName == 'Actions' && $field->fieldName == 'priority'){
800: return Actions::getPriorityLabels();
801: }
802: break;
803: case 'visibility':
804: $permissionsBehavior = Yii::app()->params->modelPermissions;
805: return $permissionsBehavior::getVisibilityOptions();
806: }
807: return array();
808: }
809:
810:
811:
812:
813:
814:
815:
816: 817: 818:
819: public function filterAuthenticate($filterChain) {
820:
821: if(!isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) && isset($_SERVER['HTTP_AUTHORIZATION'])){
822: list($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])
823: = explode(':' , base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6)));
824: }
825: foreach(array('user','pw') as $field) {
826: $srvKey = 'PHP_AUTH_'.strtoupper($field);
827: if(!isset($_SERVER[$srvKey]) || empty($_SERVER[$srvKey])) {
828: $this->authFail("Missing user credentials: $field");
829: return;
830: }
831: ${$field} = $_SERVER[$srvKey];
832: }
833: $userModel = User::model()->findByAlias($user);
834:
835: if(!($userModel instanceof User) || !PasswordUtil::slowEquals($userModel->userKey, $pw))
836: $this->authFail("Invalid user credentials.");
837: elseif(trim($userModel->userKey)==null)
838: $this->authFail("API access has been disabled for the specified user.");
839:
840:
841: Yii::app()->setSuModel($userModel);
842: $profile = $userModel->profile;
843: if($profile instanceof Profile)
844: Yii::app()->params->profile = $profile;
845:
846: $filterChain->run();
847: }
848:
849: 850: 851: 852: 853:
854: public function filterAvailable($filterChain) {
855: $this->response->httpHeader['Content-Type'] = 'application/json; '
856: . 'charset=utf-8';
857: if(is_int(Yii::app()->locked)){
858: $this->send(503,"X2Engine is currently locked. "
859: . "It may be undergoing maintenance. Please try again later.");
860: }
861: if(!$this->enabled) {
862: $this->send(503,"API access has been disabled on this system.");
863: }
864: $filterChain->run();
865: }
866:
867: 868: 869: 870: 871:
872: public function filterContentType($filterChain) {
873: if(isset($_SERVER['CONTENT_TYPE'])
874: && strpos($_SERVER['CONTENT_TYPE'],'application/json') !== 0) {
875: $this->send(415);
876: }
877: $filterChain->run();
878: }
879: 880: 881: 882: 883: 884:
885: public function filterMethods($filterChain) {
886: $id = $filterChain->action->id;
887: $methods = self::methods();
888: if(isset($methods[$id])){
889:
890: $acceptMethods = explode(',', $methods[$id]);
891: if(in_array($method = Yii::app()->request->getRequestType(), $acceptMethods)){
892:
893: $filterChain->run();
894: } else {
895:
896: $this->send(405,"Action \"$id\" does not support $method.");
897: }
898: } else {
899:
900: $filterChain->run();
901: }
902: }
903:
904: 905: 906: 907: 908: 909: 910:
911: public function filterRbac($filterChain) {
912: $action = null;
913: $data = array();
914: $method = Yii::app()->request->requestType;
915: $user = Yii::app()->getSuModel();
916: $username = $user->username;
917: $userId = $user->id;
918: $denial = "User $username does not have permission to perform action {action}";
919:
920:
921:
922:
923: if(isset($_GET['_class'])){
924: $linkable = $this->staticModel->asa('X2LinkableBehavior');
925: $module = !empty($linkable) ? ucfirst($linkable->module) : $_GET['_class'];
926:
927:
928:
929: if(isset($_GET['_id'])){
930: $data['X2Model'] = $this->model;
931: }
932: }
933:
934:
935:
936:
937:
938: switch($this->action->id) {
939: case 'count':
940: switch($method) {
941: case 'GET':
942: $action = "{$module}Index";
943: break;
944: }
945: break;
946: case 'model':
947: switch($method) {
948: case 'DELETE':
949: $action = "{$module}Delete";
950: break;
951: case 'GET':
952:
953: $action = isset($_GET['_id'])
954: ? "{$module}View"
955: : "{$module}Index";
956: break;
957: case 'PATCH':
958: case 'PUT':
959: $action = "{$module}Update";
960: }
961: break;
962: case 'relationships':
963: case 'tags':
964: switch($method) {
965: case 'DELETE':
966: case 'PATCH':
967: case 'PUT':
968: case 'POST':
969:
970:
971:
972:
973: $action = "{$module}View";
974: break;
975: case 'GET':
976: if(isset($_GET['_class']) && isset($_GET['_id'])) {
977:
978:
979:
980: $action = "{$module}View";
981: } else {
982:
983:
984:
985:
986:
987: $filterChain->run();
988: }
989: break;
990: }
991: break;
992: }
993:
994:
995: if(Yii::app()->authManager->getAuthItem($action) instanceof CAuthItem){
996: if(!Yii::app()->authManager->checkAccess($action, $userId, $data)){
997: $this->send(403, "You do not have permission to perform this action..");
998: }
999: }
1000: $filterChain->run();
1001: }
1002:
1003:
1004:
1005: public function filters() {
1006: return array(
1007: 'available',
1008:
1009: 'authenticate',
1010: 'methods',
1011: 'contentType',
1012: 'rbac + count,model,relationships,tags',
1013: );
1014: }
1015:
1016: 1017: 1018: 1019: 1020: 1021: 1022: 1023: 1024: 1025:
1026: public function findConditions($condition,$validAttributes = array()) {
1027: $conditions = explode(self::FIND_DELIM,$condition);
1028:
1029: $attributeConditions = array();
1030: foreach($conditions as $condition) {
1031: $attrVal = explode(self::FIND_EQUAL,$condition);
1032: if(count($attrVal) < 2) {
1033: continue;
1034: }
1035: $attribute = array_shift($attrVal);
1036:
1037: $attributeConditions[$attribute] = implode(
1038: self::FIND_EQUAL,$attrVal);
1039: }
1040:
1041: if(!empty($validAttributes)) {
1042:
1043: $attributeConditions = array_intersect_key(
1044: $attributeConditions,
1045: $validAttributes
1046: );
1047: }
1048: return $attributeConditions;
1049: }
1050:
1051:
1052:
1053:
1054:
1055:
1056:
1057: 1058: 1059: 1060: 1061: 1062: 1063: 1064: 1065:
1066: public function getDataProvider($modelClass=null,$extraCriteria = null,$combineOp = 'AND') {
1067:
1068: $class = $modelClass == null && isset($_GET['_class'])
1069: ? get_class($this->staticModel)
1070: : $modelClass;
1071: if(empty($class) || !class_exists($class))
1072: $this->send(500, 'Method getDataProvider called without specifying '
1073: . 'a valid model class, in action "'.$this->action->id.'".');
1074:
1075: $staticModel = CActiveRecord::model($class);
1076: $model = new $class('search');
1077:
1078:
1079: $searchAttributes = array_intersect_key($_GET, $staticModel->attributes);
1080:
1081:
1082: $optionParams = array_fill_keys($this->reservedParams['search'],0);
1083: $searchOptions = array_intersect_key($_GET, $optionParams);
1084:
1085:
1086: $criteria = new CDbCriteria;
1087: $criteria->alias = 't';
1088:
1089: if($model instanceof X2Model){
1090:
1091:
1092: if($model->asa('permissions') && $model->asa('permissions')->enabled){
1093:
1094:
1095: $criteria->mergeWith($model->getAccessCriteria());
1096: }
1097: if(isset($searchOptions['_tags'])) {
1098:
1099: $criteria->distinct = true;
1100:
1101: $tags = array_map(function($t){return '#'.$t;},explode(',',$_GET['_tags']));
1102: $tagTable = Tags::model()->tableName();
1103: if(empty($searchOptions['_tagOr']) || !(bool)(int)$searchOptions['_tagOr']){
1104:
1105: $i_tag = 0;
1106: $joins = array();
1107: foreach($tags as $tag){
1108: $tagParam = ":apiSearchTag$i_tag";
1109: $classParam = ":apiTagItemClass$i_tag";
1110: $joinAlias = "tag$i_tag";
1111: $joins[] = "INNER JOIN `$tagTable` `$joinAlias` "
1112: ."ON `$joinAlias`.`type`= $classParam "
1113: ."AND `$joinAlias`.`itemId`=`{$criteria->alias}`.`id` "
1114: ."AND `$joinAlias`.`tag`=$tagParam";
1115: $criteria->params[$tagParam] = $tag;
1116: $criteria->params[$classParam] = get_class($model);
1117: $i_tag++;
1118: }
1119: $criteria->join .= implode(' ', $joins);
1120: } else {
1121:
1122:
1123: $tagParam = AuxLib::bindArray($tags,'apiSearchTag');
1124: $tagIn = AuxLib::arrToStrList(array_keys($tagParam));
1125: $criteria->join .= "INNER JOIN `$tagTable` `tag`"
1126: . "ON `tag`.`type`=:apiTagItemClass "
1127: . "AND `tag`.`itemId`=`{$criteria->alias}`.`id` "
1128: . "AND `tag`.`tag` IN $tagIn";
1129: $tagParam[":apiTagItemClass"] = get_class($model);
1130: foreach($tagParam as $param=>$value) {
1131: $criteria->params[$param] = $value;
1132: }
1133: }
1134: }
1135:
1136:
1137:
1138:
1139: $now = time();
1140: $yesterday = $now - 86400;
1141: $tomorrow = $now + 86400;
1142: $codes = array(
1143: 'date' => compact('now','yesterday','tomorrow'),
1144: 'dateTime' => compact('now','yesterday','tomorrow'),
1145: );
1146: $fields = $model->getFields();
1147: foreach($fields as $field) {
1148: if(isset($searchAttributes[$field->fieldName])) {
1149: if(isset($codes[$field->type])) {
1150: foreach($codes[$field->type] as $name => $value){
1151: $searchAttributes[$field->fieldName] =
1152: preg_replace('/'.$name.'$/',$value,$searchAttributes[$field->fieldName]);
1153: }
1154: }
1155: }
1156: }
1157: }
1158:
1159:
1160:
1161:
1162: $partialMatch = isset($searchOptions['_partial'])
1163: ? (boolean) (integer) $searchOptions['_partial']
1164: : false;
1165:
1166: $operator = isset($searchOptions['_or']) && (boolean) (integer) $searchOptions['_or']
1167: ? 'OR'
1168: : 'AND';
1169:
1170: $escape = isset($searchOptions['_escape'])
1171: ? (boolean) (integer) $searchOptions['_escape']
1172: : true;
1173:
1174:
1175: if($class === 'Actions'){
1176: $this->kludgesForSearchingActions($searchAttributes,$criteria);
1177: }
1178:
1179:
1180: $searchCriteria = new CDbCriteria;
1181: foreach($searchAttributes as $column => $value){
1182: $searchCriteria->compare($column,$value,$partialMatch,$operator,$escape);
1183: }
1184: $criteria->mergeWith ($searchCriteria);
1185:
1186:
1187: if($extraCriteria instanceof CDbCriteria) {
1188: $criteria->mergeWith($extraCriteria,$combineOp);
1189: }
1190:
1191:
1192: if(isset($searchOptions['_order'])) {
1193: $orderBy = $searchOptions['_order'];
1194: if(preg_match('/^(?P<asc>[\+\-\s])?(?P<col>[^\+\-\s]+)$/',$orderBy,$match)) {
1195: $col = $match['col'];
1196: if(!in_array($col,$staticModel->attributeNames())) {
1197: $this->send(400,"Specified attribute to order results by ($col) "
1198: . "does not exist in active record class \"$class\".");
1199: }
1200: $ascMap = array(
1201: '+' => 'ASC',
1202: ' ' => 'ASC',
1203: '-' => 'DESC'
1204: );
1205: $criteria->order = $col
1206: .(empty($match['asc']) ? '' : ' '.$ascMap[$match['asc']]);
1207: }
1208: }
1209:
1210:
1211: $pageSize = null;
1212: $pageInd = 0;
1213: if(isset($searchOptions['_limit']) && ctype_digit((string)$searchOptions['_limit']))
1214: $pageSize = (integer) $searchOptions['_limit'];
1215: if(isset($searchOptions['_page']) && ctype_digit((string)$searchOptions['_page']))
1216: $pageInd = (integer) $searchOptions['_page'];
1217: $pagination = array(
1218: 'currentPage' => $pageInd,
1219: 'pageSize' => $pageSize !== null ? min($pageSize, $this->maxPageSize) : $this->maxPageSize
1220: );
1221:
1222:
1223: return new CActiveDataProvider($class, compact('model', 'criteria', 'pagination'));
1224: }
1225:
1226: 1227: 1228:
1229: public function getEnabled() {
1230: return self::ENABLED ;
1231: }
1232:
1233:
1234: 1235: 1236: 1237: 1238:
1239: public function getJpost() {
1240: if(!isset($this->_jpost)) {
1241: $this->_jpost = json_decode(file_get_contents('php://input'),1);
1242: if(!is_array($this->_jpost))
1243: $this->send(400,"Missing or malformed data sent to server.");
1244: }
1245: return $this->_jpost;
1246: }
1247:
1248: 1249: 1250:
1251: public function getMaxPageSize() {
1252:
1253: return self::MAX_PAGE_SIZE;
1254: }
1255:
1256: 1257: 1258: 1259: 1260: 1261: 1262:
1263: public function getModel() {
1264: if(!isset($this->_model)) {
1265: if(!(isset($_GET['_id']) || isset($_GET['_findBy']))){
1266: $method = Yii::app()->request->requestType;
1267: $this->send(400, "Cannot use method $method in action "
1268: ."\"{$this->action->id}\" without specifying a valid "
1269: ."record ID or finding condition.");
1270: }
1271: if(isset($_GET['_id'])) {
1272: $this->_model = $this->getStaticModel()->findByPk($_GET['_id']);
1273: } else {
1274:
1275:
1276:
1277: $staticModel = $this->getStaticModel();
1278: $attributeConditions = $this->findConditions(
1279: $_GET['_findBy'],
1280: $staticModel->attributes
1281: );
1282:
1283:
1284: if(count($attributeConditions) == 0) {
1285: $this->send(400,"Invalid/improperly formatted attribute".
1286: " conditions: \"{$_GET['_findBy']}\"");
1287: }
1288:
1289:
1290: $models = $staticModel->findAllByAttributes($attributeConditions);
1291: $count = count($models);
1292: switch($count) {
1293: case 0:
1294: $this->send(404,"No matching record of class ".
1295: "{$_GET['_class']} found");
1296: default:
1297: $this->_model = reset($models);
1298:
1299:
1300:
1301:
1302: if($count > 1 && empty($_GET['_useFirst'])) {
1303: $queryUri = $this->createUrl('/api2/model',array_merge(
1304: array('_class' => $_GET['_class']),
1305: $attributeConditions
1306: ));
1307: $directUris = array();
1308: foreach($models as $model) {
1309: $directUris[] = $this->createUrl(
1310: '/api2/model',
1311: array(
1312: '_class' => $_GET['_class'],
1313: '_id' => $model->id
1314: )
1315: );
1316: }
1317: $this->response->httpHeader['Location'] = $queryUri;
1318: $this->response['queryUri'] = $queryUri;
1319: $this->response['directUris'] = $directUris;
1320: $this->send(300,"Multiple records match.");
1321: }
1322: }
1323: }
1324: if(!(($this->_model) instanceof X2Model))
1325: $this->send(404, "Record {$_GET['_id']} of class \""
1326: .get_class($this->getStaticModel())."\" not found.");
1327: }
1328: return $this->_model;
1329: }
1330:
1331: 1332: 1333:
1334: public function getReservedParams() {
1335: return array(
1336:
1337: 'default' => array(
1338: '_class',
1339: '_id',
1340: '_tagName',
1341: '_relatedId',
1342: ),
1343:
1344: 'search' => array(
1345: '_escape',
1346: '_limit',
1347: '_or',
1348: '_order',
1349: '_page',
1350: '_partial',
1351: '_tagOr',
1352: '_tags',
1353: ),
1354: );
1355: }
1356:
1357:
1358:
1359:
1360:
1361:
1362:
1363: 1364: 1365: 1366: 1367: 1368: 1369:
1370: public function getStaticModel() {
1371: if(!isset($this->_staticModel)) {
1372: if(!isset($_GET['_class']))
1373: $this->send(400,'Required parameter "class" missing.');
1374: $this->_staticModel = X2Model::model($_GET['_class']);
1375: if(!($this->_staticModel instanceof X2Model))
1376: $this->send(400,"Invalid model class \"{$_GET['_class']}\".");
1377: }
1378: return $this->_staticModel;
1379: }
1380:
1381: 1382: 1383: 1384: 1385: 1386: 1387: 1388:
1389: public function handleException($e) {
1390: if($e instanceof CHttpException) {
1391: $this->send((integer) $e->statusCode, $e->statusCode == 404
1392: ? "Invalid URI: ".Yii::app()->request->requestUri
1393: : $e->getMessage());
1394: } else {
1395: $this->log("Uncaught exception [".$e->getCode()."]: ".$e->getMessage());
1396: ResponseUtil::respondWithException($e);
1397: }
1398: }
1399:
1400: 1401: 1402:
1403: public function kludgesForActions(){
1404: $method = Yii::app()->request->requestType;
1405: if($_GET['_class'] == 'Actions'){
1406:
1407: if(isset($_GET['associationType'], $_GET['associationId'])){
1408:
1409: if(!($staticAssocModel = X2Model::model($_GET['associationType']))){
1410: $this->send(400, 'Invalid association type.');
1411: }
1412:
1413: $associatedModel = $staticAssocModel->findByPk($_GET['associationId']);
1414: if(!(bool) $associatedModel) {
1415: $this->send(404, 'Associated record not found.');
1416: }
1417:
1418:
1419: if(isset($_GET['_id'])
1420: && $this->model->associationId != $_GET['associationId']) {
1421:
1422:
1423:
1424: $params = array(
1425: '_id' => $_GET['_id'],
1426: '_class' => 'Actions'
1427: );
1428: if($this->model->associationType != '') {
1429: $params['associationType'] = get_class(X2Model::model($this->model->associationType));
1430: $params['associationId'] = $this->model->associationId;
1431: }
1432: $this->response->httpHeader['Location'] = $this->createAbsoluteUrl('/api2/model',$params);
1433: $this->send(303,'Action has a different association than '
1434: . 'the one specified.');
1435: }
1436: }
1437:
1438:
1439: if($method == 'POST'){
1440: $this->model = new Actions;
1441: if(isset($_GET['associationId'], $_GET['associationType'])){
1442: $this->model->associationId = $_GET['associationId'];
1443: $this->model->associationType = X2Model::model($_GET['associationType'])->module;
1444: }
1445: }
1446: }
1447: }
1448:
1449: 1450: 1451: 1452: 1453: 1454: 1455: 1456: 1457: 1458:
1459: public function kludgesForSearchingActions(&$searchAttributes,$criteria){
1460:
1461:
1462:
1463:
1464:
1465:
1466:
1467:
1468:
1469:
1470:
1471:
1472: if(isset($_GET['actionDescription'])){
1473: $atTable = ActionText::model()->tableName();
1474: $atAlias = 'at';
1475: $criteria->join .= " INNER JOIN `$atTable` `$atAlias` "
1476: ."ON `$atAlias`.`actionId` = `{$criteria->alias}`.`id` "
1477: ."AND `$at`.`text` LIKE :apiSearchActionDescription";
1478: $criteria->params[':apiSearchActionDescription'] = $_GET['actionDescription'];
1479: }
1480:
1481:
1482:
1483:
1484:
1485:
1486:
1487:
1488:
1489:
1490:
1491:
1492:
1493: if(isset($searchAttributes['associationType'])){
1494: $associationClass = isset(X2Model::$associationModels[$searchAttributes['associationType']])
1495: ? X2Model::$associationModels[$searchAttributes['associationType']]
1496: : $searchAttributes['associationType'];
1497: $staticSearchModel = X2Model::model($associationClass);
1498: $searchAttributes['associationType'] = $staticSearchModel->asa('X2LinkableBehavior') === null
1499: ? lcfirst(get_class($staticSearchModel))
1500: : $staticSearchModel->asa('X2LinkableBehavior')->module;
1501: }
1502: }
1503:
1504: 1505: 1506: 1507: 1508: 1509: 1510:
1511: public function log($message, $level = 'info', $category = 'application.api'){
1512: $ip = Yii::app()->request->userHostAddress;
1513: $user = Yii::app()->getSuName();
1514: Yii::log("[client $ip, user $user, action {$this->action->id}]: ".$message, $level, $category);
1515: }
1516:
1517: 1518: 1519: 1520: 1521: 1522: 1523: 1524: 1525:
1526: public static function methods() {
1527: return array(
1528: 'appInfo' => 'GET',
1529: 'count' => 'GET',
1530: 'dropdowns' => 'GET',
1531: 'fieldPermissions' => 'GET',
1532: 'fields' => 'GET',
1533: 'hooks' => 'POST,DELETE',
1534: 'model' => 'DELETE,GET,PATCH,POST,PUT',
1535: 'models' => 'GET',
1536: 'relationships' => 'DELETE,GET,PATCH,POST,PUT',
1537: 'tags' => 'GET,POST,DELETE',
1538: 'users' => 'GET',
1539: 'zapierFields' => 'GET'
1540: );
1541: }
1542:
1543: 1544: 1545: 1546: 1547: 1548:
1549: public function send($status=200,$message = '') {
1550: $statMessage = ResponseUtil::statusMessage($status);
1551: if(!isset($this->response->body)) {
1552:
1553:
1554: $this->response['httpHeaders'] = $this->response->httpHeader;
1555: if(function_exists('getallheaders')) {
1556: $this->response['reqHeaders'] = getallheaders();
1557: }
1558: }
1559: $this->log("sent [$status $statMessage]".(empty($message)?'':": $message"));
1560: $this->response->sendHttp($status,$message);
1561: }
1562:
1563: 1564: 1565: 1566: 1567:
1568: public function sendEmpty($message = ''){
1569: $this->responseBody = '';
1570: $this->send(204, $message);
1571: }
1572:
1573: 1574: 1575: 1576:
1577: public function setModel(X2Model $model) {
1578: $this->_model = $model;
1579: }
1580:
1581: 1582: 1583: 1584: 1585:
1586: public function setModelAttributes($fields = array()) {
1587: if(empty($fields)) {
1588: $fields = $this->jpost;
1589: }
1590:
1591:
1592:
1593:
1594:
1595: $specialActionFields = array_fill_keys(array(
1596: 'type',
1597: 'complete'
1598: ), 0);
1599: if($this->model instanceof Actions && count(array_intersect_key($fields,$specialActionFields)>0)) {
1600: foreach($specialActionFields as $attribute => $placeholder){
1601: $this->model->$attribute = $fields[$attribute];
1602: unset($fields[$attribute]);
1603: }
1604: }
1605:
1606: $this->model->setX2Fields($fields);
1607:
1608: if(get_class ($this->model) === 'Contacts' && isset($fields['trackingKey'])){
1609:
1610: $this->model->trackingKey = $fields['trackingKey'];
1611: }
1612: }
1613:
1614: 1615: 1616: 1617: 1618:
1619: public function setResponseBody($object) {
1620: switch(gettype($object)) {
1621: case 'string':
1622: $this->response->body = $object;
1623: break;
1624: case 'array':
1625:
1626: $firstElement = reset($object);
1627: if($firstElement instanceof CActiveRecord) {
1628: $records = array();
1629: if($firstElement instanceof Tags){
1630:
1631: $records = array_map(function($t){return $t->tag;},$object);
1632: }else{
1633:
1634: $records = array_map(array($this, 'attributesOf'), $object);
1635: }
1636: $this->response->body = json_encode($records);
1637:
1638: }else{
1639: $this->response->body = json_encode($object);
1640: }
1641: break;
1642: case 'object':
1643: if($object instanceof CActiveRecord) {
1644: $this->response->body = json_encode($this->attributesOf($object));
1645: }
1646: break;
1647: default:
1648: $this->response->body = json_encode($object);
1649: }
1650: }
1651:
1652: }
1653:
1654: ?>
1655: