xref: /webtrees/index.php (revision d69397b0fb0a7e71220076299d45dc2ac034b754)
1<?php
2/**
3 * webtrees: online genealogy
4 * Copyright (C) 2018 webtrees development team
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 */
16declare(strict_types=1);
17
18namespace Fisharebest\Webtrees;
19
20use Closure;
21use DateTime;
22use ErrorException;
23use Exception;
24use Fisharebest\Webtrees\Exceptions\Handler;
25use Fisharebest\Webtrees\Http\Middleware\CheckCsrf;
26use Fisharebest\Webtrees\Http\Middleware\CheckForMaintenanceMode;
27use Fisharebest\Webtrees\Http\Middleware\MiddlewareInterface;
28use Fisharebest\Webtrees\Http\Middleware\PageHitCounter;
29use Fisharebest\Webtrees\Http\Middleware\UseTransaction;
30use League\Flysystem\Adapter\Local;
31use League\Flysystem\Filesystem;
32use PDOException;
33use Throwable;
34use Symfony\Component\HttpFoundation\JsonResponse;
35use Symfony\Component\HttpFoundation\RedirectResponse;
36use Symfony\Component\HttpFoundation\Request;
37use Symfony\Component\HttpFoundation\Response;
38
39// Identify ourself
40define('WT_WEBTREES', 'webtrees');
41define('WT_VERSION', '2.0.0-dev');
42define('WT_WEBTREES_URL', 'https://www.webtrees.net/');
43
44// Location of our modules and themes. These are used as URLs and folder paths.
45define('WT_MODULES_DIR', 'modules_v3/');
46define('WT_THEMES_DIR', 'themes/');
47define('WT_ASSETS_URL', 'public/assets-2.0.0/'); // See also webpack.mix.js
48define('WT_CKEDITOR_BASE_URL', 'public/ckeditor-4.5.2-custom/');
49
50// Enable debugging output on development builds
51define('WT_DEBUG', strpos(WT_VERSION, 'dev') !== false);
52
53// Required version of database tables/columns/indexes/etc.
54define('WT_SCHEMA_VERSION', 40);
55
56// Regular expressions for validating user input, etc.
57define('WT_MINIMUM_PASSWORD_LENGTH', 6);
58define('WT_REGEX_XREF', '[A-Za-z0-9:_-]+');
59define('WT_REGEX_TAG', '[_A-Z][_A-Z0-9]*');
60define('WT_REGEX_INTEGER', '-?\d+');
61define('WT_REGEX_BYTES', '[0-9]+[bBkKmMgG]?');
62define('WT_REGEX_PASSWORD', '.{' . WT_MINIMUM_PASSWORD_LENGTH . ',}');
63
64define('WT_UTF8_BOM', "\xEF\xBB\xBF"); // U+FEFF (Byte order mark)
65
66// Alternatives to BMD events for lists, charts, etc.
67define('WT_EVENTS_BIRT', 'BIRT|CHR|BAPM|_BRTM|ADOP');
68define('WT_EVENTS_DEAT', 'DEAT|BURI|CREM');
69define('WT_EVENTS_MARR', 'MARR|_NMR');
70define('WT_EVENTS_DIV', 'DIV|ANUL|_SEPR');
71
72// For performance, it is quicker to refer to files using absolute paths
73define('WT_ROOT', realpath(__DIR__) . DIRECTORY_SEPARATOR);
74
75// Keep track of time so we can handle timeouts gracefully.
76define('WT_START_TIME', microtime(true));
77
78// We want to know about all PHP errors during development, and fewer in production.
79if (WT_DEBUG) {
80    error_reporting(E_ALL | E_STRICT | E_NOTICE | E_DEPRECATED);
81} else {
82    error_reporting(E_ALL);
83}
84
85require WT_ROOT . 'vendor/autoload.php';
86
87// Initialise the DebugBar for development.
88// Use `composer install --dev` on a development build to enable.
89// Note that you may need to increase the size of the fcgi buffers on nginx.
90// e.g. add these lines to your fastcgi_params file:
91// fastcgi_buffers 16 16m;
92// fastcgi_buffer_size 32m;
93DebugBar::init(WT_DEBUG && class_exists('\\DebugBar\\StandardDebugBar'));
94
95// PHP requires a time zone to be set. We'll set a better one later on.
96date_default_timezone_set('UTC');
97
98// Calculate the base URL, so we can generate absolute URLs.
99$request     = Request::createFromGlobals();
100$request_uri = $request->getSchemeAndHttpHost() . $request->getRequestUri();
101
102// Remove any PHP script name and parameters.
103$base_uri = preg_replace('/[^\/]+\.php(\?.*)?$/', '', $request_uri);
104define('WT_BASE_URL', $base_uri);
105
106// Convert PHP warnings/notices into exceptions
107set_error_handler(function ($errno, $errstr, $errfile, $errline) {
108    // Ignore errors that are silenced with '@'
109    if (error_reporting() & $errno) {
110        throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
111    }
112});
113
114DebugBar::startMeasure('init database');
115
116// Load our configuration file, so we can connect to the database
117if (!file_exists(WT_ROOT . 'data/config.ini.php')) {
118    // No config file. Set one up.
119    $url      = Html::url('setup.php', ['route' => 'setup']);
120    $response = new RedirectResponse($url);
121    $response->send();
122    return;
123}
124
125// Connect to the database
126try {
127    // Read the connection settings and create the database
128    Database::createInstance(parse_ini_file(WT_ROOT . 'data/config.ini.php'));
129
130    // Update the database schema, if necessary.
131    Database::updateSchema('\Fisharebest\Webtrees\Schema', 'WT_SCHEMA_VERSION', WT_SCHEMA_VERSION);
132} catch (PDOException $ex) {
133    DebugBar::addThrowable($ex);
134
135    define('WT_DATA_DIR', 'data/');
136    I18N::init();
137    if ($ex->getCode() === 1045) {
138        // Error during connection?
139        $content = view('errors/database-connection', ['error' => $ex->getMessage()]);
140    } else {
141        // Error in a migration script?
142        $content = view('errors/database-error', ['error' => $ex->getMessage()]);
143    }
144    $html     = view('layouts/error', ['content' => $content]);
145    $response = new Response($html, 503);
146    $response->prepare($request)->send();
147    return;
148} catch (Throwable $ex) {
149    DebugBar::addThrowable($ex);
150
151    define('WT_DATA_DIR', 'data/');
152    I18N::init();
153    $content  = view('errors/database-connection', ['error' => $ex->getMessage()]);
154    $html     = view('layouts/error', ['content' => $content]);
155    $response = new Response($html, 503);
156    $response->prepare($request)->send();
157    return;
158}
159
160DebugBar::stopMeasure('init database');
161
162// The config.ini.php file must always be in a fixed location.
163// Other user files can be stored elsewhere...
164define('WT_DATA_DIR', realpath(Site::getPreference('INDEX_DIRECTORY', 'data/')) . DIRECTORY_SEPARATOR);
165
166// Some broken servers block access to their own temp folder using open_basedir...
167$data_dir = new Filesystem(new Local(WT_DATA_DIR));
168$data_dir->createDir('tmp');
169putenv('TMPDIR=' . WT_DATA_DIR . 'tmp');
170
171// Request more resources - if we can/want to
172$memory_limit = Site::getPreference('MEMORY_LIMIT');
173if ($memory_limit !== '' && strpos(ini_get('disable_functions'), 'ini_set') === false) {
174    ini_set('memory_limit', $memory_limit);
175}
176$max_execution_time = Site::getPreference('MAX_EXECUTION_TIME');
177if ($max_execution_time !== '' && strpos(ini_get('disable_functions'), 'set_time_limit') === false) {
178    set_time_limit((int) $max_execution_time);
179}
180
181// Sessions
182Session::start();
183
184DebugBar::startMeasure('init i18n');
185
186// With no parameters, init() looks to the environment to choose a language
187define('WT_LOCALE', I18N::init());
188Session::put('locale', WT_LOCALE);
189
190DebugBar::stopMeasure('init i18n');
191
192// Note that the database/webservers may not be synchronised, so use DB time throughout.
193define('WT_TIMESTAMP', (int) Database::prepare("SELECT UNIX_TIMESTAMP()")->fetchOne());
194
195// Users get their own time-zone. Visitors get the site time-zone.
196try {
197    if (Auth::check()) {
198        date_default_timezone_set(Auth::user()->getPreference('TIMEZONE'));
199    } else {
200        date_default_timezone_set(Site::getPreference('TIMEZONE'));
201    }
202} catch (ErrorException $ex) {
203    // Server upgrades and migrations can leave us with invalid timezone settings.
204    date_default_timezone_set('UTC');
205}
206
207define('WT_TIMESTAMP_OFFSET', date_offset_get(new DateTime('now')));
208
209define('WT_CLIENT_JD', 2440588 + (int) ((WT_TIMESTAMP + WT_TIMESTAMP_OFFSET) / 86400));
210
211// Update the last-login time no more than once a minute
212if (WT_TIMESTAMP - Session::get('activity_time') >= 60) {
213    if (Session::get('masquerade') === null) {
214        Auth::user()->setPreference('sessiontime', WT_TIMESTAMP);
215    }
216    Session::put('activity_time', WT_TIMESTAMP);
217}
218
219DebugBar::startMeasure('routing');
220
221// The HTTP request.
222$request = Request::createFromGlobals();
223$route   = $request->get('route');
224
225try {
226    // Most requests will need the current tree and user.
227    $all_trees = Tree::getAll();
228
229    $tree = $all_trees[$request->get('ged')] ?? null;
230
231    // No tree specified/available?  Choose one.
232    if ($tree === null && $request->getMethod() === Request::METHOD_GET) {
233        $tree = $all_trees[Site::getPreference('DEFAULT_GEDCOM')] ?? array_values($all_trees)[0] ?? null;
234    }
235
236    $request->attributes->set('tree', $tree);
237    $request->attributes->set('user', Auth::user());
238
239    // Most layouts will require a tree for the page header/footer
240    View::share('tree', $tree);
241
242    // Load the routing table.
243    $routes = require 'routes/web.php';
244
245    // Find the action for the selected route
246    $controller_action = $routes[$request->getMethod() . ':' . $route] ?? 'ErrorController@noRouteFound';
247
248    // Create the controller
249    list($controller_name, $action) = explode('@', $controller_action);
250    $controller_class = __NAMESPACE__ . '\\Http\\Controllers\\' . $controller_name;
251    $controller       = new $controller_class;
252
253    DebugBar::stopMeasure('routing');
254
255    DebugBar::startMeasure('init theme');
256
257    // Last theme used?
258    $theme_id = Session::get('theme_id');
259    // Default for tree
260    if (!array_key_exists($theme_id, Theme::themeNames()) && $tree) {
261        $theme_id = $tree->getPreference('THEME_DIR');
262    }
263    // Default for site
264    if (!array_key_exists($theme_id, Theme::themeNames())) {
265        $theme_id = Site::getPreference('THEME_DIR');
266    }
267    // Default
268    if (!array_key_exists($theme_id, Theme::themeNames())) {
269        $theme_id = 'webtrees';
270    }
271    foreach (Theme::installedThemes() as $theme) {
272        if ($theme->themeId() === $theme_id) {
273            Theme::theme($theme)->init($tree);
274            // Remember this setting
275            if (Site::getPreference('ALLOW_USER_THEMES') === '1') {
276                Session::put('theme_id', $theme_id);
277            }
278            break;
279        }
280    }
281
282    DebugBar::stopMeasure('init theme');
283
284    // Note that we can't stop this timer, as running the action will
285    // generate the response - which includes (and stops) the timer
286    DebugBar::startMeasure('controller_action', $controller_action);
287
288    $middleware_stack = [
289        new CheckForMaintenanceMode,
290    ];
291
292    if ($request->getMethod() === Request::METHOD_GET) {
293        $middleware_stack[] = new PageHitCounter;
294    }
295
296    if ($request->getMethod() === Request::METHOD_POST) {
297        $middleware_stack[] = new UseTransaction;
298        $middleware_stack[] = new CheckCsrf;
299    }
300
301    // Apply the middleware using the "onion" pattern.
302    $pipeline = array_reduce($middleware_stack, function (Closure $next, MiddlewareInterface $middleware): Closure {
303        // Create a closure to apply the middleware.
304        return function (Request $request) use ($middleware, $next): Response {
305            return $middleware->handle($request, $next);
306        };
307    }, function (Request $request) use ($controller, $action): Response {
308        // Create a closure to generate the response.
309        return call_user_func([$controller, $action], $request);
310    });
311
312    $response = call_user_func($pipeline, $request);
313} catch (Exception $exception) {
314    DebugBar::addThrowable($exception);
315
316    $response = (new Handler)->render($request, $exception);
317}
318
319// Send response
320if ($response instanceof RedirectResponse) {
321    // Show the debug data on the next page
322    DebugBar::stackData();
323} elseif ($response instanceof JsonResponse) {
324    // Use HTTP headers and some jQuery to add debug to the current page.
325    DebugBar::sendDataInHeaders();
326}
327
328$response->prepare($request)->send();
329