1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2021 webtrees development team 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <https://www.gnu.org/licenses/>. 16 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees\Module; 21 22use Fisharebest\Webtrees\Auth; 23use Fisharebest\Webtrees\Carbon; 24use Fisharebest\Webtrees\Contracts\UserInterface; 25use Fisharebest\Webtrees\Http\RequestHandlers\PendingChanges; 26use Fisharebest\Webtrees\I18N; 27use Fisharebest\Webtrees\Registry; 28use Fisharebest\Webtrees\Services\EmailService; 29use Fisharebest\Webtrees\Services\TreeService; 30use Fisharebest\Webtrees\Services\UserService; 31use Fisharebest\Webtrees\Site; 32use Fisharebest\Webtrees\SiteUser; 33use Fisharebest\Webtrees\Tree; 34use Fisharebest\Webtrees\TreeUser; 35use Illuminate\Database\Capsule\Manager as DB; 36use Illuminate\Database\Query\Builder; 37use Illuminate\Database\Query\Expression; 38use Illuminate\Support\Str; 39use Psr\Http\Message\ServerRequestInterface; 40 41/** 42 * Class ReviewChangesModule 43 */ 44class ReviewChangesModule extends AbstractModule implements ModuleBlockInterface 45{ 46 use ModuleBlockTrait; 47 48 private EmailService $email_service; 49 50 private UserService $user_service; 51 52 private TreeService $tree_service; 53 54 /** 55 * ReviewChangesModule constructor. 56 * 57 * @param EmailService $email_service 58 * @param TreeService $tree_service 59 * @param UserService $user_service 60 */ 61 public function __construct( 62 EmailService $email_service, 63 TreeService $tree_service, 64 UserService $user_service 65 ) { 66 $this->email_service = $email_service; 67 $this->tree_service = $tree_service; 68 $this->user_service = $user_service; 69 } 70 71 /** 72 * How should this module be identified in the control panel, etc.? 73 * 74 * @return string 75 */ 76 public function title(): string 77 { 78 /* I18N: Name of a module */ 79 return I18N::translate('Pending changes'); 80 } 81 82 /** 83 * A sentence describing what this module does. 84 * 85 * @return string 86 */ 87 public function description(): string 88 { 89 /* I18N: Description of the “Pending changes” module */ 90 return I18N::translate('A list of changes that need to be reviewed by a moderator, and email notifications.'); 91 } 92 93 /** 94 * Generate the HTML content of this block. 95 * 96 * @param Tree $tree 97 * @param int $block_id 98 * @param string $context 99 * @param array<string> $config 100 * 101 * @return string 102 */ 103 public function getBlock(Tree $tree, int $block_id, string $context, array $config = []): string 104 { 105 $old_language = I18N::languageTag(); 106 107 $sendmail = (bool) $this->getBlockSetting($block_id, 'sendmail', '1'); 108 $days = (int) $this->getBlockSetting($block_id, 'days', '1'); 109 110 extract($config, EXTR_OVERWRITE); 111 112 $changes_exist = DB::table('change') 113 ->where('status', 'pending') 114 ->exists(); 115 116 if ($changes_exist && $sendmail) { 117 $last_email_timestamp = Carbon::createFromTimestamp((int) Site::getPreference('LAST_CHANGE_EMAIL')); 118 $next_email_timestamp = $last_email_timestamp->addDays($days); 119 120 // There are pending changes - tell moderators/managers/administrators about them. 121 if ($next_email_timestamp < Carbon::now()) { 122 // Which users have pending changes? 123 foreach ($this->user_service->all() as $user) { 124 if ($user->getPreference(UserInterface::PREF_CONTACT_METHOD) !== 'none') { 125 foreach ($this->tree_service->all() as $tmp_tree) { 126 if ($tmp_tree->hasPendingEdit() && Auth::isManager($tmp_tree, $user)) { 127 I18N::init($user->getPreference(UserInterface::PREF_LANGUAGE)); 128 129 $this->email_service->send( 130 new SiteUser(), 131 $user, 132 new TreeUser($tmp_tree), 133 I18N::translate('Pending changes'), 134 view('emails/pending-changes-text', [ 135 'tree' => $tmp_tree, 136 'user' => $user, 137 ]), 138 view('emails/pending-changes-html', [ 139 'tree' => $tmp_tree, 140 'user' => $user, 141 ]) 142 ); 143 } 144 } 145 } 146 } 147 I18N::init($old_language); 148 Site::setPreference('LAST_CHANGE_EMAIL', (string) Carbon::now()->unix()); 149 } 150 } 151 if (Auth::isEditor($tree) && $tree->hasPendingEdit()) { 152 $content = ''; 153 if (Auth::isModerator($tree)) { 154 $content .= '<a href="' . e(route(PendingChanges::class, ['tree' => $tree->name()])) . '">' . I18N::translate('There are pending changes for you to moderate.') . '</a><br>'; 155 } 156 if ($sendmail) { 157 $last_email_timestamp = Carbon::createFromTimestamp((int) Site::getPreference('LAST_CHANGE_EMAIL')); 158 $next_email_timestamp = $last_email_timestamp->copy()->addDays($days); 159 160 $content .= I18N::translate('Last email reminder was sent ') . view('components/datetime', ['timestamp' => $last_email_timestamp]) . '<br>'; 161 $content .= I18N::translate('Next email reminder will be sent after ') . view('components/datetime', ['timestamp' => $next_email_timestamp]) . '<br><br>'; 162 } 163 $content .= '<ul>'; 164 165 $changes = DB::table('change') 166 ->where('gedcom_id', '=', $tree->id()) 167 ->whereIn('change_id', static function (Builder $query) use ($tree): void { 168 $query->select(new Expression('MAX(change_id)')) 169 ->from('change') 170 ->where('gedcom_id', '=', $tree->id()) 171 ->where('status', '=', 'pending') 172 ->groupBy(['xref']); 173 }) 174 //->select(['xref']) 175 ->get(); 176 177 foreach ($changes as $change) { 178 $record = Registry::gedcomRecordFactory()->make($change->xref, $tree, $change->new_gedcom ?: $change->old_gedcom); 179 if ($record->canShow()) { 180 $content .= '<li><a href="' . e($record->url()) . '">' . $record->fullName() . '</a></li>'; 181 } 182 } 183 $content .= '</ul>'; 184 185 if ($context !== self::CONTEXT_EMBED) { 186 return view('modules/block-template', [ 187 'block' => Str::kebab($this->name()), 188 'id' => $block_id, 189 'config_url' => $this->configUrl($tree, $context, $block_id), 190 'title' => $this->title(), 191 'content' => $content, 192 ]); 193 } 194 195 return $content; 196 } 197 198 return ''; 199 } 200 201 /** 202 * Should this block load asynchronously using AJAX? 203 * 204 * Simple blocks are faster in-line, more complex ones can be loaded later. 205 * 206 * @return bool 207 */ 208 public function loadAjax(): bool 209 { 210 return false; 211 } 212 213 /** 214 * Can this block be shown on the user’s home page? 215 * 216 * @return bool 217 */ 218 public function isUserBlock(): bool 219 { 220 return true; 221 } 222 223 /** 224 * Can this block be shown on the tree’s home page? 225 * 226 * @return bool 227 */ 228 public function isTreeBlock(): bool 229 { 230 return true; 231 } 232 233 /** 234 * Update the configuration for a block. 235 * 236 * @param ServerRequestInterface $request 237 * @param int $block_id 238 * 239 * @return void 240 */ 241 public function saveBlockConfiguration(ServerRequestInterface $request, int $block_id): void 242 { 243 $params = (array) $request->getParsedBody(); 244 245 $this->setBlockSetting($block_id, 'days', $params['days']); 246 $this->setBlockSetting($block_id, 'sendmail', $params['sendmail']); 247 } 248 249 /** 250 * An HTML form to edit block settings 251 * 252 * @param Tree $tree 253 * @param int $block_id 254 * 255 * @return string 256 */ 257 public function editBlockConfiguration(Tree $tree, int $block_id): string 258 { 259 $sendmail = $this->getBlockSetting($block_id, 'sendmail', '1'); 260 $days = $this->getBlockSetting($block_id, 'days', '1'); 261 262 return view('modules/review_changes/config', [ 263 'days' => $days, 264 'sendmail' => $sendmail, 265 ]); 266 } 267} 268