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