Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
80.58% covered (warning)
80.58%
83 / 103
75.00% covered (warning)
75.00%
6 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
StructuredMentorWriter
80.58% covered (warning)
80.58%
83 / 103
75.00% covered (warning)
75.00%
6 / 8
22.93
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 serializeMentor
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 saveMentorData
62.50% covered (warning)
62.50%
10 / 16
0.00% covered (danger)
0.00%
0 / 1
4.84
 isBlocked
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 addMentor
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
4
 removeMentor
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
2
 changeMentor
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
2
 touchList
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace GrowthExperiments\Mentorship\Provider;
4
5use GrowthExperiments\Config\Validation\StructuredMentorListValidator;
6use GrowthExperiments\Config\WikiPageConfigLoader;
7use GrowthExperiments\Config\WikiPageConfigWriterFactory;
8use GrowthExperiments\Mentorship\Mentor;
9use GrowthExperiments\Mentorship\MentorshipSummaryCreator;
10use IDBAccessObject;
11use MediaWiki\Title\Title;
12use MediaWiki\User\UserFactory;
13use MediaWiki\User\UserIdentity;
14use MediaWiki\User\UserIdentityLookup;
15use StatusValue;
16
17/**
18 * This class writes to the structured mentor list and allows to add/remove
19 * mentors from the structured mentor list.
20 *
21 * Use StructuredMentorProvider to read the mentor list.
22 *
23 * This class uses WikiPageConfigWriter under the hood.
24 */
25class StructuredMentorWriter implements IMentorWriter {
26    use GetMentorDataTrait;
27
28    /** @var string Change tag to tag structured mentor list edits with */
29    public const CHANGE_TAG = 'mentor list change';
30
31    /** @var string */
32    public const CONFIG_KEY = 'Mentors';
33
34    private MentorProvider $mentorProvider;
35    private UserIdentityLookup $userIdentityLookup;
36    private UserFactory $userFactory;
37    private WikiPageConfigWriterFactory $configWriterFactory;
38    private StructuredMentorListValidator $mentorListValidator;
39
40    /**
41     * @param MentorProvider $mentorProvider
42     * @param UserIdentityLookup $userIdentityLookup
43     * @param UserFactory $userFactory
44     * @param WikiPageConfigLoader $configLoader
45     * @param WikiPageConfigWriterFactory $configWriterFactory
46     * @param StructuredMentorListValidator $mentorListValidator
47     * @param Title $mentorList
48     */
49    public function __construct(
50        MentorProvider $mentorProvider,
51        UserIdentityLookup $userIdentityLookup,
52        UserFactory $userFactory,
53        WikiPageConfigLoader $configLoader,
54        WikiPageConfigWriterFactory $configWriterFactory,
55        StructuredMentorListValidator $mentorListValidator,
56        Title $mentorList
57    ) {
58        $this->mentorProvider = $mentorProvider;
59        $this->userIdentityLookup = $userIdentityLookup;
60        $this->userFactory = $userFactory;
61        $this->configLoader = $configLoader;
62        $this->configWriterFactory = $configWriterFactory;
63        $this->mentorListValidator = $mentorListValidator;
64        $this->mentorList = $mentorList;
65    }
66
67    /**
68     * Serialize a Mentor object to an array
69     *
70     * @param Mentor $mentor
71     * @return array
72     */
73    public static function serializeMentor( Mentor $mentor ): array {
74        return [
75            'message' => $mentor->hasCustomIntroText() ? $mentor->getIntroText() : null,
76            'weight' => $mentor->getWeight(),
77        ];
78    }
79
80    /**
81     * Wrapper around WikiPageConfigWriter to save all mentor data
82     *
83     * @param array $mentorData
84     * @param string $summary
85     * @param UserIdentity $performer
86     * @param bool $bypassWarnings Should warnings raised by the validator stop the operation?
87     * @return StatusValue
88     */
89    private function saveMentorData(
90        array $mentorData,
91        string $summary,
92        UserIdentity $performer,
93        bool $bypassWarnings
94    ): StatusValue {
95        // check if $performer is not blocked from the mentor list page
96        if ( $this->isBlocked( $performer, IDBAccessObject::READ_LATEST ) ) {
97            return StatusValue::newFatal( 'growthexperiments-mentor-writer-error-blocked' );
98        }
99
100        // add 'username' key for readability (T331444)
101        foreach ( $mentorData as $mentorId => $_ ) {
102            $mentorUser = $this->userIdentityLookup->getUserIdentityByUserId( $mentorId );
103            if ( !$mentorUser ) {
104                $this->logger->warning( $this->mentorList->getText() . ' contains an invalid user for ID {userId}', [
105                    'userId' => $mentorId,
106                    'namespace' => $this->mentorList->getNamespace(),
107                    'title' => $this->mentorList->getText()
108                ] );
109                continue;
110            }
111
112            $mentorData[$mentorId]['username'] = $mentorUser->getName();
113        }
114
115        $configWriter = $this->configWriterFactory
116            ->newWikiPageConfigWriter( $this->mentorList, $performer );
117        $configWriter->setVariable( self::CONFIG_KEY, $mentorData );
118        return $configWriter->save( $summary, false, self::CHANGE_TAG, $bypassWarnings );
119    }
120
121    /**
122     * @inheritDoc
123     */
124    public function isBlocked(
125        UserIdentity $performer,
126        int $freshness = IDBAccessObject::READ_NORMAL
127    ): bool {
128        $block = $this->userFactory->newFromUserIdentity( $performer )->getBlock( $freshness );
129        return $block && $block->appliesToTitle( $this->mentorList );
130    }
131
132    /**
133     * @inheritDoc
134     */
135    public function addMentor(
136        Mentor $mentor,
137        UserIdentity $performer,
138        string $summary,
139        bool $bypassWarnings = false
140    ): StatusValue {
141        $mentorUserIdentity = $mentor->getUserIdentity();
142        if ( !$mentorUserIdentity->isRegistered()
143            || !$this->userFactory->newFromUserIdentity( $mentorUserIdentity )->isNamed()
144        ) {
145            return StatusValue::newFatal(
146                'growthexperiments-mentor-writer-error-anonymous-user',
147                $mentorUserIdentity->getName()
148            );
149        }
150
151        $mentorData = $this->getMentorData();
152        if ( array_key_exists( $mentorUserIdentity->getId(), $mentorData ) ) {
153            // we're trying to add someone who's already added
154            return StatusValue::newFatal(
155                'growthexperiments-mentor-writer-error-already-added',
156                $mentorUserIdentity->getName()
157            );
158        }
159        $mentorData[$mentorUserIdentity->getId()] = $this->serializeMentor( $mentor );
160
161        return $this->saveMentorData(
162            $mentorData,
163            MentorshipSummaryCreator::createAddSummary(
164                $performer,
165                $mentorUserIdentity,
166                $summary
167            ),
168            $performer,
169            $bypassWarnings
170        );
171    }
172
173    /**
174     * @inheritDoc
175     */
176    public function removeMentor(
177        Mentor $mentor,
178        UserIdentity $performer,
179        string $summary,
180        bool $bypassWarnings = false
181    ): StatusValue {
182        $mentorUserIdentity = $mentor->getUserIdentity();
183
184        $mentorData = $this->getMentorData();
185        if ( !array_key_exists( $mentorUserIdentity->getId(), $mentorData ) ) {
186            // we're trying to remove someone who isn't added
187            return StatusValue::newFatal(
188                'growthexperiments-mentor-writer-error-not-in-the-list',
189                $mentorUserIdentity->getName()
190            );
191        }
192        unset( $mentorData[$mentorUserIdentity->getId()] );
193
194        return $this->saveMentorData(
195            $mentorData,
196            MentorshipSummaryCreator::createRemoveSummary(
197                $performer,
198                $mentorUserIdentity,
199                $summary
200            ),
201            $performer,
202            $bypassWarnings
203        );
204    }
205
206    /**
207     * @inheritDoc
208     */
209    public function changeMentor(
210        Mentor $mentor,
211        UserIdentity $performer,
212        string $summary,
213        bool $bypassWarnings = false
214    ): StatusValue {
215        $mentorUserIdentity = $mentor->getUserIdentity();
216
217        $mentorData = $this->getMentorData();
218        if ( !array_key_exists( $mentorUserIdentity->getId(), $mentorData ) ) {
219            // we're trying to change someone who isn't added
220            return StatusValue::newFatal(
221                'growthexperiments-mentor-writer-error-not-in-the-list',
222                $mentorUserIdentity->getName()
223            );
224        }
225        $mentorData[$mentorUserIdentity->getId()] = $this->serializeMentor( $mentor );
226
227        return $this->saveMentorData(
228            $mentorData,
229            MentorshipSummaryCreator::createChangeSummary(
230                $performer,
231                $mentorUserIdentity,
232                $summary
233            ),
234            $performer,
235            $bypassWarnings
236        );
237    }
238
239    /**
240     * @inheritDoc
241     */
242    public function touchList( UserIdentity $performer, string $summary ): StatusValue {
243        $mentorData = $this->getMentorData();
244        foreach ( $mentorData as $mentorId => $mentorArr ) {
245            $mentorUser = $this->userIdentityLookup->getUserIdentityByUserId( $mentorId );
246            if ( !$mentorUser ) {
247                continue;
248            }
249            $mentorData[$mentorUser->getId()] = $this->serializeMentor(
250                $this->mentorProvider->newMentorFromUserIdentity( $mentorUser )
251            );
252        }
253        return $this->saveMentorData(
254            $mentorData,
255            $summary,
256            $performer,
257            true
258        );
259    }
260}