Subversion Repositories oidplus

Rev

Rev 594 | Go to most recent revision | Blame | Last modification | View Log | RSS feed

  1. <?php
  2.  
  3. /*
  4.  * This file includes:
  5.  *
  6.  * 1. "PHP SVN CLIENT" class
  7.  *    Copyright (C) 2007-2008 by Sixdegrees <cesar@sixdegrees.com.br>
  8.  *    Cesar D. Rodas
  9.  *    https://code.google.com/archive/p/phpsvnclient/
  10.  *    License: BSD License
  11.  *    CHANGES by Daniel Marschall, ViaThinkSoft in 2019-2021:
  12.  *    - The class has been customized and contains specific changes for the software "OIDplus"
  13.  *    - Functions which are not used in the "SVN checkout" were removed.
  14.  *      The only important functions are getVersion() and updateWorkingCopy()
  15.  *    - The dependency class xml2array was removed and instead, SimpleXML is used
  16.  *    - Added "revision log/comment" functionality
  17.  *
  18.  * 2. "xml2array" class
  19.  *    Taken from http://www.php.net/manual/en/function.xml-parse.php#52567
  20.  *    Modified by Martin Guppy <http://www.deadpan110.com/>
  21.  *    CHANGES by Daniel Marschall, ViaThinkSoft in 2019:
  22.  *    - Converted class into a single function and added that function into the phpsvnclient class
  23.  */
  24.  
  25. if (!function_exists('_L')) {
  26.         function _L($str, ...$sprintfArgs) {
  27.                 $n = 1;
  28.                 foreach ($sprintfArgs as $val) {
  29.                         $str = str_replace("%$n", $val, $str);
  30.                         $n++;
  31.                 }
  32.                 $str = str_replace("%%", "%", $str);
  33.                 return $str;
  34.         }
  35. }
  36.  
  37. /**
  38.  *  PHP SVN CLIENT
  39.  *
  40.  *  This class is an SVN client. It can perform read operations
  41.  *  to an SVN server (over Web-DAV).
  42.  *  It can get directory files, file contents, logs. All the operaration
  43.  *  could be done for a specific version or for the last version.
  44.  *
  45.  *  @author Cesar D. Rodas <cesar@sixdegrees.com.br>
  46.  *  @license BSD License
  47.  */
  48. class phpsvnclient {
  49.  
  50.         /*protected*/ const PHPSVN_NORMAL_REQUEST = '<?xml version="1.0" encoding="utf-8"?><propfind xmlns="DAV:"><prop><getlastmodified xmlns="DAV:"/> <checked-in xmlns="DAV:"/><version-name xmlns="DAV:"/><version-controlled-configuration xmlns="DAV:"/><resourcetype xmlns="DAV:"/><baseline-relative-path xmlns="http://subversion.tigris.org/xmlns/dav/"/><repository-uuid xmlns="http://subversion.tigris.org/xmlns/dav/"/></prop></propfind>';
  51.  
  52.         /*protected*/ const PHPSVN_VERSION_REQUEST = '<?xml version="1.0" encoding="utf-8"?><propfind xmlns="DAV:"><prop><checked-in xmlns="DAV:"/></prop></propfind>';
  53.  
  54.         /*protected*/ const PHPSVN_LOGS_REQUEST = '<?xml version="1.0" encoding="utf-8"?> <S:log-report xmlns:S="svn:"> <S:start-revision>%d</S:start-revision><S:end-revision>%d</S:end-revision><S:path></S:path><S:discover-changed-paths/></S:log-report>';
  55.  
  56.         /*public*/ const NO_ERROR = 1;
  57.         /*public*/ const NOT_FOUND = 2;
  58.         /*public*/ const AUTH_REQUIRED = 3;
  59.         /*public*/ const UNKNOWN_ERROR = 4;
  60.  
  61.         public $use_cache = true;
  62.         public $cache_dir = __DIR__.'/../../userdata/cache';
  63.  
  64.         /**
  65.          *  SVN Repository URL
  66.          *
  67.          *  @var string
  68.          *  @access private
  69.          */
  70.         private $_url;
  71.  
  72.         /**
  73.          *  HTTP Client object
  74.          *
  75.          *  @var object
  76.          *  @access private
  77.          */
  78.         private $_http;
  79.  
  80.         /**
  81.          *  Respository Version.
  82.          *
  83.          *  @access private
  84.          *  @var int
  85.          */
  86.         private $_repVersion;
  87.  
  88.         /**
  89.          *  Last error number
  90.          *
  91.          *  Possible values are NO_ERROR, NOT_FOUND, AUTH_REQUIRED, UNKOWN_ERROR
  92.          *
  93.          *  @access protected
  94.          *  @var integer
  95.          */
  96.         protected $errNro = self::NO_ERROR;
  97.         public function getLastError() {
  98.                 return $this->errNro;
  99.         }
  100.  
  101.         /**
  102.          * Number of actual revision local repository.
  103.          * @var Integer, Long
  104.          */
  105.         private $actVersion;
  106.         private $file_size;
  107.         private $file_size_founded = false;
  108.  
  109.         public function __construct($url)
  110.         {
  111.                 $http =& $this->_http;
  112.                 $http             = new http_class;
  113.                 $http->user_agent = "phpsvnclient (https://code.google.com/archive/p/phpsvnclient/)";
  114.  
  115.                 $this->_url = $url;
  116.  
  117.                 $this->actVersion = $this->getVersion();
  118.         }
  119.  
  120.         /**
  121.          * Function for creating directories.
  122.          * @param string $path The path to the directory that will be created.
  123.          */
  124.         private function createDirs($path)
  125.         {
  126.                 $dirs = explode("/", $path);
  127.  
  128.                 foreach ($dirs as $dir) {
  129.                         if ($dir != "") {
  130.                                 $createDir = substr($path, 0, strpos($path, $dir) + strlen($dir));
  131.                                 @mkdir($createDir);
  132.                         }
  133.                 }
  134.         }
  135.  
  136.         /**
  137.          * Function for the recursive removal of directories.
  138.          * @param string $path The path to the directory to be deleted.
  139.          * @return boolean Returns the status of a function or function rmdir unlink.
  140.          */
  141.         private function removeDirs($path)
  142.         {
  143.                 if (is_dir($path)) {
  144.                         $entries = scandir($path);
  145.                         if ($entries === false) {
  146.                                 $entries = array();
  147.                         }
  148.                         foreach ($entries as $entry) {
  149.                                 if ($entry != '.' && $entry != '..') {
  150.                                         $this->removeDirs($path . '/' . $entry);
  151.                                 }
  152.                         }
  153.                         return @rmdir($path);
  154.                 } else {
  155.                         return @unlink($path);
  156.                 }
  157.         }
  158.  
  159.         /**
  160.          *  Public Functions
  161.          */
  162.  
  163.         /**
  164.         * Updates a working copy
  165.         * @param string $from_revision Either a revision number or a text file with the
  166.         *                              contents "Revision ..." (if it is a file,
  167.         *                              the file revision will be updated if everything
  168.         *                              was successful)
  169.         * @param string $folder        SVN remote folder
  170.         * @param string $outPath       Local path of the working copy
  171.         * @param boolean $preview      Only simulate, do not write to files
  172.         * @return boolean              true if everything is OK
  173.         **/
  174.         public function updateWorkingCopy($from_revision='version.txt', $folder = '/trunk/', $outPath = '.', $preview = false)
  175.         {
  176.                 if (!is_dir($outPath)) {
  177.                         echo _L("ERROR: Local path %1 not existing",$outPath)."\n";
  178.                         //flush();
  179.                         return false;
  180.                 }
  181.  
  182.                 $webbrowser_update = !is_numeric($from_revision);
  183.  
  184.                 if (!is_numeric($from_revision)) {
  185.                         $version_file = $from_revision;
  186.                         $from_revision = -1;
  187.  
  188.                         if (!file_exists($version_file)) {
  189.                                 echo _L("ERROR: %1 missing",$version_file)."\n";
  190.                                 //flush();
  191.                                 return false;
  192.                         } else {
  193.                                 //Obtain the number of current version number of the local copy.
  194.                                 $cont = file_get_contents($version_file);
  195.                                 $m = array();
  196.                                 if (!preg_match('@Revision (\d+)@', $cont, $m)) { // do not translate
  197.                                         echo _L("ERROR: %1 unknown format",$version_file)."\n";
  198.                                         //flush();
  199.                                         return false;
  200.                                 }
  201.                                 $from_revision = $m[1];
  202.  
  203.                                 echo _L("Found %1 with revision information %2",basename($version_file),$from_revision)."\n";
  204.                                 //flush();
  205.                         }
  206.                 } else {
  207.                         $version_file = '';
  208.                 }
  209.  
  210.                 $errors_happened = false;
  211.  
  212.                 if ($webbrowser_update) {
  213.                         // First, do some read/write test (even if we are in preview mode, because we want to detect errors before it is too late)
  214.                         $file = $outPath . '/dummy_'.uniqid().'.tmp';
  215.                         $file = str_replace("///", "/", $file);
  216.                         if (@file_put_contents($file, 'Write Test') === false) { // do not translate
  217.                                 echo (!$preview ? _L("ERROR") : _L("WARNING")).": "._L("Cannot write test file %1 ! An update through the web browser will NOT be possible.",$file)."\n";
  218.                                 //flush();
  219.                                 if (!$preview) return false;
  220.                         }
  221.                         @unlink($file);
  222.                         if (file_exists($file)) {
  223.                                 echo (!$preview ? _L("ERROR") : _L("WARNING")).": "._L("Cannot delete test file %1 ! An update through the web browser will NOT be possible.",$file)."\n";
  224.                                 //flush();
  225.                                 if (!$preview) return false;
  226.                         }
  227.                 }
  228.  
  229.                 //Get a list of objects to be updated.
  230.                 $objects_list = $this->getLogsForUpdate($folder, $from_revision + 1);
  231.                 if (!is_null($objects_list)) {
  232.                         // Output version information
  233.                         foreach ($objects_list['revisions'] as $revision) {
  234.                                 $comment = empty($revision['comment']) ? _L('No comment') : $revision['comment'];
  235.                                 $tex = _L("New revision %1 by %2",$revision['versionName'],$revision['creator'])." (".date('Y-m-d H:i:s', strtotime($revision['date'])).") ";
  236.                                 echo trim($tex . str_replace("\n", "\n".str_repeat(' ', strlen($tex)), $comment));
  237.                                 echo "\n";
  238.                         }
  239.  
  240.                         // Add dirs
  241.                         sort($objects_list['dirs']); // <-- added by Daniel Marschall: Sort folder list, so that directories will be created in the correct hierarchical order
  242.                         foreach ($objects_list['dirs'] as $file) {
  243.                                 if ($file != '') {
  244.                                         $localPath = str_replace($folder, "", $file);
  245.                                         $localPath = rtrim($outPath,DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . ltrim($localPath,DIRECTORY_SEPARATOR);
  246.  
  247.                                         echo _L("Added or modified directory: %1",$file)."\n";
  248.                                         //flush();
  249.                                         if (!$preview) {
  250.                                                 $this->createDirs($localPath);
  251.                                                 if (!is_dir($localPath)) {
  252.                                                         $errors_happened = true;
  253.                                                         echo "=> "._L("FAILED")."\n";
  254.                                                         //flush();
  255.                                                 }
  256.                                         }
  257.                                 }
  258.                         }
  259.  
  260.                         // Add files
  261.                         sort($objects_list['files']); // <-- added by Daniel Marschall: Sort list, just for cosmetic improvement
  262.                         foreach ($objects_list['files'] as $file) {
  263.                                 if ($file != '') {
  264.                                         $localFile = str_replace($folder, "", $file);
  265.                                         $localFile = rtrim($outPath,DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . ltrim($localFile,DIRECTORY_SEPARATOR);
  266.  
  267.                                         echo _L("Added or modified file: %1",$file)."\n";
  268.                                         //flush();
  269.                                         if (!$preview) {
  270.                                                 try {
  271.                                                         $contents = $this->getFile($file);
  272.                                                 } catch (Exception $e) {
  273.                                                         $contents = false;
  274.                                                 }
  275.                                                 if (($contents === false) || (@file_put_contents($localFile, $contents) === false)) {
  276.                                                         $errors_happened = true;
  277.                                                         echo "=> "._L("FAILED")."\n";
  278.                                                         //flush();
  279.                                                 }
  280.                                         }
  281.                                 }
  282.                         }
  283.  
  284.                         // Remove files
  285.                         sort($objects_list['filesDelete']); // <-- added by Daniel Marschall: Sort list, just for cosmetic improvement
  286.                         foreach ($objects_list['filesDelete'] as $file) {
  287.                                 if ($file != '') {
  288.                                         $localFile = str_replace($folder, "", $file);
  289.                                         $localFile = rtrim($outPath,DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . ltrim($localFile,DIRECTORY_SEPARATOR);
  290.  
  291.                                         echo _L("Removed file: %1",$file)."\n";
  292.                                         //flush();
  293.  
  294.                                         if (!$preview) {
  295.                                                 @unlink($localFile);
  296.                                                 if (file_exists($localFile)) {
  297.                                                         $errors_happened = true;
  298.                                                         echo "=> "._L("FAILED")."\n";
  299.                                                         //flush();
  300.                                                 }
  301.                                         }
  302.                                 }
  303.                         }
  304.  
  305.                         // Remove dirs
  306.                         // Changed by Daniel Marschall: moved this to the end, because "add/update" requests for this directory might happen before the directory gets removed
  307.                         rsort($objects_list['dirsDelete']); // <-- added by Daniel Marschall: Sort list in reverse order, so that directories get deleted in the correct hierarchical order
  308.                         foreach ($objects_list['dirsDelete'] as $file) {
  309.                                 if ($file != '') {
  310.                                         $localPath = str_replace($folder, "", $file);
  311.                                         $localPath = rtrim($outPath,DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . ltrim($localPath,DIRECTORY_SEPARATOR);
  312.  
  313.                                         echo _L("Removed directory: %1",$file)."\n";
  314.                                         //flush();
  315.  
  316.                                         if (!$preview) {
  317.                                                 $this->removeDirs($localPath);
  318.                                                 if (is_dir($localPath)) {
  319.                                                         $errors_happened = true;
  320.                                                         echo "=> "._L("FAILED")."\n";
  321.                                                         //flush();
  322.                                                 }
  323.                                         }
  324.                                 }
  325.                         }
  326.  
  327.                         // Update version file
  328.                         // Changed by Daniel Marschall: Added $errors_happened
  329.                         if (!$preview && !empty($version_file)) {
  330.                                 if (!$errors_happened) {
  331.                                         if (@file_put_contents($version_file, "Revision ".$this->actVersion."\n") === false) { // do not translate
  332.                                                 echo _L("ERROR: Could not set the revision")."\n";
  333.                                                 //flush();
  334.                                                 return false;
  335.                                         } else {
  336.                                                 echo _L("Set revision to %1", $this->actVersion) . "\n";
  337.                                                 //flush();
  338.                                                 return true;
  339.                                         }
  340.                                 } else {
  341.                                         echo _L("Revision NOT set to %1 because some files/dirs could not be updated. Please try again.",$this->actVersion)."\n";
  342.                                         //flush();
  343.                                         return false;
  344.                                 }
  345.                         } else {
  346.                                 return true;
  347.                         }
  348.                 } else {
  349.                         return true;
  350.                 }
  351.         }
  352.  
  353.         /**
  354.          *  rawDirectoryDump
  355.          *
  356.          *  Dumps SVN data for $folder in the version $version of the repository.
  357.          *
  358.          *  @param string  $folder Folder to get data
  359.          *  @param integer $version Repository version, -1 means actual
  360.          *  @return SimpleXMLElement SVN data dump.
  361.          */
  362.         private function rawDirectoryDump($folder = '/trunk/', $version = -1)
  363.         {
  364.                 if ($version == -1 || $version > $this->actVersion) {
  365.                         $version = $this->actVersion;
  366.                 }
  367.  
  368.                 $url = $this->cleanURL($this->_url . "/!svn/bc/" . $version . "/" . $folder . "/");
  369.                 $args = array();
  370.                 $this->initQuery($args, "PROPFIND", $url);
  371.                 $args['Body']                      = self::PHPSVN_NORMAL_REQUEST;
  372.                 $args['Headers']['Content-Length'] = strlen(self::PHPSVN_NORMAL_REQUEST);
  373.  
  374.                 $cache_file = $this->cache_dir.'/svnpropfind_'.md5($url.'|'.$args['Body']).'.xml';
  375.                 if ($this->use_cache && file_exists($cache_file)) {
  376.                         $body = file_get_contents($cache_file);
  377.                 } else {
  378.                         $headers = array();
  379.                         $body = '';
  380.                         if (!$this->Request($args, $headers, $body))
  381.                                 throw new Exception(_L("Cannot get rawDirectoryDump (Request failed)"));
  382.                         if (is_dir($this->cache_dir)) {
  383.                                 if ($body) @file_put_contents($cache_file, $body);
  384.                         }
  385.                 }
  386.  
  387.                 return self::xmlParse($body);
  388.         }
  389.  
  390.         /**
  391.          *  getDirectoryFiles
  392.          *
  393.          *  Returns all the files in $folder in the version $version of
  394.          *  the repository.
  395.          *
  396.          *  @param string  $folder Folder to get files
  397.          *  @param integer $version Repository version, -1 means actual
  398.          *  @return array<array<string,string>> List of files.
  399.          */
  400.         private function getDirectoryFiles($folder = '/trunk/', $version = -1)
  401.         {
  402.                 $responses = $this->rawDirectoryDump($folder, $version);
  403.  
  404.                 if ($responses) {
  405.                         $files = array();
  406.                         foreach ($responses as $response) {
  407.  
  408.                                 if ((string)$response->{'D__propstat'}->{'D__prop'}->{'lp3__baseline-relative-path'} != '') {
  409.                                         $fn = (string)$response->{'D__propstat'}->{'D__prop'}->{'lp3__baseline-relative-path'};
  410.                                 } else {
  411.                                         $fn = (string)$response->{'D__propstat'}->{'D__prop'}->{'lp2__baseline-relative-path'};
  412.                                 }
  413.  
  414.                                 $storeDirectoryFiles = array(
  415.                                         'type' => (string)$response->{'D__href'},
  416.                                         'path' => $fn,
  417.                                         'last-mod' => (string)$response->{'D__propstat'}->{'D__prop'}->{'lp1__getlastmodified'},
  418.                                         'version' => (string)$response->{'D__propstat'}->{'D__prop'}->{'lp1__version-name'},
  419.                                         'status' => (string)$response->{'D__propstat'}->{'D__status'},
  420.                                 );
  421.  
  422.                                 // Detect 'type' as either a 'directory' or 'file'
  423.                                 // $storeDirectoryFiles['file'] == /svn/oidplus/!svn/bc/553/trunk/plugins/publicPages/100_whois/whois/rfc/Internet%20Draft.url
  424.                                 // $storeDirectoryFiles['path'] == trunk/plugins/publicPages/100_whois/whois/rfc/Internet Draft.url
  425.  
  426.                                 $storeDirectoryFiles['path'] = str_replace(' ', '%20', $storeDirectoryFiles['path']);
  427.                                 if (substr($storeDirectoryFiles['type'], strlen($storeDirectoryFiles['type']) - strlen($storeDirectoryFiles['path'])) == $storeDirectoryFiles['path']) {
  428.                                         // Example:
  429.                                         // <D:href>/svn/oidplus/!svn/bc/504/trunk/3p/vts_fileformats/VtsFileTypeDetect.class.php</D:href>
  430.                                         // <lp2:baseline-relative-path>trunk/3p/vts_fileformats/VtsFileTypeDetect.class.php</lp2:baseline-relative-path>
  431.                                         $storeDirectoryFiles['type'] = 'file';
  432.                                 } else {
  433.                                         // Example:
  434.                                         // <D:href>/svn/oidplus/!svn/bc/504/trunk/plugins/publicPages/820_login_facebook/</D:href>
  435.                                         // <lp2:baseline-relative-path>trunk/plugins/publicPages/820_login_facebook</lp2:baseline-relative-path>
  436.                                         $storeDirectoryFiles['type'] = 'directory';
  437.                                 }
  438.  
  439.                                 array_push($files, $storeDirectoryFiles);
  440.                         }
  441.  
  442.                         return $files;
  443.  
  444.                 } else {
  445.  
  446.                         throw new Exception(_L("Error communicating with SVN server"));
  447.  
  448.                 }
  449.         }
  450.  
  451.         private static function dirToArray($dir, &$result) {
  452.                 $cdir = scandir($dir);
  453.                 foreach ($cdir as $key => $value) {
  454.                         if (!in_array($value,array('.','..'))) {
  455.                                 if (is_dir($dir . DIRECTORY_SEPARATOR . $value)) {
  456.                                         $result[] = $dir.DIRECTORY_SEPARATOR.$value.DIRECTORY_SEPARATOR;
  457.                                         self::dirToArray($dir.DIRECTORY_SEPARATOR.$value, $result);
  458.                                 } else {
  459.                                         $result[] = $dir.DIRECTORY_SEPARATOR.$value;
  460.                                 }
  461.                         }
  462.                 }
  463.         }
  464.  
  465.         public function compareToDirectory($local_folder, $svn_folder='/trunk/', $version=-1) {
  466.                 $local_cont = array();
  467.                 self::dirToArray($local_folder, $local_cont);
  468.                 foreach ($local_cont as $key => &$c) {
  469.                         $c = str_replace('\\', '/', $c);
  470.                         $c = substr($c, strlen($local_folder));
  471.                         if (substr($c,0,1) === '/') $c = substr($c, 1);
  472.                         if ($c === '') unset($local_cont[$key]);
  473.                         if (strpos($c,'.svn/') === 0) unset($local_cont[$key]);
  474.                         if ((strpos($c,'userdata/') === 0) && ($c !== 'userdata/info.txt') && ($c !== 'userdata/.htaccess') && ($c !== 'userdata/index.html') && (substr($c,-1) !== '/')) unset($local_cont[$key]);
  475.                 }
  476.                 unset($key);
  477.                 unset($c);
  478.                 natsort($local_cont);
  479.  
  480.                 $svn_cont = array();
  481.                 $contents = $this->getDirectoryTree($svn_folder, $version, true);
  482.                 foreach ($contents as $cont) {
  483.                         if ($cont['type'] == 'directory') {
  484.                                 $svn_cont[] = '/'.urldecode($cont['path']).'/';
  485.                         } else if ($cont['type'] == 'file') {
  486.                                 $svn_cont[] = '/'.urldecode($cont['path']);
  487.                         }
  488.                 }
  489.                 foreach ($svn_cont as $key => &$c) {
  490.                         $c = str_replace('\\', '/', $c);
  491.                         $c = substr($c, strlen($svn_folder));
  492.                         if (substr($c,0,1) === '/') $c = substr($c, 1);
  493.                         if ($c === '') unset($svn_cont[$key]);
  494.                         if ((strpos($c,'userdata/') === 0) && ($c !== 'userdata/info.txt') && ($c !== 'userdata/.htaccess') && ($c !== 'userdata/index.html') && (substr($c,-1) !== '/')) unset($svn_cont[$key]);
  495.                 }
  496.                 unset($key);
  497.                 unset($c);
  498.                 unset($contents);
  499.                 unset($cont);
  500.                 natsort($svn_cont);
  501.  
  502.                 $only_svn = array_diff($svn_cont, $local_cont);
  503.                 $only_local = array_diff($local_cont, $svn_cont);
  504.                 return array($svn_cont, $local_cont);
  505.         }
  506.  
  507.         /**
  508.          *  getDirectoryTree
  509.          *
  510.          *  Returns the complete tree of files and directories in $folder from the
  511.          *  version $version of the repository. Can also be used to get the info
  512.          *  for a single file or directory.
  513.          *
  514.          *  @param string  $folder Folder to get tree
  515.          *  @param integer $version Repository version, -1 means current
  516.          *  @param boolean $recursive Whether to get the tree recursively, or just
  517.          *  the specified directory/file.
  518.          *
  519.          *  @return array<string,string>|array<array<string,string>>|false List of files and directories.
  520.          */
  521.         private function getDirectoryTree($folder = '/trunk/', $version = -1, $recursive = true)
  522.         {
  523.                 $directoryTree = array();
  524.  
  525.                 if (!($arrOutput = $this->getDirectoryFiles($folder, $version)))
  526.                         return false;
  527.  
  528.                 if (!$recursive)
  529.                         return $arrOutput[0];
  530.  
  531.                 while (count($arrOutput) && is_array($arrOutput)) {
  532.                         $array = array_shift($arrOutput);
  533.  
  534.                         array_push($directoryTree, $array);
  535.  
  536.                         if (trim($array['path'], '/') == trim($folder, '/'))
  537.                                 continue;
  538.  
  539.                         if ($array['type'] == 'directory') {
  540.                                 $walk = $this->getDirectoryFiles($array['path'], $version);
  541.                                 array_shift($walk);
  542.  
  543.                                 foreach ($walk as $step) {
  544.                                         array_unshift($arrOutput, $step);
  545.                                 }
  546.                         }
  547.                 }
  548.                 return $directoryTree;
  549.         }
  550.  
  551.         /**
  552.          *  Returns file contents
  553.          *
  554.          *  @param  string  $file File pathname
  555.          *  @param  integer $version File Version
  556.          *  @return string  File content and information, false on error, or if a
  557.          *                  directory is requested
  558.          */
  559.         private function getFile($file, $version = -1)
  560.         {
  561.                 if ($version == -1 || $version > $this->actVersion) {
  562.                         $version = $this->actVersion;
  563.                 }
  564.  
  565.                 // check if this is a directory... if so, return false, otherwise we
  566.                 // get the HTML output of the directory listing from the SVN server.
  567.                 // This is maybe a bit heavy since it makes another connection to the
  568.                 // SVN server. Maybe add this as an option/parameter? ES 23/06/08
  569.                 $fileInfo = $this->getDirectoryTree($file, $version, false);
  570.                 if ($fileInfo["type"] == "directory") {
  571.                         throw new Exception(_L("File %1 is detected as directory. Cannot receive file contents.", $file));
  572.                         //return false;
  573.                 }
  574.  
  575.                 $args = array();
  576.                 $url = $this->cleanURL($this->_url . "/!svn/bc/" . $version . "/" . $file . "/");
  577.                 $this->initQuery($args, "GET", $url);
  578.                 $headers = array();
  579.                 $body = '';
  580.                 if (!$this->Request($args, $headers, $body))
  581.                         throw new Exception(_L("Cannot call getFile (Request failed)"));
  582.  
  583.                 return $body;
  584.         }
  585.  
  586.         private function getLogsForUpdate($file, $vini = 0, $vend = -1)
  587.         {
  588.                 $fileLogs = array();
  589.  
  590.                 if ($vend == -1) {
  591.                         $vend = $this->actVersion;
  592.                 }
  593.  
  594.                 if ($vini < 0)
  595.                         $vini = 0;
  596.  
  597.                 if ($vini > $vend) {
  598.                         $vini = $vend;
  599.                         echo _L("Nothing updated")."\n";
  600.                         //flush();
  601.                         return null;
  602.                 }
  603.  
  604.                 $cache_file = $this->cache_dir.'/svnlog_'.md5($file).'_'.$vini.'_'.$vend.'.ser';
  605.                 if ($this->use_cache && file_exists($cache_file)) {
  606.                         return unserialize(file_get_contents($cache_file));
  607.                 }
  608.  
  609.                 $url = $this->cleanURL($this->_url . "/!svn/bc/" . $this->actVersion . "/" . $file . "/");
  610.                 $args = array();
  611.                 $this->initQuery($args, "REPORT", $url);
  612.                 $args['Body']                      = sprintf(self::PHPSVN_LOGS_REQUEST, $vini, $vend);
  613.                 $args['Headers']['Content-Length'] = strlen($args['Body']);
  614.                 $args['Headers']['Depth']          = 1;
  615.  
  616.                 $cache_file2 = $this->cache_dir.'/svnreport_'.md5($url.'|'.$args['Body']).'.xml';
  617.                 if ($this->use_cache && file_exists($cache_file2)) {
  618.                         $body = file_get_contents($cache_file2);
  619.                 } else {
  620.                         $headers = array();
  621.                         $body = '';
  622.                         if (!$this->Request($args, $headers, $body))
  623.                                 throw new Exception(_L("Cannot call getLogsForUpdate (Request failed)"));
  624.                         if (is_dir($this->cache_dir)) {
  625.                                 if ($body) @file_put_contents($cache_file2, $body);
  626.                         }
  627.                 }
  628.  
  629.                 $arrOutput = self::xmlParse($body);
  630.  
  631.                 $revlogs = array();
  632.  
  633.                 $array = array();
  634.                 foreach ($arrOutput as $xmlLogItem) {
  635.                         /*
  636.                         <S:log-item>
  637.                         <D:version-name>164</D:version-name>
  638.                         <S:date>2019-08-13T13:12:13.915920Z</S:date>
  639.                         <D:comment>Update assistant bugfix</D:comment>
  640.                         <D:creator-displayname>daniel-marschall</D:creator-displayname>
  641.                         <S:modified-path node-kind="file" text-mods="true" prop-mods="false">/trunk/update/index.php</S:modified-path>
  642.                         <S:modified-path node-kind="file" text-mods="true" prop-mods="false">/trunk/update/phpsvnclient.inc.php</S:modified-path>
  643.                         </S:log-item>
  644.                         */
  645.  
  646.                         $versionName = '';
  647.                         $date = '';
  648.                         $comment = '';
  649.                         $creator = '';
  650.  
  651.                         foreach ($xmlLogItem as $tagName => $data) {
  652.                                 $tagName = strtoupper($tagName);
  653.                                 $tagName = str_replace('__', ':', $tagName);
  654.                                 if (($tagName == 'S:ADDED-PATH') || ($tagName == 'S:MODIFIED-PATH') || ($tagName == 'S:DELETED-PATH')) {
  655.                                         if ($data->attributes()['node-kind'] == "file") {
  656.                                                 $array['objects'][] = array(
  657.                                                         'object_name' => (string)$data,
  658.                                                         'action' => $tagName,
  659.                                                         'type' => 'file'
  660.                                                 );
  661.                                         } else if ($data->attributes()['node-kind'] == "dir") {
  662.                                                 $array['objects'][] = array(
  663.                                                         'object_name' => (string)$data,
  664.                                                         'action' => $tagName,
  665.                                                         'type' => 'dir'
  666.                                                 );
  667.                                         }
  668.                                 } else if ($tagName == 'D:VERSION-NAME') {
  669.                                         $versionName = (string)$data;
  670.                                 } else if ($tagName == 'S:DATE') {
  671.                                         $date = (string)$data;
  672.                                 } else if ($tagName == 'D:COMMENT') {
  673.                                         $comment = (string)$data;
  674.                                 } else if ($tagName == 'D:CREATOR-DISPLAYNAME') {
  675.                                         $creator = (string)$data;
  676.                                 }
  677.                         }
  678.                         $revlogs[] = array('versionName' => $versionName,
  679.                                            'date' => $date,
  680.                                            'comment' => $comment,
  681.                                            'creator' => $creator);
  682.                 }
  683.  
  684.                 $files       = array();
  685.                 $filesDelete = array();
  686.                 $dirs        = array();
  687.                 $dirsNew     = array();
  688.                 $dirsMod     = array();
  689.                 $dirsDelete  = array();
  690.  
  691.                 if (!isset($array['objects'])) $array['objects'] = array();
  692.                 foreach ($array['objects'] as $objects) {
  693.                         // This section was completely changed by Daniel Marschall
  694.                         if ($objects['type'] == "file") {
  695.                                 if ($objects['action'] == "S:ADDED-PATH" || $objects['action'] == "S:MODIFIED-PATH") {
  696.                                         self::xarray_add($objects['object_name'], $files);
  697.                                         self::xarray_remove($objects['object_name'], $filesDelete);
  698.                                 }
  699.                                 if ($objects['action'] == "S:DELETED-PATH") {
  700.                                         self::xarray_add($objects['object_name'], $filesDelete);
  701.                                         self::xarray_remove($objects['object_name'], $files);
  702.                                 }
  703.                         }
  704.                         if ($objects['type'] == "dir") {
  705.                                 if ($objects['action'] == "S:ADDED-PATH") {
  706.                                         self::xarray_add($objects['object_name'], $dirs);
  707.                                         self::xarray_add($objects['object_name'], $dirsNew);
  708.                                         self::xarray_remove($objects['object_name'], $dirsDelete);
  709.                                 }
  710.                                 if ($objects['action'] == "S:MODIFIED-PATH") {
  711.                                         self::xarray_add($objects['object_name'], $dirs);
  712.                                         self::xarray_add($objects['object_name'], $dirsMod);
  713.                                         self::xarray_remove($objects['object_name'], $dirsDelete);
  714.                                 }
  715.                                 if ($objects['action'] == "S:DELETED-PATH") {
  716.                                         // Delete files from filelist
  717.                                         $files_copy = $files;
  718.                                         foreach ($files_copy as $file) {
  719.                                                 if (strpos($file, $objects['object_name'].'/') === 0) self::xarray_remove($file, $files);
  720.                                         }
  721.                                         // END OF Delete files from filelist
  722.                                         // Delete dirs from dirslist
  723.                                         self::xarray_add($objects['object_name'], $dirsDelete);
  724.                                         self::xarray_remove($objects['object_name'], $dirs);
  725.                                         self::xarray_remove($objects['object_name'], $dirsMod);
  726.                                         self::xarray_remove($objects['object_name'], $dirsNew);
  727.                                         // END OF Delete dirs from dirslist
  728.                                 }
  729.                         }
  730.                 }
  731.                 foreach ($dirsNew as $dir) {
  732.                         // For new directories, also download all its contents
  733.                         try {
  734.                                 $contents = $this->getDirectoryTree($dir, $vend, true);
  735.                         } catch (Exception $e) {
  736.                                 // This can happen when you update from a very old version and a directory was new which is not existing in the newest ($vend) version
  737.                                 // In this case, we don't need it and can ignore the error
  738.                                 $contents = array();
  739.                         }
  740.                         foreach ($contents as $cont) {
  741.                                 if ($cont['type'] == 'directory') {
  742.                                         $dirname = '/'.urldecode($cont['path']);
  743.                                         self::xarray_add($dirname, $dirs);
  744.                                         self::xarray_remove($dirname, $dirsDelete);
  745.                                 } else if ($cont['type'] == 'file') {
  746.                                         $filename = '/'.urldecode($cont['path']);
  747.                                         self::xarray_add($filename, $files);
  748.                                         self::xarray_remove($filename, $filesDelete);
  749.                                 }
  750.                         }
  751.                 }
  752.                 $out                = array();
  753.                 $out['files']       = $files;
  754.                 $out['filesDelete'] = $filesDelete;
  755.                 $out['dirs']        = $dirs;
  756.                 $out['dirsDelete']  = $dirsDelete;
  757.                 $out['revisions']   = $revlogs;
  758.  
  759.                 if (is_dir($this->cache_dir)) {
  760.                         $data = serialize($out);
  761.                         if ($data) @file_put_contents($cache_file, $data);
  762.                 }
  763.  
  764.                 return $out;
  765.         }
  766.  
  767.         /**
  768.          *  Returns the repository version
  769.          *
  770.          *  @return integer Repository version
  771.          *  @access public
  772.          */
  773.         public function getVersion()
  774.         {
  775.                 if ($this->_repVersion > 0)
  776.                         return $this->_repVersion;
  777.  
  778.                 $this->_repVersion = -1;
  779.                 $args = array();
  780.                 $this->initQuery($args, "PROPFIND", $this->cleanURL($this->_url . "/!svn/vcc/default"));
  781.                 $args['Body']                      = self::PHPSVN_VERSION_REQUEST;
  782.                 $args['Headers']['Content-Length'] = strlen(self::PHPSVN_NORMAL_REQUEST);
  783.                 $args['Headers']['Depth']          = 0;
  784.  
  785.                 $tmp = array();
  786.                 $body = '';
  787.                 if (!$this->Request($args, $tmp, $body))
  788.                         throw new Exception(_L("Cannot get repository revision (Request failed)"));
  789.  
  790.                 $this->_repVersion = -1;
  791.                 $m = array();
  792.                 if (preg_match('@/(\d+)\s*</D:href>@ismU', $body, $m)) {
  793.                         $this->_repVersion = $m[1];
  794.                 } else {
  795.                         throw new Exception(_L("Cannot get repository revision (RegEx failed)"));
  796.                 }
  797.  
  798.                 return $this->_repVersion;
  799.         }
  800.  
  801.         /**
  802.          *  Private Functions
  803.          */
  804.  
  805.         /**
  806.          *  Prepare HTTP CLIENT object
  807.          *
  808.          *  @param array<string,string|array<string,string>> $arguments Byreferences variable.
  809.          *  @param string $method Method for the request (GET,POST,PROPFIND, REPORT,ETC).
  810.          *  @param string $url URL for the action.
  811.          *  @return void
  812.          *  @access private
  813.          */
  814.         private function initQuery(&$arguments, $method, $url)
  815.         {
  816.                 $http =& $this->_http;
  817.                 $http->GetRequestArguments($url, $arguments);
  818.                 $arguments["RequestMethod"]           = $method;
  819.                 $arguments["Headers"]["Content-Type"] = "text/xml";
  820.                 $arguments["Headers"]["Depth"]        = 1;
  821.         }
  822.  
  823.         /**
  824.          *  Open a connection, send request, read header
  825.          *  and body.
  826.          *
  827.          *  @param Array<string,string> $args Connetion's argument
  828.          *  @param Array<string,string> $headers Array with the header response.
  829.          *  @param string $body Body response.
  830.          *  @return boolean True is query success
  831.          *  @access private
  832.          */
  833.         private function Request($args, &$headers, &$body)
  834.         {
  835.                 $args['RequestURI'] = str_replace(' ', '%20', $args['RequestURI']); //Hack to make filenames with spaces work.
  836.                 $http =& $this->_http;
  837.                 $http->Open($args);
  838.                 $http->SendRequest($args);
  839.                 $http->ReadReplyHeaders($headers);
  840.                 if (substr($http->response_status,0,1) != 2) {
  841.                         switch ($http->response_status) {
  842.                                 case 404:
  843.                                         $this->errNro = self::NOT_FOUND;
  844.                                         break;
  845.                                 case 401:
  846.                                         $this->errNro = self::AUTH_REQUIRED;
  847.                                         break;
  848.                                 default:
  849.                                         $this->errNro = self::UNKNOWN_ERROR;
  850.                                         break;
  851.                         }
  852.                         //throw new Exception(_L('HTTP Error: %1 at %2,$http->response_status,$args['RequestURI']));
  853.                         $http->close();
  854.                         return false;
  855.                 }
  856.                 $this->errNro = self::NO_ERROR;
  857.                 $body         = '';
  858.                 $tbody        = '';
  859.                 for (;;) {
  860.                         $error = $http->ReadReplyBody($tbody, 1000);
  861.                         if ($error != "" || strlen($tbody) == 0) {
  862.                                 break;
  863.                         }
  864.                         $body .= ($tbody);
  865.                 }
  866.                 $http->close();
  867.                 return true;
  868.         }
  869.  
  870.         /**
  871.          *  Returns $url stripped of '//'
  872.          *
  873.          *  Delete "//" on URL requests.
  874.          *
  875.          *  @param string $url URL
  876.          *  @return string New cleaned URL.
  877.          *  @access private
  878.          */
  879.         private function cleanURL($url)
  880.         {
  881.                 return preg_replace("/((^:)\/\/)/", "//", $url);
  882.         }
  883.  
  884.         /**
  885.         * @return SimpleXMLElement
  886.         */
  887.         private static function xmlParse($strInputXML) {
  888.                 $strInputXML = preg_replace('@<([^>]+):@ismU','<\\1__',$strInputXML);
  889.                 return simplexml_load_string($strInputXML);
  890.         }
  891.  
  892.         /*
  893.           Small helper functions
  894.         */
  895.  
  896.         private static function xarray_add($needle, &$array) {
  897.                 $key = array_search($needle, $array);
  898.                 if ($key === false) {
  899.                         $array[] = $needle;
  900.                 }
  901.         }
  902.  
  903.         private static function xarray_remove($needle, &$array) {
  904.                 while (true) {
  905.                         $key = array_search($needle, $array);
  906.                         if ($key === false) break;
  907.                         unset($array[$key]);
  908.                 }
  909.         }
  910. }
  911.