1<?php 2/** 3 * webtrees: online genealogy 4 * Copyright (C) 2019 webtrees development team 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * You should have received a copy of the GNU General Public License 14 * along with this program. If not, see <http://www.gnu.org/licenses/>. 15 */ 16namespace Fisharebest\Webtrees; 17 18use Fisharebest\Webtrees\Controller\PageController; 19use Fisharebest\Webtrees\Functions\FunctionsEdit; 20use PDO; 21 22/** 23 * Defined in session.php 24 * 25 * @global Tree $WT_TREE 26 */ 27global $WT_TREE; 28 29define('WT_SCRIPT_NAME', 'admin_site_logs.php'); 30require './includes/session.php'; 31 32$controller = new PageController; 33$controller 34 ->restrictAccess(Auth::isManager($WT_TREE)) 35 ->setPageTitle(I18N::translate('Website logs')); 36 37$earliest = Database::prepare("SELECT IFNULL(DATE(MIN(log_time)), CURDATE()) FROM `##log`")->execute(array())->fetchOne(); 38$latest = Database::prepare("SELECT IFNULL(DATE(MAX(log_time)), CURDATE()) FROM `##log`")->execute(array())->fetchOne(); 39 40// Filtering 41$action = Filter::get('action'); 42$from = Filter::get('from', '\d\d\d\d-\d\d-\d\d', $earliest); 43$to = Filter::get('to', '\d\d\d\d-\d\d-\d\d', $latest); 44$type = Filter::get('type', 'auth|change|config|debug|edit|error|media|search'); 45$text = Filter::get('text'); 46$ip = Filter::get('ip'); 47$user = Filter::get('user'); 48$search = Filter::get('search'); 49$search = isset($search['value']) ? $search['value'] : null; 50 51if (Auth::isAdmin()) { 52 // Administrators can see all logs 53 $gedc = Filter::get('gedc'); 54} else { 55 // Managers can only see logs relating to this gedcom 56 $gedc = $WT_TREE->getName(); 57} 58 59$sql_select = 60 "SELECT SQL_CALC_FOUND_ROWS log_id, log_time, log_type, log_message, ip_address, IFNULL(user_name, '<none>') AS user_name, IFNULL(gedcom_name, '<none>') AS gedcom_name" . 61 " FROM `##log`" . 62 " LEFT JOIN `##user` USING (user_id)" . // user may be deleted 63 " LEFT JOIN `##gedcom` USING (gedcom_id)"; // gedcom may be deleted 64 65$where = " WHERE 1"; 66$args = array(); 67if ($search) { 68 $where .= " AND log_message LIKE CONCAT('%', :search, '%')"; 69 $args['search'] = $search; 70} 71if ($from) { 72 $where .= " AND log_time >= :from"; 73 $args['from'] = $from; 74} 75if ($to) { 76 $where .= " AND log_time < TIMESTAMPADD(DAY, 1 , :to)"; // before end of the day 77 $args['to'] = $to; 78} 79if ($type) { 80 $where .= " AND log_type = :type"; 81 $args['type'] = $type; 82} 83if ($text) { 84 $where .= " AND log_message LIKE CONCAT('%', :text, '%')"; 85 $args['text'] = $text; 86} 87if ($ip) { 88 $where .= " AND ip_address LIKE CONCAT('%', :ip, '%')"; 89 $args['ip'] = $ip; 90} 91if ($user) { 92 $where .= " AND user_name LIKE CONCAT('%', :user, '%')"; 93 $args['user'] = $user; 94} 95if ($gedc) { 96 $where .= " AND gedcom_name LIKE CONCAT('%', :gedc, '%')"; 97 $args['gedc'] = $gedc; 98} 99 100switch ($action) { 101 case 'delete': 102 $sql_delete = 103 "DELETE `##log` FROM `##log`" . 104 " LEFT JOIN `##user` USING (user_id)" . // user may be deleted 105 " LEFT JOIN `##gedcom` USING (gedcom_id)"; // gedcom may be deleted 106 107 Database::prepare($sql_delete . $where)->execute($args); 108 break; 109 110 case 'export': 111 header('Content-Type: text/csv'); 112 header('Content-Disposition: attachment; filename="webtrees-logs.csv"'); 113 $rows = Database::prepare($sql_select . $where . ' ORDER BY log_id')->execute($args)->fetchAll(); 114 foreach ($rows as $row) { 115 echo 116 '"', $row->log_time, '",', 117 '"', $row->log_type, '",', 118 '"', str_replace('"', '""', $row->log_message), '",', 119 '"', $row->ip_address, '",', 120 '"', str_replace('"', '""', $row->user_name), '",', 121 '"', str_replace('"', '""', $row->gedcom_name), '"', 122 "\n"; 123 } 124 125 return; 126 case 'load_json': 127 $start = Filter::getInteger('start'); 128 $length = Filter::getInteger('length'); 129 $order = Filter::getArray('order'); 130 131 if ($order) { 132 $order_by = " ORDER BY "; 133 foreach ($order as $key => $value) { 134 if ($key > 0) { 135 $order_by .= ','; 136 } 137 // Datatables numbers columns 0, 1, 2 138 // MySQL numbers columns 1, 2, 3 139 switch ($value['dir']) { 140 case 'asc': 141 $order_by .= (1 + $value['column']) . " ASC "; 142 break; 143 case 'desc': 144 $order_by .= (1 + $value['column']) . " DESC "; 145 break; 146 } 147 } 148 } else { 149 $order_by = " ORDER BY 1 ASC"; 150 } 151 152 if ($length) { 153 Auth::user()->setPreference('admin_site_log_page_size', $length); 154 $limit = " LIMIT :limit OFFSET :offset"; 155 $args['limit'] = $length; 156 $args['offset'] = $start; 157 } else { 158 $limit = ""; 159 } 160 161 // This becomes a JSON list, not array, so need to fetch with numeric keys. 162 $data = Database::prepare($sql_select . $where . $order_by . $limit)->execute($args)->fetchAll(PDO::FETCH_NUM); 163 foreach ($data as &$datum) { 164 $datum[2] = Filter::escapeHtml($datum[2]); 165 $datum[3] = '<span dir="auto">' . Filter::escapeHtml($datum[3]) . '</span>'; 166 $datum[4] = '<span dir="auto">' . Filter::escapeHtml($datum[4]) . '</span>'; 167 $datum[5] = '<span dir="auto">' . Filter::escapeHtml($datum[5]) . '</span>'; 168 $datum[6] = '<span dir="auto">' . Filter::escapeHtml($datum[6]) . '</span>'; 169 } 170 171 // Total filtered/unfiltered rows 172 $recordsFiltered = (int) Database::prepare("SELECT FOUND_ROWS()")->fetchOne(); 173 $recordsTotal = (int) Database::prepare("SELECT COUNT(*) FROM `##log`")->fetchOne(); 174 175 header('Content-type: application/json'); 176 // See http://www.datatables.net/usage/server-side 177 echo json_encode(array( 178 'draw' => Filter::getInteger('draw'), 179 'recordsTotal' => $recordsTotal, 180 'recordsFiltered' => $recordsFiltered, 181 'data' => $data, 182 )); 183 184 return; 185} 186 187$controller 188 ->pageHeader() 189 ->addExternalJavascript(WT_JQUERY_DATATABLES_JS_URL) 190 ->addExternalJavascript(WT_DATATABLES_BOOTSTRAP_JS_URL) 191 ->addExternalJavascript(WT_MOMENT_JS_URL) 192 ->addExternalJavascript(WT_BOOTSTRAP_DATETIMEPICKER_JS_URL) 193 ->addInlineJavascript(' 194 jQuery(".table-site-logs").dataTable( { 195 processing: true, 196 serverSide: true, 197 ajax: "' . WT_BASE_URL . WT_SCRIPT_NAME . '?action=load_json&from=' . $from . '&to=' . $to . '&type=' . $type . '&text=' . rawurlencode($text) . '&ip=' . rawurlencode($ip) . '&user=' . rawurlencode($user) . '&gedc=' . rawurlencode($gedc) . '", 198 ' . I18N::datatablesI18N(array(10, 20, 50, 100, 500, 1000, -1)) . ', 199 sorting: [[ 0, "desc" ]], 200 pageLength: ' . Auth::user()->getPreference('admin_site_log_page_size', 10) . ', 201 columns: [ 202 /* log_id */ { visible: false }, 203 /* Timestamp */ { sort: 0 }, 204 /* Type */ { }, 205 /* message */ { }, 206 /* IP address */ { }, 207 /* User */ { }, 208 /* Family tree */ { } 209 ] 210 }); 211 jQuery("#from,#to").parent("div").datetimepicker({ 212 format: "YYYY-MM-DD", 213 minDate: "' . $earliest . '", 214 maxDate: "' . $latest . '", 215 locale: "' . WT_LOCALE . '", 216 icons: { 217 time: "fa fa-clock-o", 218 date: "fa fa-calendar", 219 up: "fa fa-arrow-up", 220 down: "fa fa-arrow-down", 221 previous: "fa fa-arrow-' . (I18N::direction() === 'rtl' ? 'right' : 'left') . '", 222 next: "fa fa-arrow-' . (I18N::direction() === 'rtl' ? 'left' : 'right') . '", 223 today: "fa fa-trash-o", 224 clear: "fa fa-trash-o" 225 } 226 }); 227 '); 228 229$users_array = array(); 230foreach (User::all() as $tmp_user) { 231 $users_array[$tmp_user->getUserName()] = $tmp_user->getUserName(); 232} 233 234?> 235<ol class="breadcrumb small"> 236 <li><a href="admin.php"><?php echo I18N::translate('Control panel') ?></a></li> 237 <li class="active"><?php echo $controller->getPageTitle() ?></li> 238</ol> 239 240<h1><?php echo $controller->getPageTitle() ?></h1> 241 242<form class="form" name="logs"> 243 <input type="hidden" name="action" value="show"> 244 245 <div class="row"> 246 <div class="form-group col-xs-6 col-sm-3"> 247 <label for="from"> 248 <?php echo /* I18N: label for the start of a date range (from x to y) */ I18N::translate('From') ?> 249 </label> 250 <div class="input-group date"> 251 <input type="text" autocomplete="off" class="form-control" id="from" name="from" value="<?php echo Filter::escapeHtml($from) ?>" autocomplete="off"> 252 <span class="input-group-addon"><span class="fa fa-calendar"></span></span> 253 </div> 254 </div> 255 256 <div class="form-group col-xs-6 col-sm-3"> 257 <label for="to"> 258 <?php echo /* I18N: label for the end of a date range (from x to y) */ I18N::translate('To') ?> 259 </label> 260 <div class="input-group date"> 261 <input type="text" autocomplete="off" class="form-control" id="to" name="to" value="<?php echo Filter::escapeHtml($to) ?>" autocomplete="off"> 262 <span class="input-group-addon"><span class="fa fa-calendar"></span></span> 263 </div> 264 </div> 265 266 <div class="form-group col-xs-6 col-sm-2"> 267 <label for="type"> 268 <?php echo I18N::translate('Type') ?> 269 </label> 270 <?php echo FunctionsEdit::selectEditControl('type', array('' => '', 'auth' => 'auth', 'config' => 'config', 'debug' => 'debug', 'edit' => 'edit', 'error' => 'error', 'media' => 'media', 'search' => 'search'), null, $type, 'class="form-control"') ?> 271 </div> 272 273 <div class="form-group col-xs-6 col-sm-4"> 274 <label for="ip"> 275 <?php echo I18N::translate('IP address') ?> 276 </label> 277 <input class="form-control" type="text" id="ip" name="ip" value="<?php echo Filter::escapeHtml($ip) ?>"> 278 </div> 279 </div> 280 281 <div class="row"> 282 <div class="form-group col-sm-4"> 283 <label for="text"> 284 <?php echo I18N::translate('Message') ?> 285 </label> 286 <input class="form-control" type="text" id="text" name="text" value="<?php echo Filter::escapeHtml($text) ?>"> 287 </div> 288 289 <div class="form-group col-sm-4"> 290 <label for="user"> 291 <?php echo I18N::translate('User') ?> 292 </label> 293 <?php echo FunctionsEdit::selectEditControl('user', $users_array, '', $user, 'class="form-control"') ?> 294 </div> 295 296 <div class="form-group col-sm-4"> 297 <label for="gedc"> 298 <?php echo I18N::translate('Family tree') ?> 299 </label> 300 <?php echo FunctionsEdit::selectEditControl('gedc', Tree::getNameList(), '', $gedc, Auth::isAdmin() ? 'class="form-control"' : 'disabled class="form-control"') ?> 301 </div> 302 </div> 303 304 <div class="row text-center"> 305 <button type="submit" class="btn btn-primary"> 306 <?php echo /* I18N: A button label. */ I18N::translate('search') ?> 307 </button> 308 309 <button type="submit" class="btn btn-primary" onclick="document.logs.action.value='export';return true;" <?php echo $action === 'show' ? '' : 'disabled' ?>> 310 <?php echo /* I18N: A button label. */ I18N::translate('download') ?> 311 </button> 312 313 <button type="submit" class="btn btn-primary" onclick="if (confirm('<?php echo I18N::translate('Permanently delete these records?') ?>')) {document.logs.action.value='delete'; return true;} else {return false;}" <?php echo $action === 'show' ? '' : 'disabled' ?>> 314 <?php echo /* I18N: A button label. */ I18N::translate('delete') ?> 315 </button> 316 </div> 317</form> 318 319<?php if ($action): ?> 320<table class="table table-bordered table-condensed table-hover table-site-logs"> 321 <caption class="sr-only"> 322 <?php echo $controller->getPageTitle() ?> 323 </caption> 324 <thead> 325 <tr> 326 <th></th> 327 <th><?php echo I18N::translate('Timestamp') ?></th> 328 <th><?php echo I18N::translate('Type') ?></th> 329 <th><?php echo I18N::translate('Message') ?></th> 330 <th><?php echo I18N::translate('IP address') ?></th> 331 <th><?php echo I18N::translate('User') ?></th> 332 <th><?php echo I18N::translate('Family tree') ?></th> 333 </tr> 334 </thead> 335</table> 336<?php endif ?> 337