Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.10% covered (warning)
88.10%
37 / 42
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
StoryValidator
88.10% covered (warning)
88.10%
37 / 42
50.00% covered (danger)
50.00%
1 / 2
13.29
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
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    /** @var ServiceOptions */
21    private $options;
22
23    /** @var RepoGroup */
24    private $repoGroup;
25
26    /** @var PageLookup */
27    private $pageLookup;
28
29    /** @var TitleFactory */
30    private $titleFactory;
31
32    /**
33     * @param ServiceOptions $options
34     * @param RepoGroup $repoGroup
35     * @param PageLookup $pageLookup
36     * @param TitleFactory $titleFactory
37     */
38    public function __construct(
39        ServiceOptions $options,
40        RepoGroup $repoGroup,
41        PageLookup $pageLookup,
42        TitleFactory $titleFactory
43    ) {
44        $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
45        $this->options = $options;
46        $this->repoGroup = $repoGroup;
47        $this->pageLookup = $pageLookup;
48        $this->titleFactory = $titleFactory;
49    }
50
51    /**
52     * @param StoryContent $story
53     * @return StatusValue
54     */
55    public function isValid( StoryContent $story ): StatusValue {
56        // Special case: empty content needs to be valid
57        // todo: make sure we can't save empty content stories with API
58        if ( $story->getText() === '{}' ) {
59            return StatusValue::newGood();
60        }
61
62        // Validation based on json schema
63        $schemaPath = __DIR__ . "/../story.schema.v1.json";
64        $validator = new Validator();
65        $validator->check( $story->getData()->getValue(), (object)[ '$ref' => 'file://' . $schemaPath ] );
66        if ( !$validator->isValid() ) {
67            // todo: find a way to include error messages from the schema validator
68            return StatusValue::newFatal( 'wikistories-invalid-format' );
69        }
70
71        // Article exists
72        $page = $this->pageLookup->getPageById( $story->getArticleId() );
73        if ( !$page ) {
74            return StatusValue::newFatal( 'wikistories-from-article-not-found', $story->getArticleId() );
75        }
76
77        // Validation based on config
78        $frameCount = count( $story->getFrames() );
79        if ( $frameCount < $this->options->get( 'WikistoriesMinFrames' ) ) {
80            return StatusValue::newFatal( 'wikistories-not-enough-frames' );
81        }
82        if ( $frameCount > $this->options->get( 'WikistoriesMaxFrames' ) ) {
83            return StatusValue::newFatal( 'wikistories-too-many-frames' );
84        }
85        $maxTextLength = $this->options->get( 'WikistoriesMaxTextLength' );
86        foreach ( $story->getFrames() as $index => $frame ) {
87            $textLength = mb_strlen( $frame->text->value );
88            if ( $textLength > $maxTextLength ) {
89                return StatusValue::newFatal(
90                    'wikistories-text-too-long',
91                    $index + 1,
92                    $textLength,
93                    $maxTextLength
94                );
95            }
96        }
97
98        // Files exist
99        $filesUsed = array_map( static function ( $frame ) {
100            return strtr( $frame->image->filename, ' ', '_' );
101        }, $story->getFrames() );
102        $files = $this->repoGroup->findFiles( $filesUsed );
103
104        foreach ( $filesUsed as $name ) {
105            if ( !isset( $files[ $name ] ) ) {
106                return StatusValue::newFatal( 'wikistories-file-not-found', $name );
107            }
108        }
109
110        // Categories must be valid but may not exist yet
111        foreach ( $story->getCategories() as $categoryName ) {
112            $categoryTitle = $this->titleFactory->makeTitleSafe( NS_CATEGORY, $categoryName );
113            if ( $categoryTitle === null ) {
114                return StatusValue::newFatal( 'wikistories-invalid-category-name', $categoryName );
115            }
116        }
117
118        return StatusValue::newGood();
119    }
120}