More filters:
[platal.git] / classes / userfilter.php
CommitLineData
a087cc8d
FB
1<?php
2/***************************************************************************
3 * Copyright (C) 2003-2009 Polytechnique.org *
4 * http://opensource.polytechnique.org/ *
5 * *
6 * This program is free software; you can redistribute it and/or modify *
7 * it under the terms of the GNU General Public License as published by *
8 * the Free Software Foundation; either version 2 of the License, or *
9 * (at your option) any later version. *
10 * *
11 * This program is distributed in the hope that it will be useful, *
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
14 * GNU General Public License for more details. *
15 * *
16 * You should have received a copy of the GNU General Public License *
17 * along with this program; if not, write to the Free Software *
18 * Foundation, Inc., *
19 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *
20 ***************************************************************************/
21
22interface UserFilterCondition
23{
5dd9d823
FB
24 const COND_TRUE = 'TRUE';
25 const COND_FALSE = 'FALSE';
26
a087cc8d
FB
27 /** Check that the given user matches the rule.
28 */
784745ce 29 public function buildCondition(UserFilter &$uf);
a087cc8d
FB
30}
31
32abstract class UFC_OneChild implements UserFilterCondition
33{
34 protected $child;
35
5dd9d823
FB
36 public function __construct($child = null)
37 {
38 if (!is_null($child) && ($child instanceof UserFilterCondition)) {
39 $this->setChild($child);
40 }
41 }
42
a087cc8d
FB
43 public function setChild(UserFilterCondition &$cond)
44 {
45 $this->child =& $cond;
46 }
47}
48
49abstract class UFC_NChildren implements UserFilterCondition
50{
51 protected $children = array();
52
5dd9d823
FB
53 public function __construct()
54 {
55 $children = func_get_args();
56 foreach ($children as &$child) {
57 if (!is_null($child) && ($child instanceof UserFilterCondition)) {
58 $this->addChild($child);
59 }
60 }
61 }
62
a087cc8d
FB
63 public function addChild(UserFilterCondition &$cond)
64 {
65 $this->children[] =& $cond;
66 }
5dd9d823
FB
67
68 protected function catConds(array $cond, $op, $fallback)
69 {
70 if (count($cond) == 0) {
71 return $fallback;
72 } else if (count($cond) == 1) {
73 return $cond[0];
74 } else {
4927ee54 75 return '(' . implode(') ' . $op . ' (', $cond) . ')';
5dd9d823
FB
76 }
77 }
a087cc8d
FB
78}
79
80class UFC_True implements UserFilterCondition
81{
784745ce 82 public function buildCondition(UserFilter &$uf)
a087cc8d 83 {
5dd9d823 84 return self::COND_TRUE;
a087cc8d
FB
85 }
86}
87
88class UFC_False implements UserFilterCondition
89{
784745ce 90 public function buildCondition(UserFilter &$uf)
a087cc8d 91 {
5dd9d823 92 return self::COND_FALSE;
a087cc8d
FB
93 }
94}
95
96class UFC_Not extends UFC_OneChild
97{
784745ce 98 public function buildCondition(UserFilter &$uf)
a087cc8d 99 {
5dd9d823
FB
100 $val = $this->child->buildCondition($uf);
101 if ($val == self::COND_TRUE) {
102 return self::COND_FALSE;
103 } else if ($val == self::COND_FALSE) {
104 return self::COND_TRUE;
105 } else {
106 return 'NOT (' . $val . ')';
107 }
a087cc8d
FB
108 }
109}
110
111class UFC_And extends UFC_NChildren
112{
784745ce 113 public function buildCondition(UserFilter &$uf)
a087cc8d 114 {
784745ce 115 if (empty($this->children)) {
5dd9d823 116 return self::COND_FALSE;
784745ce 117 } else {
5dd9d823 118 $true = self::COND_FALSE;
784745ce
FB
119 $conds = array();
120 foreach ($this->children as &$child) {
5dd9d823
FB
121 $val = $child->buildCondition($uf);
122 if ($val == self::COND_TRUE) {
123 $true = self::COND_TRUE;
124 } else if ($val == self::COND_FALSE) {
125 return self::COND_FALSE;
126 } else {
127 $conds[] = $val;
128 }
a087cc8d 129 }
5dd9d823 130 return $this->catConds($conds, 'AND', $true);
a087cc8d 131 }
a087cc8d
FB
132 }
133}
134
135class UFC_Or extends UFC_NChildren
136{
784745ce 137 public function buildCondition(UserFilter &$uf)
a087cc8d 138 {
784745ce 139 if (empty($this->children)) {
5dd9d823 140 return self::COND_TRUE;
784745ce 141 } else {
5dd9d823 142 $true = self::COND_TRUE;
784745ce
FB
143 $conds = array();
144 foreach ($this->children as &$child) {
5dd9d823
FB
145 $val = $child->buildCondition($uf);
146 if ($val == self::COND_TRUE) {
147 return self::COND_TRUE;
148 } else if ($val == self::COND_FALSE) {
149 $true = self::COND_FALSE;
150 } else {
151 $conds[] = $val;
152 }
a087cc8d 153 }
5dd9d823 154 return $this->catConds($conds, 'OR', $true);
a087cc8d 155 }
a087cc8d
FB
156 }
157}
158
159class UFC_Promo implements UserFilterCondition
160{
a087cc8d
FB
161
162 private $grade;
163 private $promo;
164 private $comparison;
165
166 public function __construct($comparison, $grade, $promo)
167 {
168 $this->grade = $grade;
169 $this->comparison = $comparison;
170 $this->promo = $promo;
784745ce 171 UserFilter::assertGrade($this->grade);
a087cc8d
FB
172 }
173
784745ce 174 public function buildCondition(UserFilter &$uf)
a087cc8d 175 {
a087cc8d 176 // XXX: Definition of promotion for phds and masters might change in near future.
784745ce 177 if ($this->grade == UserFilter::GRADE_ING) {
a087cc8d
FB
178 $promo_year = 'entry_year';
179 } else {
180 $promo_year = 'grad_year';
181 }
784745ce
FB
182 $sub = $uf->addEducationFilter(true, $this->grade);
183 $field = 'pe' . $sub . '.' . $promo_year;
184 return $field . ' IS NOT NULL AND ' . $field . ' ' . $this->comparison . ' ' . XDB::format('{?}', $this->promo);
185 }
186}
187
188class UFC_Name implements UserFilterCondition
189{
190 const PREFIX = 1;
191 const SUFFIX = 2;
192 const PARTICLE = 7;
193 const VARIANTS = 8;
194 const CONTAINS = 3;
195
196 private $type;
197 private $text;
198 private $mode;
199
200 public function __construct($type, $text, $mode)
201 {
202 $this->type = $type;
203 $this->text = $text;
204 $this->mode = $mode;
205 }
206
207 private function buildNameQuery($type, $variant, $where, UserFilter &$uf)
208 {
209 $sub = $uf->addNameFilter($type, $variant);
210 return str_replace('$ME', 'pn' . $sub, $where);
211 }
212
213 public function buildCondition(UserFilter &$uf)
214 {
215 $left = '$ME.name';
216 $op = ' LIKE ';
217 if (($this->mode & self::PARTICLE) == self::PARTICLE) {
218 $left = 'CONCAT($ME.particle, \' \', $ME.name)';
219 }
220 if (($this->mode & self::CONTAINS) == 0) {
221 $right = XDB::format('{?}', $this->text);
222 $op = ' = ';
223 } else if (($this->mode & self::CONTAINS) == self::PREFIX) {
224 $right = XDB::format('CONCAT({?}, \'%\')', $this->text);
225 } else if (($this->mode & self::CONTAINS) == self::SUFFIX) {
226 $right = XDB::format('CONCAT(\'%\', {?})', $this->text);
227 } else {
228 $right = XDB::format('CONCAT(\'%\', {?}, \'%\')', $this->text);
229 }
230 $cond = $left . $op . $right;
231 $conds = array($this->buildNameQuery($this->type, null, $cond, $uf));
232 if (($this->mode & self::VARIANTS) != 0) {
233 foreach (UserFilter::$name_variants[$this->type] as $var) {
234 $conds[] = $this->buildNameQuery($this->type, $var, $cond, $uf);
235 }
236 }
237 return implode(' OR ', $conds);
a087cc8d
FB
238 }
239}
240
4927ee54
FB
241class UFC_Dead implements UserFilterCondition
242{
243 private $dead;
244 public function __construct($dead)
245 {
246 $this->dead = $dead;
247 }
248
249 public function buildCondition(UserFilter &$uf)
250 {
251 if ($this->dead) {
252 return 'p.deathdate IS NOT NULL';
253 } else {
254 return 'p.deathdate IS NULL';
255 }
256 }
257}
258
259class UFC_Registered implements UserFilterCondition
260{
261 private $active;
262 public function __construct($active = false)
263 {
264 $this->only_active = $active;
265 }
266
267 public function buildCondition(UserFilter &$uf)
268 {
269 if ($this->active) {
270 return 'a.uid IS NOT NULL AND a.state = \'active\'';
271 } else {
272 return 'a.uid IS NOT NULL AND a.state != \'pending\'';
273 }
274 }
275}
276
277class UFC_Sex implements UserFilterCondition
278{
279 private $sex;
280 public function __construct($sex)
281 {
282 $this->sex = $sex;
283 }
284
285 public function buildCondition(UserFilter &$uf)
286 {
287 if ($this->sex != User::GENDER_MALE && $this->sex != User::GENDER_FEMALE) {
288 return self::COND_FALSE;
289 } else {
290 return XDB::format('p.sex = {?}', $this->sex);
291 }
292 }
293}
294
295class UFC_Group implements UserFilterCondition
296{
297 private $group;
298 private $admin;
299 public function __construct($group, $admin = false)
300 {
301 $this->group = $group;
302 $this->admin = $admin;
303 }
304
305 public function buildCondition(UserFilter &$uf)
306 {
307 $sub = $uf->addGroupFilter($this->group);
308 $where = 'gpm' . $sub . '.perms IS NOT NULL';
309 if ($this->admin) {
310 $where .= ' AND gpm' . $sub . '.perms = \'admin\'';
311 }
312 return $where;
313 }
314}
315
a087cc8d
FB
316class UserFilter
317{
318 private $root;
784745ce
FB
319 private $query = null;
320
5dd9d823
FB
321 public function __construct($cond = null)
322 {
323 if (!is_null($cond)) {
324 if ($cond instanceof UserFilterCondition) {
325 $this->setCondition($cond);
326 }
327 }
328 }
329
784745ce
FB
330 private function buildQuery()
331 {
332 if (is_null($this->query)) {
333 $where = $this->root->buildCondition($this);
334 $joins = $this->buildJoins();
335 $this->query = 'FROM accounts AS a
336 INNER JOIN account_profiles AS ap ON (ap.uid = a.uid AND FIND_IN_SET(\'owner\', ap.perms))
337 INNER JOIN profiles AS p ON (p.pid = ap.pid)
338 ' . $joins . '
339 WHERE (' . $where . ')';
340 }
341 }
342
343 private function formatJoin(array $joins)
344 {
345 $str = '';
346 foreach ($joins as $key => $infos) {
347 $mode = $infos[0];
348 $table = $infos[1];
349 if ($mode == 'inner') {
350 $str .= 'INNER JOIN ';
351 } else if ($mode == 'left') {
352 $str .= 'LEFT JOIN ';
353 } else {
354 Platal::page()->kill("Join mode error");
355 }
356 $str .= $table . ' AS ' . $key;
357 if (isset($infos[2])) {
4927ee54 358 $str .= ' ON (' . str_replace(array('$ME', '$PID', '$UID'), array($key, 'p.pid', 'a.uid'), $infos[2]) . ')';
784745ce
FB
359 }
360 $str .= "\n";
361 }
362 return $str;
363 }
364
365 private function buildJoins()
366 {
4927ee54 367 $joins = $this->educationJoins() + $this->nameJoins() + $this->groupJoins();
784745ce
FB
368 return $this->formatJoin($joins);
369 }
a087cc8d
FB
370
371 /** Check that the user match the given rule.
372 */
373 public function checkUser(PlUser &$user)
374 {
784745ce
FB
375 $this->buildQuery();
376 $count = (int)XDB::fetchOneCell('SELECT COUNT(*)
377 ' . $this->query . XDB::format(' AND a.uid = {?}', $user->id()));
378 return $count == 1;
a087cc8d
FB
379 }
380
381 /** Filter a list of user to extract the users matching the rule.
382 */
383 public function filter(array $users)
384 {
4927ee54
FB
385 $this->buildQuery();
386 $table = array();
387 $uids = array();
388 foreach ($users as $user) {
389 $uids[] = $user->id();
390 $table[$user->id()] = $user;
391 }
392 $fetched = XDB::fetchColumn('SELECT a.uid
393 ' . $this->query . ' AND a.uid IN (' . implode(', ', $uids) . ')
394 GROUP BY a.uid');
a087cc8d 395 $output = array();
4927ee54
FB
396 foreach ($fetched as $uid) {
397 $output[] = $table[$uid];
a087cc8d
FB
398 }
399 return $output;
400 }
401
4927ee54
FB
402 public function getUIDs()
403 {
404 $this->buildQuery();
405 return XDB::fetchColumn('SELECT a.uid
406 ' . $this->query . '
407 GROUP BY a.uid');
408 }
409
410 public function getUsers()
411 {
412 return User::getBuildUsersWithUIDs($this->getUIDs());
413 }
414
a087cc8d
FB
415 public function setCondition(UserFilterCondition &$cond)
416 {
417 $this->root =& $cond;
784745ce 418 $this->query = null;
a087cc8d
FB
419 }
420
a087cc8d
FB
421 static public function getLegacy($promo_min, $promo_max)
422 {
a087cc8d 423 if ($promo_min != 0) {
784745ce 424 $min = new UFC_Promo('>=', self::GRADE_ING, intval($promo_min));
5dd9d823
FB
425 } else {
426 $min = new UFC_True();
a087cc8d 427 }
a087cc8d 428 if ($promo_max != 0) {
784745ce 429 $max = new UFC_Promo('<=', self::GRADE_ING, intval($promo_max));
a087cc8d 430 } else {
5dd9d823 431 $max = new UFC_True();
a087cc8d 432 }
5dd9d823 433 return new UserFilter(new UFC_And($min, $max));
a087cc8d 434 }
784745ce
FB
435
436
437 /** NAMES
438 */
439 const LASTNAME = 'lastname';
440 const FIRSTNAME = 'firstname';
441 const NICKNAME = 'nickname';
442 const PSEUDONYM = 'pseudonym';
443 const NAME = 'name';
444 const VN_MARITAL = 'marital';
445 const VN_ORDINARY = 'ordinary';
446 const VN_OTHER = 'other';
447 const VN_INI = 'ini';
448
449 static public $name_variants = array(
450 self::LASTNAME => array(self::VN_MARITAL, self::VN_ORDINARY),
451 self::FIRSTNAME => array(self::VN_ORDINARY, self::VN_INI, self::VN_OTHER),
452 self::NICKNAME => array(), self::PSEUDONYM => array(),
453 self::NAME => array());
454
455 static public function assertName($name)
456 {
457 if (!Profile::getNameTypeId($name)) {
458 Platal::page()->kill('Invalid name type');
459 }
460 }
461
462 private $pn = array();
463 private $pno = 0;
464 public function addNameFilter($type, $variant = null)
465 {
466 if (!is_null($variant)) {
467 $ft = $type . '_' . $variant;
468 } else {
469 $ft = $type;
470 }
471 $sub = '_' . $ft;
472 self::assertName($ft);
473
474 if (!is_null($variant) && $variant == 'other') {
475 $sub .= $this->pno++;
476 }
477 $this->pn[$sub] = Profile::getNameTypeId($ft);
478 return $sub;
479 }
480
481 private function nameJoins()
482 {
483 $joins = array();
484 foreach ($this->pn as $sub => $type) {
485 $joins['pn' . $sub] = array('left', 'profile_name', '$ME.pid = $PID AND $ME.typeid = ' . $type);
486 }
487 return $joins;
488 }
489
490
491 /** EDUCATION
492 */
493 const GRADE_ING = 'Ing.';
494 const GRADE_PHD = 'PhD';
495 const GRADE_MST = 'M%';
496 static public function isGrade($grade)
497 {
498 return $grade == self::GRADE_ING || self::$grade == GRADE_PHD || self::$grade == GRADE_MST;
499 }
500
501 static public function assertGrade($grade)
502 {
503 if (!self::isGrade($grade)) {
504 Platal::page()->killError("DiplĂ´me non valide");
505 }
506 }
507
508 private $pepe = array();
509 private $with_pee = false;
510 private $pe_g = 0;
511 public function addEducationFilter($x = false, $grade = null)
512 {
513 if (!$x) {
514 $index = $this->pe_g;
515 $sub = $this->pe_g++;
516 } else {
517 self::assertGrade($grade);
518 $index = $grade;
519 $sub = $grade[0];
520 $this->with_pee = true;
521 }
522 $sub = '_' . $sub;
523 $this->pepe[$index] = $sub;
524 return $sub;
525 }
526
527 private function educationJoins()
528 {
529 $joins = array();
530 if ($this->with_pee) {
531 $joins['pee'] = array('inner', 'profile_education_enum', 'pee.abbreviation = \'X\'');
532 }
533 foreach ($this->pepe as $grade => $sub) {
534 if ($this->isGrade($grade)) {
535 $joins['pe' . $sub] = array('left', 'profile_education', '$ME.eduid = pee.id AND $ME.uid = $PID');
536 $joins['pede' . $sub] = array('inner', 'profile_education_degree_enum', '$ME.id = pe' . $sub . '.degreeid AND $ME.abbreviation LIKE ' .
537 XDB::format('{?}', $grade));
538 } else {
539 $joins['pe' . $sub] = array('left', 'profile_education', '$ME.uid = $PID');
540 $joins['pee' . $sub] = array('inner', 'profile_education_enum', '$ME.id = pe' . $sub . '.eduid');
541 $joins['pede' . $sub] = array('inner', 'profile_education_degree_enum', '$ME.id = pe' . $sub . '.degreeid');
542 }
543 }
544 return $joins;
545 }
4927ee54
FB
546
547
548 /** GROUPS
549 */
550 private $gpm = array();
551 private $gpm_o = 0;
552 public function addGroupFilter($group = null)
553 {
554 if (!is_null($group)) {
555 if (ctype_digit($group)) {
556 $index = $sub = $group;
557 } else {
558 $index = $group;
559 $sub = preg_replace('/[^a-z0-9]/i', '', $group);
560 }
561 } else {
562 $sub = 'group_' . $this->gpm_o++;
563 $index = null;
564 }
565 $sub = '_' . $sub;
566 $this->gpm[$sub] = $index;
567 return $sub;
568 }
569
570 private function groupJoins()
571 {
572 $joins = array();
573 foreach ($this->gpm as $sub => $key) {
574 if (is_null($key)) {
575 $joins['gpa' . $sub] = array('inner', 'groupex.asso');
576 $joins['gpm' . $sub] = array('left', 'groupex.membres', '$ME.uid = $UID AND $ME.asso_id = gpa' . $sub . '.id');
577 } else if (ctype_digit($key)) {
578 $joins['gpm' . $sub] = array('left', 'groupex.membres', '$ME.uid = $UID AND $ME.asso_id = ' . $key);
579 } else {
580 $joins['gpa' . $sub] = array('inner', 'groupex.asso', XDB::format('$ME.diminutif = {?}', $key));
581 $joins['gpm' . $sub] = array('left', 'groupex.membres', '$ME.uid = $UID AND $ME.asso_id = gpa' . $sub . '.id');
582 }
583 }
584 return $joins;
585 }
a087cc8d
FB
586}
587
588// vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8:
589?>