Subversion Repositories oidplus

Rev

Rev 594 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
171 daniel-mar 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
505 daniel-mar 11
 *    CHANGES by Daniel Marschall, ViaThinkSoft in 2019-2021:
171 daniel-mar 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.
172 daniel-mar 14
 *      The only important functions are getVersion() and updateWorkingCopy()
505 daniel-mar 15
 *    - The dependency class xml2array was removed and instead, SimpleXML is used
172 daniel-mar 16
 *    - Added "revision log/comment" functionality
171 daniel-mar 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
 
505 daniel-mar 25
if (!function_exists('_L')) {
26
        function _L($str, ...$sprintfArgs) {
605 daniel-mar 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;
505 daniel-mar 34
        }
35
}
360 daniel-mar 36
 
171 daniel-mar 37
/**
38
 *  PHP SVN CLIENT
39
 *
360 daniel-mar 40
 *  This class is an SVN client. It can perform read operations
41
 *  to an SVN server (over Web-DAV).
171 daniel-mar 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
 */
251 daniel-mar 48
class phpsvnclient {
49
 
269 daniel-mar 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>';
293 daniel-mar 51
 
269 daniel-mar 52
        /*protected*/ const PHPSVN_VERSION_REQUEST = '<?xml version="1.0" encoding="utf-8"?><propfind xmlns="DAV:"><prop><checked-in xmlns="DAV:"/></prop></propfind>';
293 daniel-mar 53
 
269 daniel-mar 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>';
293 daniel-mar 55
 
505 daniel-mar 56
        /*public*/ const NO_ERROR = 1;
57
        /*public*/ const NOT_FOUND = 2;
58
        /*public*/ const AUTH_REQUIRED = 3;
59
        /*public*/ const UNKNOWN_ERROR = 4;
251 daniel-mar 60
 
505 daniel-mar 61
        public $use_cache = true;
62
        public $cache_dir = __DIR__.'/../../userdata/cache';
63
 
171 daniel-mar 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
386 daniel-mar 84
         *  @var int
171 daniel-mar 85
         */
86
        private $_repVersion;
87
 
88
        /**
89
         *  Last error number
90
         *
505 daniel-mar 91
         *  Possible values are NO_ERROR, NOT_FOUND, AUTH_REQUIRED, UNKOWN_ERROR
171 daniel-mar 92
         *
505 daniel-mar 93
         *  @access protected
171 daniel-mar 94
         *  @var integer
95
         */
505 daniel-mar 96
        protected $errNro = self::NO_ERROR;
97
        public function getLastError() {
98
                return $this->errNro;
99
        }
171 daniel-mar 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.
591 daniel-mar 122
         * @param string $path The path to the directory that will be created.
171 daniel-mar 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.
591 daniel-mar 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.
171 daniel-mar 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
        /**
172 daniel-mar 164
        * Updates a working copy
591 daniel-mar 165
        * @param string $from_revision Either a revision number or a text file with the
605 daniel-mar 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
172 daniel-mar 173
        **/
174
        public function updateWorkingCopy($from_revision='version.txt', $folder = '/trunk/', $outPath = '.', $preview = false)
171 daniel-mar 175
        {
176
                if (!is_dir($outPath)) {
505 daniel-mar 177
                        echo _L("ERROR: Local path %1 not existing",$outPath)."\n";
532 daniel-mar 178
                        //flush();
171 daniel-mar 179
                        return false;
180
                }
181
 
303 daniel-mar 182
                $webbrowser_update = !is_numeric($from_revision);
183
 
172 daniel-mar 184
                if (!is_numeric($from_revision)) {
185
                        $version_file = $from_revision;
605 daniel-mar 186
                        $from_revision = -1;
172 daniel-mar 187
 
188
                        if (!file_exists($version_file)) {
505 daniel-mar 189
                                echo _L("ERROR: %1 missing",$version_file)."\n";
532 daniel-mar 190
                                //flush();
171 daniel-mar 191
                                return false;
172 daniel-mar 192
                        } else {
193
                                //Obtain the number of current version number of the local copy.
194
                                $cont = file_get_contents($version_file);
386 daniel-mar 195
                                $m = array();
505 daniel-mar 196
                                if (!preg_match('@Revision (\d+)@', $cont, $m)) { // do not translate
197
                                        echo _L("ERROR: %1 unknown format",$version_file)."\n";
532 daniel-mar 198
                                        //flush();
172 daniel-mar 199
                                        return false;
200
                                }
201
                                $from_revision = $m[1];
202
 
506 daniel-mar 203
                                echo _L("Found %1 with revision information %2",basename($version_file),$from_revision)."\n";
532 daniel-mar 204
                                //flush();
171 daniel-mar 205
                        }
172 daniel-mar 206
                } else {
207
                        $version_file = '';
208
                }
171 daniel-mar 209
 
172 daniel-mar 210
                $errors_happened = false;
211
 
303 daniel-mar 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);
505 daniel-mar 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";
532 daniel-mar 218
                                //flush();
303 daniel-mar 219
                                if (!$preview) return false;
220
                        }
221
                        @unlink($file);
222
                        if (file_exists($file)) {
505 daniel-mar 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";
532 daniel-mar 224
                                //flush();
303 daniel-mar 225
                                if (!$preview) return false;
226
                        }
172 daniel-mar 227
                }
171 daniel-mar 228
 
172 daniel-mar 229
                //Get a list of objects to be updated.
505 daniel-mar 230
                $objects_list = $this->getLogsForUpdate($folder, $from_revision + 1);
172 daniel-mar 231
                if (!is_null($objects_list)) {
232
                        // Output version information
233
                        foreach ($objects_list['revisions'] as $revision) {
505 daniel-mar 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'])).") ";
216 daniel-mar 236
                                echo trim($tex . str_replace("\n", "\n".str_repeat(' ', strlen($tex)), $comment));
186 daniel-mar 237
                                echo "\n";
171 daniel-mar 238
                        }
239
 
172 daniel-mar 240
                        // Add dirs
200 daniel-mar 241
                        sort($objects_list['dirs']); // <-- added by Daniel Marschall: Sort folder list, so that directories will be created in the correct hierarchical order
172 daniel-mar 242
                        foreach ($objects_list['dirs'] as $file) {
243
                                if ($file != '') {
244
                                        $localPath = str_replace($folder, "", $file);
605 daniel-mar 245
                                        $localPath = rtrim($outPath,DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . ltrim($localPath,DIRECTORY_SEPARATOR);
171 daniel-mar 246
 
505 daniel-mar 247
                                        echo _L("Added or modified directory: %1",$file)."\n";
532 daniel-mar 248
                                        //flush();
172 daniel-mar 249
                                        if (!$preview) {
250
                                                $this->createDirs($localPath);
251
                                                if (!is_dir($localPath)) {
252
                                                        $errors_happened = true;
505 daniel-mar 253
                                                        echo "=> "._L("FAILED")."\n";
532 daniel-mar 254
                                                        //flush();
171 daniel-mar 255
                                                }
256
                                        }
257
                                }
172 daniel-mar 258
                        }
171 daniel-mar 259
 
172 daniel-mar 260
                        // Add files
200 daniel-mar 261
                        sort($objects_list['files']); // <-- added by Daniel Marschall: Sort list, just for cosmetic improvement
172 daniel-mar 262
                        foreach ($objects_list['files'] as $file) {
263
                                if ($file != '') {
264
                                        $localFile = str_replace($folder, "", $file);
605 daniel-mar 265
                                        $localFile = rtrim($outPath,DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . ltrim($localFile,DIRECTORY_SEPARATOR);
171 daniel-mar 266
 
505 daniel-mar 267
                                        echo _L("Added or modified file: %1",$file)."\n";
532 daniel-mar 268
                                        //flush();
172 daniel-mar 269
                                        if (!$preview) {
591 daniel-mar 270
                                                try {
271
                                                        $contents = $this->getFile($file);
272
                                                } catch (Exception $e) {
273
                                                        $contents = false;
274
                                                }
554 daniel-mar 275
                                                if (($contents === false) || (@file_put_contents($localFile, $contents) === false)) {
172 daniel-mar 276
                                                        $errors_happened = true;
505 daniel-mar 277
                                                        echo "=> "._L("FAILED")."\n";
532 daniel-mar 278
                                                        //flush();
171 daniel-mar 279
                                                }
280
                                        }
281
                                }
172 daniel-mar 282
                        }
216 daniel-mar 283
 
284
                        // Remove files
200 daniel-mar 285
                        sort($objects_list['filesDelete']); // <-- added by Daniel Marschall: Sort list, just for cosmetic improvement
172 daniel-mar 286
                        foreach ($objects_list['filesDelete'] as $file) {
287
                                if ($file != '') {
288
                                        $localFile = str_replace($folder, "", $file);
605 daniel-mar 289
                                        $localFile = rtrim($outPath,DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . ltrim($localFile,DIRECTORY_SEPARATOR);
171 daniel-mar 290
 
505 daniel-mar 291
                                        echo _L("Removed file: %1",$file)."\n";
532 daniel-mar 292
                                        //flush();
171 daniel-mar 293
 
172 daniel-mar 294
                                        if (!$preview) {
295
                                                @unlink($localFile);
296
                                                if (file_exists($localFile)) {
297
                                                        $errors_happened = true;
505 daniel-mar 298
                                                        echo "=> "._L("FAILED")."\n";
532 daniel-mar 299
                                                        //flush();
171 daniel-mar 300
                                                }
301
                                        }
302
                                }
172 daniel-mar 303
                        }
171 daniel-mar 304
 
172 daniel-mar 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
200 daniel-mar 307
                        rsort($objects_list['dirsDelete']); // <-- added by Daniel Marschall: Sort list in reverse order, so that directories get deleted in the correct hierarchical order
172 daniel-mar 308
                        foreach ($objects_list['dirsDelete'] as $file) {
309
                                if ($file != '') {
310
                                        $localPath = str_replace($folder, "", $file);
605 daniel-mar 311
                                        $localPath = rtrim($outPath,DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . ltrim($localPath,DIRECTORY_SEPARATOR);
171 daniel-mar 312
 
505 daniel-mar 313
                                        echo _L("Removed directory: %1",$file)."\n";
532 daniel-mar 314
                                        //flush();
171 daniel-mar 315
 
172 daniel-mar 316
                                        if (!$preview) {
317
                                                $this->removeDirs($localPath);
318
                                                if (is_dir($localPath)) {
319
                                                        $errors_happened = true;
505 daniel-mar 320
                                                        echo "=> "._L("FAILED")."\n";
532 daniel-mar 321
                                                        //flush();
171 daniel-mar 322
                                                }
323
                                        }
324
                                }
172 daniel-mar 325
                        }
171 daniel-mar 326
 
216 daniel-mar 327
                        // Update version file
172 daniel-mar 328
                        // Changed by Daniel Marschall: Added $errors_happened
329
                        if (!$preview && !empty($version_file)) {
330
                                if (!$errors_happened) {
505 daniel-mar 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";
532 daniel-mar 333
                                                //flush();
172 daniel-mar 334
                                                return false;
171 daniel-mar 335
                                        } else {
505 daniel-mar 336
                                                echo _L("Set revision to %1", $this->actVersion) . "\n";
532 daniel-mar 337
                                                //flush();
172 daniel-mar 338
                                                return true;
171 daniel-mar 339
                                        }
172 daniel-mar 340
                                } else {
505 daniel-mar 341
                                        echo _L("Revision NOT set to %1 because some files/dirs could not be updated. Please try again.",$this->actVersion)."\n";
532 daniel-mar 342
                                        //flush();
172 daniel-mar 343
                                        return false;
171 daniel-mar 344
                                }
172 daniel-mar 345
                        } else {
346
                                return true;
171 daniel-mar 347
                        }
591 daniel-mar 348
                } else {
349
                        return true;
171 daniel-mar 350
                }
351
        }
352
 
353
        /**
354
         *  rawDirectoryDump
355
         *
605 daniel-mar 356
         *  Dumps SVN data for $folder in the version $version of the repository.
171 daniel-mar 357
         *
358
         *  @param string  $folder Folder to get data
359
         *  @param integer $version Repository version, -1 means actual
594 daniel-mar 360
         *  @return SimpleXMLElement SVN data dump.
171 daniel-mar 361
         */
362
        private function rawDirectoryDump($folder = '/trunk/', $version = -1)
363
        {
364
                if ($version == -1 || $version > $this->actVersion) {
365
                        $version = $this->actVersion;
366
                }
505 daniel-mar 367
 
171 daniel-mar 368
                $url = $this->cleanURL($this->_url . "/!svn/bc/" . $version . "/" . $folder . "/");
386 daniel-mar 369
                $args = array();
171 daniel-mar 370
                $this->initQuery($args, "PROPFIND", $url);
251 daniel-mar 371
                $args['Body']                      = self::PHPSVN_NORMAL_REQUEST;
372
                $args['Headers']['Content-Length'] = strlen(self::PHPSVN_NORMAL_REQUEST);
171 daniel-mar 373
 
505 daniel-mar 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
                }
171 daniel-mar 386
 
216 daniel-mar 387
                return self::xmlParse($body);
171 daniel-mar 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
594 daniel-mar 398
         *  @return array<array<string,string>> List of files.
171 daniel-mar 399
         */
400
        private function getDirectoryFiles($folder = '/trunk/', $version = -1)
401
        {
505 daniel-mar 402
                $responses = $this->rawDirectoryDump($folder, $version);
403
 
404
                if ($responses) {
171 daniel-mar 405
                        $files = array();
505 daniel-mar 406
                        foreach ($responses as $response) {
172 daniel-mar 407
 
505 daniel-mar 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
                                }
172 daniel-mar 413
 
505 daniel-mar 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
                                );
172 daniel-mar 421
 
505 daniel-mar 422
                                // Detect 'type' as either a 'directory' or 'file'
554 daniel-mar 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']);
505 daniel-mar 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);
171 daniel-mar 440
                        }
505 daniel-mar 441
 
171 daniel-mar 442
                        return $files;
505 daniel-mar 443
 
444
                } else {
445
 
446
                        throw new Exception(_L("Error communicating with SVN server"));
447
 
171 daniel-mar 448
                }
449
        }
450
 
306 daniel-mar 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) {
350 daniel-mar 469
                        $c = str_replace('\\', '/', $c);
306 daniel-mar 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') {
316 daniel-mar 484
                                $svn_cont[] = '/'.urldecode($cont['path']).'/';
306 daniel-mar 485
                        } else if ($cont['type'] == 'file') {
316 daniel-mar 486
                                $svn_cont[] = '/'.urldecode($cont['path']);
306 daniel-mar 487
                        }
488
                }
489
                foreach ($svn_cont as $key => &$c) {
350 daniel-mar 490
                        $c = str_replace('\\', '/', $c);
306 daniel-mar 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
 
171 daniel-mar 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
         *
594 daniel-mar 519
         *  @return array<string,string>|array<array<string,string>>|false List of files and directories.
171 daniel-mar 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
         *
605 daniel-mar 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
171 daniel-mar 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);
554 daniel-mar 570
                if ($fileInfo["type"] == "directory") {
605 daniel-mar 571
                        throw new Exception(_L("File %1 is detected as directory. Cannot receive file contents.", $file));
554 daniel-mar 572
                        //return false;
573
                }
171 daniel-mar 574
 
386 daniel-mar 575
                $args = array();
171 daniel-mar 576
                $url = $this->cleanURL($this->_url . "/!svn/bc/" . $version . "/" . $file . "/");
577
                $this->initQuery($args, "GET", $url);
386 daniel-mar 578
                $headers = array();
579
                $body = '';
171 daniel-mar 580
                if (!$this->Request($args, $headers, $body))
505 daniel-mar 581
                        throw new Exception(_L("Cannot call getFile (Request failed)"));
171 daniel-mar 582
 
583
                return $body;
584
        }
585
 
505 daniel-mar 586
        private function getLogsForUpdate($file, $vini = 0, $vend = -1)
171 daniel-mar 587
        {
588
                $fileLogs = array();
589
 
172 daniel-mar 590
                if ($vend == -1) {
171 daniel-mar 591
                        $vend = $this->actVersion;
592
                }
593
 
594
                if ($vini < 0)
595
                        $vini = 0;
596
 
597
                if ($vini > $vend) {
598
                        $vini = $vend;
505 daniel-mar 599
                        echo _L("Nothing updated")."\n";
532 daniel-mar 600
                        //flush();
171 daniel-mar 601
                        return null;
602
                }
603
 
505 daniel-mar 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));
493 daniel-mar 607
                }
608
 
171 daniel-mar 609
                $url = $this->cleanURL($this->_url . "/!svn/bc/" . $this->actVersion . "/" . $file . "/");
386 daniel-mar 610
                $args = array();
171 daniel-mar 611
                $this->initQuery($args, "REPORT", $url);
251 daniel-mar 612
                $args['Body']                      = sprintf(self::PHPSVN_LOGS_REQUEST, $vini, $vend);
171 daniel-mar 613
                $args['Headers']['Content-Length'] = strlen($args['Body']);
614
                $args['Headers']['Depth']          = 1;
615
 
505 daniel-mar 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
                }
171 daniel-mar 628
 
216 daniel-mar 629
                $arrOutput = self::xmlParse($body);
171 daniel-mar 630
 
172 daniel-mar 631
                $revlogs = array();
632
 
171 daniel-mar 633
                $array = array();
505 daniel-mar 634
                foreach ($arrOutput as $xmlLogItem) {
172 daniel-mar 635
                        /*
605 daniel-mar 636
                        <S:log-item>
172 daniel-mar 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 = '';
505 daniel-mar 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") {
171 daniel-mar 656
                                                $array['objects'][] = array(
505 daniel-mar 657
                                                        'object_name' => (string)$data,
658
                                                        'action' => $tagName,
171 daniel-mar 659
                                                        'type' => 'file'
660
                                                );
505 daniel-mar 661
                                        } else if ($data->attributes()['node-kind'] == "dir") {
171 daniel-mar 662
                                                $array['objects'][] = array(
505 daniel-mar 663
                                                        'object_name' => (string)$data,
664
                                                        'action' => $tagName,
171 daniel-mar 665
                                                        'type' => 'dir'
666
                                                );
667
                                        }
505 daniel-mar 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;
171 daniel-mar 676
                                }
677
                        }
605 daniel-mar 678
                        $revlogs[] = array('versionName' => $versionName,
172 daniel-mar 679
                                           'date' => $date,
605 daniel-mar 680
                                           'comment' => $comment,
681
                                           'creator' => $creator);
171 daniel-mar 682
                }
505 daniel-mar 683
 
216 daniel-mar 684
                $files       = array();
685
                $filesDelete = array();
686
                $dirs        = array();
315 daniel-mar 687
                $dirsNew     = array();
688
                $dirsMod     = array();
216 daniel-mar 689
                $dirsDelete  = array();
171 daniel-mar 690
 
227 daniel-mar 691
                if (!isset($array['objects'])) $array['objects'] = array();
171 daniel-mar 692
                foreach ($array['objects'] as $objects) {
216 daniel-mar 693
                        // This section was completely changed by Daniel Marschall
171 daniel-mar 694
                        if ($objects['type'] == "file") {
695
                                if ($objects['action'] == "S:ADDED-PATH" || $objects['action'] == "S:MODIFIED-PATH") {
216 daniel-mar 696
                                        self::xarray_add($objects['object_name'], $files);
697
                                        self::xarray_remove($objects['object_name'], $filesDelete);
171 daniel-mar 698
                                }
699
                                if ($objects['action'] == "S:DELETED-PATH") {
216 daniel-mar 700
                                        self::xarray_add($objects['object_name'], $filesDelete);
701
                                        self::xarray_remove($objects['object_name'], $files);
171 daniel-mar 702
                                }
703
                        }
704
                        if ($objects['type'] == "dir") {
315 daniel-mar 705
                                if ($objects['action'] == "S:ADDED-PATH") {
216 daniel-mar 706
                                        self::xarray_add($objects['object_name'], $dirs);
315 daniel-mar 707
                                        self::xarray_add($objects['object_name'], $dirsNew);
216 daniel-mar 708
                                        self::xarray_remove($objects['object_name'], $dirsDelete);
171 daniel-mar 709
                                }
315 daniel-mar 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
                                }
171 daniel-mar 715
                                if ($objects['action'] == "S:DELETED-PATH") {
716
                                        // Delete files from filelist
216 daniel-mar 717
                                        $files_copy = $files;
718
                                        foreach ($files_copy as $file) {
719
                                                if (strpos($file, $objects['object_name'].'/') === 0) self::xarray_remove($file, $files);
171 daniel-mar 720
                                        }
721
                                        // END OF Delete files from filelist
722
                                        // Delete dirs from dirslist
216 daniel-mar 723
                                        self::xarray_add($objects['object_name'], $dirsDelete);
724
                                        self::xarray_remove($objects['object_name'], $dirs);
315 daniel-mar 725
                                        self::xarray_remove($objects['object_name'], $dirsMod);
726
                                        self::xarray_remove($objects['object_name'], $dirsNew);
171 daniel-mar 727
                                        // END OF Delete dirs from dirslist
728
                                }
729
                        }
730
                }
315 daniel-mar 731
                foreach ($dirsNew as $dir) {
732
                        // For new directories, also download all its contents
316 daniel-mar 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
                        }
302 daniel-mar 740
                        foreach ($contents as $cont) {
741
                                if ($cont['type'] == 'directory') {
316 daniel-mar 742
                                        $dirname = '/'.urldecode($cont['path']);
302 daniel-mar 743
                                        self::xarray_add($dirname, $dirs);
744
                                        self::xarray_remove($dirname, $dirsDelete);
745
                                } else if ($cont['type'] == 'file') {
316 daniel-mar 746
                                        $filename = '/'.urldecode($cont['path']);
302 daniel-mar 747
                                        self::xarray_add($filename, $files);
748
                                        self::xarray_remove($filename, $filesDelete);
749
                                }
750
                        }
751
                }
171 daniel-mar 752
                $out                = array();
753
                $out['files']       = $files;
754
                $out['filesDelete'] = $filesDelete;
755
                $out['dirs']        = $dirs;
756
                $out['dirsDelete']  = $dirsDelete;
172 daniel-mar 757
                $out['revisions']   = $revlogs;
493 daniel-mar 758
 
505 daniel-mar 759
                if (is_dir($this->cache_dir)) {
760
                        $data = serialize($out);
761
                        if ($data) @file_put_contents($cache_file, $data);
493 daniel-mar 762
                }
763
 
171 daniel-mar 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;
386 daniel-mar 779
                $args = array();
171 daniel-mar 780
                $this->initQuery($args, "PROPFIND", $this->cleanURL($this->_url . "/!svn/vcc/default"));
251 daniel-mar 781
                $args['Body']                      = self::PHPSVN_VERSION_REQUEST;
782
                $args['Headers']['Content-Length'] = strlen(self::PHPSVN_NORMAL_REQUEST);
171 daniel-mar 783
                $args['Headers']['Depth']          = 0;
784
 
386 daniel-mar 785
                $tmp = array();
786
                $body = '';
171 daniel-mar 787
                if (!$this->Request($args, $tmp, $body))
505 daniel-mar 788
                        throw new Exception(_L("Cannot get repository revision (Request failed)"));
171 daniel-mar 789
 
591 daniel-mar 790
                $this->_repVersion = -1;
386 daniel-mar 791
                $m = array();
171 daniel-mar 792
                if (preg_match('@/(\d+)\s*</D:href>@ismU', $body, $m)) {
793
                        $this->_repVersion = $m[1];
794
                } else {
505 daniel-mar 795
                        throw new Exception(_L("Cannot get repository revision (RegEx failed)"));
171 daniel-mar 796
                }
797
 
798
                return $this->_repVersion;
799
        }
800
 
801
        /**
802
         *  Private Functions
803
         */
804
 
805
        /**
806
         *  Prepare HTTP CLIENT object
807
         *
594 daniel-mar 808
         *  @param array<string,string|array<string,string>> $arguments Byreferences variable.
171 daniel-mar 809
         *  @param string $method Method for the request (GET,POST,PROPFIND, REPORT,ETC).
810
         *  @param string $url URL for the action.
594 daniel-mar 811
         *  @return void
171 daniel-mar 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
         *
594 daniel-mar 827
         *  @param Array<string,string> $args Connetion's argument
828
         *  @param Array<string,string> $headers Array with the header response.
591 daniel-mar 829
         *  @param string $body Body response.
171 daniel-mar 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);
415 daniel-mar 840
                if (substr($http->response_status,0,1) != 2) {
171 daniel-mar 841
                        switch ($http->response_status) {
842
                                case 404:
251 daniel-mar 843
                                        $this->errNro = self::NOT_FOUND;
171 daniel-mar 844
                                        break;
845
                                case 401:
251 daniel-mar 846
                                        $this->errNro = self::AUTH_REQUIRED;
171 daniel-mar 847
                                        break;
848
                                default:
251 daniel-mar 849
                                        $this->errNro = self::UNKNOWN_ERROR;
171 daniel-mar 850
                                        break;
851
                        }
505 daniel-mar 852
                        //throw new Exception(_L('HTTP Error: %1 at %2,$http->response_status,$args['RequestURI']));
171 daniel-mar 853
                        $http->close();
854
                        return false;
855
                }
251 daniel-mar 856
                $this->errNro = self::NO_ERROR;
171 daniel-mar 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
 
594 daniel-mar 884
        /**
885
        * @return SimpleXMLElement
886
        */
171 daniel-mar 887
        private static function xmlParse($strInputXML) {
505 daniel-mar 888
                $strInputXML = preg_replace('@<([^>]+):@ismU','<\\1__',$strInputXML);
889
                return simplexml_load_string($strInputXML);
171 daniel-mar 890
        }
216 daniel-mar 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
        }
493 daniel-mar 910
}