GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — develop ( 275d09...a512c7 )
by Bob Olde
02:56
created

ExportService::parseElementData()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 6

Importance

Changes 10
Bugs 4 Features 1
Metric Value
c 10
b 4
f 1
dl 0
loc 20
ccs 9
cts 9
cp 1
rs 8.8571
cc 6
eloc 10
nc 6
nop 2
crap 6
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']))) {
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getService($settings['type']) can also be of type boolean. However, the property $_service is declared as type object<Craft\IExportElementType>. Maybe add an additional type check?

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 the id property of an instance of the Account 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.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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) {
0 ignored issues
show
Bug introduced by
The expression $data of type array|string is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. 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:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
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;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by Craft\ExportService::getCustomTableRow of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
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) {
0 ignored issues
show
Bug introduced by
The expression $data of type string is not traversable.
Loading history...
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) {
0 ignored issues
show
Bug introduced by
The expression $data of type string is not traversable.
Loading history...
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