Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 79
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
0.00% covered (danger)
0.00%
0 / 79
0.00% covered (danger)
0.00%
0 / 6
342
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getInstance
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 onEditFilterMergedContent
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 onPageSaveComplete
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
72
 onCodeEditorGetPageLanguage
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 onArticleViewHeader
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageBundleTranslation;
5
6use Article;
7use JobQueueGroup;
8use MediaWiki\Content\Content;
9use MediaWiki\Context\IContextSource;
10use MediaWiki\Extension\Translate\LogNames;
11use MediaWiki\Extension\Translate\MessageLoading\MessageHandle;
12use MediaWiki\Hook\EditFilterMergedContentHook;
13use MediaWiki\Html\Html;
14use MediaWiki\Linker\LinkRenderer;
15use MediaWiki\Logger\LoggerFactory;
16use MediaWiki\MediaWikiServices;
17use MediaWiki\Page\Hook\ArticleViewHeaderHook;
18use MediaWiki\Parser\ParserOutput;
19use MediaWiki\Revision\SlotRecord;
20use MediaWiki\SpecialPage\SpecialPage;
21use MediaWiki\Status\Status;
22use MediaWiki\Storage\Hook\PageSaveCompleteHook;
23use MediaWiki\Title\Title;
24use MediaWiki\Title\TitleFactory;
25use MediaWiki\User\User;
26use Psr\Log\LoggerInterface;
27
28/**
29 * @author Niklas Laxström
30 * @license GPL-2.0-or-later
31 * @since 2021.05
32 */
33class Hooks implements ArticleViewHeaderHook, EditFilterMergedContentHook, PageSaveCompleteHook {
34    public const CONSTRUCTOR_OPTIONS = [
35        'TranslateEnableMessageBundleIntegration',
36    ];
37
38    private static self $instance;
39    private LoggerInterface $logger;
40    private MessageBundleStore $messageBundleStore;
41    private LinkRenderer $linkRenderer;
42    private bool $enableIntegration;
43    private TitleFactory $titleFactory;
44    private JobQueueGroup $jobQueueGroup;
45
46    public function __construct(
47        LoggerInterface $logger,
48        MessageBundleStore $messageBundleStore,
49        LinkRenderer $linkRenderer,
50        TitleFactory $titleFactory,
51        JobQueueGroup $jobQueueGroup,
52        bool $enableIntegration
53    ) {
54        $this->logger = $logger;
55        $this->messageBundleStore = $messageBundleStore;
56        $this->linkRenderer = $linkRenderer;
57        $this->titleFactory = $titleFactory;
58        $this->jobQueueGroup = $jobQueueGroup;
59        $this->enableIntegration = $enableIntegration;
60    }
61
62    public static function getInstance(): self {
63        $services = MediaWikiServices::getInstance();
64        self::$instance ??= new self(
65            LoggerFactory::getInstance( LogNames::MESSAGE_BUNDLE ),
66            $services->get( 'Translate:MessageBundleStore' ),
67            $services->getLinkRenderer(),
68            $services->getTitleFactory(),
69            $services->getJobQueueGroup(),
70            $services->getMainConfig()->get( 'TranslateEnableMessageBundleIntegration' )
71        );
72        return self::$instance;
73    }
74
75    /** @inheritDoc */
76    public function onEditFilterMergedContent(
77        IContextSource $context,
78        Content $content,
79        Status $status,
80        $summary,
81        User $user,
82        $minoredit
83    ): void {
84        if ( $content instanceof MessageBundleContent ) {
85            try {
86                // Validation is performed in the store because injecting services into the
87                // Content class is not straightforward
88                $this->messageBundleStore->validate( $context->getTitle(), $content );
89            } catch ( MalformedBundle $e ) {
90                // MalformedBundle implements MessageSpecifier, but for unknown reason it gets
91                // cast to a string if we don't convert it to a proper message.
92                $status->fatal( 'translate-messagebundle-validation-error', $context->msg( $e ) );
93            }
94        }
95    }
96
97    /** @inheritDoc */
98    public function onPageSaveComplete(
99        $wikiPage,
100        $user,
101        $summary,
102        $flags,
103        $revisionRecord,
104        $editResult
105    ): void {
106        if ( !$this->enableIntegration ) {
107            return;
108        }
109
110        $pageTitle = $wikiPage->getTitle();
111        $handle = new MessageHandle( $pageTitle );
112
113        // Check if it's a message bundle unit translation
114        if ( $handle->isValid() && $handle->isPageTranslation() ) {
115            $group = $handle->getGroup();
116            if ( $group instanceof MessageBundleMessageGroup ) {
117                $messageBundleTitle = $this->titleFactory->newFromID( $group->getBundlePageId() );
118                $this->jobQueueGroup->push( PurgeMessageBundleDependenciesJob::newJob( $messageBundleTitle ) );
119            }
120
121            return;
122        }
123
124        $method = __METHOD__;
125        $content = $revisionRecord->getContent( SlotRecord::MAIN );
126        $pageTitle = $wikiPage->getTitle();
127
128        if ( $content === null ) {
129            $this->logger->debug( "Unable to access content of page {pageName} in $method", [
130                'pageName' => $pageTitle->getPrefixedText()
131            ] );
132            return;
133        }
134
135        if ( !$content instanceof MessageBundleContent ) {
136            return;
137        }
138
139        try {
140            $this->messageBundleStore->save( $pageTitle, $revisionRecord, $content );
141        } catch ( MalformedBundle $e ) {
142            // This should not happen, as it should not be possible to save a page with invalid content
143            $this->logger->warning( "Page {pageName} is not a valid message bundle in $method", [
144                'pageName' => $pageTitle->getPrefixedText(),
145                'exception' => $e,
146            ] );
147            return;
148        }
149
150        $this->jobQueueGroup->push( PurgeMessageBundleDependenciesJob::newJob( $pageTitle ) );
151    }
152
153    /** Hook: CodeEditorGetPageLanguage */
154    public static function onCodeEditorGetPageLanguage( Title $title, ?string &$lang, string $model ) {
155        if ( $model === MessageBundleContent::CONTENT_MODEL_ID ) {
156            $lang = 'json';
157        }
158    }
159
160    /**
161     * Hook: ArticleViewHeader
162     *
163     * @param Article $article
164     * @param bool|ParserOutput|null &$outputDone
165     * @param bool &$pcache
166     */
167    public function onArticleViewHeader( $article, &$outputDone, &$pcache ) {
168        if ( !$this->enableIntegration ) {
169            return;
170        }
171
172        $articleTitle = $article->getTitle();
173        if ( MessageBundle::isSourcePage( $articleTitle ) ) {
174            $messageBundle = new MessageBundle( $articleTitle );
175            $context = $article->getContext();
176            $language = $context->getLanguage();
177
178            $translateLink = $this->linkRenderer->makeKnownLink(
179                SpecialPage::getTitleFor( 'Translate' ),
180                $context->msg( 'translate-tag-translate-mb-link-desc' )->text(),
181                [],
182                [
183                    'group' => $messageBundle->getMessageGroupId(),
184                    'action' => 'page',
185                    'filter' => '',
186                ]
187            );
188            $header = Html::rawElement(
189                'div',
190                [
191                    'class' => 'mw-mb-translate-header noprint nomobile',
192                    'dir' => $language->getDir(),
193                    'lang' => $language->getHtmlCode(),
194                ],
195                $translateLink
196            );
197
198            $output = $context->getOutput();
199            $output->addHTML( $header );
200            $output->addModuleStyles( 'ext.translate' );
201        }
202    }
203}