[ Index ] |
|
Code source de eGroupWare 1.2.106-2 |
1 <?php 2 /**************************************************************************\ 3 * eGroupWare - InfoLog * 4 * http://www.eGroupWare.org * 5 * Written by Ralf Becker <RalfBecker@outdoor-training.de> * 6 * originaly based on todo written by Joseph Engo <jengo@phpgroupware.org> * 7 * -------------------------------------------- * 8 * This program is free software; you can redistribute it and/or modify it * 9 * under the terms of the GNU General Public License as published by the * 10 * Free Software Foundation; either version 2 of the License, or (at your * 11 * option) any later version. * 12 \**************************************************************************/ 13 14 /* $Id: class.soinfolog.inc.php 22913 2006-12-08 07:13:36Z ralfbecker $ */ 15 16 include_once(EGW_API_INC.'/class.solink.inc.php'); 17 18 /** 19 * storage object / db-layer for InfoLog 20 * 21 * all values passed to this class are run either through intval or addslashes to prevent query-insertion 22 * and for pgSql 7.3 compatibility 23 * 24 * @package infolog 25 * @author Ralf Becker <RalfBecker@outdoor-training.de> 26 * @copyright (c) by Ralf Becker <RalfBecker@outdoor-training.de> 27 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License 28 */ 29 class soinfolog // DB-Layer 30 { 31 var $db; 32 var $grants; 33 var $data = array( ); 34 var $user; 35 var $info_table = 'egw_infolog'; 36 var $extra_table = 'egw_infolog_extra'; 37 38 /** 39 * constructor 40 */ 41 function soinfolog( $info_id = 0) 42 { 43 $this->db = clone($GLOBALS['egw']->db); 44 $this->db->set_app('infolog'); 45 $this->grants = $GLOBALS['egw']->acl->get_grants('infolog'); 46 $this->user = $GLOBALS['egw_info']['user']['account_id']; 47 48 $this->links =& new solink(); 49 50 $this->tz_offset = $GLOBALS['egw_info']['user']['preferences']['common']['tz_offset']; 51 52 $this->read( $info_id ); 53 } 54 55 /** 56 * checks if user has the $required_rights to access $info_id (private access is handled too) 57 * 58 * @param array/int $info data or info_id of InfoLog entry 59 * @param int $required_rights EGW_ACL_xyz anded together 60 * @param boolean $implicit_edit=false responsible has only implicit read and add rigths, unless this is set to true 61 * @return boolean True if access is granted else False 62 */ 63 function check_access( $info,$required_rights,$implicit_edit=false ) 64 { 65 if (is_array($info)) 66 { 67 68 } 69 elseif ((int) $info != $this->data['info_id']) // already loaded? 70 { 71 // dont change our own internal data, 72 // dont use new as it changes $phpgw->db 73 $private_info = $this; 74 $info = $private_info->read($info); 75 } 76 else 77 { 78 $info = $this->data; 79 } 80 if (!$info) 81 { 82 return False; 83 } 84 $owner = $info['info_owner']; 85 86 $access_ok = $owner == $this->user || // user has all rights 87 // ACL only on public entrys || $owner granted _PRIVATE 88 (!!($this->grants[$owner] & $required_rights) || 89 // implicite rights for responsible user(s) 90 in_array($this->user, $info['info_responsible']) && ($required_rights == EGW_ACL_READ || $required_rights == EGW_ACL_ADD || $implicit_edit && $required_rights == EGW_ACL_EDIT)) && 91 //$info['info_responsible'] == $this->user && $required_rights == EGW_ACL_READ) && 92 ($info['info_access'] == 'public' || 93 !!($this->grants[$owner] & EGW_ACL_PRIVATE)); 94 95 //echo "<p>check_access(info_id=$info_id (owner=$owner, user=$user),required_rights=$required_rights): access".($access_ok?"Ok":"Denied")."</p>\n"; 96 return $access_ok; 97 } 98 99 /** 100 * generate sql to be AND'ed into a query to ensure ACL is respected (incl. _PRIVATE) 101 * 102 * @param $filter: none|all - list all entrys user have rights to see<br> 103 * private|own - list only his personal entrys (incl. those he is responsible for !!!), my = entries the user is responsible for 104 * @return string the necesary sql 105 */ 106 function aclFilter($filter = False) 107 { 108 preg_match('/(my|own|privat|all|none|user)([0-9]*)/',$filter_was=$filter,$vars); 109 $filter = $vars[1]; 110 $f_user = intval($vars[2]); 111 112 if (isset($this->acl_filter[$filter.$f_user])) 113 { 114 return $this->acl_filter[$filter.$f_user]; // used cached filter if found 115 } 116 if (is_array($this->grants)) 117 { 118 foreach($this->grants as $user => $grant) 119 { 120 // echo "<p>grants: user=$user, grant=$grant</p>"; 121 if ($grant & (EGW_ACL_READ|EGW_ACL_EDIT)) 122 { 123 $public_user_list[] = $user; 124 } 125 if ($grant & EGW_ACL_PRIVATE) 126 { 127 $private_user_list[] = $user; 128 } 129 } 130 if (count($private_user_list)) 131 { 132 $has_private_access = 'info_owner IN ('.implode(',',$private_user_list).')'; 133 } 134 } 135 $filtermethod = " (info_owner=$this->user"; // user has all rights 136 137 if ($filter == 'my') 138 { 139 $filtermethod .= " AND info_responsible='0'"; 140 } 141 // implicit read-rights for responsible user 142 $filtermethod .= " OR (".$this->db->concat("','",'info_responsible',"','")." LIKE '%,$this->user,%' AND info_access='public')"; 143 144 // private: own entries plus the one user is responsible for 145 if ($filter == 'private' || $filter == 'own') 146 { 147 $filtermethod .= " OR (".$this->db->concat("','",'info_responsible',"','")." LIKE '%,$this->user,%'". 148 ($filter == 'own' && count($public_user_list) ? // offer's should show up in own, eg. startpage, but need read-access 149 " OR info_status = 'offer' AND info_owner IN(" . implode(',',$public_user_list) . ')' : '').")". 150 " AND (info_access='public'".($has_private_access?" OR $has_private_access":'').')'; 151 } 152 elseif ($filter != 'my') // none --> all entrys user has rights to see 153 { 154 if ($has_private_access) 155 { 156 $filtermethod .= " OR $has_private_access"; 157 } 158 if (count($public_user_list)) 159 { 160 $filtermethod .= " OR (info_access='public' AND info_owner IN(" . implode(',',$public_user_list) . '))'; 161 } 162 } 163 $filtermethod .= ') '; 164 165 if ($filter == 'user' && $f_user > 0) 166 { 167 $filtermethod = " ((info_owner=$f_user AND info_responsible=0 OR ".$this->db->concat("','",'info_responsible',"','")." LIKE '%,$f_user,%') AND $filtermethod)"; 168 } 169 //echo "<p>aclFilter(filter='$filter_was',user='$user') = '$filtermethod', privat_user_list=".print_r($privat_user_list,True).", public_user_list=".print_r($public_user_list,True)."</p>\n"; 170 171 return $this->acl_filter[$filter.$f_user] = $filtermethod; // cache the filter 172 } 173 174 /** 175 * generate sql to filter based on the status of the log-entry 176 * 177 * @param $filter done = done or billed, open = not ()done or billed), offer = offer 178 * @return string the necesary sql 179 */ 180 function statusFilter($filter = '') 181 { 182 preg_match('/(done|open|offer)/',$filter,$vars); 183 $filter = $vars[1]; 184 185 switch ($filter) 186 { 187 case 'done': return " AND info_status IN ('done','billed','cancelled')"; 188 case 'open': return " AND NOT (info_status IN ('done','billed','cancelled'))"; 189 case 'offer': return " AND info_status = 'offer'"; 190 } 191 return ''; 192 } 193 194 /** 195 * generate sql to filter based on the start- and enddate of the log-entry 196 * 197 * @param $filter upcoming = startdate is in the future<br> 198 * today startdate < tomorrow<br> 199 * overdue enddate < tomorrow<br> 200 * limitYYYY/MM/DD not older or open 201 * @return string the necesary sql 202 */ 203 function dateFilter($filter = '') 204 { 205 preg_match('/(upcoming|today|overdue|date)([-\\/.0-9]*)/',$filter,$vars); 206 $filter = $vars[1]; 207 208 if (isset($vars[2]) && !empty($vars[2]) && ($date = split('[-/.]',$vars[2]))) 209 { 210 $today = mktime(-$this->tz_offset,0,0,intval($date[1]),intval($date[2]),intval($date[0])); 211 $tomorrow = mktime(-$this->tz_offset,0,0,intval($date[1]),intval($date[2])+1,intval($date[0])); 212 } 213 else 214 { 215 $now = getdate(time()-60*60*$this->tz_offset); 216 $tomorrow = mktime(-$this->tz_offset,0,0,$now['mon'],$now['mday']+1,$now['year']); 217 } 218 switch ($filter) 219 { 220 case 'upcoming': 221 return " AND info_startdate >= '$tomorrow'"; 222 case 'today': 223 return " AND info_startdate < '$tomorrow'"; 224 case 'overdue': 225 return " AND (info_enddate != 0 AND info_enddate < '$tomorrow')"; 226 case 'date': 227 if (!$today || !$tomorrow) 228 { 229 return ''; 230 } 231 return " AND ($today <= info_startdate AND info_startdate < $tomorrow)"; 232 case 'limit': 233 return " AND (info_modified >= '$today' OR NOT (info_status IN ('done','billed','cancelled')))"; 234 } 235 return ''; 236 } 237 238 /** 239 * initialise the internal $this->data to be empty 240 * 241 * only non-empty values got initialised 242 */ 243 function init() 244 { 245 $this->data = array( 246 'info_owner' => $this->user, 247 'info_priority' => 1, 248 'info_responsible' => array(), 249 ); 250 } 251 252 /** 253 * read InfoLog entry $info_id 254 * 255 * some cacheing is done to prevent multiple reads of the same entry 256 * 257 * @param $info_id id of log-entry 258 * @return array/boolean the entry as array or False on error (eg. entry not found) 259 */ 260 function read($info_id) // did _not_ ensure ACL 261 { 262 $info_id = (int) $info_id; 263 264 if ($info_id && $info_id == $this->data['info_id']) 265 { 266 return $this->data; // return the already read entry 267 } 268 if ($info_id <= 0 || !$this->db->select($this->info_table,'*',array('info_id'=>$info_id),__LINE__,__FILE__) || 269 !(($this->data = $this->db->row(true)))) 270 { 271 $this->init( ); 272 return False; 273 } 274 if (!is_array($this->data['info_responsible'])) 275 { 276 $this->data['info_responsible'] = $this->data['info_responsible'] ? explode(',',$this->data['info_responsible']) : array(); 277 } 278 $this->db->select($this->extra_table,'info_extra_name,info_extra_value',array('info_id'=>$info_id),__LINE__,__FILE__); 279 while ($this->db->next_record()) 280 { 281 $this->data['#'.$this->db->f(0)] = $this->db->f(1); 282 } 283 return $this->data; 284 } 285 286 /** 287 * Read the status of the given infolog-ids 288 * 289 * @param array $ids array with id's 290 * @return array with id => status pairs 291 */ 292 function get_status($ids) 293 { 294 $this->db->select($this->info_table,'info_id,info_type,info_status,info_percent',array('info_id'=>$ids),__LINE__,__FILE__); 295 while (($info = $this->db->row(true))) 296 { 297 switch ($info['info_type'].'-'.$info['info_status']) 298 { 299 case 'phone-not-started': 300 $status = 'call'; 301 break; 302 case 'phone-ongoing': 303 $status = 'will-call'; 304 break; 305 default: 306 $status = $info['info_status'] == 'ongoing' ? $info['info_percent'].'%' : $info['info_status']; 307 } 308 $stati[$info['info_id']] = $status; 309 } 310 return $stati; 311 } 312 313 /** 314 * delete InfoLog entry $info_id AND the links to it 315 * 316 * @param int $info_id id of log-entry 317 * @param bool $delete_children delete the children, if not set there parent-id to $new_parent 318 * @param int $new_parent new parent-id to set for subs 319 */ 320 function delete($info_id,$delete_children=True,$new_parent=0) // did _not_ ensure ACL 321 { 322 //echo "<p>soinfolog::delete($info_id,'$delete_children',$new_parent)</p>\n"; 323 if ((int) $info_id <= 0) 324 { 325 return; 326 } 327 $this->db->delete($this->info_table,array('info_id'=>$info_id),__LINE__,__FILE__); 328 $this->db->delete($this->extra_table,array('info_id'=>$info_id),__LINE__,__FILE__); 329 $this->links->unlink(0,'infolog',$info_id); 330 331 if ($this->data['info_id'] == $info_id) 332 { 333 $this->init( ); 334 } 335 // delete children, if they are owned by the user 336 if ($delete_children) 337 { 338 $db2 = clone($this->db); // we need an extra result-set 339 $db2->select($this->info_table,'info_id',array( 340 'info_id_parent' => $info_id, 341 'info_owner' => $this->user, 342 ),__LINE__,__FILE__); 343 while ($db2->next_record()) 344 { 345 $this->delete($db2->f(0),$delete_children); 346 } 347 } 348 // set parent_id to $new_parent or 0 for all not deleted children 349 $this->db->update($this->info_table,array('info_id_parent'=>$new_parent),array('info_id_parent'=>$info_id),__LINE__,__FILE__); 350 } 351 352 /** 353 * changes or deletes entries with a spezified owner (for hook_delete_account) 354 * 355 * @param $owner old owner 356 * @param $new_owner new owner or 0 if entries should be deleted 357 */ 358 function change_delete_owner($owner,$new_owner=0) // new_owner=0 means delete 359 { 360 if (!(int) $new_owner) 361 { 362 $db2 = clone($this->db); // we need an extra result-set 363 $db2->select($this->info_table,'info_id',array('info_owner'=>$owner),__LINE__,__FILE__); 364 while($db2->next_record()) 365 { 366 $this->delete($this->db->f(0),False); 367 } 368 } 369 else 370 { 371 $this->db->update($this->info_table,array('info_owner'=>$new_owner),array('info_owner'=>$owner),__LINE__,__FILE__); 372 } 373 $this->db->update($this->info_table,array('info_responsible'=>$new_owner),array('info_responsible'=>$owner),__LINE__,__FILE__); 374 } 375 376 /** 377 * writes the given $values to InfoLog, a new entry gets created if info_id is not set or 0 378 * 379 * @param array $values with the data of the log-entry 380 * @param int $check_modified=0 old modification date to check before update (include in WHERE) 381 * @return int/boolean info_id, false on error or 0 if the entry has been updated in the meantime 382 */ 383 function write($values,$check_modified=0) // did _not_ ensure ACL 384 { 385 //echo "soinfolog::write(,$check_modified) values="; _debug_array($values); 386 $info_id = (int) $values['info_id']; 387 388 if (array_key_exists('info_responsible',$values)) // isset($values['info_responsible']) returns false for NULL! 389 { 390 $values['info_responsible'] = $values['info_responsible'] ? implode(',',$values['info_responsible']) : '0'; 391 } 392 $table_def = $this->db->get_table_definitions('infolog',$this->info_table); 393 $to_write = array(); 394 foreach($values as $key => $val) 395 { 396 if ($key != 'info_id' && isset($table_def['fd'][$key])) 397 { 398 $to_write[$key] = $this->data[$key] = $val; // update internal data 399 } 400 } 401 // writing no price as SQL NULL (required by postgres) 402 if ($to_write['info_price'] === '') $to_write['info_price'] = NULL; 403 404 if (($this->data['info_id'] = $info_id)) 405 { 406 $where = array('info_id' => $info_id); 407 if ($check_modified) $where['info_datemodified'] = $check_modified; 408 if (!$this->db->update($this->info_table,$to_write,$where,__LINE__,__FILE__)) 409 { 410 return false; // Error 411 } 412 if ($this->db->affected_rows() < 1) return 0; // someone else updated the modtime or deleted the entry 413 } 414 else 415 { 416 if (!isset($to_write['info_id_parent'])) $to_write['info_id_parent'] = 0; // must not be null 417 418 $this->db->insert($this->info_table,$to_write,false,__LINE__,__FILE__); 419 $info_id = $this->data['info_id'] = $this->db->get_last_insert_id($this->info_table,'info_id'); 420 } 421 //echo "<p>soinfolog.write values= "; _debug_array($values); 422 423 // write customfields now 424 foreach($values as $key => $val) 425 { 426 if ($key[0] != '#') 427 { 428 continue; // no customfield 429 } 430 $this->data[$key] = $val; // update internal data 431 432 $this->db->insert($this->extra_table,array( 433 'info_extra_value'=>$val 434 ),array( 435 'info_id' => $info_id, 436 'info_extra_name' => substr($key,1), 437 ),__LINE__,__FILE__); 438 } 439 // echo "<p>soinfolog.write this->data= "; _debug_array($this->data); 440 441 return $this->data['info_id']; 442 } 443 444 /** 445 * count the sub-entries of $info_id 446 * 447 * This is done now be search too (in key info_anz_subs), if DB can use sub-queries 448 * 449 * @param $info_id id of log-entry 450 * @return int the number of sub-entries 451 */ 452 function anzSubs( $info_id ) 453 { 454 if (($info_id = intval($info_id)) <= 0) 455 { 456 return 0; 457 } 458 $this->db->select($this->info_table,'count(*)',array( 459 'info_id_parent' => $info_id, 460 $this->aclFilter() 461 ),__LINE__,__FILE__); 462 463 $this->db->next_record(); 464 //echo "<p>anzSubs($info_id) = ".$this->db->f(0)." ($sql)</p>\n"; 465 return $this->db->f(0); 466 } 467 468 /** 469 * searches InfoLog for a certain pattern in $query 470 * 471 * If DB can use sub-queries, the number of subs are under the key info_anz_subs. 472 * 473 * @param $query[order] column-name to sort after 474 * @param $query[sort] sort-order DESC or ASC 475 * @param $query[filter] string with combination of acl-, date- and status-filters, eg. 'own-open-today' or '' 476 * @param $query[cat_id] category to use or 0 or unset 477 * @param $query[search] pattern to search, search is done in info_from, info_subject and info_des 478 * @param $query[action] / $query[action_id] if only entries linked to a specified app/entry show be used 479 * @param &$query[start], &$query[total] nextmatch-parameters will be used and set if query returns less entries 480 * @param $query[col_filter] array with column-name - data pairs, data == '' means no filter (!) 481 * @param $query[subs] boolean return subs or not, if unset the user preference is used 482 * @param $query[num_rows] number of rows to return if $query[start] is set, default is to use the value from the general prefs 483 * @return array with id's as key of the matching log-entries 484 */ 485 function search(&$query) 486 { 487 //echo "<p>soinfolog.search(".print_r($query,True).")</p>\n"; 488 $action2app = array( 489 'addr' => 'addressbook', 490 'proj' => 'projects', 491 'event' => 'calendar' 492 ); 493 $action = isset($action2app[$query['action']]) ? $action2app[$query['action']] : $query['action']; 494 495 if ($action != '') 496 { 497 $links = $this->links->get_links($action=='sp'?'infolog':$action,$query['action_id'],'infolog'); 498 499 if (count($links)) 500 { 501 $link_extra = ($action == 'sp' ? 'OR' : 'AND')." main.info_id IN (".implode(',',$links).')'; 502 } 503 } 504 if (!empty($query['order']) && eregi('^[a-z_0-9, ]+$',$query['order']) && (empty($query['sort']) || eregi('^(DESC|ASC)$',$query['sort']))) 505 { 506 $order = array(); 507 foreach(explode(',',$query['order']) as $val) 508 { 509 $val = trim($val); 510 $val = (substr($val,0,5) != 'info_' ? 'info_' : '').$val; 511 if ($val == 'info_des' && $this->db->capabilities['order_on_text'] !== true) 512 { 513 if (!$this->db->capabilities['order_on_text']) continue; 514 515 $val = sprintf($this->db->capabilities['order_on_text'],$val); 516 } 517 $order[] = $val; 518 } 519 $ordermethod = 'ORDER BY ' . implode(',',$order) . ' ' . $query['sort']; 520 } 521 else 522 { 523 $ordermethod = 'ORDER BY info_datemodified DESC'; // newest first 524 } 525 $acl_filter = $filtermethod = $this->aclFilter($query['filter']); 526 $filtermethod .= $this->statusFilter($query['filter']); 527 $filtermethod .= $this->dateFilter($query['filter']); 528 529 if (is_array($query['col_filter'])) 530 { 531 foreach($query['col_filter'] as $col => $data) 532 { 533 if (substr($col,0,5) != 'info_') $col = 'info_'.$col; 534 if (!empty($data) && eregi('^[a-z_0-9]+$',$col)) 535 { 536 if ($col == 'info_responsible') 537 { 538 $data = (int) $data; 539 if (!$data) continue; 540 $filtermethod .= " AND (".$this->db->concat("','",'info_responsible',"','")." LIKE '%,$data,%' OR info_responsible='0' AND info_owner=$data)"; 541 } 542 else 543 { 544 if (!$this->table_defs) $this->table_defs = $this->db->get_table_definitions('infolog',$this->info_table); 545 $filtermethod .= ' AND '.$col.'='.$this->db->quote($data,$this->table_defs['fd'][$col]['type']); 546 } 547 } 548 } 549 } 550 //echo "<p>filtermethod='$filtermethod'</p>"; 551 552 if ((int)$query['cat_id']) 553 { 554 //$filtermethod .= ' AND info_cat='.intval($query['cat_id']).' '; 555 if (!is_object($GLOBALS['egw']->categories)) 556 { 557 $GLOBALS['egw']->categories =& CreateObject('phpgwapi.categories'); 558 } 559 $cats = $GLOBALS['egw']->categories->return_all_children((int)$query['cat_id']); 560 $filtermethod .= ' AND info_cat'.(count($cats)>1? ' IN ('.implode(',',$cats).') ' : '='.(int)$query['cat_id']); 561 } 562 $join = $distinct = $count_subs = ''; 563 if ($query['query']) $query['search'] = $query['query']; // allow both names 564 if ($query['search']) // we search in _from, _subject, _des and _extra_value for $query 565 { 566 $pattern = $this->db->quote('%'.$query['search'].'%'); 567 568 $columns = array('info_from','info_addr','info_location','info_subject','info_extra_value'); 569 // at the moment MaxDB 7.5 cant cast nor search text columns, it's suppost to change in 7.6 570 if ($this->db->capabilities['like_on_text']) $columns[] = 'info_des'; 571 572 $sql_query = 'AND ('.(is_numeric($query['search']) ? 'main.info_id='.(int)$query['search'].' OR ' : ''). 573 implode(" LIKE $pattern OR ",$columns)." LIKE $pattern) "; 574 $join = "LEFT JOIN $this->extra_table ON main.info_id=$this->extra_table.info_id"; 575 // mssql and others cant use DISTICT if text columns (info_des) are involved 576 $distinct = $this->db->capabilities['distinct_on_text'] ? 'DISTINCT' : ''; 577 } 578 $pid = 'AND info_id_parent='.($action == 'sp' ? $query['action_id'] : 0); 579 580 if (!$GLOBALS['egw_info']['user']['preferences']['infolog']['listNoSubs'] && 581 $action != 'sp' || isset($query['subs']) && $query['subs']) 582 { 583 $pid = ''; 584 } 585 $ids = array( ); 586 if ($action == '' || $action == 'sp' || count($links)) 587 { 588 $sql_query = "FROM $this->info_table main $join WHERE ($filtermethod $pid $sql_query) $link_extra"; 589 590 $this->db->query($sql="SELECT $distinct main.info_id ".$sql_query,__LINE__,__FILE__); 591 $query['total'] = $this->db->num_rows(); 592 593 if (isset($query['start']) && $query['start'] > $query['total']) 594 { 595 $query['start'] = 0; 596 } 597 if ($this->db->capabilities['sub_queries']) 598 { 599 $count_subs = ",(SELECT count(*) FROM $this->info_table sub WHERE sub.info_id_parent=main.info_id AND $acl_filter) AS info_anz_subs"; 600 } 601 $this->db->query($sql="SELECT $distinct main.* $count_subs $sql_query $ordermethod",__LINE__,__FILE__, 602 (int) $query['start'],isset($query['start']) ? (int) $query['num_rows'] : -1); 603 //echo "<p>db::query('$sql',,,".(int)$query['start'].','.(isset($query['start']) ? (int) $query['num_rows'] : -1).")</p>\n"; 604 while (($info =& $this->db->row(true))) 605 { 606 $info['info_responsible'] = $info['info_responsible'] ? explode(',',$info['info_responsible']) : array(); 607 608 $ids[$info['info_id']] = $info; 609 } 610 } 611 else 612 { 613 $query['start'] = $query['total'] = 0; 614 } 615 return $ids; 616 } 617 }
titre
Description
Corps
titre
Description
Corps
titre
Description
Corps
titre
Corps
Généré le : Sun Feb 25 17:20:01 2007 | par Balluche grâce à PHPXref 0.7 |