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

  • ActionMetaData
  • ActionText
  • Admin
  • AmorphousModel
  • ApiHook
  • APIModel
  • ChartSetting
  • ContactForm
  • ContactList
  • Credentials
  • Criteria
  • Dropdowns
  • Events
  • EventsData
  • Fields
  • FormLayout
  • Imports
  • InlineEmail
  • LeadRouting
  • Locations
  • LoginForm
  • Maps
  • Modules
  • Notes
  • Notification
  • PhoneNumber
  • Profile
  • Record
  • Relationships
  • Roles
  • RoleToPermission
  • RoleToUser
  • RoleToWorkflow
  • Rules
  • Session
  • SessionLog
  • Social
  • Tags
  • TempFile
  • Tips
  • Tours
  • TrackEmail
  • TriggerLog
  • URL
  • ViewLog
  • Widgets
  • X2List
  • X2ListCriterion
  • X2ListItem
  • X2Model
  • Overview
  • Package
  • Class
  • Tree
   1: <?php
   2: 
   3: /*****************************************************************************************
   4:  * X2Engine Open Source Edition is a customer relationship management program developed by
   5:  * X2Engine, Inc. Copyright (C) 2011-2016 X2Engine Inc.
   6:  * 
   7:  * This program is free software; you can redistribute it and/or modify it under
   8:  * the terms of the GNU Affero General Public License version 3 as published by the
   9:  * Free Software Foundation with the addition of the following permission added
  10:  * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
  11:  * IN WHICH THE COPYRIGHT IS OWNED BY X2ENGINE, X2ENGINE DISCLAIMS THE WARRANTY
  12:  * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
  13:  * 
  14:  * This program is distributed in the hope that it will be useful, but WITHOUT
  15:  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  16:  * FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
  17:  * details.
  18:  * 
  19:  * You should have received a copy of the GNU Affero General Public License along with
  20:  * this program; if not, see http://www.gnu.org/licenses or write to the Free
  21:  * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  22:  * 02110-1301 USA.
  23:  * 
  24:  * You can contact X2Engine, Inc. P.O. Box 66752, Scotts Valley,
  25:  * California 95067, USA. or at email address contact@x2engine.com.
  26:  * 
  27:  * The interactive user interfaces in modified source and object code versions
  28:  * of this program must display Appropriate Legal Notices, as required under
  29:  * Section 5 of the GNU Affero General Public License version 3.
  30:  * 
  31:  * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
  32:  * these Appropriate Legal Notices must retain the display of the "Powered by
  33:  * X2Engine" logo. If the display of the logo is not reasonably feasible for
  34:  * technical reasons, the Appropriate Legal Notices must display the words
  35:  * "Powered by X2Engine".
  36:  *****************************************************************************************/
  37: 
  38: /**
  39:  * This is the model class for table "x2_fields".
  40:  *
  41:  * @package application.models
  42:  * @property integer $id
  43:  * @property string $modelName
  44:  * @property string $fieldName
  45:  * @property string $attributeLabel
  46:  * @property integer $show
  47:  * @property integer $custom
  48:  * @property string $myTableName The name of the table to which the field corresponds
  49:  * @author Jake Houser <jake@x2engine.com>, Demitri Morgan <demitri@x2engine.com>
  50:  */
  51: class Fields extends CActiveRecord {
  52: 
  53:     /**
  54:      * Defines the separator between name & ID in uniquely identifying "nameId"
  55:      * fields
  56:      */
  57:     const NAMEID_DELIM = '_';
  58: 
  59:     const MULTI_ASSIGNMENT_DELIM = ', ';
  60: 
  61:     const RATING_MIN = 1;
  62: 
  63:     const RATING_MAX = 5;
  64: 
  65:     const WRITE_PERMISSION = 2;
  66: 
  67:     const READ_PERMISSION = 1;
  68: 
  69:     const NO_PERMISSION = 0;
  70: 
  71:     public $includeEmpty = true;
  72: 
  73:     private $_myTableName;
  74: 
  75:     private $_typeChanged = false;
  76: 
  77:     private static $_purifier;
  78: 
  79:     /**
  80:      * PHP types corresponding to field types in X2Engine.
  81:      *
  82:      * This is to supplement Yii's active record functionality, which does not
  83:      * typecast column values according to their canonical type.
  84:      * @var type
  85:      */
  86:     public static $phpTypes = array(
  87:         'assignment' => 'string',
  88:         'boolean' => 'boolean',
  89:         'credentials' => 'integer',
  90:         'currency' => 'double',
  91:         'date' => 'integer',
  92:         'dateTime' => 'integer',
  93:         'dropdown' => 'string',
  94:         'email' => 'string',
  95:         'int' => 'integer',
  96:         'link' => 'string',
  97:         'optionalAssignment' => 'string',
  98:         'percentage' => 'double',
  99:         'rating' => 'integer',
 100:         'varchar' => 'string',
 101:     );
 102: 
 103:     /**
 104:      * Constructor override.
 105:      */
 106:     public function __construct($scenario = 'insert') {
 107:         parent::__construct($scenario);
 108:         if($scenario == 'search') {
 109:             $this->setAttributes(
 110:                 array_fill_keys(
 111:                     $this->attributeNames(),
 112:                     null
 113:                 ),
 114:                 false);
 115:         }   
 116:     }
 117: 
 118:     public function behaviors () {
 119:         return array_merge (parent::behaviors (), array (
 120:             'CommonFieldsBehavior' => array (
 121:                 'class' => 'application.components.behaviors.CommonFieldsBehavior',
 122:             )
 123:         ));
 124:     }
 125: 
 126:     public function getDropdownValue ($fieldValue) {
 127:         return X2Model::model('Dropdowns')->getDropdownValue(
 128:             $this->linkType, $fieldValue);
 129:     }
 130: 
 131:     public function getDropdownOptions () {
 132:         return Dropdowns::getItems($this->linkType, null, true);
 133:     }
 134: 
 135:     public function setAttributes ($values, $safeOnly=true) {
 136:         if (isset ($values['type']) && $this->type !== $values['type']) {
 137:             $this->_typeChanged = true;
 138:         }
 139:         return parent::setAttributes ($values, $safeOnly);
 140:     }
 141: 
 142:     /**
 143:      * Rules for saving a field.
 144:      *
 145:      * See the following MySQL documentation pages for more info on length
 146:      * restrictions and naming requirements:
 147:      * http://dev.mysql.com/doc/refman/5.0/en/identifiers.html
 148:      *
 149:      * @return array validation rules for model attributes.
 150:      */
 151:     public function rules(){
 152:         // NOTE: you should only define rules for those attributes that
 153:         // will receive user inputs.
 154:         return array(
 155:             array('modelName, attributeLabel', 'length', 'max' => 250),
 156:             array('fieldName','length','max'=>64), // Max length for column identifiers in MySQL
 157:             array('fieldName','match','pattern'=>'/^[a-zA-Z]\w+$/','message'=>Yii::t('admin','Field name may only contain alphanumeric characters and underscores.')),
 158:             array('fieldName','nonReserved'),
 159:             array('modelName, fieldName, attributeLabel', 'required'),
 160:             array(
 161:                 'modelName','in','range'=>array_keys(X2Model::getModelNames()),'allowEmpty'=>false),
 162:             array('defaultValue','validDefault'),
 163:             array('relevance','in','range'=>array_keys(self::searchRelevance())),
 164:             array('custom, modified, readOnly, searchable, required, uniqueConstraint', 'boolean'),
 165:             array('fieldName','uniqueFieldName'),
 166:             array('linkType','length','max'=>250),
 167:             array('description','length','max'=>500),
 168:             array('type','length','max'=>20),
 169:             array('keyType','in','range' => array('MUL','UNI','PRI','FIX','FOR'), 'allowEmpty'=>true),
 170:             array('keyType','requiredUnique'),
 171:             array('data','validCustom'),
 172:             // The following rule is used by search().
 173:             // Please remove those attributes that should not be searched.
 174:             array('id, modelName, fieldName, attributeLabel, custom, modified, readOnly, keyType', 'safe', 'on' => 'search'),
 175:         );
 176:     }
 177:     
 178:     /**
 179:      * Counts the number of records such that the field is not null.
 180:      *
 181:      * @return integer
 182:      */
 183:     public function countNonNull() {
 184:         return Yii::app()->db->createCommand()->
 185:             select('COUNT(*)')->
 186:             from(X2Model::model($this->modelName)->tableName())->
 187:             where(
 188:                  "{$this->fieldName} IS NOT NULL AND 
 189:                 {$this->fieldName} != :default AND
 190:                 {$this->fieldName} != ''",
 191:                 array(
 192:                     ':default' => $this->defaultValue
 193:                 )
 194:             )->queryScalar();
 195:     }
 196: 
 197:     public static function getLinkTypes () {
 198:         return Yii::app()->db->createCommand ("
 199:             SELECT distinct(modelName)
 200:             FROM x2_fields
 201:             WHERE fieldName='nameId'
 202:             ORDER by modelName ASC
 203:         ")->queryColumn ();
 204:     }
 205: 
 206:     /**
 207:      * Legacy function kept for backwards compatibility.
 208:      *
 209:      * Moved from {@link Admin} and renamed from getModelList because it doesn't
 210:      * make sense to put it there; it doesn't fit in a model intended for
 211:      * managing system settings, but it does in this model, which is intended
 212:      * for managing data models themselves.
 213:      *
 214:      * @return array
 215:      */
 216:     public static function getDisplayedModelNamesList(){
 217:         $modelList = array();
 218:         foreach(X2Model::model('Modules')->findAllByAttributes(array('editable' => true, 'visible' => 1)) as $module){
 219:             if($modelName = X2Model::getModelName($module->name)){
 220:                 $modelName = $module->name;
 221:             }else{
 222:                 $modelName = ucfirst($module->name);
 223:             }
 224:             if(Yii::app()->user->checkAccess(ucfirst($module->name).'Index', array())){
 225:                 $modelList[$modelName] = $module->title;
 226:             }
 227:         }
 228:         return array_map(function($term){
 229:                             return Yii::t('app', $term);
 230:                         },$modelList);
 231:     }
 232:     
 233:     /**
 234:      * Function to return data about the field types used by X2.
 235:      *
 236:      * This function should be called whenever information about an x2_field type
 237:      * is needed, as it can store all of that information in one consolidated
 238:      * location and has a variety of uses. If no scenario is provided, it will turn
 239:      * the array exactly as defined in $fieldTypes at the top of the function.
 240:      * If a scenario is provided, it will attempt to fill only the information
 241:      * for that scenario. So calling the function with "validator" will just return
 242:      * an array of "type" => "validator" while calling it with an array of scenarios
 243:      * (e.g. $scenario = array("title","validator")) will return an array containing
 244:      * just that data, like: "type" => array("title", "validator").
 245:      * @param mixed $scenario A string or array of the data scenario required
 246:      * @return array An array of information about the field types.
 247:      */
 248:     public static function getFieldTypes($scenario = null){
 249:         $fieldTypes = array(
 250:             'varchar' => array(
 251:                 'title' => Yii::t('admin', 'Single Line Text'),
 252:                 'validator' =>'safe',
 253:                 'columnDefinition' => 'VARCHAR(255)',
 254:                 'phpType' => 'string'
 255:             ),
 256:             'text' => array(
 257:                 'title' => Yii::t('admin', 'Multiple Line Text Area'),
 258:                 'validator' => 'safe',
 259:                 'columnDefinition' => 'TEXT',
 260:                 'phpType' => 'string'
 261:             ),
 262:             'date' =>array(
 263:                 'title'=>Yii::t('admin','Date'),
 264:                 'validator'=>'int',
 265:                 'columnDefinition'=>'BIGINT',
 266:                 'phpType' => 'integer'
 267:             ),
 268:             'dateTime' =>array(
 269:                 'title'=>Yii::t('admin','Date/Time'),
 270:                 'validator'=>'int',
 271:                 'columnDefinition'=>'BIGINT',
 272:                 'phpType' => 'integer'
 273:             ),
 274:             'dropdown'=>array(
 275:                 'title'=>Yii::t('admin','Dropdown'),
 276:                 'validator'=>'safe',
 277:                 'columnDefinition'=>'VARCHAR(255)',
 278:                 'phpType' => 'string'
 279:             ),
 280:             'int'=>array(
 281:                 'title'=>Yii::t('admin','Number'),
 282:                 'validator'=> 'int',
 283:                 'columnDefinition'=>'BIGINT',
 284:                 'phpType' => 'integer'
 285:             ),
 286:             'percentage'=>array(
 287:                 'title'=>Yii::t('admin','Percentage'),
 288:                 'validator' => 'numerical',
 289:                 'columnDefinition' => 'FLOAT',
 290:                 'phpType' => 'double'
 291:             ),
 292:             'email'=>array(
 293:                 'title'=>Yii::t('admin','Email'),
 294:                 'validator'=>'email',
 295:                 'columnDefinition'=>'VARCHAR(255)',
 296:                 'phpType' => 'string'
 297:             ),
 298:             'currency'=>array(
 299:                 'title'=>Yii::t('admin','Currency'),
 300:                 'validator'=>'numerical',
 301:                 'columnDefinition'=>'DECIMAL(18,2)',
 302:                 'phpType' => 'double'
 303:             ),
 304:             'url'=>array(
 305:                 'title'=>Yii::t('admin','URL'),
 306:                 'validator'=>'safe',
 307:                 'columnDefinition'=>'VARCHAR(255)',
 308:                 'phpType' => 'string'
 309:             ),
 310:             'float'=>array(
 311:                 'title'=>Yii::t('admin','Decimal'),
 312:                 'validator'=>'numerical',
 313:                 'columnDefinition'=>'FLOAT',
 314:                 'phpType' => 'double'
 315:             ),
 316:             'boolean'=>array(
 317:                 'title'=>Yii::t('admin','Checkbox'),
 318:                 'validator'=>'boolean',
 319:                 'columnDefinition'=>'BOOLEAN NOT NULL DEFAULT 0',
 320:                 'phpType' => 'boolean'
 321:             ),
 322:             'link'=>array(
 323:                 'title'=>Yii::t('admin','Lookup'),
 324:                 'validator'=>'safe',
 325:                 'columnDefinition'=>'VARCHAR(255)',
 326:                 'phpType' => 'integer'
 327:             ),
 328:             'rating'=>array(
 329:                 'title'=>Yii::t('admin','Rating'),
 330:                 'validator'=>'safe',
 331:                 'columnDefinition'=>'VARCHAR(255)',
 332:                 'phpType' => 'integer'
 333:             ),
 334:             'assignment'=>array(
 335:                 'title'=>Yii::t('admin','Assignment'),
 336:                 'validator'=>'safe',
 337:                 'columnDefinition' => 'VARCHAR(255)',
 338:                 'phpType' => 'string'
 339:             ),
 340:             'visibility'=>array(
 341:                 'title'=>Yii::t('admin','Visibility'),
 342:                 'validator'=>'int',
 343:                 'columnDefinition'=>'INT NOT NULL DEFAULT 1',
 344:                 'phpType' => 'boolean'
 345:             ),
 346:             'timerSum'=>array(
 347:                 'title'=>Yii::t('admin','Action Timer Sum'),
 348:                 'validator'=>'safe',
 349:                 'columnDefinition'=>'INT',
 350:                 'phpType' => 'integer'
 351:             ),
 352:             'phone'=>array(
 353:                 'title'=>Yii::t('admin','Phone Number'),
 354:                 'validator'=>'safe',
 355:                 'columnDefinition'=>'VARCHAR(40)',
 356:                 'phpType' => 'string'
 357:             ),
 358:             'custom'=>array(
 359:                 'title' => Yii::t('admin','Custom'),
 360:                 'validator' => 'safe',
 361:                 'columnDefinition' => 'VARCHAR(255)',
 362:                 'phpType' => 'string'
 363:             ),
 364:         );
 365:         // No scenario, return all data
 366:         if(empty($scenario)){
 367:             return $fieldTypes;
 368:         }else{
 369:             // Scenario is a string, need to convert to array
 370:             if(!is_array($scenario)){
 371:                 $scenario = array($scenario);
 372:             }
 373:             $ret = array();
 374:             // Add the validator information to our return data
 375:             if(in_array('validator', $scenario)){
 376:                 if(count($scenario) == 1){ // Only one scenario, can return a purely associative array
 377:                     foreach($fieldTypes as $fieldType => $data){
 378:                         $ret[$fieldType] = $data['validator'];
 379:                     }
 380:                 }else{ // More than one scenario, need to return each field type as an array
 381:                     foreach($fieldTypes as $fieldType => $data){
 382:                         $ret[$fieldType]['validator']=$data['validator'];
 383:                     }
 384:                 }
 385:             }
 386:             if(in_array('title', $scenario)){
 387:                 if(count($scenario) == 1){
 388:                     foreach($fieldTypes as $fieldType => $data){
 389:                         $ret[$fieldType] = $data['title'];
 390:                     }
 391:                 }else{
 392:                     foreach($fieldTypes as $fieldType => $data){
 393:                         $ret[$fieldType]['title']=$data['title'];
 394:                     }
 395:                 }
 396:             }
 397:             if(in_array('columnDefinition', $scenario)){
 398:                 if(count($scenario) == 1){
 399:                     foreach($fieldTypes as $fieldType => $data){
 400:                         $ret[$fieldType] = $data['columnDefinition'];
 401:                     }
 402:                 }else{
 403:                     foreach($fieldTypes as $fieldType => $data){
 404:                         $ret[$fieldType]['columnDefinition']=$data['columnDefinition'];
 405:                     }
 406:                 }
 407:             }
 408:             return $ret;
 409:         }
 410:     }
 411: 
 412:     /**
 413:      * Finds a contact matching a full name; returns Contacts::name if a match was found, null otherwise.
 414:      * @param string $type
 415:      * @param string $name
 416:      * @return mixed
 417:      */
 418:     public static function getLinkId($type, $name){
 419:         if(strtolower($type) == 'contacts')
 420:             $model = X2Model::model('Contacts')->find('CONCAT(firstName," ",lastName)=:name', array(':name' => $name));
 421:         else
 422:             $model = X2Model::model(ucfirst($type))->findByAttributes(array('name' => $name));
 423:         if(isset($model))
 424:             return $model->name;
 425:         else
 426:             return null;
 427:     }
 428: 
 429:     /**
 430:      * Constructs (if not constructed already) and returns a CHtmlPurifier instance
 431:      *
 432:      * @return CHtmlPurifier
 433:      */
 434:     public static function getPurifier(){
 435:         if(!isset(self::$_purifier)){
 436:             self::$_purifier = new CHtmlPurifier();
 437:             // Set secure default options for HTML purification in X2Engine:
 438:             self::$_purifier->options = array(
 439:                 'HTML.ForbiddenElements' => array(
 440:                     'script', // Obvious reasons (XSS)
 441:                     'form', // Hidden CSRF attempts?
 442:                     'style', // CSS injection tomfoolery
 443:                     'iframe', // Arbitrary location
 444:                     'frame', // Same reason as iframe
 445:                     'link', // Request to arbitrary location w/o user knowledge
 446:                     'video', // No,
 447:                     'audio', // No,
 448:                     'object', // Definitely no.
 449:                 ),
 450:                 'HTML.ForbiddenAttributes' => array(
 451:                     // Spoofing/mocking internal form elements:
 452:                     '*@id',
 453:                     '*@class',
 454:                     '*@name',
 455:                     // The event attributes should be removed automatically by HTMLPurifier by default
 456:                 ),
 457:                 
 458:             );
 459:         }
 460:         return self::$_purifier;
 461:     }
 462: 
 463:     /**
 464:      * Returns the static model of the specified AR class.
 465:      * @return Fields the static model class
 466:      */
 467:     public static function model($className = __CLASS__){
 468:         return parent::model($className);
 469:     }
 470: 
 471:     /**
 472:      * The inverse operation of {@link nameId()}, this splits a uniquely
 473:      * -identifying "nameId" field into name and ID.
 474:      *
 475:      * This function should always return an array with two elements, the first
 476:      * being the name and the second being the ID.
 477:      *
 478:      * @param string $nameId The nameId reference.
 479:      */
 480:     public static function nameAndId($nameId) {
 481:         // The last occurrence should be the delimeter
 482:         $delimPos = strrpos($nameId,Fields::NAMEID_DELIM);
 483: 
 484:         if($delimPos === false) {
 485:             // Delimeter not found
 486:             return array($nameId,null);
 487:         }
 488:         
 489:         if($delimPos >= strlen($nameId)-1) {
 490:             // Delimeter at the end with nothing else, i.e. a name of a
 491:             // non-existent model ending with the delimeter
 492:             return array($nameId,null);
 493:         }
 494: 
 495:         $id = substr($nameId,$delimPos+1);
 496:         $name = substr($nameId,0,$delimPos);
 497: 
 498:         if(!ctype_digit($id)) {
 499:             // Name has the delimeter in it, but does not refer to any record.
 500:             return array($nameId,null);
 501:         } else {
 502:             // Name and ID acquired.
 503:             return array($name,$id);
 504:         }
 505:     }
 506: 
 507:     public static function id ($nameId) {
 508:         list ($name, $id) = self::nameAndId ($nameId);
 509:         return $id;
 510:     }
 511: 
 512:     /**
 513:      * Generates a combination name and id field to uniquely identify the record.
 514:      */
 515:     public static function nameId($name,$id) {
 516:         return $name.self::NAMEID_DELIM.$id;
 517:     }
 518: 
 519:     /**
 520:      * Implodes an array of usernames for multiple assignment fields. This method,
 521:      * if still used anyhwere, could be refactored to use JSON
 522:      * @param mixed $arr Array or string of usernames
 523:      * @return string A properly formatted assignment string
 524:      */
 525:     public static function parseUsers($arr){
 526:         /* filters out dummy option in multiselect element used to separate usernames from group 
 527:            names */
 528:         $str="";
 529:         if(is_array($arr)){
 530:             $arr = array_filter ($arr, function ($a) { return $a !== ''; });
 531:             $str=implode(', ',$arr);
 532:         } else if(is_string($arr))
 533:             $str = $arr;
 534:         return $str;
 535:     }
 536: 
 537:     /**
 538:      * Similar to {@link Fields::parseUsers} but is used in the case where it's an associative
 539:      * array of username => full name and the array keys need to be used to generate
 540:      * our assignment string
 541:      * @param array $arr An array of format username => full name
 542:      * @return string A properly formatted assignment string
 543:      */
 544:     public static function parseUsersTwo($arr){
 545:         $str="";
 546:         if(is_array($arr)){
 547:             $arr=array_keys($arr);
 548:             $str=implode(', ',$arr);
 549:         }
 550: 
 551:         return $str;
 552:     }
 553: 
 554:     public static function searchRelevance() {
 555:         return array('Low' => Yii::t('app', 'Low'), "Medium" => Yii::t('app', "Medium"), "High" => Yii::t('app', "High"));
 556:     }
 557:     
 558:     /**
 559:      * Converts a string into a numeric value.
 560:      *
 561:      * @param string $input The string to convert
 562:      * @param string $type A hint as to the type of input; one of 'int', 'float', 'currency' or 'percentage'
 563:      * @param string $currencySymbol Optional currency symbol to trim off the string before conversion
 564:      * @param string $percentSymbol Optional percent symbol to trim off the string before conversion
 565:      */
 566:     public static function strToNumeric($input, $type = 'float', $curSym = null){
 567:         $sign = 1;
 568:         // Typecasting in the case that it's not a string
 569:         $inType = gettype($input);
 570:         if($inType != 'string'){
 571:             if($type == $inType)
 572:                 return $inType;
 573:             else
 574:                 return ($type == 'int' ? (int) $input : (float) $input);
 575:         }
 576: 
 577:         // Get rid of leading and trailing whitespace:
 578:         $value = trim($input);
 579:         if(strpos($value, '(') === 0) // Parentheses notation
 580:             $sign = -1;
 581:         $posNeg = strpos($value, '-');
 582:         if($posNeg === 0 || $posNeg === strlen($value) - 1) // Minus sign notation
 583:             $sign = -1;
 584: 
 585:         // Strip out currency/percent symbols and digit group separators, but exclude null currency symbols:
 586:         if(!function_exists('stripSymbols')){
 587:             function stripSymbols($s){ return !empty($s); }
 588:         }
 589:         $stripSymbols = array_filter(array_values(Yii::app()->params->supportedCurrencySymbols), 'stripSymbols');
 590: 
 591:         // Strip specified currency symbol
 592:         if (!is_null($curSym))
 593:             $stripSymbols[] = $curSym;
 594:         // Just in case "Other" currency used: include that currency's symbol
 595:         $defaultSym = Yii::app()->getLocale()->getCurrencySymbol(Yii::app()->settings->currency);
 596:         if($defaultSym)
 597:             if(!in_array($defaultSym, $stripSymbols))
 598:                 $stripSymbols[] = $defaultSym;
 599:         $stripSymbols[] = '%';
 600:         $grpSym = Yii::app()->getLocale()->getNumberSymbol('group');
 601:         if(!empty($grpSym) && $type != 'percentage')
 602:             $stripSymbols[] = $grpSym;
 603:         $value = strtr($value, array_fill_keys($stripSymbols, ''));
 604: 
 605:         // Trim away negative symbols and any remaining whitespace:
 606:         $value = trim($value, "-() ");
 607:         $converted = strtr($value, array_flip(get_html_translation_table(HTML_ENTITIES, ENT_QUOTES)));
 608:         $value = trim($converted, chr(0xC2).chr(0xA0));
 609: 
 610:         /* 
 611:         Setting numeric field to '' fails in MYSQL strict mode and gets coerced to 0 in non-strict 
 612:         mode. null gets used instead to allow empty number field values.
 613:         */
 614:         if($value === null || $value === '') 
 615:             return null; 
 616:         else if(!in_array($type, array('int', 'currency', 'float', 'percentage')))
 617:             return $value; // Unrecognized type
 618:         else if(!preg_match('/^([\d\.,]+)e?[\+\-]?\d*$/', $value))
 619:             return $input; // Unreadable input
 620: 
 621:         $value = str_replace(Yii::app()->getLocale()->getNumberSymbol('decimal'), '.', $value);
 622:         if(in_array($type, array('float', 'currency', 'percentage'))){
 623:             return ((float) $value) * $sign;
 624:         }else if($type == 'int'){
 625:             return ((int) $value) * $sign;
 626:         } else
 627:             return $value;
 628:     }
 629: 
 630:     /**
 631:      * Table modification is performed before field update since field should not be saved if
 632:      * column data type cannot be updated
 633:      * TODO: place column modification in a transaction with field update
 634:      */
 635:     public function beforeSave () {
 636:         $valid = parent::beforeSave ();
 637:         if ($valid && $this->_typeChanged) {
 638:             $table = Yii::app()->db->schema->tables[$this->myTableName];
 639:             $existing = array_key_exists($this->fieldName, $table->columns) && 
 640:                 $table->columns[$this->fieldName] instanceof CDbColumnSchema;
 641:             if($existing){ 
 642:                 $valid = $this->modifyColumn();
 643:             }
 644:         }
 645:         return $valid;
 646:     }
 647:     
 648:     /**
 649:      * Perform the creation of a new database column.
 650:      *
 651:      * The extra work in this method is skipped over in the "newModule" scenario
 652:      * because the database schema altering commands to set up columns are
 653:      * performed separately in that case.
 654:      *
 655:      * @return type
 656:      */
 657:     public function afterSave(){
 658:         // Does the column already exist?
 659:         $table = Yii::app()->db->schema->tables[$this->myTableName];
 660:         $existing = array_key_exists($this->fieldName, $table->columns) && 
 661:             $table->columns[$this->fieldName] instanceof CDbColumnSchema;
 662: 
 663:         if(!$existing){ // Going to create the column.
 664:             $this->createColumn();
 665:         } 
 666:         if($this->keyType != 'PRI' && $this->keyType != 'FIX'){
 667:             // The key for this column is not primary/hard-coded (managed by
 668:             // X2Engine developers, and cannot be user-modified), so it can
 669:             // be allowed to change.
 670:             if($this->keyType != null){
 671:                 $this->dropIndex();
 672:                 $this->createIndex($this->keyType === 'UNI');
 673:             }else{
 674:                 $this->dropIndex();
 675:             }
 676:         }
 677:         if ($this->isNewRecord) {
 678:             // A new fields permissions default to read/write for all roles
 679:             $dataProvider = new CActiveDataProvider('Roles');
 680:             foreach ($dataProvider->getData() as $role) {
 681:                 $permission = new RoleToPermission();
 682:                 $permission->roleId = $role->id;
 683:                 $permission->fieldId = $this->id;
 684:                 $permission->permission = 2;
 685:                 $permission->save();
 686:             }
 687:         }
 688:         
 689:         return parent::afterSave();
 690:     }
 691: 
 692:     public function afterDelete() {
 693:         $this->dropColumn();
 694:         return parent::afterDelete();
 695:     }
 696: 
 697:     /**
 698:      * @return array customized attribute labels (name=>label)
 699:      */
 700:     public function attributeLabels(){
 701:         return array(
 702:             'id' => Yii::t('admin', 'ID'),
 703:             'modelName' => Yii::t('admin', 'Model Name'),
 704:             'fieldName' => Yii::t('admin', 'Field Name'),
 705:             'attributeLabel' => Yii::t('admin', 'Attribute Label'),
 706:             'custom' => Yii::t('admin', 'Custom'),
 707:             'modified' => Yii::t('admin', 'Modified'),
 708:             'readOnly' => Yii::t('admin', 'Read Only'),
 709:             'required' => Yii::t('admin', "Required"),
 710:             'searchable' => Yii::t('admin', "Searchable"),
 711:             'relevance' => Yii::t('admin', 'Search Relevance'),
 712:             'uniqueConstraint' => Yii::t('admin', 'Unique'),
 713:             'defaultValue' => Yii::t('admin', 'Default Value'),
 714:             'keyType' => Yii::t('admin','Key Type'),
 715:             'data' => Yii::t('admin','Template'),
 716:         );
 717:     }
 718: 
 719:     /**
 720:      * Adjust the field name accordingly
 721:      *
 722:      * Under certain circumstances, the field name will be given a prefix "c_"
 723:      * to avoid name collisions with new default fields added in updates.
 724:      *
 725:      * @return bool
 726:      */
 727:     public function beforeValidate() {
 728:         if($this->isNewRecord){
 729:             if(strpos($this->fieldName,'c_') !== 0 && $this->custom && $this->scenario != 'test'){
 730:                 // This is a safeguard against fields that end up having
 731:                 // identical names to fields added later in updates.
 732:                 $this->fieldName = "c_{$this->fieldName}";
 733:             }
 734:         }
 735:         return parent::beforeValidate();
 736:     }
 737: 
 738:     /**
 739:      * Creates a column for the new field model.
 740:      */
 741:     public function createColumn(){
 742:         // Get the column definition.
 743:         $fieldType = $this->type;
 744:         $columnDefinitions = Fields::getFieldTypes('columnDefinition');
 745:         if(isset($columnDefinitions[$fieldType])){
 746:             $fieldType = $columnDefinitions[$fieldType];
 747:         }else{
 748:             $fieldType = 'VARCHAR(250)';
 749:         }
 750:         $sql = "ALTER TABLE `{$this->myTableName}` ADD COLUMN `{$this->fieldName}` $fieldType";
 751:         try{
 752:             Yii::app()->db->createCommand($sql)->execute();
 753:         }catch(CDbException $e){
 754:             $this->delete(); // If the SQL failed, remove the x2_fields record of it to prevent issues.
 755:         }
 756:     }
 757: 
 758:     /**
 759:      * Modifies the data type of an existing column 
 760:      */
 761:     public function modifyColumn () {
 762:         // Get the column definition.
 763:         $fieldType = $this->type;
 764:         $columnDefinitions = Fields::getFieldTypes('columnDefinition');
 765: 
 766:         if(isset($columnDefinitions[$fieldType])){
 767:             $fieldType = $columnDefinitions[$fieldType];
 768:         }else{
 769:             $fieldType = 'VARCHAR(250)';
 770:         }
 771: 
 772:         //Yii::app()->db->createCommand('set sql_mode=STRICT_ALL_TABLES;')->execute();
 773:         $sql = "ALTER TABLE `{$this->myTableName}` MODIFY COLUMN `{$this->fieldName}` $fieldType";
 774:         try{
 775:             Yii::app()->db->createCommand($sql)->execute();
 776:         }catch(CDbException $e){
 777:             $this->addError ('type', $e->getMessage ());
 778:             return false;
 779:         }
 780:         return true;
 781:     }
 782: 
 783:     /**
 784:      * Creates an index on the column associated with the current field record.
 785:      */
 786:     public function createIndex($unique = false){
 787:         $indexType = $unique ? "UNIQUE" : "INDEX";
 788:         $sql = "ALTER TABLE `{$this->myTableName}` ADD $indexType(`{$this->fieldName}`)";
 789:         try{
 790:             Yii::app()->db->createCommand($sql)->execute();
 791:             return true;
 792:         }catch(CDbException $e){
 793:             // Fail quietly until there's a need to take additional action after
 794:             // this happens
 795:             return false;
 796:         }
 797:     }
 798: 
 799:     /**
 800:      * Deletes the table column associated with the field record.
 801:      */
 802:     public function dropColumn(){
 803:         $sql = "ALTER TABLE `{$this->myTableName}` DROP COLUMN `{$this->fieldName}`";
 804:         try{
 805:             Yii::app()->db->createCommand($sql)->execute();
 806:             return true;
 807:         }catch(CDbException $e){
 808:             // Fail quietly until there's a need to take additional action after
 809:             // this happens
 810:             return false;
 811:         }
 812:     }
 813: 
 814:     /**
 815:      * Drops the index on the column associated with the current field record.
 816:      */
 817:     public function dropIndex(){
 818:         $sql = "ALTER TABLE `{$this->myTableName}` DROP INDEX `{$this->fieldName}`";
 819:         try{
 820:             Yii::app()->db->createCommand($sql)->execute();
 821:             return true;
 822:         }catch(CDbException $e){
 823:             // Fail quietly until there's a need to take additional action after
 824:             // this happens
 825:             return false;
 826:         }
 827:     }
 828: 
 829:     /**
 830:      * Obtains the value for {@link tableName}
 831:      * @return type
 832:      */
 833:     public function getMyTableName() {
 834:         if(!isset($this->_myTableName)) {
 835:             $this->_myTableName = X2Model::model($this->modelName)->tableName();
 836:         }
 837:         return $this->_myTableName;
 838:     }
 839: 
 840:     /**
 841:      * Validator for ensuring an identifier does not include MySQL reserved words
 842:      * or X2Engine reserved words
 843:      */
 844:     public function nonReserved($attribute,$params = array()) {
 845:         if($this->isNewRecord){
 846:             $dataFiles = array();
 847:             $reservedWords = array();
 848:             $dataFiles[] = implode(DIRECTORY_SEPARATOR, array(Yii::app()->basePath, 'data', 'mysqlReservedWords.php'));
 849:             $dataFiles[] = implode(DIRECTORY_SEPARATOR, array(Yii::app()->basePath, 'data', 'modelReservedWords.php'));
 850:             foreach($dataFiles as $path){
 851:                 if(file_exists($path)){
 852:                     $reservedWords = array_merge($reservedWords, require($path));
 853:                 }
 854:             }
 855:             if(in_array($this->$attribute, $reservedWords)){
 856:                 $this->addError($attribute, Yii::t('admin', 'This field is a MySQL or X2Engine reserved word.  Choose a different field name.'));
 857:             }
 858:         }
 859:     }
 860: 
 861:     /**
 862:      * Parses a value for table insertion using X2Fields rules
 863:      * @param mixed $value
 864:      * @param bool $filter If true, replace HTML special characters (prevents markup injection)
 865:      * @return mixed the parsed value
 866:      */
 867:     public function parseValue($value, $filter = false){
 868:         if(in_array($this->type, array('int', 'float', 'currency', 'percentage'))){
 869:             return self::strToNumeric($value, $this->type);
 870:         }
 871:         switch($this->type){
 872:             case 'assignment':
 873:                 return ($this->linkType === 'multiple') ? self::parseUsers($value) : $value;
 874: 
 875:             case 'date':
 876:             case 'dateTime':
 877:                 if(is_numeric ((string) $value))  // must already be a timestamp
 878:                     return $value;
 879:                 $value = $this->type === 'dateTime' ? Formatter::parseDateTime($value) : Formatter::parseDate($value);
 880:                 return $value === false ? null : $value;
 881: 
 882:             case 'link':
 883:                 if(empty($value) || empty($this->linkType)){
 884:                     return $value;
 885:                 }
 886:                 list($name, $id) = self::nameAndId($value);
 887:                 if(ctype_digit((string) $id)){
 888:                     // Already formatted as a proper reference. Check for existence of the record.
 889:                     $linkedModel = X2Model::model($this->linkType)->findByAttributes(array('nameId' => $value));
 890:                     // Return the plain text name if the link is broken; otherwise,
 891:                     // given how the record exists, return the value.
 892:                     return empty($linkedModel) ? $name : $value;
 893:                 }else if(ctype_digit($value)){
 894:                     // User manually entered the ID, i.e. in an API call
 895:                     $link = Yii::app()->db->createCommand()
 896:                             ->select('nameId')
 897:                             ->from(X2Model::model($this->linkType)->tableName())
 898:                             ->where('id=?', array($value))
 899:                             ->queryScalar();
 900:                 }else{
 901:                     // Look up model's unique nameId by its name:
 902:                     $link = Yii::app()->db->createCommand()
 903:                             ->select('nameId')
 904:                             ->from(X2Model::model($this->linkType)->tableName())
 905:                             ->where('name=?', array($name))
 906:                             ->queryScalar();
 907:                 }
 908:                 return $link === false ? $name : $link;
 909:             case 'boolean':
 910:                 return (bool) $value;
 911:             case 'text':
 912:                 return self::getPurifier()->purify($value);
 913:             case 'dropdown':
 914:                 return is_array($value) ? CJSON::encode($value) : $value;
 915:             default:
 916:                 return $filter ? CHtml::encode($value) : $value;
 917:         }
 918:     }
 919: 
 920:     /**
 921:      * @return array relational rules.
 922:      */
 923:     public function relations(){
 924:         return array();
 925:     }
 926: 
 927:     /**
 928:      * Retrieves a list of models based on the current search/filter conditions.
 929:      * @return CActiveDataProvider the data provider that can return the models based on the search/filter conditions.
 930:      */
 931:     public function search(){
 932:         // Warning: Please modify the following code to remove attributes that
 933:         // should not be searched.
 934: 
 935:         $criteria = new CDbCriteria;
 936: 
 937:         $criteria->compare('id', $this->id);
 938:         $criteria->compare('modelName', $this->modelName, true);
 939:         $criteria->compare('fieldName', $this->fieldName, true);
 940:         $criteria->compare('attributeLabel', $this->attributeLabel, true);
 941:         $criteria->compare('custom', $this->custom);
 942:         $criteria->compare('modified', $this->modified);
 943:         $criteria->compare('readOnly', $this->readOnly);
 944: 
 945:         return new CActiveDataProvider(get_class($this), array(
 946:                     'criteria' => $criteria,
 947:                 ));
 948:     }
 949: 
 950:     /**
 951:      * @return string the associated database table name
 952:      */
 953:     public function tableName(){
 954:         return 'x2_fields';
 955:     }
 956: 
 957:     /**
 958:      * Validator that prevents adding a unique constraint to a field without
 959:      * also making it required.
 960:      */
 961:     public function requiredUnique($attribute, $params = array()) {
 962:         if($this->$attribute == 'UNI' && !$this->uniqueConstraint) {
 963:             $this->addError($attribute,Yii::t('admin','You cannot add a unique constraint unless you also make the field unique and required.'));
 964:         }
 965:     }
 966: 
 967:     /**
 968:      * Check that the combination of model and field name will not conflict
 969:      * with any existing one.
 970:      *
 971:      * @param type $attribute
 972:      * @param type $params
 973:      */
 974:     public function uniqueFieldName($attribute, $params = array()) {
 975:         $fields = self::model()->findAllByAttributes(array($attribute=>$this->$attribute,'modelName'=>$this->modelName));
 976:         if(count($fields) > 0) {
 977:             // There can and should only be one.
 978:             $existingField = reset($fields);
 979:             if($this->id != $existingField->id) {
 980:                 // This is not the field! Saving will produce a database
 981:                 // cardinality violation error due to the unique constraint on
 982:                 // model name and field name.
 983:                 $this->addError($attribute,Yii::t('admin','A field in the specified data model with that name already exists.'));
 984:             }
 985:         }
 986:     }
 987: 
 988:     /**
 989:      * Check that the default value is appropriate given the type of the field.
 990:      * 
 991:      * @param string $attribute
 992:      * @param array $params
 993:      */
 994:     public function validDefault($attribute,$params = array()) {
 995:         if($this->fieldName == '')
 996:             return; // Nothing is possible without the field name. Validation will fail for it accordingly.
 997: 
 998:         // Use the amorphous model for "proxy" validation, and use a "dummy"
 999:         // field model (because we'll need to set the name differently to make
1000:         // things easier on ourselves, given how user input for field name might
1001:         // not be appropriate for a property name)
1002:         $dummyModel = new AmorphousModel();
1003:         $dummyField = new Fields;
1004:         foreach($this->attributes as $name=>$value) {
1005:             $dummyField->$name = $value;
1006:         }
1007:         $dummyField->fieldName = 'customized_field';
1008:         $dummyModel->scenario = 'insert';
1009:         $dummyModel->addField($dummyField,'customized_field');
1010:         $dummyModel->setAttribute('customized_field',$this->$attribute);
1011:         $dummyModel->validate();
1012:         if($dummyModel->hasErrors('customized_field')) {
1013:             foreach($dummyModel->errors['customized_field'] as $error) {
1014:                 $this->addError($attribute, str_replace($dummyField->attributeLabel, $dummyField->getAttributeLabel($attribute), $error));
1015:             }
1016:         }
1017:     }
1018: 
1019: 
1020:     /**
1021:      * Alter/purify the input for the custom data field.
1022:      *
1023:      * @param string $attribute
1024:      * @param array $params
1025:      */
1026:     public function validCustom($attribute,$params = array()) {
1027:         if($this->type == 'custom') {
1028:             if($this->linkType == 'formula') {
1029:                 $this->$attribute = trim($this->$attribute);
1030:                 if(strpos($this->$attribute,'=')!==0) {
1031:                     $this->$attribute = '='.$this->$attribute;
1032:                 }
1033:             } else if($this->linkType == 'display') {
1034:                $this->$attribute = self::getPurifier()->purify($this->$attribute);
1035:             }
1036:         }
1037:     }
1038: 
1039:     /**
1040:      * Retrieve associated dropdown, if it exists
1041:      * @return null|Dropdowns
1042:      */
1043:     private $_dropdown;
1044:     public function getDropdown () {
1045:         if ($this->type !== 'dropdown') return null;
1046:         if (!isset ($this->_dropdown)) {
1047:             $this->_dropdown = Dropdowns::model ()->findByPk ($this->linkType);
1048:         }
1049:         return $this->_dropdown;
1050:     }
1051: 
1052:     public static function getFieldsOfModelsWithFieldLevelPermissions () {
1053:         $fields = Fields::model()
1054:             ->findAll(array('order' => 'modelName ASC'));
1055:         $filtered = array ();
1056:         foreach ($fields as $field) {
1057:             $modelClass = $field->modelName;
1058:             if (class_exists ($modelClass) && 
1059:                 $modelClass::model ()->supportsFieldLevelPermissions) { 
1060: 
1061:                 $filtered[] = $field;
1062:             }
1063:         }
1064:         return $filtered;
1065:     }
1066: 
1067: }
1068: 
X2CRM Documentation API documentation generated by ApiGen 2.8.0