Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
EditFilterMergedContentHookConstraint
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
5 / 5
12
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 checkConstraint
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
7
 getLegacyStatus
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHookError
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 formatStatusErrors
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21namespace MediaWiki\EditPage\Constraint;
22
23use ApiMessage;
24use Content;
25use Language;
26use MediaWiki\Context\IContextSource;
27use MediaWiki\HookContainer\HookContainer;
28use MediaWiki\HookContainer\HookRunner;
29use MediaWiki\Html\Html;
30use MediaWiki\Message\Message;
31use MediaWiki\Status\Status;
32use MediaWiki\User\User;
33use StatusValue;
34
35/**
36 * Verify `EditFilterMergedContent` hook
37 *
38 * @since 1.36
39 * @author DannyS712
40 * @internal
41 */
42class EditFilterMergedContentHookConstraint implements IEditConstraint {
43
44    /** @var HookRunner */
45    private $hookRunner;
46
47    /** @var Content */
48    private $content;
49
50    /** @var IContextSource */
51    private $hookContext;
52
53    /** @var string */
54    private $summary;
55
56    /** @var bool */
57    private $minorEdit;
58
59    /** @var Language */
60    private $language;
61
62    /** @var User */
63    private $hookUser;
64
65    /** @var Status */
66    private $status;
67
68    /** @var string */
69    private $hookError = '';
70
71    /**
72     * @param HookContainer $hookContainer
73     * @param Content $content
74     * @param IContextSource $hookContext NOTE: This should only be passed to the hook.
75     * @param string $summary
76     * @param bool $minorEdit
77     * @param Language $language
78     * @param User $hookUser NOTE: This should only be passed to the hook.
79     */
80    public function __construct(
81        HookContainer $hookContainer,
82        Content $content,
83        IContextSource $hookContext,
84        string $summary,
85        bool $minorEdit,
86        Language $language,
87        User $hookUser
88    ) {
89        $this->hookRunner = new HookRunner( $hookContainer );
90        $this->content = $content;
91        $this->hookContext = $hookContext;
92        $this->summary = $summary;
93        $this->minorEdit = $minorEdit;
94        $this->language = $language;
95        $this->hookUser = $hookUser;
96        $this->status = Status::newGood();
97    }
98
99    public function checkConstraint(): string {
100        $hookResult = $this->hookRunner->onEditFilterMergedContent(
101            $this->hookContext,
102            $this->content,
103            $this->status,
104            $this->summary,
105            $this->hookUser,
106            $this->minorEdit
107        );
108        if ( !$hookResult ) {
109            // Error messages etc. could be handled within the hook...
110            if ( $this->status->isGood() ) {
111                $this->status->fatal( 'hookaborted' );
112                // Not setting $this->hookError here is a hack to allow the hook
113                // to cause a return to the edit page without $this->hookError
114                // being set. This is used by ConfirmEdit to display a captcha
115                // without any error message cruft.
116            } else {
117                if ( !$this->status->getErrors() ) {
118                    // Provide a fallback error message if none was set
119                    $this->status->fatal( 'hookaborted' );
120                }
121                $this->hookError = $this->formatStatusErrors( $this->status );
122            }
123            // Use the existing $status->value if the hook set it
124            if ( !$this->status->value ) {
125                // T273354: Should be AS_HOOK_ERROR_EXPECTED to display error message
126                $this->status->value = self::AS_HOOK_ERROR_EXPECTED;
127            }
128            return self::CONSTRAINT_FAILED;
129        }
130
131        if ( !$this->status->isOK() ) {
132            // ...or the hook could be expecting us to produce an error
133            // FIXME this sucks, we should just use the Status object throughout
134            if ( !$this->status->getErrors() ) {
135                // Provide a fallback error message if none was set
136                $this->status->fatal( 'hookaborted' );
137            }
138            $this->hookError = $this->formatStatusErrors( $this->status );
139            $this->status->value = self::AS_HOOK_ERROR_EXPECTED;
140            return self::CONSTRAINT_FAILED;
141        }
142
143        return self::CONSTRAINT_PASSED;
144    }
145
146    public function getLegacyStatus(): StatusValue {
147        // This returns a Status instead of a StatusValue since a Status object is
148        // used in the hook
149        return $this->status;
150    }
151
152    /**
153     * TODO this is really ugly. The constraint shouldn't know that the status
154     * will be used as wikitext, with is what the hookError represents, rather
155     * than just the error code. This needs a big refactor to remove the hook
156     * error string and just rely on the status object entirely.
157     *
158     * @internal
159     * @return string
160     */
161    public function getHookError(): string {
162        return $this->hookError;
163    }
164
165    /**
166     * Wrap status errors in error boxes for increased visibility.
167     * @param Status $status
168     * @return string
169     */
170    private function formatStatusErrors( Status $status ): string {
171        $ret = '';
172        foreach ( $status->getErrors() as $rawError ) {
173            // XXX: This interface is ugly, but it seems to be the only convenient way to convert a message specifier
174            // as used in Status to a Message without all the cruft that Status::getMessage & friends add.
175            $msg = Message::newFromSpecifier( ApiMessage::create( $rawError ) );
176            $ret .= Html::errorBox( "\n" . $msg->inLanguage( $this->language )->plain() . "\n" );
177        }
178        return $ret;
179    }
180
181}