Passed
Push — master ( f01d00...2f7647 )
by Nicolaas
03:27
created

SearchAndReplaceAPI::startSearchAndReplace()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
c 0
b 0
f 0
dl 0
loc 14
rs 10
cc 4
nc 8
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($basePath);
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->setBasePath($pathLocation);
0 ignored issues
show
Bug introduced by
The method setBasePath() does not exist on Sunnysideup\UpgradeToSilverstripe4\Api\FindFiles. ( Ignorable by Annotation )

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

159
        $this->fileFinder->/** @scrutinizer ignore-call */ 
160
                           setBasePath($pathLocation);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
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)
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
            $string .=
300
                '  * OLD: ' . $this->searchKey . ' (' . ($this->caseSensitive ? 'case sensitive' : 'ignore case') . ')' . PHP_EOL .
301
                '  * NEW: ' . $this->replacementKey . ($this->replacementType ? ' (' . $this->replacementType . ')' : '') . PHP_EOL .
302
                '  * EXP: ' . $this->comment . PHP_EOL .
303
                '  * ' . $this->endMarker . PHP_EOL .
304
                '  */' .
305
                PHP_EOL;
306
        }
307
308
        return $string;
309
    }
310
311
    //================================================
312
    // Get FINAL output
313
    //================================================
314
315
    /**
316
     * returns full output
317
     * and clears it.
318
     * @return string
319
     */
320
    public function getOutput()
321
    {
322
        $output = $this->output;
323
        $this->output = '';
324
325
        return $output;
326
    }
327
328
    /**
329
     * returns full log
330
     * and clears it.
331
     * @return string
332
     */
333
    public function getLog()
334
    {
335
        $logString = $this->logString;
336
        $this->logString = '';
337
338
        return $logString;
339
    }
340
341
    /**
342
     * returns the TOTAL TOTAL number of
343
     * found replacements
344
     */
345
    public function getTotalTotalSearches()
346
    {
347
        return $this->totalTotal;
348
    }
349
350
    /**
351
     * should be run at the end of an extension.
352
     */
353
    public function showFormattedSearchTotals($suppressOutput = false)
354
    {
355
        $totalSearches = 0;
356
        foreach ($this->searchKeyTotals as $searchKey => $total) {
357
            $totalSearches += $total;
358
        }
359
        if ($suppressOutput) {
360
            //do nothing
361
        } else {
362
            $flatArray = $this->fileFinder->getFlatFileArray();
363
            if ($flatArray && ! is_array($flatArray)) {
364
                $this->addToOutput("\n" . $flatArray . "\n");
365
            } else {
366
                $this->addToOutput("\n------------------------------------\nFiles Searched\n------------------------------------\n");
367
                foreach ($flatArray as $file) {
368
                    $strippedFile = str_replace($this->basePath, '', $file);
369
                    $this->addToOutput($strippedFile . "\n");
370
                }
371
            }
372
            $folderSimpleTotals = [];
373
            $realBase = realpath($this->basePath);
374
            $this->addToOutput("\n------------------------------------\nSummary: by search key\n------------------------------------\n");
375
            arsort($this->searchKeyTotals);
376
            foreach ($this->searchKeyTotals as $searchKey => $total) {
377
                $this->addToOutput(sprintf("%d:\t %s\n", $total, $searchKey));
378
            }
379
            $this->addToOutput("\n------------------------------------\nSummary: by directory\n------------------------------------\n");
380
            arsort($this->folderTotals);
381
            foreach ($this->folderTotals as $folder => $total) {
382
                $path = str_replace($realBase, '', realpath($folder));
383
                $pathArr = explode('/', $path);
384
                if (isset($pathArr[1])) {
385
                    $folderName = $pathArr[1] . '/';
386
                    if (! isset($folderSimpleTotals[$folderName])) {
387
                        $folderSimpleTotals[$folderName] = 0;
388
                    }
389
                    $folderSimpleTotals[$folderName] += $total;
390
                    $strippedFolder = str_replace($this->basePath, '', $folder);
391
                    $this->addToOutput(sprintf("%d:\t %s\n", $total, $strippedFolder));
392
                }
393
            }
394
            $strippedRealBase = '/';
395
            $this->addToOutput(sprintf("\n------------------------------------\nSummary: by root directory (%s)\n------------------------------------\n", $strippedRealBase));
396
            arsort($folderSimpleTotals);
397
            foreach ($folderSimpleTotals as $folder => $total) {
398
                $strippedFolder = str_replace($this->basePath, '', $folder);
399
                $this->addToOutput(sprintf("%d:\t %s\n", $total, $strippedFolder));
400
            }
401
            $this->addToOutput(sprintf("\n------------------------------------\nTotal replacements: %d\n------------------------------------\n", $totalSearches));
402
        }
403
        //add to total total
404
        $this->totalTotal += $totalSearches;
405
406
        //return total
407
        return $totalSearches;
408
    }
409
410
    //================================================
411
    // Doers
412
    //================================================
413
414
    /**
415
     * Searches all the files and creates the logs
416
     * @param to $path search
0 ignored issues
show
Bug introduced by
The type Sunnysideup\UpgradeToSilverstripe4\Api\to was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
417
     *
418
     * @return self
419
     */
420
    public function startSearchAndReplace()
421
    {
422
        $flatArray = $this->fileFinder->getFlatFileArray();
423
        foreach ($flatArray as $file) {
424
            $this->searchFileData($file);
425
        }
426
        if ($this->totalFound) {
427
            $this->addToOutput('' . $this->totalFound . ' matches (' . $this->replacementType . ') for: ' . $this->logString);
428
        }
429
        if ($this->errorText !== '') {
430
            $this->addToOutput("\t Error-----" . $this->errorText);
431
        }
432
433
        return $this;
434
    }
435
436
    /**
437
     * THE KEY METHOD!
438
     * Searches data, replaces (if enabled) with given key, prepares log
439
     * @param string $file - e.g. /var/www/mysite.co.nz/mysite/code/Page.php
440
     */
441
    private function searchFileData($file)
442
    {
443
        $myReplacementKey = $this->replacementKey;
444
        $searchKey = preg_quote($this->searchKey, '/');
445
        if ($this->isReplacingEnabled) {
446
            //prerequisites for file and content ...
447
            if ($this->testMustContain($file) === false) {
448
                return;
449
            }
450
            if ($this->testFileNameRequirements($file) === false) {
451
                return;
452
            }
453
454
            //get magic data
455
            $classNameOfFile = $this->getClassNameOfFile($file);
456
            foreach ($this->magicReplacers as $magicReplacerFind => $magicReplacerReplaceVariable) {
457
                $myReplacementKey = str_replace($magicReplacerFind, ${$magicReplacerReplaceVariable}, $myReplacementKey);
458
            }
459
            $oldFileContentArray = file($file);
460
            $newFileContentArray = [];
461
            $pattern = "/${searchKey}/U";
462
            if (! $this->caseSensitive) {
463
                $pattern = "/${searchKey}/Ui";
464
            }
465
            $foundCount = 0;
466
            $insidePreviousReplaceComment = false;
467
            $insideIgnoreArea = false;
468
            $completedTask = false;
469
            foreach ($oldFileContentArray as $key => $oldLineContent) {
470
                $newLineContent = $oldLineContent;
471
472
                if ($completedTask === false) {
473
                    $testLine = trim($oldLineContent);
474
475
                    //check if it is actually already replaced ...
476
                    if (strpos($oldLineContent, $this->startMarker) !== false) {
477
                        $insidePreviousReplaceComment = true;
478
                    }
479
                    foreach ($this->ignoreFrom as $ignoreStarter) {
480
                        if (strpos($testLine, $ignoreStarter) === 0) {
481
                            $insideIgnoreArea = true;
482
                        }
483
                    }
484
                    if ($insidePreviousReplaceComment || $insideIgnoreArea) {
485
                        //do nothing ...
486
                    } else {
487
                        $foundInLineCount = preg_match_all($pattern, $oldLineContent, $matches, PREG_PATTERN_ORDER);
488
                        if ($foundInLineCount) {
489
                            if ($this->caseSensitive) {
490
                                if (strpos($oldLineContent, $this->searchKey) === false) {
491
                                    user_error('Regex found it, but phrase does not exist: ' . $this->searchKey);
492
                                }
493
                            } else {
494
                                if (stripos($oldLineContent, $this->searchKey) === false) {
495
                                    user_error('Regex found it, but phrase does not exist: ' . $this->searchKey);
496
                                }
497
                            }
498
                            $foundCount += $foundInLineCount;
499
                            if ($this->isReplacingEnabled) {
500
                                $newLineContent = preg_replace($pattern, $myReplacementKey, $oldLineContent);
501
                                if ($fullComment = $this->getFullComment()) {
502
                                    $newFileContentArray[] = $fullComment;
503
                                }
504
                            }
505
                        } else {
506
                            $hasError = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $hasError is dead and can be removed.
Loading history...
507
                            if ($this->caseSensitive) {
508
                                if (strpos($oldLineContent, $this->searchKey) !== false) {
509
                                    user_error('Should have found: ' . $this->searchKey);
510
                                }
511
                            } else {
512
                                if (stripos($oldLineContent, $this->searchKey) !== false) {
513
                                    user_error('Should have found: ' . $this->searchKey);
514
                                }
515
                            }
516
                        }
517
                    }
518
                    if (strpos($oldLineContent, $this->endMarker) !== false) {
519
                        $insidePreviousReplaceComment = false;
520
                    }
521
                    foreach ($this->ignoreUntil as $ignoreEnder) {
522
                        if (strpos($testLine, $ignoreEnder) === 0) {
523
                            $insideIgnoreArea = false;
524
                        }
525
                    }
526
                    if ($this->fileReplacementMaxCount > 0 && $foundCount >= $this->fileReplacementMaxCount) {
527
                        $completedTask = true;
528
                    }
529
                }
530
531
                $newFileContentArray[] = $newLineContent;
532
            }
533
            if ($foundCount) {
534
                $oldFileContent = implode($oldFileContentArray);
0 ignored issues
show
Bug introduced by
It seems like $oldFileContentArray can also be of type false; however, parameter $pieces of implode() does only seem to accept array, 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

534
                $oldFileContent = implode(/** @scrutinizer ignore-type */ $oldFileContentArray);
Loading history...
535
                $newFileContent = implode($newFileContentArray);
536
                if ($newFileContent !== $oldFileContent) {
537
                    $this->writeToFile($file, $newFileContent);
538
539
                    //stats
540
                    $this->totalFound += $foundInLineCount;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $foundInLineCount does not seem to be defined for all execution paths leading up to this point.
Loading history...
541
                    if (! isset($this->searchKeyTotals[$this->searchKey])) {
542
                        $this->searchKeyTotals[$this->searchKey] = 0;
543
                    }
544
                    $this->searchKeyTotals[$this->searchKey] += $foundCount;
545
546
                    if (! isset($this->folderTotals[dirname($file)])) {
547
                        $this->folderTotals[dirname($file)] = 0;
548
                    }
549
                    $this->folderTotals[dirname($file)] += $foundCount;
550
551
                    //log
552
                    $foundStr = "-- ${foundCount} x";
553
                    if ($this->fileReplacementMaxCount) {
554
                        $foundStr .= ' limited to ' . $this->fileReplacementMaxCount;
555
                    }
556
                    $this->appendToLog($file, $foundStr);
557
                } else {
558
                    $this->appendToLog($file, '********** ERROR: NO REPLACEMENT DESPITE MATCHES - searched for: ' . $pattern . ' and replaced with ' . $myReplacementKey . " \n");
559
                }
560
            }
561
        } else {
562
            $this->appendToLog($file, '********** ERROR: Replacement Text is not defined');
563
        }
564
    }
565
566
    /**
567
     * Writes new data (after the replacement) to file
568
     * @param $file, $data
0 ignored issues
show
Documentation Bug introduced by
The doc comment $file, at position 0 could not be parsed: Unknown type name '$file' at position 0 in $file,.
Loading history...
569
     */
570
    private function writeToFile($file, $data)
571
    {
572
        if (is_writable($file)) {
573
            $fp = fopen($file, 'w');
574
            fwrite($fp, $data);
0 ignored issues
show
Bug introduced by
It seems like $fp can also be of type false; however, parameter $handle of fwrite() 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

574
            fwrite(/** @scrutinizer ignore-type */ $fp, $data);
Loading history...
575
            fclose($fp);
0 ignored issues
show
Bug introduced by
It seems like $fp can also be of type false; however, parameter $handle of fclose() 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

575
            fclose(/** @scrutinizer ignore-type */ $fp);
Loading history...
576
        } else {
577
            user_error("********** ERROR: Can not replace text. File ${file} is not writable. \nPlease make it writable\n");
578
        }
579
    }
580
581
    /**
582
     * Appends log data to previous log data
583
     * @param string $file
584
     * @param string $matchStr
585
     */
586
    private function appendToLog($file, $matchStr)
587
    {
588
        if ($this->logString === '') {
589
            $this->logString = "'" . $this->searchKey . "'\n";
590
        }
591
        $file = basename($file);
592
        $this->logString .= "   ${matchStr} IN ${file}\n";
593
    }
594
595
    /**
596
     * returns full output
597
     * and clears it.
598
     * @return string
599
     */
600
    private function addToOutput($s)
601
    {
602
        $this->output .= $s;
603
    }
604
605
    private function getClassNameOfFile($filePath)
606
    {
607
        if (! self::$_finder) {
608
            self::$_finder = new FileNameToClass();
609
        }
610
        if (! isset(self::$_class_name_cache[$filePath])) {
611
            $class = self::$_finder->getClassNameFromFile($filePath);
612
            //see: https://stackoverflow.com/questions/7153000/get-class-name-from-file/44654073
613
            // $file = 'class.php'; # contains class Foo
614
            // $class = shell_exec("php -r \"include('$file'); echo end(get_declared_classes());\"");
615
            //see: https://stackoverflow.com/questions/7153000/get-class-name-from-file/44654073
616
            // $fp = fopen($filePath, 'r');
617
            // $class = $buffer = '';
618
            // $i = 0;
619
            // while (!$class) {
620
            //     if (feof($fp)) {
621
            //         break;
622
            //     }
623
            //
624
            //     $buffer .= fread($fp, 512);
625
            //     @$tokens = token_get_all($buffer);
626
            //
627
            //     if (strpos($buffer, '{') === false) continue;
628
            //
629
            //     for (;$i<count($tokens);$i++) {
630
            //         if ($tokens[$i][0] === T_CLASS) {
631
            //             for ($j=$i+1;$j<count($tokens);$j++) {
632
            //                 if ($tokens[$j] === '{') {
633
            //                     $class = $tokens[$i+2][1];
634
            //                     break 2;
635
            //                 }
636
            //             }
637
            //         }
638
            //     }
639
            // }
640
            self::$_class_name_cache[$filePath] = $class;
641
        }
642
        return self::$_class_name_cache[$filePath];
643
    }
644
645
    private function testMustContain($fileName)
646
    {
647
        if (is_array($this->ignoreFileIfFound) && count($this->ignoreFileIfFound)) {
648
            foreach ($this->ignoreFileIfFound as $ignoreString) {
649
                if ($this->hasStringPresentInFile($fileName, $ignoreString)) {
650
                    $this->appendToLog($fileName, '********** Ignoring file, as ignore string found: ' . $ignoreString);
651
652
                    return false;
653
                }
654
            }
655
        }
656
        return true;
657
    }
658
659
    private function testFileNameRequirements($fileName)
660
    {
661
        if (is_array($this->fileNameMustContain) && count($this->fileNameMustContain)) {
662
            $passed = false;
663
            $fileBaseName = basename($fileName);
664
            foreach ($this->fileNameMustContain as $fileNameMustContainString) {
665
                if (stripos($fileBaseName, $fileNameMustContainString) !== false) {
666
                    $passed = true;
667
                }
668
            }
669
            if ($passed === false) {
670
                $this->appendToLog($fileName, "********** skipping file ('.${fileBaseName}.'), as it does not contain any of the following: " . implode(', ', $this->fileNameMustContain));
671
672
                return false;
673
            }
674
        }
675
676
        return true;
677
    }
678
679
    private function hasStringPresentInFile($fileName, $string)
680
    {
681
        // get the file contents, assuming the file to be readable (and exist)
682
        $contents = file_get_contents($fileName);
683
        if (strpos($contents, $string) !== false) {
684
            return true;
685
        }
686
        return false;
687
    }
688
}
689