Completed
Branch master (e8947e)
by Andreas
15:09
created

midcom_services_rcs_backend_rcs::rcs_create()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 12
nc 3
nop 2
dl 0
loc 23
rs 9.0856
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
1 ignored issue
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
13
{
14
    /**
15
     * GUID of the current object
16
     */
17
    private $_guid;
18
19
    /**
20
     * Cached revision history for the object
21
     */
22
    private $_history;
23
24
    private $_config;
25
26
    public function __construct($object, $config)
27
    {
28
        $this->_config = $config;
29
        $this->_guid = $object->guid;
30
    }
31
32
    private function _generate_rcs_filename($guid)
33
    {
34
        // Keep files organized to subfolders to keep filesystem sane
35
        $dirpath = $this->_config->get_rcs_root() . "/{$guid[0]}/{$guid[1]}";
36
        if (!file_exists($dirpath))
37
        {
38
            debug_add("Directory {$dirpath} does not exist, attempting to create", MIDCOM_LOG_INFO);
39
            mkdir($dirpath, 0777, true);
40
        }
41
        return "{$dirpath}/{$guid}";
42
    }
43
44
    /**
45
     * Save a new revision
46
     *
47
     * @param object object to be saved
48
     * @return boolean true on success.
49
     */
50
    public function update($object, $updatemessage = null)
0 ignored issues
show
Coding Style introduced by
update uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
51
    {
52
        // Store user identifier and IP address to the update string
53
        if (midcom::get()->auth->user)
54
        {
55
            $update_string = midcom::get()->auth->user->id . "|{$_SERVER['REMOTE_ADDR']}";
56
        }
57
        else
58
        {
59
            $update_string = "NOBODY|{$_SERVER['REMOTE_ADDR']}";
60
        }
61
62
        // Generate update message if needed
63
        if (!$updatemessage)
0 ignored issues
show
Bug Best Practice introduced by
The expression $updatemessage of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
64
        {
65
            if (midcom::get()->auth->user !== null)
66
            {
67
                $updatemessage = sprintf("Updated on %s by %s", strftime("%x %X"), midcom::get()->auth->user->name);
68
            }
69
            else
70
            {
71
                $updatemessage = sprintf("Updated on %s.", strftime("%x %X"));
72
            }
73
        }
74
        $update_string .= "|{$updatemessage}";
75
76
        $result = $this->rcs_update($object, $update_string);
77
78
        // The methods return basically what the RCS unix level command returns, so nonzero value is error and zero is ok...
79
        return ($result == 0);
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $result of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
80
    }
81
82
    /**
83
     * Update object to RCS
84
     * Should be called just before $object->update(), if the type parameter is omitted
85
     * the function will use GUID to determine the type, this makes an extra DB query.
86
     *
87
     * @param string root of rcs directory.
88
     * @param object object to be updated.
89
     * @return int :
90
     *      0 on success
91
     *      3 on missing object->guid
92
     *      nonzero on error in one of the commands.
93
     */
94
    public function rcs_update ($object, $message)
95
    {
96
        if (empty($object->guid))
97
        {
98
            debug_add("Missing GUID, returning error");
99
            return 3;
100
        }
101
102
        $filename = $this->_generate_rcs_filename($object->guid);
103
        $rcsfilename =  "{$filename},v";
104
105
        if (!file_exists($rcsfilename))
106
        {
107
            $message = str_replace('|Updated ', '|Created ', $message);
108
            // The methods return basically what the RCS unix level command returns, so nonzero value is error and zero is ok...
109
            return $this->rcs_create($object, $message);
110
        }
111
112
        $command = 'co -q -f -l ' . escapeshellarg($filename);
113
        $this->exec($command);
114
115
        $data = $this->rcs_object2data($object);
116
117
        $this->rcs_writefile($object->guid, $data);
118
        $command = 'ci -q -m' . escapeshellarg($message) . " {$filename}";
119
        $status = $this->exec($command);
120
121
        chmod ($rcsfilename, 0770);
122
123
        return $status;
124
    }
125
126
   /**
127
    * Get the object of a revision
128
    *
129
    * @param string revision identifier of revision wanted
130
    * @return array array representation of the object
131
    */
132
    public function get_revision($revision)
133
    {
134
        if (empty($this->_guid))
135
        {
136
            return array();
137
        }
138
        $filepath = $this->_generate_rcs_filename($this->_guid);
139
140
        // , must become . to work. Therefore this:
141
        str_replace(',', '.', $revision );
142
        // this seems to cause problems:
143
        //settype ($revision, "float");
144
145
        $command = 'co -q -f -r' . escapeshellarg(trim($revision)) .  " {$filepath} 2>/dev/null";
146
        $this->exec($command);
147
148
        $data = $this->rcs_readfile($this->_guid);
149
150
        $mapper = new midcom_helper_exporter_xml();
151
        $revision = $mapper->data2array($data);
152
153
        $command = "rm -f {$filepath}";
154
        $this->exec($command);
155
156
        return $revision;
157
    }
158
159
    /**
160
     * Check if a revision exists
161
     *
162
     * @param string  version
163
     * @return booleann true if exists
164
     */
165
    public function version_exists($version)
166
    {
167
        $history = $this->list_history();
168
        return array_key_exists($version, $history);
169
    }
170
171
    /**
172
     * Get the previous versionID
173
     *
174
     * @param string version
175
     * @return string versionid before this one or empty string.
176
     */
177 View Code Duplication
    public function get_prev_version($version)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
178
    {
179
        $versions = $this->list_history_numeric();
180
181
        if (   !in_array($version, $versions)
182
            || $version === end($versions))
183
        {
184
            return '';
185
        }
186
187
        $mode = end($versions);
188
189
        while( $mode
190
            && $mode !== $version)
191
        {
192
            $mode = prev($versions);
193
194
            if ($mode === $version)
195
            {
196
                return next($versions);
197
            }
198
        }
199
200
        return '';
201
    }
202
203
    /**
204
     * Mirror method for get_prev_version()
205
     *
206
     * @param string $version
207
     * @return mixed
208
     */
209
    public function get_previous_version($version)
210
    {
211
        return $this->get_prev_version($version);
212
    }
213
214
    /**
215
     * Get the next versionID
216
     *
217
     * @param string version
218
     * @return string versionid before this one or empty string.
219
     */
220 View Code Duplication
    public function get_next_version($version)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
221
    {
222
        $versions = $this->list_history_numeric();
223
224
        if (   !in_array($version, $versions)
225
            || $version === current($versions))
226
        {
227
            return '';
228
        }
229
230
        $mode = current($versions);
231
232
        while (   $mode
233
               && $mode !== $version)
234
        {
235
            $mode = next($versions);
236
237
            if ($mode === $version)
238
            {
239
                return prev($versions);
240
            }
241
        }
242
243
        return '';
244
    }
245
246
    /**
247
     * Return a list of the revisions as a key => value pair where
248
     * the key is the index of the revision and the value is the revision id.
249
     * Order: revision 0 is the newest.
250
     *
251
     * @return array
252
     */
253
    public function list_history_numeric()
254
    {
255
        $revs = $this->list_history();
256
        return array_keys($revs);
257
    }
258
259
    /**
260
     * Lists the number of changes that has been done to the object
261
     *
262
     * @return array list of changeids
263
     */
264
    public function list_history()
265
    {
266
        if (empty($this->_guid))
267
        {
268
            return array();
269
        }
270
271
        if (is_null($this->_history))
272
        {
273
            $filepath = $this->_generate_rcs_filename($this->_guid);
274
            $this->_history = $this->rcs_gethistory($filepath);
275
        }
276
277
        return $this->_history;
278
    }
279
280
    /* it is debatable to move this into the object when it resides nicely in a libary... */
281
282
    private function rcs_parse_history_entry($entry)
283
    {
284
        // Create the empty history array
285
        $history = array
286
        (
287
            'revision' => null,
288
            'date'     => null,
289
            'lines'    => null,
290
            'user'     => null,
291
            'ip'       => null,
292
            'message'  => null,
293
        );
294
295
        // Revision number is in format
296
        // revision 1.11
297
        $history['revision'] = substr($entry[0], 9);
298
299
        // Entry metadata is in format
300
        // date: 2006/01/10 09:40:49;  author: www-data;  state: Exp;  lines: +2 -2
301
        // NOTE: Time here appears to be stored as UTC according to http://parand.com/docs/rcs.html
302
        $metadata_array = explode(';', $entry[1]);
303
        foreach ($metadata_array as $metadata)
304
        {
305
            $metadata = trim($metadata);
306
            if (substr($metadata, 0, 5) == 'date:')
307
            {
308
                $history['date'] = strtotime(substr($metadata, 6));
309
            }
310
            elseif (substr($metadata, 0, 6) == 'lines:')
311
            {
312
                $history['lines'] = substr($metadata, 7);
313
            }
314
        }
315
316
        // Entry message is in format
317
        // user:27b841929d1e04118d53dd0a45e4b93a|84.34.133.194|Updated on Tue 10.Jan 2006 by admin kw
318
        $message_array = explode('|', $entry[2]);
319
        if (count($message_array) == 1)
320
        {
321
            $history['message'] = $message_array[0];
322
        }
323
        else
324
        {
325
            if ($message_array[0] != 'Object')
326
            {
327
                $history['user'] = $message_array[0];
328
            }
329
            $history['ip']   = $message_array[1];
330
            $history['message'] = $message_array[2];
331
        }
332
        return $history;
333
    }
334
335
    /*
336
     * the functions below are mostly rcs functions moved into the class. Someday I'll get rid of the
337
     * old files...
338
     */
339
    /**
340
     * Get a list of the object's history
341
     *
342
     * @param string objectid (usually the guid)
343
     * @return array list of revisions and revision comment.
344
     */
345
    private function rcs_gethistory($what)
346
    {
347
        $history = $this->rcs_exec('rlog', $what . ',v');
348
        $revisions = array();
349
        $lines = explode("\n", $history);
350
351
        for ($i = 0; $i < count($lines); $i++)
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
352
        {
353
            if (substr($lines[$i], 0, 9) == "revision ")
354
            {
355
                $history_entry[0] = $lines[$i];
0 ignored issues
show
Coding Style Comprehensibility introduced by
$history_entry was never initialized. Although not strictly required by PHP, it is generally a good practice to add $history_entry = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
356
                $history_entry[1] = $lines[$i+1];
0 ignored issues
show
Bug introduced by
The variable $history_entry does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
357
                $history_entry[2] = $lines[$i+2];
358
                $history = $this->rcs_parse_history_entry($history_entry);
359
360
                $revisions[$history['revision']] = $history;
361
362
                $i += 3;
363
364
                while (   $i < count($lines)
365
                       && substr($lines[$i], 0, 4) != '----'
366
                       && substr($lines[$i], 0, 5) != '=====')
367
                {
368
                     $i++;
369
                }
370
            }
371
        }
372
        return $revisions;
373
    }
374
375
    /**
376
     * execute a command
377
     *
378
     * @param string $command The command to execute
379
     * @param string $filename The file to operate on
380
     * @return string command result.
381
     */
382
    private function rcs_exec($command, $filename)
383
    {
384
        if (!is_readable($filename))
385
        {
386
            debug_add('file ' . $filename . ' is not readable, returning empty result', MIDCOM_LOG_INFO);
387
            return '';
388
        }
389
        $fh = popen($command . ' "' . $filename . '" 2>&1', "r");
390
        $ret = "";
391
        while ($reta = fgets($fh, 1024))
392
        {
393
            $ret .= $reta;
394
        }
395
        pclose($fh);
396
397
        return $ret;
398
    }
399
400
    /**
401
     * Writes $data to file $guid, does not return anything.
402
     */
403
    private function rcs_writefile ($guid, $data)
404
    {
405
        if (   !is_writable($this->_config->get_rcs_root())
406
            || empty($guid))
407
        {
408
            return false;
409
        }
410
        $filename = $this->_generate_rcs_filename($guid);
411
        file_put_contents($filename, $data);
412
    }
413
414
    /**
415
     * Reads data from file $guid and returns it.
416
     *
417
     * @param string guid
418
     * @return string xml representation of guid
419
     */
420
    private function rcs_readfile ($guid)
421
    {
422
        if (!empty($guid))
423
        {
424
            $filename = $this->_generate_rcs_filename($guid);
425
            if (file_exists($filename))
426
            {
427
                return file_get_contents($filename);
428
            }
429
        }
430
        return '';
431
    }
432
433
    /**
434
     * Make xml out of an object.
435
     *
436
     * @param midcom_core_dbaobject $object
437
     * @return xmldata
438
     */
439
    private function rcs_object2data(midcom_core_dbaobject $object)
440
    {
441
        $mapper = new midcom_helper_exporter_xml();
442
        if ($result = $mapper->object2data($object))
443
        {
444
            return $result;
445
        }
446
        debug_add("Objectmapper returned false.");
447
        return false;
448
    }
449
450
    /**
451
     * Add object to RCS
452
     *
453
     * @param object $object object to be saved
454
     * @param string $description changelog comment.-
455
     * @return int :
456
     *      0 on success
457
     *      3 on missing object->guid
458
     *      nonzero on error in one of the commands.
459
     */
460
    private function rcs_create(midcom_core_dbaobject $object, $description)
461
    {
462
        $data = $this->rcs_object2data($object);
463
464
        if (empty($object->guid))
465
        {
466
            return 3;
467
        }
468
        $this->rcs_writefile($object->guid, $data);
469
        $filepath = $this->_generate_rcs_filename($object->guid);
470
471
        $command = 'ci -q -i -t-' . escapeshellarg($description) . ' -m' . escapeshellarg($description) . " {$filepath}";
472
473
        $status = $this->exec($command);
474
475
        $filename = $filepath . ",v";
476
477
        if (file_exists($filename))
478
        {
479
            chmod ($filename, 0770);
480
        }
481
        return $status;
482
    }
483
484
    private function exec($command)
485
    {
486
        $status = null;
487
        $output = null;
488
489
        // Always append stderr redirect
490
        $command .= ' 2>&1';
491
492
        debug_add("Executing '{$command}'");
493
494
        try
495
        {
496
            @exec($command, $output, $status);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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...
497
        }
498
        catch (Exception $e)
499
        {
500
            debug_add($e->getMessage());
501
        }
502
503
        if ($status !== 0)
504
        {
505
            debug_add("Command '{$command}' returned with status {$status}, see debug log for output", MIDCOM_LOG_WARN);
506
            debug_print_r('Got output: ', $output);
507
        }
508
        return $status;
509
    }
510
511
    /**
512
     * Get a html diff between two versions.
513
     *
514
     * @param string latest_revision id of the latest revision
515
     * @param string oldest_revision id of the oldest revision
516
     * @return array array with the original value, the new value and a diff -u
517
     */
518
    public function get_diff($oldest_revision, $latest_revision, $renderer_style = 'inline')
519
    {
520
        $oldest = $this->get_revision($oldest_revision);
521
        $newest = $this->get_revision($latest_revision);
522
523
        $return = array();
524
        $oldest = array_intersect_key($oldest, $newest);
525
526
        $repl = array(
527
            '<del>' => "<span class=\"deleted\">",
528
            '</del>' => '</span>',
529
            '<ins>' => "<span class=\"inserted\">",
530
            '</ins>' => '</span>'
531
        );
532
        foreach ($oldest as $attribute => $oldest_value)
533
        {
534
            if (is_array($oldest_value))
535
            {
536
                continue;
537
            }
538
539
            $return[$attribute] = array
540
            (
541
                'old' => $oldest_value,
542
                'new' => $newest[$attribute]
543
            );
544
545
            if ($oldest_value != $newest[$attribute])
546
            {
547
                $lines1 = explode ("\n", $oldest_value);
548
                $lines2 = explode ("\n", $newest[$attribute]);
549
550
                $options = array();
551
                $diff = new Diff($lines1, $lines2, $options);
552
                if ($renderer_style == 'unified')
553
                {
554
                    $renderer = new Diff_Renderer_Text_Unified;
555
                }
556
                else
557
                {
558
                    $renderer = new midcom_services_rcs_renderer_html_sidebyside(array('old' => $oldest_revision, 'new' => $latest_revision));
559
                }
560
561
                if ($lines1 != $lines2)
562
                {
563
                    // Run the diff
564
                    $return[$attribute]['diff'] = $diff->render($renderer);
565
                    if ($renderer_style == 'unified')
566
                    {
567
                        $return[$attribute]['diff'] = htmlspecialchars($return[$attribute]['diff']);
568
                    }
569
570
                    if ($renderer_style == 'inline')
571
                    {
572
                        // Modify the output for nicer rendering
573
                        $return[$attribute]['diff'] = strtr($return[$attribute]['diff'], $repl);
574
                    }
575
                }
576
            }
577
        }
578
579
        return $return;
580
    }
581
582
    /**
583
     * Get the comment of one revision.
584
     *
585
     * @param string revison id
586
     * @return string comment
587
     */
588
    public function get_comment($revision)
589
    {
590
        $this->list_history();
591
        return $this->_history[$revision];
592
    }
593
594
    /**
595
     * Restore an object to a certain revision.
596
     *
597
     * @param string id of revision to restore object to.
598
     * @return boolean true on success.
599
     */
600
    public function restore_to_revision($revision)
601
    {
602
        $new = $this->get_revision($revision);
603
604
        try
605
        {
606
            $object = midcom::get()->dbfactory->get_object_by_guid($this->_guid);
607
        }
608
        catch (midcom_error $e)
609
        {
610
            debug_add("{$this->_guid} could not be resolved to object", MIDCOM_LOG_ERROR);
611
            return false;
612
        }
613
        $mapper = new midcom_helper_exporter_xml();
614
        $object = $mapper->data2object($new, $object);
615
616
        $object->set_rcs_message("Reverted to revision {$revision}");
617
618
        return $object->update();
619
    }
620
}
621