Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
47.90% covered (danger)
47.90%
80 / 167
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
StoryRenderer
47.90% covered (danger)
47.90%
80 / 167
0.00% covered (danger)
0.00%
0 / 8
97.79
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
 renderNoJS
79.55% covered (warning)
79.55%
35 / 44
0.00% covered (danger)
0.00%
0 / 1
4.14
 getStoryData
95.74% covered (success)
95.74%
45 / 47
0.00% covered (danger)
0.00%
0 / 1
9
 getUrl
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getAttribution
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
12
 getNoJsFrameHtmlString
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 getAttributionHtmlString
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
2
 getLicensesHtmlString
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\Wikistories;
4
5use MediaWiki\Context\RequestContext;
6use MediaWiki\FileRepo\File\File;
7use MediaWiki\FileRepo\RepoGroup;
8use MediaWiki\Html\Html;
9use MediaWiki\Media\FormatMetadata;
10use MediaWiki\SpecialPage\SpecialPage;
11use MediaWiki\Title\Title;
12
13class StoryRenderer {
14
15    public function __construct(
16        private readonly RepoGroup $repoGroup,
17        private readonly StoryContentAnalyzer $analyzer,
18        private readonly StoryTrackingCategories $storyTrackingCategories,
19    ) {
20    }
21
22    /**
23     * @param array $storyData
24     * @return array [ 'html', 'style' ]
25     */
26    public function renderNoJS( array $storyData ): array {
27        $articleTitle = Title::makeTitle(
28            NS_MAIN,
29            $storyData[ 'articleTitle' ],
30            '/story/' . $storyData[ 'articleId' ]
31        );
32        $missingImages = array_filter( $storyData[ 'frames' ], static function ( $frame ) {
33            return $frame[ 'fileNotFound' ];
34        } );
35
36        $html = Html::element(
37            'a',
38            [ 'href' => $articleTitle->getLinkURL() ],
39            $articleTitle->getText()
40        );
41
42        if ( in_array( $this->storyTrackingCategories::TC_NO_ARTICLE, $storyData[ 'trackingCategories' ] ) ) {
43            $context = RequestContext::getMain();
44            $html .= Html::errorBox(
45                $context->msg( 'wikistories-nojs-viewer-no-article' )->parse()
46            );
47        }
48
49        if ( count( $missingImages ) > 0 ) {
50            $context = RequestContext::getMain();
51            $html .= Html::warningBox(
52                $context->msg( 'wikistories-nojs-viewer-error' )->params( count( $missingImages ) )->parse()
53            );
54        }
55
56        $html .= Html::rawElement(
57            'div',
58            [ 'class' => 'ext-wikistories-viewer-nojs' ],
59            implode( '', array_map( function ( $frame ) {
60                return Html::rawElement(
61                    'div',
62                    [
63                        'class' => 'ext-wikistories-viewer-nojs-frame',
64                        'style' => $frame[ 'url' ] === '' ?
65                            'background-color: #000'
66                            :
67                            'background-image:url(' . $frame[ 'url' ] . ');',
68                    ],
69                    $this->getNoJsFrameHtmlString( $frame )
70                );
71            }, $storyData[ 'frames' ] ) )
72        );
73
74        return [
75            'html' => $html,
76            'style' => 'ext.wikistories.viewer-nojs'
77        ];
78    }
79
80    public function getStoryData(
81        StoryContent $story,
82        Title $storyTitle
83    ): array {
84        $frames = $story->getFrames();
85        $filesUsed = array_map( static function ( $frame ) {
86            return $frame->image->filename;
87        }, $frames );
88        $files = $this->repoGroup->findFiles( $filesUsed );
89        $firstFrame = reset( $frames );
90        $thumb = $firstFrame ? $this->getUrl( $files, $firstFrame->image->filename, 52 ) : '';
91        $article = $story->getArticleTitle();
92        $trackingCategories = [];
93
94        if ( !$article ) {
95            $trackingCategories[] = $this->storyTrackingCategories::TC_NO_ARTICLE;
96        }
97
98        $data = [
99            'articleId' => $article ? $article->getId() : 0,
100            'articleTitle' => $article ? $article->getDBkey() : '',
101            'storyId' => $storyTitle->getId(),
102            'storyTitle' => $storyTitle->getText(),
103            'editUrl' => SpecialPage::getTitleFor( 'StoryBuilder', $storyTitle->getPrefixedDBkey() )->getLinkURL(),
104            'talkUrl' => $storyTitle->getTalkPageIfDefined()->getLinkURL(),
105            'shareUrl' => $storyTitle->getFullURL( [ 'action' => 'storyview' ] ),
106            'thumbnail' => $thumb,
107            'frames' => array_map( function ( $frame ) use ( $files, $article, &$trackingCategories ) {
108                $url = $this->getUrl( $files, $frame->image->filename, 640 );
109                if ( $url === '' ) {
110                    $trackingCategories[] = $this->storyTrackingCategories::TC_NO_IMAGE;
111                }
112                $outdatedText = $article &&
113                    ( $articleText = $this->analyzer->getArticleText( $article ) ) !== null &&
114                    $this->analyzer->isOutdatedText(
115                        $articleText,
116                        $frame->text->value,
117                        $frame->text->fromArticle->originalText ?? ''
118                    );
119                if ( $outdatedText ) {
120                    $trackingCategories[] = $this->storyTrackingCategories::TC_OUTDATED_TEXT;
121                }
122                return [
123                    'url' => $url,
124                    'filename' => $frame->image->filename,
125                    'focalRect' => $frame->image->focalRect ?? null,
126                    'fileNotFound' => $url === '',
127                    'text' => $frame->text->value,
128                    'textFromArticle' => $frame->text->fromArticle->originalText ?? '',
129                    'outdatedText' => $outdatedText,
130                    'attribution' => $this->getAttribution( $files, $frame->image->filename ),
131                ];
132            }, $frames )
133        ];
134        $data[ 'trackingCategories' ] = array_unique( $trackingCategories );
135        return $data;
136    }
137
138    private function getUrl( array $files, string $filename, int $size ): string {
139        /** @var File $file */
140        $file = $files[ strtr( $filename, ' ', '_' ) ] ?? false;
141        if ( !$file ) {
142            return '';
143        }
144        return $file->createThumb( $size );
145    }
146
147    /**
148     * @param array $files
149     * @param string $filename
150     * @return array Data structure with attribution information such author or license
151     */
152    private function getAttribution( array $files, string $filename ): array {
153        /** @var File $file */
154        $file = $files[ strtr( $filename, ' ', '_' ) ] ?? false;
155        if ( !$file ) {
156            return [
157                'author' => '',
158                'license' => [],
159                'url' => '',
160            ];
161        }
162
163        $formatMetadata = new FormatMetadata();
164        $metadata = $formatMetadata->fetchExtendedMetadata( $file );
165        $rawAuthor = $metadata[ 'Artist' ][ 'value' ] ?? '';
166        $author = is_array( $rawAuthor ) ? reset( $rawAuthor ) : $rawAuthor;
167        $licenseString = $metadata[ 'LicenseShortName' ][ 'value' ] ?? '';
168        $licenseTypes = [ 'CC', 'BY', 'SA', 'Fair', 'Public' ];
169        $license = array_filter( $licenseTypes, static function ( $license ) use ( $licenseString ) {
170            return strpos( $licenseString, $license ) !== false;
171        } );
172
173        return [
174            'author' => strip_tags( $author ),
175            'license' => $license,
176            'url' => $file->getDescriptionUrl(),
177        ];
178    }
179
180    /**
181     * @param array $frame
182     * @return string html of the given frame data
183     */
184    private function getNoJsFrameHtmlString( array $frame ): string {
185        $html = Html::element(
186            'div',
187            [ 'class' => 'ext-wikistories-viewer-nojs-frame-text' ],
188            $frame[ 'text' ]
189        );
190
191        if ( $frame[ 'attribution' ][ 'license'] !== [] ) {
192            $html .= Html::rawElement(
193                'div',
194                [ 'class' => 'ext-wikistories-viewer-nojs-frame-attribution' ],
195                $this->getAttributionHtmlString( $frame[ 'attribution' ] )
196            );
197        }
198        return $html;
199    }
200
201    /**
202     * @param array $attribution
203     * @return string html of the given attribution data
204     */
205    private function getAttributionHtmlString( array $attribution ): string {
206        $attributionString = Html::rawElement(
207            'div',
208            [ 'class' => 'ext-wikistories-viewer-nojs-frame-attribution-info' ],
209            implode( '', [
210                'license' => $this->getLicensesHtmlString( $attribution[ 'license' ] ),
211                'author' => Html::rawElement(
212                    'div',
213                    [
214                        'class' => 'ext-wikistories-viewer-nojs-frame-attribution-info-author',
215                        'title' => $attribution[ 'author' ],
216                    ],
217                    $attribution[ 'author' ]
218                )
219            ] )
220        );
221
222        $attributionString .= Html::rawElement(
223            'div',
224            [ 'class' => 'ext-wikistories-viewer-nojs-frame-attribution-more-info' ],
225            Html::element(
226                'a',
227                [ 'href' => $attribution[ 'url' ],
228                    'target' => '_blank',
229                    'class' => 'ext-wikistories-viewer-nojs-frame-attribution-more-info-link'
230                ],
231                ''
232            )
233        );
234
235        return $attributionString;
236    }
237
238    /**
239     * @param array $license
240     * @return string html of the given license data
241     */
242    private function getLicensesHtmlString( array $license ): string {
243        return implode( '', array_map( static function ( $licenseType ) {
244            return Html::rawElement(
245                'div',
246                [
247                    'class' => 'ext-wikistories-viewer-nojs-frame-attribution-info-license' .
248                    ' ext-wikistories-viewer-nojs-frame-attribution-info-' . strtolower( $licenseType )
249                ],
250                ''
251            );
252        }, $license ) );
253    }
254}