Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.88% covered (success)
93.88%
46 / 49
33.33% covered (danger)
33.33%
1 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
Writer
93.88% covered (success)
93.88%
46 / 49
33.33% covered (danger)
33.33%
1 / 3
11.03
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 save
96.55% covered (success)
96.55%
28 / 29
0.00% covered (danger)
0.00%
0 / 1
7
 runEditFilterMergedContentHook
88.24% covered (warning)
88.24%
15 / 17
0.00% covered (danger)
0.00%
0 / 1
3.01
1<?php
2
3namespace MediaWiki\Extension\CommunityConfiguration\Store\WikiPage;
4
5use Content;
6use MediaWiki\CommentStore\CommentStoreComment;
7use MediaWiki\Content\JsonContent;
8use MediaWiki\Context\DerivativeContext;
9use MediaWiki\Context\RequestContext;
10use MediaWiki\HookContainer\HookContainer;
11use MediaWiki\HookContainer\HookRunner;
12use MediaWiki\Json\FormatJson;
13use MediaWiki\Page\PageIdentity;
14use MediaWiki\Page\WikiPageFactory;
15use MediaWiki\Permissions\Authority;
16use MediaWiki\Permissions\UltimateAuthority;
17use MediaWiki\Revision\SlotRecord;
18use MediaWiki\Status\Status;
19use MediaWiki\Title\Title;
20use MediaWiki\User\UserFactory;
21use RecentChange;
22
23class Writer {
24
25    private WikiPageFactory $wikiPageFactory;
26    private UserFactory $userFactory;
27    private HookContainer $hookContainer;
28
29    /**
30     * @param WikiPageFactory $wikiPageFactory
31     * @param UserFactory $userFactory
32     * @param HookContainer $hookContainer
33     */
34    public function __construct(
35        WikiPageFactory $wikiPageFactory,
36        UserFactory $userFactory,
37        HookContainer $hookContainer
38    ) {
39        $this->wikiPageFactory = $wikiPageFactory;
40        $this->userFactory = $userFactory;
41        $this->hookContainer = $hookContainer;
42    }
43
44    /**
45     * Save a new version to the configuration page
46     *
47     * No permission changes or validation is performed.
48     *
49     * @param PageIdentity $configPage
50     * @param mixed $newConfig
51     * @param Authority $performer
52     * @param string $summary
53     * @param bool $minor
54     * @param array|string $tags Tag(s) to apply (defaults to none)
55     * @return Status
56     */
57    public function save(
58        PageIdentity $configPage,
59        $newConfig,
60        Authority $performer,
61        string $summary = '',
62        bool $minor = false,
63        $tags = []
64    ): Status {
65        // REVIEW: Should this validate $configPage is an acceptable target?
66
67        // Sort config alphabetically
68        $configSorted = (array)$newConfig;
69        ksort( $configSorted );
70        $status = Status::newGood();
71        $content = new JsonContent( FormatJson::encode( (object)$configSorted ) );
72
73        $page = $this->wikiPageFactory->newFromTitle( $configPage );
74
75        // Give AbuseFilter et al. a chance to block the edit (T346235)
76        // Do not run when UltimateAuthority is used (from e.g. maintenance scripts), as in those
77        // cases, we want the edit to succeed regardless of permissions.
78        if ( !$performer instanceof UltimateAuthority ) {
79            $status->merge( $this->runEditFilterMergedContentHook(
80                $performer,
81                $page->getTitle(),
82                $content,
83                $summary,
84                $minor
85            ) );
86        }
87
88        if ( !$status->isOK() ) {
89            return $status;
90        }
91
92        $updater = $page->newPageUpdater( $performer );
93        if ( is_string( $tags ) ) {
94            $updater->addTag( $tags );
95        } elseif ( is_array( $tags ) ) {
96            $updater->addTags( $tags );
97        }
98        $updater->setContent( SlotRecord::MAIN, $content );
99
100        if ( $performer->isAllowed( 'autopatrol' ) ) {
101            $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
102        }
103
104        $updater->saveRevision(
105            CommentStoreComment::newUnsavedComment( $summary ),
106            $minor ? EDIT_MINOR : 0
107        );
108        $status->merge( $updater->getStatus() );
109
110        return $status;
111    }
112
113    /**
114     * Run the EditFilterMergedContentHook
115     *
116     * @param Authority $performer
117     * @param Title $title
118     * @param Content $content
119     * @param string $summary
120     * @param bool $minor
121     * @return Status
122     */
123    private function runEditFilterMergedContentHook(
124        Authority $performer,
125        Title $title,
126        Content $content,
127        string $summary,
128        bool $minor
129    ): Status {
130        $performerUser = $this->userFactory->newFromAuthority( $performer );
131
132        // Ensure context has right values for title and performer, which are available to the
133        // config writer. Use the global context for the rest.
134        $derivativeContext = new DerivativeContext( RequestContext::getMain() );
135        $derivativeContext->setUser( $performerUser );
136        $derivativeContext->setTitle( $title );
137
138        $status = new Status();
139        $hookRunner = new HookRunner( $this->hookContainer );
140        if ( !$hookRunner->onEditFilterMergedContent(
141            $derivativeContext,
142            $content,
143            $status,
144            $summary,
145            $performerUser,
146            $minor
147        ) ) {
148            if ( $status->isGood() ) {
149                $status->fatal( 'hookaborted' );
150            }
151        }
152        return $status;
153    }
154}