1<?php
2/**
3 * webtrees: online genealogy
4 * Copyright (C) 2021 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\Theme\AdministrationTheme;
20use PDOException;
21use Symfony\Component\HttpFoundation\Request;
22
23/**
24 * This is the bootstrap script, that is run on every request.
25 */
26
27if (version_compare(PHP_VERSION, '7.4.0') >= 0) {
28    echo '<!DOCTYPE html><html lang="en"><body>';
29    echo 'PHP ' . PHP_VERSION . ' detected.<br>';
30    echo 'webtrees 1.7 requires PHP 5.3 - 7.3.<br>';
31    echo 'Visit https://webtrees.net for more information.<br>';
32    echo '</body></html>';
33    exit;
34}
35
36// WT_SCRIPT_NAME is defined in each script that the user is permitted to load.
37if (!defined('WT_SCRIPT_NAME')) {
38    http_response_code(403);
39
40    return;
41}
42
43/**
44 * We set the following globals
45 *
46 * @global boolean $SEARCH_SPIDER
47 * @global Tree    $WT_TREE
48 */
49global $WT_TREE, $SEARCH_SPIDER;
50
51// Identify ourself
52define('WT_WEBTREES', 'webtrees');
53define('WT_VERSION', '1.7.18');
54
55// External URLs
56define('WT_WEBTREES_URL', 'https://www.webtrees.net/');
57define('WT_WEBTREES_WIKI', 'https://wiki.webtrees.net/');
58
59// Resources have version numbers in the URL, so that they can be cached indefinitely.
60define('WT_STATIC_URL', getenv('STATIC_URL')); // We could set this to load our own static resources from a cookie-free domain.
61
62if (getenv('USE_CDN')) {
63    // Caution, using a CDN will break support for responsive features in IE8, as respond.js
64    // needs to be on the same domain as all the CSS files.
65    define('WT_BOOTSTRAP_CSS_URL', '//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.6/css/bootstrap.min.css');
66    define('WT_BOOTSTRAP_DATETIMEPICKER_CSS_URL', '//cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/4.17.37/css/bootstrap-datetimepicker.min.css');
67    define('WT_BOOTSTRAP_DATETIMEPICKER_JS_URL', '//cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/4.17.37/js/bootstrap-datetimepicker.min.js');
68    define('WT_BOOTSTRAP_JS_URL', '//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.6/js/bootstrap.min.js');
69    define('WT_BOOTSTRAP_RTL_CSS_URL', '//cdnjs.cloudflare.com/ajax/libs/bootstrap-rtl/3.2.0-rc2/css/bootstrap-rtl.min.css'); // Cloudflare is out of date
70    //define('WT_DATATABLES_BOOTSTRAP_CSS_URL', '//cdn.datatables.net/plug-ins/1.10.7/integration/bootstrap/3/dataTables.bootstrap.css');
71    define('WT_DATATABLES_BOOTSTRAP_JS_URL', '//cdn.datatables.net/plug-ins/1.10.7/integration/bootstrap/3/dataTables.bootstrap.js');
72    define('WT_FONT_AWESOME_CSS_URL', '//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.4.0/css/font-awesome.min.css');
73    define('WT_JQUERYUI_JS_URL', '//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js');
74    define('WT_JQUERYUI_TOUCH_PUNCH_URL', '//cdnjs.cloudflare.com/ajax/libs/jqueryui-touch-punch/0.2.3/jquery.ui.touch-punch.min.js');
75    define('WT_JQUERY_DATATABLES_JS_URL', '//cdnjs.cloudflare.com/ajax/libs/datatables/1.10.7/js/jquery.dataTables.min.js');
76    define('WT_JQUERY_JS_URL', '//cdnjs.cloudflare.com/ajax/libs/jquery/1.12.1/jquery.min.js');
77    define('WT_JQUERY2_JS_URL', '//cdnjs.cloudflare.com/ajax/libs/jquery/2.2.1/jquery.min.js');
78    define('WT_MODERNIZR_JS_URL', '//cdnjs.cloudflare.com/ajax/libs/modernizr/2.8.3/modernizr.min.js');
79    define('WT_MOMENT_JS_URL', '//cdnjs.cloudflare.com/ajax/libs/moment.js/2.11.2/moment-with-locales.min.js');
80    define('WT_RESPOND_JS_URL', '//cdnjs.cloudflare.com/ajax/libs/respond.js/1.4.2/respond.min.js');
81} else {
82    define('WT_BOOTSTRAP_CSS_URL', WT_STATIC_URL . 'packages/bootstrap-3.3.6/css/bootstrap.min.css');
83    define('WT_BOOTSTRAP_DATETIMEPICKER_CSS_URL', WT_STATIC_URL . 'packages/bootstrap-datetimepicker-4.17.37/css/bootstrap-datetimepicker.min.css');
84    define('WT_BOOTSTRAP_DATETIMEPICKER_JS_URL', WT_STATIC_URL . 'packages/bootstrap-datetimepicker-4.17.37/js/bootstrap-datetimepicker.min.js');
85    define('WT_BOOTSTRAP_JS_URL', WT_STATIC_URL . 'packages/bootstrap-3.3.6/js/bootstrap.min.js');
86    define('WT_BOOTSTRAP_RTL_CSS_URL', WT_STATIC_URL . 'packages/bootstrap-rtl-3.3.4/css/bootstrap-rtl.min.css');
87    //define('WT_DATATABLES_BOOTSTRAP_CSS_URL', WT_STATIC_URL . 'packages/datatables-1.10.7/plugins/dataTables.bootstrap.css');
88    define('WT_DATATABLES_BOOTSTRAP_JS_URL', WT_STATIC_URL . 'packages/datatables-1.10.7/plugins/dataTables.bootstrap.js');
89    define('WT_FONT_AWESOME_CSS_URL', WT_STATIC_URL . 'packages/font-awesome-4.4.0/css/font-awesome.min.css');
90    define('WT_JQUERYUI_JS_URL', WT_STATIC_URL . 'packages/jquery-ui-1.11.4/js/jquery-ui.min.js');
91    define('WT_JQUERYUI_TOUCH_PUNCH_URL', WT_STATIC_URL . 'packages/jqueryui-touch-punch-0.2.3/jquery.ui.touch-punch.min.js');
92    define('WT_JQUERY_DATATABLES_JS_URL', WT_STATIC_URL . 'packages/datatables-1.10.7/js/jquery.dataTables.min.js');
93    define('WT_JQUERY_JS_URL', WT_STATIC_URL . 'packages/jquery-1.12.1/jquery.min.js');
94    define('WT_JQUERY2_JS_URL', WT_STATIC_URL . 'packages/jquery-2.2.1/jquery.min.js');
95    define('WT_MODERNIZR_JS_URL', WT_STATIC_URL . 'packages/modernizr-2.8.3/modernizr.min.js');
96    define('WT_MOMENT_JS_URL', WT_STATIC_URL . 'packages/moment-2.11.2/moment-with-locales.min.js');
97    define('WT_RESPOND_JS_URL', WT_STATIC_URL . 'packages/respond-1.4.2/respond.min.js');
98}
99
100// We can't load these from a CDN, as these have been patched.
101define('WT_JQUERY_COLORBOX_URL', WT_STATIC_URL . 'assets/js-1.7.9/jquery.colorbox-1.5.14.js');
102define('WT_JQUERY_WHEELZOOM_URL', WT_STATIC_URL . 'assets/js-1.7.9/jquery.wheelzoom-2.0.0.js');
103define('WT_CKEDITOR_BASE_URL', WT_STATIC_URL . 'packages/ckeditor-4.5.2-custom/');
104// See https://github.com/DataTables/Plugins/pull/178
105define('WT_DATATABLES_BOOTSTRAP_CSS_URL', WT_STATIC_URL . 'packages/datatables-1.10.7/plugins/dataTables.bootstrap-rtl.css');
106
107// Location of our own scripts
108define('WT_ADMIN_JS_URL', WT_STATIC_URL . 'assets/js-1.7.9/admin.js');
109define('WT_AUTOCOMPLETE_JS_URL', WT_STATIC_URL . 'assets/js-1.7.9/autocomplete.js');
110define('WT_WEBTREES_JS_URL', WT_STATIC_URL . 'assets/js-1.7.9/webtrees.js');
111define('WT_FONT_AWESOME_RTL_CSS_URL', WT_STATIC_URL . 'assets/js-1.7.9/font-awesome-rtl.css');
112
113// Location of our modules and themes. These are used as URLs and folder paths.
114define('WT_MODULES_DIR', 'modules_v3/'); // Update setup.php and build/Makefile when this changes
115define('WT_THEMES_DIR', 'themes/');
116
117// Enable debugging output on development builds
118define('WT_DEBUG', strpos(WT_VERSION, 'dev') !== false);
119define('WT_DEBUG_SQL', false);
120
121// Required version of database tables/columns/indexes/etc.
122define('WT_SCHEMA_VERSION', 37);
123
124// Regular expressions for validating user input, etc.
125define('WT_MINIMUM_PASSWORD_LENGTH', 6);
126define('WT_REGEX_XREF', '[A-Za-z0-9:_-]+');
127define('WT_REGEX_TAG', '[_A-Z][_A-Z0-9]*');
128define('WT_REGEX_INTEGER', '-?\d+');
129define('WT_REGEX_BYTES', '[0-9]+[bBkKmMgG]?');
130define('WT_REGEX_IPV4', '\d{1,3}(\.\d{1,3}){3}');
131define('WT_REGEX_PASSWORD', '.{' . WT_MINIMUM_PASSWORD_LENGTH . ',}');
132
133// UTF8 representation of various characters
134define('WT_UTF8_BOM', "\xEF\xBB\xBF"); // U+FEFF (Byte order mark)
135define('WT_UTF8_LRM', "\xE2\x80\x8E"); // U+200E (Left to Right mark:  zero-width character with LTR directionality)
136define('WT_UTF8_RLM', "\xE2\x80\x8F"); // U+200F (Right to Left mark:  zero-width character with RTL directionality)
137define('WT_UTF8_LRO', "\xE2\x80\xAD"); // U+202D (Left to Right override: force everything following to LTR mode)
138define('WT_UTF8_RLO', "\xE2\x80\xAE"); // U+202E (Right to Left override: force everything following to RTL mode)
139define('WT_UTF8_LRE', "\xE2\x80\xAA"); // U+202A (Left to Right embedding: treat everything following as LTR text)
140define('WT_UTF8_RLE', "\xE2\x80\xAB"); // U+202B (Right to Left embedding: treat everything following as RTL text)
141define('WT_UTF8_PDF', "\xE2\x80\xAC"); // U+202C (Pop directional formatting: restore state prior to last LRO, RLO, LRE, RLE)
142
143// Alternatives to BMD events for lists, charts, etc.
144define('WT_EVENTS_BIRT', 'BIRT|CHR|BAPM|_BRTM|ADOP');
145define('WT_EVENTS_DEAT', 'DEAT|BURI|CREM');
146define('WT_EVENTS_MARR', 'MARR|_NMR');
147define('WT_EVENTS_DIV', 'DIV|ANUL|_SEPR');
148
149// Use these line endings when writing files on the server
150define('WT_EOL', "\r\n");
151
152// Gedcom specification/definitions
153define('WT_GEDCOM_LINE_LENGTH', 255 - strlen(WT_EOL)); // Characters, not bytes
154
155// Used in Google charts
156define('WT_GOOGLE_CHART_ENCODING', 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.');
157
158// For performance, it is quicker to refer to files using absolute paths
159define('WT_ROOT', realpath(dirname(__DIR__)) . DIRECTORY_SEPARATOR);
160
161// Keep track of time statistics, for the summary in the footer
162define('WT_START_TIME', microtime(true));
163
164// We want to know about all PHP errors during development, and fewer in production.
165if (WT_DEBUG) {
166    error_reporting(E_ALL | E_STRICT | E_NOTICE | E_DEPRECATED);
167} else {
168    error_reporting(E_ALL);
169}
170
171require WT_ROOT . 'vendor/autoload.php';
172
173// PHP requires a time zone to be set. We'll set a better one later on.
174date_default_timezone_set('UTC');
175
176// Calculate the base URL, so we can generate absolute URLs.
177$request     = Request::createFromGlobals();
178$request_uri = $request->getSchemeAndHttpHost() . $request->getRequestUri();
179
180// Remove any PHP script name and parameters.
181$base_uri = preg_replace('/[^\/]+\.php(\?.*)?$/', '', $request_uri);
182define('WT_BASE_URL', $base_uri);
183
184// Convert PHP warnings/notices into exceptions
185set_error_handler(function ($errno, $errstr, $errfile, $errline) {
186    // Ignore errors that are silenced with '@'
187    if (error_reporting() & $errno) {
188        throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
189    }
190});
191
192set_exception_handler(function ($ex) {
193    $message = $ex->getFile() . ':' . $ex->getLine() . ' ' . $ex->getMessage() . PHP_EOL;
194
195    foreach ($ex->getTrace() as $level => $frame) {
196        $frame += array('args' => array(), 'file' => 'unknown', 'line' => 'unknown');
197        array_walk($frame['args'], function (&$arg) {
198            switch (gettype($arg)) {
199                case 'boolean':
200                case 'integer':
201                case 'double':
202                case 'null':
203                    $arg = var_export($arg, true);
204                    break;
205                case 'string':
206                    if (mb_strlen($arg) > 30) {
207                        $arg = substr($arg, 0, 30) . '…';
208                    }
209                    $arg = var_export($arg, true);
210                    break;
211                case 'object':
212                    $reflection = new \ReflectionClass($arg);
213                    if (is_object($arg) && method_exists($arg, '__toString')) {
214                        $arg = '[' . $reflection->getShortName() . ' ' . (string) $arg . ']';
215                    } else {
216                        $arg = '[' . $reflection->getShortName() . ']';
217                    }
218                    break;
219                default:
220                    $arg = '[' . gettype($arg) . ']';
221                    break;
222            }
223        });
224        $frame['file'] = str_replace(dirname(__DIR__), '', $frame['file']);
225        $message .= '#' . $level . ' ' . $frame['file'] . ':' . $frame['line'] . ' ';
226        if ($level) {
227            $message .= $frame['function'] . '(' . implode(', ', $frame['args']) . ')' . PHP_EOL;
228        } else {
229            $message .= get_class($ex) . '("' . $ex->getMessage() . '")' . PHP_EOL;
230        }
231    }
232
233    if ($ex instanceof PDOException || error_reporting() & $ex->getCode()) {
234        echo $message;
235    }
236
237    Log::addErrorLog($message);
238});
239
240// Load our configuration file, so we can connect to the database
241if (file_exists(WT_ROOT . 'data/config.ini.php')) {
242    $dbconfig = parse_ini_file(WT_ROOT . 'data/config.ini.php');
243    // Invalid/unreadable config file?
244    if (!is_array($dbconfig)) {
245        header('Location: ' . WT_BASE_URL . 'site-unavailable.php');
246        exit;
247    }
248    // Down for maintenance?
249    if (file_exists(WT_ROOT . 'data/offline.txt')) {
250        header('Location: ' . WT_BASE_URL . 'site-offline.php');
251        exit;
252    }
253} else {
254    // No config file. Set one up.
255    header('Location: ' . WT_BASE_URL . 'setup.php');
256    exit;
257}
258
259// What is the remote client's IP address
260if (Filter::server('HTTP_CLIENT_IP') !== null) {
261    define('WT_CLIENT_IP', Filter::server('HTTP_CLIENT_IP'));
262} elseif (Filter::server('HTTP_X_FORWARDED_FOR') !== null) {
263    define('WT_CLIENT_IP', Filter::server('HTTP_X_FORWARDED_FOR'));
264} else {
265    define('WT_CLIENT_IP', Filter::server('REMOTE_ADDR', WT_REGEX_IPV4, '127.0.0.1'));
266}
267
268// Connect to the database
269try {
270    Database::createInstance($dbconfig['dbhost'], $dbconfig['dbport'], $dbconfig['dbname'], $dbconfig['dbuser'], $dbconfig['dbpass']);
271    define('WT_TBLPREFIX', $dbconfig['tblpfx']);
272    unset($dbconfig);
273    // Some of the FAMILY JOIN HUSBAND JOIN WIFE queries can excede the MAX_JOIN_SIZE setting
274    Database::exec("SET NAMES 'utf8' COLLATE 'utf8_unicode_ci', SQL_BIG_SELECTS=1");
275    // Update the database schema
276    $updated = Database::updateSchema('\Fisharebest\Webtrees\Schema', 'WT_SCHEMA_VERSION', WT_SCHEMA_VERSION);
277    if ($updated) {
278        // updateSchema() might load custom modules - which we cannot load again.
279        header('Location: ' . WT_BASE_URL . WT_SCRIPT_NAME . (isset($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : ''));
280        exit;
281    }
282} catch (PDOException $ex) {
283    header('Location: ' . WT_BASE_URL . 'site-unavailable.php?message=' . rawurlencode($ex->getMessage()));
284    exit;
285}
286
287// The config.ini.php file must always be in a fixed location.
288// Other user files can be stored elsewhere...
289define('WT_DATA_DIR', realpath(Site::getPreference('INDEX_DIRECTORY') ? Site::getPreference('INDEX_DIRECTORY') : 'data') . DIRECTORY_SEPARATOR);
290
291// If we have a preferred URL (e.g. www.example.com instead of www.isp.com/~example), then redirect to it.
292$SERVER_URL = Site::getPreference('SERVER_URL');
293if ($SERVER_URL && $SERVER_URL != WT_BASE_URL) {
294    header('Location: ' . $SERVER_URL . WT_SCRIPT_NAME . (isset($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : ''), true, 301);
295    exit;
296}
297
298// Request more resources - if we can/want to
299if (!ini_get('safe_mode')) {
300    $memory_limit = Site::getPreference('MEMORY_LIMIT');
301    if ($memory_limit && strpos(ini_get('disable_functions'), 'ini_set') === false) {
302        ini_set('memory_limit', $memory_limit);
303    }
304    $max_execution_time = Site::getPreference('MAX_EXECUTION_TIME');
305    if ($max_execution_time && strpos(ini_get('disable_functions'), 'set_time_limit') === false) {
306        set_time_limit($max_execution_time);
307    }
308}
309
310$rule = Database::prepare(
311    "SELECT rule FROM `##site_access_rule`" .
312    " WHERE IFNULL(INET_ATON(?), 0) BETWEEN ip_address_start AND ip_address_end" .
313    " AND ? LIKE user_agent_pattern" .
314    " ORDER BY ip_address_end LIMIT 1"
315)->execute(array(WT_CLIENT_IP, Filter::server('HTTP_USER_AGENT', null, '')))->fetchOne();
316
317switch ($rule) {
318    case 'allow':
319        $SEARCH_SPIDER = false;
320        break;
321    case 'deny':
322        http_response_code(403);
323        exit;
324    case 'robot':
325    case 'unknown':
326        // Search engines don’t send cookies, and so create a new session with every visit.
327        // Make sure they always use the same one
328        Session::setId('search-engine-' . str_replace('.', '-', WT_CLIENT_IP));
329        $SEARCH_SPIDER = true;
330        break;
331    case '':
332        Database::prepare(
333        "INSERT INTO `##site_access_rule` (ip_address_start, ip_address_end, user_agent_pattern, comment) VALUES (IFNULL(INET_ATON(?), 0), IFNULL(INET_ATON(?), 4294967295), ?, '')"
334        )->execute(array(WT_CLIENT_IP, WT_CLIENT_IP, Filter::server('HTTP_USER_AGENT', null, '')));
335        $SEARCH_SPIDER = true;
336        break;
337}
338
339// Store our session data in the database.
340session_set_save_handler(
341    // open
342    function () {
343        return true;
344    },
345    // close
346    function () {
347        return true;
348    },
349    // read
350    function ($id) {
351        return (string) Database::prepare("SELECT session_data FROM `##session` WHERE session_id=?")->execute(array($id))->fetchOne();
352    },
353    // write
354    function ($id, $data) {
355        // Only update the session table once per minute, unless the session data has actually changed.
356        Database::prepare(
357            "INSERT INTO `##session` (session_id, user_id, ip_address, session_data, session_time)" .
358            " VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP - SECOND(CURRENT_TIMESTAMP))" .
359            " ON DUPLICATE KEY UPDATE" .
360            " user_id      = VALUES(user_id)," .
361            " ip_address   = VALUES(ip_address)," .
362            " session_data = VALUES(session_data)," .
363            " session_time = CURRENT_TIMESTAMP - SECOND(CURRENT_TIMESTAMP)"
364        )->execute(array($id, (int) Auth::id(), WT_CLIENT_IP, $data));
365
366        return true;
367    },
368    // destroy
369    function ($id) {
370        Database::prepare("DELETE FROM `##session` WHERE session_id=?")->execute(array($id));
371
372        return true;
373    },
374    // gc
375    function ($maxlifetime) {
376        Database::prepare("DELETE FROM `##session` WHERE session_time < DATE_SUB(NOW(), INTERVAL ? SECOND)")->execute(array($maxlifetime));
377
378        return true;
379    }
380);
381
382Session::start(array(
383    'gc_maxlifetime' => Site::getPreference('SESSION_TIME'),
384    'cookie_path'    => implode('/', array_map('rawurlencode', explode('/', parse_url(WT_BASE_URL, PHP_URL_PATH)))),
385));
386
387if (!Auth::isSearchEngine() && !Session::get('initiated')) {
388    // A new session, so prevent session fixation attacks by choosing a new PHPSESSID.
389    Session::regenerate(true);
390    Session::put('initiated', true);
391} else {
392    // An existing session
393}
394
395// Set the tree for the page; (1) the request, (2) the session, (3) the site default, (4) any tree
396foreach (array(Filter::post('ged'), Filter::get('ged'), Session::get('GEDCOM'), Site::getPreference('DEFAULT_GEDCOM')) as $tree_name) {
397    $WT_TREE = Tree::findByName($tree_name);
398    if ($WT_TREE) {
399        Session::put('GEDCOM', $tree_name);
400        break;
401    }
402}
403// No chosen tree? Use any one.
404if (!$WT_TREE) {
405    foreach (Tree::getAll() as $WT_TREE) {
406        break;
407    }
408}
409
410// With no parameters, init() looks to the environment to choose a language
411define('WT_LOCALE', I18N::init());
412Session::put('locale', WT_LOCALE);
413
414// Note that the database/webservers may not be synchronised, so use DB time throughout.
415define('WT_TIMESTAMP', (int) Database::prepare("SELECT UNIX_TIMESTAMP()")->fetchOne());
416
417// Users get their own time-zone. Visitors get the site time-zone.
418if (Auth::check()) {
419    date_default_timezone_set(Auth::user()->getPreference('TIMEZONE', 'UTC'));
420} else {
421    date_default_timezone_set(Site::getPreference('TIMEZONE') ?: 'UTC');
422}
423define('WT_TIMESTAMP_OFFSET', date_offset_get(new \DateTime('now')));
424
425define('WT_CLIENT_JD', 2440588 + (int) ((WT_TIMESTAMP + WT_TIMESTAMP_OFFSET) / 86400));
426
427// The login URL must be an absolute URL, and can be user-defined
428if (Site::getPreference('LOGIN_URL')) {
429    define('WT_LOGIN_URL', Site::getPreference('LOGIN_URL'));
430} else {
431    define('WT_LOGIN_URL', WT_BASE_URL . 'login.php');
432}
433
434// If there is no current tree and we need one, then redirect somewhere
435if (WT_SCRIPT_NAME != 'admin_trees_manage.php' && WT_SCRIPT_NAME != 'admin_pgv_to_wt.php' && WT_SCRIPT_NAME != 'login.php' && WT_SCRIPT_NAME != 'logout.php' && WT_SCRIPT_NAME != 'import.php' && WT_SCRIPT_NAME != 'help_text.php' && WT_SCRIPT_NAME != 'message.php' && WT_SCRIPT_NAME != 'action.php') {
436    if (!$WT_TREE || !$WT_TREE->getPreference('imported')) {
437        if (Auth::isAdmin()) {
438            header('Location: ' . WT_BASE_URL . 'admin_trees_manage.php');
439        } else {
440            // We're not an administrator, so we can only log in if there is a tree.
441            if (Auth::id()) {
442                Auth::logout();
443                FlashMessages::addMessage(
444                    I18N::translate('This user account does not have access to any tree.')
445                );
446            }
447            header('Location: ' . WT_LOGIN_URL . '?url=' . rawurlencode(WT_SCRIPT_NAME . (isset($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '')), true, 301);
448
449        }
450        exit;
451    }
452}
453
454// Update the last-login time no more than once a minute
455if (WT_TIMESTAMP - Session::get('activity_time') >= 60) {
456    if (Session::get('masquerade') === null) {
457        Auth::user()->setPreference('sessiontime', WT_TIMESTAMP);
458    }
459    Session::put('activity_time', WT_TIMESTAMP);
460}
461
462// Set the theme
463if (substr(WT_SCRIPT_NAME, 0, 5) === 'admin' || WT_SCRIPT_NAME === 'module.php' && substr(Filter::get('mod_action'), 0, 5) === 'admin') {
464    // Administration scripts begin with “admin” and use a special administration theme
465    Theme::theme(new AdministrationTheme)->init($WT_TREE);
466} else {
467    // Last theme used?
468    $theme_id = Session::get('theme_id');
469    // Default for tree
470    if (!array_key_exists($theme_id, Theme::themeNames()) && $WT_TREE) {
471        $theme_id = $WT_TREE->getPreference('THEME_DIR');
472    }
473    // Default for site
474    if (!array_key_exists($theme_id, Theme::themeNames())) {
475        $theme_id = Site::getPreference('THEME_DIR');
476    }
477    // Default
478    if (!array_key_exists($theme_id, Theme::themeNames())) {
479        $theme_id = 'webtrees';
480    }
481    foreach (Theme::installedThemes() as $theme) {
482        if ($theme->themeId() === $theme_id) {
483            Theme::theme($theme)->init($WT_TREE);
484            // Remember this setting
485            if (Site::getPreference('ALLOW_USER_THEMES')) {
486                Session::put('theme_id', $theme_id);
487            }
488            break;
489        }
490    }
491}
492
493// Search engines are only allowed to see certain pages.
494if (Auth::isSearchEngine() && !in_array(WT_SCRIPT_NAME, array(
495    'index.php', 'indilist.php', 'module.php', 'mediafirewall.php',
496    'individual.php', 'family.php', 'mediaviewer.php', 'note.php', 'repo.php', 'source.php',
497))) {
498    http_response_code(403);
499    $controller = new PageController;
500    $controller->setPageTitle(I18N::translate('Search engine'));
501    $controller->pageHeader();
502    echo '<p class="ui-state-error">', I18N::translate('You do not have permission to view this page.'), '</p>';
503    exit;
504}
505