Version: current

       1 <?php
       2 /**
       3  * Процесс импортирования каталога 0.9
       4  * sake /ProductCatalogImportTask
       5  *
       6  * @author menedem
       7  */
       8 class ProductCatalogImportTask extends ScheduledTask {
       9
      10     private static $baseFileURL = 'offers/';
      11     private static $importFileName = 'catalog.xml';
      12
      13     protected $importStatus;
      14     protected $importLog;
      15     private static $max_version_count = 1;
      16     private static $max_log_count = 30;
      17
      18     // разрешен импорт параметров каталога
      19     protected static $allow_parameters = true;
      20
      21     // разрешен импорт фильтров каталога
      22     protected static $allow_filters = true;
      23
      24     // кешированный список параметров каталога
      25     protected $cachedCatalogParams = array();
      26
      27     protected $categoryCount = 0;
      28     protected $productCount = 0;
      29
      30     protected static $possibleStartPages = array('StartCatalog', 'Catalog', 'DocPage');
      31
      32     // настройки импорта
      33     protected $importSettings = false;
      34
      35     protected static $possibleSettings = array('ImportMode' => 'full', 'ClearProducts' => 0, 'ClearCategory'=>0, 'StartPage'=>'/', 'SubSite'=>0, 'OldItemAction'=>'delete');
      36
      37     protected static $catalog_version = 3;
      38
      39     static $catalog_saveable_fields = array('URLSegment', 'Description', 'Content', 'MenuTitle', 'MetaTitle', 'MetaKeywords', 'MetaDescription', 'GroupTitle', 'Sort');
      40
      41     static $product_saveable_fields_v2 = array('URLSegment', 'Description', 'Content', 'Vendor', 'MenuTitle', 'MetaTitle', 'MetaKeywords', 'MetaDescription', 'Available', 'AllowPurchase', 'Sort');
      42
      43     static $product_saveable_fields_v3 = array('URLSegment', 'Description', 'Content', 'Vendor', 'SKU', 'MenuTitle', 'MetaTitle', 'MetaKeywords', 'MetaDescription', 'Available', 'AllowPurchase','Quantity', 'Weight', 'GroupID', 'GroupTitle', 'Sort');
      44
      45     /**
      46      * Задаем версию каталога для импорта (2 - для catalog-0.8, 3 - для catalog-0.9)
      47      *
      48      * @param Int $version (2 или 3)
      49      *
      50      */
      51     static function set_catalog_version($version) {
      52         if ($version == 2 || $version == 3) {
      53             self::$catalog_version = $version;
      54         }
      55         // для catalog-0.8 отключаем импорт параметров и фильтров (не реализованы в этой версии)
      56         if ($version == 2) {
      57             self::$allow_parameters = false;
      58             self::$allow_filters = false;
      59         }
      60     }
      61
      62     // получаем список полей товара для сохранения
      63     protected static function get_product_saveable_fields() {
      64         if (self::$catalog_version == 3) {
      65             return self::$product_saveable_fields_v3;
      66         }
      67         return self::$product_saveable_fields_v2;
      68     }
      69
      70     static function getImportFileName() {
      71         return self::$importFileName;
      72     }
      73
      74     static function set_base_url($url) {
      75         self::$baseFileURL = $url;
      76     }
      77
      78     static function get_base_url() {
      79         return preg_replace('!/+$!','', self::$baseFileURL) . '/';
      80     }
      81
      82     /**
      83      * Путь к папке импорта относительно корня сайта
      84      *
      85      * @param string $filename
      86      *
      87      * @return string
      88      */
      89     static function import_path($filename) {
      90         return self::get_base_url() . preg_replace('!^/!', '', $filename);
      91     }
      92
      93     /**
      94      * Полный путь к папке импорта (может быть указано имя файла в этой папке)
      95      *
      96      * @param string $filename
      97      *
      98      * @return string
      99      */
     100     static function absolute_import_path($filename) {
     101         return BASE_PATH . '/' . self::import_path($filename);
     102     }
     103
     104     /**
     105      * Полный путь к корню модуля (может быть указано имя файла в этой папке) - например для получения catalog2.xsd
     106      *
     107      * @param string $filename
     108      *
     109      * @return string
     110      */
     111     static function module_path($filename) {
     112         return BASE_PATH . '/catalog_import/' . $filename;
     113     }
     114
     115     /**
     116      * Установка количества хранимых логов импорта
     117      *
     118      * @param int $count
     119      */
     120     static function set_log_count($count) {
     121         self::$max_log_count = $count;
     122     }
     123
     124     /**
     125      * Установка количества хранимых версий страниц (чтоб не плодить версии при импорте)
     126      *
     127      * @param int $count
     128      */
     129     static function set_version_count($count) {
     130         self::$max_version_count = $count;
     131     }
     132
     133     /**
     134      * Получение допустимых типов корневых страниц для импорта
     135      *
     136      * @return Array
     137      */
     138     static function get_possible_start_pages() {
     139         return self::$possibleStartPages;
     140     }
     141
     142     /**
     143      * Установка допустимых типов корневых страниц для импорта
     144      *
     145      * @param Array $pages
     146      */
     147     static function set_possible_start_pages($pages) {
     148         self::$possibleStartPages = $pages;
     149     }
     150
     151     /**
     152      * Установка разрешения импорта параметров
     153      *
     154      * @param bool $val
     155      */
     156     static function set_allow_parameters($val) {
     157         self::$allow_parameters = $val;
     158     }
     159
     160     /**
     161      * Установка разрешения импорта параметров
     162      *
     163      * @param bool $val
     164      */
     165     static function set_allow_filters($val) {
     166         self::$allow_filters = $val;
     167     }
     168
     169     static function import_parse_bool_value($value) {
     170         if (in_array($value, array('true', '1'))) {
     171             return 1;
     172         }
     173         if (in_array($value, array('false', '0'))) {
     174             return 0;
     175         }
     176         return null;
     177     }
     178
     179     /**
     180      * Заполняем настройки импорта
     181      *
     182      * @param SimpleXMLElement $settings
     183      *
     184      */
     185     function fillImportSettings($settings, $srcName) {
     186         if (!$this->checkImportSettings($settings)) {
     187             $this->importLog->addLog(sprintf(_t('ProductCatalogImportTask.ErrorIn', 'Error In %s'), $srcName), 'warning');
     188             return false;
     189         }
     190         if (!$this->importSettings)
     191             $this->importSettings = new DataObject();
     192             foreach (array_keys(self::$possibleSettings) as $set) {
     193             if (!isset($this->importSettings->{$set}) && isset($settings->{$set})) {
     194                 // !!! для правильной интерпретации строк true и false в xml
     195                 $value = trim((string)$settings->{$set});
     196                 if ($value === 'true') $value = 1;
     197                 if ($value === 'false') $value = 0;
     198                 $this->importSettings->{$set} = $value;
     199
     200                 if ($value && $set == 'StartPage') {
     201                     if ($value == '/') {
     202                         $this->importSettings->StartPage = 0;
     203                     } else {
     204                         $this->importSettings->StartPage = DataObject::get_one('SiteTree', "URLSegment = '{$value}'")->ID;
     205                     }
     206                 }
     207                 // !!!! id или url подсайта/языковой версии !!!!
     208                 if ($value && $set == 'SubSite') {
     209                     $this->importSettings->SubSite = (int) $value;
     210                 }
     211             }
     212         }
     213         return true;
     214     }
     215
     216     /**
     217      * Проверяем настройки импорта
     218      *
     219      * @return boolean
     220      */
     221     function checkImportSettings($settings) {
     222         if (!$settings) {
     223             $this->importLog->addLog(_t('ProductCatalogImportTask.WrongSettings', 'Wrong Settings'), 'warning');
     224             return false;
     225         }
     226         if ($settings->StartPage) {
     227             if ($settings->StartPage != '/') {
     228                 $startPage = DataObject::get_one('SiteTree', "URLSegment = '{$settings->StartPage}'");
     229                 if (!$startPage) {
     230                     $this->importLog->addLog(_t('ProductCatalogImportTask.NoRoot', 'No Root'), 'warning');
     231                     return false;
     232                 }
     233             }
     234         }
     235         if (class_exists('Subsite') && $settings->SubSite) {
     236             $subsite = DataObject::get_by_id('Subsite', (int) $settings->SubSite);
     237             if (!$subsite) {
     238                 $this->importLog->addLog(_t('ProductCatalogImportTask.NoSubsite', 'No Subsite'), 'warning');
     239                 return false;
     240             }
     241         }
     242         if ($settings->ImportMode && $settings->ImportMode != 'full' && $settings->ImportMode != 'incremental' ) {
     243             $this->importLog->addLog(
     244                 sprintf(_t('ProductCatalogImportTask.WrongImportParam', 'Wrong Import Param') . '  ImportMode: "%s"!', $settings->ImportMode),
     245                 'warning'
     246             );
     247             return false;
     248         }
     249         if ($settings->ClearCategory && !in_array($settings->ClearCategory, array('true', 'false'))) {
     250             $this->importLog->addLog(
     251                 sprintf(_t('ProductCatalogImportTask.WrongImportParam', 'Wrong Import Param') . ' ClearCategory: "%s"!', $settings->ClearCategory),
     252                 'warning'
     253             );
     254             return false;
     255         }
     256         if ($settings->ClearProducts && !in_array($settings->ClearProducts, array('true', 'false'))) {
     257             $this->importLog->addLog(
     258                 sprintf(_t('ProductCatalogImportTask.WrongImportParam', 'Wrong Import Param') . ' ClearProducts: "%s"!', $settings->ClearProducts),
     259                 'warning'
     260             );
     261             return false;
     262         }
     263         if ($settings->ClearCategory == 'true' && $settings->ClearProducts == 'false' ) {
     264             $this->importLog->addLog(
     265                 _t('ProductCatalogImportTask.WrongImportParamClearProducts', 'Wrong Import Param ClearProducts'),
     266                 'warning'
     267             );
     268             return false;
     269         }
     270         if ($settings->ImportMode == 'incremental' && ($settings->ClearCategory == 'true' || $settings->ClearProducts == 'true')) {
     271             $this->importLog->addLog(
     272                 _t('ProductCatalogImportTask.WrongImportParamImportMode', 'Wrong Import Param ImportMode'),
     273                 'warning'
     274             );
     275             return false;
     276         }
     277         if ($setting->OldItemAction && !in_array($settings->OldItemAction, array('hide', 'unpublish', 'delete', ''))) {
     278             $this->importLog->addLog(
     279                 sprintf(_t('ProductCatalogImportTask.WrongOldItemActionParam', 'Wrong Old Item Action') . ' OldItemAction: "%s"!', $settings->OldItemAction),
     280                 'warning'
     281             );
     282             return false;
     283         }
     284         return true;
     285     }
     286
     287     /**
     288      *
     289      *
     290      * @param <type> $element
     291      *
     292      * @return <type>
     293      */
     294     function xml2log($element) {
     295         $result = '<' . $element->getName();
     296         foreach ($element->attributes() as $name => $val) {
     297             $result .= ' ' . $name . '="' . $val . '"';
     298         }
     299         $result .= '>';
     300         return $result;
     301     }
     302
     303     function TotalMessage() {
     304         return sprintf('Импортировано категорий: %d, товаров: %d.', $this->categoryCount, $this->productCount);
     305     }
     306
     307     /**
     308      * Процесс импорта
     309      *
     310      */
     311     function process() {
     312         $pidFile = TEMP_FOLDER . '/import.pid';
     313         if (is_file($pidFile)) {
     314             $pid = (int) file_get_contents($pidFile);
     315             if (posix_getsid($pid) !== false) {
     316                 // импорт уже работает
     317                 return;
     318             }
     319             else {
     320                 // предыдущий импорт сдох?
     321                 $log = ProductImportLog::get_last_log();
     322                 if ($log && $log->Status == 'process') {
     323                     $log->EndTime = $log->LastEdited;
     324                     $log->addStatus('error', 'Скрипт импорта неожиданно прекратил работу');
     325                 }
     326                 unlink($pidFile);
     327             }
     328         }
     329
     330         if (!is_file(self::absolute_import_path('start_ok'))) return;
     331
     332         file_put_contents($pidFile, getmypid());
     333
     334         //Запускаем импорт
     335         unlink(self::absolute_import_path('start_ok'));
     336
     337         // время начала импорта
     338         $startTime = time();
     339
     340         // установить кол-во версий для товаров
     341         Versioned::$versions_ttl = self::$max_version_count;
     342
     343         // отключаем автосоздание папок каталогов и товаров -  в импорте мы не копируем туда прикрепленный фото и файлы
     344         AssociatedFolderDecorator::$createFolders = false;
     345
     346         // отключаем протоколирование
     347         if (class_exists('LogItem')) {
     348             LogItem::enable(false);
     349         }
     350
     351         // почистим старые ProductImportLog
     352         ProductImportLog::optimize_log_tables(self::$max_log_count);
     353
     354         //Лог импорта
     355         $this->importLog = new ProductImportLog();
     356         $this->importLog->write();
     357         $this->importLog->addLog( _t('ProductCatalogImportTask.StartAt', 'Start At') .  date("Y-m-d H:i:s", $startTime));
     358         if (!is_file(self::absolute_import_path(self::$importFileName))) {
     359             $this->importLog->addLog("Нет файла импорта товаров " . self::$importFileName, 'warning');
     360             return false;
     361         }
     362
     363         $validator = new XMLValidate(self::absolute_import_path(self::$importFileName), self::module_path('catalog3.xsd'));
     364         $rs = $validator->validate();
     365         if ($rs != 'validated') {
     366             foreach ($rs as $error)
     367                 $this->importLog->addLog( _t('ProductCatalogImportTask.ValidationError', 'Validation Error') . ': '.$error, 'error');
     368             $this->importLog->addStatus('error', _t('ProductCatalogImportTask.ValidationError', 'Validation Error'));
     369             return false;
     370         }
     371
     372         // config.xml генерируем из админки - максимальный приоритет
     373         if (is_file(self::absolute_import_path('config.xml'))) {
     374             $settings = simplexml_load_file(self::absolute_import_path('config.xml'));
     375             //удаляем файл настроек, сгенереный из админки, чтобы при след.импорте не воспользоваться случайно
     376             unlink(self::absolute_import_path('config.xml'));
     377             if (!$this->fillImportSettings($settings, 'config.xml'))
     378                 return false;
     379         }
     380         if ($this->importProductCatalog($startTime)) {
     381             $this->importLog->EndTime = date('Y-m-d H:i:s');
     382             $this->importLog->addStatus('ok', $this->TotalMessage());
     383             $this->importLog->addLog(sprintf(_t('ProductCatalogImportTask.ImportDoneAt', 'Import done at %s'), date("Y-m-d H:i:s")));
     384         } else {
     385             $this->importLog->EndTime = date('Y-m-d H:i:s');
     386             $this->importLog->addStatus('error', _t('ProductCatalogImportTask.ImportError', 'Import Error'));
     387         }
     388         unlink($pidFile);
     389     }
     390
     391     function importProductCatalog($startTime) {
     392
     393         $this->categoryCount = 0;
     394         $this->productCount = 0;
     395
     396         $importData = simplexml_load_file(self::absolute_import_path(self::$importFileName));
     397         rename(self::absolute_import_path(self::$importFileName), self::absolute_import_path(self::$importFileName) . '.bak');
     398
     399         // настройки из самого xml - вторые по приоритету
     400         if ($importData->Config && !$this->fillImportSettings($importData->Config, self::$importFileName)) {
     401             return false;
     402         }
     403
     404         //Настройки из SiteConfig - самые последние
     405         $sc = SiteConfig::current_site_config();
     406         $this->fillImportSettings($sc, 'SiteConfig');
     407         if (!$this->fillImportSettings(self::$possibleSettings, 'Defaults')) {
     408             return false;
     409         }
     410
     411         $this->importLog->addLog(sprintf(
     412             _t('ProductCatalogImportTask.ImportSettings', 'Import Settings: ImportMode = %s; ClearCategory = %s; ClearProducts = %s; BaseFileURL = %s; StartPage = %s; SubsiteID = %s; OldItemAction = %s'), $this->importSettings->ImportMode, $this->importSettings->ClearCategory, $this->importSettings->ClearProducts, self::$baseFileURL, $this->importSettings->StartPage, (int)$this->importSettings->Subsite, $this->importSettings->OldItemAction
     413         ));
     414
     415         $this->importLog->ProductCatalogDate = (string) $importData->attributes()->date; // !! ПРОВЕРИТЬ атрибуты - есть ли date
     416         $this->importLog->addStatus('process', _t('ProductCatalogImportTask.ImportInProgress', 'Import In Progress'));
     417         $this->importLog->addLog(_t('ProductCatalogImportTask.CatalogImport', 'CatalogImport'));
     418
     419         // импорт параметров
     420         if (self::$allow_parameters) {
     421             $IDs = array();
     422             if ($params = DataObject::get('ProductParam', "ParentCatalogID = 0")) {
     423                 $IDs = $params->map('ID', 'ID');
     424             }
     425             $this->importParams($importData, 0, $IDs);
     426
     427             $possibleFilterFields = Product::$possible_filter_fields;
     428             if ($rootParams = DataObject::get('ProductParam', "ParentCatalogID = 0 AND ShowDefault = 1")) {
     429                 $possibleFilterFields = array_merge($possibleFilterFields, $rootParams->map('TechTitle', 'TechTitle'));
     430             }
     431             $this->cachedCatalogParams[0] = $possibleFilterFields;
     432         }
     433
     434         // импорт фильтров
     435         if (self::$allow_filters) {
     436             $IDs = array();
     437             if ($filters = DataObject::get('CatalogFilter', "ParentCatalogID = 0")) {
     438                 $IDs = $filters->map('ID', 'ID');
     439             }
     440             if ($importData->xpath('CatalogFilters')) {
     441                 $this->importFilters($importData, 0, $IDs);
     442             }
     443         }
     444
     445         $categories = $importData->xpath('Category');
     446
     447         // Очищаем категории если нужно
     448         if ($this->importSettings->ClearCategory) {
     449             singleton('Catalog')->importClearAll($this->importLog);
     450             singleton('Product')->importClearAll($this->importLog);
     451         }
     452
     453         // очищаем продукты
     454         if ($this->importSettings->ClearProducts) {
     455             singleton('Product')->importClearAll($this->importLog);
     456         }
     457
     458         $startPageID = $this->importSettings->StartPage;
     459         $this->importCategories($categories, $startPageID);
     460         if ($this->importSettings->ImportMode == 'full') {
     461             $this->importLog->addLog(_t('ProductCatalogImportTask.ClearUnupdated', 'Clear Unupdated...'));
     462             $this->unpublishNotUpdated($startTime);
     463         }
     464
     465         // Чистим записи в БД - картинки, которые есть в БД, но нет на диске.
     466         $this->deleteEmptyDBFiles();
     467
     468         return true;
     469     }
     470
     471     /*
     472      * Импорт категорий
     473      * Рекурсивная функция
     474      *
     475      * @param  array
     476      */
     477     function importCategories($categories, $parentID) {
     478         foreach($categories as $category) {
     479             $newCategoryID = $this->importCategory($category, $parentID);
     480             if (($childs = $category->xpath('Childs/Category')) && $newCategoryID) {
     481                 $this->importCategories($childs, $newCategoryID);
     482             }
     483         }
     484     }
     485
     486     /*
     487      * Импорт конкретной категории
     488      *
     489      * @param  array
     490      */
     491     function importCategory($category, $parentID) {
     492         if (!$category->attributes()->id)
     493             return false;
     494         $importID = (string)$category->attributes()->id;
     495         $newCategory = DataObject::get_one('Catalog', "ImportID = '{$importID}'");
     496         if (!$newCategory) {
     497             $newCategory = new Catalog();
     498             $newCategory->ImportID = $importID;
     499         } else {
     500             if (((string)$category->attributes()->unpublish == "true")) {
     501                 $newCategory->doUnpublish();
     502                 return false;
     503             }
     504             if (((string)$category->attributes()->delete == "true")) {
     505                 $newCategory->doUnpublish();
     506                 $newCategory->delete();
     507                 return false;
     508             }
     509         }
     510         $newCategory->ParentID = $parentID;
     511         $newCategory->Title = (string)$category->Title;
     512
     513         // сохраняем все подходящие поля каталога, если они есть в выгрузке
     514         foreach(self::$catalog_saveable_fields as $field) {
     515             if ($category->{$field}) {
     516                 $newCategory->{$field} = (string)$category->{$field};
     517             }
     518         }
     519         // прописываем подсайт
     520         if ($this->importSettings->SubSite) {
     521             $newCategory->SubsiteID = $this->importSettings->SubSite;
     522         }
     523         $img = $this->importFile($newCategory->PhotoID, $category->Photo);
     524         if ($img) {
     525             $newCategory->PhotoID = $img->ID;
     526         }
     527         // выставляем показ в меню из выгрузки - если не указано, то показывать
     528         if (isset($category->ShowInMenus)) {
     529             $newCategory->ShowInMenus = ($category->ShowInMenus == 'true') ? 1 : 0;
     530         } else {
     531             $newCategory->ShowInMenus = 1;
     532         }
     533         $newCategory->writeToStage('Stage');
     534         $newCategory->publish('Stage', 'Live');
     535
     536         // импорт параметров рубрики
     537
     538         // импорт фильтров
     539         if (self::$allow_parameters) {
     540             $IDs = array();
     541             if ($newCategory->Params() && $newCategory->Params()->Count()) {
     542                 $IDs = $newCategory->Params()->getIdList();
     543             }
     544             $this->importParams($category, $newCategory->ID, $IDs);
     545
     546             // список параметров конкретной рубрики
     547             $newCategory->OwnParams = 0;
     548             if ($category->xpath('ShowParams')) {
     549                 $params = $category->xpath('ShowParams/ShowParam');
     550                 // сначала всо удалим
     551                 if ($newCategory->EnabledParams() && $newCategory->EnabledParams()->Count()) {
     552                     $newCategory->EnabledParams()->removeAll();
     553                 }
     554                 // потом добавим, если существуют
     555                 foreach($params as $paramID) {
     556                     $param = DataObject::get_one('ProductParam', "TechTitle = '". Convert::raw2sql((string)$paramID) . "'");
     557                     if ($param) {
     558                         $newCategory->EnabledParams()->add($param);
     559                     }
     560                 }
     561                 $newCategory->OwnParams = 1; // если фильтры заданы - то отмечаем, чтот у этой категории свои фильтры
     562             }
     563             $newCategory->writeToStage('Stage');
     564             $newCategory->publish('Stage', 'Live');
     565         }
     566
     567         if (self::$allow_filters) {
     568             $possibleFilterFields = Product::$possible_filter_fields;
     569             if ($activeCatalogParams = $newCategory->catalogParams()) {
     570                 $possibleFilterFields = array_merge($possibleFilterFields, $activeCatalogParams->map('TechTitle', 'TechTitle'));
     571             }
     572             $this->cachedCatalogParams[$newCategory->ID] = $possibleFilterFields;
     573
     574             // импорт фильтров
     575             $IDs = array();
     576             if ($newCategory->CatalogFilters() && $newCategory->CatalogFilters()->Count()) {
     577                 $IDs = $newCategory->CatalogFilters()->getIdList();
     578             }
     579
     580             if ($category->xpath('CatalogFilters')) {
     581                 $this->importFilters($category, $newCategory->ID, $IDs);
     582             }
     583
     584             // список фильтров конкретной рубрики
     585             $newCategory->OwnFilter = 0;
     586             if ($category->xpath('ShowFilters')) {
     587                 $filters = $category->xpath('ShowFilters/ShowFilter');
     588                 // сначала всо удалим
     589                 if ($newCategory->EnabledCatalogFilters() && $newCategory->EnabledCatalogFilters()->Count()) {
     590                     $newCategory->EnabledCatalogFilters()->removeAll();
     591                 }
     592                 // потом добавим, если существуют
     593                 foreach($filters as $filterID) {
     594                     $filter = DataObject::get_one('CatalogFilter', "TechTitle = '". Convert::raw2sql((string)$filterID) . "'");
     595                     if ($filter) {
     596                         $newCategory->EnabledCatalogFilters()->add($filter);
     597                     }
     598                 }
     599                 $newCategory->OwnFilter = 1; // если фильтры заданы - то отмечаем, чтот у этой категории свои фильтры
     600             }
     601             $newCategory->writeToStage('Stage');
     602             $newCategory->publish('Stage', 'Live');
     603         }
     604
     605         $this->categoryCount++;
     606
     607
     608         if ($category->xpath('Products')) {
     609             $products = $category->xpath('Products/Product');
     610             foreach($products as $product) {
     611                 $this->importProduct($product, $newCategory->ID);
     612             }
     613         } else {
     614 //          $this->importLog->addLog(sprintf('У категории %s отсутствуют товары!', $newCategory->Title), 'warning');
     615         }
     616         return $newCategory->ID;
     617     }
     618
     619     function importParams($xml, $parentID, $oldIDs) {
     620         if ($xml->xpath('ProductParams')) {
     621             $params = $xml->xpath('ProductParams/ProductParam');
     622             foreach($params as $param) {
     623                 if (!$param->attributes()->id) {
     624                     $this->importLog->addLog('У элемента ProductParam отсутвует обязательный атрибут id!', 'warning');
     625                     continue;
     626                 }
     627                 $paramData = DataObject::get_one('ProductParam', "ParentCatalogID = {$parentID} AND TechTitle = '". Convert::raw2sql($param->attributes()->id) . "'");
     628                 if (!$paramData) {
     629                     $paramData = new ProductParam();
     630                     $paramData->TechTitle = Convert::raw2sql($param->attributes()->id);
     631                 }
     632                 $paramData->ParentCatalogID = $parentID;
     633                 $paramData->Title = (string)$param->Title;
     634                 $paramData->Type = (string)$param->Type;
     635                 $paramData->ShowInList = ($param->ShowInList == 'true') ? 1 : 0;
     636                 $paramData->ShowInView = ($param->ShowInView == 'true') ? 1 : 0;
     637                 $paramData->ShowDefault = ($param->ShowDefault == 'true') ? 1 : 0;
     638                 $paramData->Sort = (int)$param->Sort;
     639                 $paramData->write();
     640                 unset($oldIDs[$paramData->ID]);
     641             }
     642
     643             // удалим отсутствующие в выгрузке параметры
     644             foreach($oldIDs as $ID) {
     645                 $param = DataObject::get_by_id('ProductParam', $ID);
     646                 if ($param) {
     647                     $param->delete();
     648                 }
     649             }
     650         }
     651     }
     652
     653     function importFilters($xml, $parentID, $oldIDs) {
     654         // импорт фильтров
     655         $filters = $xml->xpath('CatalogFilters/CatalogFilter');
     656         foreach($filters as $filter) {
     657             if (!$filter->attributes()->id) {
     658                 $this->importLog->addLog('У элемента CatalogFilter отсутвует обязательный атрибут id!', 'warning');
     659                 continue;
     660             }
     661             $filterField = trim((string)$filter->Field);
     662             if (!isset($this->cachedCatalogParams[$parentID]) || (array_search($filterField, $this->cachedCatalogParams[$parentID]) === false)) {
     663                 $this->importLog->addLog("У каталога с ID {$parentID} отсутвствует параметр {$filterField} для фильтра!", 'warning');
     664                 continue;
     665             }
     666             $filterData = DataObject::get_one('CatalogFilter', "ParentCatalogID = {$parentID} AND TechTitle = '". Convert::raw2sql($filter->attributes()->id) . "'");
     667             if (!$filterData) {
     668                 $filterData = new CatalogFilter();
     669                 $filterData->TechTitle = Convert::raw2sql($filter->attributes()->id);
     670             }
     671             $filterData->ParentCatalogID = $parentID;
     672             $filterData->Title = (string)$filter->Title;
     673             $filterData->Type = (string)$filter->Type;
     674             $filterData->FilterField = $filterField;
     675             $filterData->Important = ($filter->Important == 'true') ? 1 : 0;
     676             $filterData->ShowDefault = ($filter->ShowDefault == 'true') ? 1 : 0;
     677             $filterData->Sort = (int)$filter->Sort;
     678             if ((string)$filter->GroupTitle && $filterData->Type == 'boolgroup') {
     679                 $filterData->GroupTitle = (string)$filter->GroupTitle;
     680             }
     681             if ((string)$filter->DefaultValue && $filterData->Type == 'list') {
     682                 $filterData->DefaultValue = (string)$filter->DefaultValue;
     683             }
     684             $filterData->write();
     685             unset($oldIDs[$filterData->ID]);
     686         }
     687
     688         // удалим отсутствующие в выгрузке параметры
     689         foreach($oldIDs as $ID) {
     690             $filter = DataObject::get_by_id('CatalogFilter', $ID);
     691             if ($filter) {
     692                 $filter->delete();
     693             }
     694         }
     695     }
     696
     697     function importProduct($product, $categoryID) {
     698         if (!$product->attributes()->id)
     699             return;
     700         $importID = (string)$product->attributes()->id;
     701         $productID = DB::query("SELECT ID FROM Product WHERE \"ImportID\"  = '{$importID}' ORDER BY ID DESC")->value();
     702         if ($productID) {
     703             //$newProduct = DataObject::get_by_id('Product', $productID);
     704             $newProduct = Versioned::get_one_by_stage("Product", "Stage", "\"Product\".\"ID\" = {$productID}");
     705             if (!$newProduct) {
     706                 $newProduct = Versioned::get_one_by_stage("Product", "Live", "\"Product\".\"ID\" = {$productID}");
     707             }
     708             if (!$newProduct) {
     709                 return; // кидать ошибку в лог
     710             }
     711
     712             if (((string)$product->attributes()->unpublish == "true")) {
     713                 $newProduct->doUnpublish();
     714                 return false;
     715             }
     716             if (((string)$product->attributes()->delete == "true")) {
     717                 $newProduct->doUnpublish();
     718                 $newProduct->delete();
     719                 return false;
     720             }
     721         } else {
     722             $newProduct = new Product();
     723             $newProduct->ImportID = $importID;
     724         }
     725
     726         $newProduct->ParentID = $categoryID;
     727         $newProduct->Title = (string)$product->Title;
     728
     729         // сохраняем все подходящие поля, если они есть в выгрузке
     730         foreach(self::get_product_saveable_fields() as $field) {
     731             // разбиваем текст по строкам
     732             // и оборачиваем их в <p>, тк 1С не умеет в форматирование
     733             if($field == 'Content' && $product->{$field}){
     734                 $content = "";
     735                 $splitted = explode("\n",(string)$product->{$field});
     736                 foreach ($splitted as $line) {
     737                     $content .= "<p>{$line}</p>";
     738                 }
     739                 $newProduct->Content = $content;
     740                 continue;
     741             }
     742             if ($product->{$field}) {
     743                 $newProduct->{$field} = (string)$product->{$field};
     744             }
     745         }
     746
     747         if ($product->Available) {
     748             $value = self::import_parse_bool_value($product->Available);
     749             if ($value !== null) {
     750                 $newProduct->Available = $value;
     751             }
     752         }
     753
     754         if ($product->AllowPurchase) {
     755             $value = self::import_parse_bool_value($product->AllowPurchase);
     756             if ($value !== null) {
     757                 $newProduct->AllowPurchase = $value;
     758             }
     759         }
     760
     761         // прописываем подсайт
     762         if ($this->importSettings->SubSite) {
     763             $newProduct->SubsiteID = $this->importSettings->SubSite;
     764         }
     765
     766         /*  Оставим на всяк случий!
     767         $price = htmlentities((string)$product->attributes()->price, null, 'utf-8'); // в файле импорта в цене стоит неразрывный пробел, поэтому
     768         $price = preg_replace(array('/\s|&nbsp;/', '/,/'), array('', '.'), $price); //сначала преобразуем его в HTML сущность (&nbsp;), а потом заменяем
     769         $price = (float) $price;
     770         */
     771         if ($product->BasePrice) {
     772             $newProduct->BasePrice = (float)$product->BasePrice;
     773         }
     774
     775         if ($product->CostPrice) {
     776             $newProduct->CostPrice = 0; // зануляем, на случай, если тега не указано
     777             if ((float)$product->CostPrice) {
     778                 $newProduct->CostPrice = (float)$product->CostPrice;
     779             }
     780         }
     781
     782         // импорт основного фото
     783         $img = $this->importFile($newProduct->PhotoID, (string)$product->Photo);
     784         if ($img) {
     785             $newProduct->PhotoID = $img->ID;
     786         }
     787
     788         $newProduct->writeToStage('Stage');
     789         $newProduct->publish('Stage', 'Live');
     790
     791         // импорт списка фото
     792         if ($product->xpath('Images')) {
     793             $oldPhotos = $newProduct->Photos();
     794             $oldPhotosIDs = $oldPhotos->getIdList(); //сохраняем список ID прикрепленных фоток
     795             $newPhotos = $product->xpath('Images/Image');
     796             foreach($newPhotos as $newPhoto) {
     797                 $importedImage = $this->importFile(0, $newPhoto->attributes()->path);
     798                 if ($importedImage) {
     799                     if ($mwPhoto = $oldPhotos->find('PhotoID', $importedImage->ID)) {
     800                         unset($oldPhotosIDs[$mwPhoto->ID]); //если фотка осталась, то удаляем ее ID из списка
     801                     } else {
     802                         $mwPhoto = new MediawebPage_Photo();
     803                         $mwPhoto->PhotoID = $importedImage->ID;
     804                     }
     805                     $mwPhoto->MediawebPageID = $newProduct->ID; //Цепляем к Product
     806                     $mwPhoto->Caption = (string)$newPhoto;//$importedImage->Title;
     807                     $mwPhoto->write();
     808                 }
     809             }
     810             // все, что осталось в списке - чистим!
     811             if (count($oldPhotosIDs) > 0) {
     812                 foreach($oldPhotosIDs as $oldPhotosID) {
     813                     if ($oldPhoto = DataObject::get_by_id('MediawebPage_Photo', $oldPhotosID)) {
     814                         $oldPhoto->delete();
     815                     }
     816                 }
     817             }
     818         }
     819
     820         // импорт списка файлов
     821         if ($product->xpath('Files')) {
     822             $oldFiles = $newProduct->Files();
     823             $oldFilesIDs = $oldFiles->getIdList(); //сохраняем список ID прикрепленных фоток
     824             $newFiles = $product->xpath('Files/File');
     825             foreach($newFiles as $newFile) {
     826                 $importedFile = $this->importFile(0, $newFile->attributes()->path, 'File'); //!!! TODO написать importFiles
     827                 if ($importedFile) {
     828                     if ($mwFile = $oldFiles->find('FileID', $importedFile->ID)) {
     829                         unset($oldFilesIDs[$mwFile->ID]); //если фотка осталась, то удаляем ее ID из списка
     830                     } else {
     831                         $mwFile = new MediawebPage_File();
     832                         $mwFile->AttachID = $importedFile->ID;
     833                     }
     834                     $mwFile->MediawebPageID = $newProduct->ID; //Цепляем к Product
     835                     $mwFile->Caption = $importedFile->Title;
     836                     $mwFile->write();
     837                 }
     838             }
     839             // все, что осталось в списке - чистим!
     840             if (count($oldFilesIDs) > 0) {
     841                 foreach($oldFilesIDs as $oldFilesID) {
     842                     if ($oldFile = DataObject::get_by_id('MediawebPage_File', $oldFilesID)) {
     843                         $oldFile->delete();
     844                     }
     845                 }
     846             }
     847         }
     848
     849         // ИМпорт СпецКаталогов
     850         if (class_exists('SpecialCatalog') && $product->xpath('SpecialCatalogs')) {
     851             $specCatalogs = $this->importListedSpecCatalogs($product->xpath('SpecialCatalogs/SpecialCatalog'));
     852             $productSpecialCatalogs = array(); //список ID-шники спецкаталогов, в которых изначально был продукт
     853             //складываем в список ID всех спец.каталогов, в которых есть товар
     854             $allSpecialCatalogs = DataObject::get('SpecialCatalog');
     855             if ($allSpecialCatalogs) {
     856                 foreach($allSpecialCatalogs as $specialCatalog) {
     857                     $specialCatalogProducts = $specialCatalog->LinkedProducts()->getIdList();
     858                     if (isset($specialCatalogProducts[$newProduct->ID])) {
     859                         $productSpecialCatalogs[$specialCatalog->ID] = $specialCatalog->ID;
     860                     }
     861                 }
     862             }
     863             //Добавляем товар в указанные спец.каталоги
     864             foreach($specCatalogs as $specialCatalog) {
     865                 if (isset($productSpecialCatalogs[$specialCatalog->ID])) { //если товар уже есто в новом спец.каталоге
     866                     unset($productSpecialCatalogs[$specialCatalog->ID]); //то удаляем ID спец.каталога из списка
     867                 } else {
     868                     $specialCatalog->LinkedProducts()->add($newProduct); // иначе добавляем товар в спец.каталог
     869                 }
     870             }
     871
     872             if (count($productSpecialCatalogs)) {
     873                 foreach($productSpecialCatalogs as $productSpecialCatalog) { //чистим все, что осталось в списке
     874                     if ($oldSpecialCatalog = $allSpecialCatalogs->find('ID', $productSpecialCatalog)) {
     875                         $oldSpecialCatalog->LinkedProducts()->remove($newProduct);
     876                     }
     877                 }
     878             }
     879         }
     880
     881         //импорт значений параметров
     882         if ($product->xpath('Params')) {
     883             // сначала удаляем старые значения
     884             $oldParamValues = DataObject::get('ProductParamValue', "ProductID = {$newProduct->ID}");
     885             if ($oldParamValues) {
     886                 foreach($oldParamValues as $oldParamValue)
     887                     $oldParamValue->delete();
     888             }
     889
     890             //добавляем новые
     891             $params = $product->xpath('Params/Param');
     892             foreach($params as $param) {
     893                 if (!$param->attributes()->name) {
     894                     $this->importLog->addLog('У элемента param отсутвует обязательный атрибут name!', 'warning');
     895                     continue;
     896                 }
     897
     898                 $paramData = DataObject::get_one('ProductParam', "TechTitle = '{$param->attributes()->name}'"); //ParentCatalogID = {$categoryID} AND
     899                 if ($paramData) {
     900                     $paramValueID = DB::query("SELECT \"ID\" FROM \"ProductParamValue\" WHERE \"ProductParamID\"  = {$paramData->ID} AND ProductID = {$newProduct->ID}")->value();
     901                     if ($paramValueID) {
     902                         $paramValue = DataObject::get_by_id('ProductParamValue', $paramValueID);
     903                     } else {
     904                         $paramValue = new ProductParamValue();
     905                         $paramValue->ProductParamID = $paramData->ID;
     906                         $paramValue->ProductID = $newProduct->ID;
     907                     }
     908
     909                     $value = (string)$param;
     910                     // для типа bool проверяем значения true/false
     911                     if ($paramData->Type == 'bool') {
     912                         if ($value == 'true') {
     913                             $value = 1;
     914                         }
     915                         if ($value == 'false') {
     916                             $value = 0;
     917                         }
     918                     }
     919                     $paramValue->Value = $value;
     920                     $paramValue->write();
     921
     922                 } else {
     923                     //$this->importLog->addLog('У категории отсутствует параметр: ');
     924                     $this->importLog->addLog(sprintf('У категории %s отсутствует параметр: %s', $newProduct->Parent()->Title, $param->attributes()->name), 'warning');
     925                 }
     926             }
     927             unset($params);
     928             unset($oldParamValues);
     929         }
     930         $this->productCount++;
     931         $this->importLog->addStatus('process', "Импортировано категорий: {$this->categoryCount}, товаров: {$this->productCount}");
     932
     933         unset($product);
     934         unset($newProduct);
     935         unset($oldPhotos);
     936         unset($newPhotos);
     937     }
     938
     939     function importListedSpecCatalogs($data) {
     940         //Обрабатываем списочные спец.каталоги (если есть)
     941         $catalogs = false;
     942         if (isset($data)) {
     943             $catalogs = new DataObjectSet();
     944             foreach ($data as $catalog) {
     945                 $specialCatalog = false;
     946                 $id = (int)($catalog->attributes()->id);
     947                 $url = Convert::raw2sql($catalog->attributes()->url);
     948                 if ($id > 0)
     949                     $specialCatalog = DataObject::get_by_id('SpecialCatalog', $id);
     950                 if (!$specialCatalog && $url) {
     951                     $specialCatalog = DataObject::get_one('SpecialCatalog', "URLSegment = '$url'");
     952                 }
     953                 if ($specialCatalog) {
     954                     $catalogs->push($specialCatalog);
     955                 }
     956                 else {
     957                     $this->importLog->addLog(sprintf(_t('ProductCatalogImportTask.NoSpecialCatalog', 'No specila catalog %s'), self::xml2log($catalog)));
     958                 }
     959             }
     960         }
     961         return $catalogs;
     962     }
     963
     964     /**
     965      * Скрываем элементы, которых не нужно отображать после импорта
     966      *
     967      * @param string $importStartTime
     968      *
     969      * @return boolean
     970      */
     971     function unpublishNotUpdated($importStartTime) {
     972         $this->importLog->addStatus('process', _t('ProductCatalogImportTask.HidingUnavailable', 'Hiding Unavailable categories and products'));
     973         // Скрываем не обновленные товары
     974         $needUnpublish = DataObject::get('Product', 'LastEdited < \'' . date("Y-m-d H:i:s", $importStartTime) . '\'');
     975         if ($needUnpublish)
     976             foreach ($needUnpublish as $product) {
     977                 switch ($this->importSettings->OldItemAction) {
     978                     case 'hide':
     979                         $product->AllowPurchase = 0;
     980                         $product->writeToStage('Stage');
     981                         if ($product->isPublished()) {
     982                             $product->publish('Stage', 'Live');
     983                         }
     984                         break;
     985                     case 'unpublish':
     986                         $product->doUnpublish();
     987                         break;
     988                     case 'delete':
     989                         $product->doUnpublish();
     990                         $product->delete();
     991                         break;
     992                     default:
     993                         $product->doUnpublish();
     994                         $product->delete();
     995                         break;
     996                 }
     997             }
     998         // Скрываем не обновленные рубрики
     999         $needUnpublish = DataObject::get('Catalog', 'ClassName = \'Catalog\' AND LastEdited < \'' . date("Y-m-d H:i:s", $importStartTime) . '\'');
    1000         if ($needUnpublish)
    1001             foreach ($needUnpublish as $catalog) {
    1002                 switch ($this->importSettings->OldItemAction) {
    1003                     case 'hide':
    1004                         $catalog->ShowInMenus = 0;
    1005                         $catalog->writeToStage('Stage');
    1006                         if ($catalog->isPublished()) {
    1007                             $catalog->publish('Stage', 'Live');
    1008                         }
    1009                         break;
    1010                     case 'unpublish':
    1011                         $catalog->doUnpublish();
    1012                         break;
    1013                     case 'delete':
    1014                         $catalog->doUnpublish();
    1015                         $catalog->delete();
    1016                         break;
    1017                     default:
    1018                         $catalog->doUnpublish();
    1019                         $catalog->delete();
    1020                         break;
    1021                 }
    1022             }
    1023         $this->importLog->addStatus('process', _t('ProductCatalogImportTask.UnavailableIsHide', 'Unavailable categories and products hide'));
    1024         return true;
    1025     }
    1026
    1027     /**
    1028      * Чистим записи в БД - картинки, которые есть в БД, но нет на диске.
    1029      *
    1030      * @return boolean
    1031      */
    1032     function deleteEmptyDBFiles() {
    1033         $importPath = self::get_base_url();
    1034         $images = DataObject::get('Image', "`Filename` LIKE '{$importPath}%'");
    1035         if ($images) {
    1036             foreach($images as $image) {
    1037                 if (!file_exists($image->getFullPath())) {
    1038                     DB::Query('UPDATE Catalog SET PhotoID = 0 WHERE PhotoID = ' . $image->ID);
    1039                     DB::Query('UPDATE Catalog_Live SET PhotoID = 0 WHERE PhotoID = ' . $image->ID);
    1040                     DB::Query('UPDATE Product SET PhotoID = 0 WHERE PhotoID = ' . $image->ID);
    1041                     DB::Query('UPDATE Product_Live SET PhotoID = 0 WHERE PhotoID = ' . $image->ID);
    1042                     DB::Query('DELETE FROM MediawebPage_Photo WHERE PhotoID = ' . $image->ID);
    1043                     $image->deleteFormattedImages();
    1044                     $image->deleteDatabaseOnly();
    1045                 }
    1046             }
    1047         }
    1048         return true;
    1049     }
    1050
    1051     /**
    1052      * Импорт файлов (Image или File)
    1053         *
    1054      * @param DataObject $object
    1055      * @param string $path
    1056         *
    1057      * @return Image
    1058      */
    1059     function importFile($fileID, $filePath, $type='Image') {
    1060         $filePath = trim(str_replace("\\", "/", $filePath));
    1061         $filePath = preg_replace('!^/!','', $filePath); // срежем ведущий /
    1062         if ($filePath) { // !!! $filePath всегда true
    1063             // используем полный абсолютный путь до файлов - чтоб при необходимости можно было их сложить в другие папки
    1064             $fullFilePath = BASE_PATH . '/' . $filePath;
    1065             // получим объект для картинки
    1066             $file = false;
    1067             // сначала вытащим по ID
    1068             if ($fileID != 0) {
    1069                 $file = DataObject::get_by_id($type, $fileID);
    1070             }
    1071
    1072             // не нашли по ID - поищем по пути к файлу
    1073             if (!$file || $file->ID == 0) {
    1074                 $file = DataObject::get_one($type, "\"Filename\"='".Convert::raw2sql($filePath)."'", true, 'ID DESC');
    1075             }
    1076
    1077             // картинка физически есть на диске
    1078             if (is_file($fullFilePath)) {
    1079                 // не нашли по пути - создаем новую
    1080                 if (!$file || $file->ID == 0) {
    1081                     $file = new $type();
    1082                     $file->setFilename($filePath);
    1083                     $file->Title = $file->Name;
    1084                     $file->write();
    1085                 }
    1086
    1087                 if ($file->Filename != $filePath) {
    1088                     //если поменялось имя файла, то сначала пытаемся найти имеющийся
    1089                     $file = DataObject::get_one($type, "\"Filename\"='".Convert::raw2sql($filePath)."'", true, 'ID DESC');
    1090                     //a если не находим создаем новый (старый может быть привязан к другому объекту!)
    1091                     if (!$file) {
    1092                         $file = new $type();
    1093                         $file->setFilename($filePath);
    1094                         $file->Title = $file->Name;
    1095                         $file->write();
    1096                     }
    1097                 }
    1098
    1099                 // Если поменялось имя файла (или новый файл)
    1100                 if (($fileID != $file->ID)) {
    1101                     if (!$file->Name) {
    1102                         $file->Name = basename($filePath);
    1103                     }
    1104                     if (($type == 'Image') && $file->Filename && $file->Filename != $filePath) { // для старого имени
    1105                         $file->deleteFormattedImages();
    1106                     }
    1107                     $file->setFilename($filePath);
    1108                     if ($type == 'Image') {
    1109                         $file->deleteFormattedImages(); // для нового имени
    1110                     }
    1111                     if (!$file->Title)
    1112                     $file->Title = $file->Name;
    1113                     $file->write();
    1114                 }
    1115                 elseif ($fileID != 0) {
    1116                     // Если файл обновлен по ftp, а путь к нему не поменялся
    1117                     if (filemtime($fullFilePath) > strtotime($file->LastEdited)) {
    1118                         if ($type == 'Image') {
    1119                             $file->deleteFormattedImages();
    1120                         }
    1121                         //Чтобы LastEdited обновился )))))
    1122                         $file->forceChange();
    1123                         if (!$file->Title)
    1124                         $file->Title = $file->Name;
    1125                         $file->write();
    1126                     }
    1127                 }
    1128                 if (!$file->Hidden) {
    1129                     $file->Hidden = 1;
    1130                     $file->write();
    1131                 }
    1132                 return $file;
    1133             } else {
    1134                 //Если физически файла нет, а запись в БД есть
    1135                 if ($file) {
    1136                     if ($type == 'Image') {
    1137                         $file->deleteFormattedImages();
    1138                     }
    1139                     $file->deleteDatabaseOnly(); // то удаляем запись
    1140                 }
    1141                 $this->importLog->addLog(sprintf('Отсутствует изображение: %s', $filePath), 'warning');
    1142             }
    1143         }
    1144         return false;
    1145     }
    1146 }

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -