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