1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Craft; |
4
|
|
|
|
5
|
|
|
/** |
6
|
|
|
* Export service. |
7
|
|
|
* |
8
|
|
|
* Handles common export logics. |
9
|
|
|
* |
10
|
|
|
* @author Bob Olde Hampsink <[email protected]> |
11
|
|
|
* @copyright Copyright (c) 2015, Bob Olde Hampsink |
12
|
|
|
* @license MIT |
13
|
|
|
* |
14
|
|
|
* @link http://github.com/boboldehampsink |
15
|
|
|
*/ |
16
|
|
|
class ExportService extends BaseApplicationComponent |
17
|
|
|
{ |
18
|
|
|
/** |
19
|
|
|
* Contains the working export service's name. |
20
|
|
|
* |
21
|
|
|
* @var IExportElementType |
22
|
|
|
*/ |
23
|
|
|
private $_service; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* Custom <tr> paths. |
27
|
|
|
* |
28
|
|
|
* @var array |
29
|
|
|
*/ |
30
|
|
|
public $customTableRowPaths = array(); |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* Whether custom table row paths have been loaded. |
34
|
|
|
* |
35
|
|
|
* @var bool |
36
|
|
|
*/ |
37
|
|
|
private $_loadedTableRowPaths = false; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* Saves an export map to the database. |
41
|
|
|
* |
42
|
|
|
* @param array $settings |
43
|
|
|
* @param array $map |
44
|
|
|
*/ |
45
|
2 |
|
public function saveMap(array $settings, array $map) |
46
|
|
|
{ |
47
|
|
|
// Unset non-map settings |
48
|
2 |
|
unset($settings['limit'], $settings['offset']); |
49
|
2 |
|
ksort($settings); |
50
|
|
|
|
51
|
|
|
// Set criteria |
52
|
2 |
|
$criteria = new \CDbCriteria(); |
53
|
2 |
|
$criteria->condition = 'settings = :settings'; |
54
|
2 |
|
$criteria->params = array( |
55
|
2 |
|
':settings' => JsonHelper::encode($settings), |
56
|
|
|
); |
57
|
|
|
|
58
|
|
|
// Check if we have a map already |
59
|
2 |
|
$mapRecord = $this->findMap($criteria); |
60
|
|
|
|
61
|
2 |
|
if (!count($mapRecord) || $mapRecord->settings != $settings) { |
62
|
|
|
|
63
|
|
|
// Save settings and map to database |
64
|
1 |
|
$mapRecord = $this->getNewMap(); |
65
|
1 |
|
$mapRecord->settings = $settings; |
66
|
1 |
|
} |
67
|
|
|
|
68
|
|
|
// Save new map to db |
69
|
2 |
|
$mapRecord->map = $map; |
70
|
2 |
|
$mapRecord->save(false); |
71
|
2 |
|
} |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* @codeCoverageIgnore |
75
|
|
|
* |
76
|
|
|
* @param \CDbCriteria $criteria |
77
|
|
|
* |
78
|
|
|
* @return Export_MapRecord|array|null |
79
|
|
|
*/ |
80
|
|
|
public function findMap(\CDbCriteria $criteria) |
81
|
|
|
{ |
82
|
|
|
return Export_MapRecord::model()->find($criteria); |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* @codeCoverageIgnore |
87
|
|
|
* |
88
|
|
|
* @return Export_MapRecord |
89
|
|
|
*/ |
90
|
|
|
protected function getNewMap() |
91
|
|
|
{ |
92
|
|
|
return new Export_MapRecord(); |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* Download the export csv. |
97
|
|
|
* |
98
|
|
|
* @param array $settings |
99
|
|
|
* |
100
|
|
|
* @return string |
101
|
|
|
* |
102
|
|
|
* @throws Exception |
103
|
|
|
*/ |
104
|
7 |
|
public function download(array $settings) |
105
|
|
|
{ |
106
|
|
|
// Get max power |
107
|
7 |
|
craft()->config->maxPowerCaptain(); |
108
|
|
|
|
109
|
|
|
// Check what service we're gonna need |
110
|
7 |
|
if (!($this->_service = $this->getService($settings['type']))) { |
|
|
|
|
111
|
1 |
|
throw new Exception(Craft::t('Unknown Element Type Service called.')); |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
// Get delimiter |
115
|
6 |
|
$delimiter = craft()->plugins->callFirst('registerExportCsvDelimiter'); |
116
|
6 |
|
$delimiter = is_null($delimiter) ? ',' : $delimiter; |
117
|
|
|
|
118
|
|
|
// Open output buffer |
119
|
6 |
|
ob_start(); |
120
|
|
|
|
121
|
|
|
// Write to output stream |
122
|
6 |
|
$export = fopen('php://output', 'w'); |
123
|
|
|
|
124
|
|
|
// Get data |
125
|
6 |
|
$data = $this->getData($settings); |
126
|
|
|
|
127
|
|
|
// If there is data, process |
128
|
6 |
|
if (count($data)) { |
129
|
|
|
|
130
|
|
|
// Put down columns |
131
|
3 |
|
fputcsv($export, $this->parseColumns($settings), $delimiter); |
132
|
|
|
|
133
|
|
|
// Loop trough data |
134
|
3 |
|
foreach ($data as $element) { |
|
|
|
|
135
|
|
|
|
136
|
|
|
// Fetch element in case of element id |
137
|
3 |
|
if (is_numeric($element)) { |
138
|
2 |
|
$element = craft()->elements->getElementById($element, $settings['type']); |
139
|
2 |
|
} |
140
|
|
|
|
141
|
|
|
// Get fields |
142
|
3 |
|
$fields = $this->parseFields($settings, $element); |
143
|
|
|
|
144
|
|
|
// Gather row data |
145
|
3 |
|
$rows = array(); |
146
|
|
|
|
147
|
|
|
// Loop trough the fields |
148
|
3 |
|
foreach ($fields as $handle => $data) { |
149
|
|
|
|
150
|
|
|
// Parse element data |
151
|
3 |
|
$data = $this->parseElementData($handle, $data); |
152
|
|
|
|
153
|
|
|
// Parse field data |
154
|
3 |
|
$data = $this->parseFieldData($handle, $data); |
155
|
|
|
|
156
|
|
|
// Encode and add to rows |
157
|
3 |
|
$rows[] = StringHelper::convertToUTF8($data); |
158
|
3 |
|
} |
159
|
|
|
|
160
|
|
|
// Add rows to export |
161
|
3 |
|
fputcsv($export, $rows, $delimiter); |
162
|
3 |
|
} |
163
|
3 |
|
} |
164
|
|
|
|
165
|
|
|
// Close buffer and return data |
166
|
6 |
|
fclose($export); |
167
|
6 |
|
$data = ob_get_clean(); |
168
|
|
|
|
169
|
|
|
// Use windows friendly newlines |
170
|
6 |
|
$data = str_replace("\n", "\r\n", $data); |
171
|
|
|
|
172
|
|
|
// Return the data to controller |
173
|
6 |
|
return $data; |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* Get service to use for exporting. |
178
|
|
|
* |
179
|
|
|
* @param string $elementType |
180
|
|
|
* |
181
|
|
|
* @return object|bool |
182
|
|
|
*/ |
183
|
4 |
|
public function getService($elementType) |
184
|
|
|
{ |
185
|
|
|
// Check if there's a service for this element type elsewhere |
186
|
4 |
|
$service = craft()->plugins->callFirst('registerExportService', array( |
187
|
4 |
|
'elementType' => $elementType, |
188
|
4 |
|
)); |
189
|
|
|
|
190
|
|
|
// If not, do internal check |
191
|
4 |
|
if ($service == null) { |
192
|
|
|
|
193
|
|
|
// Get from right elementType |
194
|
4 |
|
$service = 'export_'.strtolower($elementType); |
195
|
4 |
|
} |
196
|
|
|
|
197
|
|
|
// Check if elementtype can be imported |
198
|
4 |
|
if (isset(craft()->$service) && craft()->$service instanceof IExportElementType) { |
199
|
|
|
|
200
|
|
|
// Return this service |
201
|
3 |
|
return craft()->$service; |
202
|
|
|
} |
203
|
|
|
|
204
|
1 |
|
return false; |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
/** |
208
|
|
|
* Get path to fieldtype's custom <tr> template. |
209
|
|
|
* |
210
|
|
|
* @param string $fieldHandle |
211
|
|
|
* |
212
|
|
|
* @return string |
213
|
|
|
*/ |
214
|
2 |
|
public function getCustomTableRow($fieldHandle) |
215
|
|
|
{ |
216
|
|
|
// If table row paths haven't been loaded |
217
|
2 |
|
if (!$this->_loadedTableRowPaths) { |
218
|
|
|
|
219
|
|
|
// Call hook for all plugins |
220
|
2 |
|
$responses = craft()->plugins->call('registerExportTableRowPaths'); |
221
|
|
|
|
222
|
|
|
// Loop through responses from each plugin |
223
|
2 |
|
foreach ($responses as $customPaths) { |
224
|
|
|
|
225
|
|
|
// Append custom paths to master list |
226
|
1 |
|
$this->customTableRowPaths = array_merge($this->customTableRowPaths, $customPaths); |
227
|
2 |
|
} |
228
|
|
|
|
229
|
|
|
// Table row paths have been loaded |
230
|
2 |
|
$this->_loadedTableRowPaths = true; |
231
|
2 |
|
} |
232
|
|
|
|
233
|
|
|
// If fieldtype has been registered and is not falsey |
234
|
2 |
|
if (array_key_exists($fieldHandle, $this->customTableRowPaths) && $this->customTableRowPaths[$fieldHandle]) { |
235
|
|
|
|
236
|
|
|
// Return specified custom path |
237
|
1 |
|
return $this->customTableRowPaths[$fieldHandle]; |
238
|
|
|
} |
239
|
|
|
|
240
|
1 |
|
return false; |
|
|
|
|
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
/** |
244
|
|
|
* Get data from sources. |
245
|
|
|
* |
246
|
|
|
* @param array $settings |
247
|
|
|
* |
248
|
|
|
* @return array |
249
|
|
|
*/ |
250
|
6 |
|
protected function getData(array $settings) |
251
|
|
|
{ |
252
|
|
|
// Get other sources |
253
|
6 |
|
$sources = craft()->plugins->call('registerExportSource', array($settings)); |
254
|
|
|
|
255
|
|
|
// Loop through sources, see if we can get any data |
256
|
6 |
|
$data = array(); |
257
|
6 |
|
foreach ($sources as $plugin) { |
258
|
1 |
|
if (is_array($plugin)) { |
259
|
1 |
|
foreach ($plugin as $source) { |
260
|
1 |
|
$data[] = $source; |
261
|
1 |
|
} |
262
|
1 |
|
} |
263
|
6 |
|
} |
264
|
|
|
|
265
|
|
|
// Cut up data from source |
266
|
6 |
|
if (!empty($settings['offset']) && !empty($settings['limit'])) { |
267
|
|
|
$data = array_slice($data, $settings['offset'], $settings['limit']); |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
// If no data from source, get data by ourselves |
271
|
6 |
|
if (!count($data)) { |
272
|
|
|
|
273
|
|
|
// Find data |
274
|
5 |
|
$criteria = $this->_service->setCriteria($settings); |
275
|
|
|
|
276
|
|
|
// Gather element ids |
277
|
5 |
|
$data = $criteria->ids(); |
278
|
5 |
|
} |
279
|
|
|
|
280
|
6 |
|
return $data; |
281
|
|
|
} |
282
|
|
|
|
283
|
|
|
/** |
284
|
|
|
* Parse fields. |
285
|
|
|
* |
286
|
|
|
* @param array $settings |
287
|
|
|
* @param mixed $element |
288
|
|
|
* |
289
|
|
|
* @return array |
290
|
|
|
*/ |
291
|
3 |
|
protected function parseFields(array $settings, $element) |
292
|
|
|
{ |
293
|
3 |
|
$fields = array(); |
294
|
|
|
|
295
|
|
|
// Only get element attributes and content attributes |
296
|
3 |
|
$attributes = $element; |
297
|
3 |
|
if ($element instanceof BaseElementModel) { |
298
|
|
|
|
299
|
|
|
// Get service |
300
|
2 |
|
$attributes = $this->_service->getAttributes($settings['map'], $element); |
301
|
2 |
|
} |
302
|
|
|
|
303
|
|
|
// Loop through the map |
304
|
3 |
|
foreach ($settings['map'] as $handle => $data) { |
305
|
|
|
|
306
|
|
|
// Only get checked fields |
307
|
3 |
|
if ($data['checked'] == '1' && (array_key_exists($handle, $attributes) || array_key_exists(substr($handle, 0, 5), $attributes))) { |
308
|
|
|
|
309
|
|
|
// Fill them with data |
310
|
3 |
|
$fields[$handle] = $attributes[$handle]; |
311
|
3 |
|
} |
312
|
3 |
|
} |
313
|
|
|
|
314
|
3 |
|
return $fields; |
315
|
|
|
} |
316
|
|
|
|
317
|
|
|
/** |
318
|
|
|
* Parse column names. |
319
|
|
|
* |
320
|
|
|
* @param array $settings |
321
|
|
|
* |
322
|
|
|
* @return string |
323
|
|
|
*/ |
324
|
3 |
|
protected function parseColumns(array $settings) |
325
|
|
|
{ |
326
|
3 |
|
$columns = array(); |
327
|
|
|
|
328
|
|
|
// Loop trough map |
329
|
3 |
|
foreach ($settings['map'] as $handle => $data) { |
330
|
|
|
|
331
|
|
|
// If checked |
332
|
3 |
|
if ($data['checked'] == 1) { |
333
|
|
|
|
334
|
|
|
// Add column |
335
|
3 |
|
$columns[] = StringHelper::convertToUTF8($data['label']); |
336
|
3 |
|
} |
337
|
3 |
|
} |
338
|
|
|
|
339
|
3 |
|
return $columns; |
340
|
|
|
} |
341
|
|
|
|
342
|
|
|
/** |
343
|
|
|
* Parse reserved element values. |
344
|
|
|
* |
345
|
|
|
* @param string $handle |
346
|
|
|
* @param string $data |
347
|
|
|
* |
348
|
|
|
* @return string |
349
|
|
|
*/ |
350
|
3 |
|
protected function parseElementData($handle, $data) |
351
|
|
|
{ |
352
|
|
|
switch ($handle) { |
353
|
|
|
|
354
|
|
|
// Get username of author |
355
|
3 |
|
case ExportModel::HandleAuthor: |
356
|
1 |
|
return craft()->users->getUserById($data)->username; |
357
|
|
|
|
358
|
|
|
// Make data human readable |
359
|
3 |
|
case ExportModel::HandleEnabled: |
360
|
2 |
|
return $data == '0' ? Craft::t('No') : Craft::t('Yes'); |
361
|
|
|
|
362
|
3 |
|
case ExportModel::HandlePostDate: |
363
|
3 |
|
case ExportModel::HandleExpiryDate: |
364
|
1 |
|
return (string) $data; |
365
|
|
|
|
366
|
|
|
} |
367
|
|
|
|
368
|
3 |
|
return $data; |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
/** |
372
|
|
|
* Parse field values. |
373
|
|
|
* |
374
|
|
|
* @param string $handle |
375
|
|
|
* @param string $data |
376
|
|
|
* |
377
|
|
|
* @return string |
378
|
|
|
*/ |
379
|
11 |
|
public function parseFieldData($handle, $data) |
380
|
|
|
{ |
381
|
|
|
|
382
|
|
|
// Do we have any data at all |
383
|
11 |
|
if (!is_null($data)) { |
384
|
|
|
|
385
|
|
|
// Get field info |
386
|
11 |
|
$field = craft()->fields->getFieldByHandle($handle); |
387
|
|
|
|
388
|
|
|
// If it's a field ofcourse |
389
|
11 |
|
if (!is_null($field)) { |
390
|
|
|
|
391
|
|
|
// For some fieldtypes the're special rules |
392
|
11 |
|
switch ($field->type) { |
393
|
|
|
|
394
|
11 |
|
case ExportModel::FieldTypeEntries: |
395
|
11 |
|
case ExportModel::FieldTypeCategories: |
396
|
11 |
|
case ExportModel::FieldTypeAssets: |
397
|
11 |
|
case ExportModel::FieldTypeUsers: |
398
|
|
|
// Show names |
399
|
1 |
|
$data = $data instanceof ElementCriteriaModel ? implode(', ', $data->find()) : $data; |
400
|
1 |
|
break; |
401
|
|
|
|
402
|
10 |
|
case ExportModel::FieldTypeLightswitch: |
403
|
|
|
// Make data human readable |
404
|
|
|
switch ($data) { |
405
|
|
|
|
406
|
2 |
|
case '0': |
407
|
1 |
|
$data = Craft::t('No'); |
408
|
1 |
|
break; |
409
|
|
|
|
410
|
1 |
|
case '1': |
411
|
1 |
|
$data = Craft::t('Yes'); |
412
|
1 |
|
break; |
413
|
|
|
|
414
|
|
|
} |
415
|
2 |
|
break; |
416
|
|
|
|
417
|
8 |
|
case ExportModel::FieldTypeTable: |
418
|
|
|
// Parse table checkboxes |
419
|
1 |
|
$table = array(); |
420
|
1 |
|
foreach ($data as $row) { |
|
|
|
|
421
|
|
|
|
422
|
|
|
// Keep track of column # |
423
|
1 |
|
$i = 1; |
424
|
|
|
|
425
|
|
|
// Loop through columns |
426
|
1 |
|
foreach ($row as $column => $value) { |
427
|
|
|
|
428
|
|
|
// Get column |
429
|
1 |
|
$column = isset($field->settings['columns'][$column]) ? $field->settings['columns'][$column] : (isset($field->settings['columns']['col'.$i]) ? $field->settings['columns']['col'.$i] : array('type' => 'dummy')); |
430
|
|
|
|
431
|
|
|
// Keep track of column # |
432
|
1 |
|
++$i; |
433
|
|
|
|
434
|
|
|
// Parse |
435
|
1 |
|
$table[] = $column['type'] == 'checkbox' ? ($value == 1 ? Craft::t('Yes') : Craft::t('No')) : $value; |
436
|
1 |
|
} |
437
|
1 |
|
} |
438
|
|
|
|
439
|
|
|
// Return parsed data as array |
440
|
1 |
|
$data = $table; |
441
|
1 |
|
break; |
442
|
|
|
|
443
|
7 |
|
case ExportModel::FieldTypeRichText: |
444
|
7 |
|
case ExportModel::FieldTypeDate: |
445
|
7 |
|
case ExportModel::FieldTypeRadioButtons: |
446
|
7 |
|
case ExportModel::FieldTypeDropdown: |
447
|
|
|
// Resolve to string |
448
|
1 |
|
$data = (string) $data; |
449
|
1 |
|
break; |
450
|
|
|
|
451
|
6 |
|
case ExportModel::FieldTypeCheckboxes: |
452
|
6 |
|
case ExportModel::FieldTypeMultiSelect: |
453
|
|
|
// Parse multi select values |
454
|
4 |
|
$multi = array(); |
455
|
1 |
|
foreach ($data as $row) { |
|
|
|
|
456
|
1 |
|
$multi[] = $row->value; |
457
|
1 |
|
} |
458
|
|
|
|
459
|
|
|
// Return parsed data as array |
460
|
1 |
|
$data = $multi; |
461
|
1 |
|
break; |
462
|
|
|
|
463
|
11 |
|
} |
464
|
11 |
|
} |
465
|
|
|
|
466
|
|
|
// Get other operations |
467
|
11 |
|
craft()->plugins->call('registerExportOperation', array(&$data, $handle)); |
468
|
11 |
|
} else { |
469
|
|
|
|
470
|
|
|
// Don't return null, return empty |
471
|
1 |
|
$data = ''; |
472
|
|
|
} |
473
|
|
|
|
474
|
|
|
// If it's an object or an array, make it a string |
475
|
11 |
|
if (is_array($data)) { |
476
|
2 |
|
$data = StringHelper::arrayToString(ArrayHelper::filterEmptyStringsFromArray(ArrayHelper::flattenArray($data)), ', '); |
477
|
2 |
|
} |
478
|
|
|
|
479
|
|
|
// If it's an object, make it a string |
480
|
11 |
|
if (is_object($data)) { |
481
|
1 |
|
$data = StringHelper::arrayToString(ArrayHelper::filterEmptyStringsFromArray(ArrayHelper::flattenArray(get_object_vars($data))), ', '); |
482
|
1 |
|
} |
483
|
|
|
|
484
|
11 |
|
return $data; |
485
|
|
|
} |
486
|
|
|
} |
487
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.