Add stuff to enable sort of result.
[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 {
24e08e33 290 return XDB::format('p.sex = {?}', $this->sex == User::GENDER_FEMALE ? 'female' : 'male');
4927ee54
FB
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;
24e08e33 319 private $sort = array();
784745ce 320 private $query = null;
24e08e33 321 private $orderby = null;
784745ce 322
24e08e33 323 public function __construct($cond = null, $sort = null)
5dd9d823
FB
324 {
325 if (!is_null($cond)) {
326 if ($cond instanceof UserFilterCondition) {
327 $this->setCondition($cond);
328 }
329 }
24e08e33
FB
330 if (!is_null($sort)) {
331 if ($sort instanceof UserFilterOrder) {
332 $this->addSort($sort);
333 }
334 }
5dd9d823
FB
335 }
336
784745ce
FB
337 private function buildQuery()
338 {
339 if (is_null($this->query)) {
340 $where = $this->root->buildCondition($this);
341 $joins = $this->buildJoins();
342 $this->query = 'FROM accounts AS a
343 INNER JOIN account_profiles AS ap ON (ap.uid = a.uid AND FIND_IN_SET(\'owner\', ap.perms))
344 INNER JOIN profiles AS p ON (p.pid = ap.pid)
345 ' . $joins . '
346 WHERE (' . $where . ')';
347 }
24e08e33
FB
348 if (is_null($this->sortby)) {
349 $this->sortby = '';
350 }
784745ce
FB
351 }
352
353 private function formatJoin(array $joins)
354 {
355 $str = '';
356 foreach ($joins as $key => $infos) {
357 $mode = $infos[0];
358 $table = $infos[1];
359 if ($mode == 'inner') {
360 $str .= 'INNER JOIN ';
361 } else if ($mode == 'left') {
362 $str .= 'LEFT JOIN ';
363 } else {
364 Platal::page()->kill("Join mode error");
365 }
366 $str .= $table . ' AS ' . $key;
367 if (isset($infos[2])) {
4927ee54 368 $str .= ' ON (' . str_replace(array('$ME', '$PID', '$UID'), array($key, 'p.pid', 'a.uid'), $infos[2]) . ')';
784745ce
FB
369 }
370 $str .= "\n";
371 }
372 return $str;
373 }
374
375 private function buildJoins()
376 {
4927ee54 377 $joins = $this->educationJoins() + $this->nameJoins() + $this->groupJoins();
784745ce
FB
378 return $this->formatJoin($joins);
379 }
a087cc8d
FB
380
381 /** Check that the user match the given rule.
382 */
383 public function checkUser(PlUser &$user)
384 {
784745ce
FB
385 $this->buildQuery();
386 $count = (int)XDB::fetchOneCell('SELECT COUNT(*)
387 ' . $this->query . XDB::format(' AND a.uid = {?}', $user->id()));
388 return $count == 1;
a087cc8d
FB
389 }
390
391 /** Filter a list of user to extract the users matching the rule.
392 */
393 public function filter(array $users)
394 {
4927ee54
FB
395 $this->buildQuery();
396 $table = array();
397 $uids = array();
398 foreach ($users as $user) {
399 $uids[] = $user->id();
400 $table[$user->id()] = $user;
401 }
402 $fetched = XDB::fetchColumn('SELECT a.uid
403 ' . $this->query . ' AND a.uid IN (' . implode(', ', $uids) . ')
404 GROUP BY a.uid');
a087cc8d 405 $output = array();
4927ee54
FB
406 foreach ($fetched as $uid) {
407 $output[] = $table[$uid];
a087cc8d
FB
408 }
409 return $output;
410 }
411
4927ee54
FB
412 public function getUIDs()
413 {
414 $this->buildQuery();
415 return XDB::fetchColumn('SELECT a.uid
416 ' . $this->query . '
417 GROUP BY a.uid');
418 }
419
420 public function getUsers()
421 {
422 return User::getBuildUsersWithUIDs($this->getUIDs());
423 }
424
a087cc8d
FB
425 public function setCondition(UserFilterCondition &$cond)
426 {
427 $this->root =& $cond;
784745ce 428 $this->query = null;
a087cc8d
FB
429 }
430
24e08e33
FB
431 public function addSort(UserFilterOrder &$sort)
432 {
433 $this->sort[] =& $sort;
434 $this->sortby = null;
435 }
436
a087cc8d
FB
437 static public function getLegacy($promo_min, $promo_max)
438 {
a087cc8d 439 if ($promo_min != 0) {
784745ce 440 $min = new UFC_Promo('>=', self::GRADE_ING, intval($promo_min));
5dd9d823
FB
441 } else {
442 $min = new UFC_True();
a087cc8d 443 }
a087cc8d 444 if ($promo_max != 0) {
784745ce 445 $max = new UFC_Promo('<=', self::GRADE_ING, intval($promo_max));
a087cc8d 446 } else {
5dd9d823 447 $max = new UFC_True();
a087cc8d 448 }
5dd9d823 449 return new UserFilter(new UFC_And($min, $max));
a087cc8d 450 }
784745ce
FB
451
452
453 /** NAMES
454 */
455 const LASTNAME = 'lastname';
456 const FIRSTNAME = 'firstname';
457 const NICKNAME = 'nickname';
458 const PSEUDONYM = 'pseudonym';
459 const NAME = 'name';
460 const VN_MARITAL = 'marital';
461 const VN_ORDINARY = 'ordinary';
462 const VN_OTHER = 'other';
463 const VN_INI = 'ini';
464
465 static public $name_variants = array(
466 self::LASTNAME => array(self::VN_MARITAL, self::VN_ORDINARY),
467 self::FIRSTNAME => array(self::VN_ORDINARY, self::VN_INI, self::VN_OTHER),
468 self::NICKNAME => array(), self::PSEUDONYM => array(),
469 self::NAME => array());
470
471 static public function assertName($name)
472 {
473 if (!Profile::getNameTypeId($name)) {
474 Platal::page()->kill('Invalid name type');
475 }
476 }
477
478 private $pn = array();
479 private $pno = 0;
480 public function addNameFilter($type, $variant = null)
481 {
482 if (!is_null($variant)) {
483 $ft = $type . '_' . $variant;
484 } else {
485 $ft = $type;
486 }
487 $sub = '_' . $ft;
488 self::assertName($ft);
489
490 if (!is_null($variant) && $variant == 'other') {
491 $sub .= $this->pno++;
492 }
493 $this->pn[$sub] = Profile::getNameTypeId($ft);
494 return $sub;
495 }
496
497 private function nameJoins()
498 {
499 $joins = array();
500 foreach ($this->pn as $sub => $type) {
501 $joins['pn' . $sub] = array('left', 'profile_name', '$ME.pid = $PID AND $ME.typeid = ' . $type);
502 }
503 return $joins;
504 }
505
506
507 /** EDUCATION
508 */
509 const GRADE_ING = 'Ing.';
510 const GRADE_PHD = 'PhD';
511 const GRADE_MST = 'M%';
512 static public function isGrade($grade)
513 {
514 return $grade == self::GRADE_ING || self::$grade == GRADE_PHD || self::$grade == GRADE_MST;
515 }
516
517 static public function assertGrade($grade)
518 {
519 if (!self::isGrade($grade)) {
520 Platal::page()->killError("DiplĂ´me non valide");
521 }
522 }
523
524 private $pepe = array();
525 private $with_pee = false;
526 private $pe_g = 0;
527 public function addEducationFilter($x = false, $grade = null)
528 {
529 if (!$x) {
530 $index = $this->pe_g;
531 $sub = $this->pe_g++;
532 } else {
533 self::assertGrade($grade);
534 $index = $grade;
535 $sub = $grade[0];
536 $this->with_pee = true;
537 }
538 $sub = '_' . $sub;
539 $this->pepe[$index] = $sub;
540 return $sub;
541 }
542
543 private function educationJoins()
544 {
545 $joins = array();
546 if ($this->with_pee) {
547 $joins['pee'] = array('inner', 'profile_education_enum', 'pee.abbreviation = \'X\'');
548 }
549 foreach ($this->pepe as $grade => $sub) {
550 if ($this->isGrade($grade)) {
551 $joins['pe' . $sub] = array('left', 'profile_education', '$ME.eduid = pee.id AND $ME.uid = $PID');
552 $joins['pede' . $sub] = array('inner', 'profile_education_degree_enum', '$ME.id = pe' . $sub . '.degreeid AND $ME.abbreviation LIKE ' .
553 XDB::format('{?}', $grade));
554 } else {
555 $joins['pe' . $sub] = array('left', 'profile_education', '$ME.uid = $PID');
556 $joins['pee' . $sub] = array('inner', 'profile_education_enum', '$ME.id = pe' . $sub . '.eduid');
557 $joins['pede' . $sub] = array('inner', 'profile_education_degree_enum', '$ME.id = pe' . $sub . '.degreeid');
558 }
559 }
560 return $joins;
561 }
4927ee54
FB
562
563
564 /** GROUPS
565 */
566 private $gpm = array();
567 private $gpm_o = 0;
568 public function addGroupFilter($group = null)
569 {
570 if (!is_null($group)) {
571 if (ctype_digit($group)) {
572 $index = $sub = $group;
573 } else {
574 $index = $group;
575 $sub = preg_replace('/[^a-z0-9]/i', '', $group);
576 }
577 } else {
578 $sub = 'group_' . $this->gpm_o++;
579 $index = null;
580 }
581 $sub = '_' . $sub;
582 $this->gpm[$sub] = $index;
583 return $sub;
584 }
585
586 private function groupJoins()
587 {
588 $joins = array();
589 foreach ($this->gpm as $sub => $key) {
590 if (is_null($key)) {
591 $joins['gpa' . $sub] = array('inner', 'groupex.asso');
592 $joins['gpm' . $sub] = array('left', 'groupex.membres', '$ME.uid = $UID AND $ME.asso_id = gpa' . $sub . '.id');
593 } else if (ctype_digit($key)) {
594 $joins['gpm' . $sub] = array('left', 'groupex.membres', '$ME.uid = $UID AND $ME.asso_id = ' . $key);
595 } else {
596 $joins['gpa' . $sub] = array('inner', 'groupex.asso', XDB::format('$ME.diminutif = {?}', $key));
597 $joins['gpm' . $sub] = array('left', 'groupex.membres', '$ME.uid = $UID AND $ME.asso_id = gpa' . $sub . '.id');
598 }
599 }
600 return $joins;
601 }
a087cc8d
FB
602}
603
604// vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8:
605?>