Completed
Push — openstreetmap ( a371f7...06e980 )
by Greg
17:44 queued 07:41
created

AdminController::serverInformation()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 16
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 13
nc 1
nop 0
dl 0
loc 16
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * webtrees: online genealogy
4
 * Copyright (C) 2018 webtrees development team
5
 * This program is free software: you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation, either version 3 of the License, or
8
 * (at your option) any later version.
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
 * GNU General Public License for more details.
13
 * You should have received a copy of the GNU General Public License
14
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15
 */
16
declare(strict_types=1);
17
18
namespace Fisharebest\Webtrees\Http\Controllers;
19
20
use FilesystemIterator;
21
use Fisharebest\Algorithm\MyersDiff;
22
use Fisharebest\Webtrees\Auth;
23
use Fisharebest\Webtrees\Database;
24
use Fisharebest\Webtrees\Fact;
25
use Fisharebest\Webtrees\Family;
26
use Fisharebest\Webtrees\File;
27
use Fisharebest\Webtrees\FlashMessages;
28
use Fisharebest\Webtrees\Functions\Functions;
29
use Fisharebest\Webtrees\Functions\FunctionsDb;
30
use Fisharebest\Webtrees\Functions\FunctionsImport;
31
use Fisharebest\Webtrees\GedcomRecord;
32
use Fisharebest\Webtrees\GedcomTag;
33
use Fisharebest\Webtrees\I18N;
34
use Fisharebest\Webtrees\Individual;
35
use Fisharebest\Webtrees\Media;
36
use Fisharebest\Webtrees\Module;
37
use Fisharebest\Webtrees\Note;
38
use Fisharebest\Webtrees\Repository;
39
use Fisharebest\Webtrees\Source;
40
use Fisharebest\Webtrees\Tree;
41
use Fisharebest\Webtrees\User;
42
use Intervention\Image\ImageManager;
43
use RecursiveDirectoryIterator;
44
use RecursiveIteratorIterator;
45
use stdClass;
46
use Symfony\Component\HttpFoundation\JsonResponse;
47
use Symfony\Component\HttpFoundation\RedirectResponse;
48
use Symfony\Component\HttpFoundation\Request;
49
use Symfony\Component\HttpFoundation\Response;
50
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
51
use Throwable;
52
53
/**
54
 * Controller for the administration pages
55
 */
56
class AdminController extends AbstractBaseController {
57
	// This is a list of old files and directories, from earlier versions of webtrees.
58
	// git diff 1.7.9..master --name-status | grep ^D
59
	const OLD_FILES = [
60
		// Removed in 1.0.2
61
		WT_ROOT . 'language/en.mo',
62
		// Removed in 1.0.3
63
		WT_ROOT . 'themechange.php',
64
		// Removed in 1.1.0
65
		WT_ROOT . 'addremotelink.php',
66
		WT_ROOT . 'addsearchlink.php',
67
		WT_ROOT . 'client.php',
68
		WT_ROOT . 'dir_editor.php',
69
		WT_ROOT . 'editconfig_gedcom.php',
70
		WT_ROOT . 'editgedcoms.php',
71
		WT_ROOT . 'edit_merge.php',
72
		WT_ROOT . 'edit_news.php',
73
		WT_ROOT . 'genservice.php',
74
		WT_ROOT . 'logs.php',
75
		WT_ROOT . 'manageservers.php',
76
		WT_ROOT . 'media.php',
77
		WT_ROOT . 'module_admin.php',
78
		//WT_ROOT.'modules', // Do not delete - users may have stored custom modules/data here
79
		WT_ROOT . 'opensearch.php',
80
		WT_ROOT . 'PEAR.php',
81
		WT_ROOT . 'pgv_to_wt.php',
82
		WT_ROOT . 'places',
83
		//WT_ROOT.'robots.txt', // Do not delete this - it may contain user data
84
		WT_ROOT . 'serviceClientTest.php',
85
		WT_ROOT . 'siteconfig.php',
86
		WT_ROOT . 'SOAP',
87
		WT_ROOT . 'themes/clouds/mozilla.css',
88
		WT_ROOT . 'themes/clouds/netscape.css',
89
		WT_ROOT . 'themes/colors/mozilla.css',
90
		WT_ROOT . 'themes/colors/netscape.css',
91
		WT_ROOT . 'themes/fab/mozilla.css',
92
		WT_ROOT . 'themes/fab/netscape.css',
93
		WT_ROOT . 'themes/minimal/mozilla.css',
94
		WT_ROOT . 'themes/minimal/netscape.css',
95
		WT_ROOT . 'themes/webtrees/mozilla.css',
96
		WT_ROOT . 'themes/webtrees/netscape.css',
97
		WT_ROOT . 'themes/webtrees/style_rtl.css',
98
		WT_ROOT . 'themes/xenea/mozilla.css',
99
		WT_ROOT . 'themes/xenea/netscape.css',
100
		WT_ROOT . 'uploadmedia.php',
101
		WT_ROOT . 'useradmin.php',
102
		WT_ROOT . 'webservice',
103
		WT_ROOT . 'wtinfo.php',
104
		// Removed in 1.1.2
105
		WT_ROOT . 'treenav.php',
106
		// Removed in 1.2.0
107
		WT_ROOT . 'themes/clouds/jquery',
108
		WT_ROOT . 'themes/colors/jquery',
109
		WT_ROOT . 'themes/fab/jquery',
110
		WT_ROOT . 'themes/minimal/jquery',
111
		WT_ROOT . 'themes/webtrees/jquery',
112
		WT_ROOT . 'themes/xenea/jquery',
113
		// Removed in 1.2.2
114
		WT_ROOT . 'themes/clouds/chrome.css',
115
		WT_ROOT . 'themes/clouds/opera.css',
116
		WT_ROOT . 'themes/clouds/print.css',
117
		WT_ROOT . 'themes/clouds/style_rtl.css',
118
		WT_ROOT . 'themes/colors/chrome.css',
119
		WT_ROOT . 'themes/colors/opera.css',
120
		WT_ROOT . 'themes/colors/print.css',
121
		WT_ROOT . 'themes/colors/style_rtl.css',
122
		WT_ROOT . 'themes/fab/chrome.css',
123
		WT_ROOT . 'themes/fab/opera.css',
124
		WT_ROOT . 'themes/minimal/chrome.css',
125
		WT_ROOT . 'themes/minimal/opera.css',
126
		WT_ROOT . 'themes/minimal/print.css',
127
		WT_ROOT . 'themes/minimal/style_rtl.css',
128
		WT_ROOT . 'themes/xenea/chrome.css',
129
		WT_ROOT . 'themes/xenea/opera.css',
130
		WT_ROOT . 'themes/xenea/print.css',
131
		WT_ROOT . 'themes/xenea/style_rtl.css',
132
		// Removed in 1.2.3
133
		//WT_ROOT.'modules_v2', // Do not delete - users may have stored custom modules/data here
134
		// Removed in 1.2.4
135
		WT_ROOT . 'modules_v3/gedcom_favorites/help_text.php',
136
		WT_ROOT . 'modules_v3/GEDFact_assistant/_MEDIA/media_3_find.php',
137
		WT_ROOT . 'modules_v3/GEDFact_assistant/_MEDIA/media_3_search_add.php',
138
		WT_ROOT . 'modules_v3/GEDFact_assistant/_MEDIA/media_5_input.js',
139
		WT_ROOT . 'modules_v3/GEDFact_assistant/_MEDIA/media_5_input.php',
140
		WT_ROOT . 'modules_v3/GEDFact_assistant/_MEDIA/media_7_parse_addLinksTbl.php',
141
		WT_ROOT . 'modules_v3/GEDFact_assistant/_MEDIA/media_query_1a.php',
142
		WT_ROOT . 'modules_v3/GEDFact_assistant/_MEDIA/media_query_2a.php',
143
		WT_ROOT . 'modules_v3/GEDFact_assistant/_MEDIA/media_query_3a.php',
144
		WT_ROOT . 'modules_v3/lightbox/css/album_page_RTL2.css',
145
		WT_ROOT . 'modules_v3/lightbox/css/album_page_RTL.css',
146
		WT_ROOT . 'modules_v3/lightbox/css/album_page_RTL_ff.css',
147
		WT_ROOT . 'modules_v3/lightbox/css/clearbox_music.css',
148
		WT_ROOT . 'modules_v3/lightbox/css/clearbox_music_RTL.css',
149
		WT_ROOT . 'modules_v3/user_favorites/db_schema',
150
		WT_ROOT . 'modules_v3/user_favorites/help_text.php',
151
		WT_ROOT . 'search_engine.php',
152
		WT_ROOT . 'themes/clouds/modules.css',
153
		WT_ROOT . 'themes/colors/modules.css',
154
		WT_ROOT . 'themes/fab/modules.css',
155
		WT_ROOT . 'themes/minimal/modules.css',
156
		WT_ROOT . 'themes/webtrees/modules.css',
157
		WT_ROOT . 'themes/xenea/modules.css',
158
		// Removed in 1.2.5
159
		WT_ROOT . 'modules_v3/clippings/index.php',
160
		WT_ROOT . 'modules_v3/googlemap/css/googlemap_style.css',
161
		WT_ROOT . 'modules_v3/googlemap/css/wt_v3_places_edit.css',
162
		WT_ROOT . 'modules_v3/googlemap/index.php',
163
		WT_ROOT . 'modules_v3/lightbox/index.php',
164
		WT_ROOT . 'modules_v3/recent_changes/help_text.php',
165
		WT_ROOT . 'modules_v3/todays_events/help_text.php',
166
		WT_ROOT . 'sidebar.php',
167
		// Removed in 1.2.6
168
		WT_ROOT . 'modules_v3/sitemap/admin_index.php',
169
		WT_ROOT . 'modules_v3/sitemap/help_text.php',
170
		WT_ROOT . 'modules_v3/tree/css/styles',
171
		WT_ROOT . 'modules_v3/tree/css/treebottom.gif',
172
		WT_ROOT . 'modules_v3/tree/css/treebottomleft.gif',
173
		WT_ROOT . 'modules_v3/tree/css/treebottomright.gif',
174
		WT_ROOT . 'modules_v3/tree/css/tree.jpg',
175
		WT_ROOT . 'modules_v3/tree/css/treeleft.gif',
176
		WT_ROOT . 'modules_v3/tree/css/treeright.gif',
177
		WT_ROOT . 'modules_v3/tree/css/treetop.gif',
178
		WT_ROOT . 'modules_v3/tree/css/treetopleft.gif',
179
		WT_ROOT . 'modules_v3/tree/css/treetopright.gif',
180
		WT_ROOT . 'modules_v3/tree/css/treeview_print.css',
181
		WT_ROOT . 'modules_v3/tree/help_text.php',
182
		WT_ROOT . 'modules_v3/tree/images/print.png',
183
		// Removed in 1.2.7
184
		WT_ROOT . 'login_register.php',
185
		WT_ROOT . 'modules_v3/top10_givnnames/help_text.php',
186
		WT_ROOT . 'modules_v3/top10_surnames/help_text.php',
187
		// Removed in 1.3.0
188
		WT_ROOT . 'admin_site_ipaddress.php',
189
		WT_ROOT . 'downloadgedcom.php',
190
		WT_ROOT . 'export_gedcom.php',
191
		WT_ROOT . 'gedcheck.php',
192
		WT_ROOT . 'images',
193
		WT_ROOT . 'modules_v3/googlemap/admin_editconfig.php',
194
		WT_ROOT . 'modules_v3/googlemap/admin_placecheck.php',
195
		WT_ROOT . 'modules_v3/googlemap/flags.php',
196
		WT_ROOT . 'modules_v3/googlemap/images/pedigree_map.gif',
197
		WT_ROOT . 'modules_v3/googlemap/pedigree_map.php',
198
		WT_ROOT . 'modules_v3/lightbox/admin_config.php',
199
		WT_ROOT . 'modules_v3/lightbox/album.php',
200
		WT_ROOT . 'modules_v3/tree/css/vline.jpg',
201
		// Removed in 1.3.1
202
		WT_ROOT . 'imageflush.php',
203
		WT_ROOT . 'modules_v3/googlemap/wt_v3_pedigree_map.js.php',
204
		WT_ROOT . 'modules_v3/lightbox/js/tip_balloon_RTL.js',
205
		// Removed in 1.3.2
206
		WT_ROOT . 'modules_v3/address_report',
207
		WT_ROOT . 'modules_v3/lightbox/functions/lb_horiz_sort.php',
208
		WT_ROOT . 'modules_v3/random_media/help_text.php',
209
		// Removed in 1.4.0
210
		WT_ROOT . 'imageview.php',
211
		WT_ROOT . 'media/MediaInfo.txt',
212
		WT_ROOT . 'media/thumbs/ThumbsInfo.txt',
213
		WT_ROOT . 'modules_v3/GEDFact_assistant/css/media_0_inverselink.css',
214
		WT_ROOT . 'modules_v3/lightbox/help_text.php',
215
		WT_ROOT . 'modules_v3/lightbox/images/blank.gif',
216
		WT_ROOT . 'modules_v3/lightbox/images/close_1.gif',
217
		WT_ROOT . 'modules_v3/lightbox/images/image_add.gif',
218
		WT_ROOT . 'modules_v3/lightbox/images/image_copy.gif',
219
		WT_ROOT . 'modules_v3/lightbox/images/image_delete.gif',
220
		WT_ROOT . 'modules_v3/lightbox/images/image_edit.gif',
221
		WT_ROOT . 'modules_v3/lightbox/images/image_link.gif',
222
		WT_ROOT . 'modules_v3/lightbox/images/images.gif',
223
		WT_ROOT . 'modules_v3/lightbox/images/image_view.gif',
224
		WT_ROOT . 'modules_v3/lightbox/images/loading.gif',
225
		WT_ROOT . 'modules_v3/lightbox/images/next.gif',
226
		WT_ROOT . 'modules_v3/lightbox/images/nextlabel.gif',
227
		WT_ROOT . 'modules_v3/lightbox/images/norm_2.gif',
228
		WT_ROOT . 'modules_v3/lightbox/images/overlay.png',
229
		WT_ROOT . 'modules_v3/lightbox/images/prev.gif',
230
		WT_ROOT . 'modules_v3/lightbox/images/prevlabel.gif',
231
		WT_ROOT . 'modules_v3/lightbox/images/private.gif',
232
		WT_ROOT . 'modules_v3/lightbox/images/slideshow.jpg',
233
		WT_ROOT . 'modules_v3/lightbox/images/transp80px.gif',
234
		WT_ROOT . 'modules_v3/lightbox/images/zoom_1.gif',
235
		WT_ROOT . 'modules_v3/lightbox/js',
236
		WT_ROOT . 'modules_v3/lightbox/music',
237
		WT_ROOT . 'modules_v3/lightbox/pic',
238
		WT_ROOT . 'themes/_administration/jquery',
239
		WT_ROOT . 'themes/webtrees/chrome.css',
240
		// Removed in 1.4.1
241
		WT_ROOT . 'modules_v3/lightbox/images/image_edit.png',
242
		WT_ROOT . 'modules_v3/lightbox/images/image_view.png',
243
		// Removed in 1.4.2
244
		WT_ROOT . 'modules_v3/lightbox/images/image_view.png',
245
		WT_ROOT . 'modules_v3/top10_pageviews/help_text.php',
246
		WT_ROOT . 'themes/_administration/jquery-ui-1.10.0',
247
		WT_ROOT . 'themes/clouds/jquery-ui-1.10.0',
248
		WT_ROOT . 'themes/colors/jquery-ui-1.10.0',
249
		WT_ROOT . 'themes/fab/jquery-ui-1.10.0',
250
		WT_ROOT . 'themes/minimal/jquery-ui-1.10.0',
251
		WT_ROOT . 'themes/webtrees/jquery-ui-1.10.0',
252
		WT_ROOT . 'themes/xenea/jquery-ui-1.10.0',
253
		// Removed in 1.5.0
254
		WT_ROOT . 'modules_v3/GEDFact_assistant/_CENS/census_note_decode.php',
255
		WT_ROOT . 'modules_v3/GEDFact_assistant/_CENS/census_asst_date.php',
256
		WT_ROOT . 'modules_v3/googlemap/wt_v3_googlemap.js.php',
257
		WT_ROOT . 'modules_v3/lightbox/functions/lightbox_print_media.php',
258
		WT_ROOT . 'modules_v3/upcoming_events/help_text.php',
259
		WT_ROOT . 'modules_v3/stories/help_text.php',
260
		WT_ROOT . 'modules_v3/user_messages/help_text.php',
261
		WT_ROOT . 'themes/_administration/favicon.png',
262
		WT_ROOT . 'themes/_administration/images',
263
		WT_ROOT . 'themes/_administration/msie.css',
264
		WT_ROOT . 'themes/_administration/style.css',
265
		WT_ROOT . 'themes/clouds/favicon.png',
266
		WT_ROOT . 'themes/clouds/images',
267
		WT_ROOT . 'themes/clouds/msie.css',
268
		WT_ROOT . 'themes/clouds/style.css',
269
		WT_ROOT . 'themes/colors/css',
270
		WT_ROOT . 'themes/colors/favicon.png',
271
		WT_ROOT . 'themes/colors/images',
272
		WT_ROOT . 'themes/colors/ipad.css',
273
		WT_ROOT . 'themes/colors/msie.css',
274
		WT_ROOT . 'themes/fab/favicon.png',
275
		WT_ROOT . 'themes/fab/images',
276
		WT_ROOT . 'themes/fab/msie.css',
277
		WT_ROOT . 'themes/fab/style.css',
278
		WT_ROOT . 'themes/minimal/favicon.png',
279
		WT_ROOT . 'themes/minimal/images',
280
		WT_ROOT . 'themes/minimal/msie.css',
281
		WT_ROOT . 'themes/minimal/style.css',
282
		WT_ROOT . 'themes/webtrees/favicon.png',
283
		WT_ROOT . 'themes/webtrees/images',
284
		WT_ROOT . 'themes/webtrees/msie.css',
285
		WT_ROOT . 'themes/webtrees/style.css',
286
		WT_ROOT . 'themes/xenea/favicon.png',
287
		WT_ROOT . 'themes/xenea/images',
288
		WT_ROOT . 'themes/xenea/msie.css',
289
		WT_ROOT . 'themes/xenea/style.css',
290
		// Removed in 1.5.1
291
		WT_ROOT . 'themes/_administration/css-1.5.0',
292
		WT_ROOT . 'themes/clouds/css-1.5.0',
293
		WT_ROOT . 'themes/colors/css-1.5.0',
294
		WT_ROOT . 'themes/fab/css-1.5.0',
295
		WT_ROOT . 'themes/minimal/css-1.5.0',
296
		WT_ROOT . 'themes/webtrees/css-1.5.0',
297
		WT_ROOT . 'themes/xenea/css-1.5.0',
298
		// Removed in 1.5.2
299
		WT_ROOT . 'themes/_administration/css-1.5.1',
300
		WT_ROOT . 'themes/clouds/css-1.5.1',
301
		WT_ROOT . 'themes/colors/css-1.5.1',
302
		WT_ROOT . 'themes/fab/css-1.5.1',
303
		WT_ROOT . 'themes/minimal/css-1.5.1',
304
		WT_ROOT . 'themes/webtrees/css-1.5.1',
305
		WT_ROOT . 'themes/xenea/css-1.5.1',
306
		// Removed in 1.5.3
307
		WT_ROOT . 'modules_v3/GEDFact_assistant/_CENS/census_asst_help.php',
308
		WT_ROOT . 'modules_v3/googlemap/admin_places.php',
309
		WT_ROOT . 'modules_v3/googlemap/defaultconfig.php',
310
		WT_ROOT . 'modules_v3/googlemap/googlemap.php',
311
		WT_ROOT . 'modules_v3/googlemap/placehierarchy.php',
312
		WT_ROOT . 'modules_v3/googlemap/places_edit.php',
313
		WT_ROOT . 'modules_v3/googlemap/util.js',
314
		WT_ROOT . 'modules_v3/googlemap/wt_v3_places_edit.js.php',
315
		WT_ROOT . 'modules_v3/googlemap/wt_v3_places_edit_overlays.js.php',
316
		WT_ROOT . 'modules_v3/googlemap/wt_v3_street_view.php',
317
		WT_ROOT . 'readme.html',
318
		WT_ROOT . 'themes/_administration/css-1.5.2',
319
		WT_ROOT . 'themes/clouds/css-1.5.2',
320
		WT_ROOT . 'themes/colors/css-1.5.2',
321
		WT_ROOT . 'themes/fab/css-1.5.2',
322
		WT_ROOT . 'themes/minimal/css-1.5.2',
323
		WT_ROOT . 'themes/webtrees/css-1.5.2',
324
		WT_ROOT . 'themes/xenea/css-1.5.2',
325
		// Removed in 1.6.0
326
		WT_ROOT . 'downloadbackup.php',
327
		WT_ROOT . 'modules_v3/ckeditor/ckeditor-4.3.2-custom',
328
		WT_ROOT . 'site-php-version.php',
329
		WT_ROOT . 'themes/_administration/css-1.5.3',
330
		WT_ROOT . 'themes/clouds/css-1.5.3',
331
		WT_ROOT . 'themes/colors/css-1.5.3',
332
		WT_ROOT . 'themes/fab/css-1.5.3',
333
		WT_ROOT . 'themes/minimal/css-1.5.3',
334
		WT_ROOT . 'themes/webtrees/css-1.5.3',
335
		WT_ROOT . 'themes/xenea/css-1.5.3',
336
		// Removed in 1.6.2
337
		WT_ROOT . 'themes/_administration/css-1.6.0',
338
		WT_ROOT . 'themes/_administration/jquery-ui-1.10.3',
339
		WT_ROOT . 'themes/clouds/css-1.6.0',
340
		WT_ROOT . 'themes/clouds/jquery-ui-1.10.3',
341
		WT_ROOT . 'themes/colors/css-1.6.0',
342
		WT_ROOT . 'themes/colors/jquery-ui-1.10.3',
343
		WT_ROOT . 'themes/fab/css-1.6.0',
344
		WT_ROOT . 'themes/fab/jquery-ui-1.10.3',
345
		WT_ROOT . 'themes/minimal/css-1.6.0',
346
		WT_ROOT . 'themes/minimal/jquery-ui-1.10.3',
347
		WT_ROOT . 'themes/webtrees/css-1.6.0',
348
		WT_ROOT . 'themes/webtrees/jquery-ui-1.10.3',
349
		WT_ROOT . 'themes/xenea/css-1.6.0',
350
		WT_ROOT . 'themes/xenea/jquery-ui-1.10.3',
351
		WT_ROOT . 'themes/_administration/css-1.6.0',
352
		WT_ROOT . 'themes/_administration/jquery-ui-1.10.3',
353
		// Removed in 1.7.0
354
		WT_ROOT . 'admin_site_other.php',
355
		WT_ROOT . 'js',
356
		WT_ROOT . 'language/en_GB.mo',
357
		// Replaced with en-GB.mo
358
		WT_ROOT . 'language/en_US.mo',
359
		// Replaced with en-US.mo
360
		WT_ROOT . 'language/pt_BR.mo',
361
		// Replaced with pt-BR.mo
362
		WT_ROOT . 'language/zh_CN.mo',
363
		// Replaced with zh-Hans.mo
364
		WT_ROOT . 'language/extra',
365
		WT_ROOT . 'library',
366
		WT_ROOT . 'modules_v3/batch_update/admin_batch_update.php',
367
		WT_ROOT . 'modules_v3/batch_update/plugins',
368
		WT_ROOT . 'modules_v3/charts/help_text.php',
369
		WT_ROOT . 'modules_v3/ckeditor/ckeditor-4.4.1-custom',
370
		WT_ROOT . 'modules_v3/clippings/clippings_ctrl.php',
371
		WT_ROOT . 'modules_v3/clippings/help_text.php',
372
		WT_ROOT . 'modules_v3/faq/help_text.php',
373
		WT_ROOT . 'modules_v3/gedcom_favorites/db_schema',
374
		WT_ROOT . 'modules_v3/gedcom_news/db_schema',
375
		WT_ROOT . 'modules_v3/googlemap/db_schema',
376
		WT_ROOT . 'modules_v3/googlemap/help_text.php',
377
		WT_ROOT . 'modules_v3/html/help_text.php',
378
		WT_ROOT . 'modules_v3/logged_in/help_text.php',
379
		WT_ROOT . 'modules_v3/review_changes/help_text.php',
380
		WT_ROOT . 'modules_v3/todo/help_text.php',
381
		WT_ROOT . 'modules_v3/tree/class_treeview.php',
382
		WT_ROOT . 'modules_v3/user_blog/db_schema',
383
		WT_ROOT . 'modules_v3/yahrzeit/help_text.php',
384
		WT_ROOT . 'save.php',
385
		WT_ROOT . 'themes/_administration/css-1.6.2',
386
		WT_ROOT . 'themes/_administration/templates',
387
		WT_ROOT . 'themes/_administration/header.php',
388
		WT_ROOT . 'themes/_administration/footer.php',
389
		WT_ROOT . 'themes/clouds/css-1.6.2',
390
		WT_ROOT . 'themes/clouds/templates',
391
		WT_ROOT . 'themes/clouds/header.php',
392
		WT_ROOT . 'themes/clouds/footer.php',
393
		WT_ROOT . 'themes/colors/css-1.6.2',
394
		WT_ROOT . 'themes/colors/templates',
395
		WT_ROOT . 'themes/colors/header.php',
396
		WT_ROOT . 'themes/colors/footer.php',
397
		WT_ROOT . 'themes/fab/css-1.6.2',
398
		WT_ROOT . 'themes/fab/templates',
399
		WT_ROOT . 'themes/fab/header.php',
400
		WT_ROOT . 'themes/fab/footer.php',
401
		WT_ROOT . 'themes/minimal/css-1.6.2',
402
		WT_ROOT . 'themes/minimal/templates',
403
		WT_ROOT . 'themes/minimal/header.php',
404
		WT_ROOT . 'themes/minimal/footer.php',
405
		WT_ROOT . 'themes/webtrees/css-1.6.2',
406
		WT_ROOT . 'themes/webtrees/templates',
407
		WT_ROOT . 'themes/webtrees/header.php',
408
		WT_ROOT . 'themes/webtrees/footer.php',
409
		WT_ROOT . 'themes/xenea/css-1.6.2',
410
		WT_ROOT . 'themes/xenea/templates',
411
		WT_ROOT . 'themes/xenea/header.php',
412
		WT_ROOT . 'themes/xenea/footer.php',
413
		// Removed in 1.7.2
414
		WT_ROOT . 'assets/js-1.7.0',
415
		// Removed in 1.7.3
416
		WT_ROOT . 'modules_v3/GEDFact_assistant/census/date.js',
417
		WT_ROOT . 'modules_v3/GEDFact_assistant/census/dynamicoptionlist.js',
418
		// Removed in 1.7.4
419
		WT_ROOT . 'assets/js-1.7.2',
420
		WT_ROOT . 'themes/_administration/css-1.7.0',
421
		WT_ROOT . 'themes/clouds/css-1.7.0',
422
		WT_ROOT . 'themes/colors/css-1.7.0',
423
		WT_ROOT . 'themes/fab/css-1.7.0',
424
		WT_ROOT . 'themes/minimal/css-1.7.0',
425
		WT_ROOT . 'themes/webtrees/css-1.7.0',
426
		WT_ROOT . 'themes/xenea/css-1.7.0',
427
		// Removed in 1.7.5
428
		WT_ROOT . 'themes/_administration/css-1.7.4',
429
		WT_ROOT . 'themes/clouds/css-1.7.4',
430
		WT_ROOT . 'themes/colors/css-1.7.4',
431
		WT_ROOT . 'themes/fab/css-1.7.4',
432
		WT_ROOT . 'themes/minimal/css-1.7.4',
433
		WT_ROOT . 'themes/webtrees/css-1.7.4',
434
		WT_ROOT . 'themes/xenea/css-1.7.4',
435
		// Removed in 1.7.7
436
		WT_ROOT . 'assets/js-1.7.4',
437
		WT_ROOT . 'modules_v3/googlemap/images/css_sprite_facts.png',
438
		WT_ROOT . 'modules_v3/googlemap/images/flag_shadow.png',
439
		WT_ROOT . 'modules_v3/googlemap/images/shadow-left-large.png',
440
		WT_ROOT . 'modules_v3/googlemap/images/shadow-left-small.png',
441
		WT_ROOT . 'modules_v3/googlemap/images/shadow-right-large.png',
442
		WT_ROOT . 'modules_v3/googlemap/images/shadow-right-small.png',
443
		WT_ROOT . 'modules_v3/googlemap/images/shadow50.png',
444
		WT_ROOT . 'modules_v3/googlemap/images/transparent-left-large.png',
445
		WT_ROOT . 'modules_v3/googlemap/images/transparent-left-small.png',
446
		WT_ROOT . 'modules_v3/googlemap/images/transparent-right-large.png',
447
		WT_ROOT . 'modules_v3/googlemap/images/transparent-right-small.png',
448
		// Removed in 1.7.8
449
		WT_ROOT . 'themes/clouds/css-1.7.5',
450
		WT_ROOT . 'themes/colors/css-1.7.5',
451
		WT_ROOT . 'themes/fab/css-1.7.5',
452
		WT_ROOT . 'themes/minimal/css-1.7.5',
453
		WT_ROOT . 'themes/webtrees/css-1.7.5',
454
		WT_ROOT . 'themes/xenea/css-1.7.5',
455
		// Removed in 2.0.0
456
		WT_ROOT . 'addmedia.php',
457
		WT_ROOT . 'admin_media.php',
458
		WT_ROOT . 'admin_media_upload.php',
459
		WT_ROOT . 'admin_module_blocks.php',
460
		WT_ROOT . 'admin_module_charts.php',
461
		WT_ROOT . 'admin_module_menus.php',
462
		WT_ROOT . 'admin_module_reports.php',
463
		WT_ROOT . 'admin_module_sidebar.php',
464
		WT_ROOT . 'admin_module_tabs.php',
465
		WT_ROOT . 'admin_modules.php',
466
		WT_ROOT . 'admin_site_access.php',
467
		WT_ROOT . 'admin_site_clean.php',
468
		WT_ROOT . 'admin_site_info.php',
469
		WT_ROOT . 'admin_site_merge.php',
470
		WT_ROOT . 'admin_site_readme.php',
471
		WT_ROOT . 'admin_trees_check.php',
472
		WT_ROOT . 'admin_trees_config.php',
473
		WT_ROOT . 'admin_trees_download.php',
474
		WT_ROOT . 'admin_trees_duplicates.php',
475
		WT_ROOT . 'admin_trees_export.php',
476
		WT_ROOT . 'admin_trees_manage.php',
477
		WT_ROOT . 'admin_trees_merge.php',
478
		WT_ROOT . 'admin_trees_places.php',
479
		WT_ROOT . 'admin_trees_renumber.php',
480
		WT_ROOT . 'admin_trees_unconnected.php',
481
		WT_ROOT . 'admin_users_bulk.php',
482
		WT_ROOT . 'assets/js-1.7.7',
483
		WT_ROOT . 'autocomplete.php',
484
		WT_ROOT . 'block_edit.php',
485
		WT_ROOT . 'branches.php',
486
		WT_ROOT . 'compact.php',
487
		WT_ROOT . 'data/html_purifier_cache',
488
		WT_ROOT . 'descendancy.php',
489
		WT_ROOT . 'edituser.php',
490
		WT_ROOT . 'edit_changes.php',
491
		WT_ROOT . 'expand_view.php',
492
		WT_ROOT . 'familybook.php',
493
		WT_ROOT . 'famlist.php',
494
		WT_ROOT . 'fanchart.php',
495
		WT_ROOT . 'help_text.php',
496
		WT_ROOT . 'hourglass.php',
497
		WT_ROOT . 'hourglass_ajax.php',
498
		WT_ROOT . 'import.php',
499
		WT_ROOT . 'index_edit.php',
500
		WT_ROOT . 'indilist.php',
501
		WT_ROOT . 'lifespan.php',
502
		WT_ROOT . 'login.php',
503
		WT_ROOT . 'logout.php',
504
		WT_ROOT . 'medialist.php',
505
		WT_ROOT . 'message.php',
506
		WT_ROOT . 'notelist.php',
507
		WT_ROOT . 'packages',
508
		WT_ROOT . 'pedigree.php',
509
		WT_ROOT . 'relationship.php',
510
		WT_ROOT . 'repolist.php',
511
		WT_ROOT . 'reportengine.php',
512
		WT_ROOT . 'search.php',
513
		WT_ROOT . 'search_advanced.php',
514
		WT_ROOT . 'site-offline.php',
515
		WT_ROOT . 'site-unavailable.php',
516
		WT_ROOT . 'sourcelist.php',
517
		WT_ROOT . 'statistics.php',
518
		WT_ROOT . 'statisticsplot.php',
519
		WT_ROOT . 'themes/_administration/css-1.7.5',
520
		WT_ROOT . 'themes/_administration/jquery-ui-1.11.2',
521
		WT_ROOT . 'themes/clouds/css-1.7.8',
522
		WT_ROOT . 'themes/clouds/jquery-ui-1.11.2',
523
		WT_ROOT . 'themes/colors/css-1.7.8',
524
		WT_ROOT . 'themes/colors/jquery-ui-1.11.2',
525
		WT_ROOT . 'themes/fab/css-1.7.8',
526
		WT_ROOT . 'themes/fab/jquery-ui-1.11.2',
527
		WT_ROOT . 'themes/minimal/css-1.7.8',
528
		WT_ROOT . 'themes/minimal/jquery-ui-1.11.2',
529
		WT_ROOT . 'themes/webtrees/css-1.7.8',
530
		WT_ROOT . 'themes/webtrees/jquery-ui-1.11.2',
531
		WT_ROOT . 'themes/xenea/css-1.7.8',
532
		WT_ROOT . 'themes/xenea/jquery-ui-1.11.2',
533
		WT_ROOT . 'timeline.php',
534
	];
535
536
	protected $layout = 'layouts/administration';
537
538
	/**
539
	 * Show the admin page for blocks.
540
	 *
541
	 * @return Response
542
	 */
543
	public function blocks(): Response {
544
		return $this->components('block', 'blocks', I18N::translate('Block'), I18N::translate('Blocks'));
545
	}
546
547
	/**
548
	 * Show the admin page for charts.
549
	 *
550
	 * @return Response
551
	 */
552
	public function charts(): Response {
553
		return $this->components('chart', 'charts', I18N::translate('Chart'), I18N::translate('Charts'));
554
	}
555
556
	/**
557
	 * The control panel shows a summary of the site and links to admin functions.
558
	 *
559
	 * @return Response
560
	 */
561
	public function controlPanel(): Response {
562
		return $this->viewResponse('admin/control-panel', [
563
			'title'           => I18N::translate('Control panel'),
564
			'server_warnings' => $this->serverWarnings(),
565
			'latest_version'  => $this->latestVersion(),
566
			'all_users'       => User::all(),
567
			'administrators'  => User::administrators(),
568
			'managers'        => User::managers(),
569
			'moderators'      => User::moderators(),
570
			'unapproved'      => User::unapproved(),
571
			'unverified'      => User::unverified(),
572
			'all_trees'       => Tree::getAll(),
573
			'changes'         => $this->totalChanges(),
574
			'individuals'     => $this->totalIndividuals(),
575
			'families'        => $this->totalFamilies(),
576
			'sources'         => $this->totalSources(),
577
			'media'           => $this->totalMediaObjects(),
578
			'repositories'    => $this->totalRepositories(),
579
			'notes'           => $this->totalNotes(),
580
			'files_to_delete' => $this->filesToDelete(),
581
			'all_modules'     => Module::getInstalledModules('disabled'),
582
			'deleted_modules' => $this->deletedModuleNames(),
583
			'config_modules'  => Module::configurableModules(),
584
		]);
585
	}
586
587
	/**
588
	 * Managers see a restricted version of the contol panel.
589
	 *
590
	 * @return Response
591
	 */
592
	public function controlPanelManager(): Response {
593
		$all_trees = array_filter(Tree::getAll(), function (Tree $tree) {
594
			return Auth::isManager($tree);
595
		});
596
597
		return $this->viewResponse('admin/control-panel-manager', [
598
			'title'        => I18N::translate('Control panel'),
599
			'all_trees'    => $all_trees,
600
			'changes'      => $this->totalChanges(),
601
			'individuals'  => $this->totalIndividuals(),
602
			'families'     => $this->totalFamilies(),
603
			'sources'      => $this->totalSources(),
604
			'media'        => $this->totalMediaObjects(),
605
			'repositories' => $this->totalRepositories(),
606
			'notes'        => $this->totalNotes(),
607
		]);
608
	}
609
610
	/**
611
	 * Show the edit history for a tree.
612
	 *
613
	 * @param Request $request
614
	 *
615
	 * @return Response
616
	 */
617
	public function changesLog(Request $request): Response {
618
		$tree_list = [];
619
		foreach (Tree::getAll() as $tree) {
620
			if (Auth::isManager($tree)) {
621
				$tree_list[$tree->getName()] = $tree->getTitle();
622
			}
623
		}
624
625
		$user_list = ['' => ''];
626
		foreach (User::all() as $tmp_user) {
627
			$user_list[$tmp_user->getUserName()] = $tmp_user->getUserName();
628
		}
629
630
		// First and last change in the database.
631
		$earliest = Database::prepare("SELECT IFNULL(DATE(MIN(change_time)), CURDATE()) FROM `##change`")->fetchOne();
632
		$latest   = Database::prepare("SELECT IFNULL(DATE(MAX(change_time)), CURDATE()) FROM `##change`")->fetchOne();
633
634
		$action   = $request->get('action');
635
		$ged      = $request->get('ged');
636
		$from     = $request->get('from', $earliest);
637
		$to       = $request->get('to', $latest);
638
		$type     = $request->get('type', '');
639
		$oldged   = $request->get('oldged', '');
640
		$newged   = $request->get('newged', '');
641
		$xref     = $request->get('xref', '');
642
		$username = $request->get('username', '');
643
		$search   = $request->get('search', []);
644
		$search   = $search['value'] ?? null;
645
646
		if (!array_key_exists($ged, $tree_list)) {
647
			$ged = reset($tree_list);
648
		}
649
650
		$statuses = [
651
			''         => '',
652
			'accepted' => /* I18N: the status of an edit accepted/rejected/pending */
653
				I18N::translate('accepted'),
654
			'rejected' => /* I18N: the status of an edit accepted/rejected/pending */
655
				I18N::translate('rejected'),
656
			'pending'  => /* I18N: the status of an edit accepted/rejected/pending */
657
				I18N::translate('pending'),
658
		];
659
660
		return $this->viewResponse('admin/changes-log', [
661
			'action'    => $action,
662
			'earliest'  => $earliest,
663
			'from'      => $from,
664
			'ged'       => $ged,
665
			'latest'    => $latest,
666
			'newged'    => $newged,
667
			'oldged'    => $oldged,
668
			'search'    => $search,
669
			'statuses'  => $statuses,
670
			'title'     => I18N::translate('Changes log'),
671
			'to'        => $to,
672
			'tree_list' => $tree_list,
673
			'type'      => $type,
674
			'username'  => $username,
675
			'user_list' => $user_list,
676
			'xref'      => $xref,
677
		]);
678
	}
679
680
	/**
681
	 * Show the edit history for a tree.
682
	 *
683
	 * @param Request $request
684
	 *
685
	 * @return Response
686
	 */
687
	public function changesLogData(Request $request): Response {
688
		list($select, , $where, $args1) = $this->changesQuery($request);
689
		list($order_by, $limit, $args2) = $this->dataTablesPagination($request);
690
691
		$rows = Database::prepare(
692
			$select . $where . $order_by . $limit
693
		)->execute(array_merge($args1, $args2))->fetchAll();
694
695
		// Total filtered/unfiltered rows
696
		$recordsFiltered = (int) Database::prepare("SELECT FOUND_ROWS()")->fetchOne();
697
		$recordsTotal    = (int) Database::prepare("SELECT COUNT(*) FROM `##change`")->fetchOne();
698
699
		$data      = [];
700
		$algorithm = new MyersDiff;
701
702
		foreach ($rows as $row) {
703
			$old_lines = preg_split('/[\n]+/', $row->old_gedcom, -1, PREG_SPLIT_NO_EMPTY);
704
			$new_lines = preg_split('/[\n]+/', $row->new_gedcom, -1, PREG_SPLIT_NO_EMPTY);
705
706
			$differences = $algorithm->calculate($old_lines, $new_lines);
0 ignored issues
show
Bug introduced by
It seems like $new_lines can also be of type false; however, parameter $b of Fisharebest\Algorithm\MyersDiff::calculate() 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

706
			$differences = $algorithm->calculate($old_lines, /** @scrutinizer ignore-type */ $new_lines);
Loading history...
Bug introduced by
It seems like $old_lines can also be of type false; however, parameter $a of Fisharebest\Algorithm\MyersDiff::calculate() 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

706
			$differences = $algorithm->calculate(/** @scrutinizer ignore-type */ $old_lines, $new_lines);
Loading history...
707
			$diff_lines  = [];
708
709
			foreach ($differences as $difference) {
710
				switch ($difference[1]) {
711
					case MyersDiff::DELETE:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
712
						$diff_lines[] = '<del>' . $difference[0] . '</del>';
713
						break;
714
					case MyersDiff::INSERT:
715
						$diff_lines[] = '<ins>' . $difference[0] . '</ins>';
716
						break;
717
					default:
718
						$diff_lines[] = $difference[0];
719
				}
720
			}
721
722
			// Only convert valid xrefs to links
723
			$tree   = Tree::findByName($row->gedcom_name);
724
			$record = GedcomRecord::getInstance($row->xref, $tree);
725
			$data[] = [
726
				$row->change_id,
727
				$row->change_time,
728
				I18N::translate($row->status),
729
				$record ? '<a href="' . e($record->url()) . '">' . $record->getXref() . '</a>' : $row->xref,
730
				'<div class="gedcom-data" dir="ltr">' .
731
				preg_replace_callback('/@(' . WT_REGEX_XREF . ')@/',
732
					function ($match) use ($tree) {
733
						$record = GedcomRecord::getInstance($match[1], $tree);
734
735
						return $record ? '<a href="' . e($record->url()) . '">' . $match[0] . '</a>' : $match[0];
736
					},
737
					implode("\n", $diff_lines)
738
				) .
739
				'</div>',
740
				$row->user_name,
741
				$row->gedcom_name,
742
			];
743
		}
744
745
		// See http://www.datatables.net/usage/server-side
746
		return new JsonResponse([
747
			'draw'            => (int) $request->get('draw'),
748
			'recordsTotal'    => $recordsTotal,
749
			'recordsFiltered' => $recordsFiltered,
750
			'data'            => $data,
751
		]);
752
	}
753
754
	/**
755
	 * Show the edit history for a tree.
756
	 *
757
	 * @param Request $request
758
	 *
759
	 * @return Response
760
	 */
761
	public function changesLogDownload(Request $request): Response {
762
		list($select, , $where, $args) = $this->changesQuery($request);
763
764
		$rows = Database::prepare($select . $where)->execute($args)->fetchAll();
765
766
		// Convert to CSV
767
		$rows = array_map(function (stdClass $row) {
768
			return implode(',', [
769
				'"' . $row->change_time . '"',
770
				'"' . $row->status . '"',
771
				'"' . $row->xref . '"',
772
				'"' . strtr($row->old_gedcom, '"', '""') . '"',
773
				'"' . strtr($row->new_gedcom, '"', '""') . '"',
774
				'"' . strtr($row->user_name, '"', '""') . '"',
775
				'"' . strtr($row->gedcom_name, '"', '""') . '"',
776
			]);
777
		}, $rows);
778
779
		$response    = new Response(implode("\n", $rows));
780
		$disposition = $response->headers->makeDisposition(
781
			ResponseHeaderBag::DISPOSITION_ATTACHMENT, 'changes.csv'
782
		);
783
		$response->headers->set('Content-Disposition', $disposition);
784
		$response->headers->set('Content-Type', 'text/csv; charset=UTF-8');
785
786
		return $response;
787
	}
788
789
	/**
790
	 * Delete the database settings for a deleted module.
791
	 *
792
	 * @param Request $request
793
	 *
794
	 * @return RedirectResponse
795
	 */
796
	public function deleteModuleSettings(Request $request): RedirectResponse {
797
		$module_name = $request->get('module_name');
798
799
		Database::prepare(
800
			"DELETE `##block_setting` FROM `##block_setting` JOIN `##block` USING (block_id) JOIN `##module` USING (module_name) WHERE module_name = :module_name"
801
		)->execute([
802
			'module_name' => $module_name,
803
		]);
804
805
		Database::prepare(
806
			"DELETE `##block` FROM `##block` JOIN `##module` USING (module_name) WHERE module_name = :module_name"
807
		)->execute([
808
			'module_name' => $module_name,
809
		]);
810
811
		Database::prepare(
812
			"DELETE FROM `##module_setting` WHERE module_name = :module_name"
813
		)->execute([
814
			'module_name' => $module_name,
815
		]);
816
817
		Database::prepare(
818
			"DELETE FROM `##module_privacy` WHERE module_name = :module_name"
819
		)->execute([
820
			'module_name' => $module_name,
821
		]);
822
823
		Database::prepare(
824
			"DELETE FROM `##module` WHERE module_name = :module_name"
825
		)->execute([
826
			'module_name' => $module_name,
827
		]);
828
829
		FlashMessages::addMessage(I18N::translate('The preferences for the module “%s” have been deleted.', $module_name), 'success');
830
831
		return new RedirectResponse(route('admin-modules'));
832
	}
833
834
	/**
835
	 * If media objects are wronly linked to top-level records, reattach them
836
	 * to facts/events.
837
	 *
838
	 * @return Response
839
	 */
840
	public function fixLevel0Media(): Response {
841
		return $this->viewResponse('admin/fix-level-0-media', [
842
			'title' => I18N::translate('Link media objects to facts and events'),
843
		]);
844
	}
845
846
	/**
847
	 * Move a link to a media object from a level 0 record to a level 1 record.
848
	 *
849
	 * @param Request $request
850
	 *
851
	 * @return Response
852
	 */
853
	public function fixLevel0MediaAction(Request $request): Response {
854
		$fact_id   = $request->get('fact_id');
855
		$indi_xref = $request->get('indi_xref');
856
		$obje_xref = $request->get('obje_xref');
857
		$tree_id   = $request->get('tree_id');
858
859
		$tree = Tree::findById($tree_id);
860
		if ($tree !== null) {
861
			$individual = Individual::getInstance($indi_xref, $tree);
862
			$media      = Media::getInstance($obje_xref, $tree);
863
			if ($individual !== null && $media !== null) {
864
				foreach ($individual->getFacts() as $fact1) {
865
					if ($fact1->getFactId() === $fact_id) {
866
						$individual->updateFact($fact_id, $fact1->getGedcom() . "\n2 OBJE @" . $obje_xref . '@', false);
867
						foreach ($individual->getFacts('OBJE') as $fact2) {
868
							if ($fact2->getTarget() === $media) {
869
								$individual->deleteFact($fact2->getFactId(), false);
870
							}
871
						}
872
						break;
873
					}
874
				}
875
			}
876
		}
877
878
		return new Response;
879
	}
880
881
	/**
882
	 * If media objects are wronly linked to top-level records, reattach them
883
	 * to facts/events.
884
	 *
885
	 * @param Request $request
886
	 *
887
	 * @return JsonResponse
888
	 */
889
	public function fixLevel0MediaData(Request $request): JsonResponse {
890
		$ignore_facts = ['FAMC', 'FAMS', 'NAME', 'SEX', 'CHAN', 'NOTE', 'OBJE', 'SOUR', 'RESN'];
891
892
		$start  = (int) $request->get('start', 0);
893
		$length = (int) $request->get('length', 20);
894
		$search = $request->get('search', []);
895
		$search = $search['value'] ?? '';
896
897
		$select1 = "SELECT SQL_CACHE SQL_CALC_FOUND_ROWS m.*, i.* from `##media` AS m" .
898
			" JOIN `##media_file` USING (m_id, m_file)" .
899
			" JOIN `##link` AS l ON m.m_file = l.l_file AND m.m_id = l.l_to" .
900
			" JOIN `##individuals` AS i ON l.l_file = i.i_file AND l.l_from = i.i_id" .
901
			" WHERE i.i_gedcom LIKE CONCAT('%\n1 OBJE @', m.m_id, '@%')";
902
903
		$select2 = "SELECT SQL_CACHE SQL_CALC_FOUND_ROWS count(*) from `##media` AS m" .
904
			" JOIN `##media_file` USING (m_id, m_file)" .
905
			" JOIN `##link` AS l ON m.m_file = l.l_file AND m.m_id = l.l_to" .
906
			" JOIN `##individuals` AS i ON l.l_file = i.i_file AND l.l_from = i.i_id" .
907
			" WHERE i.i_gedcom LIKE CONCAT('%\n1 OBJE @', m.m_id, '@%')";
908
909
		$where = '';
910
		$args  = [];
911
912
		if ($search !== '') {
913
			$where           .= " AND (multimedia_file_refn LIKE CONCAT('%', :search1, '%') OR multimedia_file_refn LIKE CONCAT('%', :search2, '%'))";
914
			$args['search1'] = $search;
915
			$args['search2'] = $search;
916
		}
917
918
		$limit          = " LIMIT :limit OFFSET :offset";
919
		$args['limit']  = $length;
920
		$args['offset'] = $start;
921
922
		// Need a consistent order
923
		$order_by = " ORDER BY i.i_file, i.i_id, m.m_id";
924
925
		$data = Database::prepare(
926
			$select1 . $where . $order_by . $limit
927
		)->execute(
928
			$args
929
		)->fetchAll();
930
931
		// Total filtered/unfiltered rows
932
		$recordsFiltered = (int) Database::prepare("SELECT FOUND_ROWS()")->fetchOne();
933
		$recordsTotal    = (int) Database::prepare($select2)->fetchOne();
934
935
		// Turn each row from the query into a row for the table
936
		$data = array_map(function (stdClass $datum) use ($ignore_facts) {
937
			$tree       = Tree::findById($datum->m_file);
938
			$media      = Media::getInstance($datum->m_id, $tree, $datum->m_gedcom);
939
			$individual = Individual::getInstance($datum->i_id, $tree, $datum->i_gedcom);
940
941
			$facts = $individual->getFacts(null, true);
942
			$facts = array_filter($facts, function (Fact $fact) use ($ignore_facts) {
943
				return !$fact->isPendingDeletion() && !in_array($fact->getTag(), $ignore_facts);
944
			});
945
946
			// The link to the media object may have been deleted in a pending change.
947
			$deleted = true;
948
			foreach ($individual->getFacts('OBJE') as $fact) {
949
				if ($fact->getTarget() === $media && !$fact->isPendingDeletion()) {
950
					$deleted = false;
951
				}
952
			}
953
			if ($deleted) {
954
				$facts = [];
955
			}
956
957
			$facts = array_map(function (Fact $fact) use ($individual, $media, $tree) {
0 ignored issues
show
Unused Code introduced by
The import $tree is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
958
				return view('admin/fix-level-0-media-action', [
959
					'fact'       => $fact,
960
					'individual' => $individual,
961
					'media'      => $media,
962
				]);
963
			}, $facts);
964
965
			return [
966
				$tree->getName(),
967
				$media->displayImage(100, 100, 'fit', ['class' => 'img-thumbnail']),
968
				'<a href="' . e($media->url()) . '">' . $media->getFullName() . '</a>',
969
				'<a href="' . e($individual->url()) . '">' . $individual->getFullName() . '</a>',
970
				implode(' ', $facts),
971
			];
972
		}, $data);
973
974
		return new JsonResponse([
975
			'draw'            => (int) $request->get('draw'),
976
			'recordsTotal'    => $recordsTotal,
977
			'recordsFiltered' => $recordsFiltered,
978
			'data'            => $data,
979
		]);
980
	}
981
982
	/**
983
	 * Import custom thumbnails from webtres 1.x.
984
	 *
985
	 * @return Response
986
	 */
987
	public function webtrees1Thumbnails(): Response {
988
		return $this->viewResponse('admin/webtrees1-thumbnails', [
989
			'title' => I18N::translate('Import custom thumbnails from webtrees version 1'),
990
		]);
991
	}
992
993
	/**
994
	 * Import custom thumbnails from webtres 1.x.
995
	 *
996
	 * @param Request $request
997
	 *
998
	 * @return Response
999
	 */
1000
	public function webtrees1ThumbnailsAction(Request $request): Response {
1001
		$thumbnail = $request->get('thumbnail', '');
1002
		$action    = $request->get('action', '');
1003
		$xrefs     = $request->get('xref', []);
1004
		$geds      = $request->get('ged', []);
1005
1006
		$media_objects = [];
1007
1008
		foreach ($xrefs as $key => $xref) {
1009
			$tree            = Tree::findByName($geds[$key]);
1010
			$media_objects[] = Media::getInstance($xref, $tree);
1011
		}
1012
1013
		$thumbnail = WT_DATA_DIR . $thumbnail;
1014
1015
		switch ($action) {
1016
			case 'delete':
1017
				if (file_exists($thumbnail)) {
1018
					unlink($thumbnail);
1019
				}
1020
				break;
1021
1022
			case 'add':
1023
				$image_size = getimagesize($thumbnail);
1024
				list(, $extension) = explode('/', $image_size['mime']);
1025
				$move_to = dirname(dirname($thumbnail)) . '/' . sha1_file($thumbnail) . '.' . $extension;
1026
				rename($thumbnail, $move_to);
1027
1028
				foreach ($media_objects as $media_object) {
1029
					$prefix = WT_DATA_DIR . $media_object->getTree()->getPreference('MEDIA_DIRECTORY');
1030
					$gedcom = "1 FILE " . substr($move_to, strlen($prefix)) . "\n2 FORM " . $extension;
1031
1032
					if ($media_object->firstImageFile() === null) {
1033
						// The media object doesn't have an image.  Add this as a secondary file.
1034
						$media_object->createFact($gedcom, true);
1035
					} else {
1036
						// The media object already has an image.  Show this custom one in preference.
1037
						$gedcom = '0 @' . $media_object->getXref() . "@ OBJE\n" . $gedcom;
1038
						foreach ($media_object->getFacts() as $fact) {
1039
							$gedcom .= "\n" . $fact->getGedcom();
1040
						}
1041
						$media_object->updateRecord($gedcom, true);
1042
					}
1043
1044
					// Accept the changes, to keep the filesystem in sync with the GEDCOM data.
1045
					FunctionsImport::acceptAllChanges($media_object->getxref(), $media_object->getTree()->getTreeId());
1046
				}
1047
				break;
1048
		}
1049
1050
		return new JsonResponse([]);
1051
	}
1052
1053
	/**
1054
	 * Import custom thumbnails from webtres 1.x.
1055
	 *
1056
	 * @param Request $request
1057
	 *
1058
	 * @return JsonResponse
1059
	 */
1060
	public function webtrees1ThumbnailsData(Request $request): JsonResponse {
1061
		$start  = (int) $request->get('start', 0);
1062
		$length = (int) $request->get('length', 20);
1063
		$search = $request->get('search', []);
1064
		$search = $search['value'] ?? '';
1065
1066
		// Fetch all thumbnails
1067
		$thumbnails = [];
1068
1069
		$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(WT_DATA_DIR, FilesystemIterator::FOLLOW_SYMLINKS));
1070
1071
		foreach ($iterator as $iteration) {
1072
			if ($iteration->isFile() && strpos($iteration->getPathname(), '/thumbs/') !== false) {
1073
				$thumbnails[] = $iteration->getPathname();
1074
			}
1075
		}
1076
1077
		$recordsTotal = count($thumbnails);
1078
1079
		if ($search !== '') {
1080
			$thumbnails = array_filter($thumbnails, function (string $thumbnail) use ($search) {
1081
				return stripos($thumbnail, $search) !== false;
1082
			});
1083
		}
1084
1085
		$recordsFiltered = count($thumbnails);
1086
1087
		$thumbnails = array_slice($thumbnails, $start, $length);
1088
1089
		// Turn each filename into a row for the table
1090
		$data = array_map(function (string $thumbnail) {
1091
			$original = $this->findOriginalFileFromThumbnail($thumbnail);
1092
1093
			$original_url  = route('unused-media-thumbnail', [
1094
				'folder' => dirname($original),
1095
				'file'   => basename($original),
1096
				'w'      => 100,
1097
				'h'      => 100,
1098
			]);
1099
			$thumbnail_url = route('unused-media-thumbnail', [
1100
				'folder' => dirname($thumbnail),
1101
				'file'   => basename($thumbnail),
1102
				'w'      => 100,
1103
				'h'      => 100,
1104
			]);
1105
1106
			$difference = $this->imageDiff($thumbnail, $original);
1107
1108
			$original_path  = substr($original, strlen(WT_DATA_DIR));
1109
			$thumbnail_path = substr($thumbnail, strlen(WT_DATA_DIR));
1110
1111
			$media = $this->findMediaObjectsForMediaFile($original_path);
1112
1113
			$media_links = array_map(function (Media $media) {
1114
				return '<a href="' . e($media->url()) . '">' . $media->getFullName() . '</a>';
1115
			}, $media);
1116
1117
			$media_links = implode('<br>', $media_links);
1118
1119
			$action = view('admin/webtrees1-thumbnails-form', [
1120
				'difference' => $difference,
1121
				'media'      => $media,
1122
				'thumbnail'  => $thumbnail_path,
1123
			]);
1124
1125
			return [
1126
				'<img src="' . e($thumbnail_url) . '" title="' . e($thumbnail_path) . '">',
1127
				'<img src="' . e($original_url) . '" title="' . e($original_path) . '">',
1128
				$media_links,
1129
				I18N::percentage($difference / 100.0, 0),
1130
				$action,
1131
			];
1132
		}, $thumbnails);
1133
1134
		return new JsonResponse([
1135
			'draw'            => (int) $request->get('draw'),
1136
			'recordsTotal'    => $recordsTotal,
1137
			'recordsFiltered' => $recordsFiltered,
1138
			'data'            => $data,
1139
		]);
1140
	}
1141
1142
	/**
1143
	 * Merge two genealogy records.
1144
	 *
1145
	 * @param Request $request
1146
	 *
1147
	 * @return Response
1148
	 */
1149
	public function mergeRecords(Request $request): Response {
1150
		/** @var Tree $tree */
1151
		$tree  = $request->attributes->get('tree');
1152
		$title = I18N::translate('Merge records') . ' — ' . e($tree->getTitle());
1153
1154
		$xref1 = $request->get('xref1', '');
1155
		$xref2 = $request->get('xref2', '');
1156
1157
		$record1 = GedcomRecord::getInstance($xref1, $tree);
1158
		$record2 = GedcomRecord::getInstance($xref2, $tree);
1159
1160
		if ($xref1 !== '' && $record1 === null) {
1161
			$xref1 = '';
1162
		}
1163
1164
		if ($xref2 !== '' && $record2 === null) {
1165
			$xref2 = '';
1166
		}
1167
1168
		if ($record1 === $record2) {
1169
			$xref2 = '';
1170
		}
1171
1172
		if ($record1 !== null && $record2 && $record1::RECORD_TYPE !== $record2::RECORD_TYPE) {
1173
			$xref2 = '';
1174
		}
1175
1176
		if ($xref1 === '' || $xref2 === '') {
1177
			return $this->viewResponse('admin/merge-records-step-1', [
1178
				'individual1' => $record1 instanceof Individual ? $record1 : null,
1179
				'individual2' => $record2 instanceof Individual ? $record2 : null,
1180
				'family1'     => $record1 instanceof Family ? $record1 : null,
1181
				'family2'     => $record2 instanceof Family ? $record2 : null,
1182
				'source1'     => $record1 instanceof Source ? $record1 : null,
1183
				'source2'     => $record2 instanceof Source ? $record2 : null,
1184
				'repository1' => $record1 instanceof Repository ? $record1 : null,
1185
				'repository2' => $record2 instanceof Repository ? $record2 : null,
1186
				'media1'      => $record1 instanceof Media ? $record1 : null,
1187
				'media2'      => $record2 instanceof Media ? $record2 : null,
1188
				'note1'       => $record1 instanceof Note ? $record1 : null,
1189
				'note2'       => $record2 instanceof Note ? $record2 : null,
1190
				'title'       => $title,
1191
			]);
1192
		}
1193
1194
		// Facts found both records
1195
		$facts = [];
1196
		// Facts found in only one record
1197
		$facts1 = [];
1198
		$facts2 = [];
1199
1200
		foreach ($record1->getFacts() as $fact) {
1201
			if (!$fact->isPendingDeletion() && $fact->getTag() !== 'CHAN') {
1202
				$facts1[$fact->getFactId()] = $fact;
1203
			}
1204
		}
1205
1206
		foreach ($record2->getFacts() as $fact) {
1207
			if (!$fact->isPendingDeletion() && $fact->getTag() !== 'CHAN') {
1208
				$facts2[$fact->getFactId()] = $fact;
1209
			}
1210
		}
1211
1212
		foreach ($facts1 as $id1 => $fact1) {
1213
			foreach ($facts2 as $id2 => $fact2) {
1214
				if ($fact1->getFactId() === $fact2->getFactId()) {
1215
					$facts[] = $fact1;
1216
					unset($facts1[$id1]);
1217
					unset($facts2[$id2]);
1218
				}
1219
			}
1220
		}
1221
1222
		return $this->viewResponse('admin/merge-records-step-2', [
1223
			'facts'   => $facts,
1224
			'facts1'  => $facts1,
1225
			'facts2'  => $facts2,
1226
			'record1' => $record1,
1227
			'record2' => $record2,
1228
			'title'   => $title,
1229
		]);
1230
	}
1231
1232
	/**
1233
	 * @param Request $request
1234
	 *
1235
	 * @return Response
1236
	 */
1237
	public function mergeRecordsAction(Request $request): Response {
1238
		/** @var Tree $tree */
1239
		$tree  = $request->attributes->get('tree');
1240
		$xref1 = $request->get('xref1', '');
1241
		$xref2 = $request->get('xref2', '');
1242
		$keep1 = $request->get('keep1', []);
1243
		$keep2 = $request->get('keep2', []);
1244
1245
		$record1 = GedcomRecord::getInstance($xref1, $tree);
1246
		$record2 = GedcomRecord::getInstance($xref2, $tree);
1247
1248
		// Facts found both records
1249
		$facts = [];
1250
		// Facts found in only one record
1251
		$facts1 = [];
1252
		$facts2 = [];
1253
1254
		foreach ($record1->getFacts() as $fact) {
1255
			if (!$fact->isPendingDeletion() && $fact->getTag() !== 'CHAN') {
1256
				$facts1[$fact->getFactId()] = $fact;
1257
			}
1258
		}
1259
1260
		foreach ($record2->getFacts() as $fact) {
1261
			if (!$fact->isPendingDeletion() && $fact->getTag() !== 'CHAN') {
1262
				$facts2[$fact->getFactId()] = $fact;
1263
			}
1264
		}
1265
1266
		// If we are not auto-accepting, then we can show a link to the pending deletion
1267
		if (Auth::user()->getPreference('auto_accept')) {
1268
			$record2_name = $record2->getFullName();
1269
		} else {
1270
			$record2_name = '<a class="alert-link" href="' . e($record2->url()) . '">' . $record2->getFullName() . '</a>';
1271
		}
1272
1273
		// Update records that link to the one we will be removing.
1274
		$ids = FunctionsDb::fetchAllLinks($xref2, $tree->getTreeId());
1275
1276
		foreach ($ids as $id) {
1277
			$record = GedcomRecord::getInstance($id, $tree);
1278
			if (!$record->isPendingDeletion()) {
1279
				FlashMessages::addMessage(I18N::translate(
1280
				/* I18N: The placeholders are the names of individuals, sources, etc. */
1281
					'The link from “%1$s” to “%2$s” has been updated.',
1282
					'<a class="alert-link" href="' . e($record->url()) . '">' . $record->getFullName() . '</a>',
1283
					$record2_name
1284
				), 'info');
1285
				$gedcom = str_replace('@' . $xref2 . '@', '@' . $xref1 . '@', $record->getGedcom());
1286
				$gedcom = preg_replace(
1287
					'/(\n1.*@.+@.*(?:(?:\n[2-9].*)*))((?:\n1.*(?:\n[2-9].*)*)*\1)/',
1288
					'$2',
1289
					$gedcom
1290
				);
1291
				$record->updateRecord($gedcom, true);
1292
			}
1293
		}
1294
1295
		// Update any linked user-accounts
1296
		Database::prepare(
1297
			"UPDATE `##user_gedcom_setting`" .
1298
			" SET setting_value=?" .
1299
			" WHERE gedcom_id=? AND setting_name='gedcomid' AND setting_value=?"
1300
		)->execute([$xref2, $tree->getTreeId(), $xref1]);
1301
1302
		// Merge hit counters
1303
		$hits = Database::prepare(
1304
			"SELECT page_name, SUM(page_count)" .
1305
			" FROM `##hit_counter`" .
1306
			" WHERE gedcom_id=? AND page_parameter IN (?, ?)" .
1307
			" GROUP BY page_name"
1308
		)->execute([$tree->getTreeId(), $xref1, $xref2])->fetchAssoc();
1309
1310
		foreach ($hits as $page_name => $page_count) {
1311
			Database::prepare(
1312
				"UPDATE `##hit_counter` SET page_count=?" .
1313
				" WHERE gedcom_id=? AND page_name=? AND page_parameter=?"
1314
			)->execute([$page_count, $tree->getTreeId(), $page_name, $xref1]);
1315
		}
1316
1317
		Database::prepare(
1318
			"DELETE FROM `##hit_counter`" .
1319
			" WHERE gedcom_id=? AND page_parameter=?"
1320
		)->execute([$tree->getTreeId(), $xref2]);
1321
1322
		$gedcom = '0 @' . $record1->getXref() . '@ ' . $record1::RECORD_TYPE;
1323
		foreach ($facts as $fact_id => $fact) {
1324
			$gedcom .= "\n" . $fact->getGedcom();
1325
		}
1326
		foreach ($facts1 as $fact_id => $fact) {
1327
			if (in_array($fact_id, $keep1)) {
1328
				$gedcom .= "\n" . $fact->getGedcom();
1329
			}
1330
		}
1331
		foreach ($facts2 as $fact_id => $fact) {
1332
			if (in_array($fact_id, $keep2)) {
1333
				$gedcom .= "\n" . $fact->getGedcom();
1334
			}
1335
		}
1336
1337
		$record1->updateRecord($gedcom, true);
1338
		$record2->deleteRecord();
1339
		FunctionsDb::updateFavorites($xref2, $xref1, $tree);
1340
		FlashMessages::addMessage(I18N::translate(
1341
		/* I18N: Records are individuals, sources, etc. */
1342
			'The records “%1$s” and “%2$s” have been merged.',
1343
			'<a class="alert-link" href="' . e($record1->url()) . '">' . $record1->getFullName() . '</a>',
1344
			$record2_name
1345
		), 'success');
1346
1347
		return new RedirectResponse(route('merge-records', ['ged' => $tree->getName()]));
1348
	}
1349
1350
	/**
1351
	 * Show the administrator a list of modules.
1352
	 *
1353
	 * @return Response
1354
	 */
1355
	public function modules(): Response {
1356
		$module_status = Database::prepare("SELECT module_name, status FROM `##module`")->fetchAssoc();
1357
1358
		return $this->viewResponse('admin/modules', [
1359
			'title'             => I18N::translate('Module administration'),
1360
			'modules'           => Module::getInstalledModules('disabled'),
1361
			'module_status'     => $module_status,
1362
			'deleted_modules'   => $this->deletedModuleNames(),
1363
			'core_module_names' => Module::getCoreModuleNames(),
1364
		]);
1365
	}
1366
1367
	/**
1368
	 * Show the admin page for menus.
1369
	 *
1370
	 * @return Response
1371
	 */
1372
	public function menus(): Response {
1373
		return $this->components('menu', 'menus', I18N::translate('Menu'), I18N::translate('Menus'));
1374
	}
1375
1376
	/**
1377
	 * Show the admin page for reports.
1378
	 *
1379
	 * @return Response
1380
	 */
1381
	public function reports(): Response {
1382
		return $this->components('report', 'reports', I18N::translate('Report'), I18N::translate('Reports'));
1383
	}
1384
1385
	/**
1386
	 * Show the admin page for sidebars.
1387
	 *
1388
	 * @return Response
1389
	 */
1390
	public function sidebars(): Response {
1391
		return $this->components('sidebar', 'sidebars', I18N::translate('Sidebar'), I18N::translate('Sidebars'));
1392
	}
1393
1394
	/**
1395
	 * Show the admin page for tabs.
1396
	 *
1397
	 * @return Response
1398
	 */
1399
	public function tabs(): Response {
1400
		return $this->components('tab', 'tabs', I18N::translate('Tab'), I18N::translate('Tabs'));
1401
	}
1402
1403
	/**
1404
	 * @param Request $request
1405
	 *
1406
	 * @return Response
1407
	 */
1408
	public function treePrivacyEdit(Request $request): Response {
1409
		/** @var Tree $tree */
1410
		$tree                 = $request->attributes->get('tree');
1411
		$title                = e($tree->getName()) . ' — ' . I18N::translate('Privacy');
1412
		$all_tags             = $this->tagsForPrivacy($tree);
1413
		$privacy_constants    = $this->privacyConstants();
1414
		$privacy_restrictions = $this->privacyRestrictions($tree);
1415
1416
		return $this->viewResponse('admin/trees-privacy', [
1417
			'all_tags'             => $all_tags,
1418
			'count_trees'          => count(Tree::getAll()),
1419
			'privacy_constants'    => $privacy_constants,
1420
			'privacy_restrictions' => $privacy_restrictions,
1421
			'title'                => $title,
1422
		]);
1423
	}
1424
1425
	/**
1426
	 * @param Request $request
1427
	 *
1428
	 * @return RedirectResponse
1429
	 */
1430
	public function treePrivacyUpdate(Request $request): RedirectResponse {
1431
		/** @var Tree $tree */
1432
		$tree = $request->attributes->get('tree');
1433
1434
		foreach ((array) $request->get('delete') as $default_resn_id) {
1435
			Database::prepare(
1436
				"DELETE FROM `##default_resn` WHERE default_resn_id = :default_resn_id"
1437
			)->execute([
1438
				'default_resn_id' => $default_resn_id,
1439
			]);
1440
		}
1441
1442
		$xrefs     = (array) $request->get('xref');
1443
		$tag_types = (array) $request->get('tag_type');
1444
		$resns     = (array) $request->get('resn');
1445
1446
		foreach ($xrefs as $n => $xref) {
1447
			$tag_type = (string) $tag_types[$n];
1448
			$resn     = (string) $resns[$n];
1449
1450
			if ($tag_type !== '' || $xref !== '') {
1451
				// Delete any existing data
1452
				if ($xref === '') {
1453
					Database::prepare(
1454
						"DELETE FROM `##default_resn` WHERE gedcom_id = :tree_id AND tag_type = :tag_type AND xref IS NULL"
1455
					)->execute([
1456
						'tree_id'  => $tree->getTreeId(),
1457
						'tag_type' => $tag_type,
1458
					]);
1459
				}
1460
				if ($tag_type === '') {
1461
					Database::prepare(
1462
						"DELETE FROM `##default_resn` WHERE gedcom_id = ? AND xref = ? AND tag_type IS NULL"
1463
					)->execute([
1464
						'tree_id' => $tree->getTreeId(),
1465
						'xref'    => $xref,
1466
					]);
1467
				}
1468
1469
				// Add (or update) the new data
1470
				Database::prepare(
1471
					"REPLACE INTO `##default_resn` (gedcom_id, xref, tag_type, resn)" .
1472
					" VALUES (:tree_id, NULLIF(:xref, ''), NULLIF(:tag_type, ''), :resn)"
1473
				)->execute([
1474
					'tree_id'  => $tree->getTreeId(),
1475
					'xref'     => $xref,
1476
					'tag_type' => $tag_type,
1477
					'resn'     => $resn,
1478
				]);
1479
			}
1480
		}
1481
1482
		$tree->setPreference('HIDE_LIVE_PEOPLE', $request->get('HIDE_LIVE_PEOPLE'));
1483
		$tree->setPreference('KEEP_ALIVE_YEARS_BIRTH', $request->get('KEEP_ALIVE_YEARS_BIRTH', '0'));
1484
		$tree->setPreference('KEEP_ALIVE_YEARS_DEATH', $request->get('KEEP_ALIVE_YEARS_DEATH', '0'));
1485
		$tree->setPreference('MAX_ALIVE_AGE', $request->get('MAX_ALIVE_AGE', '100'));
1486
		$tree->setPreference('REQUIRE_AUTHENTICATION', $request->get('REQUIRE_AUTHENTICATION'));
1487
		$tree->setPreference('SHOW_DEAD_PEOPLE', $request->get('SHOW_DEAD_PEOPLE'));
1488
		$tree->setPreference('SHOW_LIVING_NAMES', $request->get('SHOW_LIVING_NAMES'));
1489
		$tree->setPreference('SHOW_PRIVATE_RELATIONSHIPS', $request->get('SHOW_PRIVATE_RELATIONSHIPS'));
1490
1491
		FlashMessages::addMessage(I18N::translate('The preferences for the family tree “%s” have been updated.', e($tree->getTitle()), 'success'));
1492
1493
		// Coming soon...
1494
		if ((bool) $request->get('all_trees')) {
1495
			FlashMessages::addMessage(I18N::translate('The preferences for all family trees have been updated.', e($tree->getTitle())), 'success');
1496
		}
1497
		if ((bool) $request->get('new_trees')) {
1498
			FlashMessages::addMessage(I18N::translate('The preferences for new family trees have been updated.', e($tree->getTitle())), 'success');
1499
		}
1500
1501
1502
		return new RedirectResponse(route('admin-trees', ['ged' => $tree->getName()]));
1503
	}
1504
1505
	/**
1506
	 * Update the access levels of the modules.
1507
	 *
1508
	 * @param Request $request
1509
	 *
1510
	 * @return RedirectResponse
1511
	 */
1512
	public function updateModuleAccess(Request $request): RedirectResponse {
1513
		$component = $request->get('component');
1514
		$modules   = Module::getAllModulesByComponent($component);
1515
1516
		foreach ($modules as $module) {
1517
			foreach (Tree::getAll() as $tree) {
1518
				$key          = 'access-' . $module->getName() . '-' . $tree->getTreeId();
1519
				$access_level = (int) $request->get($key, $module->defaultAccessLevel());
1520
1521
				Database::prepare("REPLACE INTO `##module_privacy` (module_name, gedcom_id, component, access_level)" . " VALUES (:module_name, :tree_id, :component, :access_level)")->execute([
1522
					'module_name'  => $module->getName(),
1523
					'tree_id'      => $tree->getTreeId(),
1524
					'component'    => $component,
1525
					'access_level' => $access_level,
1526
				]);
1527
			}
1528
		}
1529
1530
		return new RedirectResponse(route('admin-' . $component . 's'));
1531
	}
1532
1533
	/**
1534
	 * Update the enabled/disabled status of the modules.
1535
	 *
1536
	 * @param Request $request
1537
	 *
1538
	 * @return RedirectResponse
1539
	 */
1540
	public function updateModuleStatus(Request $request): RedirectResponse {
1541
		$modules       = Module::getInstalledModules('disabled');
1542
		$module_status = Database::prepare("SELECT module_name, status FROM `##module`")->fetchAssoc();
1543
1544
		foreach ($modules as $module) {
1545
			$new_status = (bool) $request->get('status-' . $module->getName()) ? 'enabled' : 'disabled';
1546
			$old_status = $module_status[$module->getName()];
1547
1548
			if ($new_status !== $old_status) {
1549
				Database::prepare("UPDATE `##module` SET status = :status WHERE module_name = :module_name")->execute([
1550
					'status'      => $new_status,
1551
					'module_name' => $module->getName(),
1552
				]);
1553
1554
				if ($new_status === 'enabled') {
1555
					FlashMessages::addMessage(I18N::translate('The module “%s” has been enabled.', $module->getTitle()), 'success');
1556
				} else {
1557
					FlashMessages::addMessage(I18N::translate('The module “%s” has been disabled.', $module->getTitle()), 'success');
1558
				}
1559
			}
1560
		}
1561
1562
		return new RedirectResponse(route('admin-modules'));
1563
	}
1564
1565
	/**
1566
	 * Create a response object from a view.
1567
	 *
1568
	 * @param string  $name
1569
	 * @param mixed[] $data
1570
	 * @param int     $status
1571
	 *
1572
	 * @return Response
1573
	 */
1574
	protected function viewResponse($name, $data, $status = Response::HTTP_OK): Response {
1575
		$html = view($this->layout, [
1576
			'content' => view($name, $data),
1577
			'title'   => strip_tags($data['title']),
1578
		]);
1579
1580
		return new Response($html, $status);
1581
	}
1582
1583
	/**
1584
	 * Generate a WHERE clause for filtering the changes log.
1585
	 *
1586
	 * @param Request $request
1587
	 *
1588
	 * @return array
1589
	 *
1590
	 */
1591
	private function changesQuery(Request $request): array {
1592
		$from     = $request->get('from', '');
1593
		$to       = $request->get('to', '');
1594
		$type     = $request->get('type', '');
1595
		$oldged   = $request->get('oldged', '');
1596
		$newged   = $request->get('newged', '');
1597
		$xref     = $request->get('xref', '');
1598
		$username = $request->get('username', '');
1599
		$ged      = $request->get('ged', '');
1600
		$search   = $request->get('search', '');
1601
		$search   = $search['value'] ?? '';
1602
1603
		$where = ' WHERE 1';
1604
		$args  = [];
1605
		if ($search !== '') {
1606
			$where            .= " AND (old_gedcom LIKE CONCAT('%', :search_1, '%') OR new_gedcom LIKE CONCAT('%', :search_2, '%'))";
1607
			$args['search_1'] = $search;
1608
			$args['search_2'] = $search;
1609
		}
1610
		if ($from !== '') {
1611
			$where        .= " AND change_time >= :from";
1612
			$args['from'] = $from;
1613
		}
1614
		if ($to !== '') {
1615
			$where      .= ' AND change_time < TIMESTAMPADD(DAY, 1 , :to)'; // before end of the day
1616
			$args['to'] = $to;
1617
		}
1618
		if ($type !== '') {
1619
			$where          .= ' AND status = :status';
1620
			$args['status'] = $type;
1621
		}
1622
		if ($oldged !== '') {
1623
			$where           .= " AND old_gedcom LIKE CONCAT('%', :old_ged, '%')";
1624
			$args['old_ged'] = $oldged;
1625
		}
1626
		if ($newged !== '') {
1627
			$where           .= " AND new_gedcom LIKE CONCAT('%', :new_ged, '%')";
1628
			$args['new_ged'] = $newged;
1629
		}
1630
		if ($xref !== '') {
1631
			$where        .= " AND xref = :xref";
1632
			$args['xref'] = $xref;
1633
		}
1634
		if ($username !== '') {
1635
			$where        .= " AND user_name LIKE CONCAT('%', :user, '%')";
1636
			$args['user'] = $username;
1637
		}
1638
		if ($ged !== '') {
1639
			$where       .= " AND gedcom_name LIKE CONCAT('%', :ged, '%')";
1640
			$args['ged'] = $ged;
1641
		}
1642
1643
		$select = "SELECT SQL_CACHE SQL_CALC_FOUND_ROWS change_id, change_time, status, xref, old_gedcom, new_gedcom, IFNULL(user_name, '<none>') AS user_name, gedcom_name FROM `##change`";
1644
		$delete = 'DELETE `##change` FROM `##change`';
1645
1646
		$join = ' LEFT JOIN `##user` USING (user_id) JOIN `##gedcom` USING (gedcom_id)';
1647
1648
		return [$select . $join, $delete . $join, $where, $args];
1649
	}
1650
1651
	/**
1652
	 * Show the admin page for blocks, charts, menus, reports, sidebars, tabs, etc..
1653
	 *
1654
	 * @param string $component
1655
	 * @param string $route
1656
	 * @param string $component_title
1657
	 * @param string $title
1658
	 *
1659
	 * @return Response
1660
	 */
1661
	private function components($component, $route, $component_title, $title): Response {
1662
		return $this->viewResponse('admin/module-components', [
1663
			'component'       => $component,
1664
			'component_title' => $component_title,
1665
			'modules'         => Module::getAllModulesByComponent($component),
1666
			'title'           => $title,
1667
			'route'           => $route,
1668
		]);
1669
	}
1670
1671
	/**
1672
	 * Conver request parameters into paging/sorting for datatables
1673
	 *
1674
	 * @param $request
1675
	 *
1676
	 * @return array
1677
	 */
1678
	private function dataTablesPagination(Request $request): array {
1679
		$start  = (int) $request->get('start', '0');
1680
		$length = (int) $request->get('length', '0');
1681
		$order  = $request->get('order', []);
1682
		$args   = [];
1683
1684
		if (is_array($order) && !empty($order)) {
1685
			$order_by = ' ORDER BY ';
1686
			foreach ($order as $key => $value) {
1687
				if ($key > 0) {
1688
					$order_by .= ',';
1689
				}
1690
				// Columns in datatables are numbered from zero.
1691
				// Columns in MySQL are numbered starting with one.
1692
				switch ($value['dir']) {
1693
					case 'asc':
1694
						$order_by .= (1 + $value['column']) . ' ASC ';
1695
						break;
1696
					case 'desc':
1697
						$order_by .= (1 + $value['column']) . ' DESC ';
1698
						break;
1699
				}
1700
			}
1701
		} else {
1702
			$order_by = '';
1703
		}
1704
1705
		if ($length) {
1706
			Auth::user()->setPreference('admin_site_change_page_size', $length);
1707
			$limit          = ' LIMIT :limit OFFSET :offset';
1708
			$args['limit']  = $length;
1709
			$args['offset'] = $start;
1710
		} else {
1711
			$limit = "";
1712
		}
1713
1714
		return [$order_by, $limit, $args];
1715
	}
1716
1717
	/**
1718
	 * Generate a list of module names which exist in the database but not on disk.
1719
	 *
1720
	 * @return string[]
1721
	 */
1722
	private function deletedModuleNames() {
1723
		$database_modules = Database::prepare("SELECT module_name FROM `##module`")->fetchOneColumn();
1724
		$disk_modules     = Module::getInstalledModules('disabled');
1725
1726
		return array_diff($database_modules, array_keys($disk_modules));
1727
	}
1728
1729
	/**
1730
	 * A list of old files that need to be deleted.
1731
	 *
1732
	 * @return string[]
1733
	 */
1734
	private function filesToDelete() {
1735
		$files_to_delete = [];
1736
		foreach (self::OLD_FILES as $file) {
1737
			// Delete the file, if we can.
1738
			if (file_exists($file) && !File::delete($file)) {
1739
				$files_to_delete[] = $file;
1740
			}
1741
		}
1742
1743
		return $files_to_delete;
1744
	}
1745
1746
	/**
1747
	 * Find the media object that uses a particular media file.
1748
	 *
1749
	 * @param string $file
1750
	 *
1751
	 * @return Media[]
1752
	 */
1753
	private function findMediaObjectsForMediaFile(string $file): array {
1754
		$rows = Database::prepare(
1755
			"SELECT DISTINCT m.*" .
1756
			" FROM  `##media` as m" .
1757
			" JOIN  `##media_file` USING (m_file, m_id)" .
1758
			" JOIN  `##gedcom_setting` ON (m_file = gedcom_id AND setting_name = 'MEDIA_DIRECTORY')" .
1759
			" WHERE CONCAT(setting_value, multimedia_file_refn) = :file"
1760
		)->execute([
1761
			'file' => $file,
1762
		])->fetchAll();
1763
1764
		$media = [];
1765
1766
		foreach ($rows as $row) {
1767
			$tree    = Tree::findById($row->m_file);
1768
			$media[] = Media::getInstance($row->m_id, $tree, $row->m_gedcom);
1769
		}
1770
1771
		return array_filter($media);
1772
	}
1773
1774
	/**
1775
	 * Find the original image that corresponds to a (webtrees 1.x) thumbnail file.
1776
	 *
1777
	 * @param string $thumbnail
1778
	 *
1779
	 * @return string
1780
	 */
1781
	private function findOriginalFileFromThumbnail(string $thumbnail): string {
1782
		// First option - a file with the same name
1783
		$original = str_replace('/thumbs/', '/', $thumbnail);
1784
1785
		// Second option - a .PNG thumbnail for some other image type
1786
		if (substr_compare($original, '.png', -4, 4) === 0) {
1787
			$pattern = substr($original, 0, -3) . '*';
1788
			$matches = glob($pattern);
1789
			if (!empty($matches) && is_file($matches[0])) {
1790
				$original = $matches[0];
1791
			}
1792
		}
1793
1794
		return $original;
1795
	}
1796
1797
	/**
1798
	 * Compare two images, and return a quantified difference.
1799
	 *
1800
	 * 0 (different) ... 100 (same)
1801
	 *
1802
	 * @param string $thumbanil
1803
	 * @param string $original
1804
	 *
1805
	 * @return int
1806
	 */
1807
	private function imageDiff($thumbanil, $original): int {
1808
		try {
1809
			if (getimagesize($thumbanil) === false) {
1810
				return 100;
1811
			}
1812
		} catch (Throwable $ex) {
1813
			// If the first file is not an image then similarity is unimportant.
1814
			// Response with an exact match, so the GUI will recommend deleting it.
1815
			return 100;
1816
		}
1817
1818
		try {
1819
			if (getimagesize($original) === false) {
1820
				return 0;
1821
			}
1822
		} catch (Throwable $ex) {
1823
			// If the first file is not an image then the thumbnail .
1824
			// Response with an exact mismatch, so the GUI will recommend importing it.
1825
			return 0;
1826
		}
1827
1828
		$pixels1 = $this->scaledImagePixels($thumbanil);
1829
		$pixels2 = $this->scaledImagePixels($original);
1830
1831
		$max_difference = 0;
1832
1833
		foreach ($pixels1 as $x => $row) {
1834
			foreach ($row as $y => $pixel) {
1835
				$max_difference = max($max_difference, abs($pixel - $pixels2[$x][$y]));
1836
			}
1837
		}
1838
1839
		// The maximum difference is 255 (black versus white).
1840
		return 100 - (int) ($max_difference * 100 / 255);
1841
	}
1842
1843
	/**
1844
	 * Scale an image to 10x10 and read the individual pixels.
1845
	 *
1846
	 * This is a slow operation, add we will do it many times on
1847
	 * the "import wetbrees 1 thumbnails" page so cache the results.
1848
	 *
1849
	 * @param string $path
1850
	 *
1851
	 * @return int[][]
1852
	 */
1853
	private function scaledImagePixels($path): array {
1854
		$size       = 10;
1855
		$sha1       = sha1_file($path);
1856
		$cache_file = WT_DATA_DIR . 'cache/' . $sha1 . '.php';
1857
1858
		if (file_exists($cache_file)) {
1859
			return include $cache_file;
1860
		}
1861
1862
		$manager = new ImageManager;
1863
		$image   = $manager->make($path)->resize($size, $size);
1864
1865
		$pixels = [];
1866
		for ($x = 0; $x < $size; ++$x) {
1867
			$pixels[$x] = [];
1868
			for ($y = 0; $y < $size; ++$y) {
1869
				$pixel          = $image->pickColor($x, $y);
1870
				$pixels[$x][$y] = (int) (($pixel[0] + $pixel[1] + $pixel[2]) / 3);
1871
			}
1872
		}
1873
1874
		file_put_contents($cache_file, '<?php return ' . var_export($pixels, true) . ';');
1875
1876
		return $pixels;
1877
	}
1878
1879
	/**
1880
	 * Look for the latest version of webtrees.
1881
	 *
1882
	 * @return string
1883
	 */
1884
	private function latestVersion() {
1885
		$latest_version_txt = Functions::fetchLatestVersion();
1886
		if (preg_match('/^[0-9.]+\|[0-9.]+\|/', $latest_version_txt)) {
1887
			list($latest_version) = explode('|', $latest_version_txt);
1888
		} else {
1889
			// Cannot determine the latest version.
1890
			$latest_version = '';
1891
		}
1892
1893
		return $latest_version;
1894
	}
1895
1896
	/**
1897
	 * Names of our privacy levels
1898
	 *
1899
	 * @return array
1900
	 */
1901
	private function privacyConstants(): array {
1902
		return [
1903
			'none'         => I18N::translate('Show to visitors'),
1904
			'privacy'      => I18N::translate('Show to members'),
1905
			'confidential' => I18N::translate('Show to managers'),
1906
			'hidden'       => I18N::translate('Hide from everyone'),
1907
		];
1908
	}
1909
1910
	/**
1911
	 * The current privacy restrictions for a tree.
1912
	 *
1913
	 * @param Tree $tree
1914
	 *
1915
	 * @return array
1916
	 */
1917
	private function privacyRestrictions(Tree $tree): array {
1918
		$restrictions = Database::prepare(
1919
			"SELECT default_resn_id, tag_type, xref, resn" .
1920
			" FROM `##default_resn`" .
1921
			" LEFT JOIN `##name` ON (gedcom_id = n_file AND xref = n_id AND n_num = 0)" .
1922
			" WHERE gedcom_id = :tree_id"
1923
		)->execute([
1924
			'tree_id' => $tree->getTreeId(),
1925
		])->fetchAll();
1926
1927
		foreach ($restrictions as $resn) {
1928
			$resn->record = GedcomRecord::getInstance($resn->xref, $tree);
1929
			if ($resn->tag_type) {
1930
				$resn->tag_label = GedcomTag::getLabel($resn->tag_type);
1931
			} else {
1932
				$resn->tag_label = '';
1933
			}
1934
		}
1935
1936
		usort($restrictions, function (stdClass $x, stdClass $y) {
1937
			return I18N::strcasecmp($x->tag_label, $y->tag_label);
1938
		});
1939
1940
		return $restrictions;
1941
	}
1942
1943
	/**
1944
	 * Generate a list of potential problems with the server.
1945
	 *
1946
	 * @return string[]
1947
	 */
1948
	private function serverWarnings() {
1949
		$php_support_url   = 'https://secure.php.net/supported-versions.php';
1950
		$version_parts     = explode('.', PHP_VERSION);
1951
		$php_minor_version = $version_parts[0] . $version_parts[1];
1952
		$today             = date('Y-m-d');
1953
		$warnings          = [];
1954
1955
		if ($php_minor_version === '70' && $today >= '2017-12-03' || $php_minor_version === '71' && $today >= '2019-12-01') {
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: {currentAssign}, Probably Intended Meaning: {alternativeAssign}
Loading history...
1956
			$warnings[] = I18N::translate('Your web server is using PHP version %s, which is no longer receiving security updates. You should upgrade to a later version as soon as possible.', PHP_VERSION) . ' <a href="' . $php_support_url . '">' . $php_support_url . '</a>';
1957
		} elseif ($php_minor_version === '70' && $today >= '2017-12-03' || $php_minor_version === '71' && $today >= '2018-12-01') {
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: {currentAssign}, Probably Intended Meaning: {alternativeAssign}
Loading history...
1958
			$warnings[] = I18N::translate('Your web server is using PHP version %s, which is no longer maintained. You should upgrade to a later version.', PHP_VERSION) . ' <a href="' . $php_support_url . '">' . $php_support_url . '</a>';
1959
		}
1960
1961
		return $warnings;
1962
	}
1963
1964
	/**
1965
	 * Generate a list of potential problems with the server.
1966
	 *
1967
	 * @param Tree $tree
1968
	 *
1969
	 * @return string[]
1970
	 */
1971
	private function tagsForPrivacy(Tree $tree): array {
1972
		$tags = array_unique(array_merge(
1973
			explode(',', $tree->getPreference('INDI_FACTS_ADD')),
1974
			explode(',', $tree->getPreference('INDI_FACTS_UNIQUE')),
1975
			explode(',', $tree->getPreference('FAM_FACTS_ADD')),
1976
			explode(',', $tree->getPreference('FAM_FACTS_UNIQUE')),
1977
			explode(',', $tree->getPreference('NOTE_FACTS_ADD')),
1978
			explode(',', $tree->getPreference('NOTE_FACTS_UNIQUE')),
1979
			explode(',', $tree->getPreference('SOUR_FACTS_ADD')),
1980
			explode(',', $tree->getPreference('SOUR_FACTS_UNIQUE')),
1981
			explode(',', $tree->getPreference('REPO_FACTS_ADD')),
1982
			explode(',', $tree->getPreference('REPO_FACTS_UNIQUE')),
1983
			['SOUR', 'REPO', 'OBJE', '_PRIM', 'NOTE', 'SUBM', 'SUBN', '_UID', 'CHAN']
1984
		));
1985
1986
		$all_tags = [];
1987
		foreach ($tags as $tag) {
1988
			if ($tag) {
1989
				$all_tags[$tag] = GedcomTag::getLabel($tag);
1990
			}
1991
		}
1992
1993
		uasort($all_tags, '\Fisharebest\Webtrees\I18N::strcasecmp');
1994
1995
		return $all_tags;
1996
	}
1997
1998
	/**
1999
	 * Count the number of pending changes in each tree.
2000
	 *
2001
	 * @return string[]
2002
	 */
2003
	private function totalChanges() {
2004
		return Database::prepare("SELECT SQL_CACHE g.gedcom_id, COUNT(change_id)" . " FROM `##gedcom` AS g" . " LEFT JOIN `##change` AS c ON g.gedcom_id = c.gedcom_id AND status = 'pending'" . " GROUP BY g.gedcom_id")->fetchAssoc();
2005
	}
2006
2007
	/**
2008
	 * Count the number of families in each tree.
2009
	 *
2010
	 * @return string[]
2011
	 */
2012
	private function totalFamilies() {
2013
		return Database::prepare("SELECT SQL_CACHE gedcom_id, COUNT(f_id)" . " FROM `##gedcom`" . " LEFT JOIN `##families` ON gedcom_id = f_file" . " GROUP BY gedcom_id")->fetchAssoc();
2014
	}
2015
2016
	/**
2017
	 * Count the number of individuals in each tree.
2018
	 *
2019
	 * @return string[]
2020
	 */
2021
	private function totalIndividuals() {
2022
		return Database::prepare("SELECT SQL_CACHE gedcom_id, COUNT(i_id)" . " FROM `##gedcom`" . " LEFT JOIN `##individuals` ON gedcom_id = i_file" . " GROUP BY gedcom_id")->fetchAssoc();
2023
	}
2024
2025
	/**
2026
	 * Count the number of media objects in each tree.
2027
	 *
2028
	 * @return string[]
2029
	 */
2030
	private function totalMediaObjects() {
2031
		return Database::prepare("SELECT SQL_CACHE gedcom_id, COUNT(m_id)" . " FROM `##gedcom`" . " LEFT JOIN `##media` ON gedcom_id = m_file" . " GROUP BY gedcom_id")->fetchAssoc();
2032
	}
2033
2034
	/**
2035
	 * Count the number of notes in each tree.
2036
	 *
2037
	 * @return string[]
2038
	 */
2039
	private function totalNotes() {
2040
		return Database::prepare("SELECT SQL_CACHE gedcom_id, COUNT(o_id)" . " FROM `##gedcom`" . " LEFT JOIN `##other` ON gedcom_id = o_file AND o_type = 'NOTE'" . " GROUP BY gedcom_id"
2041
2042
		)->fetchAssoc();
2043
	}
2044
2045
	/**
2046
	 * Count the number of repositorie in each tree.
2047
	 *
2048
	 * @return string[]
2049
	 */
2050
	private function totalRepositories() {
2051
		return Database::prepare("SELECT SQL_CACHE gedcom_id, COUNT(o_id)" . " FROM `##gedcom`" . " LEFT JOIN `##other` ON gedcom_id = o_file AND o_type = 'REPO'" . " GROUP BY gedcom_id")->fetchAssoc();
2052
	}
2053
2054
	/**
2055
	 * Count the number of sources in each tree.
2056
	 *
2057
	 * @return string[]
2058
	 */
2059
	private function totalSources() {
2060
		return Database::prepare("SELECT SQL_CACHE gedcom_id, COUNT(s_id)" . " FROM `##gedcom`" . " LEFT JOIN `##sources` ON gedcom_id = s_file" . " GROUP BY gedcom_id")->fetchAssoc();
2061
	}
2062
}
2063