archiveValidationTrait.php 16.6 KB
Newer Older
Cyril Vazquez's avatar
Cyril Vazquez committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<?php

/*
 *  Copyright (C) 2017 Maarch
 *
 *  This file is part of bundle recordsManagement.
 *  Bundle recordsManagement is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Bundle recordsManagement is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with bundle recordsManagement.  If not, see <http://www.gnu.org/licenses/>.
 */

namespace bundle\recordsManagement\Controller;

/**
 * Archive entry controller
 *
 * @author Alexis Ragot <alexis.ragot@maarch.org>
 */
trait archiveValidationTrait
{
    /**
     * Validate the archive compliance
     *
     * @param recordsManagement/archive $archive The archive to validate
     */
    public function validateCompliance($archive)
    {
        $this->validateFileplan($archive);
        $this->validateArchiveDescriptionObject($archive);
        $this->validateManagementMetadata($archive);
        $this->validateAttachments($archive);
        $this->validateProcessingStatus($archive);
    }

    /**
     * Check and set the processing status
     *
     * @param recordsManagement/archive $archive The archive to setting
     */
    public function validateProcessingStatus($archive)
    {
        if (empty($archive->processingStatus) || !isset($this->currentArchivalProfile)) {
            return;
        }

        $processingStatuses = json_decode($this->currentArchivalProfile->processingStatuses);

        if (!isset($processingStatuses->{$archive->processingStatus})) {
            throw new \core\Exception\BadRequestException("The processing status isn't initial");
        }

        $archiveProcessingStatus = $processingStatuses->{$archive->processingStatus};

        if ($archiveProcessingStatus->type != 'initial') {
            throw new \core\Exception\BadRequestException("The processing status isn't initial");
        }
    }

    /**
     * Validate archive description object
     *
     * @param recordsManagement/archive $archive The archive object
     */
    protected function validateArchiveDescriptionObject($archive)
    {
75
        if (!isset($this->currentArchivalProfile) || !is_object($archive->descriptionObject)) {
Cyril Vazquez's avatar
Cyril Vazquez committed
76
77
78
79
80
81
            return;
        }

        $this->validateDescriptionModel($archive->descriptionObject, $this->currentArchivalProfile);
    }

82
    public function validateDescriptionModel($object, $archivalProfile)
Cyril Vazquez's avatar
Cyril Vazquez committed
83
    {
84
85
        $descriptionSchemeProperties = $this->descriptionSchemeController->getDescriptionFields($archivalProfile->descriptionClass);

Cyril Vazquez's avatar
Cyril Vazquez committed
86
        $archivalProfileFields = [];
87

Cyril Vazquez's avatar
Cyril Vazquez committed
88
        foreach ($archivalProfile->archiveDescription as $archiveDescription) {
89
90
            if (!isset($object->{$archiveDescription->fieldName}) && $archiveDescription->required) {
                throw new \core\Exception\BadRequestException('Null value not allowed for metadata %1$s', 400, null, [$archiveDescription->fieldName]);
Cyril Vazquez's avatar
Cyril Vazquez committed
91
92
            }

93
            $archivalProfileFields[$archiveDescription->fieldName] = $archiveDescription;
Cyril Vazquez's avatar
Cyril Vazquez committed
94
95
96
        }

        foreach ($object as $name => $value) {
97
98
            if (!isset($archivalProfileFields[$name]) && !$archivalProfile->acceptUserIndex) {

Cyril Vazquez's avatar
Cyril Vazquez committed
99
100
                throw new \core\Exception\BadRequestException('Metadata %1$s is not allowed', 400, null, [$name]);
            }
101
102
103
104

            if (isset($descriptionSchemeProperties[$name])) {
                $this->validateDescriptionField($value, $descriptionSchemeProperties[$name]);
            }
Cyril Vazquez's avatar
Cyril Vazquez committed
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
        }
    }

    protected function validateDescriptionField($value, $descriptionField)
    {
        switch ($descriptionField->type) {
            case 'name':
                $this->validateName($value, $descriptionField);
                break;

            case 'text':
                $this->validateText($value, $descriptionField);
                break;

            case 'number':
                $this->validateNumber($value, $descriptionField);
                break;

            case 'boolean':
                $this->validateBoolean($value, $descriptionField);
                break;

            case 'date':
Cyril Vazquez's avatar
Cyril Vazquez committed
128
            case 'datetime':
Cyril Vazquez's avatar
Cyril Vazquez committed
129
130
131
132
133
134
135
136
137
138
139
140
                $this->validateDate($value, $descriptionField);
                break;

            case 'object':
                $this->validateObject($value, $descriptionField);
                break;

            case 'array':
                $this->validateArray($value, $descriptionField);
                break;

            default:
141
                if (is_string($descriptionField->type) && $descriptionField->type[0] == '#') {
Cyril Vazquez's avatar
Cyril Vazquez committed
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
                    $descriptionField->properties = $this->descriptionSchemeController->getDescriptionFields(substr($descriptionField->type, 1));
                    $this->validateObject($value, $descriptionField);
                }
        }
    }

    protected function validateName($value, $descriptionField)
    {
        if (!empty($descriptionField->enumeration) && !in_array($value, $descriptionField->enumeration) && $value != '') {
            throw new \core\Exception\BadRequestException('Forbidden value for metadata %1$s', 400, null, [$descriptionField->name]);
        }
    }

    protected function validateText($value, $descriptionField)
    {
        return true;
    }

    protected function validateNumber($value, $descriptionField)
    {
162
        if (!is_numeric($value)) {
Cyril Vazquez's avatar
Cyril Vazquez committed
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
            throw new \core\Exception\BadRequestException('Invalid value for metadata %1$s', 400, null, [$descriptionField->name]);
        }
    }

    protected function validateBoolean($value, $descriptionField)
    {
        if (!is_bool($value) && !in_array($value, [0, 1])) {
            throw new \core\Exception\BadRequestException('Invalid value for metadata %1$s', 400, null, [$descriptionField->name]);
        }
    }

    protected function validateDate($value, $descriptionField)
    {
        if (!preg_match('#^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2})?(\.\d+)?(([+-]\d{2}:\d{2})|Z)?)?$#', $value)) {
            throw new \core\Exception\BadRequestException('Invalid value for metadata %1$s', 400, null, [$descriptionField->name]);
        }

        try {
            new \DateTime($value);
        } catch (\Exception $e) {
            throw new \core\Exception\BadRequestException('Invalid value for metadata %1$s', 400, null, [$descriptionField->name]);
        }
    }

    protected function validateObject($object, $descriptionField)
    {
        // Validate object against type properties
        if (isset($descriptionField->properties)) {
            foreach ($descriptionField->properties as $name => $property) {
                if (!isset($object->{$name})) {
                    if (isset($property->required)) {
                        throw new \core\Exception\BadRequestException('Null value not allowed for metadata %1$s', 400, null, [$descriptionField->name.'-'.$name]);
                    }
                    continue;
                }

                $value = $object->{$name};

                $this->validateDescriptionField($value, $property);
            }
        }

        // Validate additionnal properties
        foreach ($object as $name => $value) {
            if (isset($descriptionField->properties) && !array_key_exists($name, $descriptionField->properties) && !isset($descriptionField->additionnalProperties)) {
                throw new \core\Exception\BadRequestException('Metadata %1$s is not allowed', 400, null, [$descriptionField->name.'-'.$name]);
            }
        }
    }

    protected function validateArray($array, $descriptionField)
    {
        if (isset($descriptionField->minItems) && count($array) < $descriptionField->minItems) {
            throw new \core\Exception\BadRequestException('Metadata %1$s does not have enough values', 400, null, [$descriptionField->name.'-'.$name]);
        }

        if (isset($descriptionField->maxItems) && count($array) > $descriptionField->maxItems) {
            throw new \core\Exception\BadRequestException('Metadata %1$s has too many values', 400, null, [$descriptionField->name.'-'.$name]);
        }

        if (isset($descriptionField->itemType)) {
            if (is_string($descriptionField->itemType)) {
                $descriptionField = (object) [
                    "type" => $descriptionField->itemType
                ];
            } else {
                $descriptionField = $descriptionField->itemType;
            }

            foreach ($array as $name => $value) {
                $this->validateDescriptionField($value, $descriptionField);
            }
        }
    }

    /**
     * Validate the archive management metadata
     *
     * @param \bundle\recordsManagement\Controller\recordsManagement/archive $archive
     */
243
    public function validateManagementMetadata($archive)
Cyril Vazquez's avatar
Cyril Vazquez committed
244
    {
245
246
247
        $organization = $this->sdoFactory->read('organization/organization', ['registrationNumber' => $archive->originatorOrgRegNumber]);

        if (!is_null($organization->enabled) && $organization->enabled === false) {
Jerome Boucher's avatar
Jerome Boucher committed
248
            throw new \core\Exception("The deposit has been blocked because activity is disabled.");
249
250
251
252
        }

        $this->checkRights($archive);

Cyril Vazquez's avatar
Cyril Vazquez committed
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
        if (isset($archive->archivalProfileReference) && !$this->sdoFactory->exists("recordsManagement/archivalProfile", ["reference"=>$archive->archivalProfileReference])) {
            throw new \core\Exception\NotFoundException("The archival profile reference not found");
        }

        if (isset($archive->retentionRuleCode) && !$this->sdoFactory->exists("recordsManagement/retentionRule", $archive->retentionRuleCode)) {
            throw new \core\Exception\NotFoundException("The retention rule not found");
        }

        if (isset($archive->accessRuleCode) && !$this->sdoFactory->exists("recordsManagement/accessRule", $archive->accessRuleCode)) {
            throw new \core\Exception\NotFoundException("The access rule not found");
        }

        $nbArchiveObjects = 0;

        if (!empty($archive->contents)) {
            $nbArchiveObjects = count($archive->contents);
        }

        if ($nbArchiveObjects) {
            $containedProfiles = [];
273
274
275
276
277
            if (isset($archive->archivalProfileReference)) {
                $this->useArchivalProfile($archive->archivalProfileReference);
                foreach ($this->currentArchivalProfile->containedProfiles as $profile) {
                    $containedProfiles[] = $profile->reference;
                }
Cyril Vazquez's avatar
Cyril Vazquez committed
278
279
280
            }

            for ($i = 0; $i < $nbArchiveObjects; $i++) {
281
282
283
284
285
286
                if (isset($archive->archivalProfileReference)) {
                    if (empty($archive->contents[$i]->archivalProfileReference)) {
                        if (!$this->currentArchivalProfile->acceptArchiveWithoutProfile) {
                            throw new \core\Exception\BadRequestException("Invalid contained archive profile %s", 400, null, $archive->contents[$i]->archivalProfileReference);
                        }
                    } elseif (!in_array($archive->contents[$i]->archivalProfileReference, $containedProfiles)) {
Cyril Vazquez's avatar
Cyril Vazquez committed
287
288
289
290
291
292
293
294
295
296
297
298
299
300
                        throw new \core\Exception\BadRequestException("Invalid contained archive profile %s", 400, null, $archive->contents[$i]->archivalProfileReference);
                    }
                }

                $this->validateManagementMetadata($archive->contents[$i]);
            }
        }
    }

    /**
     * Validate archival profile of a child archive
     *
     * @param \bundle\recordsManagement\Controller\recordsManagement/archive $archive
     */
301
    public function validateFileplan($archive)
Cyril Vazquez's avatar
Cyril Vazquez committed
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
    {
        // No parent, check orgUnit can deposit with the profile
        if (empty($archive->parentArchiveId)) {
            if (!$this->organizationController->checkProfileInOrgAccess($archive->archivalProfileReference, $archive->originatorOrgRegNumber)) {
                throw new \core\Exception\BadRequestException("Invalid archive profile");
            }

            return;
        }

        // Parent : read and check fileplan
        $parentArchive = $this->sdoFactory->read('recordsManagement/archive', $archive->parentArchiveId);

        // Check level in file plan
        if ($parentArchive->fileplanLevel == 'item') {
            throw new \core\Exception\BadRequestException("Parent archive is an item and can not contain items.");
        }

        // No profile on parent, accept any profile
        if (empty($parentArchive->archivalProfileReference)) {
            return;
        }

        // Load parent archive profile
        if (!isset($this->archivalProfiles[$parentArchive->archivalProfileReference])) {
            $parentArchivalProfile = $this->archivalProfileController->getByReference($parentArchive->archivalProfileReference);
            $this->archivalProfiles[$parentArchive->archivalProfileReference] = $parentArchivalProfile;
        } else {
            $parentArchivalProfile = $this->archivalProfiles[$parentArchive->archivalProfileReference];
        }

        // No profile : check parent profile accepts archives without profile
        if (empty($archive->archivalProfileReference) && $parentArchivalProfile->acceptArchiveWithoutProfile) {
            return;
        }

        // Profile on content : check profile is accepted
        foreach ($parentArchivalProfile->containedProfiles as $containedProfile) {
            if ($containedProfile->reference == $archive->archivalProfileReference) {
                return;
            }
        }

        throw new \core\Exception\BadRequestException("Invalid archive profile");
    }


    /**
     * Validate the archive management metadata
     *
     * @param \bundle\recordsManagement\Controller\recordsManagement/archive $archive
     */
    protected function validateAttachments($archive)
    {
        if (!$archive->digitalResources) {
            $archive->digitalResources = [];
        }

        foreach ($archive->digitalResources as $digitalResource) {
            if (empty($digitalResource->archiveId)) {
                $digitalResource->archiveId = $archive->archiveId;
            }

            if (empty($digitalResource->resId)) {
                $digitalResource->resId = \laabs::newId();
            }

369
            $this->validateDigitalResource($digitalResource);
Cyril Vazquez's avatar
Cyril Vazquez committed
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385

            if (empty($digitalResource->hash) || empty($digitalResource->hashAlgorithm)) {
                $this->digitalResourceController->getHash($digitalResource, $this->hashAlgorithm);
            }
        }

        $nbArchiveObjects = 0;
        if (!empty($archive->contents)) {
            $nbArchiveObjects = count($archive->contents);
        }

        for ($i = 0; $i < $nbArchiveObjects; $i++) {
            $this->validateAttachments($archive->contents[$i]);
        }
    }

386
387
388
389
390
391
392
393
394
    /**
     * Validate resource content
     *
     * @param \bundle\digitalResource\digitalResource $digitalResource resource
     *
     * @return
     */
    public function validateDigitalResource($digitalResource)
    {
395
        // Create temp file
Cyril Vazquez's avatar
Cyril Vazquez committed
396
        $handler = $digitalResource->getHandler();
397
        $filename = tempnam(sys_get_temp_dir(), 'digitalResource.format');
398
399
400
401
        $temp = fopen($filename, 'w');
        stream_copy_to_stream($handler, $temp);
        rewind($handler);
        fclose($temp);
402

403
        $digitalResource->size = filesize($filename);
Cyril Vazquez's avatar
Cyril Vazquez committed
404

Cyril Vazquez's avatar
Cyril Vazquez committed
405
        if ($digitalResource->size == 0) {
406
            unlink($filename);
Cyril Vazquez's avatar
Cyril Vazquez committed
407
408
409
            throw new \bundle\recordsManagement\Exception\invalidArchiveException('Resource size is null', 400);
        }

410
411
412
413
414
        if (!isset($digitalResource->mimetype)) {
            $finfo = new \finfo();
            $mimetype = $finfo->file($filename, FILEINFO_MIME_TYPE);
            $digitalResource->mimetype = $mimetype;
        }
415
        
416
417
418
419
420
421
422
423
424
425
426
427
428
        $formatDetection = strrpos($this->currentServiceLevel->control, "formatDetection") === false ? false : true;
        if ($formatDetection) {
            $format = $this->formatController->identifyFormat($filename);

            if ($format) {
                $digitalResource->puid = $format->puid;
            }
        }

        $formatValidation = strrpos($this->currentServiceLevel->control, "formatValidation") === false ? false : true;
        if ($formatValidation) {
            $validation = $this->formatController->validateFormat($filename);
            if (!$validation !== true && is_array($validation)) {
429
                unlink($filename);
430
431
432
                throw new \core\Exception\BadRequestException("Invalid format attachments for %s", 404, null, [$digitalResource->fileName]);
            }
        }
433
        unlink($filename);
434
435
    }
}