Passed
Pull Request — 4.8 (#9977)
by Ingo
07:24
created

src/Forms/GridField/GridField.php (1 issue)

Labels
Severity
1
<?php
2
3
namespace SilverStripe\Forms\GridField;
4
5
use InvalidArgumentException;
6
use LogicException;
7
use SilverStripe\Control\HasRequestHandler;
8
use SilverStripe\Control\HTTPRequest;
9
use SilverStripe\Control\HTTPResponse;
10
use SilverStripe\Control\HTTPResponse_Exception;
11
use SilverStripe\Control\RequestHandler;
12
use SilverStripe\Core\Injector\Injector;
13
use SilverStripe\Forms\Form;
14
use SilverStripe\Forms\FormField;
15
use SilverStripe\Forms\GridField\FormAction\SessionStore;
16
use SilverStripe\Forms\GridField\FormAction\StateStore;
17
use SilverStripe\ORM\ArrayList;
18
use SilverStripe\ORM\DataList;
19
use SilverStripe\ORM\DataObject;
20
use SilverStripe\ORM\DataObjectInterface;
21
use SilverStripe\ORM\FieldType\DBField;
22
use SilverStripe\ORM\SS_List;
23
use SilverStripe\View\HTML;
24
25
/**
26
 * Displays a {@link SS_List} in a grid format.
27
 *
28
 * GridField is a field that takes an SS_List and displays it in an table with rows and columns.
29
 * It reminds of the old TableFields but works with SS_List types and only loads the necessary
30
 * rows from the list.
31
 *
32
 * The minimum configuration is to pass in name and title of the field and a SS_List.
33
 *
34
 * <code>
35
 * $gridField = new GridField('ExampleGrid', 'Example grid', new DataList('Page'));
36
 * </code>
37
 *
38
 * Caution: The form field does not include any JavaScript or CSS when used outside of the CMS context,
39
 * since the required frontend dependencies are included through CMS bundling.
40
 *
41
 * @see SS_List
42
 *
43
 * @property GridState_Data $State The gridstate of this object
44
 */
45
class GridField extends FormField
46
{
47
    /**
48
     * @var array
49
     */
50
    private static $allowed_actions = [
51
        'index',
52
        'gridFieldAlterAction',
53
    ];
54
55
    /**
56
     * Data source.
57
     *
58
     * @var SS_List
59
     */
60
    protected $list = null;
61
62
    /**
63
     * Class name of the DataObject that the GridField will display.
64
     *
65
     * Defaults to the value of $this->list->dataClass.
66
     *
67
     * @var string
68
     */
69
    protected $modelClassName = '';
70
71
    /**
72
     * Current state of the GridField.
73
     *
74
     * @var GridState
75
     */
76
    protected $state = null;
77
78
    /**
79
     * @var GridFieldConfig
80
     */
81
    protected $config = null;
82
83
    /**
84
     * Components list.
85
     *
86
     * @var array
87
     */
88
    protected $components = [];
89
90
    /**
91
     * Internal dispatcher for column handlers.
92
     *
93
     * Keys are column names and values are GridField_ColumnProvider objects.
94
     *
95
     * @var array
96
     */
97
    protected $columnDispatch = null;
98
99
    /**
100
     * Map of callbacks for custom data fields.
101
     *
102
     * @var array
103
     */
104
    protected $customDataFields = [];
105
106
    /**
107
     * @var string
108
     */
109
    protected $name = '';
110
111
    /**
112
     * A whitelist of readonly component classes allowed if performReadonlyTransform is called.
113
     *
114
     * @var array
115
     */
116
    protected $readonlyComponents = [
117
        GridField_ActionMenu::class,
118
        GridFieldConfig_RecordViewer::class,
119
        GridFieldButtonRow::class,
120
        GridFieldDataColumns::class,
121
        GridFieldDetailForm::class,
122
        GridFieldLazyLoader::class,
123
        GridFieldPageCount::class,
124
        GridFieldPaginator::class,
125
        GridFieldFilterHeader::class,
126
        GridFieldSortableHeader::class,
127
        GridFieldToolbarHeader::class,
128
        GridFieldViewButton::class,
129
        GridState_Component::class,
130
    ];
131
132
    /**
133
     * Pattern used for looking up
134
     */
135
    const FRAGMENT_REGEX = '/\$DefineFragment\(([a-z0-9\-_]+)\)/i';
136
137
    /**
138
     * @param string $name
139
     * @param string $title
140
     * @param SS_List $dataList
141
     * @param GridFieldConfig $config
142
     */
143
    public function __construct($name, $title = null, SS_List $dataList = null, GridFieldConfig $config = null)
144
    {
145
        parent::__construct($name, $title, null);
146
147
        $this->name = $name;
148
149
        if ($dataList) {
150
            $this->setList($dataList);
151
        }
152
153
        if (!$config) {
154
            $config = GridFieldConfig_Base::create();
155
        }
156
157
        $this->setConfig($config);
0 ignored issues
show
It seems like $config can also be of type null; however, parameter $config of SilverStripe\Forms\GridF...\GridField::setConfig() does only seem to accept SilverStripe\Forms\GridField\GridFieldConfig, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

157
        $this->setConfig(/** @scrutinizer ignore-type */ $config);
Loading history...
158
159
        $this->addExtraClass('grid-field');
160
    }
161
162
    /**
163
     * @param HTTPRequest $request
164
     *
165
     * @return string
166
     */
167
    public function index($request)
168
    {
169
        return $this->gridFieldAlterAction([], $this->getForm(), $request);
170
    }
171
172
    /**
173
     * Set the modelClass (data object) that this field will get it column headers from.
174
     *
175
     * If no $displayFields has been set, the display fields will be $summary_fields.
176
     *
177
     * @see GridFieldDataColumns::getDisplayFields()
178
     *
179
     * @param string $modelClassName
180
     *
181
     * @return $this
182
     */
183
    public function setModelClass($modelClassName)
184
    {
185
        $this->modelClassName = $modelClassName;
186
187
        return $this;
188
    }
189
190
    /**
191
     * Returns a data class that is a DataObject type that this GridField should look like.
192
     *
193
     * @return string
194
     *
195
     * @throws LogicException
196
     */
197
    public function getModelClass()
198
    {
199
        if ($this->modelClassName) {
200
            return $this->modelClassName;
201
        }
202
203
        /** @var DataList|ArrayList $list */
204
        $list = $this->list;
205
        if ($list && $list->hasMethod('dataClass')) {
206
            $class = $list->dataClass();
207
208
            if ($class) {
209
                return $class;
210
            }
211
        }
212
213
        throw new LogicException(
214
            'GridField doesn\'t have a modelClassName, so it doesn\'t know the columns of this grid.'
215
        );
216
    }
217
218
    /**
219
     * Overload the readonly components for this gridfield.
220
     *
221
     * @param array $components an array map of component class references to whitelist for a readonly version.
222
     */
223
    public function setReadonlyComponents(array $components)
224
    {
225
        $this->readonlyComponents = $components;
226
    }
227
228
    /**
229
     * Return the readonly components
230
     *
231
     * @return array a map of component classes.
232
     */
233
    public function getReadonlyComponents()
234
    {
235
        return $this->readonlyComponents;
236
    }
237
238
    /**
239
     * Custom Readonly transformation to remove actions which shouldn't be present for a readonly state.
240
     *
241
     * @return GridField
242
     */
243
    public function performReadonlyTransformation()
244
    {
245
        $copy = clone $this;
246
        $copy->setReadonly(true);
247
        $copyConfig = $copy->getConfig();
248
        $hadEditButton = $copyConfig->getComponentByType(GridFieldEditButton::class) !== null;
249
250
        // get the whitelist for allowable readonly components
251
        $allowedComponents = $this->getReadonlyComponents();
252
        foreach ($this->getConfig()->getComponents() as $component) {
253
            // if a component doesn't exist, remove it from the readonly version.
254
            if (!in_array(get_class($component), $allowedComponents)) {
255
                $copyConfig->removeComponent($component);
256
            }
257
        }
258
259
        // If the edit button has been removed, replace it with a view button
260
        if ($hadEditButton && !$copyConfig->getComponentByType(GridFieldViewButton::class)) {
261
            $copyConfig->addComponent(new GridFieldViewButton);
262
        }
263
264
        return $copy;
265
    }
266
267
    /**
268
     * Disabling the gridfield should have the same affect as making it readonly (removing all action items).
269
     *
270
     * @return GridField
271
     */
272
    public function performDisabledTransformation()
273
    {
274
        parent::performDisabledTransformation();
275
276
        return $this->performReadonlyTransformation();
277
    }
278
279
    /**
280
     * @return GridFieldConfig
281
     */
282
    public function getConfig()
283
    {
284
        return $this->config;
285
    }
286
287
    /**
288
     * @param GridFieldConfig $config
289
     *
290
     * @return $this
291
     */
292
    public function setConfig(GridFieldConfig $config)
293
    {
294
        $this->config = $config;
295
296
        if (!$this->config->getComponentByType(GridState_Component::class)) {
297
            $this->config->addComponent(new GridState_Component());
298
        }
299
300
        return $this;
301
    }
302
303
    /**
304
     * @param bool $readonly
305
     *
306
     * @return $this
307
     */
308
    public function setReadonly($readonly)
309
    {
310
        parent::setReadonly($readonly);
311
        $this->getState()->Readonly = $readonly;
312
        return $this;
313
    }
314
315
    /**
316
     * @return ArrayList
317
     */
318
    public function getComponents()
319
    {
320
        return $this->config->getComponents();
321
    }
322
323
    /**
324
     * Cast an arbitrary value with the help of a $castingDefinition.
325
     *
326
     * @todo refactor this into GridFieldComponent
327
     *
328
     * @param mixed $value
329
     * @param string|array $castingDefinition
330
     *
331
     * @return mixed
332
     */
333
    public function getCastedValue($value, $castingDefinition)
334
    {
335
        $castingParams = [];
336
337
        if (is_array($castingDefinition)) {
338
            $castingParams = $castingDefinition;
339
            array_shift($castingParams);
340
            $castingDefinition = array_shift($castingDefinition);
341
        }
342
343
        if (strpos($castingDefinition, '->') === false) {
344
            $castingFieldType = $castingDefinition;
345
            $castingField = DBField::create_field($castingFieldType, $value);
346
347
            return call_user_func_array([$castingField, 'XML'], $castingParams);
348
        }
349
350
        list($castingFieldType, $castingMethod) = explode('->', $castingDefinition);
351
352
        $castingField = DBField::create_field($castingFieldType, $value);
353
354
        return call_user_func_array([$castingField, $castingMethod], $castingParams);
355
    }
356
357
    /**
358
     * Set the data source.
359
     *
360
     * @param SS_List $list
361
     *
362
     * @return $this
363
     */
364
    public function setList(SS_List $list)
365
    {
366
        $this->list = $list;
367
368
        return $this;
369
    }
370
371
    /**
372
     * Get the data source.
373
     *
374
     * @return SS_List
375
     */
376
    public function getList()
377
    {
378
        return $this->list;
379
    }
380
381
    /**
382
     * Get the data source after applying every {@link GridField_DataManipulator} to it.
383
     *
384
     * @return SS_List
385
     */
386
    public function getManipulatedList()
387
    {
388
        $list = $this->getList();
389
390
        foreach ($this->getComponents() as $item) {
391
            if ($item instanceof GridField_DataManipulator) {
392
                $list = $item->getManipulatedData($this, $list);
393
            }
394
        }
395
396
        return $list;
397
    }
398
399
    /**
400
     * Get the current GridState_Data or the GridState.
401
     *
402
     * @param bool $getData
403
     *
404
     * @return GridState_Data|GridState
405
     */
406
    public function getState($getData = true)
407
    {
408
        // Initialise state on first call. This ensures it's evaluated after components have been added
409
        if (!$this->state) {
410
            $this->initState();
411
        }
412
413
        if ($getData) {
414
            return $this->state->getData();
415
        }
416
417
        return $this->state;
418
    }
419
420
    private function initState(): void
421
    {
422
        $this->state = new GridState($this);
423
424
        $data = $this->state->getData();
425
426
        foreach ($this->getComponents() as $item) {
427
            if ($item instanceof GridField_StateProvider) {
428
                $item->initDefaultState($data);
429
            }
430
        }
431
    }
432
433
    /**
434
     * Returns the whole gridfield rendered with all the attached components.
435
     *
436
     * @param array $properties
437
     * @return string
438
     */
439
    public function FieldHolder($properties = [])
440
    {
441
        $columns = $this->getColumns();
442
443
        $list = $this->getManipulatedList();
444
445
        $content = [
446
            'before' => '',
447
            'after' => '',
448
            'header' => '',
449
            'footer' => '',
450
        ];
451
452
        foreach ($this->getComponents() as $item) {
453
            if ($item instanceof GridField_HTMLProvider) {
454
                $fragments = $item->getHTMLFragments($this);
455
456
                if ($fragments) {
457
                    foreach ($fragments as $fragmentKey => $fragmentValue) {
458
                        $fragmentKey = strtolower($fragmentKey);
459
460
                        if (!isset($content[$fragmentKey])) {
461
                            $content[$fragmentKey] = '';
462
                        }
463
464
                        $content[$fragmentKey] .= $fragmentValue . "\n";
465
                    }
466
                }
467
            }
468
        }
469
470
        foreach ($content as $contentKey => $contentValue) {
471
            $content[$contentKey] = trim($contentValue);
472
        }
473
474
        // Replace custom fragments and check which fragments are defined. Circular dependencies
475
        // are detected by disallowing any item to be deferred more than 5 times.
476
477
        $fragmentDefined = [
478
            'header' => true,
479
            'footer' => true,
480
            'before' => true,
481
            'after' => true,
482
        ];
483
        $fragmentDeferred = [];
484
485
        // TODO: Break the below into separate reducer methods
486
487
        // Continue looping if any placeholders exist
488
        while (array_filter($content, function ($value) {
489
            return preg_match(self::FRAGMENT_REGEX, $value);
490
        })) {
491
            foreach ($content as $contentKey => $contentValue) {
492
                // Skip if this specific content has no placeholders
493
                if (!preg_match_all(self::FRAGMENT_REGEX, $contentValue, $matches)) {
494
                    continue;
495
                }
496
                foreach ($matches[1] as $match) {
497
                    $fragmentName = strtolower($match);
498
                    $fragmentDefined[$fragmentName] = true;
499
500
                    $fragment = '';
501
502
                    if (isset($content[$fragmentName])) {
503
                        $fragment = $content[$fragmentName];
504
                    }
505
506
                    // If the fragment still has a fragment definition in it, when we should defer
507
                    // this item until later.
508
509
                    if (preg_match(self::FRAGMENT_REGEX, $fragment, $matches)) {
510
                        if (isset($fragmentDeferred[$contentKey]) && $fragmentDeferred[$contentKey] > 5) {
511
                            throw new LogicException(sprintf(
512
                                'GridField HTML fragment "%s" and "%s" appear to have a circular dependency.',
513
                                $fragmentName,
514
                                $matches[1]
515
                            ));
516
                        }
517
518
                        unset($content[$contentKey]);
519
520
                        $content[$contentKey] = $contentValue;
521
522
                        if (!isset($fragmentDeferred[$contentKey])) {
523
                            $fragmentDeferred[$contentKey] = 0;
524
                        }
525
526
                        $fragmentDeferred[$contentKey]++;
527
528
                        break;
529
                    } else {
530
                        $content[$contentKey] = preg_replace(
531
                            sprintf('/\$DefineFragment\(%s\)/i', $fragmentName),
532
                            $fragment,
533
                            $content[$contentKey]
534
                        );
535
                    }
536
                }
537
            }
538
        }
539
540
        // Check for any undefined fragments, and if so throw an exception.
541
        // While we're at it, trim whitespace off the elements.
542
543
        foreach ($content as $contentKey => $contentValue) {
544
            if (empty($fragmentDefined[$contentKey])) {
545
                throw new LogicException(sprintf(
546
                    'GridField HTML fragment "%s" was given content, but not defined. Perhaps there is a supporting GridField component you need to add?',
547
                    $contentKey
548
                ));
549
            }
550
        }
551
552
        $total = count($list);
553
554
        if ($total > 0) {
555
            $rows = [];
556
557
            foreach ($list as $index => $record) {
558
                if ($record->hasMethod('canView') && !$record->canView()) {
559
                    continue;
560
                }
561
562
                $rowContent = '';
563
564
                foreach ($this->getColumns() as $column) {
565
                    $colContent = $this->getColumnContent($record, $column);
566
567
                    // Null means this columns should be skipped altogether.
568
569
                    if ($colContent === null) {
570
                        continue;
571
                    }
572
573
                    $colAttributes = $this->getColumnAttributes($record, $column);
574
575
                    $rowContent .= $this->newCell(
576
                        $total,
577
                        $index,
578
                        $record,
579
                        $colAttributes,
580
                        $colContent
581
                    );
582
                }
583
584
                $rowAttributes = $this->getRowAttributes($total, $index, $record);
585
586
                $rows[] = $this->newRow($total, $index, $record, $rowAttributes, $rowContent);
587
            }
588
            $content['body'] = implode("\n", $rows);
589
        }
590
591
        // Display a message when the grid field is empty.
592
        if (empty($content['body'])) {
593
            $cell = HTML::createTag(
594
                'td',
595
                [
596
                    'colspan' => count($columns),
597
                ],
598
                _t('SilverStripe\\Forms\\GridField\\GridField.NoItemsFound', 'No items found')
599
            );
600
601
            $row = HTML::createTag(
602
                'tr',
603
                [
604
                    'class' => 'ss-gridfield-item ss-gridfield-no-items',
605
                ],
606
                $cell
607
            );
608
609
            $content['body'] = $row;
610
        }
611
612
        $header = $this->getOptionalTableHeader($content);
613
        $body = $this->getOptionalTableBody($content);
614
        $footer = $this->getOptionalTableFooter($content);
615
616
        $this->addExtraClass('ss-gridfield grid-field field');
617
618
        $fieldsetAttributes = array_diff_key(
619
            $this->getAttributes(),
620
            [
621
                'value' => false,
622
                'type' => false,
623
                'name' => false,
624
            ]
625
        );
626
627
        $fieldsetAttributes['data-name'] = $this->getName();
628
629
        $tableId = null;
630
631
        if ($this->id) {
632
            $tableId = $this->id;
633
        }
634
635
        $tableAttributes = [
636
            'id' => $tableId,
637
            'class' => 'table grid-field__table',
638
            'cellpadding' => '0',
639
            'cellspacing' => '0'
640
        ];
641
642
        if ($this->getDescription()) {
643
            $content['after'] .= HTML::createTag(
644
                'span',
645
                ['class' => 'description'],
646
                $this->getDescription()
647
            );
648
        }
649
650
        $table = HTML::createTag(
651
            'table',
652
            $tableAttributes,
653
            $header . "\n" . $footer . "\n" . $body
654
        );
655
656
        return HTML::createTag(
657
            'fieldset',
658
            $fieldsetAttributes,
659
            $content['before'] . $table . $content['after']
660
        );
661
    }
662
663
    /**
664
     * @param int $total
665
     * @param int $index
666
     * @param DataObject $record
667
     * @param array $attributes
668
     * @param string $content
669
     *
670
     * @return string
671
     */
672
    protected function newCell($total, $index, $record, $attributes, $content)
673
    {
674
        return HTML::createTag(
675
            'td',
676
            $attributes,
677
            $content
678
        );
679
    }
680
681
    /**
682
     * @param int $total
683
     * @param int $index
684
     * @param DataObject $record
685
     * @param array $attributes
686
     * @param string $content
687
     *
688
     * @return string
689
     */
690
    protected function newRow($total, $index, $record, $attributes, $content)
691
    {
692
        return HTML::createTag(
693
            'tr',
694
            $attributes,
695
            $content
696
        );
697
    }
698
699
    /**
700
     * @param int $total
701
     * @param int $index
702
     * @param DataObject $record
703
     *
704
     * @return array
705
     */
706
    protected function getRowAttributes($total, $index, $record)
707
    {
708
        $rowClasses = $this->newRowClasses($total, $index, $record);
709
710
        return [
711
            'class' => implode(' ', $rowClasses),
712
            'data-id' => $record->ID,
713
            'data-class' => $record->ClassName,
714
        ];
715
    }
716
717
    /**
718
     * @param int $total
719
     * @param int $index
720
     * @param DataObject $record
721
     *
722
     * @return array
723
     */
724
    protected function newRowClasses($total, $index, $record)
725
    {
726
        $classes = ['ss-gridfield-item'];
727
728
        if ($index == 0) {
729
            $classes[] = 'first';
730
        }
731
732
        if ($index == $total - 1) {
733
            $classes[] = 'last';
734
        }
735
736
        if ($index % 2) {
737
            $classes[] = 'even';
738
        } else {
739
            $classes[] = 'odd';
740
        }
741
742
        $this->extend('updateNewRowClasses', $classes, $total, $index, $record);
743
744
        return $classes;
745
    }
746
747
    /**
748
     * @param array $properties
749
     * @return string
750
     */
751
    public function Field($properties = [])
752
    {
753
        $this->extend('onBeforeRender', $this);
754
        return $this->FieldHolder($properties);
755
    }
756
757
    /**
758
     * {@inheritdoc}
759
     */
760
    public function getAttributes()
761
    {
762
        return array_merge(
763
            parent::getAttributes(),
764
            [
765
                'data-url' => $this->Link(),
766
            ]
767
        );
768
    }
769
770
    /**
771
     * Get the columns of this GridField, they are provided by attached GridField_ColumnProvider.
772
     *
773
     * @return array
774
     */
775
    public function getColumns()
776
    {
777
        $columns = [];
778
779
        foreach ($this->getComponents() as $item) {
780
            if ($item instanceof GridField_ColumnProvider) {
781
                $item->augmentColumns($this, $columns);
782
            }
783
        }
784
785
        return $columns;
786
    }
787
788
    /**
789
     * Get the value from a column.
790
     *
791
     * @param DataObject $record
792
     * @param string $column
793
     *
794
     * @return string
795
     *
796
     * @throws InvalidArgumentException
797
     */
798
    public function getColumnContent($record, $column)
799
    {
800
        if (!$this->columnDispatch) {
801
            $this->buildColumnDispatch();
802
        }
803
804
        if (!empty($this->columnDispatch[$column])) {
805
            $content = '';
806
807
            foreach ($this->columnDispatch[$column] as $handler) {
808
                /**
809
                 * @var GridField_ColumnProvider $handler
810
                 */
811
                $content .= $handler->getColumnContent($this, $record, $column);
812
            }
813
814
            return $content;
815
        } else {
816
            throw new InvalidArgumentException(sprintf(
817
                'Bad column "%s"',
818
                $column
819
            ));
820
        }
821
    }
822
823
    /**
824
     * Add additional calculated data fields to be used on this GridField
825
     *
826
     * @param array $fields a map of fieldname to callback. The callback will
827
     *                      be passed the record as an argument.
828
     */
829
    public function addDataFields($fields)
830
    {
831
        if ($this->customDataFields) {
832
            $this->customDataFields = array_merge($this->customDataFields, $fields);
833
        } else {
834
            $this->customDataFields = $fields;
835
        }
836
    }
837
838
    /**
839
     * Get the value of a named field  on the given record.
840
     *
841
     * Use of this method ensures that any special rules around the data for this gridfield are
842
     * followed.
843
     *
844
     * @param DataObject $record
845
     * @param string $fieldName
846
     *
847
     * @return mixed
848
     */
849
    public function getDataFieldValue($record, $fieldName)
850
    {
851
        if (isset($this->customDataFields[$fieldName])) {
852
            $callback = $this->customDataFields[$fieldName];
853
854
            return $callback($record);
855
        }
856
857
        if ($record->hasMethod('relField')) {
858
            return $record->relField($fieldName);
859
        }
860
861
        if ($record->hasMethod($fieldName)) {
862
            return $record->$fieldName();
863
        }
864
865
        return $record->$fieldName;
866
    }
867
868
    /**
869
     * Get extra columns attributes used as HTML attributes.
870
     *
871
     * @param DataObject $record
872
     * @param string $column
873
     *
874
     * @return array
875
     *
876
     * @throws LogicException
877
     * @throws InvalidArgumentException
878
     */
879
    public function getColumnAttributes($record, $column)
880
    {
881
        if (!$this->columnDispatch) {
882
            $this->buildColumnDispatch();
883
        }
884
885
        if (!empty($this->columnDispatch[$column])) {
886
            $attributes = [];
887
888
            foreach ($this->columnDispatch[$column] as $handler) {
889
                /**
890
                 * @var GridField_ColumnProvider $handler
891
                 */
892
                $columnAttributes = $handler->getColumnAttributes($this, $record, $column);
893
894
                if (is_array($columnAttributes)) {
895
                    $attributes = array_merge($attributes, $columnAttributes);
896
                    continue;
897
                }
898
899
                throw new LogicException(sprintf(
900
                    'Non-array response from %s::getColumnAttributes().',
901
                    get_class($handler)
902
                ));
903
            }
904
905
            return $attributes;
906
        }
907
908
        throw new InvalidArgumentException(sprintf(
909
            'Bad column "%s"',
910
            $column
911
        ));
912
    }
913
914
    /**
915
     * Get metadata for a column.
916
     *
917
     * @example "array('Title'=>'Email address')"
918
     *
919
     * @param string $column
920
     *
921
     * @return array
922
     *
923
     * @throws LogicException
924
     * @throws InvalidArgumentException
925
     */
926
    public function getColumnMetadata($column)
927
    {
928
        if (!$this->columnDispatch) {
929
            $this->buildColumnDispatch();
930
        }
931
932
        if (!empty($this->columnDispatch[$column])) {
933
            $metaData = [];
934
935
            foreach ($this->columnDispatch[$column] as $handler) {
936
                /**
937
                 * @var GridField_ColumnProvider $handler
938
                 */
939
                $columnMetaData = $handler->getColumnMetadata($this, $column);
940
941
                if (is_array($columnMetaData)) {
942
                    $metaData = array_merge($metaData, $columnMetaData);
943
                    continue;
944
                }
945
946
                throw new LogicException(sprintf(
947
                    'Non-array response from %s::getColumnMetadata().',
948
                    get_class($handler)
949
                ));
950
            }
951
952
            return $metaData;
953
        }
954
955
        throw new InvalidArgumentException(sprintf(
956
            'Bad column "%s"',
957
            $column
958
        ));
959
    }
960
961
    /**
962
     * Return how many columns the grid will have.
963
     *
964
     * @return int
965
     */
966
    public function getColumnCount()
967
    {
968
        if (!$this->columnDispatch) {
969
            $this->buildColumnDispatch();
970
        }
971
972
        return count($this->columnDispatch);
973
    }
974
975
    /**
976
     * Build an columnDispatch that maps a GridField_ColumnProvider to a column for reference later.
977
     */
978
    protected function buildColumnDispatch()
979
    {
980
        $this->columnDispatch = [];
981
982
        foreach ($this->getComponents() as $item) {
983
            if ($item instanceof GridField_ColumnProvider) {
984
                $columns = $item->getColumnsHandled($this);
985
986
                foreach ($columns as $column) {
987
                    $this->columnDispatch[$column][] = $item;
988
                }
989
            }
990
        }
991
    }
992
993
    /**
994
     * This is the action that gets executed when a GridField_AlterAction gets clicked.
995
     *
996
     * @param array $data
997
     * @param Form $form
998
     * @param HTTPRequest $request
999
     *
1000
     * @return string
1001
     */
1002
    public function gridFieldAlterAction($data, $form, HTTPRequest $request)
1003
    {
1004
        $data = $request->requestVars();
1005
1006
        // Protection against CSRF attacks
1007
        $token = $this
1008
            ->getForm()
1009
            ->getSecurityToken();
1010
        if (!$token->checkRequest($request)) {
1011
            $this->httpError(400, _t(
1012
                "SilverStripe\\Forms\\Form.CSRF_FAILED_MESSAGE",
1013
                "There seems to have been a technical problem. Please click the back button, " . "refresh your browser, and try again."
1014
            ));
1015
        }
1016
1017
        $name = $this->getName();
1018
1019
        $fieldData = null;
1020
1021
        if (isset($data[$name])) {
1022
            $fieldData = $data[$name];
1023
        }
1024
1025
        $state = $this->getState(false);
1026
1027
        /** @skipUpgrade */
1028
        if (isset($fieldData['GridState'])) {
1029
            $state->setValue($fieldData['GridState']);
1030
        }
1031
1032
        // Fetch the store for the "state" of actions (not the GridField)
1033
        /** @var StateStore $store */
1034
        $store = Injector::inst()->create(StateStore::class . '.' . $this->getName());
1035
1036
        foreach ($data as $dataKey => $dataValue) {
1037
            if (preg_match('/^action_gridFieldAlterAction\?StateID=(.*)/', $dataKey, $matches)) {
1038
                $stateChange = $store->load($matches[1]);
1039
1040
                $actionName = $stateChange['actionName'];
1041
1042
                $arguments = [];
1043
1044
                if (isset($stateChange['args'])) {
1045
                    $arguments = $stateChange['args'];
1046
                };
1047
1048
                $html = $this->handleAlterAction($actionName, $arguments, $data);
1049
1050
                if ($html) {
1051
                    return $html;
1052
                }
1053
            }
1054
        }
1055
1056
        if ($request->getHeader('X-Pjax') === 'CurrentField') {
1057
            if ($this->getState()->Readonly === true) {
1058
                $this->performDisabledTransformation();
1059
            }
1060
            return $this->FieldHolder();
1061
        }
1062
1063
        return $form->forTemplate();
1064
    }
1065
1066
    /**
1067
     * Pass an action on the first GridField_ActionProvider that matches the $actionName.
1068
     *
1069
     * @param string $actionName
1070
     * @param mixed $arguments
1071
     * @param array $data
1072
     *
1073
     * @return mixed
1074
     *
1075
     * @throws InvalidArgumentException
1076
     */
1077
    public function handleAlterAction($actionName, $arguments, $data)
1078
    {
1079
        $actionName = strtolower($actionName);
1080
1081
        foreach ($this->getComponents() as $component) {
1082
            if ($component instanceof GridField_ActionProvider) {
1083
                $actions = array_map('strtolower', (array) $component->getActions($this));
1084
1085
                if (in_array($actionName, $actions)) {
1086
                    return $component->handleAction($this, $actionName, $arguments, $data);
1087
                }
1088
            }
1089
        }
1090
1091
        throw new InvalidArgumentException(sprintf(
1092
            'Can\'t handle action "%s"',
1093
            $actionName
1094
        ));
1095
    }
1096
1097
    /**
1098
     * Custom request handler that will check component handlers before proceeding to the default
1099
     * implementation.
1100
     *
1101
     * @todo copy less code from RequestHandler.
1102
     *
1103
     * @param HTTPRequest $request
1104
     * @return array|RequestHandler|HTTPResponse|string
1105
     * @throws HTTPResponse_Exception
1106
     */
1107
    public function handleRequest(HTTPRequest $request)
1108
    {
1109
        if ($this->brokenOnConstruct) {
1110
            user_error(
1111
                sprintf(
1112
                    "parent::__construct() needs to be called on %s::__construct()",
1113
                    __CLASS__
1114
                ),
1115
                E_USER_WARNING
1116
            );
1117
        }
1118
1119
        $this->setRequest($request);
1120
1121
        $fieldData = $this->getRequest()->requestVar($this->getName());
1122
1123
        /** @skipUpgrade */
1124
        if ($fieldData && isset($fieldData['GridState'])) {
1125
            $this->getState(false)->setValue($fieldData['GridState']);
1126
        }
1127
1128
        foreach ($this->getComponents() as $component) {
1129
            if ($component instanceof GridField_URLHandler && $urlHandlers = $component->getURLHandlers($this)) {
1130
                foreach ($urlHandlers as $rule => $action) {
1131
                    if ($params = $request->match($rule, true)) {
1132
                        // Actions can reference URL parameters.
1133
                        // e.g. '$Action/$ID/$OtherID' → '$Action'
1134
1135
                        if ($action[0] == '$') {
1136
                            $action = $params[substr($action, 1)];
1137
                        }
1138
1139
                        if (!method_exists($component, 'checkAccessAction') || $component->checkAccessAction($action)) {
1140
                            if (!$action) {
1141
                                $action = "index";
1142
                            }
1143
1144
                            if (!is_string($action)) {
1145
                                throw new LogicException(sprintf(
1146
                                    'Non-string method name: %s',
1147
                                    var_export($action, true)
1148
                                ));
1149
                            }
1150
1151
                            try {
1152
                                $this->extend('beforeCallActionURLHandler', $request, $action);
1153
1154
                                $result = $component->$action($this, $request);
1155
1156
                                $this->extend('afterCallActionURLHandler', $request, $action, $result);
1157
                            } catch (HTTPResponse_Exception $responseException) {
1158
                                $result = $responseException->getResponse();
1159
                            }
1160
1161
                            if ($result instanceof HTTPResponse && $result->isError()) {
1162
                                return $result;
1163
                            }
1164
1165
                            if ($this !== $result &&
1166
                                !$request->isEmptyPattern($rule) &&
1167
                                ($result instanceof RequestHandler || $result instanceof HasRequestHandler)
1168
                            ) {
1169
                                if ($result instanceof HasRequestHandler) {
1170
                                    $result = $result->getRequestHandler();
1171
                                }
1172
                                $returnValue = $result->handleRequest($request);
1173
1174
                                if (is_array($returnValue)) {
1175
                                    throw new LogicException(
1176
                                        'GridField_URLHandler handlers can\'t return arrays'
1177
                                    );
1178
                                }
1179
1180
                                return $returnValue;
1181
                            }
1182
1183
                            if ($request->allParsed()) {
1184
                                return $result;
1185
                            }
1186
1187
                            return $this->httpError(
1188
                                404,
1189
                                sprintf(
1190
                                    'I can\'t handle sub-URLs of a %s object.',
1191
                                    get_class($result)
1192
                                )
1193
                            );
1194
                        }
1195
                    }
1196
                }
1197
            }
1198
        }
1199
1200
        return parent::handleRequest($request);
1201
    }
1202
1203
    /**
1204
     * {@inheritdoc}
1205
     */
1206
    public function saveInto(DataObjectInterface $record)
1207
    {
1208
        foreach ($this->getComponents() as $component) {
1209
            if ($component instanceof GridField_SaveHandler) {
1210
                $component->handleSave($this, $record);
1211
            }
1212
        }
1213
    }
1214
1215
    /**
1216
     * @param array $content
1217
     *
1218
     * @return string
1219
     */
1220
    protected function getOptionalTableHeader(array $content)
1221
    {
1222
        if ($content['header']) {
1223
            return HTML::createTag(
1224
                'thead',
1225
                [],
1226
                $content['header']
1227
            );
1228
        }
1229
1230
        return '';
1231
    }
1232
1233
    /**
1234
     * @param array $content
1235
     *
1236
     * @return string
1237
     */
1238
    protected function getOptionalTableBody(array $content)
1239
    {
1240
        if ($content['body']) {
1241
            return HTML::createTag(
1242
                'tbody',
1243
                ['class' => 'ss-gridfield-items'],
1244
                $content['body']
1245
            );
1246
        }
1247
1248
        return '';
1249
    }
1250
1251
    /**
1252
     * @param $content
1253
     *
1254
     * @return string
1255
     */
1256
    protected function getOptionalTableFooter($content)
1257
    {
1258
        if ($content['footer']) {
1259
            return HTML::createTag(
1260
                'tfoot',
1261
                [],
1262
                $content['footer']
1263
            );
1264
        }
1265
1266
        return '';
1267
    }
1268
}
1269