Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
53.75% covered (warning)
53.75%
43 / 80
25.00% covered (danger)
25.00%
1 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
PopulateSpeechoidLexiconFromWiki
56.58% covered (warning)
56.58%
43 / 76
25.00% covered (danger)
25.00%
1 / 4
48.55
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 execute
82.22% covered (warning)
82.22%
37 / 45
0.00% covered (danger)
0.00%
0 / 1
13.95
 getUser
20.00% covered (danger)
20.00%
2 / 10
0.00% covered (danger)
0.00%
0 / 1
7.61
 getAllLexiconTitlesForLanguage
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace MediaWiki\Wikispeech;
4
5/**
6 * @file
7 * @ingroup Extensions
8 * @license GPL-2.0-or-later
9 */
10
11use Maintenance;
12use MediaWiki\MediaWikiServices;
13use MediaWiki\Title\Title;
14use MediaWiki\User\User;
15use MediaWiki\Wikispeech\Lexicon\LexiconSpeechoidStorage;
16use MediaWiki\Wikispeech\Lexicon\LexiconWikiStorage;
17
18/** @var string MediaWiki installation path */
19$IP = getenv( 'MW_INSTALL_PATH' ) ?: __DIR__ . '/../../..';
20require_once "$IP/maintenance/Maintenance.php";
21
22/**
23 * Maintenance script to populate speechoid lexicon
24 * with entries from wiki.
25 *
26 * Be aware that you probably need to execute using mwscript, not php,
27 * in order to be executed as user www-data, who has access to deleting files.
28 *
29 * @since 0.1.13
30 */
31class PopulateSpeechoidLexiconFromWiki extends Maintenance {
32
33    /** @var LexiconWikiStorage */
34    public $lexiconWikiStorage;
35
36    /** @var LexiconSpeechoidStorage */
37    public $speechoidStorage;
38
39    /** @var User */
40    public $user;
41
42    /** @var callable|null Used to override titles in tests */
43    public $getAllLexiconTitlesForLanguageCallback = null;
44
45    public function __construct() {
46        parent::__construct();
47        $this->requireExtension( 'Wikispeech' );
48        $this->addDescription( 'Populate Speechoid storage with entries from local wiki.' );
49        $this->addOption( 'user', 'Username to perform edits as', true, true );
50    }
51
52    /**
53     * @return void
54     */
55    public function execute(): void {
56        $voices = $this->getConfig()->get( 'WikispeechVoices' );
57        $languages = array_keys( $voices );
58        sort( $languages );
59
60        $wikiStorage = $this->lexiconWikiStorage ?? WikispeechServices::getLexiconWikiStorage();
61        $speechoidStorage = $this->speechoidStorage ?? WikispeechServices::getLexiconSpeechoidStorage();
62
63        $wikiStorage->setUser( $this->getUser() );
64
65        foreach ( $languages as $language ) {
66            $this->output( "Language, $language" );
67            $anyUpdated = false;
68
69            $titles = $this->getAllLexiconTitlesForLanguageCallback
70            ? call_user_func( $this->getAllLexiconTitlesForLanguageCallback, $language )
71            : $this->getAllLexiconTitlesForLanguage( $language );
72
73            foreach ( $titles as $title ) {
74                // TODO: This breaks if the lexicon key contains a slash (e.g. "F/V").
75                // Consider a more robust way of extracting the key from the title.
76                $key = explode( '/', $title->getText(), 2 )[1] ?? null;
77                if ( !$key ) {
78                    continue;
79                }
80
81                $entry = $wikiStorage->getEntry( $language, $key );
82                if ( !$entry ) {
83                    continue;
84                }
85
86                $updated = false;
87                $validItems = [];
88
89                foreach ( $entry->getItems() as $item ) {
90                    $identity = $item->getSpeechoidIdentity();
91
92                    if ( $identity === null ) {
93                        $this->output( "Warning: $language/$key has no Speechoid ID — skipping.\n" );
94                        continue;
95                    }
96
97                    $speechoidEntry = $speechoidStorage->getEntry( $language, $key );
98                    if ( !$speechoidEntry || !$speechoidEntry->findItemBySpeechoidIdentity( $identity ) ) {
99                        $this->output( "Re-creating missing Speechoid entry for $language/$key (ID: $identity)\n" );
100
101                        $item->setSpeechoidIdentity( null );
102
103                        try {
104                            $speechoidStorage->createEntryItem( $language, $key, $item );
105                            $wikiStorage->replaceEntryItem( $language, $key, $item );
106                            $this->output( "Re-created Speechoid entry for $language/$key\n" );
107                            $updated = true;
108                            $anyUpdated = true;
109                        } catch ( \LogicException $e ) {
110                            $this->output( "Failed to re-create entry for $key" . $e->getMessage() . "\n" );
111                        }
112                    }
113
114                    $validItems[] = $item;
115                }
116
117                if ( $updated ) {
118                    $newEntry = clone $entry;
119                    $newEntry->setItems( $validItems );
120                    $wikiStorage->saveLexiconEntryRevision( $language, $key, $newEntry, 'Repaired Speechoid identity' );
121                    $this->output( "Saved updated wiki entry for $language/$key\n" );
122                }
123            }
124
125            if ( !$anyUpdated ) {
126                $this->output( "No posts to add\n" );
127            }
128        }
129    }
130
131    /**
132     * Helper function to get system user to be able to
133     * save lexicon entry revision
134     *
135     * @return User
136     */
137    protected function getUser(): User {
138        if ( $this->user !== null ) {
139                return $this->user;
140        }
141        $services = MediaWikiServices::getInstance();
142        $username = $this->getOption( 'user' );
143        $userIdentity = $services->getUserIdentityLookup()->getUserIdentityByName( $username );
144        if ( !$userIdentity ) {
145            $this->fatalError(
146                "User '$username' not found or invalid. Please provide an existing username with --user."
147            );
148        }
149        // @phan-suppress-next-line PhanTypeMismatchArgumentNullable
150        return $services->getUserFactory()->newFromUserIdentity( $userIdentity );
151    }
152
153    /**
154     * Helper function to get all lexicon titles for a specific language
155     *
156     * @param string $language
157     * @return Title[]
158     */
159    protected function getAllLexiconTitlesForLanguage( string $language ): array {
160        $titleFactory = MediaWikiServices::getInstance()->getTitleFactory();
161        $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
162
163        $prefix = ucfirst( $language ) . '/';
164        $like = "$prefix%";
165
166        $res = $dbr->select(
167            'page',
168            [ 'page_title' ],
169            [
170                'page_namespace' => NS_PRONUNCIATION_LEXICON,
171                "page_title LIKE '$like'"
172            ],
173            __METHOD__
174        );
175
176        $titles = [];
177        foreach ( $res as $row ) {
178            $titles[] = $titleFactory->makeTitle( NS_PRONUNCIATION_LEXICON, $row->page_title );
179        }
180        return $titles;
181    }
182}
183
184$maintClass = PopulateSpeechoidLexiconFromWiki::class;
185require_once RUN_MAINTENANCE_IF_MAIN;