depot_user = $_SERVER['PHP_AUTH_USER']; $this->depot_passwd = $_SERVER['PHP_AUTH_PW']; // ****** Override authentication ************ $this->depot_user = 'daniel_sabsay'; $this->depot_passwd = 'p4webdav'; return true; // **** [ remove to restore function ] ******* $r = $this->sp4cmd('user','-o',''); $p = strpos($r,'Password:'); // skip the pro-forma password if (strpos(substr($r,$p+9),'Password:')) return true; $p4 = strpos($this->sp4cmd('-V','',''),'Perforce Software'); if ($p4===false) echo 'Error: P4 client module not found at '.$this->depot_agent; return false; } /** * 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) { $datePat = array('date'=>'time '); $dirPat = array('path'=>'dir /'); $depotPat = array('raw'=>'Depot '); $filePat = array('path'=>'depotFile /', 'size'=>'fileSize ', 'date'=>'headTime '); $path = $options['path']; $p4path = '/'.$path; 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 $num_dirs = count($this->p4list('dirs -D',$p4path,$dirPat)); // dir exists? $num_depots = count($this->p4list('depots','',array('x'=>'Depot '.basename($path)))); $is_dir = 0<($num_dirs+$num_depots); if (!$is_dir) { 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'] = 'text/plain'; } $props = array(); // initialize property stack 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)); array_push($files['files'],array('path'=>$child['path'],'props'=>$props)); } } return true; } /** * detect if a given program is found in the search PATH * * helper function used by _mimetype() to detect if the * external 'file' utility is available * * @param string program name * @param string optional search path, defaults to $PATH * @return bool true if executable program found in path */ function _can_execute($name, $path = false) { // path defaults to PATH from environment if not set if ($path === false) { // ******* $path = getenv("PATH"); } // check method depends on operating system if (!strncmp(PHP_OS, "WIN", 3)) { // on Windows an appropriate COM or EXE file needs to exist $exts = array(".exe", ".com"); $check_fn = "file_exists"; } else { // anywhere else we look for an executable file of that name $exts = array(""); $check_fn = "is_executable"; } // now check the directories in the path for the program foreach (explode(PATH_SEPARATOR, $path) as $dir) { // skip invalid path entries if (!file_exists($dir)) continue; if (!is_dir($dir)) continue; // and now look for the file foreach ($exts as $ext) { if ($check_fn("$dir/$name".$ext)) return true; } } return false; } /** * try to detect the mime type of a file * * @param string file path * @return string guessed mime type */ function _mimetype($fspath) { if (@is_dir($fspath)) { // directories are easy return DIRR; // ******** } else if (@function_exists("mime_content_type")) { // use mime magic extension if available // ******** $mime_type = mime_content_type($fspath); } else if ($this->_can_execute("file")) { // it looks like we have a 'file' command, // lets see it it does have mime support $fp = popen("file -i '".$fspath."' 2>/dev/null", "r"); $reply = fgets($fp); pclose($fp); // popen will not return an error if the binary was not found // and find may not have mime support using "-i" // so we test the format of the returned string // the reply begins with the requested filename if (!strncmp($reply, "$fspath: ", strlen($fspath)+2)) { $reply = substr($reply, strlen($fspath)+2); // followed by the mime type (maybe including options) if (ereg("^[[:alnum:]_-]+/[[:alnum:]_-]+;?.*", $reply, $matches)) { $mime_type = $matches[0]; } } } if (empty($mime_type)) { // 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($fspath), "."))) { 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']; $p4path = '/'.$path; $fileAPat = array('path'=>'depotFile /','action'=>'action '); $exists = $this->p4list('files',$p4path,$fileAPat); if (empty($exists)) { return false; } else { if ($exists[0]['action']=='delete') return false; } $file = $this->base.uniqid('F'); // Generate temporary filename $this->sp4cmd('print -q -o '.$file,'"'.$p4path.'"',''); // read data from depot $options['mimetype'] = 'text/html'; $options['size'] = filesize($file); $options['stream'] = fopen($file,'r'); // Open stream to temp file $options['delete_path'] = $file; // Mark temp for disposal header('Accept-Ranges: bytes'); header('ETag: "'.uniqid('E').'"'); return true; } function HEAD(&$params) { $status = $this->GET($params); if ($status===true) { header('Content-Length: '.$params['size']); fclose($params['stream']); unlink($params['delete_path']); } return $status; } /** * PUT method handler * * @param array parameter passing array * @return bool true on success */ function PUT(&$options) { // TO DO: guard against directory instead of file path $path = $options['path']; $p4dir = '/'.dirname($path); $node = basename($path); $p4path = $p4dir.'/'.$node; // drop possible trailing separator // Create a temporary private directory on the server if ($p4dir=='//') return '403 Forbidden, cannot make depot'; $uid = uniqid(''); $ws_id = 'W'.$uid; $dir = $this->base.'D'.$uid; // temp file system directory name $fail = !mkdir($dir); // make temp directory if (!$fail) { // connect to HTTP data stream $fr = $options['stream']; $fail = !$fr; } if (!$fail) { // open temp file for data $file = $dir.'/'.$node; $fw = fopen($file,'w'); $fail = !$fw; } 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 '); $hits = $this->p4list('files',$p4path,$filePatA); if (empty($hits)) { $update = 'add'; } else { $update = ($hits[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); if (!$fail) $this->p4purge($p4dir); // drop faux dir placeholder if present $this->sp4cmd('client','-d -f '.$ws_id,''); // release workspace unlink($file); // delete temp file rmdir($dir); // delete temp directory return !$fail; } /** * 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 ($p4dir=='//') return '403 Forbidden, cannot make depot'; // Parent directory must already exist if (count(split('/',$path))>4) { $dir_count = count($this->p4list('dirs -D',$p4dir,array('x'=>'dir /'))); } else { $dir_count = count($this->p4list('depots','',array('x'=>'Depot '.substr(dirname($path),1)))); } if (!$dir_count) return '403 Forbidden, no parent directory'; // Directory with the same name must not already exist $exists = $this->p4list('dirs',$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 an empty directory by putting a "deleted" placeholder file inside $fail = !$this->_make_placeholder($p4path); return ($fail)? false:'201 Created'; } /** * DELETE method handler * * @param array general parameter passing array * @return bool true on success */ function DELETE($options) { // TO DO: this could be a directory or faux directory $path = $options['path']; $p4path = '/'.$path; $p4dir = '/'.dirname($path); if ($p4dir=='//') return '403 Forbidden, cannot delete depot.'; $node = basename($path); // 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 if (!$fail) { $file = $dir.'/'.$node; // create temporary sacrificial $fail = !touch($file); // file that P4 can delete } if (!$fail) { // check if this deletion will leave empty directory $num_dirs=count($this->p4list('dirs -D',$p4dir.'/*',$dirPat)); $num_files=count($this->p4list('files',$p4dir.'/*',array('x'=>'depotFile /'))); if (!($num_dirs+$num_files)) $fail = !$this->_make_placeholder($p4dir); } if (!$fail) { // check if this item is an empty directory $exists=$this->p4list('files',$p4path.'/*',array('x'=>'depotFile /')); if (!empty($exists)&&$exists[0]['x']==DIRF) { $fail = !$this->_p4purge($p4path); // yes, delete by removing placeholder } else { // no, actually delete the file if (!$fail) $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); 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':'204 No Content'; } /** * MOVE method handler * * @param array general parameter passing array * @return bool true on success */ function MOVE($options) { return $this->COPY($options, true); } /** * 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 $exists = $this->p4list('files',$p4src,$fileAPat); if (empty($exists)) { return '404 Not found'; } else { if ($exists[0]['action']=='delete') return '404 Not found'; } $dest = $options['dest']; $p4dest = '/'.$dest; $p4destDir = '/'.dirname($dest); $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; } $is_dir = 0p4list('dirs',$p4dest,array('dir '))); $dx = ''; $existing_col = false; if (!$new) { if ($del && $is_dir) { if (!$options['overwrite']) return '412 precondition failed'; $p4dest .= '/'.$options['path']; $hits = $this->p4list('files',$p4dest,$fileAPat); if (empty($hits)) { $exists = false; } else { $exists = ($hits[0]['action']=='delete')? false:true; } if ($exists) { $dest .= '/'.$options['path']; } else { $new = true; $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_dir = 0p4list('dirs',$p4src,array('dir '))); if ($is_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 rmdir($dir); // delete temp directory if (!$fail) { if ($del) { $stat = $this->DELETE(array('path'=>$options['path'])); if (substr($stat,0,1) != '2') return $stat; } } return ($new && !$existing_col) ? "201 Created" : "204 No Content"; } /** * 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; } /** * _make_placeholder private method * * Create a persistent placeholder file that makes an "empty" Perforce * directory visible. * * @param string workspace name * @param string Perforce directory path * @param string local filesystem directory path */ function _make_placeholder($p4dir) { $uid = uniqid(''); $ws_id = 'W'.$uid; $dir = $this->base.'D'.$uid; // temp filesystem directory name $dummy = $dir.'/'.DIRF; $p4dummy = $p4dir.'/'.DIRF; $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; } function p4purge($p4dir) { // remove dummy place-holder file $save_user = $this->depot_user; $save_passwd = $this->depot_passwd; $this->depot_user = $this->admin_user; $this->depot_passwd = $this->admin_passwd; $this->sp4cmd('obliterate -y','"'.$p4dir.'/'.DIRF.'"',''); $this->depot_user = $save_user; $this->depot_passwd = $save_passwd; } function p4submit($ws_id,$action,$p4path) { // SUBMIT change $change = 'Change: new'.CR; $change .= 'Description:'.CR; $change .= ' Auto-'.$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; } 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; } 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==3) 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]=rtrim(substr($item,$pos+$skip)); } if ($filter_count<=$hits) { $hits=0; array_push($responses,$matches); } } return $responses; } 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 sp4cmd($a,$b,$c) { return shell_exec($this->p4cmd($a,$b,$c)); } } ?>