Passed
Push — master ( 94abca...7bb122 )
by Greg
05:28
created

FixSearchAndReplace   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 382
Duplicated Lines 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 119
dl 0
loc 382
rs 8.8798
c 1
b 1
f 0
wmc 44

17 Methods

Rating   Name   Duplication   Size   Complexity  
A fixOptions() 0 27 1
B recordQuery() 0 33 9
A updateGedcom() 0 9 1
A individualsToFix() 0 12 3
A updateRecord() 0 3 1
A doesRecordNeedUpdate() 0 3 1
A mediaToFix() 0 12 3
A sourcesToFix() 0 12 3
A submittersToFix() 0 13 3
A familiesToFix() 0 10 3
A description() 0 4 1
A repositoriesToFix() 0 13 3
A __construct() 0 3 1
A title() 0 4 1
A createRegex() 0 31 6
A notesToFix() 0 13 3
A previewUpdate() 0 6 1

How to fix   Complexity   

Complex Class

Complex classes like FixSearchAndReplace often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FixSearchAndReplace, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2019 webtrees development team
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Module;
21
22
use Fisharebest\Webtrees\Exceptions\HttpNotFoundException;
23
use Fisharebest\Webtrees\Family;
24
use Fisharebest\Webtrees\GedcomRecord;
25
use Fisharebest\Webtrees\I18N;
26
use Fisharebest\Webtrees\Individual;
27
use Fisharebest\Webtrees\Media;
28
use Fisharebest\Webtrees\Note;
29
use Fisharebest\Webtrees\Repository;
30
use Fisharebest\Webtrees\Services\DataFixService;
31
use Fisharebest\Webtrees\Source;
32
use Fisharebest\Webtrees\Submitter;
33
use Fisharebest\Webtrees\Tree;
34
use Illuminate\Database\Capsule\Manager as DB;
35
use Illuminate\Database\Query\Builder;
36
use Illuminate\Support\Collection;
37
use Throwable;
38
39
use function addcslashes;
40
use function asort;
41
use function preg_match;
42
use function preg_quote;
43
use function preg_replace;
44
use function view;
45
46
/**
47
 * Class FixSearchAndReplace
48
 */
49
class FixSearchAndReplace extends AbstractModule implements ModuleDataFixInterface
50
{
51
    use ModuleDataFixTrait;
1 ignored issue
show
Bug introduced by
The trait Fisharebest\Webtrees\Module\ModuleDataFixTrait requires the property $xref which is not provided by Fisharebest\Webtrees\Module\FixSearchAndReplace.
Loading history...
52
53
    // A regular expression that never matches.
54
    private const INVALID_REGEX = '/(?!)/';
55
56
    /** @var DataFixService */
57
    private $data_fix_service;
58
59
    /**
60
     * FixMissingDeaths constructor.
61
     *
62
     * @param DataFixService $data_fix_service
63
     */
64
    public function __construct(DataFixService $data_fix_service)
65
    {
66
        $this->data_fix_service = $data_fix_service;
67
    }
68
69
    /**
70
     * How should this module be identified in the control panel, etc.?
71
     *
72
     * @return string
73
     */
74
    public function title(): string
75
    {
76
        /* I18N: Name of a module */
77
        return I18N::translate('Search and replace');
78
    }
79
80
    /**
81
     * A sentence describing what this module does.
82
     *
83
     * @return string
84
     */
85
    public function description(): string
86
    {
87
        /* I18N: Description of a “Data fix” module */
88
        return I18N::translate('Search and replace text, using simple searches or advanced pattern matching.');
89
    }
90
91
    /**
92
     * Options form.
93
     *
94
     * @param Tree $tree
95
     *
96
     * @return string
97
     */
98
    public function fixOptions(Tree $tree): string
99
    {
100
        $methods = [
101
            'exact'     => I18N::translate('Match the exact text, even if it occurs in the middle of a word.'),
102
            'words'     => I18N::translate('Match the exact text, unless it occurs in the middle of a word.'),
103
            'wildcards' => I18N::translate('Use a “?” to match a single character, use “*” to match zero or more characters.'),
104
            /* I18N: http://en.wikipedia.org/wiki/Regular_expression */
105
            'regex'     => I18N::translate('Regular expressions are an advanced pattern matching technique.') . '<br>' . I18N::translate('See %s for more information.', '<a href="http://php.net/manual/regexp.reference.php">php.net/manual/regexp.reference.php</a>'),
106
        ];
107
108
        $types = [
109
            Family::RECORD_TYPE     => I18N::translate('Families'),
110
            Individual::RECORD_TYPE => I18N::translate('Individuals'),
111
            Media::RECORD_TYPE      => I18N::translate('Media objects'),
112
            Note::RECORD_TYPE       => I18N::translate('Notes'),
113
            Repository::RECORD_TYPE => I18N::translate('Repositories'),
114
            Source::RECORD_TYPE     => I18N::translate('Sources'),
115
            Submitter::RECORD_TYPE  => I18N::translate('Submitters'),
116
        ];
117
118
        asort($types);
119
120
        return view('modules/fix-search-and-replace/options', [
121
            'default_method' => 'exact',
122
            'default_type'   => Individual::RECORD_TYPE,
123
            'methods'        => $methods,
124
            'types'          => $types,
125
        ]);
126
    }
127
128
    /**
129
     * A list of all records that need examining.  This may include records
130
     * that do not need updating, if we can't detect this quickly using SQL.
131
     *
132
     * @param Tree                 $tree
133
     * @param array<string,string> $params
134
     *
135
     * @return Collection<string>|null
136
     */
137
    protected function familiesToFix(Tree $tree, array $params): ?Collection
138
    {
139
        if ($params['type'] !== Family::RECORD_TYPE || $params['search'] === '') {
140
            return null;
141
        }
142
143
        $query = DB::table('families')->where('f_file', '=', $tree->id());
144
        $this->recordQuery($query, 'f_gedcom', $params);
145
146
        return $query->pluck('f_id');
147
    }
148
149
    /**
150
     * A list of all records that need examining.  This may include records
151
     * that do not need updating, if we can't detect this quickly using SQL.
152
     *
153
     * @param Tree                 $tree
154
     * @param array<string,string> $params
155
     *
156
     * @return Collection<string>|null
157
     */
158
    protected function individualsToFix(Tree $tree, array $params): ?Collection
159
    {
160
        if ($params['type'] !== Individual::RECORD_TYPE || $params['search'] === '') {
161
            return null;
162
        }
163
164
        $query = DB::table('individuals')
165
            ->where('i_file', '=', $tree->id());
166
        
167
        $this->recordQuery($query, 'i_gedcom', $params);
168
169
        return $query->pluck('i_id');
170
    }
171
172
    /**
173
     * A list of all records that need examining.  This may include records
174
     * that do not need updating, if we can't detect this quickly using SQL.
175
     *
176
     * @param Tree                 $tree
177
     * @param array<string,string> $params
178
     *
179
     * @return Collection<string>|null
180
     */
181
    protected function mediaToFix(Tree $tree, array $params): ?Collection
182
    {
183
        if ($params['type'] !== Media::RECORD_TYPE || $params['search'] === '') {
184
            return null;
185
        }
186
187
        $query = DB::table('media')
188
            ->where('m_file', '=', $tree->id());
189
190
        $this->recordQuery($query, 'm_gedcom', $params);
191
192
        return $query->pluck('m_id');
193
    }
194
195
    /**
196
     * A list of all records that need examining.  This may include records
197
     * that do not need updating, if we can't detect this quickly using SQL.
198
     *
199
     * @param Tree                 $tree
200
     * @param array<string,string> $params
201
     *
202
     * @return Collection<string>|null
203
     */
204
    protected function notesToFix(Tree $tree, array $params): ?Collection
205
    {
206
        if ($params['type'] !== Note::RECORD_TYPE || $params['search'] === '') {
207
            return null;
208
        }
209
210
        $query = DB::table('other')
211
            ->where('o_file', '=', $tree->id())
212
            ->where('o_type', '=', Note::RECORD_TYPE);
213
214
        $this->recordQuery($query, 'o_gedcom', $params);
215
216
        return $query->pluck('o_id');
217
    }
218
219
    /**
220
     * A list of all records that need examining.  This may include records
221
     * that do not need updating, if we can't detect this quickly using SQL.
222
     *
223
     * @param Tree                 $tree
224
     * @param array<string,string> $params
225
     *
226
     * @return Collection<string>|null
227
     */
228
    protected function repositoriesToFix(Tree $tree, array $params): ?Collection
229
    {
230
        if ($params['type'] !== Repository::RECORD_TYPE || $params['search'] === '') {
231
            return null;
232
        }
233
234
        $query = DB::table('other')
235
            ->where('o_file', '=', $tree->id())
236
            ->where('o_type', '=', Repository::RECORD_TYPE);
237
238
        $this->recordQuery($query, 'o_gedcom', $params);
239
240
        return $query->pluck('o_id');
241
    }
242
243
    /**
244
     * A list of all records that need examining.  This may include records
245
     * that do not need updating, if we can't detect this quickly using SQL.
246
     *
247
     * @param Tree                 $tree
248
     * @param array<string,string> $params
249
     *
250
     * @return Collection<string>|null
251
     */
252
    protected function sourcesToFix(Tree $tree, array $params): ?Collection
253
    {
254
        if ($params['type'] !== Source::RECORD_TYPE || $params['search'] === '') {
255
            return null;
256
        }
257
258
        $query = DB::table('sources')
259
            ->where('s_file', '=', $tree->id());
260
261
        $this->recordQuery($query, 's_gedcom', $params);
262
263
        return $query->pluck('s_id');
264
    }
265
266
    /**
267
     * A list of all records that need examining.  This may include records
268
     * that do not need updating, if we can't detect this quickly using SQL.
269
     *
270
     * @param Tree                 $tree
271
     * @param array<string,string> $params
272
     *
273
     * @return Collection<string>|null
274
     */
275
    protected function submittersToFix(Tree $tree, array $params): ?Collection
276
    {
277
        if ($params['type'] !== Submitter::RECORD_TYPE || $params['search'] === '') {
278
            return null;
279
        }
280
281
        $query = DB::table('other')
282
            ->where('o_file', '=', $tree->id())
283
            ->where('o_type', '=', Submitter::RECORD_TYPE);
284
285
        $this->recordQuery($query, 'o_gedcom', $params);
286
287
        return $query->pluck('o_id');
288
    }
289
290
    /**
291
     * Does a record need updating?
292
     *
293
     * @param GedcomRecord         $record
294
     * @param array<string,string> $params
295
     *
296
     * @return bool
297
     */
298
    public function doesRecordNeedUpdate(GedcomRecord $record, array $params): bool
299
    {
300
        return preg_match($this->createRegex($params), $record->gedcom()) === 1;
301
    }
302
303
    /**
304
     * Show the changes we would make
305
     *
306
     * @param GedcomRecord         $record
307
     * @param array<string,string> $params
308
     *
309
     * @return string
310
     */
311
    public function previewUpdate(GedcomRecord $record, array $params): string
312
    {
313
        $old = $record->gedcom();
314
        $new = $this->updateGedcom($record, $params);
315
316
        return $this->data_fix_service->gedcomDiff($record->tree(), $old, $new);
317
    }
318
319
    /**
320
     * Fix a record
321
     *
322
     * @param GedcomRecord         $record
323
     * @param array<string,string> $params
324
     *
325
     * @return void
326
     */
327
    public function updateRecord(GedcomRecord $record, array $params): void
328
    {
329
        $record->updateRecord($this->updateGedcom($record, $params), false);
330
    }
331
332
    /**
333
     * @param GedcomRecord         $record
334
     * @param array<string,string> $params
335
     *
336
     * @return string
337
     */
338
    private function updateGedcom(GedcomRecord $record, array $params): string
339
    {
340
        // Allow "\n" to indicate a line-feed in replacement text.
341
        // Back-references such as $1, $2 are handled automatically.
342
        $replace = strtr($params['replace'], ['\\n', "\n"]);
343
344
        $regex = $this->createRegex($params);
345
346
        return preg_replace($regex, $replace, $record->gedcom());
347
    }
348
349
    /**
350
     * Create a regular expression from the search pattern.
351
     *
352
     * @param array<string,string> $params
353
     *
354
     * @return string
355
     */
356
    private function createRegex(array $params): string
357
    {
358
        $search = $params['search'];
359
        $method = $params['method'];
360
        $case   = $params['case'];
361
362
        switch ($method) {
363
            case 'exact':
364
                return '/' . preg_quote($search, '/') . '/' . $case;
365
366
            case 'words':
367
                return '/\b' . preg_quote($search, '/') . '\b/' . $case;
368
369
            case 'wildcards':
370
                return '/\b' . strtr(preg_quote($search, '/'), ['\*' => '.*', '\?' => '.']) . '\b/' . $case;
371
372
            case 'regex':
373
                $regex = '/' . addcslashes($search, '/') . '/' . $case;
374
375
                try {
376
                    // A valid regex on an empty string returns zero.
377
                    // An invalid regex on an empty string returns false and throws a warning.
378
                    preg_match($regex, '');
379
                } catch (Throwable $ex) {
380
                    $regex = self::INVALID_REGEX;
381
                }
382
383
                return $regex;
384
        }
385
386
        throw new HttpNotFoundException();
387
    }
388
389
    /**
390
     * Create a regular expression from the search pattern.
391
     *
392
     * @param Builder              $query
393
     * @param string               $column
394
     * @param array<string,string> $params
395
     *
396
     * @return void
397
     */
398
    private function recordQuery(Builder $query, string $column, array $params): void
399
    {
400
        $search = $params['search'];
401
        $method = $params['method'];
402
        $like   = addcslashes($search, '_%\\');
403
404
        switch ($method) {
405
            case 'exact':
406
            case 'words':
407
                $query->where($column, 'LIKE', '%' . $like . '$');
408
                break;
409
410
            case 'wildcards':
411
                $like = strtr($like, ['?' => '_', '*' => '%']);
412
                $query->where($column, 'LIKE', '%' . $like . '$');
413
                break;
414
415
            case 'regex':
416
                switch (DB::connection()->getDriverName()) {
417
                    case 'sqlite':
418
                    case 'mysql':
419
                        $query->where($column, 'REGEXP', $search);
420
                        break;
421
422
                    case 'pgsql':
423
                        $query->where($column, '~', $search);
424
                        break;
425
426
                    case 'sqlsvr':
427
                        // Not available
428
                        break;
429
                }
430
                break;
431
        }
432
    }
433
}
434