HistoryViewerController::getCompareForm()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 31
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 19
nc 2
nop 1
dl 0
loc 31
rs 9.6333
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\VersionedAdmin\Controllers;
4
5
use InvalidArgumentException;
6
use SilverStripe\Admin\LeftAndMain;
7
use SilverStripe\Admin\LeftAndMainFormRequestHandler;
8
use SilverStripe\Control\HTTPRequest;
9
use SilverStripe\Control\HTTPResponse;
10
use SilverStripe\Core\Injector\Injector;
11
use SilverStripe\Forms\Form;
12
use SilverStripe\Forms\FormFactory;
13
use SilverStripe\ORM\DataList;
14
use SilverStripe\ORM\DataObject;
15
use SilverStripe\ORM\FieldType\DBDatetime;
16
use SilverStripe\Versioned\Versioned;
17
use SilverStripe\VersionedAdmin\Forms\DataObjectVersionFormFactory;
18
use SilverStripe\VersionedAdmin\Forms\DiffTransformation;
19
20
/**
21
 * The HistoryViewerController provides AJAX endpoints for React to enable functionality, such as retrieving the form
22
 * schema.
23
 */
24
class HistoryViewerController extends LeftAndMain
25
{
26
    /**
27
     * @var string
28
     */
29
    public const FORM_NAME_VERSION = 'versionForm';
30
31
    /**
32
     * @var string
33
     */
34
    public const FORM_NAME_COMPARE = 'compareForm';
35
36
    private static $url_segment = 'historyviewer';
0 ignored issues
show
introduced by
The private property $url_segment is not used, and could be removed.
Loading history...
37
38
    private static $url_rule = '/$Action';
0 ignored issues
show
introduced by
The private property $url_rule is not used, and could be removed.
Loading history...
39
40
    private static $url_priority = 10;
0 ignored issues
show
introduced by
The private property $url_priority is not used, and could be removed.
Loading history...
41
42
    private static $required_permission_codes = 'CMS_ACCESS_CMSMain';
0 ignored issues
show
introduced by
The private property $required_permission_codes is not used, and could be removed.
Loading history...
43
44
    private static $allowed_actions = [
0 ignored issues
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
45
        self::FORM_NAME_VERSION,
46
        self::FORM_NAME_COMPARE,
47
        'schema',
48
    ];
49
50
    /**
51
     * An array of supported form names that can be requested through the schema
52
     *
53
     * @var string[]
54
     */
55
    protected $formNames = [self::FORM_NAME_VERSION, self::FORM_NAME_COMPARE];
56
57
    public function getClientConfig()
58
    {
59
        $clientConfig = parent::getClientConfig();
60
61
        foreach ($this->formNames as $formName) {
62
            $clientConfig['form'][$formName] = [
63
                'schemaUrl' => $this->Link('schema/' . $formName),
64
            ];
65
        }
66
67
        return $clientConfig;
68
    }
69
70
    /**
71
     * Gets a JSON schema representing the current version detail form.
72
     *
73
     * WARNING: Experimental API.
74
     * @internal
75
     * @param HTTPRequest $request
76
     * @return HTTPResponse
77
     */
78
    public function schema($request)
79
    {
80
        $formName = $request->param('FormName');
81
        if (!in_array($formName, $this->formNames)) {
82
            return parent::schema($request);
83
        }
84
85
        return $this->generateSchemaForForm($formName, $request);
86
    }
87
88
    /**
89
     * Checks the requested schema name and returns a scaffolded {@link Form}. An exception is thrown
90
     * if an unexpected value is provided.
91
     *
92
     * @param string $formName
93
     * @param HTTPRequest $request
94
     * @return HTTPResponse
95
     * @throws InvalidArgumentException
96
     */
97
    protected function generateSchemaForForm($formName, HTTPRequest $request)
98
    {
99
        switch ($formName) {
100
            // Get schema for history form
101
            case self::FORM_NAME_VERSION:
102
                $form = $this->getVersionForm([
103
                    'RecordClass' => $request->getVar('RecordClass'),
104
                    'RecordID' => $request->getVar('RecordID'),
105
                    'RecordVersion' => $request->getVar('RecordVersion'),
106
                    'RecordDate' => $request->getVar('RecordDate'),
107
                ]);
108
                break;
109
            case self::FORM_NAME_COMPARE:
110
                $form = $this->getCompareForm([
111
                    'RecordClass' => $request->getVar('RecordClass'),
112
                    'RecordID' => $request->getVar('RecordID'),
113
                    'RecordVersionFrom' => $request->getVar('RecordVersionFrom'),
114
                    'RecordVersionTo' => $request->getVar('RecordVersionTo'),
115
                ]);
116
                break;
117
            default:
118
                throw new InvalidArgumentException('Invalid form name passed to generate schema: ' . $formName);
119
        }
120
121
        // Respond with this schema
122
        $response = $this->getResponse();
123
        $response->addHeader('Content-Type', 'application/json');
124
        $schemaID = $this->getRequest()->getURL();
125
126
        return $this->getSchemaResponse($schemaID, $form);
127
    }
128
129
    /**
130
     * Returns a {@link Form} showing the version details for a given version of a record
131
     *
132
     * @param array $context
133
     * @return Form
134
     */
135
    public function getVersionForm(array $context)
136
    {
137
        // Attempt to parse a date if given in case we're fetching a version form for a specific timestamp.
138
        try {
139
            $specifiesDate = !empty($context['RecordDate']) && DBDatetime::create()->setValue($context['RecordDate']);
140
        } catch (InvalidArgumentException $e) {
141
            $specifiesDate = false;
142
        }
143
144
        return $specifiesDate ? $this->getVersionFormByDate($context) : $this->getVersionFormByVersion($context);
145
    }
146
147
    /**
148
     * @param array $context
149
     * @return Form|null
150
     */
151
    protected function getVersionFormByDate(array $context)
152
    {
153
        $required = ['RecordClass', 'RecordID', 'RecordDate'];
154
        $this->validateInput($context, $required);
155
156
        $recordClass = $context['RecordClass'];
157
        $recordId = $context['RecordID'];
158
159
        $form = null;
160
161
        Versioned::withVersionedMode(function () use ($context, $recordClass, $recordId, &$form) {
162
            Versioned::reading_archived_date($context['RecordDate']);
163
164
            $record = DataList::create(DataObject::getSchema()->baseDataClass($recordClass))
165
                ->byID($recordId);
166
167
            if ($record) {
168
                $effectiveContext = array_merge($context, ['Record' => $record]);
169
170
                // Ensure the form is scaffolded with archive date enabled.
171
                $form = $this->scaffoldForm(self::FORM_NAME_VERSION, $effectiveContext, [
172
                    $recordClass,
173
                    $recordId,
174
                ]);
175
            }
176
        });
177
178
        return $form;
179
    }
180
181
    /**
182
     * @param array $context
183
     * @return Form
184
     */
185
    protected function getVersionFormByVersion(array $context)
186
    {
187
        $required = ['RecordClass', 'RecordID', 'RecordVersion'];
188
        $this->validateInput($context, $required);
189
190
        $recordClass = $context['RecordClass'];
191
        $recordId = $context['RecordID'];
192
        $recordVersion = $context['RecordVersion'];
193
194
        // Load record and perform a canView check
195
        $record = $this->getRecordVersion($recordClass, $recordId, $recordVersion);
196
197
        $effectiveContext = array_merge($context, ['Record' => $record]);
198
199
        return $this->scaffoldForm(self::FORM_NAME_VERSION, $effectiveContext, [
200
            $recordClass,
201
            $recordId,
202
        ]);
203
    }
204
205
    /**
206
     * Fetches record version and checks canView permission for result
207
     *
208
     * @param string $recordClass
209
     * @param int $recordId
210
     * @param int $recordVersion
211
     * @return DataObject|null
212
     */
213
    protected function getRecordVersion($recordClass, $recordId, $recordVersion)
214
    {
215
        $record = Versioned::get_version($recordClass, $recordId, $recordVersion);
216
217
        if (!$record) {
0 ignored issues
show
introduced by
$record is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
218
            $this->jsonError(404);
219
            return null;
220
        }
221
222
        if (!$record->canView()) {
223
            $this->jsonError(403, _t(
224
                __CLASS__ . '.ErrorItemViewPermissionDenied',
225
                "You don't have the necessary permissions to view {ObjectTitle}",
226
                ['ObjectTitle' => $record->i18n_singular_name()]
227
            ));
228
            return null;
229
        }
230
231
        return $record;
232
    }
233
234
    /**
235
     * Returns a {@link Form} containing the comparison {@link DiffTransformation} view for a record
236
     * between two specified versions.
237
     *
238
     * @param array $context
239
     * @return Form
240
     */
241
    public function getCompareForm(array $context)
242
    {
243
        $this->validateInput($context, ['RecordClass', 'RecordID', 'RecordVersionFrom', 'RecordVersionTo']);
244
245
        $recordClass = $context['RecordClass'];
246
        $recordId = $context['RecordID'];
247
        $recordVersionFrom = $context['RecordVersionFrom'];
248
        $recordVersionTo = $context['RecordVersionTo'];
249
250
        // Load record and perform a canView check
251
        $recordFrom = $this->getRecordVersion($recordClass, $recordId, $recordVersionFrom);
252
        $recordTo = $this->getRecordVersion($recordClass, $recordId, $recordVersionTo);
253
        if (!$recordFrom || !$recordTo) {
0 ignored issues
show
introduced by
$recordTo is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
introduced by
$recordFrom is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
254
            return null;
255
        }
256
257
        $effectiveContext = array_merge($context, ['Record' => $recordTo]);
258
259
        $form = $this->scaffoldForm(self::FORM_NAME_COMPARE, $effectiveContext, [
260
            $recordClass,
261
            $recordId,
262
            $recordVersionFrom,
263
            $recordVersionTo,
264
        ]);
265
266
        // Enable the "compare mode" diff view
267
        $comparisonTransformation = DiffTransformation::create();
268
        $form->transform($comparisonTransformation);
269
        $form->loadDataFrom($recordFrom);
270
271
        return $form;
272
    }
273
274
    public function versionForm(HTTPRequest $request = null)
275
    {
276
        if (!$request) {
277
            $this->jsonError(400);
278
            return null;
279
        }
280
281
        try {
282
            return $this->getVersionForm([
283
                'RecordClass' => $request->getVar('RecordClass'),
284
                'RecordID' => $request->getVar('RecordID'),
285
                'RecordVersion' => $request->getVar('RecordVersion'),
286
            ]);
287
        } catch (InvalidArgumentException $ex) {
288
            $this->jsonError(400);
289
        }
290
    }
291
292
    public function compareForm(HTTPRequest $request = null)
293
    {
294
        if (!$request) {
295
            $this->jsonError(400);
296
            return null;
297
        }
298
299
        try {
300
            return $this->getCompareForm([
301
                'RecordClass' => $request->getVar('RecordClass'),
302
                'RecordID' => $request->getVar('RecordID'),
303
                'RecordVersionFrom' => $request->getVar('RecordVersionFrom'),
304
                'RecordVersionTo' => $request->getVar('RecordVersionTo'),
305
            ]);
306
        } catch (InvalidArgumentException $ex) {
307
            $this->jsonError(400);
308
        }
309
    }
310
311
    /**
312
     * Perform some centralised validation checks on the input request and data within it
313
     *
314
     * @param array $context
315
     * @param string[] $requiredFields
316
     * @return bool
317
     * @throws InvalidArgumentException
318
     */
319
    protected function validateInput(array $context, array $requiredFields = [])
320
    {
321
        foreach ($requiredFields as $requiredField) {
322
            if (empty($context[$requiredField])) {
323
                throw new InvalidArgumentException('Missing required field ' . $requiredField);
324
            }
325
        }
326
        return true;
327
    }
328
329
    /**
330
     * Given some context, scaffold a form using the FormFactory and return it
331
     *
332
     * @param string $formName The name for the returned {@link Form}
333
     * @param array $context Context arguments for the {@link FormFactory}
334
     * @param array $extra Context arguments for the {@link LeftAndMainFormRequestHandler}
335
     * @return Form
336
     */
337
    protected function scaffoldForm($formName, array $context = [], array $extra = [])
338
    {
339
        /** @var FormFactory $scaffolder */
340
        $scaffolder = Injector::inst()->get(DataObjectVersionFormFactory::class);
341
        $form = $scaffolder->getForm($this, $formName, $context);
342
343
        // Set form handler with class name, ID and VersionID
344
        return $form->setRequestHandler(
345
            LeftAndMainFormRequestHandler::create($form, $extra)
346
        );
347
    }
348
}
349