Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
70.00% covered (warning)
70.00%
21 / 30
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
SectionTitleFetcher
70.00% covered (warning)
70.00%
21 / 30
50.00% covered (danger)
50.00%
1 / 2
12.70
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fetchSectionTitles
68.97% covered (warning)
68.97%
20 / 29
0.00% covered (danger)
0.00%
0 / 1
11.42
1<?php
2
3declare( strict_types=1 );
4
5namespace ContentTranslation\Service;
6
7use ContentTranslation\SiteMapper;
8use InvalidArgumentException;
9use MediaWiki\Http\HttpRequestFactory;
10use MediaWiki\Json\FormatJson;
11use MediaWiki\Title\Title;
12use Psr\Log\LoggerInterface;
13
14class SectionTitleFetcher {
15
16    public function __construct(
17        private readonly HttpRequestFactory $httpRequestFactory,
18        private readonly LoggerInterface $logger
19    ) {
20    }
21
22    /**
23     * Given a target language code and a page title or a revision id (at least one of them
24     * should be provided), this method fetches the target page sections using the MediaWiki
25     * Action Api and returns an array containing the titles for all the first-level page sections,
26     * indexed by their section numbers.
27     *
28     * If the HTTP request for fetching the section titles fails, the method returns an
29     * empty array.
30     *
31     * If the sections cannot be successfully retrieved from the API (i.e. when the request is
32     * completed but the requested page doesn't exist), the method returns null.
33     *
34     * If both page title and page revision are provided, the page revision is ignored and only
35     * the page title is used.
36     *
37     * @param string $targetLanguage
38     * @param Title|null $pageTitle The title of the page
39     * @param int|null $revision
40     * @return array<int,string>|null e.g. [ 1 => "Section 1", 2 => "Section 2" ]
41     */
42    public function fetchSectionTitles( string $targetLanguage, ?Title $pageTitle, ?int $revision = null ): ?array {
43        if ( !$pageTitle && !$revision ) {
44            throw new InvalidArgumentException( 'Either page title or page revision should be provided' );
45        }
46
47        $params = [
48            'action' => 'parse',
49            'prop' => 'sections',
50            'format' => 'json',
51            'formatversion' => 2
52        ];
53
54        if ( $pageTitle ) {
55            $params['page'] = $pageTitle->getPrefixedDBKey();
56        } else {
57            $params['oldid'] = $revision;
58        }
59        // Example URLs:
60        // https://en.wikipedia.org/w/api.php?action=parse&format=json&prop=sections&formatversion=2&page=Football
61        // https://en.wikipedia.org/w/api.php?action=parse&format=json&prop=sections&formatversion=2&oldid=1161269327
62        $url = SiteMapper::getApiURL( $targetLanguage, $params );
63
64        try {
65            $response = $this->httpRequestFactory->get( $url, [], __METHOD__ );
66        } catch ( \Exception $e ) {
67            $logParams = [ 'url' => $url, 'exception' => $e->getMessage() ];
68            $this->logger->info( 'Request to fetch section titles failed', $logParams );
69
70            return [];
71        }
72
73        if ( !$response ) {
74            $this->logger->info( 'Request to fetch section titles returned empty response', [ 'url' => $url ] );
75            return null;
76        }
77        $json = FormatJson::decode( $response, true );
78
79        // if an invalid title is provided the response is a json containing an error
80        // (e.g. { error: { code "missingtitle" } }
81        // successful responses are json objects with "parse.sections" property set
82        if ( !isset( $json['parse']['sections'] ) ) {
83            return null;
84        }
85
86        $sections = $json['parse']['sections'];
87
88        $out = [];
89        foreach ( $sections as $section ) {
90            if ( $section['toclevel'] === 1 ) {
91                $out[(int)$section['index']] = $section['line'];
92            }
93        }
94
95        return $out;
96    }
97}