[ Index ]
 

Code source de Horde 3.1.3

Accédez au Source d'autres logiciels libresSoutenez Angelica Josefina !

title

Body

[fermer]

/lib/VFS/ -> smb.php (source)

   1  <?php
   2  /**
   3   * Stateless VFS implementation for a SMB server, based on smbclient.
   4   *
   5   * Required values for $params:
   6   * <pre>
   7   *   'username'  - The username with which to connect to the SMB server.
   8   *   'password'  - The password with which to connect to the SMB server.
   9   *   'hostspec'  - The SMB server to connect to.
  10   *   'port'      - The SMB port number to connect to.
  11   *   'share'     - The share to access on the SMB server.
  12   *   'smbclient' - The path to the 'smbclient' executable.
  13   * </pre>
  14   *
  15   * Optional values for $params:
  16   * <pre>
  17   *   'ipaddress' - The address of the server to connect to.
  18   * </pre>
  19   *
  20   * Functions not implemented:
  21   *   - changePermissions(): The SMB permission style does not fit with the
  22   *                          module.
  23   *
  24   * $Horde: framework/VFS/VFS/smb.php,v 1.11.2.8 2006/05/31 04:50:02 slusarz Exp $
  25   *
  26   * Codebase copyright 2002 Paul Gareau <paul@xhawk.net>.  Adapted with
  27   * permission by Patrice Levesque <wayne@ptaff.ca> from phpsmb-0.8 code, and
  28   * converted to the LGPL.  Please do not taunt original author, contact
  29   * Patrice Levesque or dev@lists.horde.org.
  30   *
  31   * See the enclosed file COPYING for license information (LGPL). If you
  32   * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
  33   *
  34   * @author  Paul Gareau <paul@xhawk.net>
  35   * @author  Patrice Levesque <wayne@ptaff.ca>
  36   * @since   Horde 3.1
  37   * @package VFS
  38   */
  39  class VFS_smb extends VFS {
  40  
  41      /**
  42       * List of additional credentials required for this VFS backend.
  43       *
  44       * @var array
  45       */
  46      var $_credentials = array('username', 'password');
  47  
  48      /**
  49       * List of permissions and if they can be changed in this VFS backend.
  50       *
  51       * @var array
  52       */
  53      var $_permissions = array(
  54          'owner' => array('read' => false, 'write' => false, 'execute' => false),
  55          'group' => array('read' => false, 'write' => false, 'execute' => false),
  56          'all'   => array('read' => false, 'write' => false, 'execute' => false));
  57  
  58      /**
  59       * Authenticates a user on the SMB server and share.
  60       *
  61       * @access private
  62       *
  63       * @return boolean|PEAR_Error  True on success or a PEAR_Error on failure.
  64       */
  65      function _connect()
  66      {
  67          $cmd = array('quit');
  68          $err = $this->_command('', $cmd);
  69          if (is_a($err, 'PEAR_Error')) {
  70              return PEAR::raiseError(_("Authentication to the SMB server failed."));
  71          }
  72          return true;
  73      }
  74  
  75      /**
  76       * Retrieves a file from the VFS.
  77       *
  78       * @param string $path  The pathname to the file.
  79       * @param string $name  The filename to retrieve.
  80       *
  81       * @return string  The file data.
  82       */
  83      function read($path, $name)
  84      {
  85          list($path, $name) = $this->_escapeShellCommand($path, $name);
  86          $temp = $this->_getTempFile();
  87          $cmd = array('get \"' . $name . '\" ' . $temp);
  88          $err = $this->_command($path, $cmd);
  89          if (is_a($err, 'PEAR_Error')) {
  90              return $err;
  91          }
  92          if (!file_exists($temp)) {
  93              return PEAR::raiseError(sprintf(_("Unable to open VFS file \"%s\"."), $this->_getPath($path, $name)));
  94          }
  95          if (function_exists('file_get_contents')) {
  96              $file = file_get_contents($temp);
  97          } else {
  98              $fp = fopen($temp, 'r');
  99              $file = fread($fp, filesize($temp));
 100              fclose($fp);
 101          }
 102          unlink($temp);
 103          if ($file) {
 104              return $file;
 105          } else {
 106              return PEAR::raiseError(sprintf(_("Unable to open VFS file \"%s\"."), $this->_getPath($path, $name)));
 107          }
 108      }
 109  
 110      /**
 111       * Stores a file in the VFS.
 112       *
 113       * @param string $path         The path to store the file in.
 114       * @param string $name         The filename to use.
 115       * @param string $tmpFile      The temporary file containing the data to be
 116       *                             stored.
 117       * @param boolean $autocreate  Automatically create directories?
 118       *
 119       * @return boolean|PEAR_Error  True on success or a PEAR_Error on failure.
 120       */
 121      function write($path, $name, $tmpFile, $autocreate = false)
 122      {
 123          // Double quotes not allowed in SMB filename.
 124          $name = str_replace('"', "'", $name);
 125  
 126          list($path, $name) = $this->_escapeShellCommand($path, $name);
 127          $cmd = array('put \"' . $tmpFile . '\" \"' . $name . '\"');
 128          // do we need to first autocreate the directory?
 129          if ($autocreate) {
 130              $result = $this->autocreatePath($path);
 131              if (is_a($result, 'PEAR_Error')) {
 132                  return $result;
 133              }
 134          }
 135          $err = $this->_command($path, $cmd);
 136          if (is_a($err, 'PEAR_Error')) {
 137              return $err;
 138          }
 139          return true;
 140      }
 141  
 142      /**
 143       * Stores a file in the VFS from raw data.
 144       *
 145       * @param string $path         The path to store the file in.
 146       * @param string $name         The filename to use.
 147       * @param string $data         The file data.
 148       * @param boolean $autocreate  Automatically create directories?
 149       *
 150       * @return boolean|PEAR_Error  True on success or a PEAR_Error on failure.
 151       */
 152      function writeData($path, $name, $data, $autocreate = false)
 153      {
 154          $tmpFile = $this->_getTempFile();
 155          $fp = fopen($tmpFile, 'wb');
 156          fwrite($fp, $data);
 157          fclose($fp);
 158          $result = $this->write($path, $name, $tmpFile, $autocreate);
 159          unlink($tmpFile);
 160          return $result;
 161      }
 162  
 163      /**
 164       * Deletes a file from the VFS.
 165       *
 166       * @param string $path  The path to delete the file from.
 167       * @param string $name  The filename to use.
 168       *
 169       * @return boolean|PEAR_Error  True on success or a PEAR_Error on failure.
 170       */
 171      function deleteFile($path, $name)
 172      {
 173          list($path, $name) = $this->_escapeShellCommand($path, $name);
 174          $cmd = array('del \"' . $name . '\"');
 175          $err = $this->_command($path, $cmd);
 176          if (is_a($err, 'PEAR_Error')) {
 177              return $err;
 178          }
 179          return true;
 180      }
 181  
 182      /**
 183       * Checks if a given pathname is a folder.
 184       *
 185       * @param string $path  The path to the folder.
 186       * @param string $name  The file or folder name.
 187       *
 188       * @return boolean  True if it is a folder, false otherwise.
 189       */
 190      function isFolder($path, $name)
 191      {
 192          list($path, $name) = $this->_escapeShellCommand($path, $name);
 193          $cmd = array('quit');
 194          $err = $this->_command($this->_getPath($path, $name), $cmd);
 195          if (is_a($err, 'PEAR_Error')) {
 196              return false;
 197          }
 198          return true;
 199      }
 200  
 201      /**
 202       * Deletes a folder from the VFS.
 203       *
 204       * @param string $path        The path to delete the folder from.
 205       * @param string $name        The name of the folder to delete.
 206       * @param boolean $recursive  Force a recursive delete?
 207       *
 208       * @return boolean|PEAR_Error  True on success or a PEAR_Error on failure.
 209       */
 210      function deleteFolder($path, $name, $recursive = false)
 211      {
 212          $isDir = false;
 213          $dirCheck = $this->listFolder($path);
 214          foreach ($dirCheck as $file) {
 215              if ($file['name'] == $name && $file['type'] == '**dir') {
 216                  $isDir = true;
 217                  break;
 218              }
 219          }
 220  
 221          if (!$isDir) {
 222              return PEAR::raiseError(sprintf(_("\"%s\" is not a directory."), $path . '/' . $name));
 223          }
 224  
 225          $file_list = $this->listFolder($this->_getPath($path, $name));
 226          if (is_a($file_list, 'PEAR_Error')) {
 227              return $file_list;
 228          }
 229  
 230          if ($file_list && !$recursive) {
 231              return PEAR::raiseError(sprintf(_("Unable to delete \"%s\", the directory is not empty."),
 232                                              $this->_getPath($path, $name)));
 233          }
 234  
 235          foreach ($file_list as $file) {
 236              if ($file['type'] == '**dir') {
 237                  $result = $this->deleteFolder($this->_getPath($path, $name), $file['name'], $recursive);
 238              } else {
 239                  $result = $this->deleteFile($this->_getPath($path, $name), $file['name']);
 240              }
 241              if (is_a($result, 'PEAR_Error')) {
 242                  return $result;
 243              }
 244          }
 245  
 246          // Really delete the folder.
 247          list($path, $name) = $this->_escapeShellCommand($path, $name);
 248          $cmd = array('rmdir \"' . $name . '\"');
 249          $err = $this->_command($path, $cmd);
 250          if (is_a($err, 'PEAR_Error')) {
 251              return PEAR::raiseError(sprintf(_("Unable to delete VFS folder \"%s\"."), $this->_getPath($path, $name)));
 252          } else {
 253              return true;
 254          }
 255      }
 256  
 257      /**
 258       * Renames a file in the VFS.
 259       *
 260       * @param string $oldpath  The old path to the file.
 261       * @param string $oldname  The old filename.
 262       * @param string $newpath  The new path of the file.
 263       * @param string $newname  The new filename.
 264       *
 265       * @return boolean|PEAR_Error  True on success or a PEAR_Error on failure.
 266       */
 267      function rename($oldpath, $oldname, $newpath, $newname)
 268      {
 269          if (is_a($result = $this->autocreatePath($newpath), 'PEAR_Error')) {
 270              return $result;
 271          }
 272  
 273          // Double quotes not allowed in SMB filename.
 274          $newname = str_replace('"', "'", $newname);
 275  
 276          list($file, $name) = $this->_escapeShellCommand($oldname, $newname);
 277          $cmd = array('rename \"' .  str_replace('/', '\\\\', $oldpath) . '\\' . $file . '\" \"' .
 278                                      str_replace('/', '\\\\', $newpath) . '\\' . $name . '\"');
 279          if (is_a($err = $this->_command('', $cmd), 'PEAR_Error')) {
 280              return PEAR::raiseError(sprintf(_("Unable to rename VFS file \"%s\"."), $this->_getPath($path, $name)));
 281          }
 282  
 283          return true;
 284      }
 285  
 286      /**
 287       * Creates a folder on the VFS.
 288       *
 289       * @param string $path  The path of directory to create folder.
 290       * @param string $name  The name of the new folder.
 291       *
 292       * @return boolean|PEAR_Error  True on success or a PEAR_Error on failure.
 293       */
 294      function createFolder($path, $name)
 295      {
 296          // Double quotes not allowed in SMB filename.
 297          $name = str_replace('"', "'", $name);
 298  
 299          list($dir, $mkdir) = $this->_escapeShellCommand($path, $name);
 300          $cmd = array('mkdir \"' . $mkdir . '\"');
 301          $err = $this->_command($dir, $cmd);
 302          if (is_a($err, 'PEAR_Error')) {
 303              return PEAR::raiseError(sprintf(_("Unable to create VFS folder \"%s\"."), $this->_getPath($path, $name)));
 304          }
 305          return true;
 306      }
 307  
 308      /**
 309       * Returns an unsorted file list.
 310       *
 311       * @param string $path       The path of the directory to get the file list
 312       *                           for.
 313       * @param mixed $filter      Hash of items to filter based on filename.
 314       * @param boolean $dotfiles  Show dotfiles? This is irrelevant with
 315       *                           smbclient.
 316       * @param boolean $dironly   Show directories only?
 317       *
 318       * @return boolean|PEAR_Error  File list on success or a PEAR_Error on
 319       *                             failure.
 320       */
 321      function listFolder($path = '', $filter = null, $dotfiles = true, $dironly = false)
 322      {
 323          list($path) = $this->_escapeShellCommand($path);
 324          $cmd = array('ls');
 325          $res = $this->_command($path, $cmd);
 326          if (is_a($res, 'PEAR_Error')) {
 327              return $res;
 328          }
 329          $num_lines = count($res);
 330          $files = array();
 331          for ($r = 0; $r < $num_lines; $r++) {
 332              // Match file listing.
 333              if (preg_match('/^(\s\s.+\s{6,})/', $res[$r])) {
 334                  // Split into columns at every six spaces
 335                  $split1 = preg_split('/\s{6,}/', trim($res[$r]));
 336                  // If the file name isn't . or ..
 337                  if ($split1[0] != '.' && $split1[0] != '..') {
 338                      if (isset($split1[2])) {
 339                          // If there is a small file size, inf could be split
 340                          // into 3 cols.
 341                          $split1[1] .= ' ' . $split1[2];
 342                      }
 343                      // Split file inf at every one or more spaces.
 344                      $split2 = preg_split('/\s+/', $split1[1]);
 345                      if (is_numeric($split2[0])) {
 346                          // If there is no file attr, shift cols over.
 347                          array_unshift($split2, '');
 348                      }
 349                      $my_name = $split1[0];
 350  
 351                      // Filter out dotfiles if they aren't wanted.
 352                      if (!$dotfiles && substr($my_name, 0, 1) == '.') {
 353                          continue;
 354                      }
 355  
 356                      $my_size = $split2[1];
 357                      $ext_name = explode('.', $my_name);
 358  
 359                      if ((strpos($split2[0], 'D') !== false)) {
 360                          $my_type = '**dir';
 361                          $my_size = -1;
 362                      } else {
 363                          $my_type = VFS::strtolower($ext_name[count($ext_name) - 1]);
 364                      }
 365                      $my_date = strtotime($split2[4] . ' ' . $split2[3] . ' ' .
 366                                           $split2[6] . ' ' . $split2[5]);
 367                      $filedata = array('owner' => '',
 368                                        'group' => '',
 369                                        'perms' => '',
 370                                        'name' => $my_name,
 371                                        'type' => $my_type,
 372                                        'date' => $my_date,
 373                                        'size' => $my_size);
 374                      // watch for filters and dironly
 375                      if ($this->_filterMatch($filter, $my_name)) {
 376                          unset($file);
 377                          continue;
 378                      }
 379                      if ($dironly && $my_type !== '**dir') {
 380                          unset($file);
 381                          continue;
 382                      }
 383  
 384                      $files[$filedata['name']] = $filedata;
 385                  }
 386              }
 387          }
 388          return $files;
 389      }
 390  
 391      /**
 392       * Returns a sorted list of folders in specified directory.
 393       *
 394       * @param string $path         The path of the directory to get the
 395       *                             directory list for.
 396       * @param mixed $filter        Hash of items to filter based on folderlist.
 397       * @param boolean $dotfolders  Include dotfolders? Irrelevant for SMB.
 398       *
 399       * @return boolean|PEAR_Error  Folder list on success or a PEAR_Error on
 400       *                             failure.
 401       */
 402      function listFolders($path = '', $filter = null, $dotfolders = true)
 403      {
 404          $folders = array();
 405          $folder = array();
 406  
 407          $folderList = $this->listFolder($path, null, $dotfolders, true);
 408          if (is_a($folderList, 'PEAR_Error')) {
 409              return $folderList;
 410          }
 411  
 412          // dirname will strip last component from path, even on a directory
 413          $folder['val'] = dirname($path);
 414          $folder['abbrev'] = '..';
 415          $folder['label'] = '..';
 416  
 417          $folders[$folder['val']] = $folder;
 418  
 419          foreach ($folderList as $files) {
 420              $folder['val'] = $this->_getPath($path, $files['name']);
 421              $folder['abbrev'] = $files['name'];
 422              $folder['label'] = $folder['val'];
 423  
 424              $folders[$folder['val']] = $folder;
 425          }
 426  
 427          ksort($folders);
 428          return $folders;
 429      }
 430  
 431      /**
 432       * Copies a file through the backend.
 433       *
 434       * @param string $path  The path to store the file in.
 435       * @param string $name  The filename to use.
 436       * @param string $dest  The destination of the file.
 437       *
 438       * @return boolean|PEAR_Error  True on success or a PEAR_Error on failure.
 439       */
 440      function copy($path, $name, $dest)
 441      {
 442          $orig = $this->_getPath($path, $name);
 443          if (preg_match('|^' . preg_quote($orig) . '/?$|', $dest)) {
 444              return PEAR::raiseError(_("Cannot copy file(s) - source and destination are the same."));
 445          }
 446  
 447          $fileCheck = $this->listFolder($dest, null, true);
 448          foreach ($fileCheck as $file) {
 449              if ($file['name'] == $name) {
 450                  return PEAR::raiseError(sprintf(_("%s already exists."),
 451                                                  $this->_getPath($dest, $name)));
 452              }
 453          }
 454  
 455          $isDir = false;
 456          $dirCheck = $this->listFolder($path, null, false);
 457          foreach ($dirCheck as $file) {
 458              if ($file['name'] == $name && $file['type'] == '**dir') {
 459                  $isDir = true;
 460                  break;
 461              }
 462          }
 463  
 464          if ($isDir) {
 465              $result = $this->createFolder($dest, $name);
 466  
 467              if (is_a($result, 'PEAR_Error')) {
 468                  return $result;
 469              }
 470  
 471              $file_list = $this->listFolder($orig);
 472              foreach ($file_list as $file) {
 473                  $result = $this->copy($orig,
 474                                        $file['name'],
 475                                        $this->_getPath($dest, $name));
 476                  if (is_a($result, 'PEAR_Error')) {
 477                      return $result;
 478                  }
 479              }
 480          } else {
 481              $tmpFile = $this->_createTempFile($path, $name);
 482              if (is_a($tmpFile, 'PEAR_Error')) {
 483                  return PEAR::raiseError(sprintf(_("Failed to retrieve: %s"), $orig));
 484              }
 485  
 486              $res = $this->write($dest, $name, $tmpFile);
 487              unlink($tmpFile);
 488              if (is_a($res, 'PEAR_Error')) {
 489                  return PEAR::raiseError(sprintf(_("Copy failed: %s"),
 490                                                  $this->_getPath($dest, $name)));
 491              }
 492          }
 493  
 494          return true;
 495      }
 496  
 497      /**
 498       * Moves a file through the backend.
 499       *
 500       * @param string $path  The path to store the file in.
 501       * @param string $name  The filename to use.
 502       * @param string $dest  The destination of the file.
 503       *
 504       * @return boolean|PEAR_Error  True on success or a PEAR_Error on failure.
 505       */
 506      function move($path, $name, $dest)
 507      {
 508          $orig = $this->_getPath($path, $name);
 509          if (preg_match('|^' . preg_quote($orig) . '/?$|', $dest)) {
 510              return PEAR::raiseError(_("Cannot move file(s) - destination is within source."));
 511          }
 512  
 513          $fileCheck = $this->listFolder($dest, null, true);
 514          foreach ($fileCheck as $file) {
 515              if ($file['name'] == $name) {
 516                  return PEAR::raiseError(sprintf(_("%s already exists."),
 517                                                  $this->_getPath($dest, $name)));
 518              }
 519          }
 520  
 521          $err = $this->rename($path, $name, $dest, $name);
 522          if (is_a($err, 'PEAR_Error')) {
 523              return PEAR::raiseError(sprintf(_("Failed to move to \"%s\"."),
 524                                              $this->_getPath($dest, $name)));
 525          }
 526          return true;
 527      }
 528  
 529      /**
 530       * Returns the full path of an item.
 531       *
 532       * @param string $path  The path of directory of the item.
 533       * @param string $name  The name of the item.
 534       *
 535       * @return mixed  Full path when $path isset and just $name when not set.
 536       */
 537      function _getPath($path, $name)
 538      {
 539          if ($path !== '') {
 540               return ($path . '/' . $name);
 541          }
 542          return $name;
 543      }
 544  
 545      /**
 546       * Replacement for escapeshellcmd(), variable length args, as we only want
 547       * certain characters escaped.
 548       *
 549       * @access private
 550       *
 551       * @param array $array  Strings to escape.
 552       *
 553       * @return array
 554       */
 555      function _escapeShellCommand()
 556      {
 557          $ret = array();
 558          $args = func_get_args();
 559          foreach ($args as $arg) {
 560              $ret[] = str_replace(array(';', '\\'), array('\;', '\\\\'), $arg);
 561          }
 562          return $ret;
 563      }
 564  
 565      /**
 566       * Executes a command and returns output lines in array.
 567       *
 568       * @access private
 569       *
 570       * @param string $cmd  Command to be executed
 571       *
 572       * @return mixed  Array on success, false on failure.
 573       */
 574      function _execute($cmd)
 575      {
 576          $cmd = str_replace('"-U%"', '-N', $cmd);
 577          exec($cmd, $out, $ret);
 578  
 579          // In some cases, (like trying to delete a nonexistant file),
 580          // smbclient will return success (at least on 2.2.7 version I'm
 581          // testing on). So try to match error strings, even after success.
 582          if ($ret != 0) {
 583              $err = '';
 584              foreach ($out as $line) {
 585                  if (strpos($line, 'Usage:') === 0) {
 586                      $err = 'Command syntax incorrect';
 587                      break;
 588                  }
 589                  if (strpos($line, 'ERRSRV') !== false ||
 590                      strpos($line, 'ERRDOS') !== false) {
 591                      $err = preg_replace('/.*\((.+)\).*/', '\\1', $line);
 592                      if (!$err) {
 593                          $err = $line;
 594                      }
 595                      break;
 596                  }
 597              }
 598              if (!$err) {
 599                  $err = $out ? $out[count($out) - 1] : $ret;
 600              }
 601              return PEAR::raiseError($err);
 602          }
 603  
 604          // Check for errors even on success.
 605          $err = '';
 606          foreach ($out as $line) {
 607              if (strpos($line, 'NT_STATUS_NO_SUCH_FILE') !== false ||
 608                  strpos($line, 'NT_STATUS_OBJECT_NAME_NOT_FOUND') !== false) {
 609                  $err = _("No such file");
 610                  break;
 611              } elseif (strpos($line, 'NT_STATUS_ACCESS_DENIED') !== false) {
 612                  $err = _("Permission Denied");
 613                  break;
 614              }
 615          }
 616  
 617          if ($err) {
 618              return PEAR::raiseError($err);
 619          }
 620  
 621          return $out;
 622      }
 623  
 624      /**
 625       * Executes SMB commands - without authentication - and returns output
 626       * lines in array.
 627       *
 628       * @access private
 629       *
 630       * @param array $path  Base path for command.
 631       * @param array $cmd   Commands to be executed.
 632       *
 633       * @return mixed  Array on success, false on failure.
 634       */
 635      function _command($path, $cmd)
 636      {
 637          list($share) = $this->_escapeShellCommand($this->_params['share']);
 638          putenv('PASSWD=' . $this->_params['password']);
 639          $ipoption = (isset($this->_params['ipaddress'])) ? (' -I ' . $this->_params['ipaddress']) : null;
 640          $fullcmd = $this->_params['smbclient'] .
 641              ' "//' . $this->_params['hostspec'] . '/' . $share . '"' .
 642              ' "-p' . $this->_params['port'] . '"' .
 643              ' "-U' . $this->_params['username'] . '"' .
 644              ' -D "' . $path . '" ' .
 645              $ipoption .
 646              ' -c "';
 647          foreach ($cmd as $c) {
 648              $fullcmd .= $c . ";";
 649          }
 650          $fullcmd .= '"';
 651          return $this->_execute($fullcmd);
 652      }
 653  
 654      /**
 655       * Retrieves a file from the VFS and stores it to a temporary file.
 656       *
 657       * @access private
 658       *
 659       * @param string $path  The pathname to the file.
 660       * @param string $name  The filename to retrieve.
 661       *
 662       * @return mixed  The temporary filename or a PEAR_Error on failure.
 663       */
 664      function _createTempFile($path, $name)
 665      {
 666          list($path, $name) = $this->_escapeShellCommand($path, $name);
 667          $temp = $this->_getTempFile();
 668          $cmd = array('get \"' . $name . '\" ' . $temp);
 669          $err = $this->_command($path, $cmd);
 670          if (is_a($err, 'PEAR_Error')) {
 671              return $err;
 672          }
 673          if (!file_exists($temp)) {
 674              return PEAR::raiseError(sprintf(_("Unable to open VFS file \"%s\"."), $this->_getPath($path, $name)));
 675          }
 676          return $temp;
 677      }
 678  
 679  }


Généré le : Sun Feb 25 18:01:28 2007 par Balluche grâce à PHPXref 0.7