[ Index ] |
|
Code source de eGroupWare 1.2.106-2 |
1 <?php 2 /**************************************************************************\ 3 * eGroupWare - ProjectManager - Elements business object * 4 * http://www.egroupware.org * 5 * Written and (c) 2005 by Ralf Becker <RalfBecker@outdoor-training.de> * 6 * -------------------------------------------- * 7 * This program is free software; you can redistribute it and/or modify it * 8 * under the terms of the GNU General Public License as published by the * 9 * Free Software Foundation; either version 2 of the License, or (at your * 10 * option) any later version. * 11 \**************************************************************************/ 12 13 /* $Id: class.boprojectelements.inc.php 21518 2006-05-13 06:09:28Z ralfbecker $ */ 14 15 include_once (EGW_INCLUDE_ROOT.'/projectmanager/inc/class.soprojectelements.inc.php'); 16 17 /** 18 * Elements business object of the projectmanager 19 * 20 * @package projectmanager 21 * @author RalfBecker-AT-outdoor-training.de 22 * @copyright (c) 2005 by RalfBecker-AT-outdoor-training.de 23 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License 24 */ 25 class boprojectelements extends soprojectelements 26 { 27 /** 28 * @var int/string $debug 0 = no debug-messages, 1 = main, 2 = more, 3 = all, 4 = all incl. so_sql, or string with function-name to debug 29 */ 30 var $debug=false; 31 /** 32 * @var bolink-object $link instance of the link-class 33 */ 34 var $link; 35 /** 36 * @var boprojectmanager-object $link instance of the boprojectmanager-class 37 */ 38 var $project; 39 /** 40 * @var array $project_summary array with summary information of the current project 41 */ 42 var $project_summary; 43 /** 44 * @var soconstraints-object $constraints instance of the soconstraints-class 45 */ 46 var $constraints; 47 /** 48 * @var somilestones-object $milestones instance of the somilestones-class 49 */ 50 var $milestones; 51 /** 52 * @var array $datasources instances of the different datasources 53 */ 54 var $datasources = array(); 55 /** 56 * @var array $timestamps timestaps that need to be adjusted to user-time on reading or saving 57 */ 58 var $timestamps = array( 59 'pe_synced','pe_modified','pe_planned_start','pe_real_start','pe_planned_end','pe_real_end', 60 ); 61 /** 62 * @var int $tz_offset_s offset in secconds between user and server-time, 63 * it need to be add to a server-time to get the user-time or substracted from a user-time to get the server-time 64 */ 65 var $tz_offset_s; 66 /** 67 * @var int $now_su is the time as timestamp in user-time 68 */ 69 var $now_su; 70 /** 71 * @var array $status_filter translates filter-values to allowed stati 72 */ 73 var $status_filter = array( 74 'all' => false, 75 'used' => array('new','regular'), 76 'new' => 'new', 77 'ignored' => 'ignore', 78 ); 79 /** 80 * @var int $updated or'ed id's of the values set by the last call to the updated method 81 */ 82 var $updated = 0; 83 84 /** 85 * Constructor, class the constructor of the extended class 86 * 87 * @param int $pm_id pm_id of the project to use, default null 88 * @param int $pe_id pe_id of the project-element to load, default null 89 */ 90 function boprojectelements($pm_id=null,$pe_id=null) 91 { 92 if (!is_object($GLOBALS['egw']->datetime)) 93 { 94 $GLOBALS['egw']->datetime =& CreateObject('phpgwapi.datetime'); 95 } 96 $this->tz_offset_s = $GLOBALS['egw']->datetime->tz_offset; 97 $this->now_su = time() + $this->tz_offset_s; 98 99 $this->soprojectelements($pm_id,$pe_id); 100 101 if (!is_object($GLOBALS['egw']->link)) 102 { 103 $GLOBALS['egw']->link =& CreateObject('phpgwapi.bolink'); 104 } 105 $this->link =& $GLOBALS['egw']->link; 106 107 $this->project =& CreateObject('projectmanager.boprojectmanager',$pm_id); 108 $this->config =& $this->project->config; 109 110 $this->project->instanciate('constraints,milestones'); 111 $this->constraints =& $this->project->constraints; 112 $this->milestones =& $this->project->milestones; 113 114 $this->project_summary = $this->summary(); 115 116 if ((int)$this->debug >= 3 || $this->debug == 'boprojectelements') 117 { 118 $this->debug_message(function_backtrace()."\nboprojectelements::boprojectelements($pm_id,$pe_id) data=".print_r($this->data,true)); 119 } 120 // save us in $GLOBALS['boprojectselements'] for ExecMethod used in hooks 121 if (!is_object($GLOBALS['boprojectselements'])) 122 { 123 $GLOBALS['boprojectselements'] =& $this; 124 } 125 } 126 127 /** 128 * receives notifications from the link-class: new, deleted links to pm entries, or updated content of linked entries 129 * 130 * We only process link- & update-notifications to parent-projects! 131 * A project P is the parent of an other project C, if link_id1=P.pm_id and link_id2=C.pm_id ! 132 * 133 * @param array $data array with keys type, id, target_app, target_id, link_id, data 134 */ 135 function notify($data) 136 { 137 if ((int) $this->debug >= 2 || $this->debug == 'notify') $this->debug_message("boprojectelements::notify(link_id=$data[link_id], type=$data[type], target=$data[target_app]-$data[target_id])"); 138 139 switch($data['type']) 140 { 141 case 'link': 142 case 'update': 143 // for projectmanager we need to check the direction of the link 144 if ($data['target_app'] == 'projectmanager') 145 { 146 $link = $this->link->get_link($data['link_id']); 147 if ($link['link_id2'] == $data['id']) 148 { 149 return; // this is a notification to a child / subproject --> ignore it 150 } 151 // for new links we need to make sure the new child is not an ancestor of us 152 if ($data['type'] == 'link') 153 { 154 if (($ancestors = $this->project->ancestors($data['id'])) && in_array($data['target_id'],$ancestors)) 155 { 156 if ((int) $this->debug >= 2 || $this->debug == 'notify') $this->debug_message("boprojectelements::notify: cant use pm_id=$data[target_id] as child as it's one of our (pm_id=$data[id]) ancestors=".print_r($ancestors,true)); 157 return; // the link is not used as an project-element, thought it's still a regular link 158 } 159 if ((int) $this->debug >= 3 || $this->debug == 'notify') $this->debug_message("boprojectelements::notify: ancestors($data[id])=".print_r($ancestors,true)); 160 } 161 } 162 $this->update($data['target_app'],$data['target_id'],$data['link_id'],$data['id']); 163 break; 164 165 case 'unlink': 166 $this->delete(array('pm_id' => $data['id'],'pe_id' => $data['link_id'])); 167 break; 168 169 } 170 } 171 172 /** 173 * Updates / creates a project-element with the data of it's datasource 174 * 175 * Sets additionally $this->updated with the or'ed id's of the updated values 176 * 177 * ToDo: if end-date changed, update elements which have "us" as start-constrain 178 * 179 * @param string $app appname 180 * @param string $id id of $app as used by the link-class and the datasource 181 * @param int $pe_id=0 element- / link-id or 0 to only read and return the entry, but not save it! 182 * @param int $pm_id=null project-id, default $this->pm_id 183 * @param boolean $update_project=true update the data in the project if necessary 184 * @return array/boolean the updated project-element or false on error (eg. no read access) 185 */ 186 function &update($app,$id,$pe_id=0,$pm_id=null,$update_project=true) 187 { 188 if (!$pm_id) $pm_id = $this->pm_id; 189 190 if ((int) $this->debug >= 2 || $this->debug == 'update') $this->debug_message("boprojectelements::update(app='$app',id='$id',pe_id=$pe_id,pm_id=$pm_id)"); 191 192 if (!$app || !$id || !(int) $pm_id) 193 { 194 return false; 195 } 196 $this->init(); 197 $need_save_anyway = false; 198 // check if entry already exists and set basic values if not 199 if (!$pe_id || ($need_save_anyway = !$this->read(array('pm_id'=>$pm_id,'pe_id'=>$pe_id)))) 200 { 201 $this->data['pm_id'] = $pm_id; 202 $this->data['pe_id'] = $pe_id; 203 $this->data['pe_overwrite'] = 0; // none set so far 204 205 // only set status if it's not set by the datasource 206 if (!isset($this->data['pe_status'])) 207 { 208 $this->data['pe_status']= 'new'; 209 } 210 // if user linking has no ADD rights, the entry is set to ignored 211 if (!$this->check_acl(EGW_ACL_ADD,array('pm_id'=>$pm_id))) 212 { 213 $this->data['pe_status']= 'ignore'; 214 } 215 } 216 $datasource =& $this->datasource($app); 217 $this->updated = 0; 218 219 if (!($data = $datasource->read($id,$this->data))) 220 { 221 return false; // eg. no read access, so I cant update 222 } 223 foreach($data as $name => $value) 224 { 225 if (isset($datasource->name2id[$name]) && !($this->data['pe_overwrite'] & $datasource->name2id[$name]) && 226 $this->data[$name] != $value) 227 { 228 //if ((int) $pe_id) echo "<p>boprojectelements::update($app,$id,$pe_id,$pm_id) $name updated: '{$this->data[$name]}' != '$value'</p>\n"; 229 $this->data[$name] = $value; 230 $this->updated |= $datasource->name2id[$name]; 231 } 232 } 233 $this->data['pe_synced'] = $this->now_su; 234 235 if((int) $pe_id && ($need_save_anyway || $this->updated)) 236 { 237 $this->save(null,false,$update_project ? $this->updated & ~PM_TITLE & ~PM_DETAILS & ~PM_RESOURCES : 0); // dont set modified, only synced 238 } 239 return $this->data; 240 } 241 242 /** 243 * sync all project-elements 244 * 245 * The sync of the elements is done by calling the update-method for each (not ignored) element 246 * in the order of their planned starts and after that calling the projects update methode only 247 * once if necessary! 248 * 249 * @param int $pm_id=null id of project to use, default null=use $this->pm_id 250 * @return int number of updated elements 251 */ 252 function &sync_all($pm_id=null) 253 { 254 if (!is_array($GLOBALS['egw_info']['flags']['projectmanager']['sync_all_pm_id_visited'])) 255 { 256 $GLOBALS['egw_info']['flags']['projectmanager']['sync_all_pm_id_visited'] = array(); 257 } 258 if (!$pm_id && !($pm_id = $this->pm_id)) return 0; 259 260 if ((int) $this->debug >= 2 || $this->debug == 'sync_all') $this->debug_message("boprojectelements::sync_all(pm_id=$pm_id)"); 261 262 if ($GLOBALS['egw_info']['flags']['projectmanager']['sync_all_pm_id_visited'][$pm_id]) // project already visited 263 { 264 if ((int) $this->debug >= 2 || $this->debug == 'sync_all') $this->debug_message("boprojectelements::sync_all(pm_id=$pm_id) stoped recursion, as pm_id in (".implode(',',array_keys($GLOBALS['egw_info']['flags']['projectmanager']['sync_all_pm_id_visited'])).")"); 265 return 0; // no further recursion, might lead to an infinit loop 266 } 267 $GLOBALS['egw_info']['flags']['projectmanager']['sync_all_pm_id_visited'][$pm_id] = true; 268 269 $save_project = $this->project->data; 270 271 $updated = $update_project = 0; 272 ++$GLOBALS['egw_info']['flags']['projectmanager']['pm_ds_ignore_elements']; 273 foreach((array) $this->search(array('pm_id'=>$pm_id,"pe_status != 'ignore'"),false,'pe_planned_start') as $data) 274 { 275 $this->update($data['pe_app'],$data['pe_app_id'],$data['pe_id'],$pm_id,false); 276 277 $update_project |= $this->updated & ~PM_TITLE; 278 if ($this->updated) $updated++; 279 } 280 --$GLOBALS['egw_info']['flags']['projectmanager']['pm_ds_ignore_elements']; 281 if ($update_project) 282 { 283 $this->project->update($pm_id,$update_project); 284 } 285 if ($this->project->data['pm_id'] != $save_project['pm_id']) $this->project->data =& $save_project; 286 287 unset($GLOBALS['egw_info']['flags']['projectmanager']['sync_all_pm_id_visited'][$pm_id]); 288 289 return $updated; 290 } 291 292 /** 293 * checks if the user has enough rights for a certain operation 294 * 295 * The rights on a project-element depend on the rigths on the parent-project: 296 * - One can only read an element, if he can read the project (any rights, at least READ on the project) 297 * - Adding, editing and deleting of elements require the ADD right of the project (deleting requires the element to exist pe_id!=0) 298 * - reading or editing of budgets require the concerned rights of the project 299 * 300 * @param int $required EGW_ACL_READ, EGW_ACL_WRITE, EGW_ACL_ADD, EGW_ACL_DELETE, EGW_ACL_BUDGET or EGW_ACL_EDIT_BUDGET 301 * @param array/int $data=null project-element or pe_id to use, default the project-element in $this->data 302 * @return boolean true if the rights are ok, false if not 303 */ 304 function check_acl($required,$data=0) 305 { 306 $pe_id = is_array($data) ? $data['pe_id'] : ($data ? $data : $this->data['pe_id']); 307 $pm_id = is_array($data) ? $data['pm_id'] : ($data ? 0 : $this->data['pm_id']); 308 309 if (!$pe_id && (!$pm_id || $required == EGW_ACL_DELETE)) 310 { 311 return false; 312 } 313 if (!$pm_id) 314 { 315 $data_backup =& $this->data; unset($this->data); 316 $data =& $this->read($pe_id); 317 $this->data =& $data_backup; unset($data_backup); 318 319 if (!$data) return false; // not found ==> no rights 320 321 $pm_id = $data['pm_id']; 322 } 323 if ($required == EGW_ACL_EDIT ||$required == EGW_ACL_DELETE) 324 { 325 $required = EGW_ACL_ADD; // edit or delete of elements is handled by the ADD right of the project 326 } 327 return $this->project->check_acl($required,$pm_id); 328 } 329 330 /** 331 * Get reference to instance of the datasource used for $app 332 * 333 * The class has to be named datasource_$app and is search first in the App's inc-dir and then in the one of 334 * ProjectManager. If it's not found PM's datasource baseclass is used. 335 * 336 * @param string $app appname 337 * @return object 338 */ 339 function &datasource($app) 340 { 341 if (!isset($this->datasources[$app])) 342 { 343 if (!file_exists($classfile = EGW_INCLUDE_ROOT.'/'.$app.'/inc/class.'.($class='datasource_'.$app).'.inc.php') && 344 !file_exists($classfile = EGW_INCLUDE_ROOT.'/projectmanager/inc/class.'.($class='datasource_'.$app).'.inc.php')) 345 { 346 $classfile = EGW_INCLUDE_ROOT.'/projectmanager/inc/class.'.($class='datasource').'.inc.php'; 347 } 348 include_once($classfile); 349 $this->datasources[$app] =& new $class($app); 350 // make the project availible for the datasource 351 $this->datasources[$app]->project =& $this->project; 352 } 353 return $this->datasources[$app]; 354 } 355 356 /** 357 * changes the data from the db-format to your work-format 358 * 359 * reimplemented to adjust the timezone of the timestamps (adding $this->tz_offset_s to get user-time) 360 * Please note, we do NOT call the method of the parent or so_sql !!! 361 * 362 * @param array $data if given works on that array and returns result, else works on internal data-array 363 * @return array with changed data 364 */ 365 function db2data($data=null) 366 { 367 if (!is_array($data)) 368 { 369 $data = &$this->data; 370 } 371 foreach($this->timestamps as $name) 372 { 373 if (isset($data[$name]) && $data[$name]) $data[$name] += $this->tz_offset_s; 374 } 375 if (is_numeric($data['pe_completion'])) $data['pe_completion'] .= '%'; 376 if ($data['pe_app']) $data['pe_icon'] = $data['pe_app'].'/navbar'; 377 if ($data['pe_resources']) $data['pe_resources'] = explode(',',$data['pe_resources']); 378 379 return $data; 380 } 381 382 /** 383 * changes the data from your work-format to the db-format 384 * 385 * reimplemented to adjust the timezone of the timestamps (subtraction $this->tz_offset_s to get server-time) 386 * Please note, we do NOT call the method of the parent or so_sql !!! 387 * 388 * @param array $data if given works on that array and returns result, else works on internal data-array 389 * @return array with changed data 390 */ 391 function data2db($data=null) 392 { 393 if ($intern = !is_array($data)) 394 { 395 $data = &$this->data; 396 } 397 foreach($this->timestamps as $name) 398 { 399 if (isset($data[$name])) 400 { 401 if ($data[$name]) 402 { 403 $data[$name] -= $this->tz_offset_s; 404 } 405 else 406 { 407 $data[$name] = null; // so it's not used for min or max dates 408 } 409 } 410 } 411 if (substr($data['pe_completion'],-1) == '%') $data['pe_completion'] = (int) substr($data['pe_completion'],0,-1); 412 413 if (is_array($data['pe_resources'])) 414 { 415 $data['pe_resources'] = count($data['pe_resources']) ? implode(',',$data['pe_resources']) : null; 416 } 417 return $data; 418 } 419 420 /** 421 * saves an project-element, reimplemented from SO, to save the remark in the link, if $keys['update_remark'] 422 * 423 * @param array $keys=null if given $keys are copied to data before saveing => allows a save as 424 * @param boolean $touch_modified=true should modification date+user be set, default yes 425 * @param int $update_project=-1 update the data in the project (or'ed PM_ id's), default -1=everything 426 * @return int 0 on success and errno != 0 else 427 */ 428 function save($keys=null,$touch_modified=true,$update_project=-1) 429 { 430 if ((int) $this->debug >= 1 || $this->debug == 'save') $this->debug_message("boprojectelements::save(".print_r($keys,true).','.(int)$touch_modified.",$update_project) data=".print_r($this->data,true)); 431 432 if ($keys['update_remark'] || $this->data['update_remark']) 433 { 434 unset($keys['update_remark']); 435 unset($this->data['update_remark']); 436 $this->link->update_remark($this->data['pe_id'],$this->data['pe_remark']); 437 } 438 if ($keys) $this->data_merge($keys); 439 440 if ($touch_modified || !$this->data['pe_modified'] || !$this->data['pe_modifier']) 441 { 442 $this->data['pe_modified'] = $this->now_su; 443 $this->data['pe_modifier'] = $GLOBALS['egw_info']['user']['account_id']; 444 } 445 if (!$this->data['pm_id']) $this->data['pm_id'] = $this->pm_id; 446 447 if (!($err = parent::save())) 448 { 449 if (is_array($this->data['pe_constraints'])) 450 { 451 $this->constraints->save(array( 452 'pm_id' => $this->data['pm_id'], 453 'pe_id' => $this->data['pe_id'], 454 ) + $this->data['pe_constraints']); 455 } 456 if ($update_project) 457 { 458 $this->project->update($this->data['pm_id'],$update_project,$this->data); 459 } 460 } 461 return $err; 462 } 463 464 /** 465 * deletes a project-element or all project-elements of a project, reimplemented to remove the link too 466 * 467 * @param array/int $keys if given array with pm_id and/or pe_id or just an integer pe_id 468 * @return int affected rows, should be 1 if ok, 0 if an error 469 */ 470 function delete($keys=null) 471 { 472 if (!is_array($keys) && (int) $keys) 473 { 474 $keys = array('pe_id' => (int) $keys); 475 } 476 if (!is_null($keys)) 477 { 478 $pm_id = $keys['pm_id']; 479 $pe_id = $keys['pe_id']; 480 } 481 else 482 { 483 $pe_id = $this->data['pe_id']; 484 $pm_id = $this->data['pm_id']; 485 } 486 $ret = parent::delete($keys); 487 488 if ($pe_id) 489 { 490 // delete one link 491 $this->link->unlink($pe_id); 492 // update the project 493 $this->project->update($pm_id); 494 495 $this->constraints->delete(array('pe_id' => $pe_id)); 496 } 497 elseif ($pm_id) 498 { 499 // delete all links to project $pm_id 500 $this->link->unlink(0,'projectmanager',$pm_id); 501 } 502 return $ret; 503 } 504 505 /** 506 * reads row matched by key and puts all cols in the data array, reimplemented to also read the constraints 507 * 508 * @param array $keys array with keys in form internalName => value, may be a scalar value if only one key 509 * @param string/array $extra_cols string or array of strings to be added to the SELECT, eg. "count(*) as num" 510 * @param string $join='' sql to do a join, added as is after the table-name, eg. ", table2 WHERE x=y" or 511 * @return array/boolean data if row could be retrived else False 512 */ 513 function read($keys,$extra_cols='',$join=true) 514 { 515 if (!($data = parent::read($keys,$extra_cols,$join))) 516 { 517 return false; 518 } 519 $this->data['pe_constraints'] = $this->constraints->read(array( 520 'pm_id' => $this->data['pm_id'], 521 'pe_id' => $this->data['pe_id'], 522 )); 523 return $this->data; 524 } 525 526 /** 527 * reads the titles of all project-elements specified by $keys 528 * 529 * @param array $keys keys of elements to read, default empty = all of the project the class is instanciated for 530 * @return array with pe_id => lang(pe_app): pe_title pairs 531 */ 532 function &titles($keys=array()) 533 { 534 $titles = array(); 535 foreach((array) $this->search(array(),'pe_id,pe_title','pe_app,pe_title','','',false,'AND',false,$keys) as $element) 536 { 537 if ($element) $titles[$element['pe_id']] = lang($element['pe_app']).': '.$element['pe_title']; 538 } 539 return $titles; 540 } 541 542 /** 543 * echos a (preformatted / no-html) debug-message and evtl. log it to a file 544 * 545 * It uses the debug_message method of boprojectmanager 546 * 547 * @param string $msg 548 */ 549 function debug_message($msg) 550 { 551 $this->project->debug_message($msg); 552 } 553 554 /** 555 * Copies the elementtree from an other project 556 * 557 * This is done by calling the copy method of the datasource (if existent) and then calling update with the (new) app_id 558 * 559 * @param int $source 560 * @return boolean true on success, false otherwise 561 */ 562 function copytree($source) 563 { 564 if ((int) $this->debug >= 2 || $this->debug == 'copytree') $this->debug_message("boprojectelements::copytree($source) this->pm_id=$this->pm_id"); 565 566 $elements =& $this->search(array('pm_id' => $source),false,'pe_planned_start'); 567 if (!$elements) return true; 568 569 foreach($elements as $element) 570 { 571 $ds =& $this->datasource($element['pe_app']); 572 573 if (method_exists($ds,'copy')) 574 { 575 if ((int) $this->debug >= 3 || $this->debug == 'copytree') $this->debug_message("copying $element[pe_app]:$element[pe_app_id] $element[pe_title]"); 576 list($app_id,$link_id) = $ds->copy($element,$this->pm_id,$this->project->data); 577 } 578 else // no copy method, we just link again with that entry 579 { 580 if ((int) $this->debug >= 3 || $this->debug == 'copytree') $this->debug_message("linking $element[pe_app]:$element[pe_app_id] $element[pe_title]"); 581 $app_id = $element['pe_app_id']; 582 $link_id = $this->link->link('projectmanager',$this->pm_id,$element['pe_app'],$app_id,$element['pe_remark'],0,0,1); 583 } 584 if ((int) $this->debug >= 3 || $this->debug == 'copytree') $this->debug_message("calling update($element[pe_app],$app_id,$link_id,$this->pm_id,false);"); 585 586 if (!$app_id || !$link_id) continue; // something went wrong, eg. element no longer exists 587 588 $this->update($element['pe_app'],$app_id,$link_id,$this->pm_id,false); // false=no update of project itself => done once at the end 589 590 // copy evtl. overwriten content from the element 591 if (($need_save = $element['pe_overwrite'] != 0)) 592 { 593 foreach($ds->name2id as $name => $id) 594 { 595 if ($element['pe_overwrite'] & $id) 596 { 597 $this->data[$name] = $element[$name]; 598 } 599 } 600 $this->data['pe_overwrite'] = $element['pe_overwrite']; 601 } 602 // copy other element data 603 foreach(array('pl_id','pe_cost_per_time','cat_id','pe_share','pe_status') as $name) 604 { 605 if ($name == 'pe_status' && $element['pe_status'] != 'ignore') continue; // only copy ignored 606 607 if ($this->data[$name] != $element[$name]) 608 { 609 $this->data[$name] = $element[$name]; 610 $need_save = true; 611 } 612 } 613 if ($need_save) $this->save(null,true,false); 614 } 615 // now we do one update of our project 616 if ((int) $this->debug >= 3 || $this->debug == 'copytree') $this->debug_message("calling project->update() this->pm_id=$this->pm_id"); 617 $this->project->update(); 618 619 return true; 620 } 621 }
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 |