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
24 public const CONSTRUCTOR_OPTIONS = [
29 ];
30
31 private const LEVELS = [ 'unregistered', 'registered', 'newcomer', 'learner', 'experienced' ];
32
33 private NamedConditionHelper $namedConditionHelper;
34
35 public function __construct(
36 private ServiceOptions $config,
37 private TempUserConfig $tempUserConfig,
38 private UserFactory $userFactory
39 ) {
40 $this->namedConditionHelper = new NamedConditionHelper( $this->tempUserConfig );
41 }
42
44 public function validateValue( $value ) {
45 if ( !in_array( $value, self::LEVELS ) ) {
46 throw new InvalidArgumentException( "must be one of : " .
47 implode( ', ', self::LEVELS )
48 );
49 }
50 return $value;
51 }
52
57 public function exclude( $value ): void {
58 throw new LogicException( 'unimplemented' );
59 }
60
62 public function evaluate( stdClass $row, $value ): bool {
63 if ( $value === 'registered' || $value === 'unregistered' ) {
64 $rowValue = $this->namedConditionHelper->isNamed( $row )
65 ? 'registered' : 'unregistered';
66 return $value === $rowValue;
67 } else {
68 return $this->getExperienceFromRow( $row ) === $value;
69 }
70 }
71
72 private function getExperienceFromRow( stdClass $row ): string {
73 // This should match User::getExperienceLevel(), except that we treat
74 // temporary users as unregistered
75 if ( !$this->namedConditionHelper->isNamed( $row ) ) {
76 return 'unregistered';
77 } else {
78 // TODO: this depends on a UserArray batch query done in
79 // ChangesListSpecialPage to efficient. The batch query should be
80 // owned by this module, or we should do a join. But a join is
81 // inefficient due to T403798.
82 $user = $this->userFactory->newFromAnyId( $row->rc_user, $row->rc_user_text );
83 return $user->getExperienceLevel();
84 }
85 }
86
88 protected function prepareCapture( IReadableDatabase $dbr, QueryBackend $query ) {
89 $query->rcUserFields();
90 }
91
93 public function prepareConds( IReadableDatabase $dbr, QueryBackend $query ) {
94 $selected = array_fill_keys( $this->required, true );
95
96 $isUnregistered = $this->namedConditionHelper->getExpression( $dbr, false );
97 $isRegistered = $this->namedConditionHelper->getExpression( $dbr, true );
98 $aboveNewcomer = $this->getExperienceExpr( 'learner', $dbr );
99 $notAboveNewcomer = $this->getExperienceExpr( 'learner', $dbr, true );
100 $aboveLearner = $this->getExperienceExpr( 'experienced', $dbr );
101 $notAboveLearner = $this->getExperienceExpr( 'experienced', $dbr, true );
102
103 // We need to select some range of user experience levels, from the following table:
104 // | Unregistered | --------- Registered --------- |
105 // | | Newcomers | Learners | Experienced |
106 // |<------------>|<----------->|<---------->|<----------->|
107 // We just need to define a condition for each of the columns, figure out which are selected,
108 // and then OR them together.
109 $columnConds = [
110 'unregistered' => $isUnregistered,
111 'registered' => $isRegistered,
112 'newcomer' => $dbr->andExpr( [ $isRegistered, $notAboveNewcomer ] ),
113 'learner' => $dbr->andExpr( [ $isRegistered, $aboveNewcomer, $notAboveLearner ] ),
114 'experienced' => $dbr->andExpr( [ $isRegistered, $aboveLearner ] ),
115 ];
116
117 // There are some cases where we can easily optimize away some queries:
118 // | Unregistered | --------- Registered --------- |
119 // | | Newcomers | Learners | Experienced |
120 // | |<-------------------------------------->| (1)
121 // |<----------------------------------------------------->| (2)
122
123 // (1) Selecting all of "Newcomers", "Learners" and "Experienced users" is the same as "Registered".
124 if (
125 isset( $selected['registered'] ) ||
126 ( isset( $selected['newcomer'] ) && isset( $selected['learner'] ) && isset( $selected['experienced'] ) )
127 ) {
128 unset( $selected['newcomer'], $selected['learner'], $selected['experienced'] );
129 $selected['registered'] = true;
130 }
131 // (2) Selecting "Unregistered" and "Registered" covers all users.
132 if ( isset( $selected['registered'] ) && isset( $selected['unregistered'] ) ) {
133 unset( $selected['registered'], $selected['unregistered'] );
134 }
135
136 // Combine the conditions for the selected columns.
137 if ( !$selected ) {
138 return;
139 }
140 $selectedColumnConds = array_values( array_intersect_key( $columnConds, $selected ) );
141 $query->where( $dbr->orExpr( $selectedColumnConds ) );
142
143 // Add necessary tables to the queries.
144 $query->joinForConds( 'actor' )->straight();
145 if ( isset( $selected['newcomer'] ) || isset( $selected['learner'] ) || isset( $selected['experienced'] ) ) {
146 $query->joinForConds( 'user' )->weakLeft();
147 }
148 }
149
156 private function getExperienceExpr( $level, IReadableDatabase $dbr, $asNotCondition = false ): IExpression {
157 $configSince = $this->getRegistrationThreshold( $level );
158 $now = ConvertibleTimestamp::time();
159 $secondsPerDay = 86400;
160 $timeCutoff = $now - $configSince * $secondsPerDay;
161
162 $editCutoff = $this->getEditThreshold( $level );
163
164 if ( $asNotCondition ) {
165 return $dbr->expr( 'user_editcount', '<', $editCutoff )
166 ->or( 'user_registration', '>', $dbr->timestamp( $timeCutoff ) );
167 }
168 return $dbr->expr( 'user_editcount', '>=', $editCutoff )->andExpr(
169 // Users who don't have user_registration set are very old, so we assume they're above any cutoff
170 $dbr->expr( 'user_registration', '=', null )
171 ->or( 'user_registration', '<=', $dbr->timestamp( $timeCutoff ) )
172 );
173 }
174
179 private function getRegistrationThreshold( $level ) {
180 return match ( $level ) {
181 'learner' => $this->config->get( MainConfigNames::LearnerMemberSince ),
182 'experienced' => $this->config->get( MainConfigNames::ExperiencedUserMemberSince ),
183 };
184 }
185
190 private function getEditThreshold( $level ) {
191 return match ( $level ) {
192 'learner' => $this->config->get( MainConfigNames::LearnerEdits ),
193 'experienced' => $this->config->get( MainConfigNames::ExperiencedUserEdits ),
194 };
195 }
196}
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:71
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...