MediaWiki master
ExperienceCondition.php
Go to the documentation of this file.
1<?php
2
4
5use InvalidArgumentException;
6use LogicException;
11use stdClass;
14use Wikimedia\Timestamp\ConvertibleTimestamp;
15
22 public const CONSTRUCTOR_OPTIONS = [
27 ];
28
29 private const LEVELS = [ 'unregistered', 'registered', 'newcomer', 'learner', 'experienced' ];
30
31 private NamedConditionHelper $namedConditionHelper;
32
33 public function __construct(
34 private ServiceOptions $config,
35 private TempUserConfig $tempUserConfig,
36 private UserFactory $userFactory
37 ) {
38 $this->namedConditionHelper = new NamedConditionHelper( $this->tempUserConfig );
39 }
40
42 public function validateValue( $value ) {
43 if ( !in_array( $value, self::LEVELS ) ) {
44 throw new InvalidArgumentException( "must be one of : " .
45 implode( ', ', self::LEVELS )
46 );
47 }
48 return $value;
49 }
50
55 public function exclude( $value ): void {
56 throw new LogicException( 'unimplemented' );
57 }
58
60 public function evaluate( stdClass $row, $value ): bool {
61 if ( $value === 'registered' || $value === 'unregistered' ) {
62 $rowValue = $this->namedConditionHelper->isNamed( $row )
63 ? 'registered' : 'unregistered';
64 return $value === $rowValue;
65 } else {
66 return $this->getExperienceFromRow( $row ) === $value;
67 }
68 }
69
70 private function getExperienceFromRow( stdClass $row ): string {
71 // This should match User::getExperienceLevel(), except that we treat
72 // temporary users as unregistered
73 if ( !$this->namedConditionHelper->isNamed( $row ) ) {
74 return 'unregistered';
75 } else {
76 // TODO: this depends on a UserArray batch query done in
77 // ChangesListSpecialPage to efficient. The batch query should be
78 // owned by this module, or we should do a join. But a join is
79 // inefficient due to T403798.
80 $user = $this->userFactory->newFromAnyId( $row->rc_user, $row->rc_user_text );
81 return $user->getExperienceLevel();
82 }
83 }
84
86 protected function prepareCapture( IReadableDatabase $dbr, QueryBackend $query ) {
87 $query->rcUserFields();
88 }
89
91 public function prepareConds( IReadableDatabase $dbr, QueryBackend $query ) {
92 $selected = array_fill_keys( $this->required, true );
93
94 $isUnregistered = $this->namedConditionHelper->getExpression( $dbr, false );
95 $isRegistered = $this->namedConditionHelper->getExpression( $dbr, true );
96 $aboveNewcomer = $this->getExperienceExpr( 'learner', $dbr );
97 $notAboveNewcomer = $this->getExperienceExpr( 'learner', $dbr, true );
98 $aboveLearner = $this->getExperienceExpr( 'experienced', $dbr );
99 $notAboveLearner = $this->getExperienceExpr( 'experienced', $dbr, true );
100
101 // We need to select some range of user experience levels, from the following table:
102 // | Unregistered | --------- Registered --------- |
103 // | | Newcomers | Learners | Experienced |
104 // |<------------>|<----------->|<---------->|<----------->|
105 // We just need to define a condition for each of the columns, figure out which are selected,
106 // and then OR them together.
107 $columnConds = [
108 'unregistered' => $isUnregistered,
109 'registered' => $isRegistered,
110 'newcomer' => $dbr->andExpr( [ $isRegistered, $notAboveNewcomer ] ),
111 'learner' => $dbr->andExpr( [ $isRegistered, $aboveNewcomer, $notAboveLearner ] ),
112 'experienced' => $dbr->andExpr( [ $isRegistered, $aboveLearner ] ),
113 ];
114
115 // There are some cases where we can easily optimize away some queries:
116 // | Unregistered | --------- Registered --------- |
117 // | | Newcomers | Learners | Experienced |
118 // | |<-------------------------------------->| (1)
119 // |<----------------------------------------------------->| (2)
120
121 // (1) Selecting all of "Newcomers", "Learners" and "Experienced users" is the same as "Registered".
122 if (
123 isset( $selected['registered'] ) ||
124 ( isset( $selected['newcomer'] ) && isset( $selected['learner'] ) && isset( $selected['experienced'] ) )
125 ) {
126 unset( $selected['newcomer'], $selected['learner'], $selected['experienced'] );
127 $selected['registered'] = true;
128 }
129 // (2) Selecting "Unregistered" and "Registered" covers all users.
130 if ( isset( $selected['registered'] ) && isset( $selected['unregistered'] ) ) {
131 unset( $selected['registered'], $selected['unregistered'] );
132 }
133
134 // Combine the conditions for the selected columns.
135 if ( !$selected ) {
136 return;
137 }
138 $selectedColumnConds = array_values( array_intersect_key( $columnConds, $selected ) );
139 $query->where( $dbr->orExpr( $selectedColumnConds ) );
140
141 // Add necessary tables to the queries.
142 $query->joinForConds( 'actor' )->straight();
143 if ( isset( $selected['newcomer'] ) || isset( $selected['learner'] ) || isset( $selected['experienced'] ) ) {
144 $query->joinForConds( 'user' )->weakLeft();
145 }
146 }
147
154 private function getExperienceExpr( $level, IReadableDatabase $dbr, $asNotCondition = false ): IExpression {
155 $configSince = $this->getRegistrationThreshold( $level );
156 $now = ConvertibleTimestamp::time();
157 $secondsPerDay = 86400;
158 $timeCutoff = $now - $configSince * $secondsPerDay;
159
160 $editCutoff = $this->getEditThreshold( $level );
161
162 if ( $asNotCondition ) {
163 return $dbr->expr( 'user_editcount', '<', $editCutoff )
164 ->or( 'user_registration', '>', $dbr->timestamp( $timeCutoff ) );
165 }
166 return $dbr->expr( 'user_editcount', '>=', $editCutoff )->andExpr(
167 // Users who don't have user_registration set are very old, so we assume they're above any cutoff
168 $dbr->expr( 'user_registration', '=', null )
169 ->or( 'user_registration', '<=', $dbr->timestamp( $timeCutoff ) )
170 );
171 }
172
177 private function getRegistrationThreshold( $level ) {
178 return match ( $level ) {
179 'learner' => $this->config->get( MainConfigNames::LearnerMemberSince ),
180 'experienced' => $this->config->get( MainConfigNames::ExperiencedUserMemberSince ),
181 };
182 }
183
188 private function getEditThreshold( $level ) {
189 return match ( $level ) {
190 'learner' => $this->config->get( MainConfigNames::LearnerEdits ),
191 'experienced' => $this->config->get( MainConfigNames::ExperiencedUserEdits ),
192 };
193 }
194}
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:69
A class for passing options to services.
A class containing constants representing the names of configuration variables.
const ExperiencedUserEdits
Name constant for the ExperiencedUserEdits setting, for use with Config::get()
const LearnerEdits
Name constant for the LearnerEdits setting, for use with Config::get()
const LearnerMemberSince
Name constant for the LearnerMemberSince setting, for use with Config::get()
const ExperiencedUserMemberSince
Name constant for the ExperiencedUserMemberSince setting, for use with Config::get()
A filter condition module for user experience levels.
prepareCapture(IReadableDatabase $dbr, QueryBackend $query)
validateValue( $value)
Validate a value and return its normalized form.mixed
prepareConds(IReadableDatabase $dbr, QueryBackend $query)
Add conditions to the query according to the values passed to require() and exclude()....
evaluate(stdClass $row, $value)
Evaluate the filter condition against a row, determining whether it is true or false....
__construct(private ServiceOptions $config, private TempUserConfig $tempUserConfig, private UserFactory $userFactory)
Shared code between the named and experience filter conditions.
Create User objects.
The narrow interface passed to filter modules.
rcUserFields()
Add the rc_user and rc_user_text fields to the query, conventional aliases for actor_user and actor_n...
where(IExpression $expr)
Add a condition to the query.
joinForConds(string $table)
Join on the specified table and declare that it will be used to provide fields for the WHERE clause.
Interface for temporary user creation config and name matching.
A database connection without write operations.
expr(string $field, string $op, $value)
See Expression::__construct()
andExpr(array $conds)
See Expression::__construct()
orExpr(array $conds)
See Expression::__construct()
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by ConvertibleTimestamp to the format used for ins...