<?php
//
//=========================================================
// DepotServer.php  Implements a Perforce WebDAV server
//=========================================================
// +----------------------------------------------------------------------+
// | PHP Version 4 and 5                                                  |
// | Original Name = FileSystem.php                                       |
// +----------------------------------------------------------------------+
// | Author: Daniel Sabsay <danielsabsay@pacbell.net>                     |
// |                                                                      |
// | Based on a model by: Hartmut Holzgraefe <hholzgra@php.net>           |
// |                      Christian Stocker <chregu@bitflux.ch>           |
// +----------------------------------------------------------------------+
//
  require_once realpath('.').'/DAVServer.php';
  define ('CR',"\n");
  define ('DIRR','httpd/unix-directory');
  define ('DIRP','DavDirectoryPlaceholder');
  //  prohibited characters in file names: @ %40, # %23, * %2A, % %25

  /**
   * Perforce depot access using local p4 client
   *
   * @access public
   */
class Depot_Filesystem extends HTTP_WebDAV_Server
{
  /**
   * Root directory for WebDAV access
   *
   * @access private
   * @var    string
   */
  var $base = "";

  /**
   * Logging control: 0=none, 1=connection, 2=detail 3=debug
   *
   * @access private
   * @var    integer
   */
  var $logging = 0;

  /** 
   * Perforce Host connection parameters
   *
   * @access private
   * @var    string
   */
  var $depot_host = 'public.perforce.com';
  var $depot_port = '1666';
  var $depot_agent = '/usr/local/bin/p4';

  /**
   * Perforce user & password
   *
   * @access private
   * @var    string
   */
  var $depot_user;
  var $depot_passwd;

  /**
   * Perforce Admin user & password
   *
   * Used ONLY for the removal of placeholder files
   *  that force new directories to be "visible"
   *
   * @access private
   * @var    string
   */
  var $admin_user;
  var $admin_passwd;

  /**
   * Serve a webdav request
   *
   * @access public
   * @param  string  
   */
  function ServeRequest($method) 
  {                           
  // let the base class do all the work
    parent::ServeRequest($method);    
  }

  /**
   * PERFORCE USER AUTHORIZATION
   *
   * @access private
   * @return bool true on successful authentication
   */
  function _check_auth() 
  {
    if ($this->logging>2) {
      error_log('**Perforce P4 version='.$this->sp4cmd('-V','',''));
    }
    if (empty($_SERVER['PHP_AUTH_USER'])) return false;
    // Authenticate with the Perforce repository server
    $this->depot_user = $_SERVER['PHP_AUTH_USER'];
    $this->depot_passwd = $_SERVER['PHP_AUTH_PW'];
    $r = $this->sp4cmd('user','-o','');
    $p = strpos($r,'Password:'); // skip the pro-forma password
    // This second password label only appears if login was
    //   accepted by the Perforce repository.
    if (strpos(substr($r,$p+9),'Password:')) return true;
    $p4 = strpos($this->sp4cmd('-V','',''),'Perforce Software');
    if ($p4===false) error_log('**** P4 client module not found at '.$this->depot_agent);
    if ($this->logging>1) error_log('**Login error, user='.$this->depot_user);
    return false;
  } // end _check_auth

  /**
   * PROPFIND method handler
   *
   * @param  array  general parameter passing array
   * @param  array  return array for file properties
   * @return bool   true on success
   */
  function PROPFIND(&$options, &$files) 
  {
    $path = $options['path'];
    $p4path = '/'.$path;
    $datePat = array('date'=>'time ');
    $dirPat = array('path'=>'dir /');
    $depotPat = array('raw'=>'Depot ');
    $filePat = array('path'=>'depotFile /',
                    'size'=>'fileSize ',
                    'date'=>'headTime ');
    if ($p4path=='//') {
      $group['self'] = array(array('path'=>''));
      if (!empty($options['depth'])) { // skip if self only (depth==0)
        $group['depots'] = $this->p4list('depots','',$depotPat);
      }
    } else {
      if (substr($path,-1)=='/') {
        $is_dir = true; // Assume it's a  directory
        $path = substr($path,0,strlen($path)-1);  // drop trailing separator
        $p4path = '/'.$path;
      } else { // else might be a file
        $self_file = $this->p4list('fstat -Ol',$p4path,$filePat);
        if (empty($self_file)) { $is_dir = true; }
        else { $group['files'] = $self_file[0]; }
      }
      if ($is_dir) { // Not a file, probably a directory
        $c_dirs = count($this->p4list('dirs -D',$p4path,$dirPat)); // dir exists?
        $c_dirs += count($this->p4list('depots','',array('x'=>'Depot '.basename($path))));
        if (!$c_dirs) { return false; } // none of the above, then doesn't exist
        if (strpos($options['depth'],'noroot')===false) { // Omit self ?
          $group['self'] = array(array('path'=>$path));
        }
        if (!empty($options['depth'])) { // skip if self only (depth==0)
          $group['dirs']=$this->p4list('dirs -D',$p4path.'/*',$dirPat);     
          // Deleted files are not shown because they do not exhibit a filesize
          //  attribute with the "Ol" option, and are therefore ignored by p4list.
          $group['files'] = $this->p4list('fstat -Ol',$p4path.'/*',$filePat);
          //$group['config']=array($path.'/**my_configuration**');
        }
      }
    }
    $files['files'] = array(); // initialize item stack
    foreach ($group as $group_type=>$children) {
      foreach ($children as $child) {
        switch ($group_type) {
        case 'depots':
          $child['path'] = '/'.substr($child['raw'],0,strpos($child['raw'],' '));
          // fall thru
        case 'dirs':
          $p4subDirs = '/'.$child['path'].'/*';
          $lastMod = $this->p4list('changes -m1',$p4subDirs,$datePat);
          $child['date'] = $lastMod[0]['date'];
          // fall thru
        case 'self':
          $child['name'] = basename($child['path']);
          $child['path'] .= '/'; // sub-directories get trailing separator
          $child['rez'] = 'collection';
          $child['type'] = DIRR;
          break;
        case 'files':
          $child['name'] = basename($child['path']);
          $child['type'] = $this->_mimetype($child['path']);
        }
        if ($child['name'] == DIRP) continue; // ignore any placeholder leakage
        $props = array(); // initialize property array
        array_push($props,$this->mkprop('getcontentlength',$child['size']));
        array_push($props,$this->mkprop('resourcetype',$child['rez']));
        array_push($props,$this->mkprop('getcontenttype',$child['type']));
        array_push($props,$this->mkprop('displayname',$child['name']));
        array_push($props,$this->mkprop('getlastmodified',$child['date']));
        array_push($props,$this->mkprop('creationdate',0));
        // Summarize properties for this item
        array_push($files['files'],array('path'=>$child['path'],'props'=>$props));
      }
    }
  return true;
  } // end PROPFIND


  /**
   * try to detect the mime type of a file
   *
   * @param  string  file path
   * @return string  guessed mime type
   */
  function _mimetype($path) 
  {
    // Fallback solution: try to guess the type by the file extension
    // TODO: add more ...
    // TODO: it has been suggested to delegate mimetype detection 
    //       to apache but this has at least three issues:
    //       - works only with apache
    //       - needs file to be within the document tree
    //       - requires apache mod_magic 
    // TODO: can we use the registry for this on Windows?
    //       OTOH if the server is Windos the clients are likely to 
    //       be Windows, too, and tend do ignore the Content-Type
    //       anyway (overriding it with information taken from
    //       the registry)
    // TODO: have a seperate PEAR class for mimetype detection?
      switch (strtolower(strrchr(basename($path), "."))) {
      case ".html":
          $mime_type = "text/html";
          break;
      case ".gif":
          $mime_type = "image/gif";
          break;
      case ".jpg":
          $mime_type = "image/jpeg";
          break;
      case ".png":
          $mime_type = "image/png";
          break;
      case ".txt":
          $mime_type = "text/plain";
          break;
      case ".xml":
          $mime_type = "text/xml";
          break;
      case ".php":
          $mime_type = "text/php";
          break;
      default: 
          $mime_type = "application/octet-stream";
          break;
      }
    return $mime_type;
  }

  /**
   * GET method handler
   * 
   * @param  array  parameter passing array
   * @return bool   true on success
   */
  function GET(&$options) 
  {
    $path = $options['path'];
    $p4dir = '/'.dirname($path);
    $node = basename($path);
    $p4path = $p4dir.'/'.$node; // drop possible trailing separator
    $fileAPat = array('path'=>'depotFile /','action'=>'action ','date'=>'time ');
    $exists = $this->p4list('files',$p4path,$fileAPat);
    if (empty($exists)) return false;
    if ($exists[0]['action']=='delete') return false;
    $mod_date = $exists[0]['date'];
    header('Accept-Ranges: bytes');
    header('ETag: "'.uniqid('E').'"');
    $mod_since = $hdrs['If-Modified-Since'];
    if ($mod_since) {
      if (strtotime($mod_since) >= $mod_date) return '304 Not Modified';
    }
    $file = $this->base.uniqid('F'); // Generate temporary filename
    $this->sp4cmd('print -q -o '.$file,'"'.$p4path.'"',''); // read data from depot
    $options['mimetype'] = $this->_mimetype($path);
    $options['mtime'] = $mod_date;
    $options['size'] = filesize($file);
    $fr = fopen($file,'r');
    $fail = !$fr;
    if ($fail) {
      error_log('*** Filesystem permission error ***');
      unlink($file);
    } else {
      $options['stream'] = $fr; // Open stream to temp file
      $options['delete_path'] = $file; // Mark temp for disposal
    }
    return ($fail)? false:true;
  } // end GET
  
  function HEAD(&$params) 
   {
    $status = $this->GET($params);
    if ($status===false) return false;
    header('Content-Length: '.$params['size']);
    fclose($params['stream']);
    unlink($params['delete_path']);
    return $status;
   } // end HEAD
  
  /**
   * PUT method handler
   * 
   * @param  array  parameter passing array
   * @return bool   true on success
   */
  function PUT(&$options) 
  {
    $path = $options['path'];
    $p4dir = '/'.dirname($path);
    $node = basename($path);
    $p4path = $p4dir.'/'.$node; // drop possible trailing separator
    if ($p4path=='//') return '403 Forbidden, cannot make depot';
    $uid = uniqid('');
    $ws_id = 'W'.$uid;
    $dir = $this->base.'D'.$uid; // temp filesystem directory name
    // Create a temporary private directory on the server
    $fail = (mkdir($dir)===false); // make temp directory
    if (!$fail) { // connect to HTTP data stream
      $fr = $options['stream'];
      $fail = ($fr===false);
    }
    if (!$fail) { // open temp file for data
      $file = $dir.'/'.$node;
      $fw = fopen($file,'w');
      $fail = ($fw===false);
    }
    if (!$fail) { // Copy file contents from the user's HTTP request
      while (!feof($fr)) fwrite($fw,fread($fr,4096));
      fclose($fr); fclose($fw);
    }
    if (!$fail) $fail = !$this->p4work($ws_id,$p4dir,$dir); // new workspace
    if (!$fail) $this->sp4cmd('flush','"'.$p4path.'"','-c '.$ws_id);
    if (!$fail) { // open for EDIT or ADD depending on current existence
      $filePatA = array('path'=>'depotFile /','action'=>'action ');
      $exists = $this->p4list('files',$p4path,$filePatA);
      if (empty($exists)) { $update = 'add'; }
      else { $update = ($exists[0]['action']=='delete')? 'add':'edit'; }
      $x = $this->sp4cmd($update,'"'.$p4path.'"','-c '.$ws_id);
      $fail = (strpos($x,'opened for')===false);
      if ($fail) error_log('***'.$update.' '.$x);
    } // SUBMIT the change
    if (!$fail) $fail = !$this->p4submit($ws_id,$update,$p4path);
    $this->sp4cmd('client','-d -f '.$ws_id,''); // release workspace
    if (!$fail) {
      if (!strpos($this->sp4cmd('files',$p4dir.'/'.DIRP,''),'no such')) {
        $this->p4faux_drop($p4dir); // remove faux dir placeholder if present
      }
    }
    unlink($file); // delete temp file
    rmdir($dir); // delete temp directory
    return ($fail)? false:true;
  }  // end PUT

  /**
   * MKCOL method handler
   *
   * @param  array  general parameter passing array
   * @return bool   true on success
   */
  function MKCOL($options) 
  {           
    $path = $options['path'];
    $p4dir = '/'.dirname($path);
    $p4path = $p4dir.'/'.basename($path); // drop possible trailing separator
    $uid = uniqid('');
    $ws_id = 'W'.$uid;
    $dir = $this->base.'D'.$uid; // temp filesystem directory name
    if ($p4path=='//') return '403 Forbidden, cannot make depot';
    // Parent directory must already exist
    if (count(split('/',$path))>3) { // Regular directory or depot?
      $exists = $this->p4list('dirs -D',$p4dir,array('x'=>'dir /'));
    } else {
      $exists = $this->p4list('depots','',array('x'=>'Depot '.substr(dirname($path),1)));
    }
    if (empty($exists)) return '403 Forbidden, no parent directory';
    // Directory with the same name must not already exist
    $exists = $this->p4list('files',$p4path,array('path'=>'dir /'));
    if (!empty($exists)) return '405 Not allowed, directory already exists';
    // File with the same name must not already exist   
    $exists = $this->p4list('files',$p4path,array('path'=>'depotFile /'));
    if (!empty($exists)) return '409 Conflict, file already exists';
    if (!empty($_SERVER['CONTENT_LENGTH'])) { // no body parsing yet
      return '415 Unsupported media type';
    }

    // Create a persistent empty (faux) directory by putting
    //   a "deleted" placeholder file inside.
    
    $fail = !$this->p4faux_make($p4path);
    return ($fail)? false:'201 Created';
  } // end MKCOL
  
  /**
   * DELETE method handler
   *
   * @param  array  general parameter passing array
   * @return bool   true on success
   */
    function DELETE($options) 
    {
    $path = $options['path'];
    $p4dir = '/'.dirname($path);
    if ($p4dir=='//') return '403 Forbidden, cannot delete depot.';
    $node = basename($path);
    $p4path = $p4dir.'/'.$node;
    // Create a temporary private directory in our filespace
    $uid = uniqid('');
    $ws_id = 'W'.$uid;
    $dir = $this->base.'D'.$uid; // temp filesystem directory name
    $fail = !mkdir($dir); // make temp directory
    if (!$fail) {
      $file = $dir.'/'.$node; // create temporary sacrificial
      $fail = !touch($file); //   file that P4 can delete
    }
    if (!$fail) { // Is item to delete an empty (faux) directory?
      if (!strpos($this->sp4cmd('files',$p4path.'/'.DIRP,''),'no such')) {
        $this->p4faux_drop($p4path); // yes, delete by removing placeholder
      } else { // no, must delete the actual file [or directory]
        $fail = !$this->p4work($ws_id,$p4dir,$dir); // attach workspace
        if (!$fail) {
          $this->sp4cmd('flush','"'.$p4path.'"','-c '.$ws_id);
          $x = $this->sp4cmd('delete','"'.$p4path.'"','-c '.$ws_id);
          $fail = (strpos($x,'opened for')===false);
        }
        if ($fail) error_log('***delete '.$x.$p4path);
        if (!$fail) $fail = !$this->p4submit($ws_id,'delete',$p4path);
      }
    }
    $this->sp4cmd('client','-d -f '.$ws_id,'');  // release workspace
    unlink($file); // delete temp file (should be redundant)
    rmdir($dir); // delete temp directory
    return ($fail)? '404 Not found-3':true;
  } // end DELETE
  
  /**
   * MOVE method handler
   *
   * @param  array  general parameter passing array
   * @return bool   true on success
   */
  function MOVE($options) 
  {
      return $this->COPY($options, true);
  } // end MOVE

  /**
   * COPY method handler
   *
   * @param  array  general parameter passing array
   * @return bool   true on success
   */
  function COPY($options, $del=false) 
  {
    // TODO Property updates still broken (Litmus should detect this?)
    $fileAPat = array('path'=>'depotFile /','action'=>'action ');
    
    if (!empty($_SERVER['CONTENT_LENGTH'])) { // no body parsing yet
      return '415 Unsupported media type';
    }

    // no copying to different WebDAV Servers yet
    if (isset($options['dest_url'])) {
      return '502 bad gateway';
    }
    $path = $options['path'];
    $p4dir = '/'.dirname($path);
    $node = basename($path);
    $p4src = $p4dir.'/'.$node; // drop possible trailing separator
    $fileAPat = array('path'=>'depotFile /','action'=>'action ');
    $file = $this->p4list('files',$p4src,$fileAPat);
    $dir = $this->p4list('dirs -D',$p4src,array('x'=>'dir '));
    $depot = $this->p4list('depots',$p4src,array('x'=>'Depot '));
    if (empty($file)&&empty($dir)&&empty($depot)) { return '404 Not found-1'; }
    else { if ($file[0]['action']=='delete') return '404 Not found-2'; }
    
    $dest = $options['dest'];
    $p4destDir = '/'.dirname($dest);
    $node = basename($dest);
    $p4dest = $p4destDir.'/'.$node; // drop possible trailing separator
    $exists = $this->p4list('files',$p4dest,$fileAPat);
    if (empty($exists)) { $new = true; }
    else { $new = ($exists[0]['action']=='delete')? true:false; }
    $dir = $this->p4list('dirs -D',$p4dest,array('x'=>'dir '));
    $existing_col = false;
    if (!$new) {
      if ($del && !empty($dir)) {
        if (!$options['overwrite']) return '412 precondition failed';
        $existing_col = true;
      }
    }
    if (!$new) {
      if ($options['overwrite']) {
        $stat = $this->DELETE(array('path'=>$dest));
        if (substr($stat,0,1) != '2') return $stat; 
      } else {                
        return '412 precondition failed';
      }
    }
    // Is the source a directory?
    $c_dir = count($this->p4list('dirs -D',$p4src,array('x'=>'dir ')));
    $c_dir += count($this->p4list('depots',$p4src,array('x'=>'Depot ')));
    if ($c_dir) {
      $dx = '/...';
      // RFC 2518 Section 9.2, last paragraph
      if ($options['depth'] != 'infinity') {
        error_log('--depth='.$options['depth']);
        return '400 Bad request';
      }
    } else {
      $dx = '';
    }
    // Create a temporary private directory on the server
    $uid = uniqid('');
    $ws_id = 'W'.$uid;
    $dir = $this->base.'D'.$uid; // temp filesystem directory name
    $fail = !mkdir($dir); // make temp directory
    $fail = !$this->p4work($ws_id,$p4destDir,$dir); // attach a workspace
    if (!$fail) { 
      $this->sp4cmd('flush','"'.$p4dest.'"','-c '.$ws_id);
      $x = $this->sp4cmd('integ -f -i -d','"'.$p4src.$dx.'" "'.$p4dest.$dx.'"','-c '.$ws_id);
      $fail = (strpos($x,'branch/sync')===false);
      if ($fail) error_log('***integrate '.$x);
    }
    if (!$fail) { 
      $x = $this->sp4cmd('resolve -at','"'.$p4dest.$dx.'"','-c '.$ws_id);
      $fail = (strpos($x,'resolve')===false);
      if ($fail) error_log('***resolve '.$x);
    }
    if (!$fail) $fail = !$this->p4submit($ws_id,'copy of '.$p4src,$p4dest);
    $this->sp4cmd('client','-d -f '.$ws_id,''); // release workspace
    // Check if this makes a faux directory non-empty
    if (!strpos($this->sp4cmd('files',$p4destDir.'/'.DIRP,''),'no such')) {
      $this->p4faux_drop($p4destDir); // yes, remove unnecessary placeholder
    }
    rmdir($dir); // delete temp directory
    
    if (!$fail) {
      if ($del) {
        $stat = $this->DELETE(array('path'=>$path));
        if (substr($stat,0,1) != '2') return $stat;
      }
    }
    return ($new && !$existing_col) ? "201 Created" : "204 No Content";         
  } // end COPY

  /**
   * PROPPATCH method handler
   *
   * @param  array  general parameter passing array
   * @return bool   true on success
   */
  function PROPPATCH(&$options) 
  {
    foreach($options['props'] as $key => $prop) {
      if($ns == 'DAV:') {
        $options['props'][$key]['status'] = '403 Forbidden';
      }
    }             
    return '';
  }

  /**
   * LOCK method handler
   *
   * @param  array  general parameter passing array
   * @return bool   true on success
   */
  function LOCK(&$options) 
  {
    if(isset($options['update'])) { // Lock Update          
    }
    
    $options['timeout'] = time()+1; // 1 second hardcoded

    return '200 OK';
  }

  /**
   * UNLOCK method handler
   *
   * @param  array  general parameter passing array
   * @return bool   true on success
   */
  function UNLOCK(&$options) 
  {
    return "200 OK";
  }

  /**
   * checkLock() helper
   *
   * @param  string resource path to check for locks
   * @return bool   true on success
   */
  function checkLock($path) 
  {
      // TODO
      return false;
  }
  /**
   * p4faux_make private method
   *
   * Create a persistent placeholder file that makes a persistent
   * empty (faux) directory visible.
   *
   * @param string  workspace name
   * @param string  Perforce directory path
   */ 
  function p4faux_make($p4dir) {
    $uid = uniqid('');
    $ws_id = 'W'.$uid;
    $dir = $this->base.'D'.$uid; // temp filesystem directory name
    $dummy = $dir.'/'.DIRP;
    $p4dummy = $p4dir.'/'.DIRP;
    $fail = !mkdir($dir);
    if (!$fail) touch($dummy); // make empty place-holder file
    $fail = !$this->p4work($ws_id,$p4dir,$dir); // attach workspace
    if (!$fail) {
      $this->sp4cmd('flush','"'.$p4dir.'"','-c '.$ws_id);
      $x = $this->sp4cmd('add','"'.$p4dummy.'"','-c '.$ws_id);
      $fail = (strpos($x,'assuming')===false);
      if (!$fail) $fail = !$this->p4submit($ws_id,'make directory',$p4dummy);
    }
    if (!$fail) {
      $this->sp4cmd('flush','"'.$p4dir.'"','-c '.$ws_id);
      $x = $this->sp4cmd('delete','"'.$p4dummy.'"','-c '.$ws_id);
      $fail = (strpos($x,'opened')===false);
    }
    if (!$fail) $fail = !$this->p4submit($ws_id,'delete directory placeholder',$p4dummy);
    $this->sp4cmd('client','-d -f '.$ws_id,''); // release workspace
    unlink($dummy); // delete temp file (should be redundant)
    rmdir($dir);
    return ($fail)? false:true;
  }
  function p4faux_drop($p4dir) { // Remove empty directory dummy placeholder file.
    $save_user = $this->depot_user; // save normal user
    $save_passwd = $this->depot_passwd; // save normal password
    $this->depot_user = $this->admin_user; // substitute Perforce superuser
    $this->depot_passwd = $this->admin_passwd; // and superuser password
    $this->sp4cmd('obliterate -y','"'.$p4dir.'/'.DIRP.'"',''); // remove placeholder
    $this->depot_user = $save_user; // restore normal user
    $this->depot_passwd = $save_passwd; // restore normal password
  }
  function p4submit($ws_id,$action,$p4path) { // SUBMIT change
    $change  = 'Change: new'.CR;
    $change .= 'Description:'.CR;
    $change .= ' DAV-'.$action.' by p4DAV/'.$this->depot_user.CR;
    $change .= 'Files:'.CR;
    $change .= '  '.$p4path;
    $x=shell_exec('echo \''.$change.'\' | '.$this->p4cmd('change -i','','-c '.$ws_id));
    $fail = (strpos($x,'created with')===false);
    if ($fail) { error_log('***change '.$x.' '.$change); }
    else {
      $cl_id = substr(substr($x,0,strpos($x,' created')),7);
      $x = $this->sp4cmd('submit','-c '.$cl_id,'-c '.$ws_id);
      $fail = (strpos($x,'submitted')===false);
      if ($fail) error_log('***submit '.$x);
    }
    return ($fail)? false:true;
  }
  function p4work($ws_id,$p4dir,$fsdir) {
    $view  = 'Client:  '.$ws_id.CR;
    $view .= 'Owner:  '.$this->depot_user.CR;
    $view .= 'Options:  allwrite noclobber nocompress unlocked nomodtime normdir'.CR;
    $view .= 'LineEnd:  local'.CR;
    $view .= 'Root:  '.$fsdir.CR;
    $view .= 'View:  '.CR;
    $view .= '  "'.$p4dir.'/..."  "//'.$ws_id.'/..."';
    $x = shell_exec('echo \''.$view.'\' | '.$this->p4cmd('client -i','',''));
    $fail = (strpos($x,'saved')===false);
    if ($fail) error_log('***workspace '.$x.' '.$view);
    return ($fail)? false:true;
  }
  function p4cmd($cmd,$path,$gopts) {
    $p4cmd  = $this->depot_agent.' '.$gopts;
    $p4cmd .=' -u '.$this->depot_user;
    $p4cmd .=' -P '.$this->depot_passwd;
    $p4cmd .=' -p '.$this->depot_host.':'.$this->depot_port;
    $p4cmd .=' '.$cmd.' '.$path.' 2>&1';
    return $p4cmd;
  }
  function p4list($cmd,$path,$filters) {
    $quoted_path = (empty($path))? '':'"'.$path.'"';
    $data_lines = split(CR,$this->sp4cmd($cmd,$quoted_path,'-ztag'));
    $responses = array();
    $filter_count = count($filters);
    $hits = 0;
    $matches = array();
    foreach ($data_lines as $item) {
    if ($this->logging>2) error_log('**'.$item);
      if (empty($item)) { $hits=0; $matches=array(); continue; }
      foreach ($filters as $key=>$pattern) {
        $skip = strlen($pattern);
        $pos = strpos($item,$pattern);
        if ($pos===false) continue;
        $hits++;
        $matches[$key]=trim(substr($item,$pos+$skip));
      }
      if ($filter_count<=$hits) { $hits=0; array_push($responses,$matches); }
    }
    return $responses;
  }
  function sp4cmd($a,$b,$c) { return shell_exec($this->p4cmd($a,$b,$c)); }
}
?>