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