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
    const FORM_NAME_VERSION = 'versionForm';
30

31
    /**
32
     * @var string
33
     */
34
    const FORM_NAME_COMPARE = 'compareForm';
35

36
    private static $url_segment = 'historyviewer';
37

38
    private static $url_rule = '/$Action';
39

40
    private static $url_priority = 10;
41

42
    private static $required_permission_codes = 'CMS_ACCESS_CMSMain';
43

44
    private static $allowed_actions = [
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 1
    public function getClientConfig()
58
    {
59 1
        $clientConfig = parent::getClientConfig();
60

61 1
        foreach ($this->formNames as $formName) {
62 1
            $clientConfig['form'][$formName] = [
63 1
                'schemaUrl' => $this->Link('schema/' . $formName),
64
            ];
65
        }
66

67 1
        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 1
    public function schema($request)
79
    {
80 1
        $formName = $request->param('FormName');
81 1
        if (!in_array($formName, $this->formNames)) {
82 0
            return parent::schema($request);
83
        }
84

85 1
        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 1
    protected function generateSchemaForForm($formName, HTTPRequest $request)
98
    {
99
        switch ($formName) {
100
            // Get schema for history form
101 1
            case self::FORM_NAME_VERSION:
102 1
                $form = $this->getVersionForm([
103 1
                    'RecordClass' => $request->getVar('RecordClass'),
104 1
                    'RecordID' => $request->getVar('RecordID'),
105 1
                    'RecordVersion' => $request->getVar('RecordVersion'),
106 1
                    'RecordDate' => $request->getVar('RecordDate'),
107
                ]);
108 1
                break;
109 1
            case self::FORM_NAME_COMPARE:
110 1
                $form = $this->getCompareForm([
111 1
                    'RecordClass' => $request->getVar('RecordClass'),
112 1
                    'RecordID' => $request->getVar('RecordID'),
113 1
                    'RecordVersionFrom' => $request->getVar('RecordVersionFrom'),
114 1
                    'RecordVersionTo' => $request->getVar('RecordVersionTo'),
115
                ]);
116 1
                break;
117
            default:
118 0
                throw new InvalidArgumentException('Invalid form name passed to generate schema: ' . $formName);
119
        }
120

121
        // Respond with this schema
122 1
        $response = $this->getResponse();
123 1
        $response->addHeader('Content-Type', 'application/json');
124 1
        $schemaID = $this->getRequest()->getURL();
125

126 1
        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 1
    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 1
            $specifiesDate = !empty($context['RecordDate']) && DBDatetime::create()->setValue($context['RecordDate']);
140 0
        } catch (InvalidArgumentException $e) {
141 0
            $specifiesDate = false;
142
        }
143

144 1
        return $specifiesDate ? $this->getVersionFormByDate($context) : $this->getVersionFormByVersion($context);
145
    }
146

147
    /**
148
     * @param array $context
149
     * @return Form|null
150
     */
151 0
    protected function getVersionFormByDate(array $context)
152
    {
153 0
        $required = ['RecordClass', 'RecordID', 'RecordDate'];
154 0
        $this->validateInput($context, $required);
155

156 0
        $recordClass = $context['RecordClass'];
157 0
        $recordId = $context['RecordID'];
158

159 0
        $form = null;
160

161 0
        Versioned::withVersionedMode(function () use ($context, $recordClass, $recordId, &$form) {
162 0
            Versioned::reading_archived_date($context['RecordDate']);
163

164 0
            $record = DataList::create(DataObject::getSchema()->baseDataClass($recordClass))
165 0
                ->byID($recordId);
166

167 0
            if ($record) {
168 0
                $effectiveContext = array_merge($context, ['Record' => $record]);
169

170
                // Ensure the form is scaffolded with archive date enabled.
171 0
                $form = $this->scaffoldForm(self::FORM_NAME_VERSION, $effectiveContext, [
172 0
                    $recordClass,
173 0
                    $recordId,
174
                ]);
175
            }
176 0
        });
177

178 0
        return $form;
179
    }
180

181
    /**
182
     * @param array $context
183
     * @return Form
184
     */
185 1
    protected function getVersionFormByVersion(array $context)
186
    {
187 1
        $required = ['RecordClass', 'RecordID', 'RecordVersion'];
188 1
        $this->validateInput($context, $required);
189

190 1
        $recordClass = $context['RecordClass'];
191 1
        $recordId = $context['RecordID'];
192 1
        $recordVersion = $context['RecordVersion'];
193

194
        // Load record and perform a canView check
195 1
        $record = $this->getRecordVersion($recordClass, $recordId, $recordVersion);
196

197 1
        $effectiveContext = array_merge($context, ['Record' => $record]);
198

199 1
        return $this->scaffoldForm(self::FORM_NAME_VERSION, $effectiveContext, [
200 1
            $recordClass,
201 1
            $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 1
    protected function getRecordVersion($recordClass, $recordId, $recordVersion)
214
    {
215 1
        $record = Versioned::get_version($recordClass, $recordId, $recordVersion);
216

217 1
        if (!$record) {
218 1
            $this->jsonError(404);
219 0
            return null;
220
        }
221

222 1
        if (!$record->canView()) {
223 1
            $this->jsonError(403, _t(
224 1
                __CLASS__.'.ErrorItemViewPermissionDenied',
225 1
                "You don't have the necessary permissions to view {ObjectTitle}",
226 1
                ['ObjectTitle' => $record->i18n_singular_name()]
227
            ));
228 0
            return null;
229
        }
230

231 1
        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 0
    public function getCompareForm(array $context)
242
    {
243 0
        $this->validateInput($context, ['RecordClass', 'RecordID', 'RecordVersionFrom', 'RecordVersionTo']);
244

245 0
        $recordClass = $context['RecordClass'];
246 0
        $recordId = $context['RecordID'];
247 0
        $recordVersionFrom = $context['RecordVersionFrom'];
248 0
        $recordVersionTo = $context['RecordVersionTo'];
249

250
        // Load record and perform a canView check
251 0
        $recordFrom = $this->getRecordVersion($recordClass, $recordId, $recordVersionFrom);
252 0
        $recordTo = $this->getRecordVersion($recordClass, $recordId, $recordVersionTo);
253 0
        if (!$recordFrom || !$recordTo) {
254 0
            return null;
255
        }
256

257 0
        $effectiveContext = array_merge($context, ['Record' => $recordTo]);
258

259 0
        $form = $this->scaffoldForm(self::FORM_NAME_COMPARE, $effectiveContext, [
260 0
            $recordClass,
261 0
            $recordId,
262 0
            $recordVersionFrom,
263 0
            $recordVersionTo,
264
        ]);
265

266
        // Enable the "compare mode" diff view
267 0
        $comparisonTransformation = DiffTransformation::create();
268 0
        $form->transform($comparisonTransformation);
269 0
        $form->loadDataFrom($recordFrom);
270

271 0
        return $form;
272
    }
273

274 1
    public function versionForm(HTTPRequest $request = null)
275
    {
276 1
        if (!$request) {
277 1
            $this->jsonError(400);
278 0
            return null;
279
        }
280

281
        try {
282 1
            return $this->getVersionForm([
283 1
                'RecordClass' => $request->getVar('RecordClass'),
284 1
                'RecordID' => $request->getVar('RecordID'),
285 1
                'RecordVersion' => $request->getVar('RecordVersion'),
286
            ]);
287 0
        } catch (InvalidArgumentException $ex) {
288 0
            $this->jsonError(400);
289
        }
290
    }
291

292 1
    public function compareForm(HTTPRequest $request = null)
293
    {
294 1
        if (!$request) {
295 1
            $this->jsonError(400);
296 0
            return null;
297
        }
298

299
        try {
300 0
            return $this->getCompareForm([
301 0
                'RecordClass' => $request->getVar('RecordClass'),
302 0
                'RecordID' => $request->getVar('RecordID'),
303 0
                'RecordVersionFrom' => $request->getVar('RecordVersionFrom'),
304 0
                'RecordVersionTo' => $request->getVar('RecordVersionTo'),
305
            ]);
306 0
        } catch (InvalidArgumentException $ex) {
307 0
            $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 1
    protected function validateInput(array $context, array $requiredFields = [])
320
    {
321 1
        foreach ($requiredFields as $requiredField) {
322 1
            if (empty($context[$requiredField])) {
323 1
                throw new InvalidArgumentException('Missing required field ' . $requiredField);
324
            }
325
        }
326 1
        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 1
    protected function scaffoldForm($formName, array $context = [], array $extra = [])
338
    {
339
        /** @var FormFactory $scaffolder */
340 1
        $scaffolder = Injector::inst()->get(DataObjectVersionFormFactory::class);
341 1
        $form = $scaffolder->getForm($this, $formName, $context);
342

343
        // Set form handler with class name, ID and VersionID
344 1
        return $form->setRequestHandler(
345 1
            LeftAndMainFormRequestHandler::create($form, $extra)
346
        );
347
    }
348
}

Read our documentation on viewing source code .

Loading