This function assumes the input file layout is
level followed by a variable number of placename fields
followed by Longitude, Latitude, Zoom & Icon

@param ServerRequestInterface $request

@return ResponseInterface
@throws Exception
430982
public function importLocationsAction(ServerRequestInterface $request): ResponseInterface
    {
        $serverfile     = $request->getParsedBody()['serverfile'] ?? '';
        $options        = $request->getParsedBody()['import-options'] ?? '';
        $clear_database = (bool) ($request->getParsedBody()['cleardatabase'] ?? false);
        $local_file     = $request->getUploadedFiles()['localfile'] ?? null;

        $filename    = '';
        $places      = [];
        $field_names = [
            'pl_level',
            'pl_long',
            'pl_lati',
            'pl_zoom',
            'pl_icon',
            'fqpn',
        ];

        $url = route('map-data', ['parent_id' => 0]);

        if ($serverfile !== '' && is_dir(WT_DATA_DIR . 'places')) {
            // first choice is file on server
            $fp = fopen(WT_DATA_DIR . 'places/' . $serverfile, 'rb+');
        } elseif ($local_file instanceof UploadedFileInterface && $local_file->getError() === UPLOAD_ERR_OK) {
            // 2nd choice is local file
            $fp = $local_file->getStream()->detach();
        } else {
            return redirect($url);
        }

        if ($fp !== false) {
            $string = stream_get_contents($fp);

            // Check the filetype
            if (stripos($string, 'FeatureCollection') !== false) {
                $input_array = json_decode($string, false);

                foreach ($input_array->features as $feature) {
                    $places[] = array_combine($field_names, [
                        $feature->properties->level ?? substr_count($feature->properties->name, ','),
                        $this->gedcom_service->writeLongitude($feature->geometry->coordinates[0]),
                        $this->gedcom_service->writeLatitude($feature->geometry->coordinates[1]),
                        $feature->properties->zoom ?? null,
                        $feature->properties->icon ?? null,
                        $feature->properties->name,
                    ]);
                }
            } else {
                rewind($fp);
                while (($row = fgetcsv($fp, 0, ';')) !== false) {
                    // Skip the header
                    if (!is_numeric($row[0])) {
                        continue;
                    }

                    $level = (int) $row[0];
                    $count = count($row);

                    // convert separate place fields into a comma separated placename
                    $fqdn = implode(Gedcom::PLACE_SEPARATOR, array_reverse(array_slice($row, 1, 1 + $level)));

                    $places[] = [
                        'pl_level' => $level,
                        'pl_long'  => $row[$count - 4],
                        'pl_lati'  => $row[$count - 3],
                        'pl_zoom'  => $row[$count - 2],
                        'pl_icon'  => $row[$count - 1],
                        'fqpn'     => $fqdn,
                    ];
                }
            }

            fclose($fp);

            if ($clear_database) {
                DB::table('placelocation')->delete();
            }

            //process places
            $added   = 0;
            $updated = 0;

            foreach ($places as $place) {
                $location = new Location($place['fqpn']);
                $exists   = $location->exists();

                if ($options === 'update' && !$exists) {
                    continue;
                }

                if (!$exists) {
                    $added++;
                }

                if (!$exists || $options === 'update') {
                    DB::table('placelocation')
                        ->where('pl_id', '=', $location->id())
                        ->update([
                            'pl_lati' => $place['pl_lati'],
                            'pl_long' => $place['pl_long'],
                            'pl_zoom' => $place['pl_zoom'] ? $place['pl_zoom'] : null,
                            'pl_icon' => $place['pl_icon'] ? $place['pl_icon'] : null,
                        ]);

                    $updated++;
                }
            }
            FlashMessages::addMessage(
                I18N::translate('locations updated: %s, locations added: %s', I18N::number($updated), I18N::number($added)),
                $added + $updated === 0 ? 'info' : 'success'
            );
        } else {
            throw new Exception('Unable to open file: ' . $filename);
        }

        return redirect($url);
    }
883468
public function badRequest(BadRequestException $exception, ServerRequestInterface $request) : ResponseInterface
    {
        $this->exception = $exception;

        $acceptType = $request->getHeader('Accept');
        if (is_array($acceptType) && count($acceptType) > 0 && strpos($acceptType[0], 'json') !== false) {
            return new JsonResponse(['error' => ['message' => 'Bad request sent', 'type' => 'bad_request']], 400);
        }

        if ($this->contentFor400) {
            $this->contentBlock = $this->contentFor400;
        } else {
            $this->contentBlock->addFile(__DIR__ . '/../../../../views/400.php', $this);
        }

        return HtmlResponse::create($this->template, 400);
    }
463364
private function searchForMatchingExpectation(ServerRequestInterface $request)
    {
        $lastFound = null;
        foreach ($this->expectationsStorage->listExpectations() as $expectation) {
            $lastFound = $this->getNextMatchingExpectation($lastFound, $request, $expectation);
        }

        return $lastFound;
    }
1359528
protected function getContentType(ServerRequestInterface $request)
    {
        // Check for an attribute
        if (null !== $accept = $request->getAttribute('Content-Type', null)) {
            return $accept;
        }

        // Check for an accept header
        if ($request->hasHeader('Content-Type')) {
            return $request->getHeaderLine('Content-Type');
        }

        return null;
    }
1310762
private function determineContentType(ServerRequestInterface $request)
    {
        $acceptHeader = $request->getHeaderLine('Accept');
        $selectedContentTypes = array_intersect(explode(',', $acceptHeader), $this->knownContentTypes);
        if (count($selectedContentTypes)) {
            return reset($selectedContentTypes);
        }

        return 'text/html';
    }
845356
private function handleRequest(ServerRequestInterface $request) : ResponseInterface
    {
        $this->eventDispatcher->dispatch(new EventBeforeAppHandlingRequest($request));
        
        $routerMatch = $this->matchRoute($request);
        $stack = $this->getFullMiddlewareStack($request, $routerMatch);
        
        $this->eventDispatcher->dispatch(new EventBeforeAppMiddlewareFullStackDispatching($stack));
        $this->middlewareDispatcher->init($stack);
        $response = $this->middlewareDispatcher->process($request);
        $this->eventDispatcher->dispatch(new EventAfterAppMiddlewareFullStackDispatching($response));
        
        $this->eventDispatcher->dispatch(new EventAfterAppHandlingRequest($response));
        return $response;
    }
495788
public function webtrees1ThumbnailsData(ServerRequestInterface $request): ResponseInterface
    {
        $start  = (int) $request->getQueryParams()['start'];
        $length = (int) $request->getQueryParams()['length'];
        $search = $request->getQueryParams()['search']['value'];

        // Fetch all thumbnails
        $thumbnails = [];

        $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(WT_DATA_DIR, FilesystemIterator::FOLLOW_SYMLINKS));

        foreach ($iterator as $iteration) {
            if ($iteration->isFile() && strpos($iteration->getPathname(), '/thumbs/') !== false) {
                $thumbnails[] = $iteration->getPathname();
            }
        }

        $recordsTotal = count($thumbnails);

        if ($search !== '') {
            $thumbnails = array_filter($thumbnails, static function (string $thumbnail) use ($search): bool {
                return stripos($thumbnail, $search) !== false;
            });
        }

        $recordsFiltered = count($thumbnails);

        $thumbnails = array_slice($thumbnails, $start, $length);

        // Turn each filename into a row for the table
        $data = array_map(function (string $thumbnail): array {
            $original = $this->findOriginalFileFromThumbnail($thumbnail);

            $original_url  = route('unused-media-thumbnail', [
                'folder' => dirname($original),
                'file'   => basename($original),
                'w'      => 100,
                'h'      => 100,
            ]);
            $thumbnail_url = route('unused-media-thumbnail', [
                'folder' => dirname($thumbnail),
                'file'   => basename($thumbnail),
                'w'      => 100,
                'h'      => 100,
            ]);

            $difference = $this->imageDiff($thumbnail, $original);

            $original_path  = substr($original, strlen(WT_DATA_DIR));
            $thumbnail_path = substr($thumbnail, strlen(WT_DATA_DIR));

            $media = $this->findMediaObjectsForMediaFile($original_path);

            $media_links = array_map(static function (Media $media): string {
                return '<a href="' . e($media->url()) . '">' . $media->fullName() . '</a>';
            }, $media);

            $media_links = implode('<br>', $media_links);

            $action = view('admin/webtrees1-thumbnails-form', [
                'difference' => $difference,
                'media'      => $media,
                'thumbnail'  => $thumbnail_path,
            ]);

            return [
                '<img src="' . e($thumbnail_url) . '" title="' . e($thumbnail_path) . '">',
                '<img src="' . e($original_url) . '" title="' . e($original_path) . '">',
                $media_links,
                I18N::percentage($difference / 100.0, 0),
                $action,
            ];
        }, $thumbnails);

        return response([
            'draw'            => (int) $request->getQueryParams()['draw'],
            'recordsTotal'    => $recordsTotal,
            'recordsFiltered' => $recordsFiltered,
            'data'            => $data,
        ]);
    }
1104085
public function handle(ServerRequestInterface $request): ResponseInterface
    {
        $this->request = $request;
        $response = (new HttpFactory())->createResponse();
        $response = $response->withHeader('Content-Type', 'text/html');
        $pathInfo = RequestHelper::getPathInfo($request);

        try {
            $controller = $this->resolveController($pathInfo);
            $mixed = $this->executeAction($controller);
        } catch (\Throwable $t) {
            // handle errors with built-in error controller
            $controller = new ErrorController($this->view, $request, 'error/error');
            $controller->setError($t);
            $mixed = $this->executeAction($controller);
            $response = $response->withStatus($t->getCode());
        }

        if (is_array($mixed)) {
            $response = $response->withHeader('Content-Type', 'application/json');
            $response->getBody()->write(json_encode($mixed));
        } elseif (is_string($mixed)) {
            $response->getBody()->write($mixed);
        } else {
            throw new \Exception('Unsupported content type');
        }

        return $response;
    }
626471
public function data(ServerRequestInterface $request, DatatablesService $datatables_service): ResponseInterface
    {
        $files  = $request->getQueryParams()['files']; // local|external|unused
        $search = $request->getQueryParams()['search'];
        $search = $search['value'];
        $start  = (int) $request->getQueryParams()['start'];
        $length = (int) $request->getQueryParams()['length'];

        // Files within this folder
        $media_folder = $request->getQueryParams()['media_folder'];

        // subfolders within $media_path
        $subfolders = $request->getQueryParams()['subfolders']; // include|exclude

        $search_columns = ['multimedia_file_refn', 'descriptive_title'];

        $sort_columns = [
            0 => 'multimedia_file_refn',
            2 => DB::raw('descriptive_title || multimedia_file_refn'),
        ];

        switch ($files) {
            case 'local':
                $query = DB::table('media_file')
                    ->join('media', static function (JoinClause $join): void {
                        $join
                            ->on('media.m_file', '=', 'media_file.m_file')
                            ->on('media.m_id', '=', 'media_file.m_id');
                    })
                    ->join('gedcom_setting', 'gedcom_id', '=', 'media.m_file')
                    ->where('setting_name', '=', 'MEDIA_DIRECTORY')
                    ->where('multimedia_file_refn', 'NOT LIKE', 'http://%')
                    ->where('multimedia_file_refn', 'NOT LIKE', 'https://%')
                    ->select(['media.*', 'multimedia_file_refn', 'descriptive_title']);

                $query->where(DB::raw('setting_value || multimedia_file_refn'), 'LIKE', $media_folder . '%');

                if ($subfolders === 'exclude') {
                    $query->where(DB::raw('setting_value || multimedia_file_refn'), 'NOT LIKE', $media_folder . '%/%');
                }

                return $datatables_service->handle($request, $query, $search_columns, $sort_columns, function (stdClass $row): array {
                    /** @var Media $media */
                    $media = Media::rowMapper()($row);

                    $media_files = $media->mediaFiles()
                        ->filter(static function (MediaFile $media_file) use ($row): bool {
                            return $media_file->filename() == $row->multimedia_file_refn;
                        })
                        ->map(static function (MediaFile $media_file): string {
                            return $media_file->displayImage(150, 150, '', []);
                        })
                        ->implode('');

                    return [
                        $row->multimedia_file_refn,
                        $media_files,
                        $this->mediaObjectInfo($media),
                    ];
                });

            case 'external':
                $query = DB::table('media_file')
                    ->join('media', static function (JoinClause $join): void {
                        $join
                            ->on('media.m_file', '=', 'media_file.m_file')
                            ->on('media.m_id', '=', 'media_file.m_id');
                    })
                    ->where(static function (Builder $query): void {
                        $query
                            ->where('multimedia_file_refn', 'LIKE', 'http://%')
                            ->orWhere('multimedia_file_refn', 'LIKE', 'https://%');
                    })
                    ->select(['media.*', 'multimedia_file_refn', 'descriptive_title']);

                return $datatables_service->handle($request, $query, $search_columns, $sort_columns, function (stdClass $row): array {
                    /** @var Media $media */
                    $media = Media::rowMapper()($row);

                    $media_files = $media->mediaFiles()
                        ->filter(static function (MediaFile $media_file) use ($row): bool {
                            return $media_file->filename() === $row->multimedia_file_refn;
                        })
                        ->map(static function (MediaFile $media_file): string {
                            return $media_file->displayImage(150, 150, '', []);
                        })
                        ->implode('');

                    return [
                        $row->multimedia_file_refn,
                        $media_files,
                        $this->mediaObjectInfo($media),
                    ];
                });

            case 'unused':
                // Which trees use which media folder?
                $media_trees = DB::table('gedcom')
                    ->join('gedcom_setting', 'gedcom_setting.gedcom_id', '=', 'gedcom.gedcom_id')
                    ->where('setting_name', '=', 'MEDIA_DIRECTORY')
                    ->where('gedcom.gedcom_id', '>', 0)
                    ->pluck('setting_value', 'gedcom_name');

                $disk_files = $this->allDiskFiles($media_folder, $subfolders);
                $db_files   = $this->allMediaFiles($media_folder, $subfolders);

                // All unused files
                $unused_files = array_diff($disk_files, $db_files);
                $recordsTotal = count($unused_files);

                // Filter unused files
                if ($search) {
                    $unused_files = array_filter($unused_files, static function (string $x) use ($search): bool {
                        return strpos($x, $search) !== false;
                    });
                }
                $recordsFiltered = count($unused_files);

                // Sort files - only option is column 0
                sort($unused_files);
                $order = $request->getQueryParams()['order'];
                if ($order && $order[0]['dir'] === 'desc') {
                    $unused_files = array_reverse($unused_files);
                }

                // Paginate unused files
                $unused_files = array_slice($unused_files, $start, $length);

                $data = [];
                foreach ($unused_files as $unused_file) {
                    $imgsize = getimagesize(WT_DATA_DIR . $media_folder . $unused_file);
                    // We can’t create a URL (not in public_html) or use the media firewall (no such object)
                    if ($imgsize === false) {
                        $img = '-';
                    } else {
                        $url = route('unused-media-thumbnail', [
                            'folder' => $media_folder,
                            'file'   => $unused_file,
                            'w'      => 100,
                            'h'      => 100,
                        ]);
                        $img = '<img src="' . e($url) . '">';
                    }

                    // Form to create new media object in each tree
                    $create_form = '';
                    foreach ($media_trees as $media_tree => $media_directory) {
                        if (Str::startsWith($media_folder . $unused_file, $media_directory)) {
                            $tmp         = substr($media_folder . $unused_file, strlen($media_directory));
                            $create_form .=
                                '<p><a href="#" data-toggle="modal" data-target="#modal-create-media-from-file" data-file="' . e($tmp) . '" data-tree="' . e($media_tree) . '" onclick="document.getElementById(\'file\').value=this.dataset.file; document.getElementById(\'ged\').value=this.dataset.tree;">' . I18N::translate('Create') . '</a> — ' . e($media_tree) . '<p>';
                        }
                    }

                    $delete_link = '<p><a data-confirm="' . I18N::translate('Are you sure you want to delete “%s”?', e($unused_file)) . '" data-url="' . e(route('admin-media-delete', [
                            'file'   => $unused_file,
                            'folder' => $media_folder,
                        ])) . '" onclick="if (confirm(this.dataset.confirm)) jQuery.post(this.dataset.url, function (){document.location.reload();})" href="#">' . I18N::translate('Delete') . '</a></p>';

                    $data[] = [
                        $this->mediaFileInfo($media_folder, $unused_file) . $delete_link,
                        $img,
                        $create_form,
                    ];
                }
                break;

            default:
                throw new BadRequestHttpException();
        }

        // See http://www.datatables.net/usage/server-side
        return response([
            'draw'            => (int) $request->getQueryParams()['draw'],
            'recordsTotal'    => $recordsTotal,
            'recordsFiltered' => $recordsFiltered,
            'data'            => $data,
        ]);
    }
332534
private function getRequestUri(ServerRequestInterface $request) : string
    {
        $uri = $request->getUri()->getPath();
        $query = $request->getUri()->getQuery();

        if (!empty($query)) {
            return $uri . '?' . \urldecode($query);
        }

        return $uri;
    }
1207049
public function preferencesSave(ServerRequestInterface $request): ResponseInterface
    {
        $INDEX_DIRECTORY = $request->get('INDEX_DIRECTORY');
        if (substr($INDEX_DIRECTORY, -1) !== '/') {
            $INDEX_DIRECTORY .= '/';
        }
        if (File::mkdir($INDEX_DIRECTORY)) {
            Site::setPreference('INDEX_DIRECTORY', $INDEX_DIRECTORY);
        } else {
            FlashMessages::addMessage(I18N::translate('The folder %s does not exist, and it could not be created.', e($INDEX_DIRECTORY)), 'danger');
        }

        Site::setPreference('THEME_DIR', $request->get('THEME_DIR'));
        Site::setPreference('ALLOW_CHANGE_GEDCOM', (string) (bool) $request->get('ALLOW_CHANGE_GEDCOM'));
        Site::setPreference('TIMEZONE', $request->get('TIMEZONE'));

        FlashMessages::addMessage(I18N::translate('The website preferences have been updated.'), 'success');
        $url = route('admin-control-panel');

        return redirect($url);
    }
1008308
public function helpText(ServerRequestInterface $request): ResponseInterface
    {
        $topic = $request->get('topic');

        switch ($topic) {
            case 'DATE':
                switch ($this->dmyOrder()) {
                    case 'YMD':
                        $date_shortcuts = self::DATE_SHORTCUTS + self::YMD_SHORTCUTS;
                        break;
                    case 'MDY':
                        $date_shortcuts = self::DATE_SHORTCUTS + self::MDY_SHORTCUTS;
                        break;
                    case 'DMY':
                    default:
                        $date_shortcuts = self::DATE_SHORTCUTS + self::DMY_SHORTCUTS;
                        break;
                }

                $title = I18N::translate('Date');
                $text  = view('help/date', [
                    'date_dates'            => $this->formatDates(array_keys($date_shortcuts)),
                    'date_shortcuts'        => $date_shortcuts,
                    'date_period_dates'     => $this->formatDates(array_keys(self::DATE_PERIOD_SHORTCUTS)),
                    'date_period_shortcuts' => self::DATE_PERIOD_SHORTCUTS,
                    'date_range_dates'      => $this->formatDates(array_keys(self::DATE_RANGE_SHORTCUTS)),
                    'date_range_shortcuts'  => self::DATE_RANGE_SHORTCUTS,
                    'french_dates'          => $this->formatDates(self::FRENCH_DATES),
                    'hijri_dates'           => $this->formatDates(self::HIJRI_DATES),
                    'jewish_dates'          => $this->formatDates(self::JEWISH_DATES),
                    'julian_dates'          => $this->formatDates(self::JULIAN_DATES),
                ]);
                break;

            case 'NAME':
                $title = I18N::translate('Name');
                $text  = view('help/name');
                break;

            case 'SURN':
                $title = I18N::translate('Surname');
                $text  = view('help/surname');
                break;

            case 'OBJE':
                $title = I18N::translate('Media object');
                $text  = view('help/media-object');
                break;

            case 'PLAC':
                $title = I18N::translate('Place');
                $text  = view('help/place');
                break;

            case 'RESN':
                $title = I18N::translate('Restriction');
                $text  = view('help/romanized');
                break;

            case 'ROMN':
                $title = I18N::translate('Romanized');
                $text  = view('help/restriction');
                break;

            case '_HEB':
                $title = I18N::translate('Hebrew');
                $text  = view('help/hebrew');
                break;

            case 'annivers_year_select':
                $title = I18N::translate('Year input box');
                $text  = view('help/calendar-year');
                break;

            case 'edit_SOUR_EVEN':
                $title = I18N::translate('Associate events with this source');
                $text  = view('help/source-events');
                break;

            case 'pending_changes':
                $title = I18N::translate('Pending changes');
                $text  = view('help/pending-changes', [
                    'is_admin' => Auth::isAdmin(),
                ]);
                break;

            case 'relationship-privacy':
                $title = I18N::translate('Restrict to immediate family');
                $text  = view('help/relationship-privacy');
                break;

            default:
                $title = I18N::translate('Help');
                $text  = I18N::translate('The help text has not been written for this item.');
                break;
        }

        $html = view('modals/help', [
            'title' => $title,
            'text'  => $text,
        ]);

        return response($html);
    }
641237
public function index(ServerRequestInterface $request): ResponseInterface
    {
        $files        = $request->getQueryParams()['files'] ?? 'local'; // local|unused|external
        $media_folder = $request->getQueryParams()['media_folder'] ?? '';
        $subfolders   = $request->getQueryParams()['subfolders'] ?? 'include'; // include/exclude

        $media_folders = $this->allMediaFolders();

        // Preserve the pagination/filtering/sorting between requests, so that the
        // browser’s back button works. Pagination is dependent on the currently
        // selected folder.
        $table_id = md5($files . $media_folder . $subfolders);

        $title = I18N::translate('Manage media');

        return $this->viewResponse('admin/media', [
            'data_folder'   => WT_DATA_DIR,
            'files'         => $files,
            'media_folder'  => $media_folder,
            'media_folders' => $media_folders,
            'subfolders'    => $subfolders,
            'table_id'      => $table_id,
            'title'         => $title,
        ]);
    }
374359
public function match(ServerRequestInterface $request)
    {
        $basePathLength = strlen($this->baseUri['path']);

        return RouteCollectionMatcher::matchRouteCollection(
            $this->routeCollection,
            $request,
            $basePathLength,
            []
        ) ?: MatchResult::fromMatchFailure();
    }
851916
private function getSchema(ServerRequestInterface $request)
    {
        $uri = $request->getUri();
        $path = $uri->getPath();

        foreach ($this->schemas as $pattern => $file) {
            if (stripos($path, $pattern) === 0) {
                return new \SplFileObject($this->normalizeFilePath($file));
            }
        }

        return null;
    }
732102
public function handle(ServerRequestInterface $request): ResponseInterface
    {
        $stack = new Stack(
            $this->handlers,
            $this->handlerResolver
        );
        return $stack->handle($request);
    }
624894
public function handle(ServerRequestInterface $request): ResponseInterface
    {
        $input = $this->input($request);

        $payload = $this->domain->payload($input);

        return $this->responder->response($request, $payload);
    }