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

  • AccountsGridViewProfileWidget
  • ActionMenu
  • ActionsGridViewProfileWidget
  • ActionsQuickCreateRelationshipBehavior
  • ActiveDateRangeInput
  • ApplicationConfigBehavior
  • Attachments
  • ChatBox
  • CommonControllerBehavior
  • ContactMapInlineTags
  • ContactsGridViewProfileWidget
  • CronForm
  • CSaveRelationsBehavior
  • DateRangeInputsWidget
  • DocsGridViewProfileWidget
  • DocViewer
  • DocViewerProfileWidget
  • EButtonColumnWithClearFilters
  • EmailDeliveryBehavior
  • EmailProgressControl
  • EncryptedFieldsBehavior
  • EventsChartProfileWidget
  • FileUploader
  • FontPickerInput
  • Formatter
  • FormView
  • GridViewWidget
  • History
  • IframeWidget
  • ImportExportBehavior
  • InlineActionForm
  • InlineEmailAction
  • InlineEmailForm
  • InlineEmailModelBehavior
  • InlineQuotes
  • JSONEmbeddedModelFieldsBehavior
  • JSONFieldsDefaultValuesBehavior
  • LeadRoutingBehavior
  • LeftWidget
  • LoginThemeHelper
  • LoginThemeHelperBase
  • MarketingGridViewProfileWidget
  • MediaBox
  • MessageBox
  • MobileFormatter
  • MobileFormLayoutRenderer
  • MobileLayoutRenderer
  • MobileLoginThemeHelper
  • MobileViewLayoutRenderer
  • ModelFileUploader
  • NewWebLeadsGridViewProfileWidget
  • NormalizedJSONFieldsBehavior
  • NoteBox
  • OnlineUsers
  • OpportunitiesGridViewProfileWidget
  • Panel
  • ProfileDashboardManager
  • ProfileGridViewWidget
  • ProfileLayoutEditor
  • ProfilesGridViewProfileWidget
  • Publisher
  • PublisherActionTab
  • PublisherCalendarEventTab
  • PublisherCallTab
  • PublisherCommentTab
  • PublisherEventTab
  • PublisherSmallCalendarEventTab
  • PublisherTab
  • PublisherTimeTab
  • QuickContact
  • QuickCreateRelationshipBehavior
  • QuotesGridViewProfileWidget
  • RecordAliasesWidget
  • RecordViewLayoutManager
  • RecordViewWidgetManager
  • RememberPagination
  • Reminders
  • ResponseBehavior
  • ResponsiveHtml
  • SearchIndexBehavior
  • ServicesGridViewProfileWidget
  • SmallCalendar
  • SmartActiveDataProvider
  • SmartDataProviderBehavior
  • SmartSort
  • SocialForm
  • SortableWidgetManager
  • SortableWidgets
  • TagBehavior
  • TagCloud
  • TemplatesGridViewProfileWidget
  • TimeZone
  • TopContacts
  • TopSites
  • TransformedFieldStorageBehavior
  • TranslationLogger
  • TwitterFeed
  • TwoColumnSortableWidgetManager
  • UpdaterBehavior
  • UpdatesForm
  • UserIdentity
  • UsersChartProfileWidget
  • WorkflowBehavior
  • X2ActiveGridView
  • X2ActiveGridViewForSortableWidgets
  • X2AssetManager
  • X2AuthManager
  • X2ChangeLogBehavior
  • X2ClientScript
  • X2Color
  • X2DateUtil
  • X2FixtureManager
  • X2FlowFormatter
  • X2GridView
  • X2GridViewBase
  • X2GridViewForSortableWidgets
  • X2GridViewSortableWidgetsBehavior
  • X2LeadsGridViewProfileWidget
  • X2LinkableBehavior
  • X2ListView
  • X2PillBox
  • X2ProgressBar
  • X2SmartSearchModelBehavior
  • X2TimestampBehavior
  • X2TranslationBehavior
  • X2UrlRule
  • X2WebModule
  • X2Widget
  • X2WidgetList
  • 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: 
  39: /**
  40:  * Behavior for dealing with data files directly on the server while avoiding
  41:  * directory traversal and publicly visible files.
  42:  * 
  43:  * @package application.components
  44:  * @author Demitri Morgan <demitri@x2engine.com>
  45:  * @author Raymond Colebaugh <raymond@x2engine.com>
  46:  */
  47: class ImportExportBehavior extends CBehavior {
  48: 
  49:     private $importRelations = array();
  50:     private $createdLinkedModels = array();
  51:     private $modelContainer = array(
  52:         'Tags' => array(),
  53:         'ActionText' => array(),
  54:     );
  55: 
  56:     /**
  57:      * Sends the file to the web client upon request
  58:      * @param type $file
  59:      * @return false if send file failed (if successful, script is terminated)
  60:      */
  61:     public function sendFile($file, $deleteAfterSend=false){
  62:         if(!preg_match('/(\.\.|\/)/', $file)){
  63:             $file = Yii::app()->file->set($this->safePath($file));
  64:             return $file->send(false, false, $deleteAfterSend);
  65:         }
  66:         return false;
  67:     }
  68: 
  69:     /**
  70:      * Returns a file path that is within the protected folder, to protect data
  71:      * @param type $filename
  72:      * @return type
  73:      */
  74:     public function safePath($filename = 'data.csv'){
  75:         return implode(DIRECTORY_SEPARATOR, array(
  76:             Yii::app()->basePath,
  77:             'data',
  78:             $filename
  79:         ));
  80:     }
  81: 
  82:     /**
  83:      * Retrieve the current CSV delimeter for import/export
  84:      * @return string Import CSV delimeter
  85:      */
  86:     public function getImportDelimeter() {
  87:         if (array_key_exists ('importDelimeter', $_SESSION) &&
  88:             strlen ($_SESSION['importDelimeter']) === 1)
  89:                 return $_SESSION['importDelimeter'];
  90:         else
  91:             return ',';
  92:     }
  93: 
  94:     /**
  95:      * Retrieve the current CSV enclosure for import/export
  96:      * @return string Import CSV enclosure
  97:      */
  98:     public function getImportEnclosure() {
  99:         if (array_key_exists ('importEnclosure', $_SESSION) &&
 100:             strlen ($_SESSION['importEnclosure']) === 1)
 101:                 return $_SESSION['importEnclosure'];
 102:         else
 103:             return '"';
 104:     }
 105: 
 106:     /**
 107:      * Create tag records for each of the specified tags
 108:      * @param string $modelName The model class being imported
 109:      * @param string $tagsField Comma separated list of tags to generate
 110:      * @return array of Tag attributes
 111:      */
 112:     protected function importTags($modelName, $tagsField) {
 113:         $tagAttributes = array();
 114:         if (empty($tagsField)) return array();
 115: 
 116:         // Read in comma separated list of tags and store Tag attributes
 117:         $tags = explode(',', $tagsField);
 118:         foreach ($tags as $tagName) {
 119:             if (empty($tagName)) continue;
 120:             $tag = new Tags;
 121:             $tag->tag = $tagName;
 122:             $tag->type = $modelName;
 123:             $tag->timestamp = time();
 124:             $tag->taggedBy = Yii::app()->getSuName();
 125:             $tagAttributes[] = $tag->attributes;
 126:         }
 127:         $this->modelContainer['Tags'][] = $tagAttributes;
 128:     }
 129: 
 130:     /**
 131:      * Helper function to return the next importId
 132:      * @return int Next import ID
 133:      */
 134:     private function getNextImportId() {
 135:         $criteria = new CDbCriteria;
 136:         $criteria->order = "importId DESC";
 137:         $criteria->limit = 1;
 138:         $import = Imports::model()->find($criteria);
 139: 
 140:         // Figure out which import this is so we can set up the Imports models
 141:         // for this import.
 142:         if (isset($import)) {
 143:             $importId = $import->importId + 1;
 144:         } else {
 145:             $importId = 1;
 146:         }
 147:         return $importId;
 148:     }
 149: 
 150:     /**
 151:      * List available import maps from a directory,
 152:      * optionally of type $model
 153:      * @param string $model Name of the model to load import maps
 154:      * @return array Available import maps, with the filenames as
 155:      * keys, and import mapping names (product/version) as values
 156:      */
 157:     protected function availableImportMaps($model = null) {
 158:         $maps = array();
 159:         if ($model === "X2Leads")
 160:             $model = "Leads";
 161:         $modelName = (isset($model)) ? lcfirst($model) : '.*';
 162:         $importMapDir = "importMaps";
 163:         $files = scandir($this->safePath($importMapDir));
 164:         foreach ($files as $file) {
 165:             $filename = basename($file);
 166:             // Filenames are in the form "app-model.json"
 167:             if (!preg_match('/^.*-' . $modelName . '\.json$/', $filename))
 168:                 continue;
 169:             $mapping = file_get_contents($this->safePath($importMapDir . DIRECTORY_SEPARATOR . $file));
 170:             $mapping = json_decode($mapping, true);
 171:             $maps[$file] = $mapping['name'];
 172:         }
 173:         return $maps;
 174:     }
 175: 
 176:     /**
 177:      * Load an import map from the map directory
 178:      * @param string $filename of the import map
 179:      * @return array Import map
 180:      */
 181:     protected function loadImportMap($filename) {
 182:         if (empty($filename))
 183:             return null;
 184:         $importMapDir = "importMaps";
 185:         $map = file_get_contents($this->safePath($importMapDir . DIRECTORY_SEPARATOR . $filename));
 186:         $map = json_decode($map, true);
 187:         return $map;
 188:     }
 189: 
 190:     /**
 191:      * Parse the given keys and attributes to ensure required fields are
 192:      * mapped and new fields are to be created. The verified map will be
 193:      * stored in the 'importMap' key for the $_SESSION super global.
 194:      * @param string $model name of the model
 195:      * @param array $keys
 196:      * @param array $attributes
 197:      * @param boolean $createFields whether or not to create new fields
 198:      */
 199:     protected function verifyImportMap($model, $keys, $attributes, $createFields = false) {
 200:         if (!empty($keys) && !empty($attributes)) {
 201:             // New import map is the provided data
 202:             $importMap = array_combine($keys, $attributes);
 203:             $conflictingFields = array();
 204:             $failedFields = array();
 205: 
 206:             // To keep track of fields that were mapped multiple times
 207:             $mappedValues = array();
 208:             $multiMappings = array();
 209: 
 210:             foreach ($importMap as $key => &$value) {
 211:                 if (in_array($value, $mappedValues) && !empty($value) && !in_array($value, $multiMappings)) {
 212:                     // This attribute is mapped to two different fields in X2
 213:                     $multiMappings[] = $value;
 214:                 } else if ($value !== 'createNew') {
 215:                     $mappedValues[] = $value;
 216:                 }
 217:                 // Loop through and figure out if we need to create new fields
 218:                 $origKey = $key;
 219:                 $key = Formatter::deCamelCase($key);
 220:                 $key = preg_replace('/\[W|_]/', ' ', $key);
 221:                 $key = mb_convert_case($key, MB_CASE_TITLE, "UTF-8");
 222:                 $key = preg_replace('/\W/', '', $key);
 223:                 if ($value == 'createNew' && !$createFields) {
 224:                     $importMap[$origKey] = 'c_' . strtolower($key);
 225:                     $fieldLookup = Fields::model()->findByAttributes(array(
 226:                         'modelName' => $model,
 227:                         'fieldName' => $key
 228:                     ));
 229:                     if (isset($fieldLookup)) {
 230:                         $conflictingFields[] = $key;
 231:                         continue;
 232:                     } else {
 233:                         $customFieldLookup = Fields::model()->findByAttributes(array(
 234:                             'modelName' => $model,
 235:                             'fieldName' => $importMap[$origKey]
 236:                         ));
 237:                         if (!$customFieldLookup instanceof Fields) {
 238:                             // Create a custom field if one doesn't exist already
 239:                             $columnName = strtolower($key);
 240:                             $field = new Fields;
 241:                             $field->modelName = $model;
 242:                             $field->type = "varchar";
 243:                             $field->fieldName = $columnName;
 244:                             $field->required = 0;
 245:                             $field->searchable = 1;
 246:                             $field->relevance = "Medium";
 247:                             $field->custom = 1;
 248:                             $field->modified = 1;
 249:                             $field->attributeLabel = $field->generateAttributeLabel($key);
 250:                             if (!$field->save()) {
 251:                                 $failedFields[] = $key;
 252:                             }
 253:                         }
 254:                     }
 255:                 }
 256:             }
 257: 
 258:             // Check for required attributes that are missing
 259:             $requiredAttrs = Yii::app()->db->createCommand()
 260:                     ->select('fieldName, attributeLabel')
 261:                     ->from('x2_fields')
 262:                     ->where('modelName = :model AND required = 1', array(
 263:                         ':model' => str_replace(' ', '', $model)))
 264:                     ->query();
 265:             $missingAttrs = array();
 266:             foreach ($requiredAttrs as $attr) {
 267:                 // Skip visibility, it can be set for them
 268:                 if (strtolower($attr['fieldName']) == 'visibility')
 269:                     continue;
 270:                 // Ignore missing first/last name, this can be inferred from full name
 271:                 if ($model === 'Contacts' && ($attr['fieldName'] === 'firstName' || $attr['fieldName'] === 'lastName') && in_array('name', array_values($importMap)))
 272:                     continue;
 273:                 // Otherwise, a required field is missing and should be reported to the user
 274:                 if (!in_array($attr['fieldName'], array_values($importMap)))
 275:                     $missingAttrs[] = $attr['attributeLabel'];
 276:             }
 277:             if (!empty($conflictingFields)) {
 278:                 $result = array("2", implode(', ', $conflictingFields));
 279:             } else if (!empty($missingAttrs)) {
 280:                 $result = array("3", implode(', ', $missingAttrs));
 281:             } else if (!empty($failedFields)) {
 282:                 $result = array("1", implode(', ', $failedFields));
 283:             } else if (!empty($multiMappings)) {
 284:                 $result = array("4", implode(', ', $multiMappings));
 285:             } else {
 286:                 $result = array("0");
 287:             }
 288:             $_SESSION['importMap'] = $importMap;
 289:         } else {
 290:             $result = array("0");
 291:             $_SESSION['importMap'] = array();
 292:         }
 293:         return $result;
 294:     }
 295: 
 296:     /**
 297:      * Insert placeholders for unmapped fields to ensure the import
 298:      * map contains all possible fields
 299:      * @param array $map Import map
 300:      * @param array $fields Metadata fields from CSV
 301:      * @returns array Normalized import map
 302:      */
 303:     protected function normalizeImportMap($map, $fields) {
 304:         foreach ($fields as $field) {
 305:             if (!array_key_exists ($field, $map)) {
 306:                 $map[$field] = null;
 307:             }
 308:         }
 309:         return $map;
 310:     }
 311: 
 312:     /**
 313:      * Calculates the number of lines in a CSV file for import
 314:      * Warning: This must load and traverse the length of the file
 315:      */
 316:     protected function calculateCsvLength($csvfile) {
 317:         $lineCount = null;
 318:         ini_set('auto_detect_line_endings', 1); // Account for Mac based CSVs if possible
 319:         $fp = fopen ($csvfile, 'r');
 320:         if ($fp) {
 321:             $lineCount = 0;
 322:             while (true) {
 323:                 $arr = fgetcsv ($fp);
 324:                 if ($arr !== false && !is_null ($arr)) {
 325:                     if ($arr === array (null)) {
 326:                         continue;
 327:                     } else {
 328:                         $lineCount++;
 329:                     }
 330:                 } else {
 331:                     break;
 332:                 }
 333:             }
 334:         }
 335:         return $lineCount - 1;
 336:     }
 337: 
 338:     /**
 339:      * Remove any lone \r characters
 340:      * @param string $csvfile Path to CSV file
 341:      */
 342:     protected function fixCsvLineEndings($csvFile) {
 343:         $text = file_get_contents($csvFile);
 344:         $replacement = preg_replace('/\r([^\n])/m', "\r\n\\1", $text);
 345:         file_put_contents($csvFile, $replacement);
 346:     }
 347: 
 348:     /**
 349:      * Read metadata from the CSV and initialize session variables
 350:      * @param resource $fp File pointer to CSV
 351:      * @return array CSV Metadata and X2Attributes
 352:      */
 353:     protected function initializeModelImporter($fp) {
 354:         $meta = fgetcsv($fp);
 355:         if ($meta === false)
 356:             throw new Exception('There was an error parsing the models of the CSV.');
 357:         while ("" === end($meta)) {
 358:             array_pop($meta); // Remove empty data from the end of the metadata
 359:         }
 360:         if (count($meta) == 1) { // This was from a global export CSV, the first row is the version
 361:             $version = $meta[0]; // Remove it and repeat the above process
 362:             $meta = fgetcsv($fp);
 363:             if ($meta === false)
 364:                 throw new Exception('There was an error parsing the contents of the CSV.');
 365:             while ("" === end($meta)) {
 366:                 array_pop($meta);
 367:             }
 368:         }
 369:         if (empty($meta)) {
 370:             $_SESSION['errors'] = Yii::t('admin', "Empty CSV or no metadata specified");
 371:             $this->redirect('importModels');
 372:         }
 373: 
 374:         // Add the import failures column to the failed records meta
 375:         $failedContacts = fopen($this->safePath('failedRecords.csv'), 'w+');
 376:         $failedHeader = $meta;
 377:         if (end($meta) != 'X2_Import_Failures')
 378:             $failedHeader[] = 'X2_Import_Failures';
 379:         else
 380:             array_pop ($meta);
 381:         fputcsv($failedContacts, $failedHeader);
 382:         fclose($failedContacts);
 383: 
 384:         // Set our file offset for importing Contacts
 385:         $_SESSION['offset'] = ftell($fp);
 386:         $_SESSION['metaData'] = $meta;
 387: 
 388:         // Ensure the selected model hasn't been lost
 389:         if (array_key_exists('model', $_SESSION))
 390:             $modelName = str_replace(' ', '', $_SESSION['model']);
 391:         else
 392:             $this->errorMessage(Yii::t('admin', "Session information has been lost. Please retry your import."
 393:             ));
 394:         $x2attributes = array_keys(X2Model::model($modelName)->attributes);
 395:         while ("" === end($x2attributes)) {
 396:             array_pop($x2attributes);
 397:         }
 398:         if ($modelName === 'Actions') {
 399:             // add Action.description to attributes so that it is automatically mapped
 400:             $x2attributes[] = 'actionDescription';
 401:         }
 402:         // Initialize session data
 403:         $_SESSION['importMap'] = array();
 404:         $_SESSION['imported'] = 0;
 405:         $_SESSION['failed'] = 0;
 406:         $_SESSION['created'] = 0;
 407:         $_SESSION['fields'] = X2Model::model($modelName)->getFields(true);
 408:         $_SESSION['x2attributes'] = $x2attributes;
 409:         $_SESSION['mapName'] = "";
 410:         $_SESSION['importId'] = $this->getNextImportId();
 411: 
 412:         return array($meta, $x2attributes);
 413:     }
 414: 
 415:     /**
 416:      * The goal of this function is to attempt to map meta into a series of
 417:      * Contact attributes, which it will do via string comparison on the Contact
 418:      * attribute names, the Contact attribute labels and a pattern match.
 419:      * @param array $attributes Contact model's attributes
 420:      * @param array $meta Provided metadata in the CSV
 421:      */
 422:     protected function createImportMap($attributes, $meta) {
 423:         // We need to do data processing on attributes, first copy & preserve
 424:         $originalAttributes = $attributes;
 425:         // Easier to just do both strtolower than worry about case insensitive comparison
 426:         $attributes = array_map('strtolower', $attributes);
 427:         $processedMeta = array_map('strtolower', $meta);
 428:         // Remove any non word characters or underscores
 429:         $processedMeta = preg_replace('/[\W|_]/', '', $processedMeta);
 430:         // Now do the same with Contact attribute labels
 431:         $labels = X2Model::model(str_replace(' ', '', $_SESSION['model']))->attributeLabels();
 432:         $labels = array_map('strtolower', $labels);
 433:         $labels = preg_replace('/[\W|_]/', '', $labels);
 434:         /*
 435:          * At the end of this loop, any fields we are able to suggest a mapping
 436:          * for are automatically populated into an array in $_SESSION with
 437:          * the format:
 438:          *
 439:          * $_SESSION['importMap'][<x2_attribute>] = <metadata_attribute>
 440:          */
 441:         foreach ($meta as $metaVal) {
 442:             // Ignore the import failures column
 443:             if ($metaVal == 'X2_Import_Failures')
 444:                 continue;
 445:             if ($metaVal === 'tags') {
 446:                 $_SESSION['importMap']['applyTags'] = $metaVal;
 447:                 continue;
 448:             }
 449:             // Same reason as $originalAttributes
 450:             $originalMetaVal = $metaVal;
 451:             $metaVal = strtolower(preg_replace('/[\W|_]/', '', $metaVal));
 452:             /*
 453:              * First check if we're lucky and maybe the processed metadata value
 454:              * matches a contact attribute directly. Things like first_name
 455:              * would be converted to firstname and so match perfectly. If we
 456:              * find a match here, assume it is the most correct possibility
 457:              * and add it to our session import map
 458:              */
 459:             if (in_array($metaVal, $attributes)) {
 460:                 $attrKey = array_search($metaVal, $attributes);
 461:                 $_SESSION['importMap'][$originalAttributes[$attrKey]] = $originalMetaVal;
 462:                 /*
 463:                  * The next possibility is that the metadata value matches an attribute
 464:                  * label perfectly. This is more common for a field like company
 465:                  * where the label is "Account" but it's our second best bet for
 466:                  * figuring out the metadata.
 467:                  */
 468:             } elseif (in_array($metaVal, $labels)) {
 469:                 $attrKey = array_search($metaVal, $labels);
 470:                 $_SESSION['importMap'][$attrKey] = $originalMetaVal;
 471:                 /*
 472:                  * The third best option is that there is a partial word match
 473:                  * on the metadata value. However, we don't want to do a simple
 474:                  * preg search as that may give weird results, we want to limit
 475:                  * with a word boundary to see if the first part matches. This isn't
 476:                  * ideal but it fixes some edge cases.
 477:                  */
 478:             } elseif (count(preg_grep("/\b$metaVal/i", $attributes)) > 0) {
 479:                 $keys = array_keys(preg_grep("/\b$metaVal/i", $attributes));
 480:                 $attrKey = $keys[0];
 481:                 if (!isset($_SESSION['importMap'][$originalMetaVal]))
 482:                     $_SESSION['importMap'][$originalAttributes[$attrKey]] = $originalMetaVal;
 483:                 /*
 484:                  * Finally, check if there is a partial word match on the attribute
 485:                  * label as opposed to the field name
 486:                  */
 487:             }elseif (count(preg_grep("/\b$metaVal/i", $labels)) > 0) {
 488:                 $keys = array_keys(preg_grep("/\b$metaVal/i", $labels));
 489:                 $attrKey = $keys[0];
 490:                 if (!isset($_SESSION['importMap'][$originalMetaVal]))
 491:                     $_SESSION['importMap'][$attrKey] = $originalMetaVal;
 492:             }
 493:         }
 494:         /*
 495:          * Finally, we want to do a quick reverse operation in case there
 496:          * were any fields that weren't mapped correctly based on the directionality
 497:          * of the word boundary. For example, if we were checking "zipcode"
 498:          * against "zip" this would not be a match because the pattern "zipcode"
 499:          * is longer and will fail. However, "zip" will match into "zipcode"
 500:          * and should be accounted for. This loop goes through the x2 attributes
 501:          * instead of the metadata to ensure bidirectionality.
 502:          */
 503:         foreach ($originalAttributes as $attribute) {
 504:             if (in_array($attribute, $processedMeta)) {
 505:                 $metaKey = array_search($attribute, $processedMeta);
 506:                 $_SESSION['importMap'][$attribute] = $meta[$metaKey];
 507:             } elseif (count(preg_grep("/\b$attribute/i", $processedMeta)) > 0) {
 508:                 $matches = preg_grep("/\b$attribute/i", $processedMeta);
 509:                 $metaKeys = array_keys($matches);
 510:                 $metaValue = $meta[$metaKeys[0]];
 511:                 if (!isset($_SESSION['importMap'][$attribute]))
 512:                     $_SESSION['importMap'][$attribute] = $metaValue;
 513:             }
 514:         }
 515:     }
 516: 
 517:     /**
 518:      * Append an empty placeholder for action texts, or set the attribute of the last action
 519:      * text in the container if attributes are specified
 520:      */
 521:     protected function setCurrentActionText($attributes = null) {
 522:         if (is_null($attributes))
 523:             $this->modelContainer['ActionText'][] = array();
 524:         else {
 525:             $containerId = count($this->modelContainer['ActionText']) - 1;
 526:             $this->modelContainer['ActionText'][$containerId] = $attributes;
 527:         }
 528:     }
 529: 
 530:     /**
 531:      * The import assumes we have human readable data in the CSV and will thus need to convert. This
 532:      * method converts link, date, and dateTime fields to the appropriate machine friendly data.
 533:      * @param string $modelName The model class being imported
 534:      * @param X2Model $model The currently importing model record
 535:      * @param string $fieldName Field to set
 536:      * @param string $importAttribute Value to set field
 537:      * @returns X2Model $model
 538:      */
 539:     protected function importRecordAttribute($modelName, X2Model $model, $fieldName, $importAttribute) {
 540: 
 541:         $fieldRecord = Fields::model()->findByAttributes(array(
 542:             'modelName' => $modelName,
 543:             'fieldName' => $fieldName,
 544:         ));
 545: 
 546:         // Skip setting the attribute if it has already been set or if the entry from
 547:         // the CSV is empty.
 548:         if (empty($importAttribute) && ($importAttribute !== 0 && $importAttribute !== '0')) {
 549:             return $model;
 550:         }
 551:         if ($fieldName === 'actionDescription' && $modelName === 'Actions') {
 552:             $text = new ActionText;
 553:             $text->text = $importAttribute;
 554:             if (isset($model->id))
 555:                 $text->actionId = $model->id;
 556:             $this->setCurrentActionText ($text->attributes);
 557:             return $model;
 558:         }
 559: 
 560:         // ensure the provided id is valid
 561:         if ((strtolower($fieldName) === 'id') && (!preg_match('/^\d+$/', $importAttribute) || $importAttribute >= 4294967295)) {
 562:             $model->id = $importAttribute;
 563:             $model->addError ('id', Yii::t('importexport', "ID '$importAttribute' is not valid."));
 564:             return $model;
 565:         }
 566: 
 567:         switch ($fieldRecord->type) {
 568:             case "link":
 569:                 $model = $this->importRecordLinkAttribute($modelName, $model, $fieldRecord, $importAttribute);
 570:                 break;
 571:             case "dateTime":
 572:             case "date":
 573:                 if (Formatter::parseDateTime ($importAttribute) !== false)
 574:                     $model->$fieldName = Formatter::parseDateTime ($importAttribute);
 575:                 break;
 576:             case "visibility":
 577:                 switch ($importAttribute) {
 578:                     case 'Private':
 579:                         $model->$fieldName = 0;
 580:                         break;
 581:                     case 'Public':
 582:                         $model->$fieldName = 1;
 583:                         break;
 584:                     case 'User\'s Groups':
 585:                         $model->$fieldName = 2;
 586:                         break;
 587:                     default:
 588:                         $model->$fieldName = $importAttribute;
 589:                 }
 590:                 break;
 591:             default:
 592:                 $model->$fieldName = $importAttribute;
 593:         }
 594:         return $model;
 595:     }
 596: 
 597:     /**
 598:      * Handle setting link type fields and create linked records if specified
 599:      * @param string $modelName The model class being imported
 600:      * @param X2Model $model The currently importing model record
 601:      * @param Fields $fieldRecord Field to set
 602:      * @param string $importAttribute Value to set field
 603:      * @returns X2Model $model
 604:      */
 605:     protected function importRecordLinkAttribute($modelName, X2Model $model, Fields $fieldRecord, $importAttribute) {
 606:         $fieldName = $fieldRecord->fieldName;
 607:         $className = ucfirst($fieldRecord->linkType);
 608:         if (isset($_SESSION['linkMatchMap']) && !empty($_SESSION['linkMatchMap'][$fieldName])) {
 609:             $linkMatchAttribute = $_SESSION['linkMatchMap'][$fieldName];
 610:         }
 611: 
 612:         if (ctype_digit($importAttribute) && !isset($linkMatchAttribute)) {
 613:             $lookup = X2Model::model($className)->findByPk($importAttribute);
 614:             $model->$fieldName = $importAttribute;
 615:             if (!empty($lookup)) {
 616:                 // Create a link to the existing record
 617:                 $model->$fieldName = $lookup->nameId;
 618:                 $relationship = new Relationships;
 619:                 $relationship->firstType = $modelName;
 620:                 $relationship->secondType = $className;
 621:                 $relationship->secondId = $importAttribute;
 622:                 $this->importRelations[count($this->importRelations) - 1][] = $relationship->attributes;
 623:             }
 624:         } else {
 625:             $lookupAttr = isset($linkMatchAttribute) ? $linkMatchAttribute : 'name';
 626:             $lookup = X2Model::model($className)->findByAttributes(array($lookupAttr => $importAttribute));
 627:             if (isset($lookup)) {
 628:                 $model->$fieldName = $lookup->nameId;
 629:                 $relationship = new Relationships;
 630:                 $relationship->firstType = $modelName;
 631:                 $relationship->secondType = $className;
 632:                 $relationship->secondId = $lookup->id;
 633:                 $this->importRelations[count($this->importRelations) - 1][] = $relationship->attributes;
 634:             } elseif (isset($_SESSION['createRecords']) && $_SESSION['createRecords'] == 1 &&
 635:                     !($modelName === 'BugReports' && $fieldRecord->linkType === 'BugReports')) {
 636:                 // Skip creating related bug reports; the created report wouldn't hold any useful info.
 637:                 $className = ucfirst($fieldRecord->linkType);
 638:                 if (class_exists($className)) {
 639:                     $lookup = new $className;
 640:                     if ($_SESSION['skipActivityFeed'] === 1)
 641:                         $lookup->createEvent = false;
 642:                     $lookup->name = $importAttribute;
 643:                     if ($className === 'Contacts' || $className === 'X2Leads') {
 644:                         self::fixupImportedContactName($lookup);
 645:                     }
 646:                     if ($lookup->hasAttribute('visibility'))
 647:                         $lookup->visibility = 1;
 648:                     if ($lookup->hasAttribute('description'))
 649:                         $lookup->description = "Generated by " . $modelName . " import.";
 650:                     if ($lookup->hasAttribute('createDate'))
 651:                         $lookup->createDate = time();
 652: 
 653:                     if (!array_key_exists($className, $this->modelContainer))
 654:                         $this->modelContainer[$className] = array();
 655: 
 656:                     // Ensure this linked record has not already been accounted for
 657:                     $createNewLinkedRecord = true;
 658:                     if ($model->hasAttribute('name')) {
 659:                         $model->$fieldName = $lookup->name;
 660:                     } else {
 661:                         $model->$fieldName = $importAttribute;
 662:                     }
 663: 
 664:                     foreach ($this->modelContainer[$className] as $record) {
 665:                         if ($record['name'] === $lookup->name) {
 666:                             $createNewLinkedRecord = false;
 667:                             break;
 668:                         }
 669:                     }
 670: 
 671:                     if ($createNewLinkedRecord) {
 672:                         $this->modelContainer[$className][] = $lookup->attributes;
 673:                         if (isset($_SESSION['created'][$className])) {
 674:                             $_SESSION['created'][$className] ++;
 675:                         } else {
 676:                             $_SESSION['created'][$className] = 1;
 677:                         }
 678:                     }
 679:                     $relationship = new Relationships;
 680:                     $relationship->firstType = $modelName;
 681:                     $relationship->secondType = $className;
 682:                     $this->importRelations[count($this->importRelations) - 1][] = $relationship->attributes;
 683:                     $this->createdLinkedModels[] = $model->$fieldName;
 684:                 }
 685:             } else {
 686:                 $model->$fieldName = $importAttribute;
 687:             }
 688:         }
 689:         return $model;
 690:     }
 691: 
 692:     /**
 693:      * Helper method to help out the user in the special case where a Contact's full name
 694:      * is set, but first and last name aren't, or vice versa.
 695:      * @param $model
 696:      * @returns X2Model $model
 697:      */
 698:     protected static function fixupImportedContactName($model) {
 699:         if (!empty($model->name) || !empty($model->firstName) || !empty($model->lastName)) {
 700:             $nameFormat = Yii::app()->settings->contactNameFormat;
 701:             switch ($nameFormat) {
 702:                 case 'lastName, firstName':
 703:                     if (empty ($model->name))
 704:                         $model->name = $model->lastName . ", " . $model->firstName;
 705:                     $decomposePattern = '/^(?P<last>\w+), ?(?P<first>\w+)$/';
 706:                     break;
 707:                 case 'firstName lastName':
 708:                 default:
 709:                     if (empty ($model->name))
 710:                         $model->name = $model->firstName . " " . $model->lastName;
 711:                     $decomposePattern = '/^(?P<first>\w+) (?P<last>\w+)$/';
 712:                     break;
 713:             }
 714:             preg_match ($decomposePattern, $model->name, $matches);
 715:             if (array_key_exists ('first', $matches) && array_key_exists ('last', $matches)) {
 716:                 $model->firstName = $matches['first'];
 717:                 $model->lastName = $matches['last'];
 718:             }
 719:         }
 720:         return $model;
 721:     }
 722: 
 723:     /**
 724:      * This method is used after importing a records attributes to perform extra tasks, such as
 725:      * assigning lead routing, setting visibility, and reconstructing Action associations.
 726:      * @param string $modelName Name of the model being imported
 727:      * @param X2Model $model Current model to import
 728:      * @returns X2Model $model
 729:      */
 730:     protected function fixupImportedAttributes($modelName, X2Model $model) {
 731:         if ($modelName === 'Contacts' || $modelName === 'X2Leads')
 732:             $model = self::fixupImportedContactName($model);
 733: 
 734:         if ($modelName === 'Actions' && isset($model->associationType))
 735:             $model = $this->reconstructImportedActionAssoc($model);
 736: 
 737:         // Set visibility to Public by default if unset by import
 738:         if ($model->hasAttribute('visibility') && is_null($model->visibility))
 739:             $model->visibility = 1;
 740:         // If date fields were provided, do not create new values for them
 741:         if (!empty($model->createDate) || !empty($model->lastUpdated) ||
 742:                 !empty($model->lastActivity)) {
 743:             $now = time();
 744:             if (empty($model->createDate))
 745:                 $model->createDate = $now;
 746:             if (empty($model->lastUpdated))
 747:                 $model->lastUpdated = $now;
 748:             if ($model->hasAttribute('lastActivity') && empty($model->lastActivity))
 749:                 $model->lastActivity = $now;
 750:         }
 751:         if ($_SESSION['leadRouting'] == 1) {
 752:             $assignee = $this->getNextAssignee();
 753:             if ($assignee == "Anyone")
 754:                 $assignee = "";
 755:             $model->assignedTo = $assignee;
 756:         }
 757:         // Loop through our override and set the manual data
 758:         foreach ($_SESSION['override'] as $attr => $val) {
 759:             $model->$attr = $val;
 760:         }
 761: 
 762:         return $model;
 763:     }
 764: 
 765:     /**
 766:      * Remove a record with the same ID, save the model attributes in the container, and increment
 767:      * the count of imported records
 768:      * @param X2Model $model
 769:      * @param array $importedIds Array of ids from imported models
 770:      */
 771:     protected function saveImportedModel(X2Model $model, $modelName, $importedIds) {
 772:         if (!empty($model->id)) {
 773:             $lookup = X2Model::model(str_replace(' ', '', $modelName))->findByPk($model->id);
 774:             if (isset($lookup)) {
 775:                 Relationships::model()->deleteAllByAttributes(array(
 776:                     'firstType' => $modelName,
 777:                     'firstId' => $lookup->id)
 778:                 );
 779:                 Relationships::model()->deleteAllByAttributes(array(
 780:                     'secondType' => $modelName,
 781:                     'secondId' => $lookup->id)
 782:                 );
 783:                 $lookup->delete();
 784:                 unset($lookup);
 785:             }
 786:         }
 787:         // Save our model & create the import records and 
 788:         // relationships. Passing $validate=false to CActiveRecord.save
 789:         // because validation has already happened at this point
 790:         $this->modelContainer[$modelName][] = $model->attributes;
 791:         $_SESSION['imported'] ++;
 792:         $importedIds[] = $model->id;
 793:         return $importedIds;
 794:     }
 795: 
 796:     /**
 797:      * Execute a multiple insert command
 798:      * @param string $modelType Child of X2Model
 799:      * @param array $models Array of model attributes to create
 800:      * @return int Last inserted id
 801:      */
 802:     protected function insertMultipleRecords($modelType, $models) {
 803:         if (empty($models))
 804:             return null;
 805:         $tableName = X2Model::model($modelType)->tableName();
 806:         Yii::app()->db->schema->commandBuilder
 807:                 ->createMultipleInsertCommand($tableName, $models)
 808:                 ->execute();
 809:         $lastInsertId = Yii::app()->db->schema->commandBuilder
 810:                 ->getLastInsertId($tableName);
 811:         return $lastInsertId;
 812:     }
 813: 
 814:     /**
 815:      * This grabs 5 sample records from the CSV to get an example of what
 816:      * the data looks like.
 817:      * @return array Sample records
 818:      */
 819:     protected function prepareImportSampleRecords($meta, $fp) {
 820:         $sampleRecords = array();
 821:         for ($i = 0; $i < 5; $i++) {
 822:             if ($sampleRecord = fgetcsv($fp, 0, $_SESSION['delimeter'], $_SESSION['enclosure'])) {
 823:                 if(count($sampleRecord) > count($meta)){
 824:                     $sampleRecord = array_slice($sampleRecord, 0, count($meta));
 825:                 }
 826:                 if (count($sampleRecord) < count($meta)) {
 827:                     $sampleRecord = array_pad($sampleRecord, count($meta), null);
 828:                 }
 829:                 if (!empty($meta)) {
 830:                     $sampleRecord = array_combine($meta, $sampleRecord);
 831:                     $sampleRecords[] = $sampleRecord;
 832:                 }
 833:             }
 834:         }
 835:         return $sampleRecords;
 836:     }
 837: 
 838:     /**
 839:      * Handle reconstructing and validating Action associations
 840:      * @param Actions $model Action to reconstruct association
 841:      * @returns Actions $model
 842:      */
 843:     protected function reconstructImportedActionAssoc(Actions $model) {
 844:         $exportableModules = array_merge(
 845:                 array_keys(Modules::getExportableModules()), array('None')
 846:         );
 847:         $exportableModules = array_map('lcfirst', $exportableModules);
 848:         $model->associationType = lcfirst($model->associationType);
 849:         if (!in_array($model->associationType, $exportableModules)) {
 850:             // Invalid association type
 851:             $model->addError('associationType', Yii::t('admin', 'Unknown associationType.'));
 852:         } else if (isset($model->associationId) && $model->associationId !== '0') {
 853:             $associatedModel = X2Model::model($model->associationType)
 854:                     ->findByPk($model->associationId);
 855:             if ($associatedModel)
 856:                 $model->associationName = $associatedModel->nameId;
 857:         } else if (!isset($model->associationId) && isset($model->associationName)) {
 858:             // Retrieve associationId
 859:             $staticAssociationModel = X2Model::model($model->associationType);
 860:             if ($staticAssociationModel->hasAttribute('name') &&
 861:                     !ctype_digit($model->associationName)) {
 862:                 $associationModelParams = array('name' => $model->associationName);
 863:             } else {
 864:                 $associationModelParams = array('id' => $model->associationName);
 865:             }
 866:             $lookup = $staticAssociationModel->findByAttributes($associationModelParams);
 867:             if (isset($lookup)) {
 868:                 $model->associationId = $lookup->id;
 869:                 $model->associationName = $lookup->hasAttribute('nameId') ?
 870:                         $lookup->nameId : $lookup->name;
 871:             }
 872:         }
 873:         return $model;
 874:     }
 875: 
 876:     /**
 877:      * Finalize this batch of records by performing a mass insert, handling accounting,
 878:      * updating nameIds, and rendering the JSON response
 879:      * @param string $modelName Name of the model class being imported
 880:      * @param boolean $mappedId Whether the primary model's ID has been mapped: this alters
 881:      *    the result of lastInsertId
 882:      * @param boolean $finished Whether this batch has reached the end of the CSV
 883:      */
 884:     protected function finishImportBatch($modelName, $mappedId, $finished = false) {
 885:         if (!array_key_exists ($modelName, $this->modelContainer) || empty($this->modelContainer[$modelName])) {
 886:             $this->importerResponse ($finished);
 887:             return;
 888:         }
 889: 
 890:         // Keep track of the lastInsertId for each type
 891:         $lastInsertedIds = array();
 892: 
 893:         // First insert the records being imported
 894:         $lastInsertedIds[$modelName] = $this->insertMultipleRecords(
 895:                 $modelName, $this->modelContainer[$modelName]
 896:         );
 897:         $primaryModelCount = count($this->modelContainer[$modelName]);
 898:         // If id was mapped, then lastInsertId would actually be the last in the sequence.
 899:         // Otherwise, lastInsertId would be the first
 900:         if ($mappedId) {
 901:             $primaryIdRange = range(
 902:                     $lastInsertedIds[$modelName] - $primaryModelCount + 1, $lastInsertedIds[$modelName]
 903:             );
 904:         } else {
 905:             $primaryIdRange = range(
 906:                     $lastInsertedIds[$modelName], $lastInsertedIds[$modelName] + $primaryModelCount - 1
 907:             );
 908:         }
 909:         $this->handleImportAccounting($this->modelContainer[$modelName], $modelName, $lastInsertedIds, $mappedId);
 910:         $this->massUpdateImportedNameIds($primaryIdRange, $modelName);
 911: 
 912:         // Now create remaining auxiliary records
 913:         foreach ($this->modelContainer as $type => $models) {
 914:             if ($type === $modelName) // these were already processed
 915:                 continue;
 916: 
 917:             if ($modelName === 'Actions' && $type === 'ActionText') {
 918:                 // set the actionIds and insert ActionText records
 919:                 $firstInsertedId = $primaryIdRange[0];
 920:                 $actionTexts = array();
 921:                 foreach ($models as $i => $model) {
 922:                     if (empty($model))
 923:                         continue;
 924:                     if (!isset($model['actionId']))
 925:                         $model['actionId'] = $firstInsertedId + $i;
 926:                     $actionTexts[] = $model;
 927:                 }
 928:                 $this->insertMultipleRecords ('ActionText', $actionTexts);
 929:             } else if ($type === 'Tags') {
 930:                 // Associate each of the tags with the respective imported model
 931:                 $firstInsertedId = $primaryIdRange[0];
 932:                 $tags = array();
 933:                 foreach ($models as $i => $tagModels) {
 934:                     if (empty($tagModels))
 935:                         continue;
 936:                     foreach ($tagModels as $tag) {
 937:                         $tag['itemId'] = $firstInsertedId + $i;
 938:                         $tags[] = $tag;
 939:                     }
 940:                 }
 941:                 $this->insertMultipleRecords ('Tags', $tags);
 942:             } else {
 943:                 // otherwise handle the records normally
 944:                 $lastInsertedIds[$type] = $this->insertMultipleRecords($type, $models);
 945:                 $this->handleImportAccounting($models, $type, $lastInsertedIds);
 946:                 $this->fixupLinkFields($modelName, $type, $primaryIdRange);
 947:                 // related records won't have ID set; therefore, lastInsertId would have
 948:                 // returned the first record in a sequence
 949:                 $idRange = range(
 950:                         $lastInsertedIds[$type], $lastInsertedIds[$type] + count($models) - 1
 951:                 );
 952:                 $this->massUpdateImportedNameIds ($idRange, $type);
 953:                 $this->triggerImportedRecords ($idRange, $type);
 954:             }
 955:         }
 956: 
 957:         $this->establishImportRelationships ($primaryIdRange[0], $mappedId);
 958:         $this->triggerImportedRecords ($primaryIdRange, $modelName);
 959:         $this->importerResponse ($finished);
 960:     }
 961: 
 962:     /**
 963:      * Populate the nameId field since auto-populating fields is
 964:      * disabled and it is far more efficient to do it in a single query
 965:      * @param array $importedIds List of record ids
 966:      * @param string $type Model name
 967:      */
 968:     protected function massUpdateImportedNameIds($importedIds, $type) {
 969:         $hasNameId = Fields::model()->findByAttributes(array(
 970:             'fieldName' => 'nameId',
 971:             'modelName' => $type,
 972:         ));
 973:         if ($hasNameId)
 974:             X2Model::massUpdateNameId($type, $importedIds);
 975:     }
 976: 
 977:     /**
 978:      * Trigger the X2Workflow RecordCreateTrigger on the imported models
 979:      * @param array $importedIds List of record ids
 980:      * @param string $type Model name
 981:      */
 982:     protected function triggerImportedRecords($importedIds, $type) {
 983:         foreach ($importedIds as $id) {
 984:             $model = X2Model::model ($type)->findByPk ($id);
 985:             X2Flow::trigger('RecordCreateTrigger', array('model'=>$model));
 986:         }
 987:     }
 988: 
 989:     /**
 990:      * Render a JSON encoded response for the importer JS to handle
 991:      */
 992:     private function importerResponse ($finished) {
 993:         $finished = !isset ($finished) ? false : $finished;
 994:         echo json_encode(array(
 995:             ($finished ? '1' : '0'),
 996:             $_SESSION['imported'],
 997:             $_SESSION['failed'],
 998:             json_encode($_SESSION['created']),
 999:         ));
1000:     }
1001: 
1002:     /**
1003:      * Create additional records related to the import, including the requested Tags, comment
1004:      * Actions, Import records, Events, and Relationships
1005:      * @param array $models Array of arrays of model attributes
1006:      * @param string $modelName Name of the model being imported
1007:      * @param array $lastInsertedIds The last MySQL IDs that were created, indexed by model type
1008:      * @param boolean $mappedId Whether ID was mapped: this affects lastInsertId's behavior
1009:      */
1010:     protected function handleImportAccounting($models, $modelName, $lastInsertedIds, $mappedId = false) {
1011:         if (count($models) === 0)
1012:             return;
1013:         $now = time();
1014:         $editingUsername = Yii::app()->user->name;
1015:         $auxModelContainer = array(
1016:             'Imports' => array(),
1017:             'Actions' => array(),
1018:             'Events' => array(),
1019:             'Notification' => array(),
1020:             'Tags' => array(),
1021:         );
1022:         if ($mappedId)
1023:             $firstNewId = $lastInsertedIds[$modelName] - count($models) + 1;
1024:         else
1025:             $firstNewId = $lastInsertedIds[$modelName];
1026: 
1027:         for ($i = 0; $i < count($models); $i++) {
1028:             $record = $models[$i];
1029:             if ($mappedId) {
1030:                 $modelId = $models[$i]['id'];
1031:             } else {
1032:                 $modelId = $i + $firstNewId;
1033:             }
1034:             // Create a event for the imported record, and create a notification for the assigned
1035:             // user if one exists, since X2ChangelogBehavior will not be triggered with
1036:             // createMultipleInsertCommand()
1037:             if ($_SESSION['skipActivityFeed'] !== 1) {
1038:                 $event = new Events;
1039:                 $event->visibility = array_key_exists('visibility', $record) ?
1040:                         $record['visibility'] : 1;
1041:                 $event->associationType = $modelName;
1042:                 $event->associationId = $modelId;
1043:                 $event->timestamp = $now;
1044:                 $event->user = $editingUsername;
1045:                 $event->type = 'record_create';
1046:                 $auxModelContainer['Events'][] = $event->attributes;
1047:             }
1048:             if (array_key_exists('assignedTo', $record) && !empty($record['assignedTo']) &&
1049:                     $record['assignedTo'] != $editingUsername && $record['assignedTo'] != 'Anyone') {
1050:                 $notif = new Notification;
1051:                 $notif->user = $record['assignedTo'];
1052:                 $notif->createdBy = $editingUsername;
1053:                 $notif->createDate = $now;
1054:                 $notif->type = 'create';
1055:                 $notif->modelType = $modelName;
1056:                 $notif->modelId = $modelId;
1057:                 $auxModelContainer['Notification'][] = $notif->attributes;
1058:             }
1059: 
1060:             // Add all listed tags
1061:             foreach ($_SESSION['tags'] as $tag) {
1062:                 $tagModel = new Tags;
1063:                 $tagModel->taggedBy = 'Import';
1064:                 $tagModel->timestamp = $now;
1065:                 $tagModel->type = $modelName;
1066:                 $tagModel->itemId = $modelId;
1067:                 $tagModel->tag = $tag;
1068:                 $tagModel->itemName = $modelName;
1069:                 $auxModelContainer['Tags'][] = $tagModel->attributes;
1070:             }
1071:             // Log a comment if one was requested
1072:             if (!empty($_SESSION['comment'])) {
1073:                 $action = new Actions;
1074:                 $action->associationType = lcfirst(str_replace(' ', '', $modelName));
1075:                 $action->associationId = $modelId;
1076:                 $action->createDate = $now;
1077:                 $action->updatedBy = Yii::app()->user->getName();
1078:                 $action->lastUpdated = $now;
1079:                 $action->complete = "Yes";
1080:                 $action->completeDate = $now;
1081:                 $action->completedBy = Yii::app()->user->getName();
1082:                 $action->type = "note";
1083:                 $action->visibility = 1;
1084:                 $action->reminder = "No";
1085:                 $action->priority = 1; // Set priority to Low
1086:                 $auxModelContainer['Actions'][] = $action->attributes;
1087:             }
1088: 
1089:             $importLink = new Imports;
1090:             $importLink->modelType = $modelName;
1091:             $importLink->modelId = $modelId;
1092:             $importLink->importId = $_SESSION['importId'];
1093:             $importLink->timestamp = $now;
1094:             $auxModelContainer['Imports'][] = $importLink->attributes;
1095:         }
1096: 
1097:         foreach ($auxModelContainer as $type => $records) {
1098:             if (empty($records))
1099:                 continue;
1100:             $lastInsertId = $this->insertMultipleRecords($type, $records);
1101:             if ($type === 'Actions') {
1102:                 // Create ActionText and Import records for the comment Actions that were created
1103:                 if (empty($records))
1104:                     continue;
1105:                 $actionImportRecords = array();
1106:                 $actionTextRecords = array();
1107:                 $actionIdRange = range($lastInsertId, $lastInsertId + count($records) - 1);
1108:                 foreach ($actionIdRange as $i) {
1109:                     $importLink = new Imports;
1110:                     $importLink->modelType = "Actions";
1111:                     $importLink->modelId = $i;
1112:                     $importLink->importId = $_SESSION['importId'];
1113:                     $importLink->timestamp = $now;
1114:                     $actionImportRecords[] = $importLink->attributes;
1115: 
1116:                     $actionText = new ActionText;
1117:                     $actionText->actionId = $i;
1118:                     $actionText->text = $_SESSION['comment'];
1119:                     $actionTextRecords[] = $actionText->attributes;
1120:                 }
1121:                 $this->insertMultipleRecords('Imports', $actionImportRecords);
1122:                 $this->insertMultipleRecords('ActionText', $actionTextRecords);
1123:             }
1124:         }
1125:     }
1126: 
1127:     /**
1128:      * Process the link-type fields to set nameId
1129:      * @param int $count The number of primary models being imported
1130:      * @param string $modelName The primary model being imported
1131:      * @param string $type The model of the linked record
1132:      * @param array $lastInsertedIds Array of last inserted IDs, indexed by model name
1133:      */
1134:     protected function fixupLinkFields($modelName, $type, $primaryIdRange) {
1135:         $linkTypeFields = Yii::app()->db->createCommand()
1136:                         ->select('fieldName')
1137:                         ->from('x2_fields')
1138:                         ->where('type = "link" AND modelName = :modelName AND linkType = :linkType', array(
1139:                             ':modelName' => $modelName,
1140:                             ':linkType' => $type,
1141:                         ))->queryColumn();
1142:         $primaryTable = X2Model::model($modelName)->tableName();
1143:         foreach ($linkTypeFields as $field) {
1144:             // update each link type field
1145:             $staticModel = X2Model::model($type);
1146:             if (!$staticModel->hasAttribute('name'))
1147:                 continue;
1148:             $secondaryTable = $staticModel->tableName();
1149: 
1150:             $sql = 'UPDATE `' . $primaryTable . '` a JOIN `' . $secondaryTable . '` b ' .
1151:                     'ON a.' . $field . ' = b.name ' .
1152:                     'SET a.`' . $field . '` = CONCAT(b.name, \'' . Fields::NAMEID_DELIM . '\', b.id) ' .
1153:                     'WHERE a.id in (' . implode(',', $primaryIdRange) . ')';
1154:             Yii::app()->db->createCommand($sql)->execute();
1155:         }
1156:     }
1157: 
1158:     /**
1159:      * Create relationships records for the linked models
1160:      * @param int $firstNewId The first inserted id
1161:      * @param boolean $mappedId Whether or not ID was a mapped field
1162:      */
1163:     protected function establishImportRelationships($firstNewId, $mappedId = false) {
1164:         $validRelationships = array();
1165: 
1166:         foreach ($this->importRelations as $i => $modelsRelationships) {
1167:             $modelId = $i + $firstNewId;
1168:             if (empty($modelsRelationships)) // skip placeholders
1169:                 continue;
1170:             foreach ($modelsRelationships as $relationship) {
1171:                 $relationship['firstId'] = $modelId;
1172:                 if (empty($relationship['secondId'])) {
1173:                     $model = X2Model::model($relationship['firstType'])
1174:                             ->findByPk($modelId);
1175:                     $linkedStaticModel = X2Model::model($relationship['secondType']);
1176:                     if (!$model)
1177:                         continue;
1178:                     $fields = Yii::app()->db->createCommand()
1179:                                     ->select('fieldName')
1180:                                     ->from('x2_fields')
1181:                                     ->where('type = \'link\' AND modelName = :firstType AND ' .
1182:                                             'linkType = :secondType', array(
1183:                                         ':firstType' => $relationship['firstType'],
1184:                                         ':secondType' => $relationship['secondType'],
1185:                                     ))->queryColumn();
1186:                     foreach ($fields as $field) {
1187:                         // Check for relationships to new linked models for each link type field
1188:                         if (empty($model->$field)) // skip fields that weren't set
1189:                             continue;
1190:                         $attr = $linkedStaticModel->hasAttribute('nameId') ? 'nameId' : 'name';
1191:                         $linkedId = Yii::app()->db->createCommand()
1192:                                         ->select('id')
1193:                                         ->from($linkedStaticModel->tableName())
1194:                                         ->where($attr . ' = :reference', array(
1195:                                             ':reference' => $model->$field,
1196:                                         ))->queryScalar();
1197:                         if (!$linkedId)
1198:                             continue;
1199:                         $relationship['secondId'] = $linkedId;
1200:                     }
1201:                 }
1202:                 if (!empty($relationship['secondId']))
1203:                     $validRelationships[] = $relationship;
1204:             }
1205:         }
1206:         $this->insertMultipleRecords('Relationships', $validRelationships);
1207:     }
1208: 
1209:     /**
1210:      * Save the failed record into a CSV with validation errors
1211:      * @param string $modelName
1212:      * @param X2Model $model
1213:      * @param array $csvLine
1214:      * @param array $metadata
1215:      */
1216:     protected function markFailedRecord($modelName, X2Model $model, $csvLine, $metaData) {
1217:         // If the import failed, then put the data into the failedRecords CSV for easy recovery.
1218:         $failedRecords = fopen($this->safePath('failedRecords.csv'), 'a+');
1219:         $errorMsg = array();
1220:         foreach ($model->errors as $error)
1221:             $errorMsg[] = strtr(implode(' ', array_values($error)), '"', "'");
1222:         $errorMsg = implode(' ', $errorMsg);
1223: 
1224:         // Add the error to the last column of the csv record
1225:         if (end($metaData) === 'X2_Import_Failures')
1226:             $csvLine[count($csvLine) - 1] = $errorMsg;
1227:         else
1228:             $csvLine[] = $errorMsg;
1229:         fputcsv($failedRecords, $csvLine);
1230:         fclose($failedRecords);
1231:         $_SESSION['failed']++;
1232: 
1233:         // Remove ActionText placeholder from model container
1234:         if ($modelName === 'Actions')
1235:             array_pop ($this->modelContainer['ActionText']);
1236:     }
1237: 
1238:     /**
1239:      * Save and attempt to load the uploaded import mapping
1240:      */
1241:     protected function loadUploadedImportMap() {
1242:         $temp = CUploadedFile::getInstanceByName('mapping');
1243:         $temp->saveAs($mapPath = $this->safePath('mapping.json'));
1244:         $mappingFile = fopen($mapPath, 'r');
1245:         $importMap = fread($mappingFile, filesize($mapPath));
1246:         $importMap = json_decode($importMap, true);
1247:         if ($importMap === null) {
1248:             $_SESSION['errors'] = Yii::t('admin', 'Invalid JSON string specified');
1249:             $this->redirect('importModels');
1250:         }
1251:         $_SESSION['importMap'] = $importMap;
1252: 
1253:         if (array_key_exists('mapping', $importMap)) {
1254:             $_SESSION['importMap'] = $importMap['mapping'];
1255:             if (isset($importMap['name']))
1256:                 $_SESSION['mapName'] = $importMap['name'];
1257:             else
1258:                 $_SESSION['mapName'] = Yii::t('admin', "Untitled Mapping");
1259:             // Make sure $importMap is consistent with legacy import map format
1260:             $importMap = $importMap['mapping'];
1261:         } else {
1262:             $_SESSION['importMap'] = $importMap;
1263:             $_SESSION['mapName'] = Yii::t('admin', "Untitled Mapping");
1264:         }
1265: 
1266:         fclose($mappingFile);
1267:         if (file_exists($mapPath))
1268:             unlink($mapPath);
1269:     }
1270: 
1271:     /**
1272:      * Retrieve all associated export format options from the request parameters
1273:      * @param array $params Request parameters, e.g., $_GET
1274:      * @return array of format options, indexed by form element ID
1275:      */
1276:     protected function readExportFormatOptions($params) {
1277:         $paramNames = array(
1278:             'compressOutput',
1279:             'exportDestination',
1280:             
1281:         );
1282:         // Defaults
1283:         $formatParams = array(
1284:             'exportDestination' => 'download',
1285:             'compressOutput' => 'false',
1286:         );
1287:         foreach ($paramNames as $param) {
1288:             if (array_key_exists ($param, $params) && !empty($params[$param]))
1289:                 $formatParams[$param] = $params[$param];
1290:         }
1291:         $formatParams['compressOutput'] = $formatParams['compressOutput'] === 'true' ? true : false;
1292:         return $formatParams;
1293:     }
1294: 
1295:     /**
1296:      * Modifies the export path to ensure a consistent file extensions
1297:      * @param string $path Path to export file
1298:      * @param array $params Export format parameters
1299:      * @param string $filetype Expected file extension
1300:      * @return string $path Modified export path
1301:      */
1302:     protected function adjustExportPath($path, $params, $filetype = 'csv') {
1303:         if (isset($params['compressOutput']) && $params['compressOutput']) {
1304:             $path = str_replace('.'.$filetype, '.zip', $path);
1305:             if (!preg_match ('/\.zip$/', $path))
1306:                 $path = $path.'.zip';
1307:         } else {
1308:             if (!preg_match ('/\.'.$filetype.'$/', $path))
1309:                 $path = "{$path}.{$filetype}";
1310:         }
1311:         return $path;
1312:     }
1313: 
1314:     /*
1315:      * Handle pushing exported data to various targets, including download in browser, save
1316:      * locally to server, copy to remote server by FTP or SCP, or push to a cloud provider
1317:      * like Amazon S3 or Google Drive.
1318:      * @param string $src Source file to copy
1319:      * @param array $params Export deliverable parameters, as retrieved by {@link readExportFormatOptions}
1320:      */
1321:     public function prepareExportDeliverable($src, $params) {
1322:         $success = true;
1323:         if (!array_key_exists ('mimeType', $params))
1324:             $params['mimeType'] = 'text/csv';
1325:         if (!array_key_exists ('exportDestination', $params))
1326:             return false;
1327: 
1328:         if (array_key_exists ('compressOutput', $params) && $params['compressOutput']) {
1329:             // Package the CSV, media files, and modules
1330:             $zip = Yii::app()->zip;
1331:             $dirname = str_replace('.csv', '', $src);
1332:             $dst = $dirname .'/'. basename($src);
1333:             AppFileUtil::ccopy ($src, $dst);
1334:             $zipPath = $this->safePath(basename ($dirname) . '.zip');
1335: 
1336:             if ($zip->makeZip($dirname, $zipPath)) {
1337:                 $src = $zipPath;
1338:                 $params['mimeType'] = 'application/zip';
1339:             } else {
1340:                 $success = false;
1341:             }
1342:         }
1343:         
1344:         return $success;
1345:     }
1346: 
1347:     
1348: }
1349: 
1350: ?>
1351: 
X2CRM Documentation API documentation generated by ApiGen 2.8.0