Overview

Packages

  • application
    • commands
    • components
      • actions
      • filters
      • leftWidget
      • permissions
      • sortableWidget
      • util
      • webupdater
      • x2flow
        • actions
        • triggers
      • X2GridView
      • X2Settings
    • controllers
    • models
      • embedded
    • modules
      • accounts
        • controllers
        • models
      • actions
        • controllers
        • models
      • calendar
        • controllers
        • models
      • charts
        • models
      • contacts
        • controllers
        • models
      • docs
        • components
        • controllers
        • models
      • groups
        • controllers
        • models
      • marketing
        • components
        • controllers
        • models
      • media
        • controllers
        • models
      • mobile
        • components
      • opportunities
        • controllers
        • models
      • products
        • controllers
        • models
      • quotes
        • controllers
        • models
      • services
        • controllers
        • models
      • template
        • models
      • users
        • controllers
        • models
      • workflow
        • controllers
        • models
      • x2Leads
        • controllers
        • models
  • None
  • system
    • base
    • caching
    • console
    • db
      • ar
      • schema
    • validators
    • web
      • actions
      • auth
      • helpers
      • widgets
        • captcha
        • pagers
  • zii
    • widgets
      • grid

Classes

  • AdminController
  • Api2Controller
  • ApiController
  • BugReportsController
  • CommonSiteControllerBehavior
  • ProfileController
  • RelationshipsController
  • SearchController
  • SiteController
  • StudioController
  • TemplatesController
  • TopicsController
  • x2base
  • X2Controller
  • Overview
  • Package
  • Class
  • Tree
  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 contact@x2engine.com.
 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: Yii::import('application.modules.users.models.*');
 38: 
 39: /**
 40:  * Remote data insertion & lookup API
 41:  * @package application.controllers
 42:  * @author Jake Houser <jake@x2engine.com>, Demitri Morgan <demitri@x2engine.com>
 43:  */
 44: class ApiController extends x2base {
 45: 
 46:     /**
 47:      * @var string The model that the API is currently being used with.
 48:      */
 49:     public $modelClass;
 50:     public $user;
 51: 
 52:     private $_model;
 53: 
 54:     /**
 55:      * Auth items to be checked against in {@link filterCheckPermissions} where
 56:      * their action isn't the same as the prefix.
 57:      */
 58:     public $actionAuthItemMap = array(
 59:         'lookUp' => 'View',
 60:     );
 61: 
 62:     public function behaviors() {
 63:         return array_merge(parent::behaviors(),array(
 64:             'LeadRoutingBehavior' => array(
 65:                 'class' => 'LeadRoutingBehavior'
 66:             ),
 67:             'responds' => array(
 68:                 'class' => 'application.components.ResponseBehavior',
 69:                 'isConsole' => false,
 70:                 'exitNonFatal' => false,
 71:                 'longErrorTrace' => false,
 72:             ),
 73:             'CommonControllerBehavior' => array(
 74:                 'class' => 'application.components.CommonControllerBehavior',
 75:                 'redirectOnNullModel' => false,
 76:                 'throwOnNullModel' => false
 77:             ),
 78:         ));
 79:     }
 80: 
 81:     /**
 82:      * @return array action filters
 83:      */
 84:     public function filters() {
 85:         return array(
 86:             'noSession',
 87:             'available',
 88:             'authenticate - voip,webListener,targetedContent,x2cron',
 89:             'validModel + create,view,lookup,update,delete,relationships,tags',
 90:             'checkCRUDPermissions + create,view,lookup,update,delete',
 91:         );
 92:     }
 93: 
 94:     public function actions() {
 95:         $actions = array();
 96:         if(class_exists('WebListenerAction'))
 97:             $actions['webListener'] = array('class' => 'WebListenerAction');
 98:         if(class_exists('TargetedContentAction'))
 99:             $actions['targetedContent'] = array('class' => 'TargetedContentAction');
100:         if(class_exists('X2CronAction'))
101:             $actions['x2cron'] = array('class' => 'X2CronAction');
102:         if(class_exists('EmailImportAction'))
103:             $actions['dropbox'] = array('class'=>'EmailImportAction');
104:         return $actions;
105:     }
106: 
107:     /**
108:      * Multi-purpose method for checking permissions. If called as an action,
109:      * it will return "true" or "false" in plain text (to stay backwards-
110:      * compatibile with old API scripts). Otherwise, will return true or false.  *
111:      * @param type $action
112:      * @param type $username
113:      * @param type $api
114:      * @return type
115:      */
116:     public function actionCheckPermissions($action, $username = null, $api = 0) {
117:         $access = true; // Default: permissive if no auth item exists
118:         $this->log("Checking user permissions for API transaction.");
119:         $auth = Yii::app()->authManager;
120:         $item = $auth->getAuthItem($action);
121:         $authenticated = $auth->getAuthItem('DefaultRole');
122:         if (isset($item)) {
123:             $access = false; // Auth item exists; set true only through verification
124:             $userId = null;
125:             $access = Yii::app()->params->isAdmin;
126:             $access = $authenticated->checkAccess($action);
127: 
128:             if (!$access) { // Skip this if we already have access
129:                 if ($username != null) { // Override current API user if any
130:                     $userId = User::model()->findByAttributes(array('username' => $username))->id;
131:                 } elseif (isset($this->user)) { // Called from within another API action that required credentials
132:                     $userId = $this->user->id;
133:                 }
134:             }
135: 
136:             if ($userId != null && !$access) { // Skip this if we already have access
137:                 $access = $access || $auth->checkAccess($action,$userId);
138:             }
139:         } elseif($this->action->id != 'checkPermissions')
140:             $this->log(sprintf("Auth item %s not found. Permitting action %s.",$action,$this->action->id));
141: 
142:         if ($api == 1) { // API model:
143:             // The method is being called as an action, most likely from APIModel
144:             $access = $access ? "true" : "false";
145:             header('Content-type: text/plain');
146:             echo $access;
147:             Yii::app()->end();
148:         } else {
149:             // This method is not being called as an action; rather, from a
150:             // filter or some other method.
151:             return $access;
152:         }
153:     }
154: 
155:     /**
156:      * Creates a new record.
157:      *
158:      * This method allows for the creation of new records via API request.
159:      * Requests should be made of the following format:
160:      * www.[server].com/index.php/path/to/x2/index.php/api/create/model/[modelType]
161:      * With the model's attributes as $_POST data.  Furthermore, in the post array
162:      * a valid username and API key must be submitted under the indices
163:      * 'user' and 'userKey' for the request to be authenticated.
164:      */
165:     public function actionCreate(){
166:         // Get an instance of the respective model
167:         $model = $this->getModel(true);
168:         $model->setX2Fields($_POST);
169: 
170:         if($this->modelClass === 'Contacts') {
171:             if (isset($_POST['trackingKey'])){
172:                 // key is read-only, won't be set by setX2Fields
173:                 $model->trackingKey = $_POST['trackingKey']; 
174:             }
175:             if (isset($_POST['_leadRouting']) && $_POST['_leadRouting']) {
176:                 $model->assignedTo = $this->getNextAssignee ();
177:             }
178:         }
179: 
180:         $setUserFields = false;
181:         // $scenario = 'Changelog behavior in effect.';
182:         if(!empty($_POST['createDate'])){ // If create date is being manually set, i.e. an import, don't overwrite
183:             $model->disableBehavior('changelog');
184:             $setUserFields = true;
185:             // $scenario = 'Changelog behavior disabled; create date not empty.';
186:         }
187:         try{
188:             $editingUsername = $model->editingUsername;
189:             // $scenario .= ' Model or one of its behaviors has a property "editingUsername".';
190:         }catch(Exception $e){
191:             $setUserFields = true;
192:             // $scenario .= ' Model nor its behaviors have a property "editingUsername".';
193:         }
194:         // $this->response['scenario'] = $scenario;
195:         if($setUserFields)
196:             $this->modelSetUsernameFields($model);
197:         // Attempt to save the model, and perform special post-save (or error)
198:         // operations based on the model type:
199:         if($model->save()){ // New record successfully created
200:             $this->response['model'] = $model->attributes;
201:             $message = "A {$this->modelClass} type record was created"; //sprintf(' <b>%s</b> was created',$this->modelClass);
202:             switch($this->modelClass){
203:                 // Special extra actions to take for each model type:
204:                 case 'Actions':
205:                     // Set actionDescription manually since it's stored in a different table
206:                     // which is updated using the magic getter:
207:                     if(isset($_POST['actionDescription'])){
208:                         $model->actionDescription = $_POST['actionDescription'];
209:                     }
210:                     $message .= " with description {$model->actionDescription}";
211:                     $model->syncGoogleCalendar('create');
212:                     break;
213:                 case 'Contacts':
214:                     $message .= " with name {$model->name}";
215:                     break;
216:             }
217:             $this->_sendResponse(200, $message);
218:         }else{ // API model creation failure
219:             $this->response['modelErrors'] = $model->errors;
220:             switch($this->modelClass){
221:                 case 'Contacts':
222:                     $this->log(sprintf('Failed to save record of type %s due to errors: %s', $this->modelClass, CJSON::encode($model->errors)));
223:                     $msg = $this->validationMsg('create', $model);
224:                     // Special lead failure notification in the app and through email:
225: 
226:                     $notif = new Notification;
227:                     $notif->user = 'admin';
228:                     $notif->type = 'lead_failure';
229:                     $notif->createdBy = $this->user->username;
230:                     $notif->createDate = time();
231:                     $notif->save();
232: 
233:                     $to = Yii::app()->settings->webLeadEmail;
234:                     $subject = "Web Lead Failure";
235:                     if(!Yii::app()->params->automatedTesting){
236:                         // Send notification of failure
237:                         $responderId = Credentials::model()->getDefaultUserAccount(Credentials::$sysUseId['systemNotificationEmail'], 'email');
238:                         if($responderId != Credentials::LEGACY_ID){ // Using configured 3rd-party email account
239:                             $this->sendUserEmail(array('to' => array(array($to, 'X2Engine Administrator'))), $subject, $msg, null, $responderId);
240:                         }else{ // Using plain old PHP mail
241:                             $phpMail = $this->getPhpMailer();
242:                             $fromEmail = Yii::app()->settings->emailFromAddr;
243:                             $fromName = Yii::app()->settings->emailFromName;
244:                             $phpMail->AddReplyTo($fromEmail, $fromName);
245:                             $phpMail->SetFrom($fromEmail, $fromName);
246:                             $phpMail->Subject = $subject;
247:                             $phpMail->AddAddress($to, 'X2Engine Administrator');
248:                             $phpMail->MsgHTML($msg."<br />JSON Encoded Attributes:<br /><br />".json_encode($model->attributes));
249:                             $phpMail->Send();
250:                         }
251:                     }
252: 
253:                     $attributes = $model->attributes;
254:                     ksort($attributes);
255:                     
256:                     if(file_exists($flCsv = implode(DIRECTORY_SEPARATOR, array(Yii::app()->basePath, 'data', 'failed_leads.csv')))){
257:                         $fp = fopen($flCsv,'a+');
258:                         fputcsv($fp, $attributes);
259:                     }else{
260:                         $fp = fopen($flCsv,'w+');
261:                         fputcsv($fp, array_keys($attributes));
262:                         fputcsv($fp, $attributes);
263:                     }
264:                     $this->_sendResponse(500, $msg);
265:                     break;
266:                 default:
267:                     $this->log(sprintf('Failed to save record of type %s due to errors: %s', $this->modelClass, CJSON::encode($model->errors)));
268:                     // Errors occurred
269:                     $msg = "<h1>Error</h1>";
270:                     $msg .= sprintf("Couldn't create model <b>%s</b> due to errors:", $this->modelClass);
271:                     $msg .= "<ul>";
272:                     foreach($model->errors as $attribute => $attr_errors){
273:                         $msg .= "<li>Attribute: $attribute</li>";
274:                         $msg .= "<ul>";
275:                         foreach($attr_errors as $attr_error)
276:                             $msg .= "<li>$attr_error</li>";
277:                         $msg .= "</ul>";
278:                     }
279:                     $msg .= "</ul>";
280:                     $this->_sendResponse(500, $msg);
281:             }
282:         }
283:     }
284: 
285:     /**
286:      * Delete a model record by primary key value.
287:      */
288:     public function actionDelete() {
289:         $model = $this->model;
290:         if ($this->modelClass === 'Actions')
291:                 $model->syncGoogleCalendar('delete');
292:         // Delete the model
293:         $num = $model->delete();
294:         if ($num > 0) {
295:             $this->_sendResponse(200, 1);
296:         } else
297:             $this->_sendResponse(500, sprintf("Error: Couldn't delete model <b>%s</b> with ID <b>%s</b>.", $_GET['model'], $_POST['id']));
298:     }
299: 
300:     /**
301:      * Gets a list of contacts.
302:      */
303:     public function actionList() {
304:         $listId = $_POST['id'];
305:         $list = X2List::model()->findByPk($listId);
306:         if (isset($list)) {
307:             //$list=X2List::load($listId);
308:         } else {
309:             $list = X2List::model()->findByAttributes(array('name' => $listId));
310:             if (isset($list)) {
311:                 $listId = $list->id;
312:                 //$list=X2List::load($listId);
313:             } else {
314:                 $this->_sendResponse(404, 'No list found with id: ' . $_POST['id']);
315:             }
316:         }
317:         $model = new Contacts('search');
318:         $dataProvider = $model->searchList($listId, 10);
319:         $data = $dataProvider->getData();
320:         $this->_sendResponse(200, json_encode($data),true);
321:     }
322: 
323:     /**
324:      * Get a list of all users in the app.
325:      */
326:     public function actionListUsers() {
327:         $access = $this->actionCheckPermissions('UsersAccess');
328:         $fullAccess = false;
329:         if($access)
330:             $fullAccess = $this->actionCheckPermissions('UsersFullAccess');
331:         if(!$access)
332:             $this->sendResponse(403,"User {$this->user} does not have permission to run UsersIndex");
333:         $users = User::model()->findAll();
334:         $userAttr = User::model()->attributes;
335:         if(!$fullAccess) {
336:             unset($userAttr['password']);
337:             unset($userAttr['userKey']);
338:         }
339:         $userAttr = array_keys($userAttr);
340:         $userList = array();
341:         foreach($users as $user) {
342:             $userList[] = $user->getAttributes($userAttr);
343:         }
344:         $this->_sendResponse(200,$userList,true);
345:     }
346: 
347:     /**
348:      * Obtain a model using search parameters.
349:      *
350:      * Finds a record based on its first name, last name, and/or email and responds with its full
351:      * attributes as a JSON-encoded string.
352:      *
353:      * URLs to use this function:
354:      * index.php/api/lookup/[model name]/[attribute]/[value]/...
355:      *
356:      * 'user' and 'userKey' are required.
357:      */
358:     public function actionLookup() {
359:         $attrs = $_POST;
360:         unset($attrs['user']);
361:         unset($attrs['userKey']);
362:         $tempModel = new $this->modelClass('search');
363: 
364:         // Use only attributes that the model has
365:         $attrs = array_intersect_key($attrs,$tempModel->attributes);
366:         $model = X2Model::model($this->modelClass)->findByAttributes($attrs);
367: 
368:         // Did we find the requested model? If not, raise an error
369:         if (is_null($model)) {
370:             $this->_sendResponse(404, 'No Item found with specified attributes.');
371:         } else {
372:             $this->_sendResponse(200, $model->attributes,true);
373:         }
374:     }
375: 
376:     /**
377:      * REST-ful API method for adding and removing relationships between records.
378:      */
379:     public function actionRelationship(){
380:         $rType = Yii::app()->request->requestType;
381:         switch($rType){
382:             case 'GET': // Look up relationships on a model
383:                 $attr = array('firstType'=>$_GET['model']);
384:                 $relationships = Relationships::model()->findAllByAttributes(array_merge(array_intersect_key($_GET,array_flip(Relationships::model()->safeAttributeNames)),$attr));
385:                 if(empty($relationships))
386:                     $this->_sendResponse(404,Yii::t('api','No relationships found.'));
387:                 else
388:                     $this->_sendResponse(200,array_map(function($r){return $r->attributes;},$relationships),1);
389:             case 'POST': // Add a new relationship to model
390:                 $relationship = new Relationships('api');
391:                 $relationship->attributes = $_POST;
392:                 $relationship->firstType = $_GET['model'];
393:                 if($relationship->validate()){
394:                     $existingRelationship = Relationships::model()->findByAttributes(array_intersect_key($relationship->attributes,array_flip(array('firstType','secondType','firstId','secondId'))));
395:                     if($existingRelationship)
396:                         $this->_sendResponse(200,Yii::t('api','Such a relationship already exists.'));
397:                     if($relationship->save()){
398:                         $this->_sendResponse(200,Yii::t('api','Successfully saved a relationship.'));
399:                     } else {
400:                         $this->_sendResponse(500,Yii::t('api','Failed to save relationship record for unknown reason.'));
401:                     }
402:                 } else {
403:                     $this->response['modelErrors'] = $relationship->errors;
404:                     $this->_sendResponse(400,$this->validationMsg('create', $relationship));
405:                 }
406:                 break;
407:             case 'DELETE':
408:                 if(!isset($_GET['secondType'],$_GET['firstId'],$_GET['secondId']))
409:                     $this->_sendResponse(400,Yii::t('api','Cannot delete; no parameters specified for finding a relationship record to delete.'));
410:                 $relationships = Relationships::model()->findAllByAttributes(array_merge(array('firstType'=>$_GET['model']),array_intersect_key($_GET,array_flip(Relationships::model()->attributeNames()))));
411:                 if(empty($relationships))
412:                     $this->_sendResponse(404,Yii::t('api','No relationships deleted; none were found matching specfied parameters.'));
413:                 $n_d = 0;
414:                 $n_d_t = count($relationships);
415:                 foreach($relationships as $model) {
416:                     $n_d += $model->delete() ? 1 : 0;
417:                 }
418:                 if($n_d == $n_d_t)
419:                     $this->_sendResponse(200,Yii::t('api','{n} relationships deleted.',array('{n}'=>$n_d)));
420:                 else
421:                     $this->_sendResponse(500,Yii::t('api','One or more relationships could not be deleted.'));
422:                 break;
423:             default:
424:                 $this->_sendResponse(400,Yii::t('api','Request type not supported for this action.'));
425:                 break;;
426:         }
427:     }
428: 
429:     /**
430:      * Operations involving tags associated with a model.
431:      *
432:      * There needs to be the tagged model's primary key value in the URL's
433:      * parameters, in addition to the model's class. If DELETE, or POST, there
434:      * needs to be an array of tags, JSON-encoded, in postdata, to delete or
435:      * add to the model.
436:      */
437:     public function actionTags() {
438:         $model = $this->model;
439:         $rType = Yii::app()->request->requestType;
440:         switch($rType){
441:             case 'GET':
442:                 // Query all tags associated with a model.
443:                 $this->_sendResponse(200, $model->getTags(),true);
444:             case 'POST':
445:                 // Add tag(s).
446:                 if(array_key_exists('tags', $_POST))
447:                     $tags = json_decode($_POST['tags'], 1);
448:                 else if(array_key_exists('tag',$_POST))
449:                     $tags = array($_POST['tag']);
450:                 else
451:                     $this->_sendResponse(400, 'Parameter "tags" (json-encoded list of tags) or "tag" (single tag to add) requried.');
452:                 if($model->addTags($tags))
453:                     $this->_sendResponse(200, sprintf('Record "%s" (%s) tagged with "%s"', $model->name, get_class($model), implode('","', $tags)));
454:                 else
455:                     $this->_sendResponse(500,Yii::t('api','Tags not added.'));
456:             case 'DELETE':
457:                 // Delete a tag
458:                 if(array_key_exists('tag',$_GET))
459:                     $tag = "#".ltrim($_GET['tag'],'#'); // Works whether or not the hash is attached. It is difficult to add the tag due to how it's a special URL character.
460:                 else
461:                     $this->_sendResponse(400, 'Please specify a tag to be deleted.');
462:                 $removed = $model->removeTags($tag);
463:                 if($removed)
464:                     $this->_sendResponse(200, sprintf('Tag "%s" deleted from "%s" (%s).', $tag, $model->name, get_class($model))); // .'$_GET='.var_export($_GET,1).'; $_POST='.var_export($_POST,1).'; uri='.$_SERVER['REQUEST_URI']);
465:                 else
466:                     $this->_sendResponse(404, 'Did not delete any existing tags.');
467:                 break;
468:         }
469:     }
470: 
471:     /**
472:      * Updates a preexisting record.
473:      *
474:      * Usage of this function is very similar to {@link actionCreate}, although
475:      * it requires the "id" parameter that corresponds to the (auto-increment)
476:      * id field of the record in the database. Thus, URLs for post requests to
477:      * this API function should be formatted as follows:
478:      *
479:      * index.php/api/update/model/[model name]/id/[record id]
480:      *
481:      * The attributes of the model should be submitted in the $_POST array along
482:      * with 'user' and 'userKey' just as in create.
483:      */
484:     public function actionUpdate() {
485:         $model = $this->model;
486:         $model->setX2Fields($_POST);
487: 
488:         // Try to save the model and perform special post-save operations based on
489:         // each class:
490:         if ($model->save()) {
491:             switch ($this->modelClass) {
492:                 case 'Actions':
493:                     $model->syncGoogleCalendar('update');
494:                     break;
495:                 default:
496:                     $this->_sendResponse(200, $model->attributes,true);
497:             }
498:             $this->response['model'] = $model->attributes;
499:             $this->_sendResponse(200, 'Model created successfully');
500:         } else {
501:             // Errors occurred
502:             $this->response['modelErrors'] = $model->errors;
503:             $msg = $this->validationMsg('update', $model);
504:             $this->_sendResponse(500,$msg);
505:         }
506:     }
507: 
508:     /**
509:      * Obtain a model by its record ID.
510:      *
511:      * Looks up a model by its record ID and responds with its attributes as a
512:      * JSON-encoded string.
513:      *
514:      * URLs to use this function:
515:      * index.php/view/id/[record id]
516:      *
517:      * Include 'user' and 'userKey' just like in create and update.
518:      */
519:     public function actionView() {
520:         $this->_sendResponse(200,$this->model->attributes,true);
521:     }
522: 
523:     /**
524:      * Records a phone call as a notification.
525:      *
526:      * Given a phone number, if a contact matching that phone number exists, a
527:      * notification assigned to that contact's assignee will be created.
528:      * Software-based telephony systems such as Asterisk can thus immediately
529:      * notify sales reps of a phone call by making a cURL request to a url
530:      * formatted as follows:
531:      *
532:      * api/voip/data/[phone number]
533:      *
534:      * (Note: the phone number itself must not contain anything but digits, i.e.
535:      * no periods or dashes.)
536:      *
537:      * For Asterisk, one possible integration method is to insert into the
538:      * dialplan, at the appropriate position, a call to a script that uses
539:      * {@link http://phpagi.sourceforge.net/ PHPAGI} to extract the phone
540:      * number. The script can then make the necessary request to this action.
541:      * @param bool $actionHist If set to 1, create an action history item for the contact.
542:      */
543:     public function actionVoip($actionHist=0) {
544: 
545:         if (isset($_GET['data'])) {
546: 
547:             $matches = array();
548:             if (preg_match('/\d{10,}/', $_GET['data'], $matches)) {
549:                 $number = ltrim($matches[0],'1');
550:                 $phoneCrit = new CDbCriteria(array(
551:                     'condition' => "modelType='Contacts' AND number LIKE :number",
552:                     'params' => array(':number'=>"%$number%")
553:                 ));
554:                 $phoneCrit->join =
555:                     'join x2_contacts on modelId=x2_contacts.id AND '.
556:                     Contacts::model ()->getHiddenCondition ('x2_contacts');
557:                 $phoneNumber = PhoneNumber::model()->find($phoneCrit);
558:                 if(!empty($phoneNumber)){
559:                     $contact = X2Model::model('Contacts')->findByPk($phoneNumber->modelId);
560:                     if(isset($contact)){
561: 
562:                         $contact->disableBehavior('changelog');
563:                         $contact->updateLastActivity();
564: 
565:                         $assignees = array($contact->assignedTo);
566:                         if($contact->assignedTo == 'Anyone' || $contact->assignedTo == null) {
567:                             $users = User::model()->findAll();
568:                             $assignees = array_map(function($u){return $u->username;},$users);
569:                         }
570:                         $multiUser = count($assignees) > 1;
571:                         $usersSuccess = array();
572:                         $usersFailure = array();
573:                         // Format the phone number:
574:                         $formattedNumber = '';
575:                         $strNumber = (string) $number;
576:                         $strl = strlen($strNumber);
577:                         $formattedNumber = substr($strNumber, $strl - 4, $strl);
578:                         $formattedNumber = substr($strNumber, $strl - 7, 3)."-$formattedNumber";
579:                         if($strl >= 10){
580:                             $formattedNumber = substr($strNumber, $strl - 10, 3)."-$formattedNumber";
581:                             if($strl > 10){
582:                                 $formattedNumber = substr($strNumber, 0, $strl - 10)."-$formattedNumber";
583:                             }
584:                         }
585:                         $time = time();
586:                         // Create notifications:
587:                         foreach($assignees as $user){
588:                             $notif = new Notification;
589:                             $notif->type = 'voip_call';
590:                             $notif->user = $user;
591:                             $notif->modelType = 'Contacts';
592:                             $notif->modelId = $contact->id;
593:                             $notif->value = $formattedNumber;
594:                             $notif->createDate = $time;
595:                             if($notif->save()){
596:                                 $usersSuccess[] = $user;
597:                             }else{
598:                                 $usersFailure = array();
599:                             }
600:                         }
601:                         if($actionHist){
602:                             // Create an action:
603:                             $action = new Actions();
604:                             $action->assignedTo = 'Anyone';
605:                             $action->visibility = 1;
606:                             $action->associationId = $contact->id;
607:                             $action->associationType = 'contacts';
608:                             $action->associationName = $contact->name;
609:                             $action->dueDate = $time;
610:                             $action->createDate = $time;
611:                             $action->completeDate = $time;
612:                             $action->lastUpdated = $time;
613:                             $action->type = 'call';
614:                             $action->complete = 'Yes';
615:                             $action->completedBy = 'Anyone';
616:                             $action->save();
617:                             $action->actionText = Yii::t('app', 'Phone system reported inbound call from contact.');
618:                         }
619: 
620:                         $failure = count($usersSuccess) == 0;
621:                         $partialFailure = count($usersFailure) > 0;
622:                         if($failure) {
623:                             $message = 'Saving notifications failed.';
624:                         } else {
625:                             /*X2Flow::trigger('RecordVoipInboundTrigger', array(
626:                                 'model' => $contact,
627:                                 'number' => $matches[0]
628:                             ));*/
629:                             $message = 'Notifications created for user(s): '.implode(',',$usersSuccess);
630:                             if($partialFailure) {
631:                                 $message .= '; saving notifications failed for users(s): '.implode(',',$usersFailure);
632:                             }
633:                         }
634: 
635:                         // Create an event record for the feed:
636:                         $event = new Events();
637:                         $event->type = 'voip_call';
638:                         $event->associationType = get_class($contact);
639:                         $event->associationId = $contact->id;
640:                         $event->save();
641: 
642:                         $this->_sendResponse($failure ? 500 : 200,$message);
643:                     } else {
644:                         $this->_sendResponse(
645:                             404,'Phone number record refers to a contact that no longer exists.');
646:                     }
647:                 }else{
648:                     $this->_sendResponse(404,'No matching phone number found.');
649:                     // $notif = new Notification;
650:                     // $notif->type = 'voip_call';
651:                     // $notif->user = ?;
652:                     // $notif->modelType = 'Contacts';
653:                     // $notif->value = $matches[0];
654:                     // $notif->createDate = time();
655:                     // $notif->save();
656:                 }
657: 
658:             } else
659:                 $this->_sendResponse(400,'Invalid phone number format.');
660:         } else {
661:             $this->_sendResponse(400,'Phone number required as "data" URL parameter.');
662:         }
663:     }
664: 
665: 
666: 
667: 
668:     /**
669:      * Checks the GET parameters for a valid model class.
670:      */
671:     public function checkValidModel(){
672:         $this->log("Checking for valid model class.");
673:         $noModel = empty($_GET['model']);
674:         if(!$noModel)
675:             $noModel = preg_match('/^\s*$/', $_GET['model']);
676:         if($noModel){
677:             $this->log('Parameter "model" missing.');
678:             $this->_sendResponse(400, "Model class name required."); // .'$_GET='.var_export($_GET,1).'; $_POST='.var_export($_POST,1).'; uri='.$_SERVER['REQUEST_URI']);
679:         }
680:         if(!class_exists($_GET['model'])){
681:             $this->log("Class {$_GET['model']} not found.");
682:             $this->_sendResponse(501, "Model class \"{$_GET['model']}\" not found or does not exist.");
683:         }
684:         $modelRef = new $_GET['model'];
685:         if(get_parent_class($modelRef) != 'X2Model'){
686:             $this->log("Class {$_GET['model']} is not a child of X2Model.");
687:             $this->_sendResponse(403, "Model class \"{$_GET['model']}\" is not a child of X2Model and cannot be used in API calls.");
688:         }
689:         // We're all clear to proceed
690:         $this->modelClass = $_GET['model'];
691:     }
692: 
693:     /**
694:      * Checks credentials for API access
695:      *
696:      * @param CFilterChain $filterChain
697:      */
698:     public function filterAuthenticate($filterChain) {
699:         $haveCred = false;
700:         $this->log("Checking user record.");
701:         if (Yii::app()->request->requestType == 'POST') {
702:             $haveCred = isset($_POST['userKey']) && isset($_POST['user']);
703:             $params = $_POST;
704:         } else {
705:             $haveCred = isset($_GET['userKey']) && isset($_GET['user']);
706:             $params = $_GET;
707:         }
708: 
709:         if ($haveCred) {
710:             $this->user = User::model()->findByAttributes(array('username' => $params['user'], 'userKey' => $params['userKey']));
711:             if ((bool) $this->user) {
712:                 Yii::app()->suModel = $this->user;
713:                 if(!empty($this->user->userKey)){
714:                     Yii::app()->params->groups = Groups::getUserGroups($this->user->id);
715:                     Yii::app()->params->roles = Roles::getUserRoles($this->user->id);
716:                     // Determine if the API user is admin (so that Yii::app()->params->isAdmin gets set properly):
717:                     $roles = RoleToUser::model()->findAllByAttributes(array('userId' => $this->user->id));
718:                     $access = false;
719:                     $auth = Yii::app()->authManager;
720:                     foreach ($roles as $role) {
721:                         $access = $access || $auth->checkAccess('AdminIndex', $role->roleId);
722:                     }
723:                     if($access)
724:                         Yii::app()->params->isAdmin = true;
725:                     $filterChain->run();
726:                 } else
727:                     $this->_sendResponse(403, "User \"{$this->user->username}\" cannot use API; userKey not set.");
728:             } else {
729:                 $this->log("Authentication failed; invalid user credentials; IP = {$_SERVER['REMOTE_ADDR']}; get or post params =  " . CJSON::encode($params).'');
730:                 $this->_sendResponse(401, "Invalid user credentials.");
731:             }
732:         } else {
733:             $this->log('No user credentials provided; IP = '.$_SERVER['REMOTE_ADDR']);
734:             $this->_sendResponse(401, "No user credentials provided.");
735:         }
736:     }
737: 
738:     /**
739:      * Sends the appropriate response if X2Engine is locked.
740:      * 
741:      * @param type $filterChain
742:      */
743:     public function filterAvailable($filterChain) {
744:         if(is_int(Yii::app()->locked)) {
745:             $this->_sendResponse(503,"X2Engine is currently undergoing maintenance. Please try again later.");
746:         }
747:         
748:         $filterChain->run();
749:     }
750: 
751:     /**
752:      * Basic permissions check filter.
753:      *
754:      * It is meant to simplify the simpler actions where named after existing
755:      * actions (or actions listed among the keys of {@link actionAuthItemMap})
756:      *
757:      * @param type $filterChain
758:      */
759:     public function filterCheckCRUDPermissions($filterChain) {
760:         $model = new $this->modelClass;
761:         $module = ucfirst($model->module);
762:         $action = $this->action->id;
763:         if(array_key_exists($action,$this->actionAuthItemMap))
764:             $action = $this->actionAuthItemMap[$action];
765:         else
766:             $action = ucfirst($action);
767:         $level = $this->actionCheckPermissions($module . $action);
768:         if($level)
769:             $filterChain->run();
770:         else {
771:             $this->log("User \"{$this->user->username}\" denied API action; does not have permission for $module$action",'application.automation.api');
772:             $this->_sendResponse(403, 'This user does not have permission to perform operation "'.$action."\" on model <b>{$this->modelClass}</b>");
773:         }
774:     }
775: 
776:     public function filterNoSession($filterChain) {
777:         Yii::app()->params->noSession = true;
778:         $filterChain->run();
779:     }
780: 
781:     /**
782:      * Ensures that the "model" parameter is present and valid.
783:      *
784:      * @param CFilterChain $filterChain
785:      */
786:     public function filterValidModel($filterChain) {
787:         if (!isset($this->modelClass)) {
788:             $this->checkValidModel();
789:             // Set user for the model:
790:             Yii::app()->setSuModel($this->user);
791:         }
792:         $filterChain->run();
793:     }
794: 
795:     /**
796:      * Model getter; assumes $_GET parameters include the model's primary key,
797:      * but $_POST is included for backwards compatibility.
798:      */
799:     public function getModel($new=false){
800:         if(!isset($this->_model)){
801:             if($new){
802:                 $this->_model = new $this->modelClass;
803:             }else{
804:                 $mSingle = X2Model::model($this->modelClass);
805:                 $params = array_merge($_POST,$_GET);
806:                 $this->_model = $mSingle->findByPkInArray($params);
807:                 // Was a model found? If not, raise an error
808:                 if(empty($this->_model))
809:                     $this->_respondBadPk($mSingle,$params);
810:             }
811:         }
812:         return $this->_model;
813:     }
814: 
815:     /**
816:      * A quick and dirty hack for filling in the gaps if the model requested
817:      * does not make use of the changelog behavior (which takes care of that
818:      * automatically)
819:      */
820:     public function modelSetUsernameFields(&$model) {
821:         X2ChangeLogBehavior::usernameFieldsSet($model,$this->user->username);
822: 
823:         if($model->hasAttribute('assignedTo')){
824:             if(array_key_exists('assignedTo', $_POST)){
825:                 $model->assignedTo = $_POST['assignedTo'];
826:             }else{
827:                 $model->assignedTo = $this->user->username;
828:             }
829:         }
830:     }
831: 
832:     /**
833:      * Compose a UI-friendly validation error summary in HTML
834:      *
835:      * @param type $action
836:      * @param type $model
837:      * @return string
838:      */
839:     public function validationMsg($action, $model){
840:         $msg = "<h1>".Yii::t('api', 'Error')."</h1>";
841:         $msg .= Yii::t('api',"Couldn't perform {a} on model {m}", array('{a}' => $action, '{m}' => "<b>".get_class($model)."</b>"));
842:         $msg .= "<ul>";
843:         foreach($model->errors as $attribute => $attr_errors){
844:             $msg .= "<li>$attribute</li>";
845:             $msg .= "<ul>";
846:             foreach($attr_errors as $attr_error)
847:                 $msg .= "<li>$attr_error</li>";
848:             $msg .= "</ul>";
849:         }
850:         $msg .= "</ul>";
851:         return $msg;
852:     }
853: 
854:     public function log($message,$level='trace') {
855:         Yii::log($message,$level,'application.api'); 
856:     }
857: 
858:     /**
859:      * Respond to a request with a specified status code and body.
860:      *
861:      * @param integer $status The HTTP status code.
862:      * @param string $body The body of the response message, or the object to be
863:      *  JSON-encoded in the response (if "direct" is used)
864:      * @param bool $direct Whether the body should be JSON-encoded and returned
865:      *  directly instead of putting it into the standard response object's
866:      *  "model" property or the like.
867:      */
868:     protected function _sendResponse($status = 200, $body = '',$direct = false) {
869:         // set the status
870: 
871:         if($direct) {
872:             // Send the body without an envelope, i.e. the "message" or "error"
873:             // properties that are standard to ResponseBehavior.
874:             $this->response->body = json_encode($body);
875:             $this->response->sendHttp($status);
876:         }
877:         
878:         // Create the body if none is passed
879:         if ($body == '') {
880:             // create some body messages
881:             $message = '';
882: 
883:             // this is purely optional, but makes the pages a little nicer to read
884:             // for your users.  Since you won't likely send a lot of different status codes,
885:             // this also shouldn't be too ponderous to maintain
886:             switch ($status) {
887:                 case 401:
888:                     $message = 'You must be authorized to view this page.';
889:                     break;
890:                 case 404:
891:                     $message = 'The requested URL ' . $_SERVER['REQUEST_URI'] . ' was not found.';
892:                     break;
893:                 case 500:
894:                     $message = 'The server encountered an error processing your request.';
895:                     break;
896:                 case 501:
897:                     $message = 'The requested method is not implemented.';
898:                     break;
899:                 case 503:
900:                     $message = "X2Engine is currently unavailable.";
901:                     break;
902:             }
903: 
904:             // servers don't always have a signature turned on
905:             // (this is an apache directive "ServerSignature On")
906:             $signature = ($_SERVER['SERVER_SIGNATURE'] == '') ? $_SERVER['SERVER_SOFTWARE'] . ' Server at ' . $_SERVER['SERVER_NAME'] . ' Port ' . $_SERVER['SERVER_PORT'] : $_SERVER['SERVER_SIGNATURE'];
907: 
908:             // this should be templated in a real-world solution
909:             $body = '<h1>' . $this->_getStatusCodeMessage($status) . '</h1>
910:         <p>' . $message . '</p>
911:         <hr />
912:         <address>' . $signature . '</address>';
913:         }
914:         $this->response->sendHttp($status,$body);
915:     }
916: 
917:     /**
918:      * Obtain an appropriate message for a given HTTP response code.
919:      *
920:      * @param integer $status
921:      * @return string
922:      */
923:     protected function _getStatusCodeMessage($status) {
924:         // these could be stored in a .ini file and loaded
925:         // via parse_ini_file()... however, this will suffice
926:         // for an example
927:         $codes = Array(
928:             200 => 'OK',
929:             400 => 'Bad Request',
930:             401 => 'Unauthorized',
931:             402 => 'Payment Required',
932:             403 => 'Forbidden',
933:             404 => 'Not Found',
934:             500 => 'Internal Server Error',
935:             501 => 'Not Implemented',
936:         );
937:         return (isset($codes[$status])) ? $codes[$status] : '';
938:     }
939: 
940:     /**
941:      * Tells the client that the primary key was bad or missing.
942:      * @param X2Model $modelSingle
943:      * @param array $params
944:      */
945:     protected function _respondBadPk(X2Model $modelSingle, array $params) {
946:         $pkc = $modelSingle->tableSchema->primaryKey;
947:         $pk = array();
948:         if (is_array($pkc)) { // Composite primary key
949:             foreach ($pkc as $colName) {
950:                 if (array_key_exists($colName, $params)) {
951:                     $pk[$colName] = $params[$colName];
952:                 }
953:             }
954:             $pkc = array_keys($pkc);
955:         } else {
956:             if (array_key_exists($pkc, $params))
957:                 $pk[$pkc] = $params[$pkc];
958:             $pkc = array($pkc);
959:         }
960:         if (!empty($pk)) {
961:             $this->_sendResponse(404, "No record of model {$this->modelClass} found with specified primary key value (" . implode('-', array_keys($pk)) . '): ' . (implode('-', array_values($pk))));
962:         } else {
963:             $this->_sendResponse(400, sprintf("No parameters matching primary key column(s) <b>%s</b> for model <b>%s</b>.",implode('-',$pkc),$this->modelClass));
964:         }
965:     }
966: 
967: }
968: 
X2CRM Documentation API documentation generated by ApiGen 2.8.0