Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.19% covered (warning)
89.19%
33 / 37
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
StoryValidator
89.19% covered (warning)
89.19%
33 / 37
50.00% covered (danger)
50.00%
1 / 2
11.15
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 isValid
100.00% covered (success)
100.00%
33 / 33
100.00% covered (success)
100.00%
1 / 1
10
1<?php
2
3namespace MediaWiki\Extension\Wikistories;
4
5use JsonSchema\Validator;
6use MediaWiki\Config\ServiceOptions;
7use MediaWiki\Page\PageLookup;
8use RepoGroup;
9use StatusValue;
10
11class StoryValidator {
12
13    public const CONSTRUCTOR_OPTIONS = [
14        'WikistoriesMinFrames',
15        'WikistoriesMaxFrames',
16        'WikistoriesMaxTextLength',
17    ];
18
19    /** @var ServiceOptions */
20    private $options;
21
22    /** @var RepoGroup */
23    private $repoGroup;
24
25    /** @var PageLookup */
26    private $pageLookup;
27
28    /**
29     * @param ServiceOptions $options
30     * @param RepoGroup $repoGroup
31     * @param PageLookup $pageLookup
32     */
33    public function __construct(
34        ServiceOptions $options,
35        RepoGroup $repoGroup,
36        PageLookup $pageLookup
37    ) {
38        $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
39        $this->options = $options;
40        $this->repoGroup = $repoGroup;
41        $this->pageLookup = $pageLookup;
42    }
43
44    /**
45     * @param StoryContent $story
46     * @return StatusValue
47     */
48    public function isValid( StoryContent $story ): StatusValue {
49        // Special case: empty content needs to be valid
50        // todo: make sure we can't save empty content stories with API
51        if ( $story->getText() === '{}' ) {
52            return StatusValue::newGood();
53        }
54
55        // Validation based on json schema
56        $schemaPath = __DIR__ . "/../story.schema.v1.json";
57        $validator = new Validator();
58        $validator->check( $story->getData()->getValue(), (object)[ '$ref' => 'file://' . $schemaPath ] );
59        if ( !$validator->isValid() ) {
60            // todo: find a way to include error messages from the schema validator
61            return StatusValue::newFatal( 'wikistories-invalid-format' );
62        }
63
64        // Article exists
65        $page = $this->pageLookup->getPageById( $story->getArticleId() );
66        if ( !$page ) {
67            return StatusValue::newFatal( 'wikistories-from-article-not-found', $story->getArticleId() );
68        }
69
70        // Validation based on config
71        $frameCount = count( $story->getFrames() );
72        if ( $frameCount < $this->options->get( 'WikistoriesMinFrames' ) ) {
73            return StatusValue::newFatal( 'wikistories-not-enough-frames' );
74        }
75        if ( $frameCount > $this->options->get( 'WikistoriesMaxFrames' ) ) {
76            return StatusValue::newFatal( 'wikistories-too-many-frames' );
77        }
78        $maxTextLength = $this->options->get( 'WikistoriesMaxTextLength' );
79        foreach ( $story->getFrames() as $index => $frame ) {
80            $textLength = mb_strlen( $frame->text->value );
81            if ( $textLength > $maxTextLength ) {
82                return StatusValue::newFatal(
83                    'wikistories-text-too-long',
84                    $index + 1,
85                    $textLength,
86                    $maxTextLength
87                );
88            }
89        }
90
91        // Files exist
92        $filesUsed = array_map( static function ( $frame ) {
93            return strtr( $frame->image->filename, ' ', '_' );
94        }, $story->getFrames() );
95        $files = $this->repoGroup->findFiles( $filesUsed );
96
97        foreach ( $filesUsed as $name ) {
98            if ( !isset( $files[ $name ] ) ) {
99                return StatusValue::newFatal( 'wikistories-file-not-found', $name );
100            }
101        }
102
103        return StatusValue::newGood();
104    }
105}