Completed
Push — develop ( 9087a8...c9b4ef )
by Greg
16:31 queued 05:44
created

BatchUpdateModule::getPluginList()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 8
nc 3
nop 0
dl 0
loc 12
rs 9.2
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
namespace Fisharebest\Webtrees\Module;
17
18
use Fisharebest\Webtrees\Auth;
19
use Fisharebest\Webtrees\Bootstrap4;
20
use Fisharebest\Webtrees\Controller\PageController;
21
use Fisharebest\Webtrees\Database;
22
use Fisharebest\Webtrees\Family;
23
use Fisharebest\Webtrees\Filter;
24
use Fisharebest\Webtrees\GedcomRecord;
25
use Fisharebest\Webtrees\Html;
26
use Fisharebest\Webtrees\I18N;
27
use Fisharebest\Webtrees\Individual;
28
use Fisharebest\Webtrees\Media;
29
use Fisharebest\Webtrees\Module\BatchUpdate\BatchUpdateBasePlugin;
30
use Fisharebest\Webtrees\Note;
31
use Fisharebest\Webtrees\Repository;
32
use Fisharebest\Webtrees\Source;
33
use Fisharebest\Webtrees\Tree;
34
35
/**
36
 * Class BatchUpdateModule
37
 */
38
class BatchUpdateModule extends AbstractModule implements ModuleConfigInterface {
39
	/** @var string  Form parameter: chosen plugin*/
40
	private $plugin;
41
42
	/** @var string Form parameter: record to update */
43
	private $xref;
44
45
	/** @var string Form parameter: how to update record */
46
	private $action;
47
48
	/** @var string Form parameter: additional details for $action */
49
	private $data;
50
51
	/** @var BatchUpdateBasePlugin[] All available plugins */
52
	private $plugins;
53
54
	/** @var BatchUpdateBasePlugin  The current plugin */
55
	private $PLUGIN;
56
57
	/** @var string[] n array of all xrefs that might need to be updated */
58
	private $all_xrefs;
59
60
	/** @var string The previous xref to process */
61
	private $prev_xref;
62
63
	/** @var String The current xref being process */
64
	private $curr_xref;
65
66
	/** @var string The next xref to process */
67
	private $next_xref;
68
69
	/** @var GedcomRecord The record corresponding to $curr_xref */
70
	private $record;
71
72
	/**
73
	 * How should this module be labelled on tabs, menus, etc.?
74
	 *
75
	 * @return string
76
	 */
77
	public function getTitle() {
78
		return /* I18N: Name of a module */ I18N::translate('Batch update');
79
	}
80
81
	/**
82
	 * A sentence describing what this module does.
83
	 *
84
	 * @return string
85
	 */
86
	public function getDescription() {
87
		return /* I18N: Description of the “Batch update” module */ I18N::translate('Apply automatic corrections to your genealogy data.');
88
	}
89
90
	/**
91
	 * This is a general purpose hook, allowing modules to respond to routes
92
	 * of the form module.php?mod=FOO&mod_action=BAR
93
	 *
94
	 * @param string $mod_action
95
	 */
96
	public function modAction($mod_action) {
97
		switch ($mod_action) {
98
			case 'admin_batch_update':
99
				echo $this->main();
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->main() targeting Fisharebest\Webtrees\Mod...tchUpdateModule::main() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
100
				break;
101
102
			default:
103
				http_response_code(404);
104
				break;
105
		}
106
	}
107
108
	/**
109
	 * Main entry point
110
	 */
111
	private function main() {
112
		global $WT_TREE;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
113
114
		$this->plugins = $this->getPluginList(); // List of available plugins
115
		$this->plugin  = Filter::get('plugin'); // User parameters
116
		$this->xref    = Filter::get('xref', WT_REGEX_XREF);
117
		$this->action  = Filter::get('action');
118
		$this->data    = Filter::get('data');
119
120
		// Don't do any processing until a plugin is chosen.
121
		if ($this->plugin && array_key_exists($this->plugin, $this->plugins)) {
122
			$this->PLUGIN = new $this->plugin;
123
			$this->PLUGIN->getOptions();
124
			$this->getAllXrefs();
125
126
			switch ($this->action) {
127
				case 'update':
128
					$record = self::getLatestRecord($this->xref, $this->all_xrefs[$this->xref]);
129
					if ($this->PLUGIN->doesRecordNeedUpdate($this->xref, $record)) {
130
						$newrecord = $this->PLUGIN->updateRecord($this->xref, $record);
131
						if ($newrecord != $record) {
132
							if ($newrecord) {
133
								GedcomRecord::getInstance($this->xref, $WT_TREE)->updateRecord($newrecord, !$this->PLUGIN->chan);
134
							} else {
135
								GedcomRecord::getInstance($this->xref, $WT_TREE)->deleteRecord();
136
							}
137
						}
138
					}
139
					$this->xref = $this->findNextXref($this->xref);
140
					break;
141
				case 'update_all':
142
					foreach ($this->all_xrefs as $xref => $type) {
143
						$record = self::getLatestRecord($xref, $type);
144
						if ($this->PLUGIN->doesRecordNeedUpdate($xref, $record)) {
145
							$newrecord = $this->PLUGIN->updateRecord($xref, $record);
146
							if ($newrecord != $record) {
147
								if ($newrecord) {
148
									GedcomRecord::getInstance($xref, $WT_TREE)->updateRecord($newrecord, !$this->PLUGIN->chan);
149
								} else {
150
									GedcomRecord::getInstance($xref, $WT_TREE)->deleteRecord();
151
								}
152
							}
153
						}
154
					}
155
					$this->xref = '';
156
					break;
157
			}
158
159
			// Make sure that our requested record really does need updating.
160
			// It may have been updated in another session, or may not have
161
			// been specified at all.
162
			if (array_key_exists($this->xref, $this->all_xrefs) &&
163
				$this->PLUGIN->doesRecordNeedUpdate($this->xref, self::getLatestRecord($this->xref, $this->all_xrefs[$this->xref]))) {
164
				$this->curr_xref = $this->xref;
165
			}
166
			// The requested record doesn't need updating - find one that does
167
			if (!$this->curr_xref) {
168
				$this->curr_xref = $this->findNextXref($this->xref);
169
			}
170
			if (!$this->curr_xref) {
171
				$this->curr_xref = $this->findPrevXref($this->xref);
172
			}
173
			// If we've found a record to update, get details and look for the next/prev
174
			if ($this->curr_xref) {
175
				$this->prev_xref = $this->findPrevXref($this->curr_xref);
176
				$this->next_xref = $this->findNextXref($this->curr_xref);
177
			}
178
		}
179
180
		// HTML common to all pages
181
		$controller = new PageController;
182
		$controller
183
			->setPageTitle(I18N::translate('Batch update'))
184
			->restrictAccess(Auth::isAdmin())
185
			->pageHeader();
186
187
		echo $this->getJavascript();
188
		echo Bootstrap4::breadcrumbs([
189
			route('admin-control-panel') => I18N::translate('Control panel'),
190
			route('admin-modules')       => I18N::translate('Module administration'),
191
		], $controller->getPageTitle());
192
		?>
193
		<h1><?= $controller->getPageTitle() ?></h1>
194
195
		<form id="batch_update_form" class="form-horizontal" action="module.php">
196
			<input type="hidden" name="mod" value="batch_update">
197
			<input type="hidden" name="mod_action" value="admin_batch_update">
198
			<input type="hidden" name="xref"   value="' . $this->xref . '">
199
			<input type="hidden" name="action" value=""><?php // will be set by javascript for next update  ?>
200
			<input type="hidden" name="data"   value=""><?php // will be set by javascript for next update  ?>
201
			<div class="row form-group">
202
				<label class="col-sm-3 col-form-label"><?= I18N::translate('Family tree') ?></label>
203
				<div class="col-sm-9">
204
		<?= Bootstrap4::select(Tree::getNameList(), $WT_TREE->getName(), ['id' => 'ged', 'name' => 'ged', 'onchange' => 'reset_reload();']) ?>
205
				</div>
206
			</div>
207
			<div class="row form-group">
208
				<label class="col-sm-3 col-form-label"><?= I18N::translate('Batch update') ?></label>
209
				<div class="col-sm-9">
210
					<select class="form-control" name="plugin" onchange="reset_reload();">
211
						<?php if (!$this->plugin): ?>
212
							<option value="" selected></option>
213
						<?php endif ?>
214
						<?php foreach ($this->plugins as $class => $plugin): ?>
215
							<option value="<?= $class ?>" <?= $this->plugin == $class ? 'selected' : '' ?>><?= $plugin->getName() ?></option>
216
					<?php endforeach ?>
217
					</select>
218
					<?php if ($this->PLUGIN): ?>
219
						<p class="small text-muted"><?= $this->PLUGIN->getDescription() ?></p>
0 ignored issues
show
introduced by
The method getDescription() does not exist on Fisharebest\Webtrees\Mod...e\BatchUpdateBasePlugin. Maybe you want to declare this class abstract? ( Ignorable by Annotation )

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

219
						<p class="small text-muted"><?= $this->PLUGIN->/** @scrutinizer ignore-call */ getDescription() ?></p>
Loading history...
220
		<?php endif ?>
221
				</div>
222
			</div>
223
224
				<?php if (!Auth::user()->getPreference('auto_accept')): ?>
225
				<div class="alert alert-danger">
226
				<?= I18N::translate('Your user account does not have “automatically accept changes” enabled. You will only be able to change one record at a time.') ?>
227
				</div>
228
			<?php endif ?>
229
230
			<?php // If a plugin is selected, display the details ?>
231
			<?php if ($this->PLUGIN): ?>
232
				<?= $this->PLUGIN->getOptionsForm() ?>
233
				<?php if (substr($this->action, -4) == '_all'): ?>
234
					<?php // Reset - otherwise we might "undo all changes", which refreshes the ?>
235
					<?php // page, which makes them all again!  ?>
236
					<script>reset_reload();</script>
237
			<?php else: ?>
238
					<hr>
239
					<div id="batch_update2" class="col-sm-12">
240
						<?php if ($this->curr_xref): ?>
241
							<?php // Create an object, so we can get the latest version of the name. ?>
242
								<?php $this->record = GedcomRecord::getInstance($this->curr_xref, $WT_TREE) ?>
243
							<div class="row form-group">
244
								<?= self::createSubmitButton(I18N::translate('previous'), $this->prev_xref) ?>
245
								<?= self::createSubmitButton(I18N::translate('next'), $this->next_xref) ?>
246
							</div>
247
							<div class="row form-group">
248
								<a class="lead" href="<?= e($this->record->url()) ?>"><?= $this->record->getFullName() ?></a>
0 ignored issues
show
Bug introduced by
The method url() does not exist on null. ( Ignorable by Annotation )

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

248
								<a class="lead" href="<?= e($this->record->/** @scrutinizer ignore-call */ url()) ?>"><?= $this->record->getFullName() ?></a>

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

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

Loading history...
249
								<?= $this->PLUGIN->getActionPreview($this->record) ?>
0 ignored issues
show
Bug introduced by
It seems like $this->record can also be of type null; however, parameter $record of Fisharebest\Webtrees\Mod...gin::getActionPreview() does only seem to accept Fisharebest\Webtrees\GedcomRecord, 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

249
								<?= $this->PLUGIN->getActionPreview(/** @scrutinizer ignore-type */ $this->record) ?>
Loading history...
250
							</div>
251
							<div class="row form-group">
252
							<?= implode(' ', $this->PLUGIN->getActionButtons($this->curr_xref)) ?>
253
							</div>
254
						<?php else: ?>
255
							<div class="alert alert-info"><?= I18N::translate('Nothing found.') ?></div>
256
					<?php endif ?>
257
					</div>
258
				<?php endif ?>
259
		<?php endif ?>
260
		</form>
261
		<?php
262
	}
263
264
	/**
265
	 * Find the next record that needs to be updated.
266
	 *
267
	 * @param string $xref
268
	 *
269
	 * @return string|null
270
	 */
271
	private function findNextXref($xref) {
272
		foreach (array_keys($this->all_xrefs) as $key) {
273
			if ($key > $xref) {
274
				$record = self::getLatestRecord($key, $this->all_xrefs[$key]);
275
				if ($this->PLUGIN->doesRecordNeedUpdate($key, $record)) {
0 ignored issues
show
introduced by
The method doesRecordNeedUpdate() does not exist on Fisharebest\Webtrees\Mod...e\BatchUpdateBasePlugin. Maybe you want to declare this class abstract? ( Ignorable by Annotation )

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

275
				if ($this->PLUGIN->/** @scrutinizer ignore-call */ doesRecordNeedUpdate($key, $record)) {
Loading history...
276
					return $key;
277
				}
278
			}
279
		}
280
281
		return null;
282
	}
283
284
	/**
285
	 * Find the previous record that needs to be updated.
286
	 *
287
	 * @param string $xref
288
	 *
289
	 * @return string|null
290
	 */
291
	private function findPrevXref($xref) {
292
		foreach (array_reverse(array_keys($this->all_xrefs)) as $key) {
293
			if ($key < $xref) {
294
				$record = self::getLatestRecord($key, $this->all_xrefs[$key]);
295
				if ($this->PLUGIN->doesRecordNeedUpdate($key, $record)) {
296
					return $key;
297
				}
298
			}
299
		}
300
301
		return null;
302
	}
303
304
	/**
305
	 * Generate a list of all XREFs.
306
	 */
307
	private function getAllXrefs() {
308
		global $WT_TREE;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
309
310
		$sql  = [];
311
		$vars = [];
312
		foreach ($this->PLUGIN->getRecordTypesToUpdate() as $type) {
313
			switch ($type) {
314
				case 'INDI':
315
					$sql[]  = "SELECT i_id, 'INDI' FROM `##individuals` WHERE i_file=?";
316
					$vars[] = $WT_TREE->getTreeId();
317
					break;
318
				case 'FAM':
319
					$sql[]  = "SELECT f_id, 'FAM' FROM `##families` WHERE f_file=?";
320
					$vars[] = $WT_TREE->getTreeId();
321
					break;
322
				case 'SOUR':
323
					$sql[]  = "SELECT s_id, 'SOUR' FROM `##sources` WHERE s_file=?";
324
					$vars[] = $WT_TREE->getTreeId();
325
					break;
326
				case 'OBJE':
327
					$sql[]  = "SELECT m_id, 'OBJE' FROM `##media` WHERE m_file=?";
328
					$vars[] = $WT_TREE->getTreeId();
329
					break;
330
				default:
331
					$sql[]  = "SELECT o_id, ? FROM `##other` WHERE o_type=? AND o_file=?";
332
					$vars[] = $type;
333
					$vars[] = $type;
334
					$vars[] = $WT_TREE->getTreeId();
335
					break;
336
			}
337
		}
338
		$this->all_xrefs = Database::prepare(implode(' UNION ', $sql) . ' ORDER BY 1 ASC')
339
				->execute($vars)
340
				->fetchAssoc();
341
	}
342
343
	/**
344
	 * Scan the plugin folder for a list of plugins
345
	 *
346
	 * @return BatchUpdateBasePlugin[]
347
	 */
348
	private function getPluginList() {
349
		$plugins    = [];
350
		$dir_handle = opendir(__DIR__ . '/BatchUpdate');
351
		while (($file = readdir($dir_handle)) !== false) {
0 ignored issues
show
introduced by
The condition $file = readdir($dir_handle) !== false can never be false.
Loading history...
Bug introduced by
It seems like $dir_handle can also be of type false; however, parameter $dir_handle of readdir() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

351
		while (($file = readdir(/** @scrutinizer ignore-type */ $dir_handle)) !== false) {
Loading history...
352
			if (substr($file, -10) == 'Plugin.php' && $file !== 'BatchUpdateBasePlugin.php') {
353
				$class           = '\Fisharebest\Webtrees\Module\BatchUpdate\\' . basename($file, '.php');
354
				$plugins[$class] = new $class;
355
			}
356
		}
357
		closedir($dir_handle);
358
359
		return $plugins;
360
	}
361
362
	/**
363
	 * Javascript that gets included on every page
364
	 *
365
	 * @return string
366
	 */
367
	private function getJavascript() {
368
		return
369
			'<script>' .
370
			'function reset_reload() {' .
371
			' var bu_form=document.getElementById("batch_update_form");' .
372
			' bu_form.xref.value="";' .
373
			' bu_form.action.value="";' .
374
			' bu_form.data.value="";' .
375
			' bu_form.submit();' .
376
			'}</script>';
377
	}
378
379
	/**
380
	 * Create a submit button for our form
381
	 *
382
	 * @param string $text
383
	 * @param string $xref
384
	 * @param string $action
385
	 * @param string $data
386
	 *
387
	 * @return string
388
	 */
389
	public static function createSubmitButton($text, $xref, $action = '', $data = '') {
390
		return
391
			'<input class="btn btn-primary" type="submit" value="' . $text . '" onclick="' .
392
			'this.form.xref.value=\'' . e($xref) . '\';' .
393
			'this.form.action.value=\'' . e($action) . '\';' .
394
			'this.form.data.value=\'' . e($data) . '\';' .
395
			'return true;"' .
396
			($xref ? '' : ' disabled') . '>';
397
	}
398
399
	/**
400
	 * Get the current view of a record, allowing for pending changes
401
	 *
402
	 * @param string $xref
403
	 * @param string $type
404
	 *
405
	 * @return string
406
	 */
407
	public static function getLatestRecord($xref, $type) {
408
		global $WT_TREE;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
409
410
		switch ($type) {
411
			case 'INDI':
412
				return Individual::getInstance($xref, $WT_TREE)->getGedcom();
413
			case 'FAM':
414
				return Family::getInstance($xref, $WT_TREE)->getGedcom();
415
			case 'SOUR':
416
				return Source::getInstance($xref, $WT_TREE)->getGedcom();
417
			case 'REPO':
418
				return Repository::getInstance($xref, $WT_TREE)->getGedcom();
419
			case 'OBJE':
420
				return Media::getInstance($xref, $WT_TREE)->getGedcom();
421
			case 'NOTE':
422
				return Note::getInstance($xref, $WT_TREE)->getGedcom();
423
			default:
424
				return GedcomRecord::getInstance($xref, $WT_TREE)->getGedcom();
425
		}
426
	}
427
428
	/**
429
	 * The URL to a page where the user can modify the configuration of this module.
430
	 * These links are displayed in the admin page menu.
431
	 *
432
	 * @return string
433
	 */
434
	public function getConfigLink() {
435
		return Html::url('module.php', [
436
			'mod'        => $this->getName(),
437
			'mod_action' => 'admin_batch_update',
438
		]);
439
	}
440
}
441