xref: /webtrees/index.php (revision 616faa381faddbb480f46796af4d8844a1055fa1)
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', 39);
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$method  = $request->getMethod();
234$route   = $request->get('route');
235
236try {
237	// Most requests will need the current tree and user.
238	$all_trees = Tree::getAll();
239
240	$tree = $all_trees[$request->get('ged')] ?? null;
241
242	// No tree specified/available?  Choose one.
243	if ($tree === null && $method === 'GET') {
244		$tree = $all_trees[Site::getPreference('DEFAULT_GEDCOM')] ?? array_values($all_trees)[0] ?? null;
245	}
246
247	$request->attributes->set('tree', $tree);
248	$request->attributes->set('user', Auth::user());
249
250	// Most layouts will require a tree for the page header/footer
251	View::share('tree', $tree);
252
253	// Load the routing table.
254	$routes = require 'routes/web.php';
255
256	// Find the action for the selected route
257	$controller_action = $routes[$method . ':' . $route] ?? 'ErrorController@noRouteFound';
258
259	// Create the controller
260	list($controller_name, $action) = explode('@', $controller_action);
261	$controller_class = __NAMESPACE__ . '\\Http\\Controllers\\' . $controller_name;
262	$controller       = new $controller_class;
263
264	DebugBar::stopMeasure('routing');
265
266	DebugBar::startMeasure('init theme');
267
268	// Last theme used?
269	$theme_id = Session::get('theme_id');
270	// Default for tree
271	if (!array_key_exists($theme_id, Theme::themeNames()) && $tree) {
272		$theme_id = $tree->getPreference('THEME_DIR');
273	}
274	// Default for site
275	if (!array_key_exists($theme_id, Theme::themeNames())) {
276		$theme_id = Site::getPreference('THEME_DIR');
277	}
278	// Default
279	if (!array_key_exists($theme_id, Theme::themeNames())) {
280		$theme_id = 'webtrees';
281	}
282	foreach (Theme::installedThemes() as $theme) {
283		if ($theme->themeId() === $theme_id) {
284			Theme::theme($theme)->init($tree);
285			// Remember this setting
286			if (Site::getPreference('ALLOW_USER_THEMES') === '1') {
287				Session::put('theme_id', $theme_id);
288			}
289			break;
290		}
291	}
292
293	DebugBar::stopMeasure('init theme');
294
295	// Note that we can't stop this timer, as running the action will
296	// generate the response - which includes (and stops) the timer
297	DebugBar::startMeasure('controller_action', $controller_action);
298
299	$middleware_stack = [
300		new CheckForMaintenanceMode,
301	];
302
303	if ($method === 'GET') {
304		$middleware_stack[] = new PageHitCounter;
305	}
306
307	if ($method === 'POST') {
308		$middleware_stack[] = new UseTransaction;
309		$middleware_stack[] = new CheckCsrf;
310	}
311
312	// Apply the middleware using the "onion" pattern.
313	$pipeline = array_reduce($middleware_stack, function (Closure $next, MiddlewareInterface $middleware): Closure {
314		// Create a closure to apply the middleware.
315		return function (Request $request) use ($middleware, $next): Response {
316			return $middleware->handle($request, $next);
317		};
318	}, function (Request $request) use ($controller, $action): Response {
319		// Create a closure to generate the response.
320		return call_user_func([$controller, $action], $request);
321	});
322
323	$response = call_user_func($pipeline, $request);
324} catch (Exception $exception) {
325	DebugBar::addThrowable($exception);
326
327	$response = (new Handler)->render($request, $exception);
328}
329
330// Send response
331if ($response instanceof RedirectResponse) {
332	// Show the debug data on the next page
333	DebugBar::stackData();
334} elseif ($response instanceof JsonResponse) {
335	// Use HTTP headers and some jQuery to add debug to the current page.
336	DebugBar::sendDataInHeaders();
337}
338
339$response->prepare($request)->send();
340