xref: /webtrees/app/Module/FixDuplicateLinks.php (revision ab6971f7c60169bd49b369ec3340e17c287a548b)
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
18declare(strict_types=1);
19
20namespace Fisharebest\Webtrees\Module;
21
22use Fisharebest\Webtrees\GedcomRecord;
23use Fisharebest\Webtrees\I18N;
24use Fisharebest\Webtrees\Note;
25use Fisharebest\Webtrees\Repository;
26use Fisharebest\Webtrees\Services\DataFixService;
27use Fisharebest\Webtrees\Submitter;
28use Fisharebest\Webtrees\Tree;
29use Illuminate\Database\Capsule\Manager as DB;
30use Illuminate\Support\Collection;
31
32use function preg_match;
33use function preg_replace;
34
35/**
36 * Class FixDuplicateLinks
37 */
38class FixDuplicateLinks extends AbstractModule implements ModuleDataFixInterface
39{
40    use ModuleDataFixTrait;
41
42    /** @var DataFixService */
43    private $data_fix_service;
44
45    /**
46     * FixMissingDeaths constructor.
47     *
48     * @param DataFixService $data_fix_service
49     */
50    public function __construct(DataFixService $data_fix_service)
51    {
52        $this->data_fix_service = $data_fix_service;
53    }
54
55    /**
56     * How should this module be identified in the control panel, etc.?
57     *
58     * @return string
59     */
60    public function title(): string
61    {
62        /* I18N: Name of a module */
63        return I18N::translate('Remove duplicate links');
64    }
65
66    /**
67     * A sentence describing what this module does.
68     *
69     * @return string
70     */
71    public function description(): string
72    {
73        /* I18N: Description of a “Data fix” module */
74        return I18N::translate('A common error is to have multiple links to the same record, for example listing the same child more than once in a family record.');
75    }
76
77    /**
78     * A list of all records that need examining.  This may include records
79     * that do not need updating, if we can't detect this quickly using SQL.
80     *
81     * @param Tree          $tree
82     * @param array<string> $params
83     *
84     * @return Collection<string>
85     */
86    protected function familiesToFix(Tree $tree, array $params): Collection
87    {
88        // No DB querying possible?  Select all.
89        return DB::table('families')
90            ->where('f_file', '=', $tree->id())
91            ->pluck('f_id');
92    }
93
94    /**
95     * A list of all records that need examining.  This may include records
96     * that do not need updating, if we can't detect this quickly using SQL.
97     *
98     * @param Tree                 $tree
99     * @param array<string,string> $params
100     *
101     * @return Collection<string>|null
102     */
103    protected function individualsToFix(Tree $tree, array $params): ?Collection
104    {
105        // No DB querying possible?  Select all.
106        return DB::table('individuals')
107            ->where('i_file', '=', $tree->id())
108            ->pluck('i_id');
109    }
110
111    /**
112     * A list of all records that need examining.  This may include records
113     * that do not need updating, if we can't detect this quickly using SQL.
114     *
115     * @param Tree                 $tree
116     * @param array<string,string> $params
117     *
118     * @return Collection<string>
119     */
120    protected function mediaToFix(Tree $tree, array $params): Collection
121    {
122        // No DB querying possible?  Select all.
123        return DB::table('media')
124            ->where('m_file', '=', $tree->id())
125            ->pluck('m_id');
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>
136     */
137    protected function notesToFix(Tree $tree, array $params): Collection
138    {
139        // No DB querying possible?  Select all.
140        return DB::table('other')
141            ->where('o_file', '=', $tree->id())
142            ->where('o_type', '=', Note::RECORD_TYPE)
143            ->pluck('o_id');
144    }
145
146    /**
147     * A list of all records that need examining.  This may include records
148     * that do not need updating, if we can't detect this quickly using SQL.
149     *
150     * @param Tree                 $tree
151     * @param array<string,string> $params
152     *
153     * @return Collection<string>
154     */
155    protected function repositoriesToFix(Tree $tree, array $params): Collection
156    {
157        // No DB querying possible?  Select all.
158        return DB::table('other')
159            ->where('o_file', '=', $tree->id())
160            ->where('o_type', '=', Repository::RECORD_TYPE)
161            ->pluck('o_id');
162    }
163
164    /**
165     * A list of all records that need examining.  This may include records
166     * that do not need updating, if we can't detect this quickly using SQL.
167     *
168     * @param Tree                 $tree
169     * @param array<string,string> $params
170     *
171     * @return Collection<string>
172     */
173    protected function sourcesToFix(Tree $tree, array $params): Collection
174    {
175        // No DB querying possible?  Select all.
176        return DB::table('sources')
177            ->where('s_file', '=', $tree->id())
178            ->pluck('s_id');
179    }
180
181    /**
182     * A list of all records that need examining.  This may include records
183     * that do not need updating, if we can't detect this quickly using SQL.
184     *
185     * @param Tree                 $tree
186     * @param array<string,string> $params
187     *
188     * @return Collection<string>
189     */
190    protected function submittersToFix(Tree $tree, array $params): Collection
191    {
192        // No DB querying possible?  Select all.
193        return DB::table('other')
194            ->where('o_file', '=', $tree->id())
195            ->where('o_type', '=', Submitter::RECORD_TYPE)
196            ->pluck('o_id');
197    }
198
199    /**
200     * Does a record need updating?
201     *
202     * @param GedcomRecord         $record
203     * @param array<string,string> $params
204     *
205     * @return bool
206     */
207    public function doesRecordNeedUpdate(GedcomRecord $record, array $params): bool
208    {
209        $gedcom = $record->gedcom();
210
211        return
212            preg_match('/(\n1.*@.+@.*(?:(?:\n[2-9].*)*))(?:\n1.*(?:\n[2-9].*)*)*\1/', $gedcom) ||
213            preg_match('/(\n2.*@.+@.*(?:(?:\n[3-9].*)*))(?:\n2.*(?:\n[3-9].*)*)*\1/', $gedcom) ||
214            preg_match('/(\n3.*@.+@.*(?:(?:\n[4-9].*)*))(?:\n3.*(?:\n[4-9].*)*)*\1/', $gedcom);
215    }
216
217    /**
218     * Show the changes we would make
219     *
220     * @param GedcomRecord         $record
221     * @param array<string,string> $params
222     *
223     * @return string
224     */
225    public function previewUpdate(GedcomRecord $record, array $params): string
226    {
227        $old = $record->gedcom();
228        $new = $this->updateGedcom($record);
229
230        return $this->data_fix_service->gedcomDiff($record->tree(), $old, $new);
231    }
232
233    /**
234     * Fix a record
235     *
236     * @param GedcomRecord         $record
237     * @param array<string,string> $params
238     *
239     * @return void
240     */
241    public function updateRecord(GedcomRecord $record, array $params): void
242    {
243        $record->updateRecord($this->updateGedcom($record), false);
244    }
245
246    /**
247     * @param GedcomRecord $record
248     *
249     * @return string
250     */
251    private function updateGedcom(GedcomRecord $record): string
252    {
253        return preg_replace([
254            '/(\n1.*@.+@.*(?:(?:\n[2-9].*)*))((?:\n1.*(?:\n[2-9].*)*)*\1)/',
255            '/(\n2.*@.+@.*(?:(?:\n[3-9].*)*))((?:\n2.*(?:\n[3-9].*)*)*\1)/',
256            '/(\n3.*@.+@.*(?:(?:\n[4-9].*)*))((?:\n3.*(?:\n[4-9].*)*)*\1)/',
257        ], '$2', $record->gedcom());
258    }
259}
260