Passed
Push — master ( 541fc6...192d70 )
by Nicolaas
02:09
created

SearchAndReplaceAPI::getFullComment()   B

Complexity

Conditions 6
Paths 17

Size

Total Lines 28
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 22
c 1
b 0
f 0
dl 0
loc 28
rs 8.9457
cc 6
nc 17
nop 0
1
<?php
2
3
namespace Sunnysideup\UpgradeToSilverstripe4\Api;
4
5
/**
6
 * @BasedOn  :  MA Razzaque Rupom <[email protected]>, <[email protected]>
7
 *             Moderator, phpResource Group(http://groups.yahoo.com/group/phpresource/)
8
 *             URL: http://rupom.wordpress.com
9
 */
10
11
class SearchAndReplaceAPI
12
{
13
    //generic search settings
14
15
    private $debug = false;
16
17
    private $basePath = '';
18
19
    private $isReplacingEnabled = false;
20
21
    //specific search settings
22
23
    private $searchKey = '';
24
25
    private $replacementKey = '';
26
27
    private $comment = '';
28
29
    private $startMarker = '### @@@@ START REPLACEMENT @@@@ ###';
30
31
    private $endMarker = '### @@@@ STOP REPLACEMENT @@@@ ###';
32
33
    private $replacementHeader = '';
34
35
    private $replacementType = '';
36
37
    private $caseSensitive = true;
38
39
    private $ignoreFrom = [
40
        '//',
41
        '#',
42
        '/**',
43
    ];
44
45
    private $fileReplacementMaxCount = 0;
46
47
    private $ignoreUntil = [
48
        '//',
49
        '#',
50
        '*/',
51
    ];
52
53
    private $ignoreFileIfFound = [];
54
55
    private $fileNameMustContain = [];
56
57
    // special stuff
58
59
    private $magicReplacers = [
60
        '[SEARCH_REPLACE_CLASS_NAME_GOES_HERE]' => 'classNameOfFile',
61
    ];
62
63
    // files
64
65
    private $fileFinder = null;
66
67
    //stats and reporting
68
69
    private $logString = ''; //details of one search
70
71
    private $errorText = ''; //details of one search
72
73
    private $totalFound = 0; //total matches in one search
74
75
    private $output = ''; //buffer of output, until it is retrieved
76
77
    // static counts
78
79
    private $searchKeyTotals = [];
80
81
    private $folderTotals = [];
82
83
    private $totalTotal = 0;
84
85
    /**
86
     * magic replacement functions
87
     */
88
    private static $class_name_cache = [];
89
90
    private static $finder = null;
91
92
    public function __construct($basePath = '')
93
    {
94
        $this->basePath = $basePath;
95
        $this->fileFinder = new FindFiles();
96
    }
97
98
    //================================================
99
    // Setters Before Run
100
    //================================================
101
102
    /**
103
     *   @return $this
104
     */
105
    public function setDebug($b)
106
    {
107
        $this->debug = $b;
108
109
        return $this;
110
    }
111
112
    /**
113
     *   @return $this
114
     */
115
    public function setIsReplacingEnabled($b)
116
    {
117
        $this->isReplacingEnabled = $b;
118
119
        return $this;
120
    }
121
122
    /**
123
     *   Sets folders to ignore
124
     *   @param array $ignoreFolderArray
125
     *   @return self
126
     */
127
    public function setIgnoreFolderArray($ignoreFolderArray = [])
128
    {
129
        $this->fileFinder->setIgnoreFolderArray($ignoreFolderArray);
130
131
        return $this;
132
    }
133
134
    /**
135
     *   Sets folders to ignore
136
     *   @param array $ignoreFolderArray
137
     *   @return self
138
     */
139
    public function addToIgnoreFolderArray($ignoreFolderArray = [])
140
    {
141
        $this->fileFinder->addToIgnoreFolderArray($ignoreFolderArray);
142
143
        return $this;
144
    }
145
146
    /**
147
     * remove ignore folders
148
     */
149
    public function resetIgnoreFolderArray()
150
    {
151
        $this->fileFinder->resetIgnoreFolderArray();
152
153
        return $this;
154
    }
155
156
    public function setBasePath($pathLocation)
157
    {
158
        $this->basePath = $pathLocation;
159
        $this->fileFinder->setSearchPath($pathLocation);
160
161
        return $this;
162
    }
163
164
    public function setSearchPath($pathLocation)
165
    {
166
        $this->fileFinder->setSearchPath($pathLocation);
167
168
        return $this;
169
    }
170
171
    /**
172
     *   Sets extensions to look
173
     *   @param array $extensions
174
     */
175
    public function setExtensions($extensions = [])
176
    {
177
        $this->fileFinder->setExtensions($extensions);
178
179
        return $this;
180
    }
181
182
    /**
183
     *   Sets extensions to look
184
     *   @param bool $boolean
185
     */
186
    public function setFindAllExts($boolean = true)
187
    {
188
        $this->fileFinder->setFindAllExts($boolean);
189
190
        return $this;
191
    }
192
193
    public function setStartMarker($s)
194
    {
195
        $this->startMarker = $s;
196
197
        return $this;
198
    }
199
200
    public function setEndMarker($s)
201
    {
202
        $this->endMarker = $s;
203
204
        return $this;
205
    }
206
207
    public function setReplacementHeader($s)
208
    {
209
        $this->replacementHeader = $s;
210
211
        return $this;
212
    }
213
214
    public function setIgnoreFileIfFound($a)
215
    {
216
        if (is_string($a)) {
217
            $a = [$a];
218
        }
219
        $this->ignoreFileIfFound = $a;
220
221
        return $this;
222
    }
223
224
    public function setFileNameMustContain($a)
225
    {
226
        if (is_string($a)) {
227
            $a = [$a];
228
        }
229
        $this->fileNameMustContain = $a;
230
231
        return $this;
232
    }
233
234
    public function setFileReplacementMaxCount($i)
235
    {
236
        $this->fileReplacementMaxCount = $i;
237
238
        return $this;
239
    }
240
241
    //================================================
242
    // Setters Before Every Search
243
    //================================================
244
245
    /**
246
     * Sets search key and case sensitivity
247
     * @param string $searchKey,
248
     * @param bool $caseSensitive
249
     */
250
    public function setSearchKey($searchKey, $caseSensitive = false, $replacementType = 'noType')
251
    {
252
        $this->searchKey = $searchKey;
253
        $this->caseSensitive = $caseSensitive;
254
        $this->replacementType = $replacementType;
255
        //reset comment
256
        $this->comment = '';
257
258
        return $this;
259
    }
260
261
    /**
262
     *   Sets key to replace searchKey with
263
     *   @param string $replacementKey
264
     */
265
    public function setReplacementKey($replacementKey)
266
    {
267
        $this->replacementKey = $replacementKey;
268
        $this->setIsReplacingEnabled(true);
269
270
        return $this;
271
    }
272
273
    /**
274
     *   Sets a comment to go with the replacement.
275
     *   @param string $comment
276
     */
277
    public function setComment($comment)
278
    {
279
        $this->comment = $comment;
280
281
        return $this;
282
    }
283
284
    /**
285
     * makes a comment into a PHP proper comment (like this one)
286
     * @return string
287
     */
288
    public function getFullComment()
289
    {
290
        $string = '';
291
        if ($this->comment) {
292
            $string .=
293
            PHP_EOL .
294
                '/**' . PHP_EOL .
295
                '  * ' . $this->startMarker . PHP_EOL;
296
            if ($this->replacementHeader) {
297
                $string .= '  * WHY: ' . $this->replacementHeader . PHP_EOL;
298
            }
299
            $caseSensitiveStatement = ($this->caseSensitive ? '' : ' (ignore case)');
300
            $replacementTypeStatement = ($this->replacementType ? ' (' . $this->replacementType . ')' : '');
301
            $new = '';
302
            if($this->searchKey !== $this->replacementKey) {
303
            } else {
304
                $new = '  * NEW: ' . $this->replacementKey . ' ... ' . $replacementTypeStatement . PHP_EOL ;
305
            }
306
            $string .=
307
                '  * OLD: ' . $this->searchKey . $caseSensitiveStatement  . PHP_EOL .
308
                $new.
309
                '  * EXP: ' . $this->comment . PHP_EOL .
310
                '  * ' . $this->endMarker . PHP_EOL .
311
                '  */' .
312
                PHP_EOL;
313
        }
314
315
        return $string;
316
    }
317
318
    //================================================
319
    // Get FINAL output
320
    //================================================
321
322
    /**
323
     * @return bool
324
     */
325
    public function getDebug()
326
    {
327
        return $this->debug;
328
    }
329
330
    /**
331
     * returns full output
332
     * and clears it.
333
     * @return string
334
     */
335
    public function getOutput()
336
    {
337
        $output = $this->output;
338
        $this->output = '';
339
340
        return $output;
341
    }
342
343
    /**
344
     * returns full log
345
     * and clears it.
346
     * @return string
347
     */
348
    public function getLog()
349
    {
350
        $logString = $this->logString;
351
        $this->logString = '';
352
353
        return $logString;
354
    }
355
356
    /**
357
     * returns the TOTAL TOTAL number of
358
     * found replacements
359
     */
360
    public function getTotalTotalSearches()
361
    {
362
        return $this->totalTotal;
363
    }
364
365
    /**
366
     * should be run at the end of an extension.
367
     */
368
    public function showFormattedSearchTotals($suppressOutput = false)
369
    {
370
        $totalSearches = 0;
371
        foreach ($this->searchKeyTotals as $searchKey => $total) {
372
            $totalSearches += $total;
373
        }
374
        if ($suppressOutput) {
375
            //do nothing
376
        } else {
377
            $flatArray = $this->fileFinder->getFlatFileArray();
378
            if ($flatArray && ! is_array($flatArray)) {
379
                $this->addToOutput("\n" . $flatArray . "\n");
380
            } else {
381
                $this->addToOutput("\n--------------\nFiles Searched\n--------------\n");
382
                foreach ($flatArray as $file) {
383
                    $strippedFile = str_replace($this->basePath, '', $file);
384
                    $this->addToOutput($strippedFile . "\n");
385
                }
386
            }
387
            $folderSimpleTotals = [];
388
            $realBase = (string) realpath($this->basePath);
389
            $this->addToOutput("\n--------------\nSummary: by search key\n--------------\n");
390
            arsort($this->searchKeyTotals);
391
            foreach ($this->searchKeyTotals as $searchKey => $total) {
392
                $this->addToOutput(sprintf("%d:\t %s\n", $total, $searchKey));
393
            }
394
            $this->addToOutput("\n--------------\nSummary: by directory\n--------------\n");
395
            arsort($this->folderTotals);
396
            foreach ($this->folderTotals as $folder => $total) {
397
                $path = str_replace($realBase, '', (string) realpath($folder));
398
                $pathArr = explode('/', $path);
399
                if (isset($pathArr[1])) {
400
                    $folderName = $pathArr[1] . '/';
401
                    if (! isset($folderSimpleTotals[$folderName])) {
402
                        $folderSimpleTotals[$folderName] = 0;
403
                    }
404
                    $folderSimpleTotals[$folderName] += $total;
405
                    $strippedFolder = str_replace($this->basePath, '', $folder);
406
                    $this->addToOutput(sprintf("%d:\t %s\n", $total, $strippedFolder));
407
                }
408
            }
409
            $strippedRealBase = '/';
410
            $this->addToOutput(
411
                sprintf("\n--------------\nSummary: by root directory (%s)\n--------------\n", $strippedRealBase)
412
            );
413
            arsort($folderSimpleTotals);
414
            foreach ($folderSimpleTotals as $folder => $total) {
415
                $strippedFolder = str_replace($this->basePath, '', $folder);
416
                $this->addToOutput(sprintf("%d:\t %s\n", $total, $strippedFolder));
417
            }
418
            $this->addToOutput(sprintf("\n--------------\nTotal replacements: %d\n--------------\n", $totalSearches));
419
        }
420
        //add to total total
421
        $this->totalTotal += $totalSearches;
422
423
        //return total
424
        return $totalSearches;
425
    }
426
427
    //================================================
428
    // Doers
429
    //================================================
430
431
    /**
432
     * Searches all the files and creates the logs
433
     *
434
     * @return self
435
     */
436
    public function startSearchAndReplace()
437
    {
438
        $flatArray = $this->fileFinder->getFlatFileArray();
439
        foreach ($flatArray as $file) {
440
            $this->searchFileData($file);
441
        }
442
        if ($this->totalFound) {
443
            $msg = $this->totalFound . ' matches (' . $this->replacementType . ') for: ' . $this->logString;
444
            $this->addToOutput($msg);
445
        }
446
        if ($this->errorText !== '') {
447
            $this->addToOutput("\t Error-----" . $this->errorText);
448
        }
449
450
        return $this;
451
    }
452
453
    /**
454
     * THE KEY METHOD!
455
     * Searches data, replaces (if enabled) with given key, prepares log
456
     * @param string $file - e.g. /var/www/mysite.co.nz/mysite/code/Page.php
457
     */
458
    private function searchFileData($file)
459
    {
460
        $foundInLineCount = 0;
461
        $myReplacementKey = $this->replacementKey;
462
        $searchKey = preg_quote($this->searchKey, '/');
463
        if ($this->isReplacingEnabled) {
464
            //prerequisites for file and content ...
465
            if ($this->testMustContain($file) === false) {
466
                return;
467
            }
468
            if ($this->testFileNameRequirements($file) === false) {
469
                return;
470
            }
471
472
            $magicalData = [];
473
            $magicalData['classNameOfFile'] = $this->getClassNameOfFile($file);
474
            foreach ($this->magicReplacers as $magicReplacerFind => $magicReplacerReplaceVariable) {
475
                $myReplacementKey = str_replace(
476
                    $magicReplacerFind,
477
                    $magicalData[$magicReplacerReplaceVariable],
478
                    $myReplacementKey
479
                );
480
            }
481
            $oldFileContentArray = (array) file($file) ?? [];
482
            $newFileContentArray = [];
483
            $pattern = "/${searchKey}/U";
484
            if (! $this->caseSensitive) {
485
                $pattern = "/${searchKey}/Ui";
486
            }
487
            $foundCount = 0;
488
            $insidePreviousReplaceComment = false;
489
            $insideIgnoreArea = false;
490
            $completedTask = false;
491
            foreach ($oldFileContentArray as $oldLineContent) {
492
                $newLineContent = (string) $oldLineContent . '';
493
494
                if ($completedTask === false) {
495
                    $testLine = (string) trim((string) $oldLineContent);
496
497
                    //check if it is actually already replaced ...
498
                    if (strpos((string) $oldLineContent, $this->startMarker) !== false) {
499
                        $insidePreviousReplaceComment = true;
500
                    }
501
                    foreach ($this->ignoreFrom as $ignoreStarter) {
502
                        if (strpos((string) $testLine, $ignoreStarter) === 0) {
503
                            $insideIgnoreArea = true;
504
                        }
505
                    }
506
                    if ($insidePreviousReplaceComment || $insideIgnoreArea) {
507
                        //do nothing ...
508
                    } else {
509
                        $foundInLineCount = preg_match_all(
510
                            $pattern,
511
                            (string) $oldLineContent,
512
                            $matches,
513
                            PREG_PATTERN_ORDER
514
                        );
515
                        if ($foundInLineCount) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $foundInLineCount of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. 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 integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
516
                            if ($this->caseSensitive) {
517
                                if (strpos((string) $oldLineContent, (string) $this->searchKey) === false) {
518
                                    user_error('Regex found it, but phrase does not exist: ' . $this->searchKey);
519
                                }
520
                            } else {
521
                                if (stripos((string) $oldLineContent, $this->searchKey) === false) {
522
                                    user_error('Regex found it, but phrase does not exist: ' . $this->searchKey);
523
                                }
524
                            }
525
                            $foundCount += $foundInLineCount;
526
                            if ($this->isReplacingEnabled) {
527
                                $newLineContent = preg_replace($pattern, $myReplacementKey, (string) $oldLineContent);
528
                                if ($fullComment = $this->getFullComment()) {
529
                                    $newFileContentArray[] = $fullComment;
530
                                }
531
                            }
532
                        } else {
533
                            if ($this->caseSensitive) {
534
                                if (strpos((string) $oldLineContent, (string) $this->searchKey) !== false) {
535
                                    user_error('Should have found: ' . $this->searchKey);
536
                                }
537
                            } else {
538
                                if (stripos((string) $oldLineContent, (string) $this->searchKey) !== false) {
539
                                    user_error('Should have found: ' . $this->searchKey);
540
                                }
541
                            }
542
                        }
543
                    }
544
                    if (strpos((string) $oldLineContent, (string) $this->endMarker) !== false) {
545
                        $insidePreviousReplaceComment = false;
546
                    }
547
                    foreach ($this->ignoreUntil as $ignoreEnder) {
548
                        if (strpos((string) $testLine, (string) $ignoreEnder) === 0) {
549
                            $insideIgnoreArea = false;
550
                        }
551
                    }
552
                    if ($this->fileReplacementMaxCount > 0 && $foundCount >= $this->fileReplacementMaxCount) {
553
                        $completedTask = true;
554
                    }
555
                }
556
557
                $newFileContentArray[] = $newLineContent;
558
            }
559
            if ($foundCount) {
560
                $oldFileContent = implode($oldFileContentArray);
561
                $newFileContent = implode($newFileContentArray);
562
                if ($newFileContent !== $oldFileContent) {
563
                    $this->writeToFile($file, $newFileContent);
564
565
                    //stats
566
                    $this->totalFound += $foundInLineCount;
567
                    if (! isset($this->searchKeyTotals[$this->searchKey])) {
568
                        $this->searchKeyTotals[$this->searchKey] = 0;
569
                    }
570
                    $this->searchKeyTotals[$this->searchKey] += $foundCount;
571
572
                    if (! isset($this->folderTotals[dirname($file)])) {
573
                        $this->folderTotals[dirname($file)] = 0;
574
                    }
575
                    $this->folderTotals[dirname($file)] += $foundCount;
576
577
                    //log
578
                    $foundStr = "-- ${foundCount} x";
579
                    if ($this->fileReplacementMaxCount) {
580
                        $foundStr .= ' limited to ' . $this->fileReplacementMaxCount;
581
                    }
582
                    $this->appendToLog($file, $foundStr);
583
                } else {
584
                    $this->appendToLog(
585
                        $file,
586
                        '********** ERROR: NO REPLACEMENT DESPITE MATCHES - searched for: ' .
587
                        $pattern . ' and replaced with ' . $myReplacementKey . " \n"
588
                    );
589
                }
590
            }
591
        } else {
592
            $this->appendToLog($file, '********** ERROR: Replacement Text is not defined');
593
        }
594
    }
595
596
    /**
597
     * Writes new data (after the replacement) to file
598
     * @param string $file,
599
     * @param string $data
600
     */
601
    private function writeToFile($file, $data)
602
    {
603
        if (is_writable($file)) {
604
            $fp = fopen($file, 'w');
605
            if ($fp) {
0 ignored issues
show
introduced by
$fp is of type resource, thus it always evaluated to false.
Loading history...
606
                fwrite($fp, $data);
607
                fclose($fp);
608
            } else {
609
                user_error('Could not open ' . $file);
610
            }
611
        } else {
612
            user_error(
613
                "********** ERROR: Can not replace text. File ${file} is not writable.",
614
                "\nPlease make it writable\n"
0 ignored issues
show
Bug introduced by
' Please make it writable ' of type string is incompatible with the type integer expected by parameter $error_level of user_error(). ( Ignorable by Annotation )

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

614
                /** @scrutinizer ignore-type */ "\nPlease make it writable\n"
Loading history...
615
            );
616
        }
617
    }
618
619
    /**
620
     * Appends log data to previous log data
621
     * @param string $file
622
     * @param string $matchStr
623
     */
624
    private function appendToLog($file, $matchStr)
625
    {
626
        if ($this->logString === '') {
627
            $this->logString = "'" . $this->searchKey . "'\n";
628
        }
629
        $file = basename($file);
630
        $this->logString .= "   ${matchStr} IN ${file}\n";
631
    }
632
633
    /**
634
     * returns full output
635
     * and clears it.
636
     * @return string
637
     */
638
    private function addToOutput($s)
639
    {
640
        $this->output .= $s;
641
    }
642
643
    private function getClassNameOfFile($filePath)
644
    {
645
        if (! self::$finder) {
646
            self::$finder = new FileNameToClass();
647
        }
648
        if (! isset(self::$class_name_cache[$filePath])) {
649
            $class = self::$finder->getClassNameFromFile($filePath);
650
            //see: https://stackoverflow.com/questions/7153000/get-class-name-from-file/44654073
651
            // $file = 'class.php'; # contains class Foo
652
            // $class = shell_exec("php -r \"include('$file'); echo end(get_declared_classes());\"");
653
            //see: https://stackoverflow.com/questions/7153000/get-class-name-from-file/44654073
654
            // $fp = fopen($filePath, 'r');
655
            // $class = $buffer = '';
656
            // $i = 0;
657
            // while (!$class) {
658
            //     if (feof($fp)) {
659
            //         break;
660
            //     }
661
            //
662
            //     $buffer .= fread($fp, 512);
663
            //     @$tokens = token_get_all($buffer);
664
            //
665
            //     if (strpos($buffer, '{') === false) continue;
666
            //
667
            //     for (;$i<count($tokens);$i++) {
668
            //         if ($tokens[$i][0] === T_CLASS) {
669
            //             for ($j=$i+1;$j<count($tokens);$j++) {
670
            //                 if ($tokens[$j] === '{') {
671
            //                     $class = $tokens[$i+2][1];
672
            //                     break 2;
673
            //                 }
674
            //             }
675
            //         }
676
            //     }
677
            // }
678
            self::$class_name_cache[$filePath] = $class;
679
        }
680
        return self::$class_name_cache[$filePath];
681
    }
682
683
    private function testMustContain($fileName)
684
    {
685
        if (is_array($this->ignoreFileIfFound) && count($this->ignoreFileIfFound)) {
686
            foreach ($this->ignoreFileIfFound as $ignoreString) {
687
                if ($this->hasStringPresentInFile($fileName, $ignoreString)) {
688
                    $this->appendToLog($fileName, '********** Ignoring file, as ignore string found: ' . $ignoreString);
689
690
                    return false;
691
                }
692
            }
693
        }
694
        return true;
695
    }
696
697
    private function testFileNameRequirements($fileName)
698
    {
699
        if (is_array($this->fileNameMustContain) && count($this->fileNameMustContain)) {
700
            $passed = false;
701
            $fileBaseName = basename($fileName);
702
            foreach ($this->fileNameMustContain as $fileNameMustContainString) {
703
                if (stripos($fileBaseName, $fileNameMustContainString) !== false) {
704
                    $passed = true;
705
                }
706
            }
707
            if ($passed === false) {
708
                $this->appendToLog(
709
                    $fileName,
710
                    "********** skipping file (${fileBaseName}), as it does not contain: " .
711
                    implode(', ', $this->fileNameMustContain)
712
                );
713
714
                return false;
715
            }
716
        }
717
718
        return true;
719
    }
720
721
    private function hasStringPresentInFile(string $fileName, string $string): bool
722
    {
723
        // get the file contents, assuming the file to be readable (and exist)
724
        $contents = file_get_contents($fileName);
725
        if (strpos((string) $contents, (string) $string) !== false) {
726
            return true;
727
        }
728
        return false;
729
    }
730
}
731