| 1 | <?php |
| 2 | /*************************************************************************** |
| 3 | * Copyright (C) 2003-2010 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 | /** Class Phone is meant to perform most of the access to the table profile_phones. |
| 23 | * |
| 24 | * profile_phone describes a Phone, which can be related to an Address, |
| 25 | * a Job, a Profile or a Company: |
| 26 | * - for a Profile: |
| 27 | * - `link_type` is set to 'user' |
| 28 | * - `link_id` is set to 0 |
| 29 | * - `pid` is set to the id of the related Profile (in profiles) |
| 30 | * |
| 31 | * - for a Company: |
| 32 | * - `link_type` is set to 'hq' |
| 33 | * - `link_id` is set to the id of the related Company (in profile_job_enum) |
| 34 | * - `pid` is set to 0 |
| 35 | * |
| 36 | * - for an Address (this only applies to a personal address) |
| 37 | * - `link_type` is set to 'address' |
| 38 | * - `link_id` is set to the related Address `id` (in profile_addresses) |
| 39 | * - `pid` is set to the related Address `pid` (in both profiles and profile_addresses) |
| 40 | * |
| 41 | * - for a Job: |
| 42 | * - `link_type` is set to 'pro' |
| 43 | * - `link_id` is set to the related Job `id` (not `jobid`) (in profile_job) |
| 44 | * - `pid` is set to the related Job `pid` (in both profiles and profile_job) |
| 45 | * |
| 46 | * Thus a Phone can be linked to a Company, a Profile, a Job, or a Profile-related Address. |
| 47 | */ |
| 48 | class Phone |
| 49 | { |
| 50 | const TYPE_FAX = 'fax'; |
| 51 | const TYPE_FIXED = 'fixed'; |
| 52 | const TYPE_MOBILE = 'mobile'; |
| 53 | |
| 54 | const LINK_JOB = 'pro'; |
| 55 | const LINK_ADDRESS = 'address'; |
| 56 | const LINK_PROFILE = 'user'; |
| 57 | const LINK_COMPANY = 'hq'; |
| 58 | |
| 59 | /** The following fields, but $error, all correspond to the fields of the |
| 60 | * database table profile_phones. |
| 61 | */ |
| 62 | private $id = 0; |
| 63 | private $pid = 0; |
| 64 | private $search = ''; |
| 65 | private $link_type = 'user'; |
| 66 | private $link_id = 0; |
| 67 | // The following fields are the fields of the form in the profile edition. |
| 68 | private $type = 'fixed'; |
| 69 | public $display = ''; |
| 70 | private $pub = 'private'; |
| 71 | public $comment = ''; |
| 72 | private $error = false; |
| 73 | |
| 74 | public function __construct(array $data = array()) |
| 75 | { |
| 76 | if (count($data) > 0) { |
| 77 | foreach ($data as $key => $val) { |
| 78 | $this->$key = $val; |
| 79 | } |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | public function linkType() |
| 84 | { |
| 85 | return $this->link_type; |
| 86 | } |
| 87 | |
| 88 | public function linkId() |
| 89 | { |
| 90 | return $this->link_id; |
| 91 | } |
| 92 | |
| 93 | public function id() |
| 94 | { |
| 95 | return $this->id; |
| 96 | } |
| 97 | |
| 98 | public function pid() |
| 99 | { |
| 100 | return $this->pid; |
| 101 | } |
| 102 | |
| 103 | public function search() |
| 104 | { |
| 105 | return $this->search; |
| 106 | } |
| 107 | |
| 108 | public function setId($id) |
| 109 | { |
| 110 | $this->id = $id; |
| 111 | } |
| 112 | |
| 113 | /** Returns the unique ID of a phone. |
| 114 | * This ID will allow to link it to an address, a user or a job. |
| 115 | * The format is address_addressId_phoneId (where phoneId is the id |
| 116 | * of the phone in the list of those associated with the address). |
| 117 | */ |
| 118 | public function uniqueId() { |
| 119 | return $this->link_type . '_' . $this->link_id . '_' . $this->id; |
| 120 | } |
| 121 | |
| 122 | public function hasFlags($flags) { |
| 123 | return $this->hasType($flags) && $this->hasLink($flags); |
| 124 | } |
| 125 | |
| 126 | /** Returns true if this phone's type matches the flags. |
| 127 | */ |
| 128 | public function hasType($flags) { |
| 129 | $flags = $flags & Profile::PHONE_TYPE_ANY; |
| 130 | return ( |
| 131 | ($flags == Profile::PHONE_TYPE_ANY) |
| 132 | || |
| 133 | (($flags & Profile::PHONE_TYPE_FAX) && $this->type == self::TYPE_FAX) |
| 134 | || |
| 135 | (($flags & Profile::PHONE_TYPE_FIXED) && $this->type == self::TYPE_FIXED) |
| 136 | || |
| 137 | (($flags & Profile::PHONE_TYPE_MOBILE) && $this->type == self::TYPE_MOBILE) |
| 138 | ); |
| 139 | } |
| 140 | |
| 141 | /** User-friendly accessible version of the type. |
| 142 | */ |
| 143 | public function displayType($short = false) |
| 144 | { |
| 145 | switch ($this->type) { |
| 146 | case Phone::TYPE_FIXED: |
| 147 | return $short ? 'Tél' : 'Fixe'; |
| 148 | case Phone::TYPE_FAX: |
| 149 | return 'Fax'; |
| 150 | case Phone::TYPE_MOBILE: |
| 151 | return $short ? 'Mob' : 'Mobile'; |
| 152 | default: |
| 153 | return $this->type; |
| 154 | } |
| 155 | } |
| 156 | |
| 157 | /** Returns true if this phone's link matches the flags. |
| 158 | */ |
| 159 | public function hasLink($flags) |
| 160 | { |
| 161 | $flags = $flags & Profile::PHONE_LINK_ANY; |
| 162 | return ( |
| 163 | ($flags == Profile::PHONE_LINK_ANY) |
| 164 | || |
| 165 | (($flags & Profile::PHONE_LINK_COMPANY) && $this->link_type == self::LINK_COMPANY) |
| 166 | || |
| 167 | (($flags & Profile::PHONE_LINK_JOB) && $this->link_type == self::LINK_JOB) |
| 168 | || |
| 169 | (($flags & Profile::PHONE_LINK_ADDRESS) && $this->link_type == self::LINK_ADDRESS) |
| 170 | || |
| 171 | (($flags & Profile::PHONE_LINK_PROFILE) && $this->link_type == self::LINK_PROFILE) |
| 172 | ); |
| 173 | } |
| 174 | |
| 175 | /* Properly formats the search phone, based on actual display phone. |
| 176 | * |
| 177 | * Computes a base form of the phone number with the international prefix. |
| 178 | * This number only contains digits, thus does not begin with the '+' sign. |
| 179 | * Numbers starting with 0 (or '(0)') are considered as French. |
| 180 | * This assumes that non-French numbers have their international prefix. |
| 181 | */ |
| 182 | private function formatSearch() |
| 183 | { |
| 184 | $tel = trim($this->display); |
| 185 | // Number starting with "(0)" is a French number. |
| 186 | if (substr($tel, 0, 3) === '(0)') { |
| 187 | $tel = '33' . $tel; |
| 188 | } |
| 189 | // Removes all "(0)" often used as local prefix. |
| 190 | $tel = str_replace('(0)', '', $tel); |
| 191 | // Removes all non-digit chars. |
| 192 | $tel = preg_replace('/[^0-9]/', '', $tel); |
| 193 | |
| 194 | if (substr($tel, 0, 2) === '00') { |
| 195 | // Removes prefix for international calls. |
| 196 | $tel = substr($tel, 2); |
| 197 | } else if (substr($tel, 0, 1) === '0') { |
| 198 | // Number starting with 0 is a French number. |
| 199 | $tel = '33' . substr($tel, 1); |
| 200 | } |
| 201 | $this->search = $tel; |
| 202 | } |
| 203 | |
| 204 | // Properly formats the display phone, it requires the search phone to be already formatted. |
| 205 | private function formatDisplay($format = array()) |
| 206 | { |
| 207 | $tel = $this->search; |
| 208 | $ret = ''; |
| 209 | $telLength = strlen($tel); |
| 210 | // Try to find the country by trying to find a matching prefix of 1, 2 or 3 digits. |
| 211 | if ((!isset($format['phoneprf'])) || ($format['phoneprf'] == '')) { |
| 212 | $res = XDB::query('SELECT phonePrefix AS phoneprf, phoneFormat AS format |
| 213 | FROM geoloc_countries |
| 214 | WHERE phonePrefix = {?} OR phonePrefix = {?} OR phonePrefix = {?} |
| 215 | LIMIT 1', |
| 216 | substr($tel, 0, 1), substr($tel, 0, 2), substr($tel, 0, 3)); |
| 217 | if ($res->numRows() == 0) { |
| 218 | // No country found, does not format more than prepending a '+' sign. |
| 219 | $this->error = true; |
| 220 | $this->display = '+' . $tel; |
| 221 | return; |
| 222 | } |
| 223 | $format = $res->fetchOneAssoc(); |
| 224 | } |
| 225 | if ($format['format'] == '') { |
| 226 | // If the country does not have a phone number format, the number will be displayed |
| 227 | // as "+prefix ## ## ## ##...". |
| 228 | $format['format'] = '(+p)'; |
| 229 | } |
| 230 | |
| 231 | /* Formats the phone number according t the template with these rules: |
| 232 | * - p is replaced by the international prefix, |
| 233 | * - # is replaced by one digit, |
| 234 | * - other chars are left intact. |
| 235 | * If the number is longer than the format, remaining digits are |
| 236 | * appended by blocks of two digits separated by spaces. |
| 237 | * The last block can have 3 digits to avoid a final single-digit block. |
| 238 | */ |
| 239 | $j = 0; |
| 240 | $i = strlen($format['phoneprf']); |
| 241 | $lengthFormat = strlen($format['format']); |
| 242 | while (($i < $telLength) && ($j < $lengthFormat)) { |
| 243 | if ($format['format'][$j] == '#') { |
| 244 | $ret .= $tel[$i]; |
| 245 | ++$i; |
| 246 | } else if ($format['format'][$j] == 'p') { |
| 247 | $ret .= $format['phoneprf']; |
| 248 | } else { |
| 249 | $ret .= $format['format'][$j]; |
| 250 | } |
| 251 | ++$j; |
| 252 | } |
| 253 | for (; $i < $telLength - 1; $i += 2) { |
| 254 | $ret .= ' ' . substr($tel, $i, 2); |
| 255 | } |
| 256 | // Appends last left alone numbers to the last block. |
| 257 | if ($i < $telLength) { |
| 258 | $ret .= substr($tel, $i); |
| 259 | } |
| 260 | $this->display = $ret; |
| 261 | } |
| 262 | |
| 263 | |
| 264 | public function format($format = array()) |
| 265 | { |
| 266 | if (!($this->type == Phone::TYPE_FIXED |
| 267 | || $this->type == Phone::TYPE_MOBILE |
| 268 | || $this->type == Phone::TYPE_FAX)) { |
| 269 | $this->type = Phone::TYPE_FIXED; |
| 270 | } |
| 271 | $this->formatSearch(); |
| 272 | $this->formatDisplay($format); |
| 273 | return !$this->error; |
| 274 | } |
| 275 | |
| 276 | public function toFormArray() |
| 277 | { |
| 278 | return array( |
| 279 | 'type' => $this->type, |
| 280 | 'display' => $this->display, |
| 281 | 'pub' => $this->pub, |
| 282 | 'comment' => $this->comment, |
| 283 | 'error' => $this->error |
| 284 | ); |
| 285 | } |
| 286 | |
| 287 | private function toString() |
| 288 | { |
| 289 | return 'type : ' . $this->type .', numéro : ' . $this->display |
| 290 | . ', commentaire : « ' . $this->comment . ' », affichage : ' . $this->pub; |
| 291 | } |
| 292 | |
| 293 | private function isEmpty() |
| 294 | { |
| 295 | return (!$this->search || $this->search == ''); |
| 296 | } |
| 297 | |
| 298 | public function save() |
| 299 | { |
| 300 | $this->format(); |
| 301 | if (!$this->isEmpty()) { |
| 302 | XDB::execute('INSERT INTO profile_phones (pid, link_type, link_id, tel_id, tel_type, |
| 303 | search_tel, display_tel, pub, comment) |
| 304 | VALUES ({?}, {?}, {?}, {?}, {?}, {?}, {?}, {?}, {?})', |
| 305 | $this->pid, $this->link_type, $this->link_id, $this->id, $this->type, |
| 306 | $this->search, $this->display, $this->pub, $this->comment); |
| 307 | } |
| 308 | } |
| 309 | |
| 310 | public function delete() |
| 311 | { |
| 312 | XDB::execute('DELETE FROM profile_phones |
| 313 | WHERE pid = {?} AND link_type = {?} AND link_id = {?} AND tel_id = {?}', |
| 314 | $this->pid, $this->link_type, $this->link_id, $this->id); |
| 315 | } |
| 316 | |
| 317 | static public function deletePhones($pid, $link_type, $link_id = null) |
| 318 | { |
| 319 | $where = ''; |
| 320 | if (!is_null($link_id)) { |
| 321 | $where = XDB::format(' AND link_id = {?}', $link_id); |
| 322 | } |
| 323 | XDB::execute('DELETE FROM profile_phones |
| 324 | WHERE pid = {?} AND link_type = {?}' . $where, |
| 325 | $pid, $link_type); |
| 326 | } |
| 327 | |
| 328 | /** Saves phones into the database. |
| 329 | * @param $data: an array of form formatted phones. |
| 330 | * @param $pid, $link_type, $link_id: pid, link_type and link_id concerned by the update. |
| 331 | */ |
| 332 | static public function savePhones(array $data, $pid, $link_type, $link_id = null) |
| 333 | { |
| 334 | foreach ($data as $id => $value) { |
| 335 | $value['id'] = $id; |
| 336 | if (!is_null($pid)) { |
| 337 | $value['pid'] = $pid ; |
| 338 | } |
| 339 | if (!is_null($link_type)) { |
| 340 | $value['link_type'] = $link_type ; |
| 341 | } |
| 342 | if (!is_null($link_id)) { |
| 343 | $value['link_id'] = $link_id ; |
| 344 | } |
| 345 | $phone = new Phone($value); |
| 346 | $phone->save(); |
| 347 | } |
| 348 | } |
| 349 | |
| 350 | static private function formArrayWalk(array $data, $function, &$success = true, $requiresEmptyPhone = false) |
| 351 | { |
| 352 | $phones = array(); |
| 353 | foreach ($data as $item) { |
| 354 | $phone = new Phone($item); |
| 355 | $success = (!$phone->error && ($phone->format() || $phone->isEmpty()) && $success); |
| 356 | if (!$phone->isEmpty()) { |
| 357 | $phones[] = call_user_func(array($phone, $function)); |
| 358 | } |
| 359 | } |
| 360 | if (count($phones) == 0 && $requiresEmptyPhone) { |
| 361 | $phone = new Phone(); |
| 362 | $phones[] = call_user_func(array($phone, $function)); |
| 363 | } |
| 364 | return $phones; |
| 365 | } |
| 366 | |
| 367 | // Formats an array of form phones into an array of form formatted phones. |
| 368 | static public function formatFormArray(array $data, &$success = true) |
| 369 | { |
| 370 | return self::formArrayWalk($data, 'toFormArray', $success, true); |
| 371 | } |
| 372 | |
| 373 | static public function formArrayToString(array $data) |
| 374 | { |
| 375 | return implode(' ; ', self::formArrayWalk($data, 'toString')); |
| 376 | } |
| 377 | |
| 378 | static public function iterate(array $pids = array(), array $link_types = array(), |
| 379 | array $link_ids = array(), array $pubs = array()) |
| 380 | { |
| 381 | return new PhoneIterator($pids, $link_types, $link_ids, $pubs); |
| 382 | } |
| 383 | } |
| 384 | |
| 385 | /** Iterator over a set of Phones |
| 386 | * |
| 387 | * @param $pid, $link_type, $link_id, $pub |
| 388 | * |
| 389 | * The iterator contains the phones that correspond to the value stored in the |
| 390 | * parameters' arrays. |
| 391 | */ |
| 392 | class PhoneIterator implements PlIterator |
| 393 | { |
| 394 | private $dbiter; |
| 395 | |
| 396 | public function __construct(array $pids, array $link_types, array $link_ids, array $pubs) |
| 397 | { |
| 398 | $where = array(); |
| 399 | if (count($pids) != 0) { |
| 400 | $where[] = XDB::format('(pid IN {?})', $pids); |
| 401 | } |
| 402 | if (count($link_types) != 0) { |
| 403 | $where[] = XDB::format('(link_type IN {?})', $link_types); |
| 404 | } |
| 405 | if (count($link_ids) != 0) { |
| 406 | $where[] = XDB::format('(link_id IN {?})', $link_ids); |
| 407 | } |
| 408 | if (count($pubs) != 0) { |
| 409 | $where[] = XDB::format('(pub IN {?})', $pubs); |
| 410 | } |
| 411 | $sql = 'SELECT search_tel AS search, display_tel AS display, comment, link_id, |
| 412 | tel_type AS type, link_type, tel_id AS id, pid, pub |
| 413 | FROM profile_phones |
| 414 | ' . ((count($where) > 0) ? 'WHERE ' . implode(' AND ', $where) : '') . ' |
| 415 | ORDER BY pid, link_id, tel_id'; |
| 416 | $this->dbiter = XDB::iterator($sql); |
| 417 | } |
| 418 | |
| 419 | public function next() |
| 420 | { |
| 421 | if (is_null($this->dbiter)) { |
| 422 | return null; |
| 423 | } |
| 424 | $data = $this->dbiter->next(); |
| 425 | if (is_null($data)) { |
| 426 | return null; |
| 427 | } |
| 428 | return new Phone($data); |
| 429 | } |
| 430 | |
| 431 | public function total() |
| 432 | { |
| 433 | return $this->dbiter->total(); |
| 434 | } |
| 435 | |
| 436 | public function first() |
| 437 | { |
| 438 | return $this->dbiter->first(); |
| 439 | } |
| 440 | |
| 441 | public function last() |
| 442 | { |
| 443 | return $this->dbiter->last(); |
| 444 | } |
| 445 | |
| 446 | public function value() |
| 447 | { |
| 448 | return $this->dbiter; |
| 449 | } |
| 450 | } |
| 451 | |
| 452 | // vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8: |
| 453 | ?> |