Completed
Push — master ( 530e5d...e2eae8 )
by Andreas
17:20
created

midcom_services_rcs_backend_rcs::get_comment()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
ccs 3
cts 3
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author tarjei huse
4
 * @package midcom.services.rcs
5
 * @copyright The Midgard Project, http://www.midgard-project.org
6
 * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
7
 */
8
9
/**
10
 * @package midcom.services.rcs
11
 */
12
class midcom_services_rcs_backend_rcs implements midcom_services_rcs_backend
13
{
14
    /**
15
     * GUID of the current object
16
     */
17
    private $_guid;
18
19
    /**
20
     * Cached revision history for the object
21
     *
22
     * @var midcom_services_rcs_history
23
     */
24
    private $history;
25
26
    /**
27
     * @var midcom_services_rcs_config
28
     */
29 81
    private $_config;
30
31 81
    public function __construct($object, midcom_services_rcs_config $config)
32 81
    {
33 81
        $this->_config = $config;
34
        $this->_guid = $object->guid;
35 80
    }
36
37
    private function _generate_rcs_filename(string $guid) : string
38 80
    {
39 80
        // Keep files organized to subfolders to keep filesystem sane
40 56
        $dirpath = $this->_config->get_rcs_root() . "/{$guid[0]}/{$guid[1]}";
41 56
        if (!file_exists($dirpath)) {
42
            debug_add("Directory {$dirpath} does not exist, attempting to create", MIDCOM_LOG_INFO);
43 80
            mkdir($dirpath, 0777, true);
44
        }
45
        return "{$dirpath}/{$guid}";
46
    }
47
48
    /**
49
     * Save a new revision
50
     *
51
     * @param object $object object to be saved
52 74
     * @return boolean true on success.
53
     */
54
    public function update($object, $updatemessage = null) : bool
55 74
    {
56 39
        // Store user identifier and IP address to the update string
57
        if (midcom::get()->auth->user) {
58 35
            $update_string = midcom::get()->auth->user->id . "|{$_SERVER['REMOTE_ADDR']}";
59
        } else {
60
            $update_string = "NOBODY|{$_SERVER['REMOTE_ADDR']}";
61 74
        }
62
63 74
        $update_string .= "|{$updatemessage}";
64
65
        $result = $this->rcs_update($object, $update_string);
66 74
67
        // The methods return basically what the RCS unix level command returns, so nonzero value is error and zero is ok...
68
        return $result == 0;
69
    }
70
71
    /**
72
     * Get the object of a revision
73
     *
74
     * @param string $revision identifier of revision wanted
75 4
     * @return array array representation of the object
76
     */
77 4
    public function get_revision($revision) : array
78
    {
79
        if (empty($this->_guid)) {
80 4
            return [];
81 4
        }
82
        $filepath = $this->_generate_rcs_filename($this->_guid);
83
        if ($this->exec('co -q -f -r' . escapeshellarg(trim($revision)) . " {$filepath} 2>/dev/null") != 0) {
84
            return [];
85 4
        }
86
87 4
        $data = (file_exists($filepath)) ? file_get_contents($filepath) : '';
88 4
89
        $mapper = new midcom_helper_exporter_xml();
90 4
        $revision = $mapper->data2array($data);
91
92 4
        $this->exec("rm -f {$filepath}", false);
93
94
        return $revision;
95
    }
96
97
    /**
98
     * Lists the number of changes that has been done to the object
99
     * Order: The first entry is the newest.
100 2
     *
101
     * @return array list of changeids
102 2
     */
103 2
    public function get_history() : ?midcom_services_rcs_history
104
    {
105
        if (empty($this->_guid)) {
106
            return null;
107
        }
108
109
        if ($this->history === null) {
110
            $filepath = $this->_generate_rcs_filename($this->_guid);
111
            $this->history = $this->rcs_gethistory($filepath);
112 4
        }
113
114 4
        return $this->history;
115 4
    }
116
117 4
    /* it is debatable to move this into the object when it resides nicely in a library... */
118 2
119
    private function rcs_parse_history_entry(array $entry) : array
120 2
    {
121
        // Create the empty history array
122
        $history = [
123
            'revision' => null,
124
            'date'     => null,
125
            'lines'    => null,
126
            'user'     => null,
127
            'ip'       => null,
128
            'message'  => null,
129 4
        ];
130
131 4
        // Revision number is in format
132 4
        // revision 1.11
133
        $history['revision'] = preg_replace('/(\d+\.\d+).*/', '$1', substr($entry[0], 9));
134 4
135 2
        // Entry metadata is in format
136
        // date: 2006/01/10 09:40:49;  author: www-data;  state: Exp;  lines: +2 -2
137 2
        // NOTE: Time here appears to be stored as UTC according to http://parand.com/docs/rcs.html
138
        $metadata_array = explode(';', $entry[1]);
139
        foreach ($metadata_array as $metadata) {
140
            $metadata = trim($metadata);
141
            if (substr($metadata, 0, 5) == 'date:') {
142
                $history['date'] = strtotime(substr($metadata, 6));
143
            } elseif (substr($metadata, 0, 6) == 'lines:') {
144
                $history['lines'] = substr($metadata, 7);
145
            }
146 7
        }
147
148 7
        // Entry message is in format
149
        // user:27b841929d1e04118d53dd0a45e4b93a|84.34.133.194|message
150
        $message_array = explode('|', $entry[2]);
151
        if (count($message_array) == 1) {
152 7
            $history['message'] = $message_array[0];
153 7
        } else {
154 7
            if ($message_array[0] != 'Object') {
155
                $history['user'] = $message_array[0];
156
            }
157 7
            $history['ip'] = $message_array[1];
158
            $history['message'] = $message_array[2];
159
        }
160
        return $history;
161
    }
162 7
163
    /*
164
     * the functions below are mostly rcs functions moved into the class. Someday I'll get rid of the
165
     * old files...
166 7
     */
167
    /**
168
     * Get a list of the object's history
169
     *
170
     * @param string $what objectid (usually the guid)
171
     */
172
    private function rcs_gethistory(string $what) : midcom_services_rcs_history
173
    {
174
        $history = $this->rcs_exec('rlog', $what . ',v');
175
        $revisions = [];
176 7
        $lines = explode("\n", $history);
177
        $total = count($lines);
178
179
        for ($i = 0; $i < $total; $i++) {
180
            if (substr($lines[$i], 0, 9) == "revision ") {
181 7
                $history_entry = [$lines[$i], $lines[$i + 1], $lines[$i + 2]];
182 7
                $history = $this->rcs_parse_history_entry($history_entry);
183 7
184 7
                $revisions[$history['revision']] = $history;
185 7
186 7
                $i += 3;
187 6
188
                while (   $i < $total
189
                       && substr($lines[$i], 0, 4) != '----'
190
                       && substr($lines[$i], 0, 5) != '=====') {
191
                    $i++;
192
                }
193 7
            }
194 7
        }
195
        return new midcom_services_rcs_history($revisions);
196
    }
197 7
198 7
    /**
199
     * execute a command
200 7
     *
201 7
     * @param string $command The command to execute
202
     * @param string $filename The file to operate on
203 7
     * @return string command result.
204
     */
205
    private function rcs_exec(string $command, string $filename) : string
206
    {
207
        if (!is_readable($filename)) {
208
            debug_add('file ' . $filename . ' is not readable, returning empty result', MIDCOM_LOG_INFO);
209
            return '';
210
        }
211
        $fh = popen($this->_config->get_bin_prefix() . $command . ' "' . $filename . '" 2>&1', "r");
212
        $ret = "";
213
        while ($reta = fgets($fh, 1024)) {
0 ignored issues
show
Bug introduced by
It seems like $fh can also be of type false; however, parameter $handle of fgets() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

213
        while ($reta = fgets(/** @scrutinizer ignore-type */ $fh, 1024)) {
Loading history...
214
            $ret .= $reta;
215 7
        }
216
        pclose($fh);
0 ignored issues
show
Bug introduced by
It seems like $fh can also be of type false; however, parameter $handle of pclose() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

216
        pclose(/** @scrutinizer ignore-type */ $fh);
Loading history...
217 7
218 7
        return $ret;
219 7
    }
220 7
221
    /**
222 7
     * Update object to RCS
223 7
     * Should be called just before $object->update()
224 7
     *
225 7
     * @param object $object to be updated.
226
     * @param string $message
227 7
     * @return int :
228
     *      0 on success
229 7
     *      3 on missing object->guid
230
     *      nonzero on error in one of the commands.
231 7
     */
232 7
    private function rcs_update(midcom_core_dbaobject $object, $message)
233 7
    {
234
        if (empty($object->guid)) {
235
            debug_add("Missing GUID, returning error");
236
            return 3;
237
        }
238 7
239
        $filename = $this->_generate_rcs_filename($object->guid);
240
        $rcsfilename = "{$filename},v";
241
        $message = escapeshellarg($message);
242
243
        if (file_exists($rcsfilename)) {
244
            $this->exec('co -q -f -l ' . escapeshellarg($filename));
245
            $command = 'ci -q -m' . $message . " {$filename}";
246
        } else {
247
            $command = 'ci -q -i -t-' . $message . ' -m' . $message . " {$filename}";
248 7
        }
249
        if (is_writable($this->_config->get_rcs_root())) {
250 7
            file_put_contents($filename, $this->rcs_object2data($object));
251 3
        }
252 3
        $stat = $this->exec($command);
253
254 7
        if (file_exists($rcsfilename)) {
255 7
            chmod($rcsfilename, 0770);
256 7
        }
257 7
258
        return $stat;
259 7
    }
260
261 7
    /**
262
     * Make xml out of an object.
263
     *
264
     * @param midcom_core_dbaobject $object
265
     */
266
    private function rcs_object2data(midcom_core_dbaobject $object) : string
267
    {
268
        $mapper = new midcom_helper_exporter_xml();
269
        return $mapper->object2data($object);
270
    }
271
272
    private function exec(string $command, $use_rcs_bindir = true)
273
    {
274
        $status = null;
275 74
        $output = null;
276
277 74
        // Always append stderr redirect
278
        $command .= ' 2>&1';
279
280
        if ($use_rcs_bindir) {
281
            $command = $this->_config->get_bin_prefix() . $command;
282 74
        }
283 74
284 74
        debug_add("Executing '{$command}'");
285
286 74
        try {
287 37
            @exec($command, $output, $status);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for exec(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

287
            /** @scrutinizer ignore-unhandled */ @exec($command, $output, $status);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
288 37
        } catch (Exception $e) {
289
            debug_add($e->getMessage());
290 64
        }
291
292 74
        if ($status !== 0) {
293 74
            debug_add("Command '{$command}' returned with status {$status}, see debug log for output", MIDCOM_LOG_WARN);
294
            debug_print_r('Got output: ', $output);
295 74
        }
296
        return $status;
297 74
    }
298 74
299
    /**
300
     * Get a html diff between two versions.
301 74
     *
302
     * @param string $oldest_revision id of the oldest revision
303
     * @param string $latest_revision id of the latest revision
304
     */
305
    public function get_diff($oldest_revision, $latest_revision) : array
306
    {
307
        $oldest = $this->get_revision($oldest_revision);
308
        $newest = $this->get_revision($latest_revision);
309 74
310
        $return = [];
311 74
        $oldest = array_intersect_key($oldest, $newest);
312 74
313
        $repl = [
314
            '<del>' => "<span class=\"deleted\">",
315 78
            '</del>' => '</span>',
316
            '<ins>' => "<span class=\"inserted\">",
317 78
            '</ins>' => '</span>'
318 78
        ];
319
        foreach ($oldest as $attribute => $oldest_value) {
320
            if (is_array($oldest_value)) {
321 78
                continue;
322
            }
323 78
324 78
            $return[$attribute] = [
325
                'old' => $oldest_value,
326
                'new' => $newest[$attribute]
327 78
            ];
328
329
            if ($oldest_value != $newest[$attribute]) {
330 78
                $lines1 = explode("\n", $oldest_value);
331
                $lines2 = explode("\n", $newest[$attribute]);
332
333
                $renderer = new midcom_services_rcs_renderer_html_sidebyside(['old' => $oldest_revision, 'new' => $latest_revision]);
334
335 78
                if ($lines1 != $lines2) {
336
                    $diff = new Diff($lines1, $lines2);
337
                    // Run the diff
338
                    $return[$attribute]['diff'] = $diff->render($renderer);
339 78
                    // Modify the output for nicer rendering
340
                    $return[$attribute]['diff'] = strtr($return[$attribute]['diff'], $repl);
341
                }
342
            }
343
        }
344
345
        return $return;
346
    }
347
348 2
    /**
349
     * Restore an object to a certain revision.
350 2
     *
351 2
     * @param string $revision of revision to restore object to.
352
     * @return boolean true on success.
353 2
     */
354 2
    public function restore_to_revision($revision) : bool
355
    {
356
        $new = $this->get_revision($revision);
357 2
358
        try {
359
            $object = midcom::get()->dbfactory->get_object_by_guid($this->_guid);
360
        } catch (midcom_error $e) {
361
            debug_add("{$this->_guid} could not be resolved to object", MIDCOM_LOG_ERROR);
362 2
            return false;
363 2
        }
364 2
        $mapper = new midcom_helper_exporter_xml();
365
        $object = $mapper->data2object($new, $object);
366
367 2
        $object->set_rcs_message("Reverted to revision {$revision}");
368 2
369 2
        return $object->update();
370
    }
371
}
372