Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
43.37% covered (danger)
43.37%
36 / 83
25.00% covered (danger)
25.00%
2 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialBookSources
43.90% covered (danger)
43.90%
36 / 82
25.00% covered (danger)
25.00%
2 / 8
135.33
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
 execute
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 isValidISBN
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
11
 cleanIsbn
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildForm
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
2
 showList
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
30
 makeListItem
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Specials;
8
9use MediaWiki\Content\TextContent;
10use MediaWiki\Html\Html;
11use MediaWiki\HTMLForm\HTMLForm;
12use MediaWiki\Revision\RevisionLookup;
13use MediaWiki\Revision\SlotRecord;
14use MediaWiki\SpecialPage\SpecialPage;
15use MediaWiki\Title\TitleFactory;
16use UnexpectedValueException;
17
18/**
19 * Information on citing a book with a particular ISBN.
20 *
21 * The parser can create automatic links to this special page when
22 * it sees an ISBN in wikitext.
23 *
24 * @author Rob Church <robchur@gmail.com>
25 * @ingroup SpecialPage
26 */
27class SpecialBookSources extends SpecialPage {
28
29    public function __construct(
30        private readonly RevisionLookup $revisionLookup,
31        private readonly TitleFactory $titleFactory
32    ) {
33        parent::__construct( 'Booksources' );
34    }
35
36    /**
37     * @param string|null $isbn ISBN passed as a subpage parameter
38     */
39    public function execute( $isbn ) {
40        $out = $this->getOutput();
41
42        $this->setHeaders();
43        $this->outputHeader();
44
45        // User provided ISBN
46        $isbn = $isbn ?: $this->getRequest()->getText( 'isbn' );
47        $isbn = trim( $isbn );
48
49        $this->buildForm( $isbn );
50
51        if ( $isbn !== '' ) {
52            if ( !self::isValidISBN( $isbn ) ) {
53                $out->wrapWikiMsg(
54                    "<div class=\"error\">\n$1\n</div>",
55                    'booksources-invalid-isbn'
56                );
57            }
58
59            $this->showList( $isbn );
60        }
61    }
62
63    /**
64     * Return whether a given ISBN (10 or 13) is valid.
65     *
66     * @param string $isbn ISBN passed for check
67     * @return bool
68     */
69    public static function isValidISBN( $isbn ) {
70        $isbn = self::cleanIsbn( $isbn );
71        $sum = 0;
72        if ( strlen( $isbn ) == 13 ) {
73            for ( $i = 0; $i < 12; $i++ ) {
74                if ( $isbn[$i] === 'X' ) {
75                    return false;
76                } elseif ( $i % 2 == 0 ) {
77                    $sum += (int)$isbn[$i];
78                } else {
79                    $sum += 3 * (int)$isbn[$i];
80                }
81            }
82
83            $check = ( 10 - ( $sum % 10 ) ) % 10;
84            if ( (string)$check === $isbn[12] ) {
85                return true;
86            }
87        } elseif ( strlen( $isbn ) == 10 ) {
88            for ( $i = 0; $i < 9; $i++ ) {
89                if ( $isbn[$i] === 'X' ) {
90                    return false;
91                }
92                $sum += (int)$isbn[$i] * ( $i + 1 );
93            }
94
95            $check = $sum % 11;
96            if ( $check == 10 ) {
97                $check = "X";
98            }
99            if ( (string)$check === $isbn[9] ) {
100                return true;
101            }
102        }
103
104        return false;
105    }
106
107    /**
108     * Trim ISBN and remove characters which aren't required
109     *
110     * @param string $isbn Unclean ISBN
111     * @return string
112     */
113    private static function cleanIsbn( $isbn ) {
114        return trim( preg_replace( '![^0-9X]!', '', $isbn ) );
115    }
116
117    /**
118     * Generate a form to allow users to enter an ISBN
119     *
120     * @param string $isbn
121     */
122    private function buildForm( $isbn ) {
123        $formDescriptor = [
124            'isbn' => [
125                'type' => 'text',
126                'name' => 'isbn',
127                'label-message' => 'booksources-isbn',
128                'default' => $isbn,
129                'autofocus' => true,
130                'required' => true,
131            ],
132        ];
133
134        HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() )
135            ->setTitle( $this->getPageTitle() )
136            ->setWrapperLegendMsg( 'booksources-search-legend' )
137            ->setSubmitTextMsg( 'booksources-search' )
138            ->setMethod( 'get' )
139            ->prepareForm()
140            ->displayForm( false );
141    }
142
143    /**
144     * Determine where to get the list of book sources from,
145     * format and output them
146     *
147     * @param string $isbn
148     * @return bool
149     */
150    private function showList( $isbn ) {
151        $out = $this->getOutput();
152
153        $isbn = self::cleanIsbn( $isbn );
154        # Hook to allow extensions to insert additional HTML,
155        # e.g. for API-interacting plugins and so on
156        $this->getHookRunner()->onBookInformation( $isbn, $out );
157
158        # Check for a local page such as Project:Book_sources and use that if available
159        $page = $this->msg( 'booksources' )->inContentLanguage()->text();
160        // Show list in content language
161        $title = $this->titleFactory->makeTitleSafe( NS_PROJECT, $page );
162        if ( is_object( $title ) && $title->exists() ) {
163            $rev = $this->revisionLookup->getRevisionByTitle( $title );
164            $content = $rev->getContent( SlotRecord::MAIN );
165
166            if ( $content instanceof TextContent ) {
167                // XXX: in the future, this could be stored as structured data, defining a list of book sources
168
169                $text = $content->getText();
170                $out->addWikiTextAsInterface( str_replace( 'MAGICNUMBER', $isbn, $text ) );
171
172                return true;
173            } else {
174                throw new UnexpectedValueException(
175                    "Unexpected content type for book sources: " . $content->getModel()
176                );
177            }
178        }
179
180        # Fall back to the defaults given in the language file
181        $out->addWikiMsg( 'booksources-text' );
182        $out->addHTML( '<ul>' );
183        $items = $this->getContentLanguage()->getBookstoreList();
184        foreach ( $items as $label => $url ) {
185            $out->addHTML( $this->makeListItem( $isbn, $label, $url ) );
186        }
187        $out->addHTML( '</ul>' );
188
189        return true;
190    }
191
192    /**
193     * Format a book source list item
194     *
195     * @param string $isbn
196     * @param string $label Book source label
197     * @param string $url Book source URL
198     * @return string
199     */
200    private function makeListItem( $isbn, $label, $url ) {
201        $url = str_replace( '$1', $isbn, $url );
202
203        return Html::rawElement( 'li', [],
204            Html::element( 'a', [ 'href' => $url, 'class' => 'external' ], $label )
205        );
206    }
207
208    /** @inheritDoc */
209    protected function getGroupName() {
210        return 'wiki';
211    }
212}
213
214/** @deprecated class alias since 1.41 */
215class_alias( SpecialBookSources::class, 'SpecialBookSources' );