64f310bdbb411d894aad8fc00436aabc57f656bf
[platal.git] / classes / userfilter.php
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
22
23 /******************
24 * CONDITIONS
25 ******************/
26
27 // {{{ interface UserFilterCondition
28 /** This interface describe objects which filter users based
29 * on various parameters.
30 * The parameters of the filter must be given to the constructor.
31 * The buildCondition function is called by UserFilter when
32 * actually building the query. That function must call
33 * $uf->addWheteverFilter so that the UserFilter makes
34 * adequate joins. It must return the 'WHERE' condition to use
35 * with the filter.
36 */
37 interface UserFilterCondition
38 {
39 /** Check that the given user matches the rule.
40 */
41 public function buildCondition(UserFilter &$uf);
42 }
43 // }}}
44
45 // {{{ class UFC_Profile
46 /** Filters users who have a profile
47 */
48 class UFC_Profile implements UserFilterCondition
49 {
50 public function buildCondition(UserFilter &$uf)
51 {
52 return '$PID IS NOT NULL';
53 }
54 }
55 // }}}
56
57 // {{{ class UFC_Promo
58 /** Filters users based on promotion
59 * @param $comparison Comparison operator (>, =, ...)
60 * @param $grade Formation on which to restrict, UserFilter::DISPLAY for "any formation"
61 * @param $promo Promotion on which the filter is based
62 */
63 class UFC_Promo implements UserFilterCondition
64 {
65
66 private $grade;
67 private $promo;
68 private $comparison;
69
70 public function __construct($comparison, $grade, $promo)
71 {
72 $this->grade = $grade;
73 $this->comparison = $comparison;
74 $this->promo = $promo;
75 if ($this->grade != UserFilter::DISPLAY) {
76 UserFilter::assertGrade($this->grade);
77 }
78 }
79
80 public function buildCondition(UserFilter &$uf)
81 {
82 if ($this->grade == UserFilter::DISPLAY) {
83 $sub = $uf->addDisplayFilter();
84 return XDB::format('pd' . $sub . '.promo ' . $this->comparison . ' {?}', $this->promo);
85 } else {
86 $sub = $uf->addEducationFilter(true, $this->grade);
87 $field = 'pe' . $sub . '.' . UserFilter::promoYear($this->grade);
88 return $field . ' IS NOT NULL AND ' . $field . ' ' . $this->comparison . ' ' . XDB::format('{?}', $this->promo);
89 }
90 }
91 }
92 // }}}
93
94 // {{{ class UFC_Name
95 /** Filters users based on name
96 * @param $type Type of name field on which filtering is done (firstname, lastname...)
97 * @param $text Text on which to filter
98 * @param $mode Flag indicating search type (prefix, suffix, with particule...)
99 */
100 class UFC_Name implements UserFilterCondition
101 {
102 const PREFIX = 1;
103 const SUFFIX = 2;
104 const PARTICLE = 7;
105 const VARIANTS = 8;
106 const CONTAINS = 3;
107
108 private $type;
109 private $text;
110 private $mode;
111
112 public function __construct($type, $text, $mode)
113 {
114 $this->type = $type;
115 $this->text = $text;
116 $this->mode = $mode;
117 }
118
119 private function buildNameQuery($type, $variant, $where, UserFilter &$uf)
120 {
121 $sub = $uf->addNameFilter($type, $variant);
122 return str_replace('$ME', 'pn' . $sub, $where);
123 }
124
125 public function buildCondition(UserFilter &$uf)
126 {
127 $left = '$ME.name';
128 $op = ' LIKE ';
129 if (($this->mode & self::PARTICLE) == self::PARTICLE) {
130 $left = 'CONCAT($ME.particle, \' \', $ME.name)';
131 }
132 if (($this->mode & self::CONTAINS) == 0) {
133 $right = XDB::format('{?}', $this->text);
134 $op = ' = ';
135 } else if (($this->mode & self::CONTAINS) == self::PREFIX) {
136 $right = XDB::format('CONCAT({?}, \'%\')', $this->text);
137 } else if (($this->mode & self::CONTAINS) == self::SUFFIX) {
138 $right = XDB::format('CONCAT(\'%\', {?})', $this->text);
139 } else {
140 $right = XDB::format('CONCAT(\'%\', {?}, \'%\')', $this->text);
141 }
142 $cond = $left . $op . $right;
143 $conds = array($this->buildNameQuery($this->type, null, $cond, $uf));
144 if (($this->mode & self::VARIANTS) != 0 && isset(UserFilter::$name_variants[$this->type])) {
145 foreach (UserFilter::$name_variants[$this->type] as $var) {
146 $conds[] = $this->buildNameQuery($this->type, $var, $cond, $uf);
147 }
148 }
149 return implode(' OR ', $conds);
150 }
151 }
152 // }}}
153
154 // {{{ class UFC_NameTokens
155 /** Selects users based on tokens in their name (for quicksearch)
156 * @param $tokens An array of tokens to search
157 * @param $flags Flags the tokens must have (e.g 'public' for public search)
158 * @param $soundex (bool) Whether those tokens are fulltext or soundex
159 */
160 class UFC_NameTokens implements UserFilterCondition
161 {
162 /* Flags */
163 const FLAG_PUBLIC = 'public';
164
165 private $tokens;
166 private $flags;
167 private $soundex;
168 private $exact;
169
170 public function __construct($tokens, $flags = array(), $soundex = false, $exact = false)
171 {
172 $this->tokens = $tokens;
173 if (is_array($flags)) {
174 $this->flags = $flags;
175 } else {
176 $this->flags = array($flags);
177 }
178 $this->soundex = $soundex;
179 $this->exact = $exact;
180 }
181
182 public function buildCondition(UserFilter &$uf)
183 {
184 $sub = $uf->addNameTokensFilter(!($this->exact || $this->soundex));
185 $conds = array();
186 if ($this->soundex) {
187 $conds[] = $sub . '.soundex IN ' . XDB::formatArray($this->tokens);
188 } else if ($this->exact) {
189 $conds[] = $sub . '.token IN ' . XDB::formatArray($this->tokens);
190 } else {
191 $tokconds = array();
192 foreach ($this->tokens as $token) {
193 $tokconds[] = $sub . '.token LIKE ' . XDB::format('CONCAT(\'%\', {?}, \'%\')', $token);
194 }
195 $conds[] = implode(' OR ', $tokconds);
196 }
197
198 if ($this->flags != null) {
199 $conds[] = $sub . '.flags IN ' . XDB::formatArray($this->flags);
200 }
201
202 return implode(' AND ', $conds);
203 }
204 }
205 // }}}
206
207 // {{{ class UFC_Dead
208 /** Filters users based on death date
209 * @param $comparison Comparison operator
210 * @param $date Date to which death date should be compared
211 */
212 class UFC_Dead implements UserFilterCondition
213 {
214 private $comparison;
215 private $date;
216
217 public function __construct($comparison = null, $date = null)
218 {
219 $this->comparison = $comparison;
220 $this->date = $date;
221 }
222
223 public function buildCondition(UserFilter &$uf)
224 {
225 $str = 'p.deathdate IS NOT NULL';
226 if (!is_null($this->comparison)) {
227 $str .= ' AND p.deathdate ' . $this->comparison . ' ' . XDB::format('{?}', date('Y-m-d', $this->date));
228 }
229 return $str;
230 }
231 }
232 // }}}
233
234 // {{{ class UFC_Registered
235 /** Filters users based on registration state
236 * @param $active Whether we want to use only "active" users (i.e with a valid redirection)
237 * @param $comparison Comparison operator
238 * @param $date Date to which users registration date should be compared
239 */
240 class UFC_Registered implements UserFilterCondition
241 {
242 private $active;
243 private $comparison;
244 private $date;
245
246 public function __construct($active = false, $comparison = null, $date = null)
247 {
248 $this->active = $active;
249 $this->comparison = $comparison;
250 $this->date = $date;
251 }
252
253 public function buildCondition(UserFilter &$uf)
254 {
255 if ($this->active) {
256 $date = 'a.uid IS NOT NULL AND a.state = \'active\'';
257 } else {
258 $date = 'a.uid IS NOT NULL AND a.state != \'pending\'';
259 }
260 if (!is_null($this->comparison)) {
261 $date .= ' AND a.registration_date ' . $this->comparison . ' ' . XDB::format('{?}', date('Y-m-d', $this->date));
262 }
263 return $date;
264 }
265 }
266 // }}}
267
268 // {{{ class UFC_ProfileUpdated
269 /** Filters users based on profile update date
270 * @param $comparison Comparison operator
271 * @param $date Date to which profile update date must be compared
272 */
273 class UFC_ProfileUpdated implements UserFilterCondition
274 {
275 private $comparison;
276 private $date;
277
278 public function __construct($comparison = null, $date = null)
279 {
280 $this->comparison = $comparison;
281 $this->date = $date;
282 }
283
284 public function buildCondition(UserFilter &$uf)
285 {
286 return 'p.last_change ' . $this->comparison . XDB::format(' {?}', date('Y-m-d H:i:s', $this->date));
287 }
288 }
289 // }}}
290
291 // {{{ class UFC_Birthday
292 /** Filters users based on next birthday date
293 * @param $comparison Comparison operator
294 * @param $date Date to which users next birthday date should be compared
295 */
296 class UFC_Birthday implements UserFilterCondition
297 {
298 private $comparison;
299 private $date;
300
301 public function __construct($comparison = null, $date = null)
302 {
303 $this->comparison = $comparison;
304 $this->date = $date;
305 }
306
307 public function buildCondition(UserFilter &$uf)
308 {
309 return 'p.next_birthday ' . $this->comparison . XDB::format(' {?}', date('Y-m-d', $this->date));
310 }
311 }
312 // }}}
313
314 // {{{ class UFC_Sex
315 /** Filters users based on sex
316 * @parm $sex One of User::GENDER_MALE or User::GENDER_FEMALE, for selecting users
317 */
318 class UFC_Sex implements UserFilterCondition
319 {
320 private $sex;
321 public function __construct($sex)
322 {
323 $this->sex = $sex;
324 }
325
326 public function buildCondition(UserFilter &$uf)
327 {
328 if ($this->sex != User::GENDER_MALE && $this->sex != User::GENDER_FEMALE) {
329 return self::COND_FALSE;
330 } else {
331 return XDB::format('p.sex = {?}', $this->sex == User::GENDER_FEMALE ? 'female' : 'male');
332 }
333 }
334 }
335 // }}}
336
337 // {{{ class UFC_Group
338 /** Filters users based on group membership
339 * @param $group Group whose members we are selecting
340 * @param $anim Whether to restrict selection to animators of that group
341 */
342 class UFC_Group implements UserFilterCondition
343 {
344 private $group;
345 private $anim;
346 public function __construct($group, $anim = false)
347 {
348 $this->group = $group;
349 $this->anim = $anim;
350 }
351
352 public function buildCondition(UserFilter &$uf)
353 {
354 $sub = $uf->addGroupFilter($this->group);
355 $where = 'gpm' . $sub . '.perms IS NOT NULL';
356 if ($this->anim) {
357 $where .= ' AND gpm' . $sub . '.perms = \'admin\'';
358 }
359 return $where;
360 }
361 }
362 // }}}
363
364 // {{{ class UFC_Email
365 /** Filters users based on email address
366 * @param $email Email whose owner we are looking for
367 */
368 class UFC_Email implements UserFilterCondition
369 {
370 private $email;
371 public function __construct($email)
372 {
373 $this->email = $email;
374 }
375
376 public function buildCondition(UserFilter &$uf)
377 {
378 if (User::isForeignEmailAddress($this->email)) {
379 $sub = $uf->addEmailRedirectFilter($this->email);
380 return XDB::format('e' . $sub . '.email IS NOT NULL OR a.email = {?}', $this->email);
381 } else if (User::isVirtualEmailAddress($this->email)) {
382 $sub = $uf->addVirtualEmailFilter($this->email);
383 return 'vr' . $sub . '.redirect IS NOT NULL';
384 } else {
385 @list($user, $domain) = explode('@', $this->email);
386 $sub = $uf->addAliasFilter($user);
387 return 'al' . $sub . '.alias IS NOT NULL';
388 }
389 }
390 }
391 // }}}
392
393 // {{{ class UFC_EmailList
394 /** Filters users based on an email list
395 * @param $emails List of emails whose owner must be selected
396 */
397 class UFC_EmailList implements UserFilterCondition
398 {
399 private $emails;
400 public function __construct($emails)
401 {
402 $this->emails = $emails;
403 }
404
405 public function buildCondition(UserFilter &$uf)
406 {
407 $email = null;
408 $virtual = null;
409 $alias = null;
410 $cond = array();
411
412 if (count($this->emails) == 0) {
413 return UserFilterCondition::COND_TRUE;
414 }
415
416 foreach ($this->emails as $entry) {
417 if (User::isForeignEmailAddress($entry)) {
418 if (is_null($email)) {
419 $email = $uf->addEmailRedirectFilter();
420 }
421 $cond[] = XDB::format('e' . $email . '.email = {?} OR a.email = {?}', $entry, $entry);
422 } else if (User::isVirtualEmailAddress($entry)) {
423 if (is_null($virtual)) {
424 $virtual = $uf->addVirtualEmailFilter();
425 }
426 $cond[] = XDB::format('vr' . $virtual . '.redirect IS NOT NULL AND v' . $virtual . '.alias = {?}', $entry);
427 } else {
428 if (is_null($alias)) {
429 $alias = $uf->addAliasFilter();
430 }
431 @list($user, $domain) = explode('@', $entry);
432 $cond[] = XDB::format('al' . $alias . '.alias = {?}', $user);
433 }
434 }
435 return '(' . implode(') OR (', $cond) . ')';
436 }
437 }
438 // }}}
439
440 // {{{ class UFC_Address
441 /** Filters users based on their address
442 * @param $text Text for filter in fulltext search
443 * @param $textSearchMode Mode for search (PREFIX, SUFFIX, ...)
444 * @param $type Filter on address type
445 * @param $flags Filter on address flags
446 * @param $countryId Filter on address countryId
447 * @param $administrativeAreaId Filter on address administrativeAreaId
448 * @param $subAdministrativeAreaId Filter on address subAdministrativeAreaId
449 * @param $localityId Filter on address localityId
450 * @param $postalCode Filter on address postalCode
451 */
452 class UFC_Address implements UserFilterCondition
453 {
454 /** Flags for text search
455 */
456 const PREFIX = 0x0001;
457 const SUFFIX = 0x0002;
458 const CONTAINS = 0x0003;
459
460 /** Valid address type ('hq' is reserved for company addresses)
461 */
462 const TYPE_HOME = 'home';
463 const TYPE_PRO = 'job';
464
465 /** Flags for addresses
466 */
467 const FLAG_CURRENT = 0x0001;
468 const FLAG_TEMP = 0x0002;
469 const FLAG_SECOND = 0x0004;
470 const FLAG_MAIL = 0x0008;
471 const FLAG_CEDEX = 0x0010;
472
473 // Binary OR of those flags
474 const FLAG_ANY = 0x001F;
475
476 /** Text of these flags
477 */
478 private static $flagtexts = array(
479 self::FLAG_CURRENT => 'current',
480 self::FLAG_TEMP => 'temporary',
481 self::FLAG_SECOND => 'secondary',
482 self::FLAG_MAIL => 'mail',
483 self::FLAG_CEDEX => 'cedex',
484 );
485
486 /** Data of the filter
487 */
488 private $text;
489 private $type;
490 private $flags;
491 private $countryId;
492 private $administrativeAreaId;
493 private $subAdministrativeAreaId;
494 private $localityId;
495 private $postalCode;
496
497 private $textSearchMode;
498
499 public function __construct($text = null, $textSearchMode = self::CONTAINS,
500 $type = null, $flags = self::FLAG_ANY, $countryId = null, $administrativeAreaId = null,
501 $subAdministrativeAreaId = null, $localityId = null, $postalCode = null)
502 {
503 $this->text = $text;
504 $this->textSearchMode = $textSearchMode;
505 $this->type = $type;
506 $this->flags = $flags;
507 $this->countryId = $countryId;
508 $this->administrativeAreaId = $administrativeAreaId;
509 $this->subAdministrativeAreaId = $subAdministrativeAreaId;
510 $this->localityId = $localityId;
511 $this->postalCode = $postalCode;
512 }
513
514 public function buildCondition(UserFilter &$uf)
515 {
516 $sub = $uf->addAddressFilter();
517 $conds = array();
518 if ($this->text != null) {
519 $left = $sub . '.text ';
520 $op = ' LIKE ';
521 if (($this->textSearchMode & self::CONTAINS) == 0) {
522 $right = XDB::format('{?}', $this->text);
523 $op = ' = ';
524 } else if (($this->mode & self::CONTAINS) == self::PREFIX) {
525 $right = XDB::format('CONCAT({?}, \'%\')', $this->text);
526 } else if (($this->mode & self::CONTAINS) == self::SUFFIX) {
527 $right = XDB::format('CONCAT(\'%\', {?})', $this->text);
528 } else {
529 $right = XDB::format('CONCAT(\'%\', {?}, \'%\')', $this->text);
530 }
531 $conds[] = $left . $op . $right;
532 }
533
534 if ($this->type != null) {
535 $conds[] = $sub . '.type = ' . XDB::format('{?}', $this->type);
536 }
537
538 if ($this->flags != self::FLAG_ANY) {
539 foreach(self::$flagtexts as $flag => $text) {
540 if ($flag & $this->flags) {
541 $conds[] = 'FIND_IN_SET(' . XDB::format('{?}', $text) . ', ' . $sub . '.flags)';
542 }
543 }
544 }
545
546 if ($this->countryId != null) {
547 $conds[] = $sub . '.countryId = ' . XDB::format('{?}', $this->countryId);
548 }
549 if ($this->administrativeAreaId != null) {
550 $conds[] = $sub . '.administrativeAreaId = ' . XDB::format('{?}', $this->administrativeAreaId);
551 }
552 if ($this->subAdministrativeAreaId != null) {
553 $conds[] = $sub . '.subAdministrativeAreaId = ' . XDB::format('{?}', $this->subAdministrativeAreaId);
554 }
555 if ($this->localityId != null) {
556 $conds[] = $sub . '.localityId = ' . XDB::format('{?}', $this->localityId);
557 }
558 if ($this->postalCode != null) {
559 $conds[] = $sub . '.postalCode = ' . XDB::format('{?}', $this->postalCode);
560 }
561
562 return implode(' AND ', $conds);
563 }
564 }
565 // }}}
566
567 // {{{ class UFC_Corps
568 /** Filters users based on the corps they belong to
569 * @param $corps Corps we are looking for (abbreviation)
570 * @param $type Whether we search for original or current corps
571 */
572 class UFC_Corps implements UserFilterCondition
573 {
574 const CURRENT = 1;
575 const ORIGIN = 2;
576
577 private $corps;
578 private $type;
579
580 public function __construct($corps, $type = self::CURRENT)
581 {
582 $this->corps = $corps;
583 $this->type = $type;
584 }
585
586 public function buildCondition(UserFilter &$uf)
587 {
588 /** Tables shortcuts:
589 * pc for profile_corps,
590 * pceo for profile_corps_enum - orginal
591 * pcec for profile_corps_enum - current
592 */
593 $sub = $uf->addCorpsFilter($this->type);
594 $cond = $sub . '.abbreviation = ' . $corps;
595 return $cond;
596 }
597 }
598 // }}}
599
600 // {{{ class UFC_Corps_Rank
601 /** Filters users based on their rank in the corps
602 * @param $rank Rank we are looking for (abbreviation)
603 */
604 class UFC_Corps_Rank implements UserFilterCondition
605 {
606 private $rank;
607 public function __construct($rank)
608 {
609 $this->rank = $rank;
610 }
611
612 public function buildCondition(UserFilter &$uf)
613 {
614 /** Tables shortcuts:
615 * pcr for profile_corps_rank
616 */
617 $sub = $uf->addCorpsRankFilter();
618 $cond = $sub . '.abbreviation = ' . $rank;
619 return $cond;
620 }
621 }
622 // }}}
623
624 // {{{ class UFC_Job_Company
625 /** Filters users based on the company they belong to
626 * @param $type The field being searched (self::JOBID, self::JOBNAME or self::JOBACRONYM)
627 * @param $value The searched value
628 */
629 class UFC_Job_Company implements UserFilterCondition
630 {
631 const JOBID = 'id';
632 const JOBNAME = 'name';
633 const JOBACRONYM = 'acronym';
634
635 private $type;
636 private $value;
637
638 public function __construct($type, $value)
639 {
640 $this->assertType($type);
641 $this->type = $type;
642 $this->value = $value;
643 }
644
645 private function assertType($type)
646 {
647 if ($type != self::JOBID && $type != self::JOBNAME && $type != self::JOBACRONYM) {
648 Platal::page()->killError("Type de recherche non valide.");
649 }
650 }
651
652 public function buildCondition(UserFilter &$uf)
653 {
654 $sub = $uf->addJobCompanyFilter();
655 $cond = $sub . '.' . $this->type . ' = ' . XDB::format('{?}', $this->value);
656 return $cond;
657 }
658 }
659 // }}}
660
661 // {{{ class UFC_Job_Sectorization
662 /** Filters users based on the ((sub)sub)sector they work in
663 * @param $sector The sector searched
664 * @param $subsector The subsector
665 * @param $subsubsector The subsubsector
666 */
667 class UFC_Job_Sectorization implements UserFilterCondition
668 {
669
670 private $sector;
671 private $subsector;
672 private $subsubsector;
673
674 public function __construct($sector = null, $subsector = null, $subsubsector = null)
675 {
676 $this->sector = $sector;
677 $this->subsector = $subsector;
678 $this->subsubsector = $subsubsector;
679 }
680
681 public function buildCondition(UserFilter &$uf)
682 {
683 // No need to add the JobFilter, it will be done by addJobSectorizationFilter
684 $conds = array();
685 if ($this->sector !== null) {
686 $sub = $uf->addJobSectorizationFilter(UserFilter::JOB_SECTOR);
687 $conds[] = $sub . '.id = ' . XDB::format('{?}', $this->sector);
688 }
689 if ($this->subsector !== null) {
690 $sub = $uf->addJobSectorizationFilter(UserFilter::JOB_SUBSECTOR);
691 $conds[] = $sub . '.id = ' . XDB::format('{?}', $this->subsector);
692 }
693 if ($this->subsubsector !== null) {
694 $sub = $uf->addJobSectorizationFilter(UserFilter::JOB_SUBSUBSECTOR);
695 $conds[] = $sub . '.id = ' . XDB::format('{?}', $this->subsubsector);
696 }
697 return implode(' AND ', $conds);
698 }
699 }
700 // }}}
701
702 // {{{ class UFC_Job_Description
703 /** Filters users based on their job description
704 * @param $description The text being searched for
705 * @param $fields The fields to search for (user-defined, ((sub|)sub|)sector)
706 */
707 class UFC_Job_Description implements UserFilterCondition
708 {
709
710 /** Meta-filters
711 * Built with binary OR on UserFilter::JOB_*
712 */
713 const ANY = 31;
714 const SECTORIZATION = 15;
715
716 private $description;
717 private $fields;
718
719 public function __construct($description)
720 {
721 $this->fields = $fields;
722 $this->description = $description;
723 }
724
725 public function buildCondition(UserFilter &$uf)
726 {
727 $conds = array();
728 if ($this->fields & UserFilter::JOB_USERDEFINED) {
729 $sub = $uf->addJobFilter();
730 $conds[] = $sub . '.description LIKE ' . XDB::format('CONCAT(\'%\', {?}, \'%\')', $this->description);
731 }
732 if ($this->fields & UserFilter::JOB_SECTOR) {
733 $sub = $uf->addJobSectorizationFilter(UserFilter::JOB_SECTOR);
734 $conds[] = $sub . '.name LIKE ' . XDB::format('CONCAT(\'%\', {?}, \'%\')', $this->description);
735 }
736 if ($this->fields & UserFilter::JOB_SUBSECTOR) {
737 $sub = $uf->addJobSectorizationFilter(UserFilter::JOB_SUBSECTOR);
738 $conds[] = $sub . '.name LIKE ' . XDB::format('CONCAT(\'%\', {?}, \'%\')', $this->description);
739 }
740 if ($this->fields & UserFilter::JOB_SUBSUBSECTOR) {
741 $sub = $uf->addJobSectorizationFilter(UserFilter::JOB_SUBSUBSECTOR);
742 $conds[] = $sub . '.name LIKE ' . XDB::format('CONCAT(\'%\', {?}, \'%\')', $this->description);
743 $sub = $uf->addJobSectorizationFilter(UserFilter::JOB_ALTERNATES);
744 $conds[] = $sub . '.name LIKE ' . XDB::format('CONCAT(\'%\', {?}, \'%\')', $this->description);
745 }
746 return implode(' OR ', $conds);
747 }
748 }
749 // }}}
750
751 // {{{ class UFC_Networking
752 /** Filters users based on network identity (IRC, ...)
753 * @param $type Type of network (-1 for any)
754 * @param $value Value to search
755 */
756 class UFC_Networking implements UserFilterCondition
757 {
758 private $type;
759 private $value;
760
761 public function __construct($type, $value)
762 {
763 $this->type = $type;
764 $this->value = $value;
765 }
766
767 public function buildCondition(UserFilter &$uf)
768 {
769 $sub = $uf->addNetworkingFilter();
770 $conds = array();
771 $conds[] = $sub . '.address = ' . XDB::format('CONCAT(\'%\', {?}, \'%\')', $this->value);
772 if ($this->type != -1) {
773 $conds[] = $sub . '.network_type = ' . XDB::format('{?}', $this->type);
774 }
775 return implode(' AND ', $conds);
776 }
777 }
778 // }}}
779
780 // {{{ class UFC_Phone
781 /** Filters users based on their phone number
782 * @param $num_type Type of number (pro/user/home)
783 * @param $phone_type Type of phone (fixed/mobile/fax)
784 * @param $number Phone number
785 */
786 class UFC_Phone implements UserFilterCondition
787 {
788 const NUM_PRO = 'pro';
789 const NUM_USER = 'user';
790 const NUM_HOME = 'address';
791 const NUM_ANY = 'any';
792
793 const PHONE_FIXED = 'fixed';
794 const PHONE_MOBILE = 'mobile';
795 const PHONE_FAX = 'fax';
796 const PHONE_ANY = 'any';
797
798 private $num_type;
799 private $phone_type;
800 private $number;
801
802 public function __construct($number, $num_type = self::NUM_ANY, $phone_type = self::PHONE_ANY)
803 {
804 require_once('profil.func.inc.php');
805 $this->number = $number;
806 $this->num_type = $num_type;
807 $this->phone_type = format_phone_number($phone_type);
808 }
809
810 public function buildCondition(UserFilter &$uf)
811 {
812 $sub = $uf->addPhoneFilter();
813 $conds = array();
814 $conds[] = $sub . '.search_tel = ' . XDB::format('{?}', $this->number);
815 if ($this->num_type != self::NUM_ANY) {
816 $conds[] = $sub . '.link_type = ' . XDB::format('{?}', $this->num_type);
817 }
818 if ($this->phone_type != self::PHONE_ANY) {
819 $conds[] = $sub . '.tel_type = ' . XDB::format('{?}', $this->phone_type);
820 }
821 return implode(' AND ', $conds);
822 }
823 }
824 // }}}
825
826 // {{{ class UFC_Medal
827 /** Filters users based on their medals
828 * @param $medal ID of the medal
829 * @param $grade Grade of the medal (null for 'any')
830 */
831 class UFC_Medal implements UserFilterCondition
832 {
833 private $medal;
834 private $grade;
835
836 public function __construct($medal, $grade = null)
837 {
838 $this->medal = $medal;
839 $this->grade = $grade;
840 }
841
842 public function buildCondition(UserFilter &$uf)
843 {
844 $conds = array();
845 $sub = $uf->addMedalFilter();
846 $conds[] = $sub . '.mid = ' . XDB::format('{?}', $this->medal);
847 if ($this->grade != null) {
848 $conds[] = $sub . '.gid = ' . XDB::format('{?}', $this->grade);
849 }
850 return implode(' AND ', $conds);
851 }
852 }
853 // }}}
854
855 // {{{ class UFC_Mentor_Expertise
856 /** Filters users by mentoring expertise
857 * @param $expertise Domain of expertise
858 */
859 class UFC_Mentor_Expertise implements UserFilterCondition
860 {
861 private $expertise;
862
863 public function __construct($expertise)
864 {
865 $this->expertise = $expertise;
866 }
867
868 public function buildCondition(UserFilter &$uf)
869 {
870 $sub = $uf->addMentorFilter(UserFilter::MENTOR_EXPERTISE);
871 return $sub . '.expertise LIKE ' . XDB::format('CONCAT(\'%\', {?}, \'%\'', $this->expertise);
872 }
873 }
874 // }}}
875
876 // {{{ class UFC_Mentor_Country
877 /** Filters users by mentoring country
878 * @param $country Two-letters code of country being searched
879 */
880 class UFC_Mentor_Country implements UserFilterCondition
881 {
882 private $country;
883
884 public function __construct($country)
885 {
886 $this->country = $country;
887 }
888
889 public function buildCondition(UserFilter &$uf)
890 {
891 $sub = $uf->addMentorFilter(UserFilter::MENTOR_COUNTRY);
892 return $sub . '.country = ' . XDB::format('{?}', $this->country);
893 }
894 }
895 // }}}
896
897 // {{{ class UFC_Mentor_Sectorization
898 /** Filters users based on mentoring (sub|)sector
899 * @param $sector ID of sector
900 * @param $subsector Subsector (null for any)
901 */
902 class UFC_Mentor_Sectorization implements UserFilterCondition
903 {
904 private $sector;
905 private $subsector;
906
907 public function __construct($sector, $subsector = null)
908 {
909 $this->sector = $sector;
910 $this->subsubsector = $subsector;
911 }
912
913 public function buildCondition(UserFilter &$uf)
914 {
915 $sub = $uf->addMentorFilter(UserFilter::MENTOR_SECTOR);
916 $conds = array();
917 $conds[] = $sub . '.sectorid = ' . XDB::format('{?}', $this->sector);
918 if ($this->subsector != null) {
919 $conds[] = $sub . '.subsectorid = ' . XDB::format('{?}', $this->subsector);
920 }
921 return implode(' AND ', $conds);
922 }
923 }
924 // }}}
925
926 // {{{ class UFC_UserRelated
927 /** Filters users based on a relation toward a user
928 * @param $user User to which searched users are related
929 */
930 abstract class UFC_UserRelated implements UserFilterCondition
931 {
932 protected $user;
933 public function __construct(PlUser &$user)
934 {
935 $this->user =& $user;
936 }
937 }
938 // }}}
939
940 // {{{ class UFC_Contact
941 /** Filters users who belong to selected user's contacts
942 */
943 class UFC_Contact extends UFC_UserRelated
944 {
945 public function buildCondition(UserFilter &$uf)
946 {
947 $sub = $uf->addContactFilter($this->user->id());
948 return 'c' . $sub . '.contact IS NOT NULL';
949 }
950 }
951 // }}}
952
953 // {{{ class UFC_WatchRegistration
954 /** Filters users being watched by selected user
955 */
956 class UFC_WatchRegistration extends UFC_UserRelated
957 {
958 public function buildCondition(UserFilter &$uf)
959 {
960 if (!$this->user->watch('registration')) {
961 return UserFilterCondition::COND_FALSE;
962 }
963 $uids = $this->user->watchUsers();
964 if (count($uids) == 0) {
965 return UserFilterCondition::COND_FALSE;
966 } else {
967 return '$UID IN ' . XDB::formatArray($uids);
968 }
969 }
970 }
971 // }}}
972
973 // {{{ class UFC_WatchPromo
974 /** Filters users belonging to a promo watched by selected user
975 * @param $user Selected user (the one watching promo)
976 * @param $grade Formation the user is watching
977 */
978 class UFC_WatchPromo extends UFC_UserRelated
979 {
980 private $grade;
981 public function __construct(PlUser &$user, $grade = UserFilter::GRADE_ING)
982 {
983 parent::__construct($user);
984 $this->grade = $grade;
985 }
986
987 public function buildCondition(UserFilter &$uf)
988 {
989 $promos = $this->user->watchPromos();
990 if (count($promos) == 0) {
991 return UserFilterCondition::COND_FALSE;
992 } else {
993 $sube = $uf->addEducationFilter(true, $this->grade);
994 $field = 'pe' . $sube . '.' . UserFilter::promoYear($this->grade);
995 return $field . ' IN ' . XDB::formatArray($promos);
996 }
997 }
998 }
999 // }}}
1000
1001 // {{{ class UFC_WatchContact
1002 /** Filters users watched by selected user
1003 */
1004 class UFC_WatchContact extends UFC_Contact
1005 {
1006 public function buildCondition(UserFilter &$uf)
1007 {
1008 if (!$this->user->watchContacts()) {
1009 return UserFilterCondition::COND_FALSE;
1010 }
1011 return parent::buildCondition($uf);
1012 }
1013 }
1014 // }}}
1015
1016
1017 /******************
1018 * ORDERS
1019 ******************/
1020
1021 // {{{ class UserFilterOrder
1022 /** Base class for ordering results of a query.
1023 * Parameters for the ordering must be given to the constructor ($desc for a
1024 * descending order).
1025 * The getSortTokens function is used to get actual ordering part of the query.
1026 */
1027 abstract class UserFilterOrder extends PlFilterOrder
1028 {
1029 /** This function must return the tokens to use for ordering
1030 * @param &$uf The UserFilter whose results must be ordered
1031 * @return The name of the field to use for ordering results
1032 */
1033 abstract protected function getSortTokens(UserFilter &$uf);
1034 }
1035 // }}}
1036
1037 // {{{ class UFO_Promo
1038 /** Orders users by promotion
1039 * @param $grade Formation whose promotion users should be sorted by (restricts results to users of that formation)
1040 * @param $desc Whether sort is descending
1041 */
1042 class UFO_Promo extends UserFilterOrder
1043 {
1044 private $grade;
1045
1046 public function __construct($grade = null, $desc = false)
1047 {
1048 parent::__construct($desc);
1049 $this->grade = $grade;
1050 }
1051
1052 protected function getSortTokens(UserFilter &$uf)
1053 {
1054 if (UserFilter::isGrade($this->grade)) {
1055 $sub = $uf->addEducationFilter($this->grade);
1056 return 'pe' . $sub . '.' . UserFilter::promoYear($this->grade);
1057 } else {
1058 $sub = $uf->addDisplayFilter();
1059 return 'pd' . $sub . '.promo';
1060 }
1061 }
1062 }
1063 // }}}
1064
1065 // {{{ class UFO_Name
1066 /** Sorts users by name
1067 * @param $type Type of name on which to sort (firstname...)
1068 * @param $variant Variant of that name to use (marital, ordinary...)
1069 * @param $particle Set to true if particles should be included in the sorting order
1070 * @param $desc If sort order should be descending
1071 */
1072 class UFO_Name extends UserFilterOrder
1073 {
1074 private $type;
1075 private $variant;
1076 private $particle;
1077
1078 public function __construct($type, $variant = null, $particle = false, $desc = false)
1079 {
1080 parent::__construct($desc);
1081 $this->type = $type;
1082 $this->variant = $variant;
1083 $this->particle = $particle;
1084 }
1085
1086 protected function getSortTokens(UserFilter &$uf)
1087 {
1088 if (UserFilter::isDisplayName($this->type)) {
1089 $sub = $uf->addDisplayFilter();
1090 return 'pd' . $sub . '.' . $this->type;
1091 } else {
1092 $sub = $uf->addNameFilter($this->type, $this->variant);
1093 if ($this->particle) {
1094 return 'CONCAT(pn' . $sub . '.particle, \' \', pn' . $sub . '.name)';
1095 } else {
1096 return 'pn' . $sub . '.name';
1097 }
1098 }
1099 }
1100 }
1101 // }}}
1102
1103 // {{{ class UFO_Score
1104 class UFO_Score extends UserFilterOrder
1105 {
1106 protected function getSortTokens(UserFilter &$uf)
1107 {
1108 $sub = $uf->addNameTokensFilter();
1109 return 'SUM(' . $sub . '.score)';
1110 }
1111 }
1112 // }}}
1113
1114 // {{{ class UFO_Registration
1115 /** Sorts users based on registration date
1116 */
1117 class UFO_Registration extends UserFilterOrder
1118 {
1119 protected function getSortTokens(UserFilter &$uf)
1120 {
1121 return 'a.registration_date';
1122 }
1123 }
1124 // }}}
1125
1126 // {{{ class UFO_Birthday
1127 /** Sorts users based on next birthday date
1128 */
1129 class UFO_Birthday extends UserFilterOrder
1130 {
1131 protected function getSortTokens(UserFilter &$uf)
1132 {
1133 return 'p.next_birthday';
1134 }
1135 }
1136 // }}}
1137
1138 // {{{ class UFO_ProfileUpdate
1139 /** Sorts users based on last profile update
1140 */
1141 class UFO_ProfileUpdate extends UserFilterOrder
1142 {
1143 protected function getSortTokens(UserFilter &$uf)
1144 {
1145 return 'p.last_change';
1146 }
1147 }
1148 // }}}
1149
1150 // {{{ class UFO_Death
1151 /** Sorts users based on death date
1152 */
1153 class UFO_Death extends UserFilterOrder
1154 {
1155 protected function getSortTokens(UserFilter &$uf)
1156 {
1157 return 'p.deathdate';
1158 }
1159 }
1160 // }}}
1161
1162
1163 /***********************************
1164 *********************************
1165 USER FILTER CLASS
1166 *********************************
1167 ***********************************/
1168
1169 // {{{ class UserFilter
1170 /** This class provides a convenient and centralized way of filtering users.
1171 *
1172 * Usage:
1173 * $uf = new UserFilter(new UFC_Blah($x, $y), new UFO_Coin($z, $t));
1174 *
1175 * Resulting UserFilter can be used to:
1176 * - get a list of User objects matching the filter
1177 * - get a list of UIDs matching the filter
1178 * - get the number of users matching the filter
1179 * - check whether a given User matches the filter
1180 * - filter a list of User objects depending on whether they match the filter
1181 *
1182 * Usage for UFC and UFO objects:
1183 * A UserFilter will call all private functions named XXXJoins.
1184 * These functions must return an array containing the list of join
1185 * required by the various UFC and UFO associated to the UserFilter.
1186 * Entries in those returned array are of the following form:
1187 * 'join_tablealias' => array('join_type', 'joined_table', 'join_criter')
1188 * which will be translated into :
1189 * join_type JOIN joined_table AS join_tablealias ON (join_criter)
1190 * in the final query.
1191 *
1192 * In the join_criter text, $ME is replaced with 'join_tablealias', $PID with
1193 * profile.pid, and $UID with auth_user_md5.user_id.
1194 *
1195 * For each kind of "JOIN" needed, a function named addXXXFilter() should be defined;
1196 * its parameter will be used to set various private vars of the UserFilter describing
1197 * the required joins ; such a function shall return the "join_tablealias" to use
1198 * when referring to the joined table.
1199 *
1200 * For example, if data from profile_job must be available to filter results,
1201 * the UFC object will call $uf-addJobFilter(), which will set the 'with_pj' var and
1202 * return 'pj', the short name to use when referring to profile_job; when building
1203 * the query, calling the jobJoins function will return an array containing a single
1204 * row:
1205 * 'pj' => array('left', 'profile_job', '$ME.pid = $UID');
1206 *
1207 * The 'register_optional' function can be used to generate unique table aliases when
1208 * the same table has to be joined several times with different aliases.
1209 */
1210 class UserFilter extends PlFilter
1211 {
1212 protected $joinMethods = array();
1213
1214 protected $joinMetas = array('$PID' => 'p.pid',
1215 '$UID' => 'a.uid',
1216 );
1217
1218 private $root;
1219 private $sort = array();
1220 private $query = null;
1221 private $orderby = null;
1222
1223 private $lastcount = null;
1224
1225 public function __construct($cond = null, $sort = null)
1226 {
1227 if (empty($this->joinMethods)) {
1228 $class = new ReflectionClass('UserFilter');
1229 foreach ($class->getMethods() as $method) {
1230 $name = $method->getName();
1231 if (substr($name, -5) == 'Joins' && $name != 'buildJoins') {
1232 $this->joinMethods[] = $name;
1233 }
1234 }
1235 }
1236 if (!is_null($cond)) {
1237 if ($cond instanceof PlFilterCondition) {
1238 $this->setCondition($cond);
1239 }
1240 }
1241 if (!is_null($sort)) {
1242 if ($sort instanceof UserFilterOrder) {
1243 $this->addSort($sort);
1244 } else if (is_array($sort)) {
1245 foreach ($sort as $s) {
1246 $this->addSort($s);
1247 }
1248 }
1249 }
1250 }
1251
1252 private function buildQuery()
1253 {
1254 if (is_null($this->orderby)) {
1255 $orders = array();
1256 foreach ($this->sort as $sort) {
1257 $orders = array_merge($orders, $sort->buildSort($this));
1258 }
1259 if (count($orders) == 0) {
1260 $this->orderby = '';
1261 } else {
1262 $this->orderby = 'ORDER BY ' . implode(', ', $orders);
1263 }
1264 }
1265 if (is_null($this->query)) {
1266 $where = $this->root->buildCondition($this);
1267 if ($this->with_forced_sn) {
1268 $this->requireProfiles();
1269 $from = 'search_name AS sn';
1270 } else if ($this->with_accounts) {
1271 $from = 'accounts AS a';
1272 } else {
1273 $this->requireProfiles();
1274 $from = 'profiles AS p';
1275 }
1276 $joins = $this->buildJoins();
1277 $this->query = 'FROM ' . $from . '
1278 ' . $joins . '
1279 WHERE (' . $where . ')';
1280 }
1281 }
1282
1283 private function getUIDList($uids = null, PlLimit &$limit)
1284 {
1285 $this->requireAccounts();
1286 $this->buildQuery();
1287 $lim = $limit->getSql();
1288 $cond = '';
1289 if (!is_null($uids)) {
1290 $cond = ' AND a.uid IN ' . XDB::formatArray($uids);
1291 }
1292 $fetched = XDB::fetchColumn('SELECT SQL_CALC_FOUND_ROWS a.uid
1293 ' . $this->query . $cond . '
1294 GROUP BY a.uid
1295 ' . $this->orderby . '
1296 ' . $lim);
1297 $this->lastcount = (int)XDB::fetchOneCell('SELECT FOUND_ROWS()');
1298 return $fetched;
1299 }
1300
1301 private function getPIDList($pids = null, PlLimit &$limit)
1302 {
1303 $this->requireProfiles();
1304 $this->buildQuery();
1305 $lim = $limit->getSql();
1306 $cond = '';
1307 if (!is_null($pids)) {
1308 $cond = ' AND p.pid IN ' . XDB::formatArray($pids);
1309 }
1310 $fetched = XDB::fetchColumn('SELECT SQL_CALC_FOUND_ROWS p.pid
1311 ' . $this->query . $cond . '
1312 GROUP BY p.pid
1313 ' . $this->orderby . '
1314 ' . $lim);
1315 $this->lastcount = (int)XDB::fetchOneCell('SELECT FOUND_ROWS()');
1316 return $fetched;
1317 }
1318
1319 /** Check that the user match the given rule.
1320 */
1321 public function checkUser(PlUser &$user)
1322 {
1323 $this->requireAccounts();
1324 $this->buildQuery();
1325 $count = (int)XDB::fetchOneCell('SELECT COUNT(*)
1326 ' . $this->query . XDB::format(' AND a.uid = {?}', $user->id()));
1327 return $count == 1;
1328 }
1329
1330 /** Check that the profile match the given rule.
1331 */
1332 public function checkProfile(Profile &$profile)
1333 {
1334 $this->requireProfiles();
1335 $this->buildQuery();
1336 $count = (int)XDB::fetchOneCell('SELECT COUNT(*)
1337 ' . $this->query . XDB::format(' AND p.pid = {?}', $profile->id()));
1338 return $count == 1;
1339 }
1340
1341 /** Default filter is on users
1342 */
1343 public function filter(array $users, PlLimit &$limit)
1344 {
1345 return $this->filterUsers($users, $limit);
1346 }
1347
1348 /** Filter a list of users to extract the users matching the rule.
1349 */
1350 public function filterUsers(array $users, PlLimit &$limit)
1351 {
1352 $this->requireAccounts();
1353 $this->buildQuery();
1354 $table = array();
1355 $uids = array();
1356 foreach ($users as $user) {
1357 if ($user instanceof PlUser) {
1358 $uid = $user->id();
1359 } else {
1360 $uid = $user;
1361 }
1362 $uids[] = $uid;
1363 $table[$uid] = $user;
1364 }
1365 $fetched = $this->getUIDList($uids, $limit);
1366 $output = array();
1367 foreach ($fetched as $uid) {
1368 $output[] = $table[$uid];
1369 }
1370 return $output;
1371 }
1372
1373 /** Filter a list of profiles to extract the users matching the rule.
1374 */
1375 public function filterProfiles(array $profiles, PlLimit &$limit)
1376 {
1377 $this->requireProfiles();
1378 $this->buildQuery();
1379 $table = array();
1380 $pids = array();
1381 foreach ($profiles as $profile) {
1382 if ($profile instanceof Profile) {
1383 $pid = $profile->id();
1384 } else {
1385 $pid = $profile;
1386 }
1387 $pids[] = $pid;
1388 $table[$pid] = $profile;
1389 }
1390 $fetched = $this->getPIDList($pids, $limit);
1391 $output = array();
1392 foreach ($fetched as $pid) {
1393 $output[] = $table[$pid];
1394 }
1395 return $output;
1396 }
1397
1398 public function getUIDs(PlLimit &$limit)
1399 {
1400 return $this->getUIDList(null, $limit);
1401 }
1402
1403 public function getPIDs(PlLimit &$limit)
1404 {
1405 return $this->getPIDList(null, $limit);
1406 }
1407
1408 public function getUsers(PlLimit &$limit)
1409 {
1410 return User::getBulkUsersWithUIDs($this->getUIDs($limit));
1411 }
1412
1413 public function getProfiles(PlLimit &$limit)
1414 {
1415 return Profile::getBulkProfilesWithPIDs($this->getPIDs($limit));
1416 }
1417
1418 public function get(PlLimit &$limit)
1419 {
1420 return $this->getUsers($limit);
1421 }
1422
1423 public function getTotalCount()
1424 {
1425 if (is_null($this->lastcount)) {
1426 $this->buildQuery();
1427 if ($this->requireAccounts()) {
1428 $field = 'a.uid';
1429 } else {
1430 $field = 'p.pid';
1431 }
1432 return (int)XDB::fetchOneCell('SELECT COUNT(DISTINCT ' . $field . ')
1433 ' . $this->query);
1434 } else {
1435 return $this->lastcount;
1436 }
1437 }
1438
1439 public function setCondition(PlFilterCondition &$cond)
1440 {
1441 $this->root =& $cond;
1442 $this->query = null;
1443 }
1444
1445 public function addSort(PlFilterOrder &$sort)
1446 {
1447 $this->sort[] = $sort;
1448 $this->orderby = null;
1449 }
1450
1451 static public function getLegacy($promo_min, $promo_max)
1452 {
1453 if ($promo_min != 0) {
1454 $min = new UFC_Promo('>=', self::GRADE_ING, intval($promo_min));
1455 } else {
1456 $min = new UFC_True();
1457 }
1458 if ($promo_max != 0) {
1459 $max = new UFC_Promo('<=', self::GRADE_ING, intval($promo_max));
1460 } else {
1461 $max = new UFC_True();
1462 }
1463 return new UserFilter(new PFC_And($min, $max));
1464 }
1465
1466 static public function sortByName()
1467 {
1468 return array(new UFO_Name(self::LASTNAME), new UFO_Name(self::FIRSTNAME));
1469 }
1470
1471 static public function sortByPromo()
1472 {
1473 return array(new UFO_Promo(), new UFO_Name(self::LASTNAME), new UFO_Name(self::FIRSTNAME));
1474 }
1475
1476 static private function getDBSuffix($string)
1477 {
1478 return preg_replace('/[^a-z0-9]/i', '', $string);
1479 }
1480
1481
1482 /** Stores a new (and unique) table alias in the &$table table
1483 * @param &$table Array in which the table alias must be stored
1484 * @param $val Value which will then be used to build the join
1485 * @return Name of the newly created alias
1486 */
1487 private $option = 0;
1488 private function register_optional(array &$table, $val)
1489 {
1490 if (is_null($val)) {
1491 $sub = $this->option++;
1492 $index = null;
1493 } else {
1494 $sub = self::getDBSuffix($val);
1495 $index = $val;
1496 }
1497 $sub = '_' . $sub;
1498 $table[$sub] = $index;
1499 return $sub;
1500 }
1501
1502 /** PROFILE VS ACCOUNT
1503 */
1504 private $with_profiles = false;
1505 private $with_accounts = false;
1506 private $with_forced_sn = false;
1507 public function requireAccounts()
1508 {
1509 $this->with_accounts = true;
1510 }
1511
1512 public function requireProfiles()
1513 {
1514 $this->with_profiles = true;
1515 }
1516
1517 /** Forces the "FROM" to use search_name instead of accounts or profiles */
1518 public function forceSearchName()
1519 {
1520 $this->with_forced_sn = true;
1521 }
1522
1523 protected function accountJoins()
1524 {
1525 $joins = array();
1526 /** Quick search is much more efficient with sn first and PID second */
1527 if ($this->with_forced_sn) {
1528 $joins['p'] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'profiles', '$PID = sn.uid');
1529 if ($this->with_accounts) {
1530 $joins['ap'] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'account_profiles', '$ME.pid = $PID');
1531 $joins['a'] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'accounts', '$UID = ap.uid');
1532 }
1533 } else if ($this->with_profiles && $this->with_accounts) {
1534 $joins['ap'] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'account_profiles', '$ME.uid = $UID AND FIND_IN_SET(\'owner\', ap.perms)');
1535 $joins['p'] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'profiles', '$PID = ap.pid');
1536 }
1537 return $joins;
1538 }
1539
1540 /** DISPLAY
1541 */
1542 const DISPLAY = 'display';
1543 private $pd = false;
1544 public function addDisplayFilter()
1545 {
1546 $this->requireProfiles();
1547 $this->pd = true;
1548 return '';
1549 }
1550
1551 protected function displayJoins()
1552 {
1553 if ($this->pd) {
1554 return array('pd' => new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'profile_display', '$ME.pid = $PID'));
1555 } else {
1556 return array();
1557 }
1558 }
1559
1560 /** NAMES
1561 */
1562 /* name tokens */
1563 const LASTNAME = 'lastname';
1564 const FIRSTNAME = 'firstname';
1565 const NICKNAME = 'nickname';
1566 const PSEUDONYM = 'pseudonym';
1567 const NAME = 'name';
1568 /* name variants */
1569 const VN_MARITAL = 'marital';
1570 const VN_ORDINARY = 'ordinary';
1571 const VN_OTHER = 'other';
1572 const VN_INI = 'ini';
1573 /* display names */
1574 const DN_FULL = 'directory_name';
1575 const DN_DISPLAY = 'yourself';
1576 const DN_YOURSELF = 'yourself';
1577 const DN_DIRECTORY = 'directory_name';
1578 const DN_PRIVATE = 'private_name';
1579 const DN_PUBLIC = 'public_name';
1580 const DN_SHORT = 'short_name';
1581 const DN_SORT = 'sort_name';
1582
1583 static public $name_variants = array(
1584 self::LASTNAME => array(self::VN_MARITAL, self::VN_ORDINARY),
1585 self::FIRSTNAME => array(self::VN_ORDINARY, self::VN_INI, self::VN_OTHER)
1586 );
1587
1588 static public function assertName($name)
1589 {
1590 if (!Profile::getNameTypeId($name)) {
1591 Platal::page()->kill('Invalid name type: ' . $name);
1592 }
1593 }
1594
1595 static public function isDisplayName($name)
1596 {
1597 return $name == self::DN_FULL || $name == self::DN_DISPLAY
1598 || $name == self::DN_YOURSELF || $name == self::DN_DIRECTORY
1599 || $name == self::DN_PRIVATE || $name == self::DN_PUBLIC
1600 || $name == self::DN_SHORT || $name == self::DN_SORT;
1601 }
1602
1603 private $pn = array();
1604 public function addNameFilter($type, $variant = null)
1605 {
1606 $this->requireProfiles();
1607 if (!is_null($variant)) {
1608 $ft = $type . '_' . $variant;
1609 } else {
1610 $ft = $type;
1611 }
1612 $sub = '_' . $ft;
1613 self::assertName($ft);
1614
1615 if (!is_null($variant) && $variant == 'other') {
1616 $sub .= $this->option++;
1617 }
1618 $this->pn[$sub] = Profile::getNameTypeId($ft);
1619 return $sub;
1620 }
1621
1622 protected function nameJoins()
1623 {
1624 $joins = array();
1625 foreach ($this->pn as $sub => $type) {
1626 $joins['pn' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'profile_name', '$ME.pid = $PID AND $ME.typeid = ' . $type);
1627 }
1628 return $joins;
1629 }
1630
1631 /** NAMETOKENS
1632 */
1633 private $with_sn = false;
1634 // Set $doingQuickSearch to true if you wish to optimize the query
1635 public function addNameTokensFilter($doingQuickSearch = false)
1636 {
1637 $this->requireProfiles();
1638 $this->with_forced_sn = ($this->with_forced_sn || $doingQuickSearch);
1639 $this->with_sn = true;
1640 return 'sn';
1641 }
1642
1643 protected function nameTokensJoins()
1644 {
1645 /* We don't return joins, since with_sn forces the SELECT to run on search_name first */
1646 if ($this->with_sn && !$this->with_forced_sn) {
1647 return array(
1648 'sn' => new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'search_name', '$ME.uid = $PID')
1649 );
1650 } else {
1651 return array();
1652 }
1653 }
1654
1655 /** EDUCATION
1656 */
1657 const GRADE_ING = 'Ing.';
1658 const GRADE_PHD = 'PhD';
1659 const GRADE_MST = 'M%';
1660 static public function isGrade($grade)
1661 {
1662 return $grade == self::GRADE_ING || $grade == self::GRADE_PHD || $grade == self::GRADE_MST;
1663 }
1664
1665 static public function assertGrade($grade)
1666 {
1667 if (!self::isGrade($grade)) {
1668 Platal::page()->killError("Diplôme non valide");
1669 }
1670 }
1671
1672 static public function promoYear($grade)
1673 {
1674 // XXX: Definition of promotion for phds and masters might change in near future.
1675 return ($grade == UserFilter::GRADE_ING) ? 'entry_year' : 'grad_year';
1676 }
1677
1678 private $pepe = array();
1679 private $with_pee = false;
1680 public function addEducationFilter($x = false, $grade = null)
1681 {
1682 $this->requireProfiles();
1683 if (!$x) {
1684 $index = $this->option;
1685 $sub = $this->option++;
1686 } else {
1687 self::assertGrade($grade);
1688 $index = $grade;
1689 $sub = $grade[0];
1690 $this->with_pee = true;
1691 }
1692 $sub = '_' . $sub;
1693 $this->pepe[$index] = $sub;
1694 return $sub;
1695 }
1696
1697 protected function educationJoins()
1698 {
1699 $joins = array();
1700 if ($this->with_pee) {
1701 $joins['pee'] = new PlSqlJoin(PlSqlJoin::MODE_INNER, 'profile_education_enum', 'pee.abbreviation = \'X\'');
1702 }
1703 foreach ($this->pepe as $grade => $sub) {
1704 if ($this->isGrade($grade)) {
1705 $joins['pe' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'profile_education', '$ME.eduid = pee.id AND $ME.uid = $PID');
1706 $joins['pede' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_INNER, 'profile_education_degree_enum', '$ME.id = pe' . $sub . '.degreeid AND $ME.abbreviation LIKE ' .
1707 XDB::format('{?}', $grade));
1708 } else {
1709 $joins['pe' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'profile_education', '$ME.uid = $PID');
1710 $joins['pee' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_INNER, 'profile_education_enum', '$ME.id = pe' . $sub . '.eduid');
1711 $joins['pede' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_INNER, 'profile_education_degree_enum', '$ME.id = pe' . $sub . '.degreeid');
1712 }
1713 }
1714 return $joins;
1715 }
1716
1717
1718 /** GROUPS
1719 */
1720 private $gpm = array();
1721 public function addGroupFilter($group = null)
1722 {
1723 $this->requireAccounts();
1724 if (!is_null($group)) {
1725 if (ctype_digit($group)) {
1726 $index = $sub = $group;
1727 } else {
1728 $index = $group;
1729 $sub = self::getDBSuffix($group);
1730 }
1731 } else {
1732 $sub = 'group_' . $this->option++;
1733 $index = null;
1734 }
1735 $sub = '_' . $sub;
1736 $this->gpm[$sub] = $index;
1737 return $sub;
1738 }
1739
1740 protected function groupJoins()
1741 {
1742 $joins = array();
1743 foreach ($this->gpm as $sub => $key) {
1744 if (is_null($key)) {
1745 $joins['gpa' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_INNER, 'groupex.asso');
1746 $joins['gpm' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'groupex.membres', '$ME.uid = $UID AND $ME.asso_id = gpa' . $sub . '.id');
1747 } else if (ctype_digit($key)) {
1748 $joins['gpm' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'groupex.membres', '$ME.uid = $UID AND $ME.asso_id = ' . $key);
1749 } else {
1750 $joins['gpa' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_INNER, 'groupex.asso', XDB::format('$ME.diminutif = {?}', $key));
1751 $joins['gpm' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'groupex.membres', '$ME.uid = $UID AND $ME.asso_id = gpa' . $sub . '.id');
1752 }
1753 }
1754 return $joins;
1755 }
1756
1757 /** EMAILS
1758 */
1759 private $e = array();
1760 public function addEmailRedirectFilter($email = null)
1761 {
1762 $this->requireAccounts();
1763 return $this->register_optional($this->e, $email);
1764 }
1765
1766 private $ve = array();
1767 public function addVirtualEmailFilter($email = null)
1768 {
1769 $this->addAliasFilter(self::ALIAS_FORLIFE);
1770 return $this->register_optional($this->ve, $email);
1771 }
1772
1773 const ALIAS_BEST = 'bestalias';
1774 const ALIAS_FORLIFE = 'forlife';
1775 private $al = array();
1776 public function addAliasFilter($alias = null)
1777 {
1778 $this->requireAccounts();
1779 return $this->register_optional($this->al, $alias);
1780 }
1781
1782 protected function emailJoins()
1783 {
1784 global $globals;
1785 $joins = array();
1786 foreach ($this->e as $sub=>$key) {
1787 if (is_null($key)) {
1788 $joins['e' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'emails', '$ME.uid = $UID AND $ME.flags != \'filter\'');
1789 } else {
1790 $joins['e' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'emails', XDB::format('$ME.uid = $UID AND $ME.flags != \'filter\' AND $ME.email = {?}', $key));
1791 }
1792 }
1793 foreach ($this->al as $sub=>$key) {
1794 if (is_null($key)) {
1795 $joins['al' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'aliases', '$ME.id = $UID AND $ME.type IN (\'alias\', \'a_vie\')');
1796 } else if ($key == self::ALIAS_BEST) {
1797 $joins['al' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'aliases', '$ME.id = $UID AND $ME.type IN (\'alias\', \'a_vie\') AND FIND_IN_SET(\'bestalias\', $ME.flags)');
1798 } else if ($key == self::ALIAS_FORLIFE) {
1799 $joins['al' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'aliases', '$ME.id = $UID AND $ME.type = \'a_vie\'');
1800 } else {
1801 $joins['al' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'aliases', XDB::format('$ME.id = $UID AND $ME.type IN (\'alias\', \'a_vie\') AND $ME.alias = {?}', $key));
1802 }
1803 }
1804 foreach ($this->ve as $sub=>$key) {
1805 if (is_null($key)) {
1806 $joins['v' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'virtual', '$ME.type = \'user\'');
1807 } else {
1808 $joins['v' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'virtual', XDB::format('$ME.type = \'user\' AND $ME.alias = {?}', $key));
1809 }
1810 $joins['vr' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'virtual_redirect', XDB::format('$ME.vid = v' . $sub . '.vid
1811 AND ($ME.redirect IN (CONCAT(al_forlife.alias, \'@\', {?}),
1812 CONCAT(al_forlife.alias, \'@\', {?}),
1813 a.email))',
1814 $globals->mail->domain, $globals->mail->domain2));
1815 }
1816 return $joins;
1817 }
1818
1819
1820 /** ADDRESSES
1821 */
1822 private $with_pa = false;
1823 public function addAddressFilter()
1824 {
1825 $this->requireProfiles();
1826 $this->with_pa = true;
1827 return 'pa';
1828 }
1829
1830 protected function addressJoins()
1831 {
1832 $joins = array();
1833 if ($this->with_pa) {
1834 $joins['pa'] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'profile_address', '$ME.pid = $PID');
1835 }
1836 return $joins;
1837 }
1838
1839
1840 /** CORPS
1841 */
1842
1843 private $pc = false;
1844 private $pce = array();
1845 private $pcr = false;
1846 public function addCorpsFilter($type)
1847 {
1848 $this->requireProfiles();
1849 $this->pc = true;
1850 if ($type == UFC_Corps::CURRENT) {
1851 $pce['pcec'] = 'current_corpsid';
1852 return 'pcec';
1853 } else if ($type == UFC_Corps::ORIGIN) {
1854 $pce['pceo'] = 'original_corpsid';
1855 return 'pceo';
1856 }
1857 }
1858
1859 public function addCorpsRankFilter()
1860 {
1861 $this->requireProfiles();
1862 $this->pc = true;
1863 $this->pcr = true;
1864 return 'pcr';
1865 }
1866
1867 protected function corpsJoins()
1868 {
1869 $joins = array();
1870 if ($this->pc) {
1871 $joins['pc'] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'profile_corps', '$ME.uid = $UID');
1872 }
1873 if ($this->pcr) {
1874 $joins['pcr'] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'profile_corps_rank_enum', '$ME.id = pc.rankid');
1875 }
1876 foreach($this->pce as $sub => $field) {
1877 $joins[$sub] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'profile_corps_enum', '$ME.id = pc.' . $field);
1878 }
1879 return $joins;
1880 }
1881
1882 /** JOBS
1883 */
1884
1885 const JOB_SECTOR = 1;
1886 const JOB_SUBSECTOR = 2;
1887 const JOB_SUBSUBSECTOR = 4;
1888 const JOB_ALTERNATES = 8;
1889 const JOB_USERDEFINED = 16;
1890
1891 /** Joins :
1892 * pj => profile_job
1893 * pje => profile_job_enum
1894 * pjse => profile_job_sector_enum
1895 * pjsse => profile_job_subsector_enum
1896 * pjssse => profile_job_subsubsector_enum
1897 * pja => profile_job_alternates
1898 */
1899 private $with_pj = false;
1900 private $with_pje = false;
1901 private $with_pjse = false;
1902 private $with_pjsse = false;
1903 private $with_pjssse = false;
1904 private $with_pja = false;
1905
1906 public function addJobFilter()
1907 {
1908 $this->requireProfiles();
1909 $this->with_pj = true;
1910 return 'pj';
1911 }
1912
1913 public function addJobCompanyFilter()
1914 {
1915 $this->addJobFilter();
1916 $this->with_pje = true;
1917 return 'pje';
1918 }
1919
1920 public function addJobSectorizationFilter($type)
1921 {
1922 $this->addJobFilter();
1923 if ($type == self::JOB_SECTOR) {
1924 $this->with_pjse = true;
1925 return 'pjse';
1926 } else if ($type == self::JOB_SUBSECTOR) {
1927 $this->with_pjsse = true;
1928 return 'pjsse';
1929 } else if ($type == self::JOB_SUBSUBSECTOR) {
1930 $this->with_pjssse = true;
1931 return 'pjssse';
1932 } else if ($type == self::JOB_ALTERNATES) {
1933 $this->with_pja = true;
1934 return 'pja';
1935 }
1936 }
1937
1938 protected function jobJoins()
1939 {
1940 $joins = array();
1941 if ($this->with_pj) {
1942 $joins['pj'] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'profile_job', '$ME.uid = $UID');
1943 }
1944 if ($this->with_pje) {
1945 $joins['pje'] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'profile_job_enum', '$ME.id = pj.jobid');
1946 }
1947 if ($this->with_pjse) {
1948 $joins['pjse'] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'profile_job_sector_enum', '$ME.id = pj.sectorid');
1949 }
1950 if ($this->with_pjsse) {
1951 $joins['pjsse'] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'profile_job_subsector_enum', '$ME.id = pj.subsectorid');
1952 }
1953 if ($this->with_pjssse) {
1954 $joins['pjssse'] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'profile_job_subsubsector_enum', '$ME.id = pj.subsubsectorid');
1955 }
1956 if ($this->with_pja) {
1957 $joins['pja'] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'profile_job_alternates', '$ME.subsubsectorid = pj.subsubsectorid');
1958 }
1959 return $joins;
1960 }
1961
1962 /** NETWORKING
1963 */
1964
1965 private $with_pnw = false;
1966 public function addNetworkingFilter()
1967 {
1968 $this->requireAccounts();
1969 $this->with_pnw = true;
1970 return 'pnw';
1971 }
1972
1973 protected function networkingJoins()
1974 {
1975 $joins = array();
1976 if ($this->with_pnw) {
1977 $joins['pnw'] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'profile_networking', '$ME.uid = $UID');
1978 }
1979 return $joins;
1980 }
1981
1982 /** PHONE
1983 */
1984
1985 private $with_ptel = false;
1986
1987 public function addPhoneFilter()
1988 {
1989 $this->requireAccounts();
1990 $this->with_ptel = true;
1991 return 'ptel';
1992 }
1993
1994 protected function phoneJoins()
1995 {
1996 $joins = array();
1997 if ($this->with_ptel) {
1998 $joins['ptel'] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'profile_phones', '$ME.uid = $UID');
1999 }
2000 return $joins;
2001 }
2002
2003 /** MEDALS
2004 */
2005
2006 private $with_pmed = false;
2007 public function addMedalFilter()
2008 {
2009 $this->requireProfiles();
2010 $this->with_pmed = true;
2011 return 'pmed';
2012 }
2013
2014 protected function medalJoins()
2015 {
2016 $joins = array();
2017 if ($this->with_pmed) {
2018 $joins['pmed'] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'profile_medals_sub', '$ME.uid = $UID');
2019 }
2020 return $joins;
2021 }
2022
2023 /** MENTORING
2024 */
2025
2026 private $pms = array();
2027 const MENTOR_EXPERTISE = 1;
2028 const MENTOR_COUNTRY = 2;
2029 const MENTOR_SECTOR = 3;
2030
2031 public function addMentorFilter($type)
2032 {
2033 $this->requireAccounts();
2034 switch($type) {
2035 case MENTOR_EXPERTISE:
2036 $pms['pme'] = 'profile_mentor';
2037 return 'pme';
2038 case MENTOR_COUNTRY:
2039 $pms['pmc'] = 'profile_mentor_country';
2040 return 'pmc';
2041 case MENTOR_SECTOR:
2042 $pms['pms'] = 'profile_mentor_sector';
2043 return 'pms';
2044 default:
2045 Platal::page()->killError("Undefined mentor filter.");
2046 }
2047 }
2048
2049 protected function mentorJoins()
2050 {
2051 $joins = array();
2052 foreach ($this->pms as $sub => $tab) {
2053 $joins[$sub] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, $tab, '$ME.uid = $UID');
2054 }
2055 return $joins;
2056 }
2057
2058 /** CONTACTS
2059 */
2060 private $cts = array();
2061 public function addContactFilter($uid = null)
2062 {
2063 $this->requireAccounts();
2064 return $this->register_optional($this->cts, is_null($uid) ? null : 'user_' . $uid);
2065 }
2066
2067 protected function contactJoins()
2068 {
2069 $joins = array();
2070 foreach ($this->cts as $sub=>$key) {
2071 if (is_null($key)) {
2072 $joins['c' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'contacts', '$ME.contact = $UID');
2073 } else {
2074 $joins['c' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'contacts', XDB::format('$ME.uid = {?} AND $ME.contact = $UID', substr($key, 5)));
2075 }
2076 }
2077 return $joins;
2078 }
2079
2080
2081 /** CARNET
2082 */
2083 private $wn = array();
2084 public function addWatchRegistrationFilter($uid = null)
2085 {
2086 $this->requireAccounts();
2087 return $this->register_optional($this->wn, is_null($uid) ? null : 'user_' . $uid);
2088 }
2089
2090 private $wp = array();
2091 public function addWatchPromoFilter($uid = null)
2092 {
2093 $this->requireAccounts();
2094 return $this->register_optional($this->wp, is_null($uid) ? null : 'user_' . $uid);
2095 }
2096
2097 private $w = array();
2098 public function addWatchFilter($uid = null)
2099 {
2100 $this->requireAccounts();
2101 return $this->register_optional($this->w, is_null($uid) ? null : 'user_' . $uid);
2102 }
2103
2104 protected function watchJoins()
2105 {
2106 $joins = array();
2107 foreach ($this->w as $sub=>$key) {
2108 if (is_null($key)) {
2109 $joins['w' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'watch');
2110 } else {
2111 $joins['w' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'watch', XDB::format('$ME.uid = {?}', substr($key, 5)));
2112 }
2113 }
2114 foreach ($this->wn as $sub=>$key) {
2115 if (is_null($key)) {
2116 $joins['wn' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'watch_nonins', '$ME.ni_id = $UID');
2117 } else {
2118 $joins['wn' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'watch_nonins', XDB::format('$ME.uid = {?} AND $ME.ni_id = $UID', substr($key, 5)));
2119 }
2120 }
2121 foreach ($this->wn as $sub=>$key) {
2122 if (is_null($key)) {
2123 $joins['wn' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'watch_nonins', '$ME.ni_id = $UID');
2124 } else {
2125 $joins['wn' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'watch_nonins', XDB::format('$ME.uid = {?} AND $ME.ni_id = $UID', substr($key, 5)));
2126 }
2127 }
2128 foreach ($this->wp as $sub=>$key) {
2129 if (is_null($key)) {
2130 $joins['wp' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'watch_promo');
2131 } else {
2132 $joins['wp' . $sub] = new PlSqlJoin(PlSqlJoin::MODE_LEFT, 'watch_promo', XDB::format('$ME.uid = {?}', substr($key, 5)));
2133 }
2134 }
2135 return $joins;
2136 }
2137 }
2138 // }}}
2139
2140 // vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8:
2141 ?>