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|bool |
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 (is_array($data) && count($data)) { |
129
|
|
|
|
130
|
|
|
// Put down columns |
131
|
3 |
|
fputcsv($export, $this->parseColumns($settings), $delimiter); |
132
|
|
|
|
133
|
|
|
// Loop through 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|bool |
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 (array_key_exists('offset', $settings) && is_numeric($settings['offset'])) { |
267
|
2 |
|
$data = array_slice($data, $settings['offset'], $settings['limit']); |
268
|
2 |
|
} |
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 through 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 mixed $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 |
|
if (is_array($data)) { |
421
|
1 |
|
foreach ($data as $row) { |
422
|
|
|
|
423
|
|
|
// Keep track of column # |
424
|
1 |
|
$i = 1; |
425
|
|
|
|
426
|
|
|
// Loop through columns |
427
|
1 |
|
foreach ($row as $column => $value) { |
428
|
|
|
|
429
|
|
|
// Get column |
430
|
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')); |
431
|
|
|
|
432
|
|
|
// Keep track of column # |
433
|
1 |
|
++$i; |
434
|
|
|
|
435
|
|
|
// Parse |
436
|
1 |
|
$table[] = $column['type'] == 'checkbox' ? ($value == 1 ? Craft::t('Yes') : Craft::t('No')) : $value; |
437
|
1 |
|
} |
438
|
1 |
|
} |
439
|
1 |
|
} |
440
|
|
|
|
441
|
|
|
// Return parsed data as array |
442
|
1 |
|
$data = $table; |
443
|
1 |
|
break; |
444
|
|
|
|
445
|
7 |
|
case ExportModel::FieldTypeRichText: |
446
|
7 |
|
case ExportModel::FieldTypeDate: |
447
|
7 |
|
case ExportModel::FieldTypeRadioButtons: |
448
|
7 |
|
case ExportModel::FieldTypeDropdown: |
449
|
|
|
// Resolve to string |
450
|
1 |
|
$data = (string) $data; |
451
|
1 |
|
break; |
452
|
|
|
|
453
|
6 |
|
case ExportModel::FieldTypeCheckboxes: |
454
|
6 |
|
case ExportModel::FieldTypeMultiSelect: |
455
|
|
|
// Parse multi select values |
456
|
1 |
|
$multi = array(); |
457
|
1 |
|
if (is_array($data)) { |
458
|
1 |
|
foreach ($data as $row) { |
459
|
1 |
|
$multi[] = $row->value; |
460
|
1 |
|
} |
461
|
1 |
|
} |
462
|
|
|
|
463
|
|
|
// Return parsed data as array |
464
|
1 |
|
$data = $multi; |
465
|
1 |
|
break; |
466
|
|
|
|
467
|
11 |
|
} |
468
|
11 |
|
} |
469
|
|
|
|
470
|
|
|
// Get other operations |
471
|
11 |
|
craft()->plugins->call('registerExportOperation', array(&$data, $handle)); |
472
|
11 |
|
} else { |
473
|
|
|
|
474
|
|
|
// Don't return null, return empty |
475
|
1 |
|
$data = ''; |
476
|
|
|
} |
477
|
|
|
|
478
|
|
|
// If it's an object or an array, make it a string |
479
|
11 |
|
if (is_array($data)) { |
480
|
2 |
|
$data = StringHelper::arrayToString(ArrayHelper::filterEmptyStringsFromArray(ArrayHelper::flattenArray($data)), ', '); |
481
|
2 |
|
} |
482
|
|
|
|
483
|
|
|
// If it's an object, make it a string |
484
|
11 |
|
if (is_object($data)) { |
485
|
1 |
|
$data = StringHelper::arrayToString(ArrayHelper::filterEmptyStringsFromArray(ArrayHelper::flattenArray(get_object_vars($data))), ', '); |
486
|
1 |
|
} |
487
|
|
|
|
488
|
11 |
|
return $data; |
489
|
|
|
} |
490
|
|
|
} |
491
|
|
|
|
There are different options of fixing this problem.
If you want to be on the safe side, you can add an additional type-check:
If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:
Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.