forked from TYPO3-extensions/ExternalImport
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclass.tx_externalimport_importer.php
More file actions
1754 lines (1611 loc) · 63.4 KB
/
class.tx_externalimport_importer.php
File metadata and controls
1754 lines (1611 loc) · 63.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?php
/***************************************************************
* Copyright notice
*
* (c) 2007-2014 Francois Suter (Cobweb) <typo3@cobweb.ch>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
/**
* This class performs the data update from the external sources
*
* @author Francois Suter (Cobweb) <typo3@cobweb.ch>
* @package TYPO3
* @subpackage tx_externalimport
*/
class tx_externalimport_importer {
public $extKey = 'external_import';
protected $vars = array(); // Variables from the query string (TODO: remove, unused)
protected $extConf = array(); // Extension configuration
protected $messages = array(); // List of result messages
protected $table; // Name of the table being synchronised
/**
* @var mixed Index of the synchronisation configuration in use
*/
protected $index;
/**
* @var mixed Index for the columns, may be different from $this->index
*/
protected $columnIndex;
protected $tableTCA; // TCA of the table being synchronised
protected $externalConfig; // Ctrl-section external config being used for synchronisation
protected $pid = 0; // uid of the page where the records will be stored
protected $additionalFields = array(); // List of fields to import, but not to save
protected $numAdditionalFields = 0; // Number of such fields
/**
* @var array $temporaryKeys list of temporary keys created on the fly for new records. Used in TCEmain data map.
*/
protected $temporaryKeys = array();
/**
* @var int $newKeysCounter simple counter for generating the temporary keys
*/
protected $newKeysCounter = 0;
/**
* This is the constructor
* It initialises some properties and makes sure that a lang object is available
*
* @return tx_externalimport_importer object
*/
public function __construct() {
$this->extConf = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'][$this->extKey]);
$this->messages = array(t3lib_FlashMessage::ERROR => array(), t3lib_FlashMessage::WARNING => array(), t3lib_FlashMessage::OK => array());
// Make sure we have a language object
// If initialised, use existing, if not, initialise it
if (!isset($GLOBALS['LANG'])) {
require_once(PATH_typo3 . 'sysext/lang/lang.php');
$GLOBALS['LANG'] = t3lib_div::makeInstance('language');
$GLOBALS['LANG']->init($GLOBALS['BE_USER']->uc['lang']);
}
$GLOBALS['LANG']->includeLLFile('EXT:' . $this->extKey . '/locallang.xml');
// Force PHP limit execution time if set
if (isset($this->extConf['timelimit']) && ($this->extConf['timelimit'] > -1)) {
set_time_limit($this->extConf['timelimit']);
if ($this->extConf['debug'] || TYPO3_DLOG) {
t3lib_div::devLog($GLOBALS['LANG']->getLL('timelimit'), $this->extKey, 0, $this->extConf['timelimit']);
}
}
}
/**
* This method synchronises all the external tables, respecting the order of priority
*
* @return array List of all messages
*/
public function synchronizeAllTables() {
// Look in the TCA for tables with an "external" control section and a "connector"
// Tables without connectors cannot be synchronised
$externalTables = array();
foreach ($GLOBALS['TCA'] as $tableName => $sections) {
if (isset($sections['ctrl']['external'])) {
foreach ($sections['ctrl']['external'] as $index => $externalConfig) {
if (!empty($externalConfig['connector'])) {
// Default priority if not defined, set to very low
$priority = 1000;
if (isset($externalConfig['priority'])) {
$priority = $externalConfig['priority'];
}
if (!isset($externalTables[$priority])) $externalTables[$priority] = array();
$externalTables[$priority][] = array('table' => $tableName, 'index' => $index);
}
}
}
}
// Sort tables by priority (lower number is highest priority)
ksort($externalTables);
if ($this->extConf['debug'] || TYPO3_DLOG) {
t3lib_div::devLog($GLOBALS['LANG']->getLL('sync_all'), $this->extKey, 0, $externalTables);
}
// Synchronise all tables
$allMessages = array();
foreach ($externalTables as $tables) {
foreach ($tables as $tableData) {
$this->messages = array(t3lib_FlashMessage::ERROR => array(), t3lib_FlashMessage::WARNING => array(), t3lib_FlashMessage::OK => array()); // Reset error messages array
$messages = $this->synchronizeData($tableData['table'], $tableData['index']);
$key = $tableData['table'] . '/' .$tableData['index'];
$allMessages[$key] = $messages;
}
}
// Return compiled array of messages for all imports
return $allMessages;
}
/**
* This method stores information about the synchronised table into member variables
*
* @param string $table: name of the table to synchronise
* @param integer $index: index of the synchronisation configuration to use
* @return void
*/
protected function initTCAData($table, $index) {
$this->table = $table;
$this->index = $index;
t3lib_div::loadTCA($this->table);
$this->tableTCA = $GLOBALS['TCA'][$this->table];
$this->externalConfig = $GLOBALS['TCA'][$this->table]['ctrl']['external'][$index];
// Set the pid where the records will be stored
// This is either specific for the given table or generic from the extension configuration
if (isset($this->externalConfig['pid'])) {
$this->pid = $this->externalConfig['pid'];
} else {
$this->pid = $this->extConf['storagePID'];
}
// Sets the column configuration index (may differ from main one)
if (isset($this->externalConfig['useColumnIndex'])) {
$this->columnIndex = $this->externalConfig['useColumnIndex'];
} else {
$this->columnIndex = $index;
}
// Set this storage page as the related page for the devLog entries
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['debugData']['pid'] = $this->pid;
// Get the list of additional fields
// Additional fields are fields that must be taken from the imported data,
// but that will not be saved into the database
if (!empty($this->externalConfig['additional_fields'])) {
$this->additionalFields = t3lib_div::trimExplode(',', $this->externalConfig['additional_fields'], 1);
$this->numAdditionalFields = count($this->additionalFields);
}
}
/**
* This method calls on the distant data source and synchronises the data in the TYPO3 database
* It returns information about the results of the operation
*
* @param string $table: name of the table to synchronise
* @param integer $index: index of the synchronisation configuration to use
* @return array List of error or success messages
*/
public function synchronizeData($table, $index) {
// If the user has enough rights on the table, proceed with synchronization
if ($GLOBALS['BE_USER']->check('tables_modify', $table)) {
$this->initTCAData($table, $index);
// Instantiate specific connector service
if (empty($this->externalConfig['connector'])) {
$this->addMessage(
$GLOBALS['LANG']->getLL('no_connector')
);
} else {
$services = t3lib_extMgm::findService('connector', $this->externalConfig['connector']);
// The service is not available
if ($services === FALSE) {
$this->addMessage(
$GLOBALS['LANG']->getLL('no_service')
);
} else {
/** @var $connector tx_svconnector_base */
$connector = t3lib_div::makeInstanceService('connector', $this->externalConfig['connector']);
// The service was instantiated, but an error occurred while initiating the connection
// If the returned value is an array, an error has occurred
if (is_array($connector)) {
$this->addMessage(
$GLOBALS['LANG']->getLL('data_not_fetched')
);
// The connection is established, get the data
} else {
$data = array();
// Pre-process connector parameters
$this->externalConfig['parameters'] = $this->processParameters($this->externalConfig['parameters']);
// A problem may happen while fetching the data
// If so, the import process has to be aborted
$abortImportProcess = FALSE;
switch ($this->externalConfig['data']) {
case 'xml':
try {
$data = $connector->fetchXML($this->externalConfig['parameters']);
}
catch (Exception $e) {
$abortImportProcess = TRUE;
$this->addMessage(
sprintf($GLOBALS['LANG']->getLL('data_not_fetched_connector_error'), $e->getMessage())
);
}
break;
case 'array':
try {
$data = $connector->fetchArray($this->externalConfig['parameters']);
}
catch (Exception $e) {
$abortImportProcess = TRUE;
$this->addMessage(
sprintf($GLOBALS['LANG']->getLL('data_not_fetched_connector_error'), $e->getMessage())
);
}
break;
// If the data type is not defined, issue error and abort process
default:
$abortImportProcess = TRUE;
$this->addMessage(
$GLOBALS['LANG']->getLL('data_type_not_defined')
);
break;
}
// Continue, if the process was not marked as aborted
if (!$abortImportProcess) {
if ($this->extConf['debug'] || TYPO3_DLOG) {
$debugData = $this->prepareDataSample($data);
t3lib_div::devLog('Data received (sample)', $this->extKey, -1, $debugData);
}
$this->handleData($data);
}
// Call connector's post-processing with a rough error status
$errorStatus = FALSE;
if (count($this->messages[t3lib_FlashMessage::ERROR]) > 0) {
$errorStatus = TRUE;
}
$connector->postProcessOperations($this->externalConfig['parameters'], $errorStatus);
}
}
}
// The user doesn't have enough rights on the table
// Log error
} else {
$userName = $GLOBALS['BE_USER']->user['username'];
$this->addMessage(
sprintf($GLOBALS['LANG']->getLL('no_rights_for_sync'), $userName, $table)
);
}
// Log results to devlog
if ($this->extConf['debug'] || TYPO3_DLOG) {
$this->logMessages();
}
return $this->messages;
}
/**
* Pre-processes the configured connector parameters.
*
* @param array $parameters List of parameters to process
* @return array The processed parameters
*/
protected function processParameters($parameters) {
if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF'][$this->extKey]['processParameters'])) {
foreach($GLOBALS['TYPO3_CONF_VARS']['EXTCONF'][$this->extKey]['processParameters'] as $className) {
$preProcessor = t3lib_div::getUserObj($className);
$parameters = $preProcessor->processParameters($parameters, $this);
}
}
return $parameters;
}
/**
* This method receives raw data from some external source, transforms it and stores it into the local database
* It returns information about the results of the operation
*
* @param string $table: name of the table to import into
* @param integer $index: index of the synchronisation configuration to use
* @param mixed $rawData: data in the format provided by the external source (XML string, PHP array, etc.)
* @return array List of error or success messages
*/
public function importData($table, $index, $rawData) {
$this->initTCAData($table, $index);
$this->handleData($rawData);
// Log results to devlog
if ($this->extConf['debug'] || TYPO3_DLOG) {
$this->logMessages();
}
return $this->messages;
}
/**
* This method prepares a sample from the data to import, based on the preview limit
* The process applied for this depends on the data type (array or XML)
*
* @param mixed $data: the input data as a XML string or a PHP array
* @return array The data sample, in same format as input (but written inside an array in case of XML data)
*/
protected function prepareDataSample($data) {
$dataSample = $data;
if (!empty($this->extConf['previewLimit'])) {
switch ($this->externalConfig['data']) {
case 'xml':
// Load the XML into a DOM object
$dom = new DOMDocument();
$dom->loadXML($data, LIBXML_PARSEHUGE);
// Prepare an empty DOM object for the sample data
$domSample = new DOMDocument();
// Define a root node
$element = $domSample->createElement('sample');
$domSample->appendChild($element);
// Get the desired nodes
$selectedNodes = $dom->getElementsByTagName($this->externalConfig['nodetype']);
// Loop until the preview limit and import selected nodes into the sample XML object
$loopLimit = min($selectedNodes->length, $this->extConf['previewLimit']);
for ($i = 0; $i < $loopLimit; $i++) {
$newNode = $domSample->importNode($selectedNodes->item($i), TRUE);
$domSample->documentElement->appendChild($newNode);
}
// Store the XML sample in an array, to have a common return format
$dataSample = array();
$dataSample[] = $domSample->saveXML();
break;
case 'array':
$dataSample = array_slice($data, 0, $this->extConf['previewLimit']);
break;
}
}
return $dataSample;
}
/**
* Receives raw data from some external source, transforms it and stores it into the local database
*
* Returns information about the results of the operation
*
* @param mixed $rawData Data in the format provided by the external source (XML string, PHP array, etc.)
* @return void
*/
protected function handleData($rawData) {
// Check for custom data handlers
if (!empty($this->externalConfig['dataHandler'])) {
/** @var $dataHandler tx_externalimport_dataHandler */
$dataHandler = t3lib_div::makeInstance($this->externalConfig['dataHandler']);
if ($dataHandler instanceof tx_externalimport_dataHandler) {
$records = $dataHandler->handleData($rawData, $this);
// If the data handler is not appropriate, keep the raw data
} else {
$records = $rawData;
}
// Use default handlers
} else {
// Prepare the data, depending on result type
switch ($this->externalConfig['data']) {
case 'xml':
$records = $this->handleXML($rawData);
break;
case 'array':
$records = $this->handleArray($rawData);
break;
// This should really not happen
default:
$records = $rawData;
break;
}
}
// Apply any existing pre-processing hook to the raw data
$records = $this->preprocessRawData($records);
// Check the raw data to see if import process should continue
$continueImport = $this->validateRawData($records);
// If raw data was judged valid, continue with import
if ($continueImport) {
// Transform data
$records = $this->transformData($records);
// Apply any existing pre-processing hook to the transformed data
$records = $this->preprocessData($records);
// Store data
$this->storeData($records);
// Clear cache
$this->clearCache();
// Import was aborted, issue warning message
} else {
$this->addMessage($GLOBALS['LANG']->getLL('importAborted'), t3lib_FlashMessage::WARNING);
}
}
/**
* This method takes the data returned by the distant source as array and prepares it
* for update/insertion/deletion in the database
*
* @param array $rawData: response array
* @return array response stored as an indexed array of records (associative array of fields)
*/
protected function handleArray($rawData) {
$data = array();
// Loop on all entries
if (is_array($rawData) && count($rawData) > 0) {
foreach ($rawData as $theRecord) {
$theData = array();
// Loop on the database columns and get the corresponding value from the import data
foreach ($this->tableTCA['columns'] as $columnName => $columnData) {
if (isset($columnData['external'][$this->columnIndex]['field'])) {
if (isset($theRecord[$columnData['external'][$this->columnIndex]['field']])) {
$theData[$columnName] = $theRecord[$columnData['external'][$this->columnIndex]['field']];
}
}
}
// Get additional fields data, if any
if ($this->numAdditionalFields > 0) {
foreach ($this->additionalFields as $fieldName) {
if (isset($theRecord[$fieldName])) {
$theData[$fieldName] = $theRecord[$fieldName];
}
}
}
$data[] = $theData;
}
}
return $data;
}
/**
* This method takes the data returned by the distant source as XML and prepares it
* for update/insertion/deletion in the database
*
* @param string $rawData: response XML as a string
* @return array response stored as an indexed array of records (associative array of fields)
*/
protected function handleXML($rawData) {
$data = array();
// Load the XML into a DOM object
$dom = new DOMDocument();
$dom->loadXML($rawData, LIBXML_PARSEHUGE);
// Instantiate a XPath object and load with any defined namespaces
$xPathObject = new DOMXPath($dom);
if (isset($this->externalConfig['namespaces']) && is_array($this->externalConfig['namespaces'])) {
foreach ($this->externalConfig['namespaces'] as $prefix => $uri) {
$xPathObject->registerNamespace($prefix, $uri);
}
}
// Get the nodes that represent the root of each data record
$records = $dom->getElementsByTagName($this->externalConfig['nodetype']);
for ($i = 0; $i < $records->length; $i++) {
/** @var DOMElement $theRecord */
$theRecord = $records->item($i);
$theData = array();
// Loop on the database columns and get the corresponding value from the import data
foreach ($this->tableTCA['columns'] as $columnName => $columnData) {
// Act only if there's an external import definition
if (isset($columnData['external'][$this->columnIndex])) {
// If a "field" is defined, refine the selection to get the correct node
if (isset($columnData['external'][$this->columnIndex]['field'])) {
// Use namespace or not
if (empty($columnData['external'][$this->columnIndex]['fieldNS'])) {
$nodeList = $theRecord->getElementsByTagName($columnData['external'][$this->columnIndex]['field']);
} else {
$nodeList = $theRecord->getElementsByTagNameNS($columnData['external'][$this->columnIndex]['fieldNS'], $columnData['external'][$this->columnIndex]['field']);
}
if ($nodeList->length > 0) {
/** @var $selectedNode DOMNode */
$selectedNode = $nodeList->item(0);
// If an XPath expression is defined, apply it (relative to currently selected node)
if (!empty($columnData['external'][$this->columnIndex]['xpath'])) {
try {
$selectedNode = $this->selectNodeWithXpath(
$xPathObject,
$columnData['external'][$this->columnIndex]['xpath'],
$selectedNode
);
}
catch (Exception $e) {
// Nothing to do, data is ignored
}
}
$theData[$columnName] = $this->extractValueFromNode(
$selectedNode,
$columnData['external'][$this->columnIndex]
);
}
// Without "field" property, use the current node itself
} else {
// If an XPath expression is defined, apply it (relative to current node)
if (!empty($columnData['external'][$this->columnIndex]['xpath'])) {
try {
$selectedNode = $this->selectNodeWithXpath(
$xPathObject,
$columnData['external'][$this->columnIndex]['xpath'],
$theRecord
);
$theData[$columnName] = $this->extractValueFromNode(
$selectedNode,
$columnData['external'][$this->columnIndex]
);
}
catch (Exception $e) {
// Nothing to do, data is ignored
}
} else {
$theData[$columnName] = $this->extractValueFromNode(
$theRecord,
$columnData['external'][$this->columnIndex]
);
}
}
}
}
// Get additional fields data, if any
if ($this->numAdditionalFields > 0) {
foreach ($this->additionalFields as $fieldName) {
$node = $theRecord->getElementsByTagName($fieldName);
if ($node->length > 0) {
$theData[$fieldName] = $node->item(0)->nodeValue;
}
}
}
if (count($theData) > 0) {
$data[] = $theData;
}
}
return $data;
}
/**
* Extracts either an attribute from a XML node or the value of the node itself,
* based on the configuration received.
*
* @param DOMNode $node Currently handled XML node
* @param array $columnData Handling information for the XML node
* @return mixed The extracted value
*/
protected function extractValueFromNode($node, $columnData) {
// Get the named attribute, if defined
if (!empty($columnData['attribute'])) {
// Use namespace or not
if (empty($columnData['attributeNS'])) {
$value = $node->attributes->getNamedItem($columnData['attribute'])->nodeValue;
} else {
$value = $node->attributes->getNamedItemNS($columnData['attributeNS'], $columnData['attribute'])->nodeValue;
}
// Otherwise directly take the node's value
} else {
// If "xmlValue" is set, we want the node's inner XML structure as is.
// Otherwise, we take the straight node value, which is similar but with tags stripped.
if (empty($columnData['xmlValue'])) {
$value = $node->nodeValue;
} else {
$value = $this->getXmlValue($node);
}
}
return $value;
}
/**
* Extracts the value of the node as structured XML.
*
* @param DOMNode $node Currently handled XML node
* @throws Exception
* @return string Code inside the node
*/
protected function getXmlValue($node) {
$innerHTML = '';
$children = $node->childNodes;
foreach ($children as $child) {
$innerHTML .= $child->ownerDocument->saveXML($child);
}
return $innerHTML;
}
/**
* Queries the current structure with an XPath query.
*
* @param DOMXPath $xPathObject Instantiated DOMXPath object
* @param string $xPath XPath query to evaluate
* @param DOMNode $context Node giving the context of the XPath query
* @return DOMElement First node found
* @throws Exception
*/
protected function selectNodeWithXpath($xPathObject, $xPath, $context) {
$resultNodes = $xPathObject->evaluate($xPath, $context);
if ($resultNodes->length > 0) {
return $resultNodes->item(0);
} else {
throw new Exception('No node found with xPath: ' . $xPath, 1399497464);
}
}
/**
* This method applies any transformation necessary on the data
* Transformations are defined by mappings or custom functions
* applied to one or more columns
*
* @param array $records: records containing the data
* @return array the transformed records
*/
protected function transformData($records) {
$numRecords = count($records);
// Loop on all tables to find any defined transformations. This might be mappings and/or user functions
foreach ($this->tableTCA['columns'] as $columnName => $columnData) {
// If the column's content must be trimmed, apply trim to all records
if (!empty($columnData['external'][$this->columnIndex]['trim'])) {
for ($i = 0; $i < $numRecords; $i++) {
$records[$i][$columnName] = trim($records[$i][$columnName]);
}
}
// Get existing mappings and apply them to records
if (isset($columnData['external'][$this->columnIndex]['mapping'])) {
$records = $this->mapData($records, $columnName, $columnData['external'][$this->columnIndex]['mapping']);
// Otherwise apply constant value, if defined
} elseif (isset($columnData['external'][$this->columnIndex]['value'])) {
for ($i = 0; $i < $numRecords; $i++) {
$records[$i][$columnName] = $columnData['external'][$this->columnIndex]['value'];
}
}
// Add field for RTE transformation to each record, if column has RTE enabled
if (!empty($columnData['external'][$this->columnIndex]['rteEnabled'])) {
for ($i = 0; $i < $numRecords; $i++) {
$records[$i]['_TRANSFORM_' . $columnName] = 'RTE';
}
}
// Apply defined user function
if (isset($columnData['external'][$this->columnIndex]['userFunc'])) {
// Try to get the referenced class
$userObject = t3lib_div::getUserObj($columnData['external'][$this->columnIndex]['userFunc']['class']);
// Could not instantiate the class, log error and do nothing
if ($userObject === FALSE) {
if ($this->extConf['debug'] || TYPO3_DLOG) {
t3lib_div::devLog(
sprintf(
$GLOBALS['LANG']->getLL('invalid_userfunc'),
$columnData['external'][$this->columnIndex]['userFunc']['class']
),
$this->extKey,
2,
$columnData['external'][$this->columnIndex]['userFunc']
);
}
// Otherwise call referenced class on all records
} else {
$methodName = $columnData['external'][$this->columnIndex]['userFunc']['method'];
$parameters = isset($columnData['external'][$this->columnIndex]['userFunc']['params']) ? $columnData['external'][$this->columnIndex]['userFunc']['params'] : array();
for ($i = 0; $i < $numRecords; $i++) {
$records[$i][$columnName] = $userObject->$methodName($records[$i], $columnName, $parameters);
}
}
}
}
return $records;
}
/**
* This method takes the records and applies a mapping to a selected column
*
* @param array $records: original records to handle
* @param string $columnName: name of the column whose values must be mapped
* @param array $mappingInformation: mapping configuration
* @return array The records with the mapped values
*/
protected function mapData($records, $columnName, $mappingInformation) {
$mappings = $this->getMapping($mappingInformation);
$numRecords = count($records);
// If no particular matching method is defined, match exactly on the keys of the mapping table
if (empty($mappingInformation['match_method'])) {
// Determine if mapping is self-referential
// Self-referential mappings cause a problem, because they may refer to a record that is not yet
// in the database, but is part of the import. In this case we need to create a temporary ID for that
// record and store it in order to reuse it when assembling the TCEmain data map (in storeData()).
$isSelfReferencing = FALSE;
if ($mappingInformation['table'] == $this->table) {
$isSelfReferencing = TRUE;
}
for ($i = 0; $i < $numRecords; $i++) {
$externalValue = $records[$i][$columnName];
// If the external value is empty, don't even try to map it. Otherwise, proceed.
if (empty($externalValue)) {
unset($records[$i][$columnName]);
} else {
// The external field may contain multiple values
if (!empty($mappingInformation['multipleValuesSeparator'])) {
$singleExternalValues = t3lib_div::trimExplode(
$mappingInformation['multipleValuesSeparator'],
$externalValue,
TRUE
);
// The external field is expected to contain a single value
} else {
$singleExternalValues = array($externalValue);
}
// Loop on all values and try to map them
$mappedExternalValues = array();
foreach ($singleExternalValues as $singleValue) {
// Value is matched in the available mapping
if (isset($mappings[$singleValue])) {
$mappedExternalValues[] = $mappings[$singleValue];
// Value is not matched, maybe it matches a temporary key, if self-referential
} else {
// If the relation is self-referential, use a temporary key
if ($isSelfReferencing) {
// Check if a temporary key was already created for that external key
if (isset($this->temporaryKeys[$singleValue])) {
$temporaryKey = $this->temporaryKeys[$singleValue];
// If not, create a new temporary key
} else {
$this->newKeysCounter++;
$temporaryKey = 'NEW_' . $this->newKeysCounter;
$this->temporaryKeys[$singleValue] = $temporaryKey;
}
// Use temporary key
$mappedExternalValues[] = $temporaryKey;
}
}
}
if (count($mappedExternalValues) > 0) {
$records[$i][$columnName] = implode(',', $mappedExternalValues);
} else {
unset($records[$i][$columnName]);
}
}
}
// If a particular mapping method is defined, use it on the keys of the mapping table
// NOTE: self-referential relations are not checked in this case, as it does not seem to make sense
// to have weak-matching external keys
} else {
if ($mappingInformation['match_method'] == 'strpos' || $mappingInformation['match_method'] == 'stripos') {
for ($i = 0; $i < $numRecords; $i++) {
$externalValue = $records[$i][$columnName];
// The external field may contain multiple values
if (!empty($mappingInformation['multipleValuesSeparator'])) {
$singleExternalValues = t3lib_div::trimExplode(
$mappingInformation['multipleValuesSeparator'],
$externalValue,
TRUE
);
// The external field is expected to contain a single value
} else {
$singleExternalValues = array($externalValue);
}
// Loop on all values and try to map them
$mappedExternalValues = array();
foreach ($singleExternalValues as $singleValue) {
// Try matching the value. If matching fails, unset it.
try {
$mappedExternalValues[] = $this->matchSingleField($singleValue, $mappingInformation, $mappings);
}
catch (Exception $e) {
// Ignore unmapped values
}
}
if (count($mappedExternalValues) > 0) {
$records[$i][$columnName] = implode(',', $mappedExternalValues);
} else {
unset($records[$i][$columnName]);
}
}
}
}
return $records;
}
/**
* This method tries to match a single value to a table of mappings
*
* @param mixed $externalValue The value to match
* @param array $mappingInformation Mapping configuration
* @param array $mappingTable Value map
* @throws UnexpectedValueException
* @return mixed The matched value
*/
public function matchSingleField($externalValue, $mappingInformation, $mappingTable) {
$returnValue = '';
$function = $mappingInformation['match_method'];
if (!empty($externalValue)) {
$hasMatch = FALSE;
foreach ($mappingTable as $key => $value) {
$hasMatch = (call_user_func($function, $key, $externalValue) !== FALSE);
if (!empty($mappingInformation['match_symmetric'])) {
$hasMatch |= (call_user_func($function, $externalValue, $key) !== FALSE);
}
if ($hasMatch) {
$returnValue = $value;
break;
}
}
// If unmatched, throw exception
if (!$hasMatch) {
throw new UnexpectedValueException('Unmatched value ' . $externalValue, 1294739120);
}
}
return $returnValue;
}
/**
* This method applies any existing pre-processing to the data just as it was fetched,
* before any transformation.
*
* Note that this method does not do anything by itself. It just calls on a pre-processing hook.
*
* @param array $records Records containing the raw data
* @return array The processed records
*/
protected function preprocessRawData($records) {
if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF'][$this->extKey]['preprocessRawRecordset'])) {
foreach($GLOBALS['TYPO3_CONF_VARS']['EXTCONF'][$this->extKey]['preprocessRawRecordset'] as $className) {
$preProcessor = &t3lib_div::getUserObj($className);
$records = $preProcessor->preprocessRawRecordset($records, $this);
// Compact the array again, in case some values were unset in the pre-processor
$records = array_values($records);
}
}
return $records;
}
/**
* This method is used to check whether the data is ok and import should continue
* It performs a basic check on the minimum number of records expected (if defined),
* but provides a hook for more refined tests.
*
* @param array $records: records containing the raw data (after preprocessRawData())
* @return boolean True if data is valid and import should continue, false otherwise
*/
protected function validateRawData($records) {
$continueImport = TRUE;
// Check if number of records is larger than or equal to the minimum required number of records
// Note that if the minimum is not defined, this test is skipped
if (!empty($this->externalConfig['minimumRecords'])) {
$numRecords = count($records);
$continueImport = $numRecords >= $this->externalConfig['minimumRecords'];
if (!$continueImport) {
$this->addMessage(sprintf($GLOBALS['LANG']->getLL('notEnoughRecords'), $numRecords, $this->externalConfig['minimumRecords']));
}
}
// Call hooks to perform additional checks,
// but only if previous check was passed
if ($continueImport) {
if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF'][$this->extKey]['validateRawRecordset'])) {
foreach($GLOBALS['TYPO3_CONF_VARS']['EXTCONF'][$this->extKey]['validateRawRecordset'] as $className) {
$validator = &t3lib_div::getUserObj($className);
$continueImport = $validator->validateRawRecordset($records, $this);
// If a single check fails, don't call further hooks
if (!$continueImport) {
break;
}
}
}
}
return $continueImport;
}
/**
* This method applies any existing pre-processing to the data before it is stored
* (but after is has been transformed).
*
* Note that this method does not do anything by itself. It just calls on a pre-processing hook.
*
* @param array $records Records containing the data
* @return array The processed records
*/
protected function preprocessData($records) {
if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF'][$this->extKey]['preprocessRecordset'])) {
foreach($GLOBALS['TYPO3_CONF_VARS']['EXTCONF'][$this->extKey]['preprocessRecordset'] as $className) {
$preProcessor = &t3lib_div::getUserObj($className);
$records = $preProcessor->preprocessRecordset($records, $this);
// Compact the array again, in case some values were unset in the pre-processor
$records = array_values($records);
}
}
return $records;
}
/**
* This method stores the imported data in the database
* New data is inserted, existing data is updated and absent data is deleted
*
* @param array $records: records containing the data
* @return void
*/
protected function storeData($records) {
if ($this->extConf['debug'] || TYPO3_DLOG) {
t3lib_div::devLog('Data received for storage', $this->extKey, 0, $records);
}
// Initialize some variables
$fieldsExcludedFromInserts = array();
$fieldsExcludedFromUpdates = array();
// Get the list of existing uids for the table
$existingUids = $this->getExistingUids();
// Check which columns are MM-relations and get mappings to foreign tables for each
// NOTE: as it is now, it is assumed that the imported data is denormalised
//
// NOTE2: as long as we're looping on all columns, we assemble the list
// of fields that are excluded from insert or update operations
//
// There's more to do than that:
//
// 1. a sorting field may have been defined, but TCEmain assumes the MM-relations are in the right order
// and inserts its own number for the table's sorting field. So MM-relations must be sorted before executing TCEmain.
// 2.a it is possible to store additional fields in the MM-relations. This is not TYPO3-standard, so TCEmain will
// not be able to handle it. We thus need to store all that data now and rework the MM-relations when TCEmain is done.
// 2.b if a pair of records is related to each other several times (because the additional fields vary), this will be filtered out
// by TCEmain. So we must preserve also these additional relations.
$mappings = array();
$fullMappings = array();
foreach ($this->tableTCA['columns'] as $columnName => $columnData) {
// Check if some fields are excluded from some operations
// and add them to the relevant list
if (isset($columnData['external'][$this->columnIndex]['disabledOperations'])) {
if (t3lib_div::inList($columnData['external'][$this->columnIndex]['disabledOperations'], 'insert')) {
$fieldsExcludedFromInserts[] = $columnName;
}
if (t3lib_div::inList($columnData['external'][$this->columnIndex]['disabledOperations'], 'update')) {
$fieldsExcludedFromUpdates[] = $columnName;
}
}
// The "excludedOperations" property is deprecated and replaced by "disabledOperations"
// It is currently kept for backwards-compatibility reasons
// TODO: remove in next major version
if (isset($columnData['external'][$this->columnIndex]['excludedOperations'])) {
$deprecationMessage = 'Property "excludedOperations" has been deprecated. Please use "disabledOperations" instead.';
$deprecationMessage .= LF . 'Support for "excludedOperations" will be removed in external_import version 3.0.';
t3lib_div::deprecationLog($deprecationMessage);
if (t3lib_div::inList($columnData['external'][$this->columnIndex]['excludedOperations'], 'insert')) {
$fieldsExcludedFromInserts[] = $columnName;
}
if (t3lib_div::inList($columnData['external'][$this->columnIndex]['excludedOperations'], 'update')) {
$fieldsExcludedFromUpdates[] = $columnName;
}
}