DepotServer.php #28

  • //
  • guest/
  • daniel_sabsay/
  • p4dav/
  • DepotServer.php
  • View
  • Commits
  • Open Download .zip Download (25 KB)
<?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)); }
}
?>
# Change User Description Committed
#28 4801 Daniel Sabsay Copyright and default user name cleaned up
#27 4800 Daniel Sabsay Final documentation update
GET method updated
Log now reports depot host & port being served
#26 4798 Daniel Sabsay Add logging P4 version to server log if "debug-level" logging
#25 4797 Daniel Sabsay Fix bug in MKCOL method
#24 4794 Daniel Sabsay Fix bugs in MKCOL, COPY, DELETE & PUT
Restore full authentication checking
#23 4792 Daniel Sabsay Refinements and bug fixes to MOVE method
#22 4779 Daniel Sabsay A few bugs chased
#21 4778 Daniel Sabsay Improvements in logging (now shows response headers)
Change in directory searching mechanism
Now appropriately handles If-Modified-Since headers
#20 4777 Daniel Sabsay adjustments to MOVE & DELETE
#19 4775 Daniel Sabsay Fix PUT method.
#18 4774 Daniel Sabsay Repackaged functions all working
#17 4764 Daniel Sabsay MOVE now works for a single file
#16 4752 Daniel Sabsay First cut at MOVE & COPY methods
#15 4750 Daniel Sabsay Mac Finder can now mount the root (repository) level.
Directory last-modified dates are now retrieved.
Full log implementation now shows both sides of dialog.
#14 4749 Daniel Sabsay The code now uses no persistant workspaces or changelists.
Switched the faux directory mechanism to use deleted placeholder files.
Switched the logging mechanism to use the webserver logging methods.
#13 4748 Daniel Sabsay Fix a few remaining bugs in PROPFIND and remove debugging code from PUT
#12 4747 Daniel Sabsay PROPFIND method refinements
#11 4746 Daniel Sabsay Refinements to PUT & MKCOL
#10 4744 Daniel Sabsay Fix PUT so it overwrites a deleted file
Improve MKCOL logic
#9 4737 Daniel Sabsay Initial working version of MKCOL method
#8 4731 Daniel Sabsay Re-code faux directory logic
#7 4729 Daniel Sabsay Add new logic to handle disposal of faux directory files at end of PUT command
#6 4728 Daniel Sabsay Update descriptive comments.
#5 4727 Daniel Sabsay Improve PUT functioning.
Package workspace & change specs into functions.
#4 4726 Daniel Sabsay Dates and File sizes now work, added -D flag to DIRS commands.
#3 4725 Daniel Sabsay Changes to the DELETE and LIST logic
#2 4716 Daniel Sabsay fix mispelled PUT return value
#1 4711 Daniel Sabsay PUT works for first time