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; // ******************************************* $r = $this->sp4cmd('user','-o',''); $p = strpos($r,'Password:'); // bypass the commented 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) { $dirPat = array('path'=>'dir /'); $filePat = array('path'=>'depotFile /','size'=>'fileSize ','date'=>'headTime '); $depotPat = array('raw'=>'Depot '); $path = $options['path']; $p4path = '/'.$path; $files['files'] = array(); if ($p4path=='//') { $group['dirs'] = array(array('path'=>'')); $group['depots'] = $this->p4list('depots','',$depotPat); } else { if (substr($p4path,-1)=='/') { $is_dir = true; // Assume it's a directory $Dpath = substr($p4path,0,strlen($p4path)-1); // drop trailing separator $exists = 0p4list('dirs -D',$Dpath,$dirPat)); // check if exists if (!$exists) { // check for faux directory $exists = 0DIRD))); if (!$exists) return false; } } else { $is_dir = 0p4list('dirs -D',$p4path,$dirPat)); // see if exists if ($is_dir) { $path .= '/'; $p4path .= '/'; } else { $group['files']=$this->p4list('fstat -Ol',$p4path,$filePat); } } if (strpos($options['depth'],'noroot')===false) { // Omit self ? if ($is_dir) { $group['self']=array(array('path'=>$path)); } else { $group['files']=array(array('path'=>$path)); } } if ($is_dir&&!empty($options['depth'])) { // depth 0 = self only $group['dirs']=$this->p4list('dirs -D',$p4path.'*',$dirPat); $group['files']=$this->p4list('fstat -Ol',$p4path.'*',$filePat); //$group['config']=array($path.'/**my_configuration**'); } } foreach ($group as $group_type=>$children) { foreach ($children as $child) { $props = array(); switch ($group_type) { case 'depots': $child['path'] = '/'.substr($child['raw'],0,strpos($child['raw'],' ')); case 'dirs': $child['path'] .= '/'; // sub-directories get trailing separator case 'self': $child['name'] = basename($child['path']); $child['res'] = 'collection'; $child['type'] = DIRR; break; case 'files': $child['name'] = basename($child['path']); $child['type'] = 'text/plain'; } array_push($props,$this->mkprop('getcontentlength',$child['size'])); array_push($props,$this->mkprop('resourcetype',$child['res'])); array_push($props,$this->mkprop('getcontenttype',$child['type'])); array_push($props,$this->mkprop('displayname',$child['name'])); array_push($props,$this->mkprop('creationdate',0)); array_push($props,$this->mkprop('getlastmodified',$child['date'])); array_push($files['files'],array('path'=>$child['path'],'props'=>$props)); } } return; } /** * 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 "httpd/unix-directory"; // ******** } 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; // $exists = 0<$this->p4list('files',$p4path,array('path'=>'depotFile /')); // if (!$exists) 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: "7b995d-4e30-3cf7d0b4"'); return true; } /** * PUT method handler * * @param array parameter passing array * @return bool true on success */ function PUT(&$options) { $path = $options['path']; $p4path = '/'.$path; $p4dir = '/'.dirname($path); // TO DO: guard against directory instead of file path $node = basename($path); // Create a temporary private directory on the server $uid = uniqid(''); $ws_id = 'W'.$uid; $dir = $this->base.'D'.$uid; // temp 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); // Attach a WORKSPACE spec to this temp directory $view = $this->p4view($ws_id,$p4dir.'/',$dir); $x = shell_exec('echo \''.$view.'\' | '.$this->p4cmd('client -i','','')); $fail = (strpos($x,'saved')===false); } if (!$fail) { $this->sp4cmd('flush','"'.$p4path.'"','-c '.$ws_id); } if (!$fail) { // open for EDIT or ADD depending on current existence $exists = 0p4list('files',$p4path,array('path'=>'depotFile /'))); $update = ($exists)? 'edit':'add'; $x = $this->sp4cmd($update,'"'.$p4path.'"','-c '.$ws_id); $fail = (strpos($x,'opened for')===false); } if (!$fail) { // make a pro-forma CHANGE spec $change = $this->p4change($update,$p4path); $x=shell_exec('echo \''.$change.'\' | '.$this->p4cmd('change -i','','-c '.$ws_id)); $fail = (strpos($x,'created with')===false); } if (!$fail) { // submit the change $CL = substr(substr($x,0,strpos($x,' created')),7); $x = $this->sp4cmd('submit','-c '.$CL,'-c '.$ws_id); $fail = (strpos($x,'submitted')===false); } $this->sp4cmd('client','-d -f '.$ws_id,''); // detach workspace unlink($file); // delete temp file rmdir($dir); // delete temp directory // remove the dummy directory file if one exists if (!$fail) { $status = $this->sp4cmd('opened -a',$p4dir.'/'.DIRD.'*',''); $pos = strpos($status,DIRD); $fail = ($pos===false); if (!$fail) { // get change_list_number and workspace_name $str = substr($status,$pos+strlen(DIRD.'_')); // discard preliminary stuff $pos = strpos($str,'#'); // dir_name is terminated by revision number $CL = substr($str,strpos($str,'_'),$pos); // change_list_# is last part of faux dir_name $str = substr($str,strpos($str,'by ')+3); // isolate user_name@workspace_name $str = substr($str,strpos($str,'@')+1); // isolate workspace_name $ws_id = substr($str,0,strpos($str,"\n")-1); // get workspace_name $dummy = $p4dir.'/'.DIRD.'_'.$CL; $this->sp4cmd('revert',$dummy,''); // release pending add from changelist $this->sp4cmd('change','-d '.$CL,''); // delete changelist $this->sp4cmd('client','-d -f '.$ws_id,''); // detach workspace } } return !$fail; } /** * MKCOL method handler * * @param array general parameter passing array * @return bool true on success */ function MKCOL($options) { // bad characters in file names // @ %40 // # %23 // * %2A // % %25 $path = $options['path']; $parent = dirname($path); $name = basename($path); $p4path = $path.'/'; $p4dir = '/'.dirname($path); $uid = uniqid(''); $ws_id = 'W'.$uid; $exists = 0p4list('dirs',$p4path,array('path'=>'dir /'))); if(!file_exists($parent)) { return "409 Conflict"; } if(!is_dir($parent)) { return "403 Forbidden"; } if( file_exists($parent."/".$name) ) { return "405 Method not allowed"; } if(!empty($_SERVER["CONTENT_LENGTH"])) { // no body parsing yet return "415 Unsupported media type"; } // // Perforce doesn't create directories (see, that's what you would // have learned if you'd read my book :-). // // For CREATE DIRECTORY X, you don't need to do anything if // X already exists, of course. // // But if X doesn't exist, you'll need to feign an empty directory // for the DAV client. // // (CREATE DIRECTORY is probably going to be the goofiest part // of the P4<->DAV mapping.) // // You need to feign it in a way that is "persistent" -- // that is, if user2 happens to connect after user1 has done // CREATE DIRECTORY but before user1 puts any files in it, // user2 still has to see that there's an empty directory // there. // // What I suggest for CREATE DIRECTORY X is something like this: // // p4 add X/DavDirectoryDummy $view = $this->p4view($ws_id,$p4dir,$dir); $x = shell_exec('echo \''.$view.'\' | '.$this->p4cmd('client -i','','')); $fail = (strpos($x,'saved')===false); if (!$fail) { $this->sp4cmd('flush','"'.$p4path.'"','-c '.$ws_id); } $dummy = $file.'/DavDirectoryDummy_'.$ws_id; $x = $this->sp4cmd('add','"'.$dummy.'"','-c '.$ws_id); $fail = (strpos($x,'opened for')===false); if (!$fail) { // make a pro-forma CHANGE spec $change = $this->p4change($update,$p4path); $CL=shell_exec('echo \''.$change.'\' | '.$this->p4cmd('change -i','','-c '.$ws_id)); $fail = (strpos($CL,'created with')===false); } // // Do not submit this; it's just a placeholder so your DAV // server can keep track of faux directories it's created. // // This means that whenever a DAV client wants READ DIRECTORY X, // you'll have to look for these dummies and tell the client // they're directories. // // To look for dummy subdirectories in directory Y: // // p4 opened -a Y/*/DavDirectoryDummy // // This will returned something like: // // //depot/Y/X/DavDirectoryDummy#1 - add ... by user@client // // And then your DAV server has to tell its client that theres // a subdirectory X in directory Y. // // Once a user has created a file in X, you can get rid of the // dummy, using: // // p4 revert X/DavDirectoryDummy $stat = mkdir ($parent."/".$name,0777); if(!$stat) { return "403 Forbidden"; } return "201 Created"; } /** * DELETE method handler * * @param array general parameter passing array * @return bool true on success */ function DELETE($options) { $path = $options['path']; $p4path = '/'.$path; $p4dir = '/'.dirname($path).'/'; // TO DO: MUST handle directory (currently only file) $node = basename($path); // Create a temporary private directory on the server $uid = uniqid(''); $ws_id = 'W'.$uid; $dir = $this->base.'D'.$uid; // temp directory name $fail = !mkdir($dir); // make temp directory if (!$fail) { $file = $dir.'/'.$node; $fail = !touch($file); // create dummy temp file } if (!$fail) { // attach a WORKSPACE spec for temp directory $view = $this->p4view($ws_id,$p4dir,$dir); $x = shell_exec('echo \''.$view.'\' | '.$this->p4cmd('client -i','','')); $fail = (strpos($x,'saved')===false); } if (!$fail) { $this->sp4cmd('flush','"'.$p4path.'"','-c '.$ws_id); } if (!$fail) { // open for DELETE $x = $this->sp4cmd('delete','"'.$file.'"','-c '.$ws_id); $fail = (strpos($x,'opened for')===false); } if (!$fail) { // make a pro-forma CHANGE spec $change = $this->p4change('delete',$p4path); $CL=shell_exec('echo \''.$change.'\' | '.$this->p4cmd('change -i','','-c '.$ws_id)); $fail = (strpos($CL,'created with')===false); } if (!$fail) { // submit the change $change_id = substr(substr($CL,0,strpos($CL,' created')),7); $x = $this->sp4cmd('submit','-c '.$change_id,'-c '.$ws_id); $fail = (strpos($x,'submitted')===false); } $this->sp4cmd('client','-d -f '.$ws_id,''); // detach 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?) 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"; } $source = $this->base . $options["path"]; if(!file_exists($source)) return "404 Not found"; $dest = $this->base . $options["dest"]; $new = !file_exists($dest); $existing_col = false; if(!$new) { if($del && is_dir($dest)) { if(!$options["overwrite"]) { return "412 precondition failed"; } $dest .= basename($source); if(file_exists($dest.basename($source))) { $options["dest"] .= basename($source); } else { $new = true; $existing_col = true; } } } if(!$new) { if($options["overwrite"]) { $stat = $this->delete(array('path'=>$options['dest'])); if($stat{0} != '2') return $stat; } else { return '412 precondition failed'; } } if (is_dir($source)) { // RFC 2518 Section 9.2, last paragraph if ($options["depth"] != "infinity") { error_log("---- ".$options["depth"]); return "400 Bad request"; } system(escapeshellcmd("cp -R ".escapeshellarg($source) ." " . escapeshellarg($dest))); if($del) { system(escapeshellcmd("rm -rf ".escapeshellarg($source)) ); } } else { if($del) { @unlink($dest); rename($source, $dest); } else { if(substr($dest,-1)=="/") $dest = substr($dest,0,-1); copy($source, $dest); } } 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) { $msg = ""; $path = $options["path"]; $dir = dirname($path)."/"; $base = basename($path); 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()+300; // 5min. 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; } function p4change($action,$file) { // create CHANGE specification $change = 'Change: new'."\n"; $change .= 'Description:'."\n"; $change .= ' Auto-'.$action.' by p4DAV/'.$this->depot_user."\n"; $change .= 'Files:'."\n"; $change .= ' '.$file; return $change; } function p4view($ws_id,$p4dir,$fsdir) { // create WORKSPACE specification $view = 'Client: '.$ws_id."\n\n"; $view .= 'Owner: '.$this->depot_user."\n\n"; $view .= 'Options: allwrite noclobber nocompress unlocked nomodtime normdir'."\n\n"; $view .= 'LineEnd: local'."\n\n"; $view .= 'Root: '.$fsdir."\n\n"; $view .= 'View: '."\n"; $view .= ' "'.$p4dir.'*" "//'.$ws_id.'/*"'; return $view; } function p4list($cmd,$path,$filters) { $quoted_path = (empty($path))? '':'"'.$path.'"'; $data_lines = split("\n",$this->sp4cmd($cmd,$quoted_path,'-ztag')); $responses = array(); $filter_count = count($filters); $hits = 0; $matches = array(); foreach ($data_lines as $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)); } } ?>