Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.08% covered (warning)
86.08%
68 / 79
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
PreloadedContentBuilder
86.08% covered (warning)
86.08%
68 / 79
80.00% covered (warning)
80.00%
4 / 5
26.69
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getPreloadedContent
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
7
 getDefaultContent
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 getPreloadedContentFromParams
73.17% covered (warning)
73.17%
30 / 41
0.00% covered (danger)
0.00%
0 / 1
16.26
 transform
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace MediaWiki\EditPage;
4
5use MediaWiki\Content\Content;
6use MediaWiki\Content\IContentHandlerFactory;
7use MediaWiki\Content\Transform\ContentTransformer;
8use MediaWiki\HookContainer\HookContainer;
9use MediaWiki\HookContainer\HookRunner;
10use MediaWiki\Page\PageReference;
11use MediaWiki\Page\ProperPageIdentity;
12use MediaWiki\Page\RedirectLookup;
13use MediaWiki\Page\WikiPageFactory;
14use MediaWiki\Parser\ParserOptions;
15use MediaWiki\Permissions\Authority;
16use MediaWiki\Revision\RevisionRecord;
17use MediaWiki\SpecialPage\SpecialPageFactory;
18use MediaWiki\Title\Title;
19use MessageCache;
20use Wikimedia\Assert\Assert;
21
22/**
23 * Provides the initial content of the edit box displayed in an edit form
24 * when creating a new page or a new section.
25 *
26 * Used by EditPage, and may be used by extensions providing alternative editors.
27 *
28 * @since 1.41
29 */
30class PreloadedContentBuilder {
31
32    use ParametersHelper;
33
34    private IContentHandlerFactory $contentHandlerFactory;
35    private WikiPageFactory $wikiPageFactory;
36    private RedirectLookup $redirectLookup;
37    private SpecialPageFactory $specialPageFactory;
38    private ContentTransformer $contentTransformer;
39    private HookRunner $hookRunner;
40
41    public function __construct(
42        IContentHandlerFactory $contentHandlerFactory,
43        WikiPageFactory $wikiPageFactory,
44        RedirectLookup $redirectLookup,
45        SpecialPageFactory $specialPageFactory,
46        ContentTransformer $contentTransformer,
47        HookContainer $hookContainer
48    ) {
49        // Services
50        $this->contentHandlerFactory = $contentHandlerFactory;
51        $this->wikiPageFactory = $wikiPageFactory;
52        $this->redirectLookup = $redirectLookup;
53        $this->specialPageFactory = $specialPageFactory;
54        $this->contentTransformer = $contentTransformer;
55        $this->hookRunner = new HookRunner( $hookContainer );
56    }
57
58    /**
59     * Get the initial content of the edit box displayed in an edit form
60     * when creating a new page or a new section.
61     *
62     * @param ProperPageIdentity $page
63     * @param Authority $performer
64     * @param string|null $preload
65     * @param string[] $preloadParams
66     * @param string|null $section
67     * @return Content
68     */
69    public function getPreloadedContent(
70        ProperPageIdentity $page,
71        Authority $performer,
72        ?string $preload,
73        array $preloadParams,
74        ?string $section
75    ): Content {
76        Assert::parameterElementType( 'string', $preloadParams, '$preloadParams' );
77
78        $content = null;
79        if ( $section !== 'new' ) {
80            $content = $this->getDefaultContent( $page );
81        }
82        if ( $content === null ) {
83            if ( ( $preload === null || $preload === '' ) && $section === 'new' ) {
84                // Custom preload text for new sections
85                $preload = 'MediaWiki:addsection-preload';
86            }
87            $content = $this->getPreloadedContentFromParams( $page, $performer, $preload, $preloadParams );
88        }
89        $title = Title::newFromPageIdentity( $page );
90        if ( !$title->getArticleID() ) {
91            $contentModel = $title->getContentModel();
92            $contentHandler = $this->contentHandlerFactory->getContentHandler( $contentModel );
93            $contentFormat = $contentHandler->getDefaultFormat();
94            $text = $contentHandler->serializeContent( $content, $contentFormat );
95            $this->hookRunner->onEditFormPreloadText( $text, $title );
96            $content = $contentHandler->unserializeContent( $text, $contentFormat );
97        }
98        return $content;
99    }
100
101    /**
102     * Get the content that is displayed when viewing a page that does not exist.
103     * Users should be discouraged from saving the page with identical content to this.
104     *
105     * Some code may depend on the fact that this is only non-null for the 'MediaWiki:' namespace.
106     * Beware.
107     *
108     * @param ProperPageIdentity $page
109     * @return Content|null
110     */
111    public function getDefaultContent( ProperPageIdentity $page ): ?Content {
112        $title = Title::newFromPageIdentity( $page );
113        $contentModel = $title->getContentModel();
114        $contentHandler = $this->contentHandlerFactory->getContentHandler( $contentModel );
115        $contentFormat = $contentHandler->getDefaultFormat();
116        if ( $title->getNamespace() === NS_MEDIAWIKI ) {
117            // If this is a system message, get the default text.
118            $text = $title->getDefaultMessageText();
119            if ( $text !== false ) {
120                return $contentHandler->unserializeContent( $text, $contentFormat );
121            }
122        }
123        return null;
124    }
125
126    /**
127     * Get the contents to be preloaded into the box by loading the given page.
128     *
129     * @param ProperPageIdentity $contextPage
130     * @param Authority $performer
131     * @param string|null $preload Representing the title to preload from.
132     * @param string[] $preloadParams Parameters to use (interface-message style) in the preloaded text
133     * @return Content
134     */
135    private function getPreloadedContentFromParams(
136        ProperPageIdentity $contextPage,
137        Authority $performer,
138        ?string $preload,
139        array $preloadParams
140    ): Content {
141        $contextTitle = Title::newFromPageIdentity( $contextPage );
142        $contentModel = $contextTitle->getContentModel();
143        $handler = $this->contentHandlerFactory->getContentHandler( $contentModel );
144
145        // T297725: Don't trick users into making edits to e.g. .js subpages
146        if ( !$handler->supportsPreloadContent() || $preload === null || $preload === '' ) {
147            return $handler->makeEmptyContent();
148        }
149
150        $title = Title::newFromText( $preload );
151
152        if ( $title && $title->getNamespace() == NS_MEDIAWIKI ) {
153            // When the preload source is in NS_MEDIAWIKI, get the content via wfMessage, to
154            // enable preloading from i18n messages. The message framework can work with normal
155            // pages in NS_MEDIAWIKI, so this does not restrict preloading only to i18n messages.
156            $msg = wfMessage( MessageCache::normalizeKey( $title->getText() ) );
157
158            if ( $msg->isDisabled() ) {
159                // Message is disabled and should not be used for preloading
160                return $handler->makeEmptyContent();
161            }
162
163            return $this->transform(
164                $handler->unserializeContent( $msg
165                    ->page( $title )
166                    ->params( $preloadParams )
167                    ->inContentLanguage()
168                    ->plain()
169                ),
170                $title
171            );
172        }
173
174        // (T299544) Use SpecialMyLanguage redirect so that nonexistent translated pages can
175        // fall back to the corresponding page in a suitable language
176        $title = $this->getTargetTitleIfSpecialMyLanguage( $title );
177
178        # Check for existence to avoid getting MediaWiki:Noarticletext
179        if ( !$this->isPageExistingAndViewable( $title, $performer ) ) {
180            // TODO: somehow show a warning to the user!
181            return $handler->makeEmptyContent();
182        }
183
184        $page = $this->wikiPageFactory->newFromTitle( $title );
185        if ( $page->isRedirect() ) {
186            $redirTarget = $this->redirectLookup->getRedirectTarget( $title );
187            $redirTarget = Title::castFromLinkTarget( $redirTarget );
188            # Same as before
189            if ( !$this->isPageExistingAndViewable( $redirTarget, $performer ) ) {
190                // TODO: somehow show a warning to the user!
191                return $handler->makeEmptyContent();
192            }
193            $page = $this->wikiPageFactory->newFromTitle( $redirTarget );
194        }
195
196        $content = $page->getContent( RevisionRecord::RAW );
197
198        if ( !$content ) {
199            // TODO: somehow show a warning to the user!
200            return $handler->makeEmptyContent();
201        }
202
203        if ( $content->getModel() !== $handler->getModelID() ) {
204            $converted = $content->convert( $handler->getModelID() );
205
206            if ( !$converted ) {
207                // TODO: somehow show a warning to the user!
208                wfDebug( "Attempt to preload incompatible content: " .
209                    "can't convert " . $content->getModel() .
210                    " to " . $handler->getModelID() );
211
212                return $handler->makeEmptyContent();
213            }
214
215            $content = $converted;
216        }
217        return $this->transform( $content, $title, $preloadParams );
218    }
219
220    private function transform(
221        Content $content,
222        PageReference $title,
223        array $preloadParams = []
224    ) {
225        return $this->contentTransformer->preloadTransform(
226            $content,
227            $title,
228            // The preload transformations don't depend on the user anyway
229            ParserOptions::newFromAnon(),
230            $preloadParams
231        );
232    }
233}