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: /**
  38:  * 2nd generation REST API for X2Engine.
  39:  *
  40:  * Has the following conventions:
  41:  *
  42:  * General:
  43:  * 
  44:  * - The bodies of all requests sent and all responses received to/from this
  45:  *   controller shall be JSON-encoded, not URL-encoded as in traditional POST
  46:  *   (i.e. as if to mimic form submission)
  47:  * - HTTP Basic authentication in use. Note, this allows direct browser
  48:  *   explorability via entering the username and API key when prompted.
  49:  * - The status code is to be included in the "status" property of the response,
  50:  *   if it is not in the "success" category.
  51:  * - The "Content-Type" header in all responses shall be "application/json"
  52:  *
  53:  * Model-Centric (Active Record) Actions:
  54:  * - If the request is successful, the returned object should not be within an
  55:  *   envelope.
  56:  * - All URLs referring to operations on existing records shall end in ".json"
  57:  * - In responses with errors, the "errors" property is to contain the
  58:  *   validation errors as returned from {@link CActiveRecord.getErrors()}
  59:  *
  60:  * @property CActiveDataProvider $dataProvider A data provider for performing
  61:  *  searches via API using special underscore-prefixed query parameters: _page,
  62:  *  _limit and _order.
  63:  * @property boolean $enabled Returns true or false based on whether API access
  64:  *  is enabled.
  65:  * @property array $jpost (read-only) JSON data posted to the server. This 
  66:  *  should be used instead of the superglobal $_POST because PHP does not
  67:  *  natively support parsing the request body into $_POST unless it's URL-form
  68:  *  -encoded data.
  69:  * @property integer $maxPageSize Maximum page size.
  70:  * @property X2Model $model Active record instance, when/where applicable
  71:  * @property array $reservedParams Underscore-prefixed parameters used by the API
  72:  * @property string $responseBody (write-only) The body of the response to be sent
  73:  * @property Api2Settings $settings (Platinum Edition only) Advanced API settings
  74:  * @property X2Model $staticModel Static model instance, when/where applicable
  75:  * @package application.controllers
  76:  * @author Demitri Morgan <demitri@x2engine.com>
  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:      * Stores {@link post}
  88:      * @var array
  89:      */
  90:     private $_jpost;
  91: 
  92:     /**
  93:      * Active record model currently being operated on, if applicable.
  94:      * @var X2Model
  95:      */
  96:     private $_model;
  97: 
  98:     /**
  99:      * Stores {@link reqHeaders}
 100:      * @var array
 101:      */
 102:     private $_reqHeaders;
 103: 
 104:     /**
 105:      * If the "model" parameter is specified, this should be a "static" instance
 106:      * of that model.
 107:      *
 108:      * @var X2Model
 109:      */
 110:     private $_staticModel;
 111: 
 112:     /**
 113:      * Stores {@link user}
 114:      * @var type 
 115:      */
 116:     private $_user;
 117: 
 118:     /////////////
 119:     // ACTIONS //
 120:     /////////////
 121:     //
 122:     // The following methods define the available API functionality:
 123: 
 124:     /**
 125:      * "Hello world" action.
 126:      *
 127:      * Prints application and network info
 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:      * Retrieve the number of models of a specific class or matching query criteria
 141:      *
 142:      * @param string $_class Model name
 143:      * @param array $_findBy Model query criteria
 144:      */
 145:     public function actionCount($_class,$_findBy=null) {
 146:         $staticModel = $this->getStaticModel();
 147: 
 148:         if(!empty($_findBy)) {
 149:             // Use case: count models by uniquely identifying attributes
 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:             // Use case: count all models
 160:             $count = $this->getDataProvider()->getTotalItemCount();
 161:         }
 162:         $this->responseBody = $count;
 163:     }
 164: 
 165:     /**
 166:      * Responds with dropdown list metadata
 167:      *
 168:      * @param integer $_id
 169:      */
 170:     public function actionDropdowns($_id=null) {
 171:         if($_id !== null){
 172:             // Look for a specific dropdown record
 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:             // Query dropdowns
 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:      * Returns an array of role-specific field-level permissions
 188:      */
 189:     public function actionFieldPermissions($_class) {
 190:         $this->responseBody = $this->staticModel->fieldPermissions;
 191:     }
 192: 
 193:     /**
 194:      * Access fields for a given X2Model class
 195:      * 
 196:      * @param string $_class Model name
 197:      * @param string $_fieldName Field name
 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; // ALL fields
 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:      * Action for the creation of "hooks" (see {@link ApiHook})
 218:      *
 219:      * @param integer $_id ID of the hook.
 220:      * @param string $_class Subclass of {@link X2Model} to which the hook pertains
 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 { // POST (will respond to all other methods with 405)
 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:      * Basic operations on an X2Engine model.
 267:      *
 268:      * @param string $class
 269:      * @param integer $id
 270:      * @param array $modelInput Model parameters, i.e. if doing a lookup
 271:      */
 272:     public function actionModel($_class,$_id=null,$_findBy=null) {
 273:         $method = Yii::app()->request->getRequestType();
 274: 
 275:         // Run extra special stuff for the Actions class
 276:         $this->kludgesForActions();
 277: 
 278:         switch($method) {
 279:             case 'GET':
 280:                 if((!empty($_id) && ctype_digit((string) $_id)) ||
 281:                         !empty($_findBy)) {
 282:                     // Use case: directly access the model by ID or uniquely
 283:                     // identifying attributes
 284:                     $this->responseBody = $this->model;
 285:                 } else {
 286:                     // Use case: if no model was directly accessed by ID,
 287:                     // perform a search using the available parameters
 288:                     $this->responseBody = $this->getDataProvider()->getData();
 289:                 }
 290:                 break;
 291:             case 'PATCH':
 292:             case 'POST':
 293:             case 'PUT':
 294:                 // Additional check for request validity
 295:                 if($method == 'POST') {
 296:                     if(!(empty($_id) && empty($_findBy))) // POST on an existing record
 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:                     // Instantiate a new active record model, but go through
 302:                     // getStaticModel to check for class validity:
 303:                     $class = get_class($this->getStaticModel());
 304:                     if (!isset ($this->_model)) $this->model = new $class;
 305:                 }
 306: 
 307:                 // Set attributes
 308:                 $this->setModelAttributes();
 309:                 
 310:                 // Validate/save
 311:                 $saved = false;
 312:                 if($method == 'POST') {
 313:                     // Save new
 314:                     $saved = $this->model->save();
 315:                 } else {
 316:                     // Update existing
 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:                 // Check for errors and respond if necessary.
 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:                 // Set body
 344:                 $this->responseBody = $this->model;
 345: 
 346:                 // Add resource location header for a newly created record
 347:                 // and send with 201 status
 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:      * Responds with a JSON-encoded list of model classes.
 369:      *
 370:      * @param integer $partialSupport 1 to include partially-supported models,
 371:      *  0 to include only fully-supported models.
 372:      */
 373:     public function actionModels($partialSupport=1) {
 374:         // To obtain all models: iterate through modules
 375:         $modelNames = X2Model::getModelNames();
 376:         // Partially-supported models
 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:      * Action for viewing or modifying relationships on a model.
 403:      * 
 404:      * @param type $_class
 405:      * @param type $_id
 406:      * @param type $_relatedId
 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:             // Check whether the relationship is actually attached to this model:
 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:                     // Get an individual relationship record. Also, include the
 438:                     // resource URL of the related model.
 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:                     // Query relationships on a model.
 451:                     $criteria = null;
 452:                     if(!($relationship instanceof Relationships)) {
 453:                         // Both ingoing and outgoing relationships.
 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:                         // Cannot PUT on a nonexistent model
 475:                         $this->send(405,"Method \"POST\" is required to create new relationships.");
 476:                     }
 477:                     $relationship = new Relationships;
 478:                 }
 479:                 // Scenario kludge that adds special validation rule for the
 480:                 // Relations model class, which dicates that it must point to
 481:                 // an existing record on both ends:
 482:                 $relationship->setScenario('api');
 483:                 $relationship->setAttributes($this->jpost);
 484:                 // Set missing attributes, if any:
 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:                     // Validation errors
 494:                     $this->response['errors'] = $relationship->errors;
 495:                     $this->send(422);
 496:                 } else {
 497:                     $this->responseBody = $relationship;
 498:                     if($method === 'POST'){
 499:                         // Set location header and respond with "201 Created"
 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:      * Query, add, or remove tags on a model.
 524:      *
 525:      * The body sent to this method in POST/PUT should be a JSON-encoded array
 526:      * of tag names.
 527:      *
 528:      * @param string $_class The active record model class being tagged
 529:      * @param integer $_id The ID of the active record being tagged
 530:      * @param type $_relatedId The ID of a tag itself
 531:      */
 532:     public function actionTags($_class=null,$_id=null,$_tagName=null) {
 533:         $method = Yii::app()->request->requestType;
 534: 
 535:         // Get the current tag being acted upon, if applicable:
 536:         $tag = null;
 537:         if($_class !== null && $_id !== null && $_tagName != null){
 538:             // Use case: operating on a tag of a specific model by its name in
 539:             // order to get or delete it.
 540:             $tag = Tags::model()->findByAttributes(array(
 541:                 'type' => $_class,
 542:                 'itemId' => $this->model->id, // Look up model
 543:                 'tag' => '#'.ltrim($_tagName, '#') // Auto-prepend "#" if missing
 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:                     // Use case: no tag ID could be found, either directly or in
 553:                     // association with a X2Model model record. Search tags:
 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:                     // Get an individual tag by name, one way or another:
 567:                     $this->responseBody = $tag;
 568:                 }
 569:                 break;
 570:             case 'POST':
 571:                 if($tag instanceof Tags) {
 572:                     // This is not the appropriate way to modify tags.
 573:                     $this->send(405,"Tags cannot be individually modified.");
 574:                 }
 575:                 // Add tags using the native method in TagBehavior:
 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:      * Hello-world error action; test for support of unconventional status code.
 594:      */
 595:     public function actionTeapot(){
 596:         $this->send(418, "I'm a teapot.");
 597:     }
 598: 
 599:     /**
 600:      * Prints user metadata.
 601:      * @param type $_id
 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:      * Returns a list of fields in the format required by Zapier's custom action
 617:      * fields feature.
 618:      *
 619:      * @param type $_class
 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:             // Populate the "choices" array for dropdowns in the Zap editing UI:
 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:      * Respond if a response hasn't already been sent.
 669:      *
 670:      * If a response hasn't been sent yet and the action has executed fully,
 671:      * this method sends an empty response with the 204 status if the body has
 672:      * not been set, and the body itself with 200 otherwise.
 673:      *
 674:      * This eliminates the need to call {@link send} at the end of every action
 675:      * where content would be sent.
 676:      *
 677:      * @param type $action
 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:      * Returns the viewable attributes of an active record model in an array.
 688:      * 
 689:      * @param CActiveRecord $model
 690:      */
 691:     public function attributesOf(CActiveRecord $model){
 692:         if($model instanceof X2Model){
 693:             $attributes = $model->getAttributes($model->getReadableAttributeNames());
 694: 
 695:             // Kludge for including actionDescription in Actions:
 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:                 // Attribute whitelisting for privacy
 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:      * Sends an authentication failure message to the client.
 721:      *
 722:      * @param string $message The message to include
 723:      */
 724:     public function authFail($message){
 725:         // Set "realm" header:
 726:         $this->response->httpHeader['WWW-Authenticate'] =
 727:                 'Basic realm="X2Engine API v2"';
 728: 
 729:         
 730: 
 731:         $this->send(401, $message);
 732:     }
 733: 
 734:     /**
 735:      * Special behaviors for the controller.
 736:      *
 737:      * This should be really basic/minimal.
 738:      * 
 739:      * @return type
 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:      * Gets possible values for a field.
 758:      *
 759:      * Note, this is meant to be a stripped-down imitation of what is in
 760:      * {@link X2Model} already. I know this is code duplication, but considering 
 761:      *
 762:      * Note, does not yet handle multiple choice (selecting more than one).
 763:      * 
 764:      * @param Fields $field
 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:                 // Dropdown options
 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:                 // Special kludge for actions priority dropdown mapping
 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:     // FILTERS //
 812:     /////////////
 813:     //
 814:     // These define access control/denial to the API.
 815: 
 816:     /**
 817:      * Sets the user for a stateless API request
 818:      */
 819:     public function filterAuthenticate($filterChain) {
 820:         // Check for the availability of authentication:
 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:         // Invalid/not found
 835:         if(!($userModel instanceof User) || !PasswordUtil::slowEquals($userModel->userKey, $pw))
 836:             $this->authFail("Invalid user credentials.");
 837:         elseif(trim($userModel->userKey)==null) // Null user key = disabled
 838:             $this->authFail("API access has been disabled for the specified user.");
 839: 
 840:         // Set user model and profile to respect permissions
 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:      * Ends the request if the app is locked.
 851:      * 
 852:      * @param CFilterChain $filterChain
 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:      * JSON-only enforcement for input data
 869:      *
 870:      * Rejects POST/PUT requests with improper content type request header.
 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:      * Halts execution if the method does not match the list of acceptable
 881:      * methods for the action.
 882:      *
 883:      * @param type $filterChain
 884:      */
 885:     public function filterMethods($filterChain) {
 886:         $id = $filterChain->action->id;
 887:         $methods = self::methods();
 888:         if(isset($methods[$id])){
 889:             // List of methods specified for action. Check.
 890:             $acceptMethods = explode(',', $methods[$id]);
 891:             if(in_array($method = Yii::app()->request->getRequestType(), $acceptMethods)){
 892:                 // Method OK; it's listed as an accepted request type
 893:                 $filterChain->run();
 894:             } else {
 895:                 // Method NOT OK; not listed.
 896:                 $this->send(405,"Action \"$id\" does not support $method.");
 897:             }
 898:         } else {
 899:             // No list of acceptable types specified in methods(), so just run:
 900:             $filterChain->run();
 901:         }
 902:     }
 903: 
 904:     /**
 905:      * Performs RBAC permission checks before allowing access to something.
 906:      *
 907:      * This is to make permissions consistent with  normal use of te app
 908:      *
 909:      * @param type $filterChain
 910:      */
 911:     public function filterRbac($filterChain) {
 912:         $action = null; // The name of the RBAC item to check
 913:         $data = array(); // Additional parameters for RBAC
 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:         // Include module-specific, assignment-based permissions if operating
 921:         // on a model (as opposed to, say, querying all tags regardless of the
 922:         // type of record they're attached to)
 923:         if(isset($_GET['_class'])){
 924:             $linkable = $this->staticModel->asa('X2LinkableBehavior');
 925:             $module = !empty($linkable) ? ucfirst($linkable->module) : $_GET['_class'];
 926:             // Assignment/ownership as stored in the model should be
 927:             // included in the RBAC parameters for business rules to execute
 928:             // properly, if an ID is specified:
 929:             if(isset($_GET['_id'])){
 930:                 $data['X2Model'] = $this->model;
 931:             }
 932:         }
 933:         
 934:         // Resolve the name of the auth item to check.
 935:         //
 936:         // There are three actions and five different request types (DELETE,
 937:         // GET, PATCH, POST, PUT) two of which (PATCH/PUT) are indistinct.
 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:                         // Query or view individual:
 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:                         // As long as the user has permission to view the
 970:                         // record, they should have permission to alter these
 971:                         // metadata (this is the behavior of the base app, as
 972:                         // of this writing):
 973:                         $action = "{$module}View";
 974:                         break;
 975:                     case 'GET':
 976:                         if(isset($_GET['_class']) && isset($_GET['_id'])) {
 977:                             // Respect the permissions of that particular model,
 978:                             // so that URI's corresponding to a given model
 979:                             // record respond consistently:
 980:                             $action = "{$module}View";
 981:                         } else {
 982:                             // Querying all relationships/tags. Simply allow
 983:                             // access because there's no analogue of this
 984:                             // functionality in the application (as of this 
 985:                             // writing), let alone permission entries for them,
 986:                             // and thus nothing on which to base permissions.
 987:                             $filterChain->run();
 988:                         }
 989:                         break;
 990:                 }
 991:                 break;
 992:         }
 993: 
 994:         // Use RBAC to check permission if an auth item exists.
 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', // Application not locked
1008:             
1009:             'authenticate', // Valid user
1010:             'methods', // Valid request method for the given action
1011:             'contentType', // Valid content type when submitting data
1012:             'rbac + count,model,relationships,tags', // Checks permission
1013:         );
1014:     }
1015: 
1016:     /**
1017:      * Generates attributes from a query parameter
1018:      * 
1019:      * Takes a special format of query parameter and returns an array of
1020:      * attributes. The parameter should be formatted as:
1021:      * name1**value1,,name2**value2[...]
1022:      * 
1023:      * @param string $condition The condition parameter
1024:      * @return array Associative array of key=>value pairs.
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:             // Filter out attributes not present among those that are allowable
1043:             $attributeConditions = array_intersect_key(
1044:                 $attributeConditions,
1045:                 $validAttributes
1046:             );
1047:         }
1048:         return $attributeConditions;
1049:     }
1050: 
1051:     //////////////////////
1052:     // PROPERTY GETTERS //
1053:     //////////////////////
1054:     //
1055:     // Functions that provide the value of properties
1056: 
1057:     /**
1058:      * Creates a {@link CActiveDataProvider} object for search queries.
1059:      *
1060:      * @param string $modelClass Optional, class of {@link CActiveRecord}. If
1061:      *  unspecified, the class of {@link staticModel} will be used.
1062:      * @param CDbCriteria $extraCriteria Criteria to merge with the automatically
1063:      *  created criteria.
1064:      * @param string $combineOp How to combine the custom/extra criteria
1065:      */
1066:     public function getDataProvider($modelClass=null,$extraCriteria = null,$combineOp = 'AND') {
1067:         // Check for model
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:         // Compose attributes in the query parameters for the comparison:
1079:         $searchAttributes = array_intersect_key($_GET, $staticModel->attributes);
1080: 
1081:         // Get search option parameters
1082:         $optionParams = array_fill_keys($this->reservedParams['search'],0);
1083:         $searchOptions = array_intersect_key($_GET, $optionParams);
1084: 
1085:         // Configure the CDbCriteria object
1086:         $criteria = new CDbCriteria;
1087:         $criteria->alias = 't';
1088: 
1089:         if($model instanceof X2Model){
1090:             // Special handling of X2Model subclasses:
1091: 
1092:             if($model->asa('permissions') && $model->asa('permissions')->enabled){
1093:                 // Working with an X2Model instance having its permissions behavior
1094:                 // enabled. Include access/permissions criteria.
1095:                 $criteria->mergeWith($model->getAccessCriteria());
1096:             }
1097:             if(isset($searchOptions['_tags'])) {
1098:                 // Add tag search criteria
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:                     // Perform an "and" tag search (must have all tags)
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:                     // Perform an "or" tag search (could have any one of the
1122:                     // tags in the list)
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:             // Special "codes" in comparison values.
1136:             //
1137:             // Not intended for more advanced formulae parsing but for basic
1138:             // stuff, i.e. dynamic points in time like "yesterday"
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:         // Search options:
1160:         //
1161:         // Send with parameter _partial=1 to enable partial match in searches
1162:         $partialMatch = isset($searchOptions['_partial'])
1163:                 ? (boolean) (integer) $searchOptions['_partial']
1164:                 : false;
1165:         // Send with parameter _or=1 to enable the "OR" operator in the search
1166:         $operator = isset($searchOptions['_or']) && (boolean) (integer) $searchOptions['_or']
1167:                 ? 'OR'
1168:                 : 'AND';
1169:         // Send with parameter _escape=0 to enable searching with MySQL wildcards
1170:         $escape = isset($searchOptions['_escape'])
1171:                 ? (boolean) (integer) $searchOptions['_escape']
1172:                 : true;
1173: 
1174:         // If searching for Actions, perform additional stuff first:
1175:         if($class === 'Actions'){
1176:             $this->kludgesForSearchingActions($searchAttributes,$criteria);
1177:         }
1178: 
1179:         // Run comparisons:
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:         // Merge extra criteria:
1187:         if($extraCriteria instanceof CDbCriteria) {
1188:             $criteria->mergeWith($extraCriteria,$combineOp);
1189:         }
1190: 
1191:         // Interpret "order" configuration from parameters:
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', // "+" translates to a space on some servers
1203:                     '-' => 'DESC'
1204:                 );
1205:                 $criteria->order = $col
1206:                         .(empty($match['asc']) ? '' : ' '.$ascMap[$match['asc']]);
1207:             }
1208:         }
1209: 
1210:         // Interpret pagination from parameters:
1211:         $pageSize = null; // Default query size
1212:         $pageInd = 0; // Default page
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:         // Construct the data provider object
1223:         return new CActiveDataProvider($class, compact('model', 'criteria', 'pagination'));
1224:     }
1225: 
1226:     /**
1227:      * Returns {@link enabled}
1228:      */
1229:     public function getEnabled() {
1230:         return self::ENABLED ;
1231:     }
1232: 
1233: 
1234:     /**
1235:      * Gets POST-ed, JSON-encoded data.
1236:      *
1237:      * @return array
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:      * Returns the current active record currently being operated on.
1258:      *
1259:      * Checks for a valid model type/ID and sets the {@link model} property.
1260:      * 
1261:      * @return X2Model
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:                 // Find model by attributes.
1275:                 // 
1276:                 // First transform the _findBy parameter into conditions
1277:                 $staticModel = $this->getStaticModel();
1278:                 $attributeConditions = $this->findConditions(
1279:                     $_GET['_findBy'],
1280:                     $staticModel->attributes
1281:                 );
1282:                 
1283:                 // No conditions present
1284:                 if(count($attributeConditions) == 0) {
1285:                     $this->send(400,"Invalid/improperly formatted attribute".
1286:                         " conditions: \"{$_GET['_findBy']}\"");
1287:                 }
1288: 
1289:                 // Find:
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:                         // Return with status 300 (multiple choices) and point 
1300:                         // the client to the query URL if more than one result
1301:                         // was found, and the
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:      * Returns an array listing all "reserved" query parameters.
1333:      */
1334:     public function getReservedParams() {
1335:         return array(
1336:             // Basic query parameters
1337:             'default' => array(
1338:                 '_class', // Model class when acting on an X2Model child
1339:                 '_id', // ID of a specific record
1340:                 '_tagName', // Tag name
1341:                 '_relatedId', // ID of relationship record
1342:             ),
1343:             // Search queries
1344:             'search' => array(
1345:                 '_escape', // Escape input (i.e. "%")
1346:                 '_limit', // Page size
1347:                 '_or', // Use the "OR" operator instead of "AND" in searches
1348:                 '_order', // Sorting
1349:                 '_page', // Page offset
1350:                 '_partial', // Enable partial matching
1351:                 '_tagOr', // Use "or" for tag searches (default: false)
1352:                 '_tags', // Comma-delineated list of tags to search for
1353:             ),
1354:         );
1355:     }
1356: 
1357:     
1358: 
1359:     /////////////////////////////
1360:     // MISCELLANEOUS FUNCTIONS //
1361:     /////////////////////////////
1362: 
1363:     /**
1364:      * Returns a static instance of the current effective model type.
1365:      *
1366:      * Performs a check for valid model class.
1367:      *
1368:      * @return X2Model
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:      * Exception handling for the web API
1383:      *
1384:      * Handle CHttpException instances more gracefully, and defer to the
1385:      * exception handler of {@link ResponseUtil} in all other cases.
1386:      *
1387:      * @param Exception $e
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:      * Special checks and operations to perform when working with Actions.
1402:      */
1403:     public function kludgesForActions(){
1404:         $method = Yii::app()->request->requestType;
1405:         if($_GET['_class'] == 'Actions'){
1406:             // Check association:
1407:             if(isset($_GET['associationType'], $_GET['associationId'])){
1408:                 // Must check to see if association type is valid:
1409:                 if(!($staticAssocModel = X2Model::model($_GET['associationType']))){
1410:                     $this->send(400, 'Invalid association type.');
1411:                 }
1412:                 // Check to see if associated record exists:
1413:                 $associatedModel = $staticAssocModel->findByPk($_GET['associationId']);
1414:                 if(!(bool) $associatedModel) {
1415:                     $this->send(404, 'Associated record not found.');
1416:                 }
1417:                 
1418:                 // Check if the association matches:
1419:                 if(isset($_GET['_id']) 
1420:                         && $this->model->associationId != $_GET['associationId']) {
1421:                     // Looking at an action that exists but isn't associated
1422:                     // with the current model. Construct a proper URI for the
1423:                     // client to follow:
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:             // Special attribute override, i.e. when POST is sent to {model}/{id}/Actions:
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:      * Work-arounds for querying records of the Actions class, which is special
1451:      *
1452:      * The {@link Actions} class is special and different from all the other
1453:      * {@link X2Model} sub-classes, hence this function was written to deal with
1454:      * the differences.
1455:      * 
1456:      * @param type $searchAttributes Search parameters
1457:      * @param CDbCriteria $criteria The search criteria to modify
1458:      */
1459:     public function kludgesForSearchingActions(&$searchAttributes,$criteria){
1460:         // Searching through actionDescription
1461:         //
1462:         // The property Actions.actionDescription is actually a TEXT column in a
1463:         // related table, x2_action_text. That's why searching based on
1464:         // actionDescription is NOT recommended; it will be very, very slow.
1465:         //
1466:         // Also, note (THIS IS IMPORTANT) because it's in a joined table, we 
1467:         // cannot use the elegant CDbCriteria.compare() function to perform
1468:         // the comparison. We thus lose all the advanced comparison and sorting
1469:         // options. Just know that whatever the 'actionDescription' parameter
1470:         // equals will be included directly as a parameter to a "LIKE"
1471:         // comparison statement.
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:         // Awful, ugly kludge for validating Actions' associationType field:
1482:         //
1483:         // The following lines should be removed as soon as associationType in
1484:         // Actions is "fixed" (meaning, it references actual class names instead
1485:         // of module names). The following line was added to account for the
1486:         // case of a database with case-sensitive collation, whereupon a query
1487:         // for associated actions using api2/[class]/[id]/Actions would for
1488:         // instance always return zero results, because associationType (which
1489:         // by URL rules maps to the "_class" parameter) is "Contacts" (the
1490:         // actual class name) rather than "contacts" (the "association type" as
1491:         // dictated by the unwieldy convention that we've had for Actions almost
1492:         // from the very beginnings of X2Engine).
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:      * Logs an API message.
1506:      *
1507:      * @param type $message
1508:      * @param type $level
1509:      * @param type $category
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:      * Returns an array describing acceptable methods for each API action
1519:      *
1520:      * - Each key in the returned array is a controller ID;
1521:      * - Each value in the array is a comma-delineated list (string) of methods
1522:      * - If a controller's ID is not in this array, it is assumed that it should
1523:      *   accept any request method used.
1524:      * @return type
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:      * Sends a HTTP response and logs the message that was sent.
1545:      *
1546:      * @param integer $status
1547:      * @param string $message
1548:      */
1549:     public function send($status=200,$message = '') {
1550:         $statMessage = ResponseUtil::statusMessage($status);
1551:         if(!isset($this->response->body)) {
1552:             // Copy the headers into the response JSON for inferior HTTP client
1553:             // libraries that don't know how to read response headers:
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:      * Respond with empty body and optionally log a message.
1565:      * 
1566:      * @param type $message
1567:      */
1568:     public function sendEmpty($message = ''){
1569:         $this->responseBody = '';
1570:         $this->send(204, $message);
1571:     }
1572: 
1573:     /**
1574:      * Sets the current working model.
1575:      * @param X2Model $model
1576:      */
1577:     public function setModel(X2Model $model) {
1578:         $this->_model = $model;
1579:     }
1580: 
1581:     /**
1582:      * Sets the current acting model with data submitted to the server.
1583:      * @param type $fields
1584:      * @return type
1585:      */
1586:     public function setModelAttributes($fields = array()) {
1587:         if(empty($fields)) {
1588:             $fields = $this->jpost;
1589:         }
1590:         
1591: 
1592:         // Kludge to allow setting special fields like "type" directly in
1593:         // Actions (which is necessary in order to properly work with action
1594:         // records that have an association, i.e. call log on a lead)
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:             // key is read-only, won't be set by setX2Fields
1610:             $this->model->trackingKey = $fields['trackingKey']; 
1611:         }
1612:     }
1613: 
1614:     /**
1615:      * Sets the body of the response
1616:      *
1617:      * @param mixed $object The object to JSON encode and include in the response
1618:      */
1619:     public function setResponseBody($object) {
1620:         switch(gettype($object)) {
1621:             case 'string':
1622:                 $this->response->body = $object;
1623:                 break;
1624:             case 'array':
1625:                 // Assume all elements are of the same type
1626:                 $firstElement = reset($object);
1627:                 if($firstElement instanceof CActiveRecord) {
1628:                     $records = array();
1629:                     if($firstElement instanceof Tags){
1630:                         // For tags: just get the tag name of each tag (flat list)
1631:                         $records = array_map(function($t){return $t->tag;},$object);
1632:                     }else{
1633:                         // For everything else: get full list of attributes
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: 
X2CRM Documentation API documentation generated by ApiGen 2.8.0