xref: /webtrees/index.php (revision b295534922aa0d15bf8b3821f74784a5ff810d62)
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::setSaveHandler();
183Session::start([
184	'gc_maxlifetime' => Site::getPreference('SESSION_TIME'),
185	'cookie_path'    => implode('/', array_map('rawurlencode', explode('/', parse_url(WT_BASE_URL, PHP_URL_PATH)))),
186]);
187
188// A new session, so prevent session fixation attacks by choosing a new PHPSESSID.
189if (!Session::get('initiated')) {
190	Session::regenerate(true);
191	Session::put('initiated', true);
192}
193
194DebugBar::startMeasure('init i18n');
195
196// With no parameters, init() looks to the environment to choose a language
197define('WT_LOCALE', I18N::init());
198Session::put('locale', WT_LOCALE);
199
200DebugBar::stopMeasure('init i18n');
201
202// Note that the database/webservers may not be synchronised, so use DB time throughout.
203define('WT_TIMESTAMP', (int) Database::prepare("SELECT UNIX_TIMESTAMP()")->fetchOne());
204
205// Users get their own time-zone. Visitors get the site time-zone.
206try {
207	if (Auth::check()) {
208		date_default_timezone_set(Auth::user()->getPreference('TIMEZONE'));
209	} else {
210		date_default_timezone_set(Site::getPreference('TIMEZONE'));
211	}
212} catch (ErrorException $ex) {
213	// Server upgrades and migrations can leave us with invalid timezone settings.
214	date_default_timezone_set('UTC');
215}
216
217define('WT_TIMESTAMP_OFFSET', date_offset_get(new DateTime('now')));
218
219define('WT_CLIENT_JD', 2440588 + (int) ((WT_TIMESTAMP + WT_TIMESTAMP_OFFSET) / 86400));
220
221// Update the last-login time no more than once a minute
222if (WT_TIMESTAMP - Session::get('activity_time') >= 60) {
223	if (Session::get('masquerade') === null) {
224		Auth::user()->setPreference('sessiontime', WT_TIMESTAMP);
225	}
226	Session::put('activity_time', WT_TIMESTAMP);
227}
228
229DebugBar::startMeasure('routing');
230
231// The HTTP request.
232$request = Request::createFromGlobals();
233$route   = $request->get('route');
234
235try {
236	// Most requests will need the current tree and user.
237	$all_trees = Tree::getAll();
238
239	$tree = $all_trees[$request->get('ged')] ?? null;
240
241	// No tree specified/available?  Choose one.
242	if ($tree === null && $request->getMethod() === Request::METHOD_GET) {
243		$tree = $all_trees[Site::getPreference('DEFAULT_GEDCOM')] ?? array_values($all_trees)[0] ?? null;
244	}
245
246	$request->attributes->set('tree', $tree);
247	$request->attributes->set('user', Auth::user());
248
249	// Most layouts will require a tree for the page header/footer
250	View::share('tree', $tree);
251
252	// Load the routing table.
253	$routes = require 'routes/web.php';
254
255	// Find the action for the selected route
256	$controller_action = $routes[$request->getMethod() . ':' . $route] ?? 'ErrorController@noRouteFound';
257
258	// Create the controller
259	list($controller_name, $action) = explode('@', $controller_action);
260	$controller_class = __NAMESPACE__ . '\\Http\\Controllers\\' . $controller_name;
261	$controller       = new $controller_class;
262
263	DebugBar::stopMeasure('routing');
264
265	DebugBar::startMeasure('init theme');
266
267	// Last theme used?
268	$theme_id = Session::get('theme_id');
269	// Default for tree
270	if (!array_key_exists($theme_id, Theme::themeNames()) && $tree) {
271		$theme_id = $tree->getPreference('THEME_DIR');
272	}
273	// Default for site
274	if (!array_key_exists($theme_id, Theme::themeNames())) {
275		$theme_id = Site::getPreference('THEME_DIR');
276	}
277	// Default
278	if (!array_key_exists($theme_id, Theme::themeNames())) {
279		$theme_id = 'webtrees';
280	}
281	foreach (Theme::installedThemes() as $theme) {
282		if ($theme->themeId() === $theme_id) {
283			Theme::theme($theme)->init($tree);
284			// Remember this setting
285			if (Site::getPreference('ALLOW_USER_THEMES') === '1') {
286				Session::put('theme_id', $theme_id);
287			}
288			break;
289		}
290	}
291
292	DebugBar::stopMeasure('init theme');
293
294	// Note that we can't stop this timer, as running the action will
295	// generate the response - which includes (and stops) the timer
296	DebugBar::startMeasure('controller_action', $controller_action);
297
298	$middleware_stack = [
299		new CheckForMaintenanceMode,
300	];
301
302	if ($request->getMethod() === Request::METHOD_GET) {
303		$middleware_stack[] = new PageHitCounter;
304	}
305
306	if ($request->getMethod() === Request::METHOD_POST) {
307		$middleware_stack[] = new UseTransaction;
308		$middleware_stack[] = new CheckCsrf;
309	}
310
311	// Apply the middleware using the "onion" pattern.
312	$pipeline = array_reduce($middleware_stack, function (Closure $next, MiddlewareInterface $middleware): Closure {
313		// Create a closure to apply the middleware.
314		return function (Request $request) use ($middleware, $next): Response {
315			return $middleware->handle($request, $next);
316		};
317	}, function (Request $request) use ($controller, $action): Response {
318		// Create a closure to generate the response.
319		return call_user_func([$controller, $action], $request);
320	});
321
322	$response = call_user_func($pipeline, $request);
323} catch (Exception $exception) {
324	DebugBar::addThrowable($exception);
325
326	$response = (new Handler)->render($request, $exception);
327}
328
329// Send response
330if ($response instanceof RedirectResponse) {
331	// Show the debug data on the next page
332	DebugBar::stackData();
333} elseif ($response instanceof JsonResponse) {
334	// Use HTTP headers and some jQuery to add debug to the current page.
335	DebugBar::sendDataInHeaders();
336}
337
338$response->prepare($request)->send();
339