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