Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 145
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
SubversionShell
0.00% covered (danger)
0.00%
0 / 145
0.00% covered (danger)
0.00%
0 / 7
1640
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 canConnect
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 getFile
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getDiff
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 getLog
0.00% covered (danger)
0.00%
0 / 72
0.00% covered (danger)
0.00%
0 / 1
380
 getDirList
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
132
 getExtraArgs
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace MediaWiki\Extension\CodeReview\Backend;
4
5use DOMDocument;
6use Exception;
7
8/**
9 * Using the thingy-bobber
10 */
11class SubversionShell extends SubversionAdaptor {
12    private const MIN_MEMORY = 204800;
13
14    public function __construct( $repo ) {
15        parent::__construct( $repo );
16        global $wgMaxShellMemory;
17        if ( $wgMaxShellMemory < self::MIN_MEMORY ) {
18            $wgMaxShellMemory = self::MIN_MEMORY;
19            wfDebug( __METHOD__ . " raised wgMaxShellMemory to $wgMaxShellMemory\n" );
20        }
21    }
22
23    public function canConnect() {
24        $command = sprintf(
25            'svn info %s %s',
26            $this->getExtraArgs(),
27            wfEscapeShellArg( $this->mRepoPath )
28        );
29
30        $result = wfShellExec( $command );
31        if ( $result == '' ) {
32            return false;
33        } elseif ( strpos( $result, 'No repository found' ) !== false ) {
34            return false;
35        } else {
36            return true;
37        }
38    }
39
40    public function getFile( $path, $rev = null ) {
41        if ( $rev ) {
42            $path .= "@$rev";
43        }
44        $command = sprintf(
45            'svn cat %s %s',
46            $this->getExtraArgs(),
47            wfEscapeShellArg( $this->mRepoPath . $path ) );
48
49        return wfShellExec( $command );
50    }
51
52    public function getDiff( $path, $rev1, $rev2 ) {
53        $command = sprintf(
54            'svn diff -r%d:%d %s %s',
55            intval( $rev1 ),
56            intval( $rev2 ),
57            $this->getExtraArgs(),
58            wfEscapeShellArg( $this->mRepoPath . $path )
59        );
60
61        return wfShellExec( $command );
62    }
63
64    public function getLog( $path, $startRev = null, $endRev = null ) {
65        $lang = wfIsWindows() ? '' : 'LC_ALL=en_US.utf-8 ';
66        $command = sprintf(
67            "{$lang}svn log -v -r%s:%s %s %s",
68            wfEscapeShellArg( $this->_rev( $startRev, 'BASE' ) ),
69            wfEscapeShellArg( $this->_rev( $endRev, 'HEAD' ) ),
70            $this->getExtraArgs(),
71            wfEscapeShellArg( $this->mRepoPath . $path ) );
72
73        $lines = explode( "\n", wfShellExec( $command ) );
74        $out = [];
75
76        $divider = str_repeat( '-', 72 );
77        $formats = [
78            'rev' => '/^r(\d+)$/',
79            'author' => '/^(.*)$/',
80            // account for '(no date)'
81            'date' => '/^(?:(.*?) )?\(.*\)$/',
82            'lines' => '/^(\d+) lines?$/',
83        ];
84        $state = "start";
85        foreach ( $lines as $line ) {
86            $line = rtrim( $line );
87
88            switch ( $state ) {
89            case 'start':
90                if ( $line == $divider ) {
91                    $state = 'revdata';
92                    break;
93                } else {
94                    return $out;
95                    # throw new Exception( "Unexpected start line: $line" );
96                }
97                // Fall through
98            case 'revdata':
99                if ( $line == '' ) {
100                    $state = 'done';
101                    break;
102                }
103                $data = [];
104                $bits = explode( ' | ', $line );
105                $i = 0;
106                foreach ( $formats as $key => $regex ) {
107                    $text = $bits[$i++];
108                    $matches = [];
109                    if ( preg_match( $regex, $text, $matches ) ) {
110                        $data[$key] = $matches[1];
111                    } else {
112                        throw new Exception(
113                            "Unexpected format for $key in '$text'" );
114                    }
115                }
116                $data['msg'] = '';
117                $data['paths'] = [];
118                $state = 'changedpaths';
119                break;
120            case 'changedpaths':
121                if ( $line == 'Changed paths:' ) {
122                    // broken when svn messages are not in English
123                    $state = 'path';
124                } elseif ( $line == '' ) {
125                    // No changed paths?
126                    $state = 'msg';
127                } else {
128                    throw new Exception(
129                        "Expected 'Changed paths:' or '', got '$line'" );
130                }
131                break;
132            case 'path':
133                if ( $line == '' ) {
134                    // Out of paths. Move on to the message...
135                    $state = 'msg';
136                } else {
137                    $matches = [];
138                    if ( preg_match( '/^   (.) (.*)$/', $line, $matches ) ) {
139                        // @phan-suppress-next-line PhanUndeclaredVariableDim
140                        $data['paths'][] = [
141                            'action' => $matches[1],
142                            'path' => $matches[2]
143                        ];
144                    }
145                }
146                break;
147            case 'msg':
148                $data['msg'] .= $line;
149                // @phan-suppress-next-line PhanTypeArraySuspiciousNull, PhanTypeInvalidUnaryOperandIncOrDec
150                if ( --$data['lines'] ) {
151                    $data['msg'] .= "\n";
152                } else {
153                    unset( $data['lines'] );
154                    $out[] = $data;
155                    $state = 'start';
156                }
157                break;
158            case 'done':
159                throw new Exception( "Unexpected input after end: $line" );
160            default:
161                throw new Exception( "Invalid state '$state'" );
162            }
163        }
164
165        return $out;
166    }
167
168    public function getDirList( $path, $rev = null ) {
169        $command = sprintf(
170            'svn list --xml -r%s %s %s',
171            wfEscapeShellArg( $this->_rev( $rev, 'HEAD' ) ),
172            $this->getExtraArgs(),
173            wfEscapeShellArg( $this->mRepoPath . $path )
174        );
175        $document = new DOMDocument();
176
177        $listXml = wfShellExec( $command );
178        if ( !$listXml || !$document->loadXML( $listXml ) ) {
179            // svn list --xml returns invalid XML if the file does not exist
180            // FIXME: report bug upstream
181            return false;
182        }
183
184        $entries = $document->getElementsByTagName( 'entry' );
185        $result = [];
186        foreach ( $entries as $entry ) {
187            $item = [];
188            $item['type'] = $entry->getAttribute( 'kind' );
189            foreach ( $entry->childNodes as $child ) {
190                switch ( $child->nodeName ) {
191                case 'name':
192                    $item['name'] = $child->textContent;
193                    break;
194                case 'size':
195                    $item['size'] = intval( $child->textContent );
196                    break;
197                case 'commit':
198                    $item['created_rev'] = intval( $child->getAttribute( 'revision' ) );
199                    foreach ( $child->childNodes as $commitEntry ) {
200                        switch ( $commitEntry->nodeName ) {
201                        case 'author':
202                            $item['last_author'] = $commitEntry->textContent;
203                            break;
204                        case 'date':
205                            $item['time_t'] = wfTimestamp( TS_UNIX, $commitEntry->textContent );
206                            break;
207                        }
208                    }
209                    break;
210                }
211            }
212            $result[] = $item;
213        }
214        return $result;
215    }
216
217    /**
218     * Returns a string of extra arguments to be passed into the shell commands
219     * @return string
220     */
221    private function getExtraArgs() {
222        global $wgSubversionOptions, $wgSubversionUser, $wgSubversionPassword;
223        $args = $wgSubversionOptions;
224        if ( $wgSubversionUser ) {
225            $args .= ' --username ' . wfEscapeShellArg( $wgSubversionUser )
226                . ' --password ' . wfEscapeShellArg( $wgSubversionPassword );
227        }
228        return $args;
229    }
230}