Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
89.19% |
33 / 37 |
|
50.00% |
1 / 2 |
CRAP | |
0.00% |
0 / 1 |
StoryValidator | |
89.19% |
33 / 37 |
|
50.00% |
1 / 2 |
11.15 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
isValid | |
100.00% |
33 / 33 |
|
100.00% |
1 / 1 |
10 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Wikistories; |
4 | |
5 | use JsonSchema\Validator; |
6 | use MediaWiki\Config\ServiceOptions; |
7 | use MediaWiki\Page\PageLookup; |
8 | use RepoGroup; |
9 | use StatusValue; |
10 | |
11 | class 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 | } |