summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/Functions/FunctionsImport.php36
-rw-r--r--app/GedcomRecord.php387
-rw-r--r--app/Http/Controllers/Admin/ChangesLogController.php319
-rw-r--r--app/Http/Controllers/Admin/ImportThumbnailsController.php14
-rw-r--r--app/Http/Controllers/EditMediaController.php23
-rw-r--r--app/Http/Controllers/PendingChangesController.php326
-rw-r--r--app/Http/RequestHandlers/PendingChanges.php146
-rw-r--r--app/Http/RequestHandlers/PendingChangesAcceptChange.php69
-rw-r--r--app/Http/RequestHandlers/PendingChangesAcceptRecord.php78
-rw-r--r--app/Http/RequestHandlers/PendingChangesAcceptTree.php66
-rw-r--r--app/Http/RequestHandlers/PendingChangesLogAction.php66
-rw-r--r--app/Http/RequestHandlers/PendingChangesLogData.php135
-rw-r--r--app/Http/RequestHandlers/PendingChangesLogDelete.php58
-rw-r--r--app/Http/RequestHandlers/PendingChangesLogDownload.php75
-rw-r--r--app/Http/RequestHandlers/PendingChangesLogPage.php133
-rw-r--r--app/Http/RequestHandlers/PendingChangesRejectChange.php69
-rw-r--r--app/Http/RequestHandlers/PendingChangesRejectRecord.php73
-rw-r--r--app/Http/RequestHandlers/PendingChangesRejectTree.php67
-rw-r--r--app/Module/ModuleThemeTrait.php3
-rw-r--r--app/Module/ReviewChangesModule.php3
-rw-r--r--app/Services/PendingChangesService.php236
-rw-r--r--app/Tree.php25
-rw-r--r--resources/views/admin/changes-log.phtml68
-rw-r--r--resources/views/admin/control-panel.phtml3
-rw-r--r--resources/views/admin/trees.phtml3
-rw-r--r--resources/views/emails/pending-changes-html.phtml9
-rw-r--r--resources/views/emails/pending-changes-text.phtml9
-rw-r--r--resources/views/family-page.phtml6
-rw-r--r--resources/views/gedcom-record-page.phtml8
-rw-r--r--resources/views/individual-page.phtml6
-rw-r--r--resources/views/media-page.phtml5
-rw-r--r--resources/views/note-page.phtml8
-rw-r--r--resources/views/pending-changes-page.phtml52
-rw-r--r--resources/views/repository-page.phtml6
-rw-r--r--resources/views/source-page.phtml8
-rw-r--r--routes/web.php39
-rw-r--r--tests/app/AgeTest.php2
-rw-r--r--tests/app/Http/Controllers/Admin/ChangesLogControllerTest.php106
-rw-r--r--tests/app/Http/Controllers/Admin/ImportThumbnailsControllerTest.php28
39 files changed, 1659 insertions, 1114 deletions
diff --git a/app/Functions/FunctionsImport.php b/app/Functions/FunctionsImport.php
index 04ed276aca..2a5c6feaab 100644
--- a/app/Functions/FunctionsImport.php
+++ b/app/Functions/FunctionsImport.php
@@ -1071,42 +1071,6 @@ class FunctionsImport
}
/**
- * Accept all pending changes for a specified record.
- *
- * @param string $xref
- * @param Tree $tree
- *
- * @return void
- */
- public static function acceptAllChanges($xref, Tree $tree): void
- {
- $changes = DB::table('change')
- ->join('gedcom', 'change.gedcom_id', '=', 'gedcom.gedcom_id')
- ->where('status', '=', 'pending')
- ->where('xref', '=', $xref)
- ->where('gedcom.gedcom_id', '=', $tree->id())
- ->orderBy('change_id')
- ->select(['change_id', 'gedcom_name', 'old_gedcom', 'new_gedcom'])
- ->get();
-
- foreach ($changes as $change) {
- if ($change->new_gedcom === '') {
- // delete
- self::updateRecord($change->old_gedcom, $tree, true);
- } else {
- // add/update
- self::updateRecord($change->new_gedcom, $tree, false);
- }
-
- DB::table('change')
- ->where('change_id', '=', $change->change_id)
- ->update(['status' => 'accepted']);
-
- Log::addEditLog("Accepted change {$change->change_id} for {$xref} / {$change->gedcom_name} into database", $tree);
- }
- }
-
- /**
* update a record in the database
*
* @param string $gedrec
diff --git a/app/GedcomRecord.php b/app/GedcomRecord.php
index 2a14df9541..ef064d6148 100644
--- a/app/GedcomRecord.php
+++ b/app/GedcomRecord.php
@@ -21,16 +21,17 @@ namespace Fisharebest\Webtrees;
use Closure;
use Exception;
-use Fisharebest\Webtrees\Functions\FunctionsImport;
use Fisharebest\Webtrees\Functions\FunctionsPrint;
+use Fisharebest\Webtrees\Services\PendingChangesService;
use Illuminate\Database\Capsule\Manager as DB;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\Expression;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection;
-use Illuminate\Support\Str;
use stdClass;
+use function app;
+
/**
* A GEDCOM object.
*/
@@ -39,37 +40,27 @@ class GedcomRecord
public const RECORD_TYPE = 'UNKNOWN';
protected const ROUTE_NAME = 'record';
-
+ /** @var GedcomRecord[][] Allow getInstance() to return references to existing objects */
+ public static $gedcom_record_cache;
+ /** @var stdClass[][] Fetch all pending edits in one database query */
+ public static $pending_record_cache;
/** @var string The record identifier */
protected $xref;
-
/** @var Tree The family tree to which this record belongs */
protected $tree;
-
/** @var string GEDCOM data (before any pending edits) */
protected $gedcom;
-
/** @var string|null GEDCOM data (after any pending edits) */
protected $pending;
-
/** @var Fact[] facts extracted from $gedcom/$pending */
protected $facts;
-
/** @var string[][] All the names of this individual */
protected $getAllNames;
-
/** @var int|null Cached result */
protected $getPrimaryName;
-
/** @var int|null Cached result */
protected $getSecondaryName;
- /** @var GedcomRecord[][] Allow getInstance() to return references to existing objects */
- public static $gedcom_record_cache;
-
- /** @var stdClass[][] Fetch all pending edits in one database query */
- public static $pending_record_cache;
-
/**
* Create a GedcomRecord object from raw GEDCOM data.
*
@@ -152,45 +143,6 @@ class GedcomRecord
}
/**
- * Split the record into facts
- *
- * @return void
- */
- private function parseFacts(): void
- {
- // Split the record into facts
- if ($this->gedcom) {
- $gedcom_facts = preg_split('/\n(?=1)/s', $this->gedcom);
- array_shift($gedcom_facts);
- } else {
- $gedcom_facts = [];
- }
- if ($this->pending) {
- $pending_facts = preg_split('/\n(?=1)/s', $this->pending);
- array_shift($pending_facts);
- } else {
- $pending_facts = [];
- }
-
- $this->facts = [];
-
- foreach ($gedcom_facts as $gedcom_fact) {
- $fact = new Fact($gedcom_fact, $this, md5($gedcom_fact));
- if ($this->pending !== null && !in_array($gedcom_fact, $pending_facts, true)) {
- $fact->setPendingDeletion();
- }
- $this->facts[] = $fact;
- }
- foreach ($pending_facts as $pending_fact) {
- if (!in_array($pending_fact, $gedcom_facts, true)) {
- $fact = new Fact($pending_fact, $this, md5($pending_fact));
- $fact->setPendingAddition();
- $this->facts[] = $fact;
- }
- }
- }
-
- /**
* Get an instance of a GedcomRecord object. For single records,
* we just receive the XREF. For bulk records (such as lists
* and search results) we can receive the GEDCOM data as well.
@@ -199,8 +151,8 @@ class GedcomRecord
* @param Tree $tree
* @param string|null $gedcom
*
- * @throws Exception
* @return GedcomRecord|Individual|Family|Source|Repository|Media|Note|null
+ * @throws Exception
*/
public static function getInstance(string $xref, Tree $tree, string $gedcom = null)
{
@@ -412,71 +364,6 @@ class GedcomRecord
}
/**
- * Work out whether this record can be shown to a user with a given access level
- *
- * @param int $access_level
- *
- * @return bool
- */
- private function canShowRecord(int $access_level): bool
- {
- // This setting would better be called "$ENABLE_PRIVACY"
- if (!$this->tree->getPreference('HIDE_LIVE_PEOPLE')) {
- return true;
- }
-
- // We should always be able to see our own record (unless an admin is applying download restrictions)
- if ($this->xref() === $this->tree->getUserPreference(Auth::user(), 'gedcomid') && $access_level === Auth::accessLevel($this->tree)) {
- return true;
- }
-
- // Does this record have a RESN?
- if (strpos($this->gedcom, "\n1 RESN confidential") !== false) {
- return Auth::PRIV_NONE >= $access_level;
- }
- if (strpos($this->gedcom, "\n1 RESN privacy") !== false) {
- return Auth::PRIV_USER >= $access_level;
- }
- if (strpos($this->gedcom, "\n1 RESN none") !== false) {
- return true;
- }
-
- // Does this record have a default RESN?
- $individual_privacy = $this->tree->getIndividualPrivacy();
- if (isset($individual_privacy[$this->xref()])) {
- return $individual_privacy[$this->xref()] >= $access_level;
- }
-
- // Privacy rules do not apply to admins
- if (Auth::PRIV_NONE >= $access_level) {
- return true;
- }
-
- // Different types of record have different privacy rules
- return $this->canShowByType($access_level);
- }
-
- /**
- * Each object type may have its own special rules, and re-implement this function.
- *
- * @param int $access_level
- *
- * @return bool
- */
- protected function canShowByType(int $access_level): bool
- {
- $fact_privacy = $this->tree->getFactPrivacy();
-
- if (isset($fact_privacy[static::RECORD_TYPE])) {
- // Restriction found
- return $fact_privacy[static::RECORD_TYPE] >= $access_level;
- }
-
- // No restriction found - must be public:
- return true;
- }
-
- /**
* Can the details of this record be shown?
*
* @param int|null $access_level
@@ -565,80 +452,6 @@ class GedcomRecord
}
/**
- * Generate a private version of this record
- *
- * @param int $access_level
- *
- * @return string
- */
- protected function createPrivateGedcomRecord(int $access_level): string
- {
- return '0 @' . $this->xref . '@ ' . static::RECORD_TYPE . "\n1 NOTE " . I18N::translate('Private');
- }
-
- /**
- * Convert a name record into sortable and full/display versions. This default
- * should be OK for simple record types. INDI/FAM records will need to redefine it.
- *
- * @param string $type
- * @param string $value
- * @param string $gedcom
- *
- * @return void
- */
- protected function addName(string $type, string $value, string $gedcom): void
- {
- $this->getAllNames[] = [
- 'type' => $type,
- 'sort' => preg_replace_callback('/([0-9]+)/', static function (array $matches): string {
- return str_pad($matches[0], 10, '0', STR_PAD_LEFT);
- }, $value),
- 'full' => '<span dir="auto">' . e($value) . '</span>',
- // This is used for display
- 'fullNN' => $value,
- // This goes into the database
- ];
- }
-
- /**
- * Get all the names of a record, including ROMN, FONE and _HEB alternatives.
- * Records without a name (e.g. FAM) will need to redefine this function.
- * Parameters: the level 1 fact containing the name.
- * Return value: an array of name structures, each containing
- * ['type'] = the gedcom fact, e.g. NAME, TITL, FONE, _HEB, etc.
- * ['full'] = the name as specified in the record, e.g. 'Vincent van Gogh' or 'John Unknown'
- * ['sort'] = a sortable version of the name (not for display), e.g. 'Gogh, Vincent' or '@N.N., John'
- *
- * @param int $level
- * @param string $fact_type
- * @param Collection $facts
- *
- * @return void
- */
- protected function extractNamesFromFacts(int $level, string $fact_type, Collection $facts): void
- {
- $sublevel = $level + 1;
- $subsublevel = $sublevel + 1;
- foreach ($facts as $fact) {
- if (preg_match_all("/^{$level} ({$fact_type}) (.+)((\n[{$sublevel}-9].+)*)/m", $fact->gedcom(), $matches, PREG_SET_ORDER)) {
- foreach ($matches as $match) {
- // Treat 1 NAME / 2 TYPE married the same as _MARNM
- if ($match[1] === 'NAME' && strpos($match[3], "\n2 TYPE married") !== false) {
- $this->addName('_MARNM', $match[2], $fact->gedcom());
- } else {
- $this->addName($match[1], $match[2], $fact->gedcom());
- }
- if ($match[3] && preg_match_all("/^{$sublevel} (ROMN|FONE|_\w+) (.+)((\n[{$subsublevel}-9].+)*)/m", $match[3], $submatches, PREG_SET_ORDER)) {
- foreach ($submatches as $submatch) {
- $this->addName($submatch[1], $submatch[2], $match[3]);
- }
- }
- }
- }
- }
- }
-
- /**
* Default for "other" object types
*
* @return void
@@ -1226,7 +1039,7 @@ class GedcomRecord
$this->pending = $new_gedcom;
if (Auth::user()->getPreference('auto_accept')) {
- FunctionsImport::acceptAllChanges($this->xref, $this->tree);
+ app(PendingChangesService::class)->acceptRecord($this);
$this->gedcom = $new_gedcom;
$this->pending = null;
}
@@ -1268,7 +1081,7 @@ class GedcomRecord
// Accept this pending change
if (Auth::user()->getPreference('auto_accept')) {
- FunctionsImport::acceptAllChanges($this->xref, $this->tree);
+ app(PendingChangesService::class)->acceptRecord($this);
$this->gedcom = $gedcom;
$this->pending = null;
}
@@ -1298,7 +1111,7 @@ class GedcomRecord
// Auto-accept this pending change
if (Auth::user()->getPreference('auto_accept')) {
- FunctionsImport::acceptAllChanges($this->xref, $this->tree);
+ app(PendingChangesService::class)->acceptRecord($this);
}
// Clear the cache
@@ -1367,4 +1180,182 @@ class GedcomRecord
return GedcomRecord::getInstance($xref, $this->tree);
})->all();
}
+
+ /**
+ * Each object type may have its own special rules, and re-implement this function.
+ *
+ * @param int $access_level
+ *
+ * @return bool
+ */
+ protected function canShowByType(int $access_level): bool
+ {
+ $fact_privacy = $this->tree->getFactPrivacy();
+
+ if (isset($fact_privacy[static::RECORD_TYPE])) {
+ // Restriction found
+ return $fact_privacy[static::RECORD_TYPE] >= $access_level;
+ }
+
+ // No restriction found - must be public:
+ return true;
+ }
+
+ /**
+ * Generate a private version of this record
+ *
+ * @param int $access_level
+ *
+ * @return string
+ */
+ protected function createPrivateGedcomRecord(int $access_level): string
+ {
+ return '0 @' . $this->xref . '@ ' . static::RECORD_TYPE . "\n1 NOTE " . I18N::translate('Private');
+ }
+
+ /**
+ * Convert a name record into sortable and full/display versions. This default
+ * should be OK for simple record types. INDI/FAM records will need to redefine it.
+ *
+ * @param string $type
+ * @param string $value
+ * @param string $gedcom
+ *
+ * @return void
+ */
+ protected function addName(string $type, string $value, string $gedcom): void
+ {
+ $this->getAllNames[] = [
+ 'type' => $type,
+ 'sort' => preg_replace_callback('/([0-9]+)/', static function (array $matches): string {
+ return str_pad($matches[0], 10, '0', STR_PAD_LEFT);
+ }, $value),
+ 'full' => '<span dir="auto">' . e($value) . '</span>',
+ // This is used for display
+ 'fullNN' => $value,
+ // This goes into the database
+ ];
+ }
+
+ /**
+ * Get all the names of a record, including ROMN, FONE and _HEB alternatives.
+ * Records without a name (e.g. FAM) will need to redefine this function.
+ * Parameters: the level 1 fact containing the name.
+ * Return value: an array of name structures, each containing
+ * ['type'] = the gedcom fact, e.g. NAME, TITL, FONE, _HEB, etc.
+ * ['full'] = the name as specified in the record, e.g. 'Vincent van Gogh' or 'John Unknown'
+ * ['sort'] = a sortable version of the name (not for display), e.g. 'Gogh, Vincent' or '@N.N., John'
+ *
+ * @param int $level
+ * @param string $fact_type
+ * @param Collection $facts
+ *
+ * @return void
+ */
+ protected function extractNamesFromFacts(int $level, string $fact_type, Collection $facts): void
+ {
+ $sublevel = $level + 1;
+ $subsublevel = $sublevel + 1;
+ foreach ($facts as $fact) {
+ if (preg_match_all("/^{$level} ({$fact_type}) (.+)((\n[{$sublevel}-9].+)*)/m", $fact->gedcom(), $matches, PREG_SET_ORDER)) {
+ foreach ($matches as $match) {
+ // Treat 1 NAME / 2 TYPE married the same as _MARNM
+ if ($match[1] === 'NAME' && strpos($match[3], "\n2 TYPE married") !== false) {
+ $this->addName('_MARNM', $match[2], $fact->gedcom());
+ } else {
+ $this->addName($match[1], $match[2], $fact->gedcom());
+ }
+ if ($match[3] && preg_match_all("/^{$sublevel} (ROMN|FONE|_\w+) (.+)((\n[{$subsublevel}-9].+)*)/m", $match[3], $submatches, PREG_SET_ORDER)) {
+ foreach ($submatches as $submatch) {
+ $this->addName($submatch[1], $submatch[2], $match[3]);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Split the record into facts
+ *
+ * @return void
+ */
+ private function parseFacts(): void
+ {
+ // Split the record into facts
+ if ($this->gedcom) {
+ $gedcom_facts = preg_split('/\n(?=1)/s', $this->gedcom);
+ array_shift($gedcom_facts);
+ } else {
+ $gedcom_facts = [];
+ }
+ if ($this->pending) {
+ $pending_facts = preg_split('/\n(?=1)/s', $this->pending);
+ array_shift($pending_facts);
+ } else {
+ $pending_facts = [];
+ }
+
+ $this->facts = [];
+
+ foreach ($gedcom_facts as $gedcom_fact) {
+ $fact = new Fact($gedcom_fact, $this, md5($gedcom_fact));
+ if ($this->pending !== null && !in_array($gedcom_fact, $pending_facts, true)) {
+ $fact->setPendingDeletion();
+ }
+ $this->facts[] = $fact;
+ }
+ foreach ($pending_facts as $pending_fact) {
+ if (!in_array($pending_fact, $gedcom_facts, true)) {
+ $fact = new Fact($pending_fact, $this, md5($pending_fact));
+ $fact->setPendingAddition();
+ $this->facts[] = $fact;
+ }
+ }
+ }
+
+ /**
+ * Work out whether this record can be shown to a user with a given access level
+ *
+ * @param int $access_level
+ *
+ * @return bool
+ */
+ private function canShowRecord(int $access_level): bool
+ {
+ // This setting would better be called "$ENABLE_PRIVACY"
+ if (!$this->tree->getPreference('HIDE_LIVE_PEOPLE')) {
+ return true;
+ }
+
+ // We should always be able to see our own record (unless an admin is applying download restrictions)
+ if ($this->xref() === $this->tree->getUserPreference(Auth::user(), 'gedcomid') && $access_level === Auth::accessLevel($this->tree)) {
+ return true;
+ }
+
+ // Does this record have a RESN?
+ if (strpos($this->gedcom, "\n1 RESN confidential") !== false) {
+ return Auth::PRIV_NONE >= $access_level;
+ }
+ if (strpos($this->gedcom, "\n1 RESN privacy") !== false) {
+ return Auth::PRIV_USER >= $access_level;
+ }
+ if (strpos($this->gedcom, "\n1 RESN none") !== false) {
+ return true;
+ }
+
+ // Does this record have a default RESN?
+ $individual_privacy = $this->tree->getIndividualPrivacy();
+ if (isset($individual_privacy[$this->xref()])) {
+ return $individual_privacy[$this->xref()] >= $access_level;
+ }
+
+ // Privacy rules do not apply to admins
+ if (Auth::PRIV_NONE >= $access_level) {
+ return true;
+ }
+
+ // Different types of record have different privacy rules
+ return $this->canShowByType($access_level);
+ }
}
diff --git a/app/Http/Controllers/Admin/ChangesLogController.php b/app/Http/Controllers/Admin/ChangesLogController.php
deleted file mode 100644
index d1e8d388fb..0000000000
--- a/app/Http/Controllers/Admin/ChangesLogController.php
+++ /dev/null
@@ -1,319 +0,0 @@
-<?php
-
-/**
- * webtrees: online genealogy
- * Copyright (C) 2019 webtrees development team
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-declare(strict_types=1);
-
-namespace Fisharebest\Webtrees\Http\Controllers\Admin;
-
-use Fig\Http\Message\StatusCodeInterface;
-use Fisharebest\Algorithm\MyersDiff;
-use Fisharebest\Webtrees\Auth;
-use Fisharebest\Webtrees\Carbon;
-use Fisharebest\Webtrees\Gedcom;
-use Fisharebest\Webtrees\GedcomRecord;
-use Fisharebest\Webtrees\I18N;
-use Fisharebest\Webtrees\Services\DatatablesService;
-use Fisharebest\Webtrees\Services\TreeService;
-use Fisharebest\Webtrees\Services\UserService;
-use Fisharebest\Webtrees\Tree;
-use Illuminate\Database\Capsule\Manager as DB;
-use Illuminate\Database\Query\Builder;
-use Illuminate\Database\Query\Expression;
-use Psr\Http\Message\ResponseInterface;
-use Psr\Http\Message\ServerRequestInterface;
-use stdClass;
-
-use function explode;
-use function implode;
-use function preg_replace_callback;
-
-/**
- * Controller for the changes log.
- */
-class ChangesLogController extends AbstractAdminController
-{
- /** @var DatatablesService */
- private $datatables_service;
-
- /** @var MyersDiff */
- private $myers_diff;
-
- /** @var TreeService */
- private $tree_service;
-
- /** @var UserService */
- private $user_service;
-
- /**
- * ChangesLogController constructor.
- *
- * @param DatatablesService $datatables_service
- * @param MyersDiff $myers_diff
- * @param TreeService $tree_service
- * @param UserService $user_service
- */
- public function __construct(DatatablesService $datatables_service, MyersDiff $myers_diff, TreeService $tree_service, UserService $user_service)
- {
- $this->datatables_service = $datatables_service;
- $this->myers_diff = $myers_diff;
- $this->tree_service = $tree_service;
- $this->user_service = $user_service;
- }
-
- /**
- * Show the edit history for a tree.
- *
- * @param ServerRequestInterface $request
- *
- * @return ResponseInterface
- */
- public function changesLog(ServerRequestInterface $request): ResponseInterface
- {
- $tree_list = [];
- foreach ($this->tree_service->all() as $tree) {
- if (Auth::isManager($tree)) {
- $tree_list[$tree->name()] = $tree->title();
- }
- }
-
- $user_list = ['' => ''];
- foreach ($this->user_service->all() as $tmp_user) {
- $user_list[$tmp_user->userName()] = $tmp_user->userName();
- }
-
- $action = $request->getQueryParams()['action'] ?? '';
-
- // @TODO This ought to be a POST action
- if ($action === 'delete') {
- $this->changesQuery($request)->delete();
- }
-
- // First and last change in the database.
- $earliest = DB::table('change')->min('change_time');
- $latest = DB::table('change')->max('change_time');
-
- $earliest = $earliest !== null ? Carbon::make($earliest) : Carbon::now();
- $latest = $latest !== null ? Carbon::make($latest) : Carbon::now();
-
- $earliest = $earliest->toDateString();
- $latest = $latest->toDateString();
-
- $ged = $request->getQueryParams()['ged'] ?? '';
- $from = $request->getQueryParams()['from'] ?? $earliest;
- $to = $request->getQueryParams()['to'] ?? $latest;
- $type = $request->getQueryParams()['type'] ?? '';
- $oldged = $request->getQueryParams()['oldged'] ?? '';
- $newged = $request->getQueryParams()['newged'] ?? '';
- $xref = $request->getQueryParams()['xref'] ?? '';
- $username = $request->getQueryParams()['username'] ?? '';
- $search = $request->getQueryParams()['search'] ?? [];
- $search = $search['value'] ?? null;
-
- if (!array_key_exists($ged, $tree_list)) {
- $ged = reset($tree_list);
- }
-
- $statuses = [
- '' => '',
- /* I18N: the status of an edit accepted/rejected/pending */
- 'accepted' => I18N::translate('accepted'),
- /* I18N: the status of an edit accepted/rejected/pending */
- 'rejected' => I18N::translate('rejected'),
- /* I18N: the status of an edit accepted/rejected/pending */
- 'pending' => I18N::translate('pending'),
- ];
-
- return $this->viewResponse('admin/changes-log', [
- 'action' => $action,
- 'earliest' => $earliest,
- 'from' => $from,
- 'ged' => $ged,
- 'latest' => $latest,
- 'newged' => $newged,
- 'oldged' => $oldged,
- 'search' => $search,
- 'statuses' => $statuses,
- 'title' => I18N::translate('Changes log'),
- 'to' => $to,
- 'tree_list' => $tree_list,
- 'type' => $type,
- 'username' => $username,
- 'user_list' => $user_list,
- 'xref' => $xref,
- ]);
- }
-
- /**
- * Show the edit history for a tree.
- *
- * @param ServerRequestInterface $request
- *
- * @return ResponseInterface
- */
- public function changesLogData(ServerRequestInterface $request): ResponseInterface
- {
- $query = $this->changesQuery($request);
-
- $callback = function (stdClass $row): array {
- $old_lines = explode("\n", $row->old_gedcom);
- $new_lines = explode("\n", $row->new_gedcom);
-
- $differences = $this->myers_diff->calculate($old_lines, $new_lines);
- $diff_lines = [];
-
- foreach ($differences as $difference) {
- switch ($difference[1]) {
- case MyersDiff::DELETE:
- $diff_lines[] = '<del>' . $difference[0] . '</del>';
- break;
- case MyersDiff::INSERT:
- $diff_lines[] = '<ins>' . $difference[0] . '</ins>';
- break;
- default:
- $diff_lines[] = $difference[0];
- }
- }
-
- // Only convert valid xrefs to links
- $tree = $this->tree_service->all()->get($row->gedcom_name);
- $record = GedcomRecord::getInstance($row->xref, $tree);
-
- return [
- $row->change_id,
- Carbon::make($row->change_time)->local()->format('Y-m-d H:i:s'),
- I18N::translate($row->status),
- $record ? '<a href="' . e($record->url()) . '">' . $record->xref() . '</a>' : $row->xref,
- '<div class="gedcom-data" dir="ltr">' .
- preg_replace_callback(
- '/@(' . Gedcom::REGEX_XREF . ')@/',
- static function (array $match) use ($tree): string {
- $record = GedcomRecord::getInstance($match[1], $tree);
-
- return $record ? '<a href="' . e($record->url()) . '">' . $match[0] . '</a>' : $match[0];
- },
- implode("\n", $diff_lines)
- ) .
- '</div>',
- $row->user_name,
- $row->gedcom_name,
- ];
- };
-
- return $this->datatables_service->handle($request, $query, [], [], $callback);
- }
-
- /**
- * Show the edit history for a tree.
- *
- * @param ServerRequestInterface $request
- *
- * @return ResponseInterface
- */
- public function changesLogDownload(ServerRequestInterface $request): ResponseInterface
- {
- $content = $this->changesQuery($request)
- ->get()
- ->map(static function (stdClass $row): string {
- // Convert to CSV
- return implode(',', [
- '"' . $row->change_time . '"',
- '"' . $row->status . '"',
- '"' . $row->xref . '"',
- '"' . str_replace('"', '""', $row->old_gedcom) . '"',
- '"' . str_replace('"', '""', $row->new_gedcom) . '"',
- '"' . str_replace('"', '""', $row->user_name) . '"',
- '"' . str_replace('"', '""', $row->gedcom_name) . '"',
- ]);
- })
- ->implode("\n");
-
- return response($content, StatusCodeInterface::STATUS_OK, [
- 'Content-Type' => 'text/csv; charset=utf-8',
- 'Content-Length' => strlen($content),
- 'Content-Disposition' => 'attachment; filename="changes.csv"',
- ]);
- }
-
- /**
- * Generate a query for filtering the changes log.
- *
- * @param ServerRequestInterface $request
- *
- * @return Builder
- */
- private function changesQuery(ServerRequestInterface $request): Builder
- {
- $from = $request->getQueryParams()['from'] ?? '';
- $to = $request->getQueryParams()['to'] ?? '';
- $type = $request->getQueryParams()['type'] ?? '';
- $oldged = $request->getQueryParams()['oldged'] ?? '';
- $newged = $request->getQueryParams()['newged'] ?? '';
- $xref = $request->getQueryParams()['xref'] ?? '';
- $username = $request->getQueryParams()['username'] ?? '';
- $ged = $request->getQueryParams()['ged'] ?? '';
- $search = $request->getQueryParams()['search'] ?? [];
- $search = $search['value'] ?? '';
-
-
- $query = DB::table('change')
- ->leftJoin('user', 'user.user_id', '=', 'change.user_id')
- ->join('gedcom', 'gedcom.gedcom_id', '=', 'change.gedcom_id')
- ->select(['change.*', new Expression("COALESCE(user_name, '<none>') AS user_name"), 'gedcom_name']);
-
- if ($search !== '') {
- $query->where(static function (Builder $query) use ($search): void {
- $query
- ->whereContains('old_gedcom', $search)
- ->whereContains('new_gedcom', $search, 'or');
- });
- }
-
- if ($from !== '') {
- $query->where('change_time', '>=', $from);
- }
-
- if ($to !== '') {
- // before end of the day
- $query->where('change_time', '<', Carbon::make($to)->addDay());
- }
-
- if ($type !== '') {
- $query->where('status', '=', $type);
- }
-
- if ($oldged !== '') {
- $query->whereContains('old_gedcom', $oldged);
- }
- if ($newged !== '') {
- $query->whereContains('new_gedcom', $oldged);
- }
-
- if ($xref !== '') {
- $query->where('xref', '=', $xref);
- }
-
- if ($username !== '') {
- $query->whereContains('user_name', $username);
- }
-
- if ($ged !== '') {
- $query->whereContains('gedcom_name', $ged);
- }
-
- return $query;
- }
-}
diff --git a/app/Http/Controllers/Admin/ImportThumbnailsController.php b/app/Http/Controllers/Admin/ImportThumbnailsController.php
index 012479f7ab..07340a71c8 100644
--- a/app/Http/Controllers/Admin/ImportThumbnailsController.php
+++ b/app/Http/Controllers/Admin/ImportThumbnailsController.php
@@ -23,6 +23,7 @@ use FilesystemIterator;
use Fisharebest\Webtrees\Functions\FunctionsImport;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Media;
+use Fisharebest\Webtrees\Services\PendingChangesService;
use Fisharebest\Webtrees\Services\TreeService;
use Fisharebest\Webtrees\Webtrees;
use Illuminate\Database\Capsule\Manager as DB;
@@ -43,14 +44,19 @@ class ImportThumbnailsController extends AbstractAdminController
/** @var TreeService */
private $tree_service;
+ /** @var PendingChangesService */
+ private $pending_changes_service;
+
/**
* ImportThumbnailsController constructor.
*
- * @param TreeService $tree_service
+ * @param PendingChangesService $pending_changes_service
+ * @param TreeService $tree_service
*/
- public function __construct(TreeService $tree_service)
+ public function __construct(PendingChangesService $pending_changes_service, TreeService $tree_service)
{
- $this->tree_service = $tree_service;
+ $this->pending_changes_service = $pending_changes_service;
+ $this->tree_service = $tree_service;
}
/**
@@ -120,7 +126,7 @@ class ImportThumbnailsController extends AbstractAdminController
}
// Accept the changes, to keep the filesystem in sync with the GEDCOM data.
- FunctionsImport::acceptAllChanges($media_object->xref(), $media_object->tree());
+ $this->pending_changes_service->acceptRecord($media_object);
}
break;
}
diff --git a/app/Http/Controllers/EditMediaController.php b/app/Http/Controllers/EditMediaController.php
index 84325d5d66..ef7d589ee7 100644
--- a/app/Http/Controllers/EditMediaController.php
+++ b/app/Http/Controllers/EditMediaController.php
@@ -23,12 +23,12 @@ use Exception;
use Fig\Http\Message\StatusCodeInterface;
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\FlashMessages;
-use Fisharebest\Webtrees\Functions\FunctionsImport;
use Fisharebest\Webtrees\GedcomRecord;
use Fisharebest\Webtrees\GedcomTag;
use Fisharebest\Webtrees\Html;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Media;
+use Fisharebest\Webtrees\Services\PendingChangesService;
use Fisharebest\Webtrees\Tree;
use Illuminate\Database\Capsule\Manager as DB;
use InvalidArgumentException;
@@ -63,6 +63,19 @@ class EditMediaController extends AbstractEditController
'confidential',
];
+ /** @var PendingChangesService */
+ private $pending_changes_service;
+
+ /**
+ * EditMediaController constructor.
+ *
+ * @param PendingChangesService $pending_changes_service
+ */
+ public function __construct(PendingChangesService $pending_changes_service)
+ {
+ $this->pending_changes_service = $pending_changes_service;
+ }
+
/**
* Add a media file to an existing media object.
*
@@ -135,7 +148,7 @@ class EditMediaController extends AbstractEditController
$media->createFact($gedcom, true);
// Accept the changes, to keep the filesystem in sync with the GEDCOM data.
- FunctionsImport::acceptAllChanges($media->xref(), $tree);
+ $this->pending_changes_service->acceptRecord($media);
return redirect($media->url());
}
@@ -266,7 +279,7 @@ class EditMediaController extends AbstractEditController
// Accept the changes, to keep the filesystem in sync with the GEDCOM data.
if ($old !== $new && !$media_file->isExternal()) {
- FunctionsImport::acceptAllChanges($media->xref(), $tree);
+ $this->pending_changes_service->acceptRecord($media);
}
return redirect($media->url());
@@ -327,7 +340,7 @@ class EditMediaController extends AbstractEditController
$media_object = $tree->createRecord($gedcom);
// Accept the new record. Rejecting it would leave the filesystem out-of-sync with the genealogy
- FunctionsImport::acceptAllChanges($media_object->xref(), $tree);
+ $this->pending_changes_service->acceptRecord($media_object);
return redirect($media_object->url());
}
@@ -383,7 +396,7 @@ class EditMediaController extends AbstractEditController
$record = $tree->createMediaObject($gedcom);
// Accept the new record to keep the filesystem synchronized with the genealogy.
- FunctionsImport::acceptAllChanges($record->xref(), $record->tree());
+ $this->pending_changes_service->acceptRecord($record);
return response([
'id' => $record->xref(),
diff --git a/app/Http/Controllers/PendingChangesController.php b/app/Http/Controllers/PendingChangesController.php
deleted file mode 100644
index 91fb1c59e8..0000000000
--- a/app/Http/Controllers/PendingChangesController.php
+++ /dev/null
@@ -1,326 +0,0 @@
-<?php
-
-/**
- * webtrees: online genealogy
- * Copyright (C) 2019 webtrees development team
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-declare(strict_types=1);
-
-namespace Fisharebest\Webtrees\Http\Controllers;
-
-use Fisharebest\Webtrees\Auth;
-use Fisharebest\Webtrees\Carbon;
-use Fisharebest\Webtrees\Family;
-use Fisharebest\Webtrees\FlashMessages;
-use Fisharebest\Webtrees\Functions\FunctionsImport;
-use Fisharebest\Webtrees\Gedcom;
-use Fisharebest\Webtrees\GedcomRecord;
-use Fisharebest\Webtrees\I18N;
-use Fisharebest\Webtrees\Individual;
-use Fisharebest\Webtrees\Log;
-use Fisharebest\Webtrees\Media;
-use Fisharebest\Webtrees\Note;
-use Fisharebest\Webtrees\Repository;
-use Fisharebest\Webtrees\Source;
-use Fisharebest\Webtrees\Tree;
-use Illuminate\Database\Capsule\Manager as DB;
-use InvalidArgumentException;
-use Psr\Http\Message\ResponseInterface;
-use Psr\Http\Message\ServerRequestInterface;
-
-use function assert;
-use function route;
-
-/**
- * Show, accept and reject pending changes.
- */
-class PendingChangesController extends AbstractBaseController
-{
- /**
- * Accept all changes to a tree.
- *
- * @param ServerRequestInterface $request
- *
- * @return ResponseInterface
- */
- public function acceptAllChanges(ServerRequestInterface $request): ResponseInterface
- {
- $tree = $request->getAttribute('tree');
- assert($tree instanceof Tree, new InvalidArgumentException());
-
- $url = $request->getQueryParams()['url'];
-
- $changes = DB::table('change')
- ->where('gedcom_id', '=', $tree->id())
- ->where('status', '=', 'pending')
- ->orderBy('change_id')
- ->get();
-
- foreach ($changes as $change) {
- if ($change->new_gedcom === '') {
- // delete
- FunctionsImport::updateRecord($change->old_gedcom, $tree, true);
- } else {
- // add/update
- FunctionsImport::updateRecord($change->new_gedcom, $tree, false);
- }
-
- DB::table('change')
- ->where('change_id', '=', $change->change_id)
- ->update(['status' => 'accepted']);
-
- Log::addEditLog('Accepted change ' . $change->change_id . ' for ' . $change->xref . ' / ' . $tree->name(), $tree);
- }
-
- return redirect(route('show-pending', [
- 'tree' => $tree->name(),
- 'url' => $url,
- ]));
- }
-
- /**
- * Accept a change (and all previous changes) to a single record.
- *
- * @param ServerRequestInterface $request
- *
- * @return ResponseInterface
- */
- public function acceptChange(ServerRequestInterface $request): ResponseInterface
- {
- $tree = $request->getAttribute('tree');
- $params = $request->getQueryParams();
- $url = $params['url'];
- $xref = $params['xref'];
- $change_id = $params['change_id'];
-
- $changes = DB::table('change')
- ->where('gedcom_id', '=', $tree->id())
- ->where('xref', '=', $xref)
- ->where('change_id', '<=', $change_id)
- ->where('status', '=', 'pending')
- ->orderBy('change_id')
- ->get();
-
- foreach ($changes as $change) {
- if ($change->new_gedcom === '') {
- // delete
- FunctionsImport::updateRecord($change->old_gedcom, $tree, true);
- } else {
- // add/update
- FunctionsImport::updateRecord($change->new_gedcom, $tree, false);
- }
-
- DB::table('change')
- ->where('change_id', '=', $change->change_id)
- ->update(['status' => 'accepted']);
-
- Log::addEditLog('Accepted change ' . $change->change_id . ' for ' . $change->xref . ' / ' . $tree->name(), $tree);
- }
-
- return redirect(route('show-pending', [
- 'tree' => $tree->name(),
- 'url' => $url,
- ]));
- }
-
- /**
- * Accept all changes to a single record.
- *
- * @param ServerRequestInterface $request
- *
- * @return ResponseInterface
- */
- public function acceptChanges(ServerRequestInterface $request): ResponseInterface
- {
- $tree = $request->getAttribute('tree');
- $xref = $request->getAttribute('xref');
- $record = GedcomRecord::getInstance($xref, $tree);
-
- Auth::checkRecordAccess($record, false);
-
- if ($record && Auth::isModerator($tree)) {
- if ($record->isPendingDeletion()) {
- /* I18N: %s is the name of a genealogy record */
- FlashMessages::addMessage(I18N::translate('“%s” has been deleted.', $record->fullName()));
- } else {
- /* I18N: %s is the name of a genealogy record */
- FlashMessages::addMessage(I18N::translate('The changes to “%s” have been accepted.', $record->fullName()));
- }
- FunctionsImport::acceptAllChanges($record->xref(), $record->tree());
- }
-
- return response();
- }
-
- /**
- * Reject all changes to a tree.
- *
- * @param ServerRequestInterface $request
- *
- * @return ResponseInterface
- */
- public function rejectAllChanges(ServerRequestInterface $request): ResponseInterface
- {
- $tree = $request->getAttribute('tree');
- assert($tree instanceof Tree, new InvalidArgumentException());
-
- $url = $request->getQueryParams()['url'];
-
- DB::table('change')
- ->where('gedcom_id', '=', $tree->id())
- ->where('status', '=', 'pending')
- ->update(['status' => 'rejected']);
-
- return redirect(route('show-pending', [
- 'tree' => $tree->name(),
- 'url' => $url,
- ]));
- }
-
- /**
- * Reject a change (and all subsequent changes) to a single record.
- *
- * @param ServerRequestInterface $request
- *
- * @return ResponseInterface
- */
- public function rejectChange(ServerRequestInterface $request): ResponseInterface
- {
- $tree = $request->getAttribute('tree');
- $params = $request->getQueryParams();
- $url = $params['url'];
- $xref = $params['xref'];
- $change_id = $params['change_id'];
-
- // Reject a change, and subsequent changes to the same record
- DB::table('change')
- ->where('gedcom_id', '=', $tree->id())
- ->where('xref', '=', $xref)
- ->where('change_id', '>=', $change_id)
- ->where('status', '=', 'pending')
- ->update(['status' => 'rejected']);
-
- return redirect(route('show-pending', [
- 'tree' => $tree->name(),
- 'url' => $url,
- ]));
- }
-
- /**
- * Accept all changes to a single record.
- *
- * @param ServerRequestInterface $request
- *
- * @return ResponseInterface
- */
- public function rejectChanges(ServerRequestInterface $request): ResponseInterface
- {
- $tree = $request->getAttribute('tree');
- $xref = $request->getAttribute('xref');
- $record = GedcomRecord::getInstance($xref, $tree);
-
- Auth::checkRecordAccess($record);
-
- if (Auth::isModerator($tree)) {
- DB::table('change')
- ->where('gedcom_id', '=', $record->tree()->id())
- ->where('xref', '=', $record->xref())
- ->where('status', '=', 'pending')
- ->update(['status' => 'rejected']);
-
- /* I18N: %s is the name of an individual, source or other record */
- FlashMessages::addMessage(I18N::translate('The changes to “%s” have been rejected.', $record->fullName()));
- }
-
- return response();
- }
-
- /**
- * Show the pending changes for the current tree.
- *
- * @param ServerRequestInterface $request
- *
- * @return ResponseInterface
- */
- public function showChanges(ServerRequestInterface $request): ResponseInterface
- {
- $tree = $request->getAttribute('tree');
- $default_url = route('tree-page', ['tree' => $tree->name()]);
-
- $url = $request->getQueryParams()['url'] ?? $default_url;
-
- $rows = DB::table('change')
- ->join('user', 'user.user_id', '=', 'change.user_id')
- ->join('gedcom', 'gedcom.gedcom_id', '=', 'change.gedcom_id')
- ->where('status', '=', 'pending')
- ->orderBy('change.gedcom_id')
- ->orderBy('change.xref')
- ->orderBy('change.change_id')
- ->select(['change.*', 'user.user_name', 'user.real_name', 'gedcom_name'])
- ->get();
-
- $changes = [];
- foreach ($rows as $row) {
- $row->change_time = Carbon::make($row->change_time);
-
- $change_tree = Tree::findById((int) $row->gedcom_id);
-
- preg_match('/^0 (?:@' . Gedcom::REGEX_XREF . '@ )?(' . Gedcom::REGEX_TAG . ')/', $row->old_gedcom . $row->new_gedcom, $match);
-
- switch ($match[1]) {
- case 'INDI':
- $row->record = new Individual($row->xref, $row->old_gedcom, $row->new_gedcom, $change_tree);
- break;
- case 'FAM':
- $row->record = new Family($row->xref, $row->old_gedcom, $row->new_gedcom, $change_tree);
- break;
- case 'SOUR':
- $row->record = new Source($row->xref, $row->old_gedcom, $row->new_gedcom, $change_tree);
- break;
- case 'REPO':
- $row->record = new Repository($row->xref, $row->old_gedcom, $row->new_gedcom, $change_tree);
- break;
- case 'OBJE':
- $row->record = new Media($row->xref, $row->old_gedcom, $row->new_gedcom, $change_tree);
- break;
- case 'NOTE':
- $row->record = new Note($row->xref, $row->old_gedcom, $row->new_gedcom, $change_tree);
- break;
- default:
- $row->record = new GedcomRecord($row->xref, $row->old_gedcom, $row->new_gedcom, $change_tree);
- break;
- }
-
- $changes[$row->gedcom_id][$row->xref][] = $row;
- }
-
- $title = I18N::translate('Pending changes');
-
- // If the current tree has changes, activate that tab. Otherwise activate the first tab.
- if (($changes[$tree->id()] ?? []) === []) {
- reset($changes);
- $active_tree_id = key($changes);
- } else {
- $active_tree_id = $tree->id();
- }
-
- return $this->viewResponse('pending-changes-page', [
- 'active_tree_id' => $active_tree_id,
- 'changes' => $changes,
- 'title' => $title,
- 'tree' => $tree,
- 'url' => $url,
- ]);
- }
-}
diff --git a/app/Http/RequestHandlers/PendingChanges.php b/app/Http/RequestHandlers/PendingChanges.php
new file mode 100644
index 0000000000..94bfec4349
--- /dev/null
+++ b/app/Http/RequestHandlers/PendingChanges.php
@@ -0,0 +1,146 @@
+<?php
+
+/**
+ * webtrees: online genealogy
+ * Copyright (C) 2019 webtrees development team
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+declare(strict_types=1);
+
+namespace Fisharebest\Webtrees\Http\RequestHandlers;
+
+use Fisharebest\Webtrees\Carbon;
+use Fisharebest\Webtrees\Family;
+use Fisharebest\Webtrees\Gedcom;
+use Fisharebest\Webtrees\GedcomRecord;
+use Fisharebest\Webtrees\Http\ViewResponseTrait;
+use Fisharebest\Webtrees\I18N;
+use Fisharebest\Webtrees\Individual;
+use Fisharebest\Webtrees\Media;
+use Fisharebest\Webtrees\Note;
+use Fisharebest\Webtrees\Repository;
+use Fisharebest\Webtrees\Services\PendingChangesService;
+use Fisharebest\Webtrees\Services\TreeService;
+use Fisharebest\Webtrees\Source;
+use Fisharebest\Webtrees\Tree;
+use Illuminate\Database\Capsule\Manager as DB;
+use InvalidArgumentException;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+
+use function assert;
+use function key;
+use function preg_match;
+use function reset;
+use function route;
+
+/**
+ * Show all pending changes.
+ */
+class PendingChanges implements RequestHandlerInterface
+{
+ use ViewResponseTrait;
+
+ /** @var PendingChangesService */
+ private $pending_changes_service;
+
+ /** @var TreeService */
+ private $tree_service;
+
+ /**
+ * @param PendingChangesService $pending_changes_service
+ * @param TreeService $tree_service
+ */
+ public function __construct(PendingChangesService $pending_changes_service, TreeService $tree_service)
+ {
+ $this->pending_changes_service = $pending_changes_service;
+ $this->tree_service = $tree_service;
+ }
+
+ /**
+ * @param ServerRequestInterface $request
+ *
+ * @return ResponseInterface
+ */
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $tree = $request->getAttribute('tree');
+ assert($tree instanceof Tree, new InvalidArgumentException());
+
+ $url = $request->getQueryParams()['url'] ?? route('tree-page', ['tree' => $tree->name()]);
+
+ $rows = DB::table('change')
+ ->join('user', 'user.user_id', '=', 'change.user_id')
+ ->join('gedcom', 'gedcom.gedcom_id', '=', 'change.gedcom_id')
+ ->where('status', '=', 'pending')
+ ->orderBy('change.gedcom_id')
+ ->orderBy('change.xref')
+ ->orderBy('change.change_id')
+ ->select(['change.*', 'user.user_name', 'user.real_name', 'gedcom_name'])
+ ->get();
+
+ $changes = [];
+ foreach ($rows as $row) {
+ $row->change_time = Carbon::make($row->change_time);
+
+ $change_tree = $this->tree_service->all()->get($row->gedcom_name);
+
+ preg_match('/^0 (?:@' . Gedcom::REGEX_XREF . '@ )?(' . Gedcom::REGEX_TAG . ')/', $row->old_gedcom . $row->new_gedcom, $match);
+
+ switch ($match[1]) {
+ case 'INDI':
+ $row->record = new Individual($row->xref, $row->old_gedcom, $row->new_gedcom, $change_tree);
+ break;
+ case 'FAM':
+ $row->record = new Family($row->xref, $row->old_gedcom, $row->new_gedcom, $change_tree);
+ break;
+ case 'SOUR':
+ $row->record = new Source($row->xref, $row->old_gedcom, $row->new_gedcom, $change_tree);
+ break;
+ case 'REPO':
+ $row->record = new Repository($row->xref, $row->old_gedcom, $row->new_gedcom, $change_tree);
+ break;
+ case 'OBJE':
+ $row->record = new Media($row->xref, $row->old_gedcom, $row->new_gedcom, $change_tree);
+ break;
+ case 'NOTE':
+ $row->record = new Note($row->xref, $row->old_gedcom, $row->new_gedcom, $change_tree);
+ break;
+ default:
+ $row->record = new GedcomRecord($row->xref, $row->old_gedcom, $row->new_gedcom, $change_tree);
+ break;
+ }
+
+ $changes[$row->gedcom_id][$row->xref][] = $row;
+ }
+
+ $title = I18N::translate('Pending changes');
+
+ // If the current tree has changes, activate that tab. Otherwise activate the first tab.
+ if (($changes[$tree->id()] ?? []) === []) {
+ reset($changes);
+ $active_tree_id = key($changes);
+ } else {
+ $active_tree_id = $tree->id();
+ }
+
+ return $this->viewResponse('pending-changes-page', [
+ 'active_tree_id' => $active_tree_id,
+ 'changes' => $changes,
+ 'title' => $title,
+ 'tree' => $tree,
+ 'url' => $url,
+ ]);
+ }
+}
diff --git a/app/Http/RequestHandlers/PendingChangesAcceptChange.php b/app/Http/RequestHandlers/PendingChangesAcceptChange.php
new file mode 100644
index 0000000000..4130482850
--- /dev/null
+++ b/app/Http/RequestHandlers/PendingChangesAcceptChange.php
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * webtrees: online genealogy
+ * Copyright (C) 2019 webtrees development team
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+declare(strict_types=1);
+
+namespace Fisharebest\Webtrees\Http\RequestHandlers;
+
+use Fisharebest\Webtrees\GedcomRecord;
+use Fisharebest\Webtrees\Services\PendingChangesService;
+use Fisharebest\Webtrees\Tree;
+use InvalidArgumentException;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+
+use function assert;
+use function response;
+
+/**
+ * Accept pending changes for a record.
+ */
+class PendingChangesAcceptChange implements RequestHandlerInterface
+{
+ /** @var PendingChangesService */
+ private $pending_changes_service;
+
+ /**
+ * @param PendingChangesService $pending_changes_service
+ */
+ public function __construct(PendingChangesService $pending_changes_service)
+ {
+ $this->pending_changes_service = $pending_changes_service;
+ }
+
+ /**
+ * @param ServerRequestInterface $request
+ *
+ * @return ResponseInterface
+ */
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $tree = $request->getAttribute('tree');
+ assert($tree instanceof Tree, new InvalidArgumentException());
+
+ $xref = $request->getAttribute('xref');
+ $record = GedcomRecord::getInstance($xref, $tree);
+ $change = $request->getAttribute('change');
+
+ if ($record instanceof GedcomRecord) {
+ $this->pending_changes_service->acceptChange($record, $change);
+ }
+
+ return response();
+ }
+}
diff --git a/app/Http/RequestHandlers/PendingChangesAcceptRecord.php b/app/Http/RequestHandlers/PendingChangesAcceptRecord.php
new file mode 100644
index 0000000000..645e08cecb
--- /dev/null
+++ b/app/Http/RequestHandlers/PendingChangesAcceptRecord.php
@@ -0,0 +1,78 @@
+<?php
+
+/**
+ * webtrees: online genealogy
+ * Copyright (C) 2019 webtrees development team
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+declare(strict_types=1);
+
+namespace Fisharebest\Webtrees\Http\RequestHandlers;
+
+use Fisharebest\Webtrees\FlashMessages;
+use Fisharebest\Webtrees\GedcomRecord;
+use Fisharebest\Webtrees\I18N;
+use Fisharebest\Webtrees\Services\PendingChangesService;
+use Fisharebest\Webtrees\Tree;
+use InvalidArgumentException;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+
+use function assert;
+use function response;
+
+/**
+ * Accept pending changes for a record.
+ */
+class PendingChangesAcceptRecord implements RequestHandlerInterface
+{
+ /** @var PendingChangesService */
+ private $pending_changes_service;
+
+ /**
+ * @param PendingChangesService $pending_changes_service
+ */
+ public function __construct(PendingChangesService $pending_changes_service)
+ {
+ $this->pending_changes_service = $pending_changes_service;
+ }
+
+ /**
+ * @param ServerRequestInterface $request
+ *
+ * @return ResponseInterface
+ */
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $tree = $request->getAttribute('tree');
+ assert($tree instanceof Tree, new InvalidArgumentException());
+
+ $xref = $request->getAttribute('xref') ?? '';
+ $record = GedcomRecord::getInstance($xref, $tree);
+
+ if ($record) {
+ if ($record->isPendingDeletion()) {
+ /* I18N: %s is the name of a genealogy record */
+ FlashMessages::addMessage(I18N::translate('“%s” has been deleted.', $record->fullName()));
+ } else {
+ /* I18N: %s is the name of a genealogy record */
+ FlashMessages::addMessage(I18N::translate('The changes to “%s” have been accepted.', $record->fullName()));
+ }
+
+ $this->pending_changes_service->acceptRecord($record);
+ }
+
+ return response();
+ }
+}
diff --git a/app/Http/RequestHandlers/PendingChangesAcceptTree.php b/app/Http/RequestHandlers/PendingChangesAcceptTree.php
new file mode 100644
index 0000000000..18d505941e
--- /dev/null
+++ b/app/Http/RequestHandlers/PendingChangesAcceptTree.php
@@ -0,0 +1,66 @@
+<?php
+
+/**
+ * webtrees: online genealogy
+ * Copyright (C) 2019 webtrees development team
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+declare(strict_types=1);
+
+namespace Fisharebest\Webtrees\Http\RequestHandlers;
+
+use Fisharebest\Webtrees\FlashMessages;
+use Fisharebest\Webtrees\I18N;
+use Fisharebest\Webtrees\Services\PendingChangesService;
+use Fisharebest\Webtrees\Tree;
+use InvalidArgumentException;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+
+use function assert;
+use function e;
+use function response;
+
+/**
+ * Accept pending changes for a tree.
+ */
+class PendingChangesAcceptTree implements RequestHandlerInterface
+{
+ /** @var PendingChangesService */
+ private $pending_changes_service;
+
+ /**
+ * @param PendingChangesService $pending_changes_service
+ */
+ public function __construct(PendingChangesService $pending_changes_service)
+ {
+ $this->pending_changes_service = $pending_changes_service;
+ }
+
+ /**
+ * @param ServerRequestInterface $request
+ *
+ * @return ResponseInterface
+ */
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $tree = $request->getAttribute('tree');
+ assert($tree instanceof Tree, new InvalidArgumentException());
+
+ $this->pending_changes_service->acceptTree($tree);
+
+ FlashMessages::addMessage(I18N::translate('The changes to “%s” have been accepted.', e($tree->title())));
+ return response();
+ }
+}
diff --git a/app/Http/RequestHandlers/PendingChangesLogAction.php b/app/Http/RequestHandlers/PendingChangesLogAction.php
new file mode 100644
index 0000000000..c2ddd37389
--- /dev/null
+++ b/app/Http/RequestHandlers/PendingChangesLogAction.php
@@ -0,0 +1,66 @@
+<?php
+
+/**
+ * webtrees: online genealogy
+ * Copyright (C) 2019 webtrees development team
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+declare(strict_types=1);
+
+namespace Fisharebest\Webtrees\Http\RequestHandlers;
+
+use Fisharebest\Webtrees\Services\PendingChangesService;
+use Fisharebest\Webtrees\Tree;
+use InvalidArgumentException;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+
+use function assert;
+use function response;
+
+/**
+ * Show pending changes.
+ */
+class PendingChangesLogAction implements RequestHandlerInterface
+{
+ /** @var PendingChangesService */
+ private $pending_changes_service;
+
+ /**
+ * @param PendingChangesService $pending_changes_service
+ */
+ public function __construct(PendingChangesService $pending_changes_service)
+ {
+ $this->pending_changes_service = $pending_changes_service;
+ }
+
+ /**
+ * @param ServerRequestInterface $request
+ *
+ * @return ResponseInterface
+ */
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ return redirect(route(PendingChangesLogPage::class, [
+ 'tree' => $request->getParsedBody()['tree'],
+ 'from' => $request->getParsedBody()['from'] ?? '',
+ 'to' => $request->getParsedBody()['to'] ?? '',
+ 'type' => $request->getParsedBody()['type'] ?? '',
+ 'oldged' => $request->getParsedBody()['oldged'] ?? '',
+ 'newged' => $request->getParsedBody()['newged'] ?? '',
+ 'xref' => $request->getParsedBody()['xref'] ?? '',
+ 'username' => $request->getParsedBody()['username'] ?? '',
+ ]));
+ }
+}
diff --git a/app/Http/RequestHandlers/PendingChangesLogData.php b/app/Http/RequestHandlers/PendingChangesLogData.php
new file mode 100644
index 0000000000..a4af95c3b3
--- /dev/null
+++ b/app/Http/RequestHandlers/PendingChangesLogData.php
@@ -0,0 +1,135 @@
+<?php
+
+/**
+ * webtrees: online genealogy
+ * Copyright (C) 2019 webtrees development team
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+declare(strict_types=1);
+
+namespace Fisharebest\Webtrees\Http\RequestHandlers;
+
+use Fisharebest\Algorithm\MyersDiff;
+use Fisharebest\Webtrees\Carbon;
+use Fisharebest\Webtrees\Gedcom;
+use Fisharebest\Webtrees\GedcomRecord;
+use Fisharebest\Webtrees\I18N;
+use Fisharebest\Webtrees\Services\DatatablesService;
+use Fisharebest\Webtrees\Services\PendingChangesService;
+use Fisharebest\Webtrees\Services\TreeService;
+use Fisharebest\Webtrees\Tree;
+use InvalidArgumentException;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use stdClass;
+
+use function e;
+use function explode;
+use function implode;
+use function preg_replace_callback;
+
+/**
+ * Find pending changes.
+ */
+class PendingChangesLogData implements RequestHandlerInterface
+{
+ /** @var DatatablesService */
+ private $datatables_service;
+
+ /** @var MyersDiff */
+ private $myers_diff;
+
+ /** @var PendingChangesService */
+ private $pending_changes_service;
+
+ /** @var TreeService */
+ private $tree_service;
+
+ /**
+ * @param DatatablesService $datatables_service
+ * @param MyersDiff $myers_diff
+ * @param PendingChangesService $pending_changes_service
+ * @param TreeService $tree_service
+ */
+ public function __construct(
+ DatatablesService $datatables_service,
+ MyersDiff $myers_diff,
+ PendingChangesService $pending_changes_service,
+ TreeService $tree_service
+ ) {
+ $this->datatables_service = $datatables_service;
+ $this->myers_diff = $myers_diff;
+ $this->pending_changes_service = $pending_changes_service;
+ $this->tree_service = $tree_service;
+ }
+
+ /**
+ * @param ServerRequestInterface $request
+ *
+ * @return ResponseInterface
+ */
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $tree = $request->getAttribute('tree');
+ assert($tree instanceof Tree, new InvalidArgumentException());
+
+ $query = $this->pending_changes_service->changesQuery($request);
+
+ $callback = function (stdClass $row) use ($tree): array {
+ $old_lines = explode("\n", $row->old_gedcom);
+ $new_lines = explode("\n", $row->new_gedcom);
+
+ $differences = $this->myers_diff->calculate($old_lines, $new_lines);
+ $diff_lines = [];
+
+ foreach ($differences as $difference) {
+ switch ($difference[1]) {
+ case MyersDiff::DELETE:
+ $diff_lines[] = '<del>' . $difference[0] . '</del>';
+ break;
+ case MyersDiff::INSERT:
+ $diff_lines[] = '<ins>' . $difference[0] . '</ins>';
+ break;
+ default:
+ $diff_lines[] = $difference[0];
+ }
+ }
+
+ // Only convert valid xrefs to links
+ $record = GedcomRecord::getInstance($row->xref, $tree);
+
+ return [
+ $row->change_id,
+ Carbon::make($row->change_time)->local()->format('Y-m-d H:i:s'),
+ I18N::translate($row->status),
+ $record ? '<a href="' . e($record->url()) . '">' . $record->xref() . '</a>' : $row->xref,
+ '<div class="gedcom-data" dir="ltr">' .
+ preg_replace_callback(
+ '/@(' . Gedcom::REGEX_XREF . ')@/',
+ static function (array $match) use ($tree): string {
+ $record = GedcomRecord::getInstance($match[1], $tree);
+
+ return $record ? '<a href="' . e($record->url()) . '">' . $match[0] . '</a>' : $match[0];
+ },
+ implode("\n", $diff_lines)
+ ) .
+ '</div>',
+ $row->user_name,
+ $row->gedcom_name,
+ ];
+ };
+
+ return $this->datatables_service->handle($request, $query, [], [], $callback);
+ }
+}
diff --git a/app/Http/RequestHandlers/PendingChangesLogDelete.php b/app/Http/RequestHandlers/PendingChangesLogDelete.php
new file mode 100644
index 0000000000..d8c72fe91c
--- /dev/null
+++ b/app/Http/RequestHandlers/PendingChangesLogDelete.php
@@ -0,0 +1,58 @@
+<?php
+
+/**
+ * webtrees: online genealogy
+ * Copyright (C) 2019 webtrees development team
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+declare(strict_types=1);
+
+namespace Fisharebest\Webtrees\Http\RequestHandlers;
+
+use Fisharebest\Webtrees\Services\PendingChangesService;
+use Fisharebest\Webtrees\Tree;
+use InvalidArgumentException;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+
+use function response;
+
+/**
+ * Delete pending changes.
+ */
+class PendingChangesLogDelete implements RequestHandlerInterface
+{
+ /** @var PendingChangesService */
+ private $pending_changes_service;
+
+ /**
+ * @param PendingChangesService $pending_changes_service
+ */
+ public function __construct(PendingChangesService $pending_changes_service)
+ {
+ $this->pending_changes_service = $pending_changes_service;
+ }
+
+ /**
+ * @param ServerRequestInterface $request
+ *
+ * @return ResponseInterface
+ */
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $this->pending_changes_service->changesQuery($request)->delete();
+
+ return response();
+ }
+}
diff --git a/app/Http/RequestHandlers/PendingChangesLogDownload.php b/app/Http/RequestHandlers/PendingChangesLogDownload.php
new file mode 100644
index 0000000000..de5a3afb6b
--- /dev/null
+++ b/app/Http/RequestHandlers/PendingChangesLogDownload.php
@@ -0,0 +1,75 @@
+<?php
+
+/**
+ * webtrees: online genealogy
+ * Copyright (C) 2019 webtrees development team
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+declare(strict_types=1);
+
+namespace Fisharebest\Webtrees\Http\RequestHandlers;
+
+use Fig\Http\Message\StatusCodeInterface;
+use Fisharebest\Webtrees\Services\PendingChangesService;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use stdClass;
+
+use function response;
+
+/**
+ * Download pending changes.
+ */
+class PendingChangesLogDownload implements RequestHandlerInterface
+{
+ /** @var PendingChangesService */
+ private $pending_changes_service;
+
+ /**
+ * @param PendingChangesService $pending_changes_service
+ */
+ public function __construct(PendingChangesService $pending_changes_service)
+ {
+ $this->pending_changes_service = $pending_changes_service;
+ }
+
+ /**
+ * @param ServerRequestInterface $request
+ *
+ * @return ResponseInterface
+ */
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $content = $this->pending_changes_service->changesQuery($request)
+ ->get()
+ ->map(static function (stdClass $row): string {
+ // Convert to CSV
+ return implode(',', [
+ '"' . $row->change_time . '"',
+ '"' . $row->status . '"',
+ '"' . $row->xref . '"',
+ '"' . str_replace('"', '""', $row->old_gedcom) . '"',
+ '"' . str_replace('"', '""', $row->new_gedcom) . '"',
+ '"' . str_replace('"', '""', $row->user_name) . '"',
+ '"' . str_replace('"', '""', $row->gedcom_name) . '"',
+ ]);
+ })
+ ->implode("\n");
+
+ return response($content, StatusCodeInterface::STATUS_OK, [
+ 'Content-Type' => 'text/csv; charset=utf-8',
+ 'Content-Disposition' => 'attachment; filename="changes.csv"',
+ ]);
+ }
+}
diff --git a/app/Http/RequestHandlers/PendingChangesLogPage.php b/app/Http/RequestHandlers/PendingChangesLogPage.php
new file mode 100644
index 0000000000..76c887f289
--- /dev/null
+++ b/app/Http/RequestHandlers/PendingChangesLogPage.php
@@ -0,0 +1,133 @@
+<?php
+
+/**
+ * webtrees: online genealogy
+ * Copyright (C) 2019 webtrees development team
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+declare(strict_types=1);
+
+namespace Fisharebest\Webtrees\Http\RequestHandlers;
+
+use Fisharebest\Webtrees\Carbon;
+use Fisharebest\Webtrees\Http\ViewResponseTrait;
+use Fisharebest\Webtrees\I18N;
+use Fisharebest\Webtrees\Services\TreeService;
+use Fisharebest\Webtrees\Services\UserService;
+use Fisharebest\Webtrees\Tree;
+use Illuminate\Database\Capsule\Manager as DB;
+use InvalidArgumentException;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+
+use function array_key_exists;
+use function reset;
+
+/**
+ * Show pending changes.
+ */
+class PendingChangesLogPage implements RequestHandlerInterface
+{
+ use ViewResponseTrait;
+
+ /** @var TreeService */
+ private $tree_service;
+
+ /** @var UserService */
+ private $user_service;
+
+ /**
+ * @param TreeService $tree_service
+ * @param UserService $user_service
+ */
+ public function __construct(TreeService $tree_service, UserService $user_service)
+ {
+ $this->tree_service = $tree_service;
+ $this->user_service = $user_service;
+ }
+
+ /**
+ * @param ServerRequestInterface $request
+ *
+ * @return ResponseInterface
+ */
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $this->layout = 'layouts/administration';
+
+ $tree = $request->getAttribute('tree');
+ assert($tree instanceof Tree, new InvalidArgumentException());
+
+ $trees = $this->tree_service->titles();
+
+ $users = ['' => ''];
+ foreach ($this->user_service->all() as $user) {
+ $user_name = $user->userName();
+ $users[$user_name] = $user_name;
+ }
+
+ // First and last change in the database.
+ $earliest = DB::table('change')->min('change_time');
+ $latest = DB::table('change')->max('change_time');
+
+ $earliest = $earliest !== null ? Carbon::make($earliest) : Carbon::now();
+ $latest = $latest !== null ? Carbon::make($latest) : Carbon::now();
+
+ $earliest = $earliest->toDateString();
+ $latest = $latest->toDateString();
+
+ $from = $request->getQueryParams()['from'] ?? $earliest;
+ $to = $request->getQueryParams()['to'] ?? $latest;
+ $type = $request->getQueryParams()['type'] ?? '';
+ $oldged = $request->getQueryParams()['oldged'] ?? '';
+ $newged = $request->getQueryParams()['newged'] ?? '';
+ $xref = $request->getQueryParams()['xref'] ?? '';
+ $username = $request->getQueryParams()['username'] ?? '';
+
+ return $this->viewResponse('admin/changes-log', [
+ 'earliest' => $earliest,
+ 'from' => $from,
+ 'latest' => $latest,
+ 'newged' => $newged,
+ 'oldged' => $oldged,
+ 'statuses' => $this->changeStatuses(),
+ 'title' => I18N::translate('Changes log'),
+ 'to' => $to,
+ 'tree' => $tree,
+ 'trees' => $trees,
+ 'type' => $type,
+ 'username' => $username,
+ 'users' => $users,
+ 'xref' => $xref,
+ ]);
+ }
+
+ /**
+ * Labels for the various statuses.
+ *
+ * @return array
+ */
+ private function changeStatuses(): array
+ {
+ return [
+ '' => '',
+ /* I18N: the status of an edit accepted/rejected/pending */
+ 'accepted' => I18N::translate('accepted'),
+ /* I18N: the status of an edit accepted/rejected/pending */
+ 'rejected' => I18N::translate('rejected'),
+ /* I18N: the status of an edit accepted/rejected/pending */
+ 'pending' => I18N::translate('pending'),
+ ];
+ }
+}
diff --git a/app/Http/RequestHandlers/PendingChangesRejectChange.php b/app/Http/RequestHandlers/PendingChangesRejectChange.php
new file mode 100644
index 0000000000..07a146016c
--- /dev/null
+++ b/app/Http/RequestHandlers/PendingChangesRejectChange.php
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * webtrees: online genealogy
+ * Copyright (C) 2019 webtrees development team
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+declare(strict_types=1);
+
+namespace Fisharebest\Webtrees\Http\RequestHandlers;
+
+use Fisharebest\Webtrees\GedcomRecord;
+use Fisharebest\Webtrees\Services\PendingChangesService;
+use Fisharebest\Webtrees\Tree;
+use InvalidArgumentException;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+
+use function assert;
+use function response;
+
+/**
+ * Reject a pending change for a record.
+ */
+class PendingChangesRejectChange implements RequestHandlerInterface
+{
+ /** @var PendingChangesService */
+ private $pending_changes_service;
+
+ /**
+ * @param PendingChangesService $pending_changes_service
+ */
+ public function __construct(PendingChangesService $pending_changes_service)
+ {
+ $this->pending_changes_service = $pending_changes_service;
+ }
+
+ /**
+ * @param ServerRequestInterface $request
+ *
+ * @return ResponseInterface
+ */
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $tree = $request->getAttribute('tree');
+ assert($tree instanceof Tree, new InvalidArgumentException());
+
+ $xref = $request->getAttribute('xref');
+ $record = GedcomRecord::getInstance($xref, $tree);
+ $change = $request->getAttribute('change');
+
+ if ($record instanceof GedcomRecord) {
+ $this->pending_changes_service->rejectChange($record, $change);
+ }
+
+ return response();
+ }
+}
diff --git a/app/Http/RequestHandlers/PendingChangesRejectRecord.php b/app/Http/RequestHandlers/PendingChangesRejectRecord.php
new file mode 100644
index 0000000000..0f4f6e1ac4
--- /dev/null
+++ b/app/Http/RequestHandlers/PendingChangesRejectRecord.php
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * webtrees: online genealogy
+ * Copyright (C) 2019 webtrees development team
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+declare(strict_types=1);
+
+namespace Fisharebest\Webtrees\Http\RequestHandlers;
+
+use Fisharebest\Webtrees\FlashMessages;
+use Fisharebest\Webtrees\GedcomRecord;
+use Fisharebest\Webtrees\I18N;
+use Fisharebest\Webtrees\Services\PendingChangesService;
+use Fisharebest\Webtrees\Tree;
+use InvalidArgumentException;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+
+use function assert;
+use function response;
+
+/**
+ * Reject pending changes for a record.
+ */
+class PendingChangesRejectRecord implements RequestHandlerInterface
+{
+ /** @var PendingChangesService */
+ private $pending_changes_service;
+
+ /**
+ * @param PendingChangesService $pending_changes_service
+ */
+ public function __construct(PendingChangesService $pending_changes_service)
+ {
+ $this->pending_changes_service = $pending_changes_service;
+ }
+
+ /**
+ * @param ServerRequestInterface $request
+ *
+ * @return ResponseInterface
+ */
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $tree = $request->getAttribute('tree');
+ assert($tree instanceof Tree, new InvalidArgumentException());
+
+ $xref = $request->getAttribute('xref') ?? '';
+ $record = GedcomRecord::getInstance($xref, $tree);
+
+ if ($record instanceof GedcomRecord) {
+ $this->pending_changes_service->rejectRecord($record);
+
+ /* I18N: %s is the name of a genealogy record */
+ FlashMessages::addMessage(I18N::translate('The changes to “%s” have been rejected.', $record->fullName()));
+ }
+
+ return response();
+ }
+}
diff --git a/app/Http/RequestHandlers/PendingChangesRejectTree.php b/app/Http/RequestHandlers/PendingChangesRejectTree.php
new file mode 100644
index 0000000000..4089661a0b
--- /dev/null
+++ b/app/Http/RequestHandlers/PendingChangesRejectTree.php
@@ -0,0 +1,67 @@
+<?php
+
+/**
+ * webtrees: online genealogy
+ * Copyright (C) 2019 webtrees development team
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+declare(strict_types=1);
+
+namespace Fisharebest\Webtrees\Http\RequestHandlers;
+
+use Fisharebest\Webtrees\FlashMessages;
+use Fisharebest\Webtrees\I18N;
+use Fisharebest\Webtrees\Services\PendingChangesService;
+use Fisharebest\Webtrees\Tree;
+use InvalidArgumentException;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+
+use function assert;
+use function e;
+use function response;
+
+/**
+ * Reject pending changes for a tree.
+ */
+class PendingChangesRejectTree implements RequestHandlerInterface
+{
+ /** @var PendingChangesService */
+ private $pending_changes_service;
+
+ /**
+ * @param PendingChangesService $pending_changes_service
+ */
+ public function __construct(PendingChangesService $pending_changes_service)
+ {
+ $this->pending_changes_service = $pending_changes_service;
+ }
+
+ /**
+ * @param ServerRequestInterface $request
+ *
+ * @return ResponseInterface
+ */
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $tree = $request->getAttribute('tree');
+ assert($tree instanceof Tree, new InvalidArgumentException());
+
+ $this->pending_changes_service->rejectTree($tree);
+
+ FlashMessages::addMessage(I18N::translate('The changes to “%s” have been rejected.', e($tree->title())));
+
+ return response();
+ }
+}
diff --git a/app/Module/ModuleThemeTrait.php b/app/Module/ModuleThemeTrait.php
index a629c82add..e43466f59f 100644
--- a/app/Module/ModuleThemeTrait.php
+++ b/app/Module/ModuleThemeTrait.php
@@ -27,6 +27,7 @@ use Fisharebest\Webtrees\Http\RequestHandlers\ControlPanel;
use Fisharebest\Webtrees\Http\RequestHandlers\HomePage;
use Fisharebest\Webtrees\Http\RequestHandlers\LoginPage;
use Fisharebest\Webtrees\Http\RequestHandlers\Logout;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChanges;
use Fisharebest\Webtrees\Http\RequestHandlers\SelectLanguage;
use Fisharebest\Webtrees\Http\RequestHandlers\SelectTheme;
use Fisharebest\Webtrees\I18N;
@@ -438,7 +439,7 @@ trait ModuleThemeTrait
public function menuPendingChanges(?Tree $tree): ?Menu
{
if ($tree instanceof Tree && $tree->hasPendingEdit() && Auth::isModerator($tree)) {
- $url = route('show-pending', [
+ $url = route(PendingChanges::class, [
'tree' => $tree->name(),
'url' => (string) app(ServerRequestInterface::class)->getUri(),
]);
diff --git a/app/Module/ReviewChangesModule.php b/app/Module/ReviewChangesModule.php
index e5e6e045ec..470f88d74e 100644
--- a/app/Module/ReviewChangesModule.php
+++ b/app/Module/ReviewChangesModule.php
@@ -22,6 +22,7 @@ namespace Fisharebest\Webtrees\Module;
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Carbon;
use Fisharebest\Webtrees\GedcomRecord;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChanges;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Services\MailService;
use Fisharebest\Webtrees\Services\TreeService;
@@ -148,7 +149,7 @@ class ReviewChangesModule extends AbstractModule implements ModuleBlockInterface
if (Auth::isEditor($tree) && $tree->hasPendingEdit()) {
$content = '';
if (Auth::isModerator($tree)) {
- $content .= '<a href="' . e(route('show-pending', ['tree' => $tree->name()])) . '">' . I18N::translate('There are pending changes for you to moderate.') . '</a><br>';
+ $content .= '<a href="' . e(route(PendingChanges::class, ['tree' => $tree->name()])) . '">' . I18N::translate('There are pending changes for you to moderate.') . '</a><br>';
}
if ($sendmail) {
$last_email_timestamp = Carbon::createFromTimestamp((int) Site::getPreference('LAST_CHANGE_EMAIL'));
diff --git a/app/Services/PendingChangesService.php b/app/Services/PendingChangesService.php
new file mode 100644
index 0000000000..40e80cfaa1
--- /dev/null
+++ b/app/Services/PendingChangesService.php
@@ -0,0 +1,236 @@
+<?php
+
+/**
+ * webtrees: online genealogy
+ * Copyright (C) 2019 webtrees development team
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+declare(strict_types=1);
+
+namespace Fisharebest\Webtrees\Services;
+
+use Fisharebest\Webtrees\Carbon;
+use Fisharebest\Webtrees\Functions\FunctionsImport;
+use Fisharebest\Webtrees\GedcomRecord;
+use Fisharebest\Webtrees\Tree;
+use Illuminate\Database\Capsule\Manager as DB;
+use Illuminate\Database\Query\Builder;
+use Illuminate\Database\Query\Expression;
+use InvalidArgumentException;
+use Psr\Http\Message\ServerRequestInterface;
+
+/**
+ * Manage pending changes
+ */
+class PendingChangesService
+{
+ /**
+ * Accept all changes to a tree.
+ *
+ * @param Tree $tree
+ *
+ * @return void
+ */
+ public function acceptTree(Tree $tree): void
+ {
+ $changes = DB::table('change')
+ ->where('gedcom_id', '=', $tree->id())
+ ->where('status', '=', 'pending')
+ ->orderBy('change_id')
+ ->get();
+
+ foreach ($changes as $change) {
+ if ($change->new_gedcom === '') {
+ // delete
+ FunctionsImport::updateRecord($change->old_gedcom, $tree, true);
+ } else {
+ // add/update
+ FunctionsImport::updateRecord($change->new_gedcom, $tree, false);
+ }
+
+ DB::table('change')
+ ->where('change_id', '=', $change->change_id)
+ ->update(['status' => 'accepted']);
+ }
+ }
+
+ /**
+ * Accept all changes to a record.
+ *
+ * @param GedcomRecord $record
+ */
+ public function acceptRecord(GedcomRecord $record): void
+ {
+ $changes = DB::table('change')
+ ->where('gedcom_id', '=', $record->tree()->id())
+ ->where('xref', '=', $record->xref())
+ ->where('status', '=', 'pending')
+ ->orderBy('change_id')
+ ->get();
+
+ foreach ($changes as $change) {
+ if ($change->new_gedcom === '') {
+ // delete
+ FunctionsImport::updateRecord($change->old_gedcom, $record->tree(), true);
+ } else {
+ // add/update
+ FunctionsImport::updateRecord($change->new_gedcom, $record->tree(), false);
+ }
+
+ DB::table('change')
+ ->where('change_id', '=', $change->change_id)
+ ->update(['status' => 'accepted']);
+ }
+ }
+
+ /**
+ * Accept a change (and previous changes) to a record.
+ *
+ * @param GedcomRecord $record
+ * @param string $change_id
+ */
+ public function acceptChange(GedcomRecord $record, string $change_id): void
+ {
+ $changes = DB::table('change')
+ ->where('gedcom_id', '=', $record->tree()->id())
+ ->where('xref', '=', $record->xref())
+ ->where('change_id', '<=', $change_id)
+ ->where('status', '=', 'pending')
+ ->orderBy('change_id')
+ ->get();
+
+ foreach ($changes as $change) {
+ if ($change->new_gedcom === '') {
+ // delete
+ FunctionsImport::updateRecord($change->old_gedcom, $record->tree(), true);
+ } else {
+ // add/update
+ FunctionsImport::updateRecord($change->new_gedcom, $record->tree(), false);
+ }
+
+ DB::table('change')
+ ->where('change_id', '=', $change->change_id)
+ ->update(['status' => 'accepted']);
+ }
+ }
+
+ /**
+ * Reject all changes to a tree.
+ *
+ * @param Tree $tree
+ */
+ public function rejectTree(Tree $tree): void
+ {
+ DB::table('change')
+ ->where('gedcom_id', '=', $tree->id())
+ ->where('status', '=', 'pending')
+ ->update(['status' => 'rejected']);
+ }
+
+ /**
+ * Reject a change (subsequent changes) to a record.
+ *
+ * @param GedcomRecord $record
+ * @param string $change_id
+ */
+ public function rejectChange(GedcomRecord $record, string $change_id): void
+ {
+ DB::table('change')
+ ->where('gedcom_id', '=', $record->tree()->id())
+ ->where('xref', '=', $record->xref())
+ ->where('change_id', '>=', $change_id)
+ ->where('status', '=', 'pending')
+ ->update(['status' => 'rejected']);
+ }
+
+ /**
+ * Reject all changes to a record.
+ *
+ * @param GedcomRecord $record
+ */
+ public function rejectRecord(GedcomRecord $record): void
+ {
+ DB::table('change')
+ ->where('gedcom_id', '=', $record->tree()->id())
+ ->where('xref', '=', $record->xref())
+ ->where('status', '=', 'pending')
+ ->update(['status' => 'rejected']);
+ }
+
+ /**
+ * Generate a query for filtering the changes log.
+ *
+ * @param ServerRequestInterface $request
+ *
+ * @return Builder
+ */
+ public function changesQuery(ServerRequestInterface $request): Builder
+ {
+ $tree = $request->getAttribute('tree');
+ assert($tree instanceof Tree, new InvalidArgumentException());
+
+ $from = $request->getQueryParams()['from'] ?? '';
+ $to = $request->getQueryParams()['to'] ?? '';
+ $type = $request->getQueryParams()['type'] ?? '';
+ $oldged = $request->getQueryParams()['oldged'] ?? '';
+ $newged = $request->getQueryParams()['newged'] ?? '';
+ $xref = $request->getQueryParams()['xref'] ?? '';
+ $username = $request->getQueryParams()['username'] ?? '';
+ $search = $request->getQueryParams()['search'] ?? [];
+ $search = $search['value'] ?? '';
+
+ $query = DB::table('change')
+ ->leftJoin('user', 'user.user_id', '=', 'change.user_id')
+ ->join('gedcom', 'gedcom.gedcom_id', '=', 'change.gedcom_id')
+ ->select(['change.*', new Expression("COALESCE(user_name, '<none>') AS user_name"), 'gedcom_name'])
+ ->where('gedcom_name', '=', $tree->name());
+
+ if ($search !== '') {
+ $query->where(static function (Builder $query) use ($search): void {
+ $query
+ ->whereContains('old_gedcom', $search)
+ ->whereContains('new_gedcom', $search, 'or');
+ });
+ }
+
+ if ($from !== '') {
+ $query->where('change_time', '>=', $from);
+ }
+
+ if ($to !== '') {
+ // before end of the day
+ $query->where('change_time', '<', Carbon::make($to)->addDay());
+ }
+
+ if ($type !== '') {
+ $query->where('status', '=', $type);
+ }
+
+ if ($oldged !== '') {
+ $query->whereContains('old_gedcom', $oldged);
+ }
+ if ($newged !== '') {
+ $query->whereContains('new_gedcom', $oldged);
+ }
+
+ if ($xref !== '') {
+ $query->where('xref', '=', $xref);
+ }
+
+ if ($username !== '') {
+ $query->whereContains('user_name', $username);
+ }
+
+ return $query;
+ }
+}
diff --git a/app/Tree.php b/app/Tree.php
index 13c1c2e019..a3e97221ee 100644
--- a/app/Tree.php
+++ b/app/Tree.php
@@ -24,6 +24,7 @@ use Fisharebest\Flysystem\Adapter\ChrootAdapter;
use Fisharebest\Webtrees\Contracts\UserInterface;
use Fisharebest\Webtrees\Functions\FunctionsExport;
use Fisharebest\Webtrees\Functions\FunctionsImport;
+use Fisharebest\Webtrees\Services\PendingChangesService;
use Fisharebest\Webtrees\Services\TreeService;
use Illuminate\Database\Capsule\Manager as DB;
use Illuminate\Database\Query\Expression;
@@ -514,9 +515,11 @@ class Tree
// Accept this pending change
if (Auth::user()->getPreference('auto_accept')) {
- FunctionsImport::acceptAllChanges($xref, $this);
+ $record = new GedcomRecord($xref, $gedcom, null, $this);
+
+ app(PendingChangesService::class)->acceptRecord($record);
- return new GedcomRecord($xref, $gedcom, null, $this);
+ return $record;
}
return GedcomRecord::getInstance($xref, $this, $gedcom);
@@ -593,9 +596,11 @@ class Tree
// Accept this pending change
if (Auth::user()->getPreference('auto_accept')) {
- FunctionsImport::acceptAllChanges($xref, $this);
+ $record = new Family($xref, $gedcom, null, $this);
- return new Family($xref, $gedcom, null, $this);
+ app(PendingChangesService::class)->acceptRecord($record);
+
+ return $record;
}
return new Family($xref, '', $gedcom, $this);
@@ -632,9 +637,11 @@ class Tree
// Accept this pending change
if (Auth::user()->getPreference('auto_accept')) {
- FunctionsImport::acceptAllChanges($xref, $this);
+ $record = new Individual($xref, $gedcom, null, $this);
+
+ app(PendingChangesService::class)->acceptRecord($record);
- return new Individual($xref, $gedcom, null, $this);
+ return $record;
}
return new Individual($xref, '', $gedcom, $this);
@@ -671,9 +678,11 @@ class Tree
// Accept this pending change
if (Auth::user()->getPreference('auto_accept')) {
- FunctionsImport::acceptAllChanges($xref, $this);
+ $record = new Media($xref, $gedcom, null, $this);
+
+ app(PendingChangesService::class)->acceptRecord($record);
- return new Media($xref, $gedcom, null, $this);
+ return $record;
}
return new Media($xref, '', $gedcom, $this);
diff --git a/resources/views/admin/changes-log.phtml b/resources/views/admin/changes-log.phtml
index 28e23ef0f7..7c8108d4a2 100644
--- a/resources/views/admin/changes-log.phtml
+++ b/resources/views/admin/changes-log.phtml
@@ -1,18 +1,19 @@
<?php
use Fisharebest\Webtrees\Http\RequestHandlers\ControlPanel;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesLogData;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesLogDelete;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesLogDownload;
use Fisharebest\Webtrees\I18N;
?>
-<?= view('components/breadcrumbs', ['links' => [route(ControlPanel::class) => I18N::translate('Control panel'), route('manage-trees') => I18N::translate('Manage family trees'), $title]]) ?>
+<?= view('components/breadcrumbs', ['links' => [route(ControlPanel::class) => I18N::translate('Control panel'), route('manage-trees', ['tree' => $tree->name()]) => I18N::translate('Manage family trees'), $title]]) ?>
<h1><?= $title ?></h1>
-<form method="get" action="<?= e(route('admin-changes-log')) ?>" class="form" name="logs">
- <input type="hidden" name="action" value="show">
- <input type="hidden" name="route" value="admin-changes-log">
- <input type="hidden" name="tree" value="<?= e($ged) ?>">
+<form method="post" class="form" name="logs">
+ <?= csrf_field() ?>
<div class="row">
<div class="form-group col-xs-6 col-md-3">
@@ -63,14 +64,14 @@ use Fisharebest\Webtrees\I18N;
<label for="username">
<?= I18N::translate('User') ?>
</label>
- <?= view('components/select', ['name' => 'username', 'selected' => $username, 'options' => $user_list]) ?>
+ <?= view('components/select', ['name' => 'username', 'selected' => $username, 'options' => $users]) ?>
</div>
<div class="form-group col-xs-6 col-md-3">
<label for="tree">
<?= I18N::translate('Family tree') ?>
</label>
- <?= view('components/select', ['name' => 'ged', 'selected' => $ged, 'options' => $tree_list]) ?>
+ <?= view('components/select', ['name' => 'tree', 'selected' => $tree->name(), 'options' => $trees]) ?>
</div>
</div>
@@ -80,38 +81,37 @@ use Fisharebest\Webtrees\I18N;
<?= I18N::translate('search') ?>
</button>
- <button type="submit" class="btn btn-secondary" onclick="document.logs.action.value='export';return true;" <?= $action === 'show' ? '' : 'disabled' ?>>
+ <a class="btn btn-secondary" href="<?= (route(PendingChangesLogDownload::class, ['from' => $from, 'to' => $to, 'type' => $type, 'xref' => $xref, 'oldged' => $oldged, 'newged' => $newged, 'tree' => $tree->name(), 'username' => $username])) ?>">
<?= view('icons/download') ?>
<?= /* I18N: A button label. */ I18N::translate('download') ?>
- </button>
+ </a>
- <button type="submit" class="btn btn-danger" data-confirm="<?= I18N::translate('Permanently delete these records?') ?>" onclick="if (confirm(this.dataset.confirm)) {document.logs.action.value='delete'; return true;} else {return false;}" <?= $action === 'show' ? '' : 'disabled' ?>>
+ <a href="#" class="btn btn-danger" data-confirm="<?= I18N::translate('Permanently delete these records?') ?>" data-post-url="<?= (route(PendingChangesLogDelete::class, ['from' => $from, 'to' => $to, 'type' => $type, 'xref' => $xref, 'oldged' => $oldged, 'newged' => $newged, 'tree' => $tree->name(), 'username' => $username])) ?>">
<?= view('icons/delete') ?>
<?= I18N::translate('delete') ?>
- </button>
+ </a>
</div>
</form>
-<?php if ($action === 'show') : ?>
- <table
- class="table table-bordered table-sm table-hover table-site-changes datatables"
- data-ajax="<?= route('admin-changes-log-data', ['from' => $from, 'to' => $to, 'type' => $type, 'xref' => $xref, 'oldged' => $oldged, 'newged' => $newged, 'ged' => $ged, 'username' => $username]) ?>"
- data-server-side="true"
- data-sorting="<?= e('[[ 0, "desc" ]]') ?>"
- >
- <caption class="sr-only">
- <?= $title ?>
- </caption>
- <thead>
- <tr>
- <th data-visible="false"></th>
- <th><?= I18N::translate('Timestamp') ?></th>
- <th><?= I18N::translate('Status') ?></th>
- <th><?= I18N::translate('Record') ?></th>
- <th data-sortable="false"><?= I18N::translate('Data') ?></th>
- <th><?= I18N::translate('User') ?></th>
- <th><?= I18N::translate('Family tree') ?></th>
- </tr>
- </thead>
- </table>
-<?php endif ?>
+<table
+ class="table table-bordered table-sm table-hover table-site-changes datatables"
+ data-ajax="<?= route(PendingChangesLogData::class, ['from' => $from, 'to' => $to, 'type' => $type, 'xref' => $xref, 'oldged' => $oldged, 'newged' => $newged, 'tree' => $tree->name(), 'username' => $username]) ?>"
+ data-server-side="true"
+ data-sorting="<?= e('[[ 0, "desc" ]]') ?>"
+>
+ <caption class="sr-only">
+ <?= $title ?>
+ </caption>
+
+ <thead>
+ <tr>
+ <th data-visible="false"></th>
+ <th><?= I18N::translate('Timestamp') ?></th>
+ <th><?= I18N::translate('Status') ?></th>
+ <th><?= I18N::translate('Record') ?></th>
+ <th data-sortable="false"><?= I18N::translate('Data') ?></th>
+ <th><?= I18N::translate('User') ?></th>
+ <th><?= I18N::translate('Family tree') ?></th>
+ </tr>
+ </thead>
+</table>
diff --git a/resources/views/admin/control-panel.phtml b/resources/views/admin/control-panel.phtml
index df9a916818..575242479a 100644
--- a/resources/views/admin/control-panel.phtml
+++ b/resources/views/admin/control-panel.phtml
@@ -3,6 +3,7 @@
use Fisharebest\Webtrees\Http\RequestHandlers\CleanDataFolder;
use Fisharebest\Webtrees\Http\RequestHandlers\ControlPanel;
use Fisharebest\Webtrees\Http\RequestHandlers\CreateTreePage;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChanges;
use Fisharebest\Webtrees\Http\RequestHandlers\PhpInformation;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Module\FamilyListModule;
@@ -182,7 +183,7 @@ use Illuminate\Support\Collection;
</th>
<td style="text-align: right;">
<?php if ($changes[$tree->id()]) : ?>
- <a href="<?= e(route('show-pending', ['tree' => $tree->name(), 'url' => route(ControlPanel::class)])) ?>">
+ <a href="<?= e(route(PendingChanges::class, ['tree' => $tree->name(), 'url' => route(ControlPanel::class)])) ?>">
<?= I18N::number($changes[$tree->id()]) ?>
<span class="sr-only"><?= I18N::translate('Pending changes') ?> <?= e($tree->title()) ?></span>
</a>
diff --git a/resources/views/admin/trees.phtml b/resources/views/admin/trees.phtml
index 20ee632470..1ec9170616 100644
--- a/resources/views/admin/trees.phtml
+++ b/resources/views/admin/trees.phtml
@@ -3,6 +3,7 @@
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Http\RequestHandlers\ControlPanel;
use Fisharebest\Webtrees\Http\RequestHandlers\DeleteTreeAction;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesLogPage;
use Fisharebest\Webtrees\Http\RequestHandlers\SelectDefaultTree;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Site;
@@ -186,7 +187,7 @@ use Illuminate\Database\Capsule\Manager as DB;
<!-- CHANGES -->
<li>
<span class="fa-li"><?= view('icons/edit') ?></span>
- <a href="<?= route('admin-changes-log', ['tree' => $managed_tree->name()]) ?>">
+ <a href="<?= route(PendingChangesLogPage::class, ['tree' => $managed_tree->name()]) ?>">
<?= I18N::translate('Changes log') ?>
<span class="sr-only">
<?= e($managed_tree->title()) ?>
diff --git a/resources/views/emails/pending-changes-html.phtml b/resources/views/emails/pending-changes-html.phtml
index 9e3647dde7..5662c0d5e4 100644
--- a/resources/views/emails/pending-changes-html.phtml
+++ b/resources/views/emails/pending-changes-html.phtml
@@ -1,4 +1,9 @@
-<?php use Fisharebest\Webtrees\I18N; ?>
+<?php
+
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChanges;
+use Fisharebest\Webtrees\I18N;
+
+?>
<div <?= I18N::htmlAttributes() ?>>
<p>
<?= I18N::translate('Hello %s…', '<span dir="auto">' . e($user->realName()) . '</span>') ?>
@@ -10,7 +15,7 @@
<ul>
<li>
- <a href="<?= e(route('show-pending', ['tree' => $tree->name()])) ?>">
+ <a href="<?= e(route(PendingChanges::class, ['tree' => $tree->name()])) ?>">
<?= e($tree->title()) ?>
</a>
</li>
diff --git a/resources/views/emails/pending-changes-text.phtml b/resources/views/emails/pending-changes-text.phtml
index 31f17ca8d3..239a891254 100644
--- a/resources/views/emails/pending-changes-text.phtml
+++ b/resources/views/emails/pending-changes-text.phtml
@@ -1,7 +1,12 @@
-<?php use Fisharebest\Webtrees\I18N; ?>
+<?php
+
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChanges;
+use Fisharebest\Webtrees\I18N;
+
+?>
<?= I18N::translate('Hello %s…', $user->realName()) ?>
<?= I18N::translate('There are pending changes for you to moderate.') ?>
-<?= $tree->title() ?> - <?= route('show-pending', ['tree' => $tree->name()]) ?>
+<?= $tree->title() ?> - <?= route(PendingChanges::class, ['tree' => $tree->name()]) ?>
diff --git a/resources/views/family-page.phtml b/resources/views/family-page.phtml
index 49f3fb4660..572c36d5c3 100644
--- a/resources/views/family-page.phtml
+++ b/resources/views/family-page.phtml
@@ -3,6 +3,8 @@
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Functions\FunctionsPrint;
use Fisharebest\Webtrees\Functions\FunctionsPrintFacts;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesAcceptRecord;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesRejectRecord;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\View;
@@ -10,13 +12,13 @@ use Fisharebest\Webtrees\View;
<?php if ($record->isPendingDeletion()) : ?>
<?php if (Auth::isModerator($record->tree())) : ?>
- <?= view('components/alert-warning-dismissible', ['alert' => /* I18N: %1$s is “accept”, %2$s is “reject”. These are links. */ I18N::translate('This family has been deleted. You should review the deletion and then %1$s or %2$s it.', '<a href="#" class="alert-link" data-post-url="' . e(route('accept-changes', ['tree' => $record->tree()->name(), 'xref' => $record->xref()])) . '">' . I18N::translateContext('You should review the deletion and then accept or reject it.', 'accept') . '</a>', '<a href="#" class="alert-link" data-post-url="' . e(route('reject-changes', ['tree' => $record->tree()->name(), 'xref' => $record->xref()])) . '">' . I18N::translateContext('You should review the deletion and then accept or reject it.', 'reject') . '</a>') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
+ <?= view('components/alert-warning-dismissible', ['alert' => /* I18N: %1$s is “accept”, %2$s is “reject”. These are links. */ I18N::translate('This family has been deleted. You should review the deletion and then %1$s or %2$s it.', '<a href="#" class="alert-link" data-post-url="' . e(route(PendingChangesAcceptRecord::class, ['tree' => $record->tree()->name(), 'xref' => $record->xref()])) . '">' . I18N::translateContext('You should review the deletion and then accept or reject it.', 'accept') . '</a>', '<a href="#" class="alert-link" data-post-url="' . e(route(PendingChangesRejectRecord::class, ['tree' => $record->tree()->name(), 'xref' => $record->xref()])) . '">' . I18N::translateContext('You should review the deletion and then accept or reject it.', 'reject') . '</a>') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
<?php elseif (Auth::isEditor($record->tree())) : ?>
<?= view('components/alert-warning-dismissible', ['alert' => I18N::translate('This family has been deleted. The deletion will need to be reviewed by a moderator.') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
<?php endif ?>
<?php elseif ($record->isPendingAddition()) : ?>
<?php if (Auth::isModerator($record->tree())) : ?>
- <?= view('components/alert-warning-dismissible', ['alert' => /* I18N: %1$s is “accept”, %2$s is “reject”. These are links. */ I18N::translate('This family has been edited. You should review the changes and then %1$s or %2$s them.', '<a href="#" class="alert-link" data-post-url="' . e(route('accept-changes', ['tree' => $record->tree()->name(), 'xref' => $record->xref()])) . '">' . I18N::translateContext('You should review the changes and then accept or reject them.', 'accept') . '</a>', '<a href="#" class="alert-link" data-post-url="' . e(route('reject-changes', ['tree' => $record->tree()->name(), 'xref' => $record->xref()])) . '">' . I18N::translateContext('You should review the changes and then accept or reject them.', 'reject') . '</a>') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
+ <?= view('components/alert-warning-dismissible', ['alert' => /* I18N: %1$s is “accept”, %2$s is “reject”. These are links. */ I18N::translate('This family has been edited. You should review the changes and then %1$s or %2$s them.', '<a href="#" class="alert-link" data-post-url="' . e(route(PendingChangesAcceptRecord::class, ['tree' => $record->tree()->name(), 'xref' => $record->xref()])) . '">' . I18N::translateContext('You should review the changes and then accept or reject them.', 'accept') . '</a>', '<a href="#" class="alert-link" data-post-url="' . e(route(PendingChangesRejectRecord::class, ['tree' => $record->tree()->name(), 'xref' => $record->xref()])) . '">' . I18N::translateContext('You should review the changes and then accept or reject them.', 'reject') . '</a>') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
<?php elseif (Auth::isEditor($record->tree())) : ?>
<?= view('components/alert-warning-dismissible', ['alert' => I18N::translate('This family has been edited. The changes need to be reviewed by a moderator.') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
<?php endif ?>
diff --git a/resources/views/gedcom-record-page.phtml b/resources/views/gedcom-record-page.phtml
index e35540308e..84c237f678 100644
--- a/resources/views/gedcom-record-page.phtml
+++ b/resources/views/gedcom-record-page.phtml
@@ -1,17 +1,19 @@
<?php use Fisharebest\Webtrees\Auth; ?>
<?php use Fisharebest\Webtrees\Functions\FunctionsPrint; ?>
<?php use Fisharebest\Webtrees\Functions\FunctionsPrintFacts; ?>
-<?php use Fisharebest\Webtrees\I18N; ?>
+<?php use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesAcceptRecord;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesRejectRecord;
+use Fisharebest\Webtrees\I18N; ?>
<?php if ($record->isPendingDeletion()) : ?>
<?php if (Auth::isModerator($record->tree())) : ?>
- <?= view('components/alert-warning-dismissible', ['alert' => /* I18N: %1$s is “accept”, %2$s is “reject”. These are links. */ I18N::translate('This record has been deleted. You should review the deletion and then %1$s or %2$s it.', '<a href="#" class="alert-link" data-post-url="' . e(route('accept-changes', ['tree' => $record->tree()->name(), 'xref' => $record->xref()])) . '">' . I18N::translateContext('You should review the deletion and then accept or reject it.', 'accept') . '</a>', '<a href="#" class="alert-link" data-post-url="' . e(route('reject-changes', ['tree' => $record->tree()->name(), 'xref' => $record->xref()])) . '">' . I18N::translateContext('You should review the deletion and then accept or reject it.', 'reject') . '</a>') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
+ <?= view('components/alert-warning-dismissible', ['alert' => /* I18N: %1$s is “accept”, %2$s is “reject”. These are links. */ I18N::translate('This record has been deleted. You should review the deletion and then %1$s or %2$s it.', '<a href="#" class="alert-link" data-post-url="' . e(route(PendingChangesAcceptRecord::class, ['tree' => $record->tree()->name(), 'xref' => $record->xref()])) . '">' . I18N::translateContext('You should review the deletion and then accept or reject it.', 'accept') . '</a>', '<a href="#" class="alert-link" data-post-url="' . e(route(PendingChangesRejectRecord::class, ['tree' => $record->tree()->name(), 'xref' => $record->xref()])) . '">' . I18N::translateContext('You should review the deletion and then accept or reject it.', 'reject') . '</a>') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
<?php elseif (Auth::isEditor($record->tree())) : ?>
<?= view('components/alert-warning-dismissible', ['alert' => I18N::translate('This record has been deleted. The deletion will need to be reviewed by a moderator.') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
<?php endif ?>
<?php elseif ($record->isPendingAddition()) : ?>
<?php if (Auth::isModerator($record->tree())) : ?>
- <?= view('components/alert-warning-dismissible', ['alert' => /* I18N: %1$s is “accept”, %2$s is “reject”. These are links. */ I18N::translate('This record has been edited. You should review the changes and then %1$s or %2$s them.', '<a href="#" class="alert-link" data-post-url="' . e(route('accept-changes', ['tree' => $record->tree()->name(), 'xref' => $record->xref()])) . '">' . I18N::translateContext('You should review the changes and then accept or reject them.', 'accept') . '</a>', '<a href="#" class="alert-link" data-post-url="' . e(route('reject-changes', ['tree' => $record->tree()->name(), 'xref' => $record->xref()])) . '">' . I18N::translateContext('You should review the changes and then accept or reject them.', 'reject') . '</a>') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
+ <?= view('components/alert-warning-dismissible', ['alert' => /* I18N: %1$s is “accept”, %2$s is “reject”. These are links. */ I18N::translate('This record has been edited. You should review the changes and then %1$s or %2$s them.', '<a href="#" class="alert-link" data-post-url="' . e(route(PendingChangesAcceptRecord::class, ['tree' => $record->tree()->name(), 'xref' => $record->xref()])) . '">' . I18N::translateContext('You should review the changes and then accept or reject them.', 'accept') . '</a>', '<a href="#" class="alert-link" data-post-url="' . e(route(PendingChangesRejectRecord::class, ['tree' => $record->tree()->name(), 'xref' => $record->xref()])) . '">' . I18N::translateContext('You should review the changes and then accept or reject them.', 'reject') . '</a>') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
<?php elseif (Auth::isEditor($record->tree())) : ?>
<?= view('components/alert-warning-dismissible', ['alert' => I18N::translate('This record has been edited. The changes need to be reviewed by a moderator.') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
<?php endif ?>
diff --git a/resources/views/individual-page.phtml b/resources/views/individual-page.phtml
index f6f92fbe2b..ea6260d51e 100644
--- a/resources/views/individual-page.phtml
+++ b/resources/views/individual-page.phtml
@@ -1,6 +1,8 @@
<?php
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Functions\FunctionsPrint;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesAcceptRecord;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesRejectRecord;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
use Fisharebest\Webtrees\View;
@@ -22,7 +24,7 @@ use Illuminate\Support\Collection;
<?php if (Auth::isModerator($individual->tree())) : ?>
<?= view('components/alert-warning-dismissible', [
'alert' => /* I18N: %1$s is “accept”, %2$s is “reject”. These are links. */
- I18N::translate('This individual has been deleted. You should review the deletion and then %1$s or %2$s it.', '<a href="#" class="alert-link" data-post-url="' . e(route('accept-changes', ['tree' => $individual->tree()->name(), 'xref' => $individual->xref()])) . '">' . I18N::translateContext('You should review the deletion and then accept or reject it.', 'accept') . '</a>', '<a href="#" class="alert-link" data-post-url="' . e(route('reject-changes', ['tree' => $individual->tree()->name(), 'xref' => $individual->xref()])) . '">' . I18N::translateContext('You should review the deletion and then accept or reject it.', 'reject') . '</a>') . ' ' . FunctionsPrint::helpLink('pending_changes'),
+ I18N::translate('This individual has been deleted. You should review the deletion and then %1$s or %2$s it.', '<a href="#" class="alert-link" data-post-url="' . e(route(PendingChangesAcceptRecord::class, ['tree' => $individual->tree()->name(), 'xref' => $individual->xref()])) . '">' . I18N::translateContext('You should review the deletion and then accept or reject it.', 'accept') . '</a>', '<a href="#" class="alert-link" data-post-url="' . e(route(PendingChangesRejectRecord::class, ['tree' => $individual->tree()->name(), 'xref' => $individual->xref()])) . '">' . I18N::translateContext('You should review the deletion and then accept or reject it.', 'reject') . '</a>') . ' ' . FunctionsPrint::helpLink('pending_changes'),
]) ?>
<?php elseif (Auth::isEditor($individual->tree())) : ?>
<?= view('components/alert-warning-dismissible', ['alert' => I18N::translate('This individual has been deleted. The deletion will need to be reviewed by a moderator.') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
@@ -31,7 +33,7 @@ use Illuminate\Support\Collection;
<?php if (Auth::isModerator($individual->tree())) : ?>
<?= view('components/alert-warning-dismissible', [
'alert' => /* I18N: %1$s is “accept”, %2$s is “reject”. These are links. */
- I18N::translate('This individual has been edited. You should review the changes and then %1$s or %2$s them.', '<a href="#" class="alert-link" data-post-url="' . e(route('accept-changes', ['tree' => $individual->tree()->name(), 'xref' => $individual->xref()])) . '">' . I18N::translateContext('You should review the changes and then accept or reject them.', 'accept') . '</a>', '<a href="#" class="alert-link" data-post-url="' . e(route('reject-changes', ['tree' => $individual->tree()->name(), 'xref' => $individual->xref()])) . '">' . I18N::translateContext('You should review the changes and then accept or reject them.', 'reject') . '</a>') . ' ' . FunctionsPrint::helpLink('pending_changes'),
+ I18N::translate('This individual has been edited. You should review the changes and then %1$s or %2$s them.', '<a href="#" class="alert-link" data-post-url="' . e(route(PendingChangesAcceptRecord::class, ['tree' => $individual->tree()->name(), 'xref' => $individual->xref()])) . '">' . I18N::translateContext('You should review the changes and then accept or reject them.', 'accept') . '</a>', '<a href="#" class="alert-link" data-post-url="' . e(route(PendingChangesRejectRecord::class, ['tree' => $individual->tree()->name(), 'xref' => $individual->xref()])) . '">' . I18N::translateContext('You should review the changes and then accept or reject them.', 'reject') . '</a>') . ' ' . FunctionsPrint::helpLink('pending_changes'),
]) ?>
<?php elseif (Auth::isEditor($individual->tree())) : ?>
<?= view('components/alert-warning-dismissible', ['alert' => I18N::translate('This individual has been edited. The changes need to be reviewed by a moderator.') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
diff --git a/resources/views/media-page.phtml b/resources/views/media-page.phtml
index 9b162a6ecf..26ccbe410b 100644
--- a/resources/views/media-page.phtml
+++ b/resources/views/media-page.phtml
@@ -4,6 +4,7 @@ use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Functions\FunctionsPrint;
use Fisharebest\Webtrees\Functions\FunctionsPrintFacts;
use Fisharebest\Webtrees\GedcomTag;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesAcceptRecord;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Media;
use Illuminate\Support\Collection;
@@ -21,13 +22,13 @@ use Illuminate\Support\Collection;
<?php if ($media->isPendingDeletion()) : ?>
<?php if (Auth::isModerator($media->tree())) : ?>
- <?= view('components/alert-warning-dismissible', ['alert' => /* I18N: %1$s is “accept”, %2$s is “reject”. These are links. */ I18N::translate('This media object has been deleted. You should review the deletion and then %1$s or %2$s it.', '<a href="#" class="alert-link" data-post-url="' . e(route('accept-changes', ['tree' => $media->tree()->name(), 'xref' => $media->xref()])) . '">' . I18N::translateContext('You should review the deletion and then accept or reject it.', 'accept') . '</a>', '<a href="#" class="alert-link" data-post-url="' . e(route('reject-changes', ['tree' => $media->tree()->name(), 'xref' => $media->xref()])) . '">' . I18N::translateContext('You should review the deletion and then accept or reject it.', 'reject') . '</a>') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
+ <?= view('components/alert-warning-dismissible', ['alert' => /* I18N: %1$s is “accept”, %2$s is “reject”. These are links. */ I18N::translate('This media object has been deleted. You should review the deletion and then %1$s or %2$s it.', '<a href="#" class="alert-link" data-post-url="' . e(route(PendingChangesAcceptRecord::class, ['tree' => $media->tree()->name(), 'xref' => $media->xref()])) . '">' . I18N::translateContext('You should review the deletion and then accept or reject it.', 'accept') . '</a>', '<a href="#" class="alert-link" data-post-url="' . e(route(PendingChangesRejectRecord::class, ['tree' => $media->tree()->name(), 'xref' => $media->xref()])) . '">' . I18N::translateContext('You should review the deletion and then accept or reject it.', 'reject') . '</a>') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
<?php elseif (Auth::isEditor($media->tree())) : ?>
<?= view('components/alert-warning-dismissible', ['alert' => I18N::translate('This media object has been deleted. The deletion will need to be reviewed by a moderator.') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
<?php endif ?>
<?php elseif ($media->isPendingAddition()) : ?>
<?php if (Auth::isModerator($media->tree())) : ?>
- <?= view('components/alert-warning-dismissible', ['alert' => /* I18N: %1$s is “accept”, %2$s is “reject”. These are links. */ I18N::translate('This media object has been edited. You should review the changes and then %1$s or %2$s them.', '<a href="#" class="alert-link" data-post-url="' . e(route('accept-changes', ['tree' => $media->tree()->name(), 'xref' => $media->xref()])) . '">' . I18N::translateContext('You should review the changes and then accept or reject them.', 'accept') . '</a>', '<a href="#" class="alert-link" data-post-url="' . e(route('reject-changes', ['tree' => $media->tree()->name(), 'xref' => $media->xref()])) . '">' . I18N::translateContext('You should review the changes and then accept or reject them.', 'reject') . '</a>') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
+ <?= view('components/alert-warning-dismissible', ['alert' => /* I18N: %1$s is “accept”, %2$s is “reject”. These are links. */ I18N::translate('This media object has been edited. You should review the changes and then %1$s or %2$s them.', '<a href="#" class="alert-link" data-post-url="' . e(route(PendingChangesAcceptRecord::class, ['tree' => $media->tree()->name(), 'xref' => $media->xref()])) . '">' . I18N::translateContext('You should review the changes and then accept or reject them.', 'accept') . '</a>', '<a href="#" class="alert-link" data-post-url="' . e(route(PendingChangesRejectRecord::class, ['tree' => $media->tree()->name(), 'xref' => $media->xref()])) . '">' . I18N::translateContext('You should review the changes and then accept or reject them.', 'reject') . '</a>') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
<?php elseif (Auth::isEditor($media->tree())) : ?>
<?= view('components/alert-warning-dismissible', ['alert' => I18N::translate('This media object has been edited. The changes need to be reviewed by a moderator.') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
<?php endif ?>
diff --git a/resources/views/note-page.phtml b/resources/views/note-page.phtml
index fc15cbe1a6..17b496f40c 100644
--- a/resources/views/note-page.phtml
+++ b/resources/views/note-page.phtml
@@ -1,17 +1,19 @@
<?php use Fisharebest\Webtrees\Auth; ?>
<?php use Fisharebest\Webtrees\Functions\FunctionsPrint; ?>
<?php use Fisharebest\Webtrees\Functions\FunctionsPrintFacts; ?>
-<?php use Fisharebest\Webtrees\I18N; ?>
+<?php use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesAcceptRecord;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesRejectRecord;
+use Fisharebest\Webtrees\I18N; ?>
<?php if ($note->isPendingDeletion()) : ?>
<?php if (Auth::isModerator($note->tree())) : ?>
- <?= view('components/alert-warning-dismissible', ['alert' => /* I18N: %1$s is “accept”, %2$s is “reject”. These are links. */ I18N::translate('This note has been deleted. You should review the deletion and then %1$s or %2$s it.', '<a href="#" class="alert-link" data-post-url="' . e(route('accept-changes', ['tree' => $note->tree()->name(), 'xref' => $note->xref()])) . '">' . I18N::translateContext('You should review the deletion and then accept or reject it.', 'accept') . '</a>', '<a href="#" class="alert-link" data-post-url="' . e(route('reject-changes', ['tree' => $note->tree()->name(), 'xref' => $note->xref()])) . '">' . I18N::translateContext('You should review the deletion and then accept or reject it.', 'reject') . '</a>') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
+ <?= view('components/alert-warning-dismissible', ['alert' => /* I18N: %1$s is “accept”, %2$s is “reject”. These are links. */ I18N::translate('This note has been deleted. You should review the deletion and then %1$s or %2$s it.', '<a href="#" class="alert-link" data-post-url="' . e(route(PendingChangesAcceptRecord::class, ['tree' => $note->tree()->name(), 'xref' => $note->xref()])) . '">' . I18N::translateContext('You should review the deletion and then accept or reject it.', 'accept') . '</a>', '<a href="#" class="alert-link" data-post-url="' . e(route(PendingChangesRejectRecord::class, ['tree' => $note->tree()->name(), 'xref' => $note->xref()])) . '">' . I18N::translateContext('You should review the deletion and then accept or reject it.', 'reject') . '</a>') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
<?php elseif (Auth::isEditor($note->tree())) : ?>
<?= view('components/alert-warning-dismissible', ['alert' => I18N::translate('This note has been deleted. The deletion will need to be reviewed by a moderator.') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
<?php endif ?>
<?php elseif ($note->isPendingAddition()) : ?>
<?php if (Auth::isModerator($note->tree())) : ?>
- <?= view('components/alert-warning-dismissible', ['alert' => /* I18N: %1$s is “accept”, %2$s is “reject”. These are links. */ I18N::translate('This note has been edited. You should review the changes and then %1$s or %2$s them.', '<a href="#" class="alert-link" data-post-url="' . e(route('accept-changes', ['tree' => $note->tree()->name(), 'xref' => $note->xref()])) . '">' . I18N::translateContext('You should review the changes and then accept or reject them.', 'accept') . '</a>', '<a href="#" class="alert-link" data-post-url="' . e(route('reject-changes', ['tree' => $note->tree()->name(), 'xref' => $note->xref()])) . '">' . I18N::translateContext('You should review the changes and then accept or reject them.', 'reject') . '</a>') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
+ <?= view('components/alert-warning-dismissible', ['alert' => /* I18N: %1$s is “accept”, %2$s is “reject”. These are links. */ I18N::translate('This note has been edited. You should review the changes and then %1$s or %2$s them.', '<a href="#" class="alert-link" data-post-url="' . e(route(PendingChangesAcceptRecord::class, ['tree' => $note->tree()->name(), 'xref' => $note->xref()])) . '">' . I18N::translateContext('You should review the changes and then accept or reject them.', 'accept') . '</a>', '<a href="#" class="alert-link" data-post-url="' . e(route(PendingChangesRejectRecord::class, ['tree' => $note->tree()->name(), 'xref' => $note->xref()])) . '">' . I18N::translateContext('You should review the changes and then accept or reject them.', 'reject') . '</a>') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
<?php elseif (Auth::isEditor($note->tree())) : ?>
<?= view('components/alert-warning-dismissible', ['alert' => I18N::translate('This note has been edited. The changes need to be reviewed by a moderator.') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
<?php endif ?>
diff --git a/resources/views/pending-changes-page.phtml b/resources/views/pending-changes-page.phtml
index 8af92e03df..dddf6ff315 100644
--- a/resources/views/pending-changes-page.phtml
+++ b/resources/views/pending-changes-page.phtml
@@ -1,5 +1,13 @@
-<?php use Fisharebest\Webtrees\I18N; ?>
-<?php use Fisharebest\Webtrees\Tree; ?>
+<?php
+
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesAcceptChange;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesAcceptTree;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesRejectChange;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesRejectTree;
+use Fisharebest\Webtrees\I18N;
+use Fisharebest\Webtrees\Tree;
+
+?>
<h2 class="wt-page-title">
<?= $title ?>
@@ -51,13 +59,11 @@
<?php foreach ($record_changes as $record_change) : ?>
<tr>
<td>
- <form method="post" action="<?= e(route('accept-pending', ['change_id' => $record_change->change_id, 'xref' => $record_change->xref, 'tree' => $record_change->gedcom_name, 'url' => $url])) ?>">
- <?= csrf_field() ?>
- <button class="btn btn-primary" type="submit">
- <?= I18N::translate('Accept') ?>
- </button>
- </form>
+ <a class="btn btn-primary" href="#" data-post-url="<?= e(route(PendingChangesAcceptChange::class, ['tree' => $record_change->gedcom_name, 'xref' => $record_change->xref, 'change' => $record_change->change_id])) ?>">
+ <?= I18N::translate('Accept') ?>
+ </a>
</td>
+
<td>
<?php foreach ($record_change->record->facts() as $fact) : ?>
<?php if ($fact->getTag() !== 'CHAN' && $fact->isPendingAddition()) : ?>
@@ -71,21 +77,21 @@
<?php endif ?>
<?php endforeach ?>
</td>
+
<td>
<a href="<?= e(route('message', ['to' => $record_change->user_name, 'subject' => I18N::translate('Pending changes') . ' - ' . strip_tags($record_change->record->fullName()), 'body' => $record_change->record->url(), 'tree' => $record_change->gedcom_name])) ?>" title="<?= I18N::translate('Send a message') ?>">
<?= e($record_change->real_name) ?> - <?= e($record_change->user_name) ?>
</a>
</td>
+
<td>
<?= view('components/datetime', ['timestamp' => $record_change->change_time]) ?>
</td>
+
<td>
- <form method="post" action="<?= e(route('reject-pending', ['change_id' => $record_change->change_id, 'xref' => $record_change->xref, 'tree' => $record_change->gedcom_name, 'url' => $url])) ?>">
- <?= csrf_field() ?>
- <button class="btn btn-secondary" type="submit">
- <?= I18N::translate('Reject') ?>
- </button>
- </form>
+ <a class="btn btn-secondary" href="#" data-post-url="<?= e(route(PendingChangesRejectChange::class, ['tree' => $record_change->gedcom_name, 'xref' => $record_change->xref, 'change' => $record_change->change_id])) ?>">
+ <?= I18N::translate('Reject') ?>
+ </a>
</td>
</tr>
<?php endforeach ?>
@@ -94,19 +100,13 @@
<?php endforeach ?>
<div class="d-flex justify-content-between">
- <form method="post" action="<?= e(route('accept-all-changes', ['tree' => $tree->name(), 'url' => $url])) ?>">
- <?= csrf_field() ?>
- <button class="btn btn-primary" type="submit">
- <?= I18N::translate('Accept all changes') ?>
- </button>
- </form>
+ <a class="btn btn-primary" href="#" data-post-url="<?= e(route(PendingChangesAcceptTree::class, ['tree' => $tree->name()])) ?>">
+ <?= I18N::translate('Accept all changes') ?>
+ </a>
- <form method="post" action="<?= e(route('reject-all-changes', ['tree' => $tree->name(), 'url' => $url])) ?>">
- <?= csrf_field() ?>
- <button class="btn btn-secondary" type="submit" data-confirm="<?= I18N::translate('Are you sure you want to reject all the changes to this family tree?') ?>" onclick="return confirm(this.dataset.confirm);">
- <?= I18N::translate('Reject all changes') ?>
- </button>
- </form>
+ <a class="btn btn-secondary" href="#" data-post-url="<?= e(route(PendingChangesRejectTree::class, ['tree' => $tree->name()])) ?>" data-confirm="<?= I18N::translate('Are you sure you want to reject all the changes to this family tree?') ?>">
+ <?= I18N::translate('Reject all changes') ?>
+ </a>
</div>
</div>
<?php endforeach ?>
diff --git a/resources/views/repository-page.phtml b/resources/views/repository-page.phtml
index 7fac1eccdb..9e169db337 100644
--- a/resources/views/repository-page.phtml
+++ b/resources/views/repository-page.phtml
@@ -3,19 +3,21 @@
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Functions\FunctionsPrint;
use Fisharebest\Webtrees\Functions\FunctionsPrintFacts;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesAcceptRecord;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesRejectRecord;
use Fisharebest\Webtrees\I18N;
?>
<?php if ($repository->isPendingDeletion()) : ?>
<?php if (Auth::isModerator($repository->tree())) : ?>
- <?= view('components/alert-warning-dismissible', ['alert' => /* I18N: %1$s is “accept”, %2$s is “reject”. These are links. */ I18N::translate('This repository has been deleted. You should review the deletion and then %1$s or %2$s it.', '<a href="#" class="alert-link" data-post-url="' . e(route('accept-changes', ['tree' => $repository->tree()->name(), 'xref' => $repository->xref()])) . '">' . I18N::translateContext('You should review the deletion and then accept or reject it.', 'accept') . '</a>', '<a href="#" class="alert-link" data-post-url="' . e(route('reject-changes', ['tree' => $repository->tree()->name(), 'xref' => $repository->xref()])) . '">' . I18N::translateContext('You should review the deletion and then accept or reject it.', 'reject') . '</a>') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
+ <?= view('components/alert-warning-dismissible', ['alert' => /* I18N: %1$s is “accept”, %2$s is “reject”. These are links. */ I18N::translate('This repository has been deleted. You should review the deletion and then %1$s or %2$s it.', '<a href="#" class="alert-link" data-post-url="' . e(route(PendingChangesAcceptRecord::class, ['tree' => $repository->tree()->name(), 'xref' => $repository->xref()])) . '">' . I18N::translateContext('You should review the deletion and then accept or reject it.', 'accept') . '</a>', '<a href="#" class="alert-link" data-post-url="' . e(route(PendingChangesRejectRecord::class, ['tree' => $repository->tree()->name(), 'xref' => $repository->xref()])) . '">' . I18N::translateContext('You should review the deletion and then accept or reject it.', 'reject') . '</a>') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
<?php elseif (Auth::isEditor($repository->tree())) : ?>
<?= view('components/alert-warning-dismissible', ['alert' => I18N::translate('This repository has been deleted. The deletion will need to be reviewed by a moderator.') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
<?php endif ?>
<?php elseif ($repository->isPendingAddition()) : ?>
<?php if (Auth::isModerator($repository->tree())) : ?>
- <?= view('components/alert-warning-dismissible', ['alert' => /* I18N: %1$s is “accept”, %2$s is “reject”. These are links. */ I18N::translate('This repository has been edited. You should review the changes and then %1$s or %2$s them.', '<a href="#" class="alert-link" data-post-url="' . e(route('accept-changes', ['tree' => $repository->tree()->name(), 'xref' => $repository->xref()])) . '">' . I18N::translateContext('You should review the changes and then accept or reject them.', 'accept') . '</a>', '<a href="#" class="alert-link" data-post-url="' . e(route('reject-changes', ['tree' => $repository->tree()->name(), 'xref' => $repository->xref()])) . '">' . I18N::translateContext('You should review the changes and then accept or reject them.', 'reject') . '</a>') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
+ <?= view('components/alert-warning-dismissible', ['alert' => /* I18N: %1$s is “accept”, %2$s is “reject”. These are links. */ I18N::translate('This repository has been edited. You should review the changes and then %1$s or %2$s them.', '<a href="#" class="alert-link" data-post-url="' . e(route(PendingChangesAcceptRecord::class, ['tree' => $repository->tree()->name(), 'xref' => $repository->xref()])) . '">' . I18N::translateContext('You should review the changes and then accept or reject them.', 'accept') . '</a>', '<a href="#" class="alert-link" data-post-url="' . e(route(PendingChangesRejectRecord::class, ['tree' => $repository->tree()->name(), 'xref' => $repository->xref()])) . '">' . I18N::translateContext('You should review the changes and then accept or reject them.', 'reject') . '</a>') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
<?php elseif (Auth::isEditor($repository->tree())) : ?>
<?= view('components/alert-warning-dismissible', ['alert' => I18N::translate('This repository has been edited. The changes need to be reviewed by a moderator.') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
<?php endif ?>
diff --git a/resources/views/source-page.phtml b/resources/views/source-page.phtml
index 405c6d160b..aef4c5114c 100644
--- a/resources/views/source-page.phtml
+++ b/resources/views/source-page.phtml
@@ -1,17 +1,19 @@
<?php use Fisharebest\Webtrees\Auth; ?>
<?php use Fisharebest\Webtrees\Functions\FunctionsPrint; ?>
<?php use Fisharebest\Webtrees\Functions\FunctionsPrintFacts; ?>
-<?php use Fisharebest\Webtrees\I18N; ?>
+<?php use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesAcceptRecord;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesRejectRecord;
+use Fisharebest\Webtrees\I18N; ?>
<?php if ($source->isPendingDeletion()) : ?>
<?php if (Auth::isModerator($source->tree())) : ?>
- <?= view('components/alert-warning-dismissible', ['alert' => /* I18N: %1$s is “accept”, %2$s is “reject”. These are links. */ I18N::translate('This source has been deleted. You should review the deletion and then %1$s or %2$s it.', '<a href="#" class="alert-link" data-post-url="' . e(route('accept-changes', ['tree' => $source->tree()->name(), 'xref' => $source->xref()])) . '">' . I18N::translateContext('You should review the deletion and then accept or reject it.', 'accept') . '</a>', '<a href="#" class="alert-link" data-post-url="' . e(route('reject-changes', ['tree' => $source->tree()->name(), 'xref' => $source->xref()])) . '">' . I18N::translateContext('You should review the deletion and then accept or reject it.', 'reject') . '</a>') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
+ <?= view('components/alert-warning-dismissible', ['alert' => /* I18N: %1$s is “accept”, %2$s is “reject”. These are links. */ I18N::translate('This source has been deleted. You should review the deletion and then %1$s or %2$s it.', '<a href="#" class="alert-link" data-post-url="' . e(route(PendingChangesAcceptRecord::class, ['tree' => $source->tree()->name(), 'xref' => $source->xref()])) . '">' . I18N::translateContext('You should review the deletion and then accept or reject it.', 'accept') . '</a>', '<a href="#" class="alert-link" data-post-url="' . e(route(PendingChangesRejectRecord::class, ['tree' => $source->tree()->name(), 'xref' => $source->xref()])) . '">' . I18N::translateContext('You should review the deletion and then accept or reject it.', 'reject') . '</a>') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
<?php elseif (Auth::isEditor($source->tree())) : ?>
<?= view('components/alert-warning-dismissible', ['alert' => I18N::translate('This source has been deleted. The deletion will need to be reviewed by a moderator.') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
<?php endif ?>
<?php elseif ($source->isPendingAddition()) : ?>
<?php if (Auth::isModerator($source->tree())) : ?>
- <?= view('components/alert-warning-dismissible', ['alert' => /* I18N: %1$s is “accept”, %2$s is “reject”. These are links. */ I18N::translate('This source has been edited. You should review the changes and then %1$s or %2$s them.', '<a href="#" class="alert-link" data-post-url="' . e(route('accept-changes', ['tree' => $source->tree()->name(), 'xref' => $source->xref()])) . '">' . I18N::translateContext('You should review the changes and then accept or reject them.', 'accept') . '</a>', '<a href="#" class="alert-link" data-post-url="' . e(route('reject-changes', ['tree' => $source->tree()->name(), 'xref' => $source->xref()])) . '">' . I18N::translateContext('You should review the changes and then accept or reject them.', 'reject') . '</a>') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
+ <?= view('components/alert-warning-dismissible', ['alert' => /* I18N: %1$s is “accept”, %2$s is “reject”. These are links. */ I18N::translate('This source has been edited. You should review the changes and then %1$s or %2$s them.', '<a href="#" class="alert-link" data-post-url="' . e(route(PendingChangesAcceptRecord::class, ['tree' => $source->tree()->name(), 'xref' => $source->xref()])) . '">' . I18N::translateContext('You should review the changes and then accept or reject them.', 'accept') . '</a>', '<a href="#" class="alert-link" data-post-url="' . e(route(PendingChangesRejectRecord::class, ['tree' => $source->tree()->name(), 'xref' => $source->xref()])) . '">' . I18N::translateContext('You should review the changes and then accept or reject them.', 'reject') . '</a>') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
<?php elseif (Auth::isEditor($source->tree())) : ?>
<?= view('components/alert-warning-dismissible', ['alert' => I18N::translate('This source has been edited. The changes need to be reviewed by a moderator.') . ' ' . FunctionsPrint::helpLink('pending_changes')]) ?>
<?php endif ?>
diff --git a/routes/web.php b/routes/web.php
index e339e0d8f0..9116ce2db7 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -38,6 +38,18 @@ use Fisharebest\Webtrees\Http\RequestHandlers\DeleteTreeAction;
use Fisharebest\Webtrees\Http\RequestHandlers\DeleteUser;
use Fisharebest\Webtrees\Http\RequestHandlers\HelpText;
use Fisharebest\Webtrees\Http\RequestHandlers\HomePage;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChanges;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesAcceptChange;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesAcceptRecord;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesAcceptTree;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesLogAction;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesLogData;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesLogDelete;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesLogDownload;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesLogPage;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesRejectChange;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesRejectRecord;
+use Fisharebest\Webtrees\Http\RequestHandlers\PendingChangesRejectTree;
use Fisharebest\Webtrees\Http\RequestHandlers\PhpInformation;
use Fisharebest\Webtrees\Http\RequestHandlers\RedirectFamilyPhp;
use Fisharebest\Webtrees\Http\RequestHandlers\RedirectGedRecordPhp;
@@ -199,9 +211,11 @@ $router->attach('', '/tree/{tree}', static function (Map $router) {
'middleware' => [AuthManager::class]
]);
- $router->get('admin-changes-log', '/changes-log', 'Admin\ChangesLogController::changesLog');
- $router->get('admin-changes-log-data', '/changes-log-data', 'Admin\ChangesLogController::changesLogData');
- $router->get('admin-changes-log-download', '/changes-log-download', 'Admin\ChangesLogController::changesLogDownload');
+ $router->get(PendingChangesLogPage::class, '/changes-log', PendingChangesLogPage::class);
+ $router->post(PendingChangesLogAction::class, '/changes-log', PendingChangesLogAction::class);
+ $router->get(PendingChangesLogData::class, '/changes-data', PendingChangesLogData::class);
+ $router->post(PendingChangesLogDelete::class, '/changes-delete', PendingChangesLogDelete::class);
+ $router->get(PendingChangesLogDownload::class, '/changes-download', PendingChangesLogDownload::class);
$router->get('admin-trees-check', '/check', 'AdminTreesController::check');
$router->get('admin-trees-duplicates', '/duplicates', 'AdminTreesController::duplicates');
$router->get('admin-trees-export', '/export', 'AdminTreesController::export');
@@ -229,17 +243,18 @@ $router->attach('', '/tree/{tree}', static function (Map $router) {
$router->post('tree-privacy-update', '/privacy', 'AdminController::treePrivacyUpdate');
});
-// Manager routes.
+// Moderator routes.
$router->attach('', '/tree/{tree}', static function (Map $router) {
$router->extras([
'middleware' => [AuthModerator::class]
]);
-
- $router->get('show-pending', '/show-pending', 'PendingChangesController::showChanges');
- $router->post('accept-pending', '/accept-pending', 'PendingChangesController::acceptChange');
- $router->post('reject-pending', '/reject-pending', 'PendingChangesController::rejectChange');
- $router->post('accept-all-pending', '/accept-all-pending', 'PendingChangesController::acceptAllChanges');
- $router->post('reject-all-pending', '/reject-all-pending', 'PendingChangesController::rejectAllChanges');
+ $router->post(PendingChangesAcceptTree::class, '/accept', PendingChangesAcceptTree::class);
+ $router->post(PendingChangesAcceptRecord::class, '/accept/{xref}', PendingChangesAcceptRecord::class);
+ $router->post(PendingChangesAcceptChange::class, '/accept/{xref}/{change}', PendingChangesAcceptChange::class);
+ $router->get(PendingChanges::class, '/pending', PendingChanges::class);
+ $router->post(PendingChangesRejectTree::class, '/reject', PendingChangesRejectTree::class);
+ $router->post(PendingChangesRejectRecord::class, '/reject/{xref}', PendingChangesRejectRecord::class);
+ $router->post(PendingChangesRejectChange::class, '/reject/{xref}/{change}', PendingChangesRejectChange::class);
});
// Editor routes.
@@ -342,8 +357,6 @@ $router->attach('', '', static function (Map $router) {
// Public routes.
$router->attach('', '/tree/{tree}', static function (Map $router) {
$router->get('tree-page', '/', 'HomePageController::treePage');
- $router->post('accept-changes', '/accept/{xref}', 'PendingChangesController::acceptChanges');
- $router->post('accept-all-changes', '/accept-all-changes', 'PendingChangesController::acceptAllChanges');
$router->get('autocomplete-folder', '/autocomplete-folder', 'AutocompleteController::folder');
$router->get('autocomplete-page', '/autocomplete-page', 'AutocompleteController::page');
$router->get('autocomplete-place', '/autocomplete-place', 'AutocompleteController::place');
@@ -361,8 +374,6 @@ $router->attach('', '/tree/{tree}', static function (Map $router) {
$router->get('note', '/note/{xref}{/slug}', 'NoteController::show');
$router->get('record', '/record/{xref}{/slug}', 'GedcomRecordController::show');
$router->get('repository', '/repository/{xref}{/slug}', 'RepositoryController::show');
- $router->post('reject-changes', '/reject/{xref}', 'PendingChangesController::rejectChanges');
- $router->post('reject-all-changes', '/reject-all-changes', 'PendingChangesController::rejectAllChanges');
$router->get(ReportListPage::class, '/report', ReportListPage::class);
$router->post(ReportListAction::class, '/report', ReportListAction::class);
$router->get(ReportSetupPage::class, '/report/{report}', ReportSetupPage::class);
diff --git a/tests/app/AgeTest.php b/tests/app/AgeTest.php
index f64ac813c1..47561aa01b 100644
--- a/tests/app/AgeTest.php
+++ b/tests/app/AgeTest.php
@@ -43,7 +43,7 @@ class AgeTest extends TestCase
* @covers \Fisharebest\Webtrees\Age::extractNumber
* @return void
*/
- public function testConstructor($ageTest, $ageAsText):void
+ public function testConstructor($ageTest, $ageAsText): void
{
$age = new Age($ageTest);
$this->assertSame($ageAsText, $age->asText());
diff --git a/tests/app/Http/Controllers/Admin/ChangesLogControllerTest.php b/tests/app/Http/Controllers/Admin/ChangesLogControllerTest.php
deleted file mode 100644
index 8c42485e98..0000000000
--- a/tests/app/Http/Controllers/Admin/ChangesLogControllerTest.php
+++ /dev/null
@@ -1,106 +0,0 @@
-<?php
-
-/**
- * webtrees: online genealogy
- * Copyright (C) 2019 webtrees development team
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-declare(strict_types=1);
-
-namespace Fisharebest\Webtrees\Http\Controllers\Admin;
-
-use Fig\Http\Message\RequestMethodInterface;
-use Fig\Http\Message\StatusCodeInterface;
-use Fisharebest\Algorithm\MyersDiff;
-use Fisharebest\Webtrees\Auth;
-use Fisharebest\Webtrees\Services\DatatablesService;
-use Fisharebest\Webtrees\Services\TreeService;
-use Fisharebest\Webtrees\Services\UserService;
-use Fisharebest\Webtrees\TestCase;
-
-/**
- * Test the changes log controller
- *
- * @covers \Fisharebest\Webtrees\Http\Controllers\Admin\ChangesLogController
- */
-class ChangesLogControllerTest extends TestCase
-{
- protected static $uses_database = true;
-
- /**
- * @return void
- */
- public function testChangeLog(): void
- {
- $datatables_service = new DatatablesService();
- $myers_diff = new MyersDiff();
- $tree_service = new TreeService();
- $user_service = new UserService();
- $controller = new ChangesLogController($datatables_service, $myers_diff, $tree_service, $user_service);
- $request = self::createRequest();
- $response = $controller->changesLog($request);
-
- $this->assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode());
- }
-
- /**
- * @return void
- */
- public function testChangeLogData(): void
- {
- $datatables_service = new DatatablesService();
- $myers_diff = new MyersDiff();
- $tree_service = new TreeService();
- $user_service = new UserService();
- $tree = $tree_service->create('name', 'title');
- $user = $user_service->create('user', 'name', 'email', 'password');
- $user->setPreference('canadmin', '1');
- Auth::login($user);
- $individual = $tree->createIndividual("0 @@ INDI\n1 NAME Joe Bloggs");
- $controller = new ChangesLogController($datatables_service, $myers_diff, $tree_service, $user_service);
- $request = self::createRequest(RequestMethodInterface::METHOD_GET, [
- 'search' => 'Joe',
- 'from' => '2000-01-01',
- 'to' => '2099-12-31',
- 'type' => 'pending',
- 'xref' => $individual->xref(),
- 'tree' => $tree->name(),
- 'user' => $user->userName(),
- ]);
- $response = $controller->changesLogData($request);
-
- $this->assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode());
- }
-
- /**
- * @return void
- */
- public function testChangeLogDownload(): void
- {
- $datatables_service = new DatatablesService();
- $myers_diff = new MyersDiff();
- $tree_service = new TreeService();
- $user_service = new UserService();
- $tree = $tree_service->create('name', 'title');
- $user = $user_service->create('user', 'name', 'email', 'password');
- $user->setPreference('canadmin', '1');
- Auth::login($user);
- $tree->createIndividual("0 @@ INDI\n1 NAME Joe Bloggs");
-
- $controller = new ChangesLogController($datatables_service, $myers_diff, $tree_service, $user_service);
- $request = self::createRequest();
- $response = $controller->changesLogDownload($request);
-
- $this->assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode());
- }
-}
diff --git a/tests/app/Http/Controllers/Admin/ImportThumbnailsControllerTest.php b/tests/app/Http/Controllers/Admin/ImportThumbnailsControllerTest.php
index 2c709c0f14..c7fbae1fe2 100644
--- a/tests/app/Http/Controllers/Admin/ImportThumbnailsControllerTest.php
+++ b/tests/app/Http/Controllers/Admin/ImportThumbnailsControllerTest.php
@@ -20,6 +20,7 @@ declare(strict_types=1);
namespace Fisharebest\Webtrees\Http\Controllers\Admin;
use Fig\Http\Message\StatusCodeInterface;
+use Fisharebest\Webtrees\Services\PendingChangesService;
use Fisharebest\Webtrees\Services\TreeService;
use Fisharebest\Webtrees\TestCase;
@@ -37,10 +38,11 @@ class ImportThumbnailsControllerTest extends TestCase
*/
public function testWebtrees1Thumbnails(): void
{
- $tree_service = new TreeService();
- $controller = new ImportThumbnailsController($tree_service);
- $request = self::createRequest();
- $response = $controller->webtrees1Thumbnails($request);
+ $tree_service = new TreeService();
+ $pending_changes_service = new PendingChangesService();
+ $controller = new ImportThumbnailsController($pending_changes_service, $tree_service);
+ $request = self::createRequest();
+ $response = $controller->webtrees1Thumbnails($request);
$this->assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode());
}
@@ -50,11 +52,12 @@ class ImportThumbnailsControllerTest extends TestCase
*/
public function testWebtrees1ThumbnailsAction(): void
{
- $tree_service = new TreeService();
- $controller = new ImportThumbnailsController($tree_service);
- $request = self::createRequest()
+ $tree_service = new TreeService();
+ $pending_changes_service = new PendingChangesService();
+ $controller = new ImportThumbnailsController($pending_changes_service, $tree_service);
+ $request = self::createRequest()
->withParsedBody(['thumbnail' => 'foo', 'action' => '', 'xref' => [], 'ged' => []]);
- $response = $controller->webtrees1ThumbnailsAction($request);
+ $response = $controller->webtrees1ThumbnailsAction($request);
$this->assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode());
}
@@ -64,11 +67,12 @@ class ImportThumbnailsControllerTest extends TestCase
*/
public function testWebtrees1ThumbnailsData(): void
{
- $tree_service = new TreeService();
- $controller = new ImportThumbnailsController($tree_service);
- $request = self::createRequest()
+ $tree_service = new TreeService();
+ $pending_changes_service = new PendingChangesService();
+ $controller = new ImportThumbnailsController($pending_changes_service, $tree_service);
+ $request = self::createRequest()
->withQueryParams(['start' => '0', 'length' => '10', 'search' => ['value' => ''], 'draw' => '1']);
- $response = $controller->webtrees1ThumbnailsData($request);
+ $response = $controller->webtrees1ThumbnailsData($request);
$this->assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode());
}