Import the next chunk of a GEDCOM file.

@param TimeoutService $timeout_service
@param Tree           $tree

@return ResponseInterface
512911
public function import(TimeoutService $timeout_service, Tree $tree): ResponseInterface
    {
        try {
            // Only allow one process to import each gedcom at a time
            DB::table('gedcom_chunk')
                ->where('gedcom_id', '=', $tree->id())
                ->lockForUpdate()
                ->get();

            // What is the current import status?
            $import_offset = (int) DB::table('gedcom_chunk')
                ->where('gedcom_id', '=', $tree->id())
                ->where('imported', '=', '1')
                ->sum(DB::raw('LENGTH(chunk_data)'));

            $import_total = (int) DB::table('gedcom_chunk')
                ->where('gedcom_id', '=', $tree->id())
                ->sum(DB::raw('LENGTH(chunk_data)'));

            // Finished?
            if ($import_offset === $import_total) {
                $tree->setPreference('imported', '1');

                $html = view('admin/import-complete');

                return response($html);
            }

            // Calculate progress so far
            $progress = $import_offset / $import_total;

            $first_time = ($import_offset === 0);

            // Run for a short period of time. This keeps the resource requirements low.
            do {
                $data = DB::table('gedcom_chunk')
                    ->where('gedcom_id', '=', $tree->id())
                    ->where('imported', '=', '0')
                    ->orderBy('gedcom_chunk_id')
                    ->select(['gedcom_chunk_id', 'chunk_data'])
                    ->first();

                // If we are loading the first (header) record, make sure the encoding is UTF-8.
                if ($first_time) {
                    // Remove any byte-order-mark
                    if (Str::startsWith($data->chunk_data, Gedcom::UTF8_BOM)) {
                        $data->chunk_data = Str::after($data->chunk_data, Gedcom::UTF8_BOM);
                        // Put it back in the database, so we can do character conversion
                        DB::table('gedcom_chunk')
                            ->where('gedcom_chunk_id', '=', $data->gedcom_chunk_id)
                            ->update(['chunk_data' => $data->chunk_data]);
                    }

                    if (!Str::startsWith($data->chunk_data, '0 HEAD')) {
                        return $this->viewResponse('admin/import-fail', [
                            'error' => I18N::translate('Invalid GEDCOM file - no header record found.'),
                        ]);
                    }

                    // What character set is this? Need to convert it to UTF8
                    if (preg_match('/[\r\n][ \t]*1 CHAR(?:ACTER)? (.+)/', $data->chunk_data, $match)) {
                        $charset = strtoupper(trim($match[1]));
                    } else {
                        $charset = 'ASCII';
                    }
                    // MySQL supports a wide range of collation conversions. These are ones that
                    // have been encountered "in the wild".
                    switch ($charset) {
                        case 'ASCII':
                            DB::table('gedcom_chunk')
                                ->where('gedcom_id', '=', $tree->id())
                                ->update(['chunk_data' => DB::raw('CONVERT(CONVERT(chunk_data USING ascii) USING utf8)')]);
                            break;
                        case 'IBMPC':   // IBMPC, IBM WINDOWS and MS-DOS could be anything. Mostly it means CP850.
                        case 'IBM WINDOWS':
                        case 'MS-DOS':
                        case 'CP437':
                        case 'CP850':
                            // CP850 has extra letters with diacritics to replace box-drawing chars in CP437.
                            DB::table('gedcom_chunk')
                                ->where('gedcom_id', '=', $tree->id())
                                ->update(['chunk_data' => DB::raw('CONVERT(CONVERT(chunk_data USING cp850) USING utf8)')]);
                            break;
                        case 'ANSI': // ANSI could be anything. Most applications seem to treat it as latin1.
                        case 'WINDOWS':
                        case 'CP1252':
                        case 'ISO8859-1':
                        case 'ISO-8859-1':
                        case 'LATIN1':
                        case 'LATIN-1':
                            // Convert from ISO-8859-1 (western european) to UTF8.
                            DB::table('gedcom_chunk')
                                ->where('gedcom_id', '=', $tree->id())
                                ->update(['chunk_data' => DB::raw('CONVERT(CONVERT(chunk_data USING latin1) USING utf8)')]);
                            break;
                        case 'CP1250':
                        case 'ISO8859-2':
                        case 'ISO-8859-2':
                        case 'LATIN2':
                        case 'LATIN-2':
                            // Convert from ISO-8859-2 (eastern european) to UTF8.
                            DB::table('gedcom_chunk')
                                ->where('gedcom_id', '=', $tree->id())
                                ->update(['chunk_data' => DB::raw('CONVERT(CONVERT(chunk_data USING latin2) USING utf8)')]);
                            break;
                        case 'MACINTOSH':
                            // Convert from MAC Roman to UTF8.
                            DB::table('gedcom_chunk')
                                ->where('gedcom_id', '=', $tree->id())
                                ->update(['chunk_data' => DB::raw('CONVERT(CONVERT(chunk_data USING macroman) USING utf8)')]);
                            break;
                        case 'UTF8':
                        case 'UTF-8':
                            // Already UTF-8 so nothing to do!
                            break;
                        case 'ANSEL':
                        default:
                            return $this->viewResponse('admin/import-fail', [
                                'error' => I18N::translate('Error: converting GEDCOM files from %s encoding to UTF-8 encoding not currently supported.', $charset),
                            ]);
                    }
                    $first_time = false;

                    // Re-fetch the data, now that we have performed character set conversion.
                    $data = DB::table('gedcom_chunk')
                        ->where('gedcom_chunk_id', '=', $data->gedcom_chunk_id)
                        ->select(['gedcom_chunk_id', 'chunk_data'])
                        ->first();
                }

                if (!$data) {
                    break;
                }

                $data->chunk_data = str_replace("\r", "\n", $data->chunk_data);

                // Import all the records in this chunk of data
                foreach (preg_split('/\n+(?=0)/', $data->chunk_data) as $rec) {
                    try {
                        FunctionsImport::importRecord($rec, $tree, false);
                    } catch (Throwable $ex) {
                        // Make sure the error message includes the GEDCOM record being imported.
                        throw new Exception($ex->getMessage() . '<pre>' . e($rec) . '</pre>');
                    }
                }

                // Mark the chunk as imported
                DB::table('gedcom_chunk')
                    ->where('gedcom_chunk_id', '=', $data->gedcom_chunk_id)
                    ->update(['imported' => 1]);
            } while (!$timeout_service->isTimeLimitUp());

            return $this->viewResponse('admin/import-progress', [
                'progress' => $progress,
            ]);
        } catch (Exception $ex) {
            DB::connection()->rollBack();

            return $this->viewResponse('admin/import-fail', [
                'error' => $ex->getMessage(),
            ]);
        }
    }
361231
public function importAction(ServerRequestInterface $request, Tree $tree): ResponseInterface
    {
        $source             = $request->get('source');
        $keep_media         = (bool) $request->get('keep_media');
        $WORD_WRAPPED_NOTES = (bool) $request->get('WORD_WRAPPED_NOTES');
        $GEDCOM_MEDIA_PATH  = $request->get('GEDCOM_MEDIA_PATH');

        // Save these choices as defaults
        $tree->setPreference('keep_media', $keep_media ? '1' : '0');
        $tree->setPreference('WORD_WRAPPED_NOTES', $WORD_WRAPPED_NOTES ? '1' : '0');
        $tree->setPreference('GEDCOM_MEDIA_PATH', $GEDCOM_MEDIA_PATH);

        if ($source === 'client') {
            $upload = $request->getUploadedFiles()['tree_name'] ?? null;

            if ($upload instanceof UploadedFile) {
                if ($upload->getError() === UPLOAD_ERR_OK) {
                    $tree->importGedcomFile($upload->getStream(), basename($upload->getClientFilename()));
                } else {
                    FlashMessages::addMessage(Functions::fileUploadErrorText($upload->getError()), 'danger');
                }
            } else {
                FlashMessages::addMessage(I18N::translate('No GEDCOM file was received.'), 'danger');
            }
        }

        if ($source === 'server') {
            $basename = basename($request->get('tree_name'));

            if ($basename) {
                $stream = app(StreamFactoryInterface::class)->createStreamFromFile(WT_DATA_DIR . $basename);
                $tree->importGedcomFile($stream, $basename);
            } else {
                FlashMessages::addMessage(I18N::translate('No GEDCOM file was received.'), 'danger');
            }
        }

        $url = route('admin-trees', ['ged' => $tree->name()]);

        return redirect($url);
    }
646548
public function treePageEdit(Tree $tree): ResponseInterface
    {
        $main_blocks = $this->treeBlocks($tree->id(), 'main');
        $side_blocks = $this->treeBlocks($tree->id(), 'side');

        $all_blocks = $this->availableTreeBlocks();
        $title      = I18N::translate('Change the “Home page” blocks');
        $url_cancel = route('tree-page', ['ged' => $tree->name()]);
        $url_save   = route('tree-page-update', ['ged' => $tree->name()]);

        return $this->viewResponse('edit-blocks-page', [
            'all_blocks'  => $all_blocks,
            'can_reset'   => true,
            'main_blocks' => $main_blocks,
            'side_blocks' => $side_blocks,
            'title'       => $title,
            'url_cancel'  => $url_cancel,
            'url_save'    => $url_save,
        ]);
    }
312861
public function step(ServerRequestInterface $request, ?Tree $tree): ResponseInterface
    {
        $step = $request->getQueryParams()['step'] ?? self::STEP_CHECK;

        switch ($step) {
            case self::STEP_CHECK:
                return $this->wizardStepCheck();

            case self::STEP_PREPARE:
                return $this->wizardStepPrepare();

            case self::STEP_PENDING:
                return $this->wizardStepPending();

            case self::STEP_EXPORT:
                if ($tree === null) {
                    throw new RuntimeException('Exporting a non-existant tree?');
                }

                return $this->wizardStepExport($tree);

            case self::STEP_DOWNLOAD:
                return $this->wizardStepDownload();

            case self::STEP_UNZIP:
                return $this->wizardStepUnzip();

            case self::STEP_COPY:
                return $this->wizardStepCopy();

            case self::STEP_CLEANUP:
                return $this->wizardStepCleanup();
        }

        throw new NotFoundHttpException();
    }
726313
public function reportRun(ServerRequestInterface $request, Tree $tree, UserInterface $user): ResponseInterface
    {
        $report   = $request->get('report', '');
        $output   = $request->get('output');
        $vars     = $request->get('vars', []);
        $varnames = $request->get('varnames', []);
        $type     = $request->get('type', []);

        $module = $this->module_service->findByName($report);

        if (!$module instanceof ModuleReportInterface) {
            throw new NotFoundHttpException('Report ' . $report . ' not found.');
        }

        //-- setup the arrays
        $newvars = [];
        foreach ($vars as $name => $var) {
            $newvars[$name]['id'] = $var;
            if (!empty($type[$name])) {
                switch ($type[$name]) {
                    case 'INDI':
                        $record = Individual::getInstance($var, $tree);
                        if ($record && $record->canShowName()) {
                            $newvars[$name]['gedcom'] = $record->privatizeGedcom(Auth::accessLevel($tree));
                        } else {
                            return $this->reportSetup($request, $tree, $user);
                        }
                        break;
                    case 'FAM':
                        $record = Family::getInstance($var, $tree);
                        if ($record && $record->canShowName()) {
                            $newvars[$name]['gedcom'] = $record->privatizeGedcom(Auth::accessLevel($tree));
                        } else {
                            return $this->reportSetup($request, $tree, $user);
                        }
                        break;
                    case 'SOUR':
                        $record = Source::getInstance($var, $tree);
                        if ($record && $record->canShowName()) {
                            $newvars[$name]['gedcom'] = $record->privatizeGedcom(Auth::accessLevel($tree));
                        } else {
                            return $this->reportSetup($request, $tree, $user);
                        }
                        break;
                }
            }
        }
        $vars = $newvars;

        foreach ($varnames as $name) {
            if (!isset($vars[$name])) {
                $vars[$name]['id'] = '';
            }
        }

        $xml_filename = $module->resourcesFolder() . $module->xmlFilename();

        switch ($output) {
            default:
            case 'HTML':
                ob_start();
                new ReportParserGenerate($xml_filename, new ReportHtml(), $vars, $tree);
                $html = ob_get_clean();

                $this->layout = 'layouts/report';

                return $this->viewResponse('report-page', [
                    'content' => $html,
                    'title'   => I18N::translate('Report'),
                ]);

            case 'PDF':
                ob_start();
                new ReportParserGenerate($xml_filename, new ReportPdf(), $vars, $tree);
                $pdf = ob_get_clean();

                return response($pdf, StatusCodeInterface::STATUS_OK, [
                    'Content-Type' => 'application/pdf',
                    'Content-Disposition' => 'attachment; filename="' . addcslashes($report, '"') . '"',
                ]);
        }
    }
1045914
public function quick(ServerRequestInterface $request, Tree $tree): ResponseInterface
    {
        $query = $request->get('query', '');

        // Was the search query an XREF in the current tree?
        // If so, go straight to it.
        $record = GedcomRecord::getInstance($query, $tree);

        if ($record !== null && $record->canShow()) {
            return redirect($record->url());
        }

        return $this->general($request, $tree);
    }
760238
public function page(ServerRequestInterface $request, Tree $tree): ResponseInterface
    {
        $query = $request->get('query', '');
        $xref  = $request->get('extra', '');

        $source = Source::getInstance($xref, $tree);

        Auth::checkSourceAccess($source);

        $regex_query = preg_quote(strtr($query, [' ' => '.+']), '/');

        // Fetch all records with a link to this source
        $individuals = DB::table('individuals')
            ->join('link', static function (JoinClause $join): void {
                $join
                    ->on('l_file', '=', 'i_file')
                    ->on('l_from', '=', 'i_id');
            })
            ->where('i_file', '=', $tree->id())
            ->where('l_to', '=', $xref)
            ->where('l_type', '=', 'SOUR')
            ->distinct()
            ->select(['individuals.*'])
            ->get()
            ->map(Individual::rowMapper())
            ->filter(GedcomRecord::accessFilter());

        $families = DB::table('families')
            ->join('link', static function (JoinClause $join): void {
                $join
                    ->on('l_file', '=', 'f_file')
                    ->on('l_from', '=', 'f_id')
                    ->where('l_type', '=', 'SOUR');
            })
            ->where('f_file', '=', $tree->id())
            ->where('l_to', '=', $xref)
            ->where('l_type', '=', 'SOUR')
            ->distinct()
            ->select(['families.*'])
            ->get()
            ->map(Family::rowMapper())
            ->filter(GedcomRecord::accessFilter());

        $pages = [];

        foreach ($individuals->merge($families) as $record) {
            if (preg_match_all('/\n1 SOUR @' . $xref . '@(?:\n[2-9].*)*\n2 PAGE (.*' . $regex_query . '.*)/i', $record->gedcom(), $matches)) {
                $pages = array_merge($pages, $matches[1]);
            }

            if (preg_match_all('/\n2 SOUR @' . $xref . '@(?:\n[3-9].*)*\n3 PAGE (.*' . $regex_query . '.*)/i', $record->gedcom(), $matches)) {
                $pages = array_merge($pages, $matches[1]);
            }
        }

        $pages = array_unique($pages);

        $pages = array_map(static function (string $page): array {
            return ['value' => $page];
        }, $pages);

        return response($pages);
    }
20277
public function postAddIndividualAction(ServerRequestInterface $request, Tree $tree): ResponseInterface
    {
        $xref   = $request->get('xref', '');
        $option = $request->get('option', '');

        $individual = Individual::getInstance($xref, $tree);

        if ($individual === null) {
            throw new IndividualNotFoundException();
        }

        switch ($option) {
            case 'self':
                $this->addRecordToCart($individual);
                break;

            case 'parents':
                foreach ($individual->childFamilies() as $family) {
                    $this->addFamilyAndChildrenToCart($family);
                }
                break;

            case 'spouses':
                foreach ($individual->spouseFamilies() as $family) {
                    $this->addFamilyAndChildrenToCart($family);
                }
                break;

            case 'ancestors':
                $this->addAncestorsToCart($individual);
                break;

            case 'ancestor_families':
                $this->addAncestorFamiliesToCart($individual);
                break;

            case 'descendants':
                foreach ($individual->spouseFamilies() as $family) {
                    $this->addFamilyAndDescendantsToCart($family);
                }
                break;
        }

        return redirect($individual->url());
    }
830660
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        /** @var Tree|null $tree */
        $tree = app(Tree::class);

        // The tree exists, we have access to it, and it is fully imported.
        if ($tree instanceof Tree && $tree->getPreference('imported') === '1') {
            return redirect(route('tree-page', ['ged' => $tree->name()]));
        }

        // Not logged in?
        if (!Auth::check()) {
            return redirect(route('login', ['url' => (string) $request->getUri()]));
        }

        // No tree or tree not imported?
        if (Auth::isAdmin()) {
            return redirect(route('admin-trees'));
        }

        return $this->viewResponse('errors/no-tree-access', ['title' => '']);
    }
206811
public function postAddMediaAction(ServerRequestInterface $request, Tree $tree): ResponseInterface
    {
        $xref = $request->get('xref', '');

        $media = Media::getInstance($xref, $tree);

        if ($media === null) {
            throw new MediaNotFoundException();
        }

        $this->addRecordToCart($media);

        return redirect($media->url());
    }
211966
public function editMediaFile(ServerRequestInterface $request, Tree $tree): ResponseInterface
    {
        $xref    = $request->get('xref', '');
        $fact_id = $request->get('fact_id', '');
        $media   = Media::getInstance($xref, $tree);

        try {
            Auth::checkMediaAccess($media);
        } catch (Exception $ex) {
            return response(view('modals/error', [
                'title' => I18N::translate('Edit a media file'),
                'error' => $ex->getMessage(),
            ]), StatusCodeInterface::STATUS_FORBIDDEN);
        }

        foreach ($media->mediaFiles() as $media_file) {
            if ($media_file->factId() === $fact_id) {
                return response(view('modals/edit-media-file', [
                    'media_file'      => $media_file,
                    'max_upload_size' => $this->maxUploadFilesize(),
                    'media'           => $media,
                    'media_types'     => $this->mediaTypes(),
                    'unused_files'    => $this->unusedFiles($tree),
                ]));
            }
        }

        return response('', StatusCodeInterface::STATUS_NOT_FOUND);
    }
478158
public function place(ServerRequestInterface $request, Tree $tree): ResponseInterface
    {
        $query = $request->get('query', '');
        $data  = [];

        foreach ($this->search_service->searchPlaces($tree, $query) as $place) {
            $data[] = ['value' => $place->gedcomName()];
        }

        if (empty($data) && $tree->getPreference('GEONAMES_ACCOUNT')) {
            // No place found? Use an external gazetteer
            $url =
                'http://api.geonames.org/searchJSON' .
                '?name_startsWith=' . urlencode($query) .
                '&lang=' . WT_LOCALE .
                '&fcode=CMTY&fcode=ADM4&fcode=PPL&fcode=PPLA&fcode=PPLC' .
                '&style=full' .
                '&username=' . $tree->getPreference('GEONAMES_ACCOUNT');

            // try to use curl when file_get_contents not allowed
            if (ini_get('allow_url_fopen')) {
                $json = file_get_contents($url);
            } elseif (function_exists('curl_init')) {
                $ch = curl_init();
                curl_setopt($ch, CURLOPT_URL, $url);
                curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
                $json = curl_exec($ch);
                curl_close($ch);
            } else {
                return response([]);
            }

            $places = json_decode($json, true);
            if (isset($places['geonames']) && is_array($places['geonames'])) {
                foreach ($places['geonames'] as $k => $place) {
                    $data[] = ['value' => $place['name'] . ', ' . $place['adminName2'] . ', ' . $place['adminName1'] . ', ' . $place['countryName']];
                }
            }
        }

        return response($data);
    }
472614
public function index(Tree $tree = null): ResponseInterface
    {
        $multiple_tree_threshold = (int) Site::getPreference('MULTIPLE_TREE_THRESHOLD', self::MULTIPLE_TREE_THRESHOLD);
        $gedcom_files            = $this->gedcomFiles(WT_DATA_DIR);

        $all_trees = Tree::getAll();

        // On sites with hundreds or thousands of trees, this page becomes very large.
        // Just show the current tree, the default tree, and unimported trees
        if (count($all_trees) >= $multiple_tree_threshold) {
            $all_trees = array_filter($all_trees, static function (Tree $x) use ($tree): bool {
                return $x->getPreference('imported') === '0' || $tree->id() === $x->id() || $x->name() === Site::getPreference('DEFAULT_GEDCOM');
            });
        }

        $default_tree_name  = $this->generateNewTreeName();
        $default_tree_title = I18N::translate('My family tree');

        $title = I18N::translate('Manage family trees');

        return $this->viewResponse('admin/trees', [
            'all_trees'               => $all_trees,
            'default_tree_name'       => $default_tree_name,
            'default_tree_title'      => $default_tree_title,
            'gedcom_files'            => $gedcom_files,
            'multiple_tree_threshold' => $multiple_tree_threshold,
            'title'                   => $title,
        ]);
    }
1161283
public function check(Tree $tree): ResponseInterface
    {
        // We need to work with raw GEDCOM data, as we are looking for errors
        // which may prevent the GedcomRecord objects from working.

        $q1 = DB::table('individuals')
            ->where('i_file', '=', $tree->id())
            ->select(['i_id AS xref', 'i_gedcom AS gedcom', DB::raw("'INDI' AS type")]);
        $q2 = DB::table('families')
            ->where('f_file', '=', $tree->id())
            ->select(['f_id AS xref', 'f_gedcom AS gedcom', DB::raw("'FAM' AS type")]);
        $q3 = DB::table('media')
            ->where('m_file', '=', $tree->id())
            ->select(['m_id AS xref', 'm_gedcom AS gedcom', DB::raw("'OBJE' AS type")]);
        $q4 = DB::table('sources')
            ->where('s_file', '=', $tree->id())
            ->select(['s_id AS xref', 's_gedcom AS gedcom', DB::raw("'SOUR' AS type")]);
        $q5 = DB::table('other')
            ->where('o_file', '=', $tree->id())
            ->whereNotIn('o_type', ['HEAD', 'TRLR'])
            ->select(['o_id AS xref', 'o_gedcom AS gedcom', 'o_type']);
        $q6 = DB::table('change')
            ->where('gedcom_id', '=', $tree->id())
            ->where('status', '=', 'pending')
            ->orderBy('change_id')
            ->select(['xref', 'new_gedcom AS gedcom', DB::raw("'' AS type")]);

        $rows = $q1
            ->unionAll($q2)
            ->unionAll($q3)
            ->unionAll($q4)
            ->unionAll($q5)
            ->unionAll($q6)
            ->get()
            ->map(static function (stdClass $row): stdClass {
                // Extract type for pending record
                if ($row->type === '' && preg_match('/^0 @[^@]*@ ([_A-Z0-9]+)/', $row->gedcom, $match)) {
                    $row->type = $match[1];
                }

                return $row;
            });

        $records = [];

        foreach ($rows as $row) {
            if ($row->gedcom !== '') {
                // existing or updated record
                $records[$row->xref] = $row;
            } else {
                // deleted record
                unset($records[$row->xref]);
            }
        }

        // LOOK FOR BROKEN LINKS
        $XREF_LINKS = [
            'NOTE'          => 'NOTE',
            'SOUR'          => 'SOUR',
            'REPO'          => 'REPO',
            'OBJE'          => 'OBJE',
            'SUBM'          => 'SUBM',
            'FAMC'          => 'FAM',
            'FAMS'          => 'FAM',
            //'ADOP'=>'FAM', // Need to handle this case specially. We may have both ADOP and FAMC links to the same FAM, but only store one.
            'HUSB'          => 'INDI',
            'WIFE'          => 'INDI',
            'CHIL'          => 'INDI',
            'ASSO'          => 'INDI',
            '_ASSO'         => 'INDI',
            // A webtrees extension
            'ALIA'          => 'INDI',
            'AUTH'          => 'INDI',
            // A webtrees extension
            'ANCI'          => 'SUBM',
            'DESI'          => 'SUBM',
            '_WT_OBJE_SORT' => 'OBJE',
            '_LOC'          => '_LOC',
        ];

        $RECORD_LINKS = [
            'INDI' => [
                'NOTE',
                'OBJE',
                'SOUR',
                'SUBM',
                'ASSO',
                '_ASSO',
                'FAMC',
                'FAMS',
                'ALIA',
                '_WT_OBJE_SORT',
                '_LOC',
            ],
            'FAM'  => [
                'NOTE',
                'OBJE',
                'SOUR',
                'SUBM',
                'ASSO',
                '_ASSO',
                'HUSB',
                'WIFE',
                'CHIL',
                '_LOC',
            ],
            'SOUR' => [
                'NOTE',
                'OBJE',
                'REPO',
                'AUTH',
            ],
            'REPO' => ['NOTE'],
            'OBJE' => ['NOTE'],
            // The spec also allows SOUR, but we treat this as a warning
            'NOTE' => [],
            // The spec also allows SOUR, but we treat this as a warning
            'SUBM' => [
                'NOTE',
                'OBJE',
            ],
            'SUBN' => ['SUBM'],
            '_LOC' => [
                'SOUR',
                'OBJE',
                '_LOC',
                'NOTE',
            ],
        ];

        $errors   = [];
        $warnings = [];

        // Generate lists of all links
        $all_links   = [];
        $upper_links = [];
        foreach ($records as $record) {
            $all_links[$record->xref]               = [];
            $upper_links[strtoupper($record->xref)] = $record->xref;
            preg_match_all('/\n\d (' . Gedcom::REGEX_TAG . ') @([^#@\n][^\n@]*)@/', $record->gedcom, $matches, PREG_SET_ORDER);
            foreach ($matches as $match) {
                $all_links[$record->xref][$match[2]] = $match[1];
            }
        }

        foreach ($all_links as $xref1 => $links) {
            $type1 = $records[$xref1]->type;
            foreach ($links as $xref2 => $type2) {
                $type3 = isset($records[$xref2]) ? $records[$xref2]->type : '';
                if (!array_key_exists($xref2, $all_links)) {
                    if (array_key_exists(strtoupper($xref2), $upper_links)) {
                        $warnings[] =
                            $this->checkLinkMessage($tree, $type1, $xref1, $type2, $xref2) . ' ' .
                            /* I18N: placeholders are GEDCOM XREFs, such as R123 */
                            I18N::translate('%1$s does not exist. Did you mean %2$s?', $this->checkLink($tree, $xref2), $this->checkLink($tree, $upper_links[strtoupper($xref2)]));
                    } else {
                        /* I18N: placeholders are GEDCOM XREFs, such as R123 */
                        $errors[] = $this->checkLinkMessage($tree, $type1, $xref1, $type2, $xref2) . ' ' . I18N::translate('%1$s does not exist.', $this->checkLink($tree, $xref2));
                    }
                } elseif ($type2 === 'SOUR' && $type1 === 'NOTE') {
                    // Notes are intended to add explanations and comments to other records. They should not have their own sources.
                } elseif ($type2 === 'SOUR' && $type1 === 'OBJE') {
                    // Media objects are intended to illustrate other records, facts, and source/citations. They should not have their own sources.
                } elseif ($type2 === 'OBJE' && $type1 === 'REPO') {
                    $warnings[] =
                        $this->checkLinkMessage($tree, $type1, $xref1, $type2, $xref2) .
                        ' ' .
                        I18N::translate('This type of link is not allowed here.');
                } elseif (!array_key_exists($type1, $RECORD_LINKS) || !in_array($type2, $RECORD_LINKS[$type1], true) || !array_key_exists($type2, $XREF_LINKS)) {
                    $errors[] =
                        $this->checkLinkMessage($tree, $type1, $xref1, $type2, $xref2) .
                        ' ' .
                        I18N::translate('This type of link is not allowed here.');
                } elseif ($XREF_LINKS[$type2] !== $type3) {
                    // Target XREF does exist - but is invalid
                    $errors[] =
                        $this->checkLinkMessage($tree, $type1, $xref1, $type2, $xref2) . ' ' .
                        /* I18N: %1$s is an internal ID number such as R123. %2$s and %3$s are record types, such as INDI or SOUR */
                        I18N::translate('%1$s is a %2$s but a %3$s is expected.', $this->checkLink($tree, $xref2), $this->formatType($type3), $this->formatType($type2));
                } elseif (
                    $type2 === 'FAMC' && (!array_key_exists($xref1, $all_links[$xref2]) || $all_links[$xref2][$xref1] !== 'CHIL') ||
                    $type2 === 'FAMS' && (!array_key_exists($xref1, $all_links[$xref2]) || $all_links[$xref2][$xref1] !== 'HUSB' && $all_links[$xref2][$xref1] !== 'WIFE') ||
                    $type2 === 'CHIL' && (!array_key_exists($xref1, $all_links[$xref2]) || $all_links[$xref2][$xref1] !== 'FAMC') ||
                    $type2 === 'HUSB' && (!array_key_exists($xref1, $all_links[$xref2]) || $all_links[$xref2][$xref1] !== 'FAMS') ||
                    $type2 === 'WIFE' && (!array_key_exists($xref1, $all_links[$xref2]) || $all_links[$xref2][$xref1] !== 'FAMS')
                ) {
                    /* I18N: %1$s and %2$s are internal ID numbers such as R123 */
                    $errors[] = $this->checkLinkMessage($tree, $type1, $xref1, $type2, $xref2) . ' ' . I18N::translate('%1$s does not have a link back to %2$s.', $this->checkLink($tree, $xref2), $this->checkLink($tree, $xref1));
                }
            }
        }

        $title = I18N::translate('Check for errors') . ' — ' . e($tree->title());

        return $this->viewResponse('admin/trees-check', [
            'errors'   => $errors,
            'title'    => $title,
            'tree'     => $tree,
            'warnings' => $warnings,
        ]);
    }
583611
public function edit(Tree $tree, UserInterface $user): ResponseInterface
    {
        $my_individual_record = Individual::getInstance($tree->getUserPreference(Auth::user(), 'gedcomid'), $tree);
        $contact_methods      = FunctionsEdit::optionsContactMethods();
        $default_individual   = Individual::getInstance($tree->getUserPreference(Auth::user(), 'rootid'), $tree);
        $installed_languages  = FunctionsEdit::optionsInstalledLanguages();
        $show_delete_option   = !$user->getPreference('canadmin');
        $themes               = $this->themeOptions();
        $timezone_ids         = DateTimeZone::listIdentifiers();
        $timezones            = array_combine($timezone_ids, $timezone_ids);
        $title                = I18N::translate('My account');

        return $this->viewResponse('edit-account-page', [
            'contact_methods'      => $contact_methods,
            'default_individual'   => $default_individual,
            'installed_languages'  => $installed_languages,
            'my_individual_record' => $my_individual_record,
            'show_delete_option'   => $show_delete_option,
            'themes'               => $themes,
            'timezones'            => $timezones,
            'title'                => $title,
            'user'                 => $user,
        ]);
    }
847014
public function getShowAction(Tree $tree): ResponseInterface
    {
        // Filter foreign languages.
        $faqs = $this->faqsForTree($tree)
            ->filter(static function (stdClass $faq): bool {
                return $faq->languages === '' || in_array(WT_LOCALE, explode(',', $faq->languages), true);
            });

        return $this->viewResponse('modules/faq/show', [
            'faqs'  => $faqs,
            'title' => I18N::translate('Frequently asked questions'),
            'tree'  => $tree,
        ]);
    }
1224381
public function exportClient(ServerRequestInterface $request, Tree $tree): ResponseInterface
    {
        // Validate user parameters
        $convert          = (bool) $request->get('convert');
        $zip              = (bool) $request->get('zip');
        $media            = (bool) $request->get('media');
        $media_path       = $request->get('media-path', '');
        $privatize_export = $request->get('privatize_export', '');

        $access_levels = [
            'gedadmin' => Auth::PRIV_NONE,
            'user'     => Auth::PRIV_USER,
            'visitor'  => Auth::PRIV_PRIVATE,
            'none'     => Auth::PRIV_HIDE,
        ];

        $access_level = $access_levels[$privatize_export];
        $encoding     = $convert ? 'ANSI' : 'UTF-8';

        // What to call the downloaded file
        $download_filename = $tree->name();
        if (strtolower(substr($download_filename, -4, 4)) !== '.ged') {
            $download_filename .= '.ged';
        }

        if ($zip || $media) {
            // Export the GEDCOM to an in-memory stream.
            $tmp_stream = tmpfile();
            FunctionsExport::exportGedcom($tree, $tmp_stream, $access_level, $media_path, $encoding);
            rewind($tmp_stream);

            // Create a new/empty .ZIP file
            $temp_zip_file  = tempnam(sys_get_temp_dir(), 'webtrees-zip-');
            $zip_filesystem = new Filesystem(new ZipArchiveAdapter($temp_zip_file));
            $zip_filesystem->writeStream($download_filename, $tmp_stream);

            if ($media) {
                $rows = DB::table('media')
                    ->where('m_file', '=', $tree->id())
                    ->get();

                $path = $tree->getPreference('MEDIA_DIRECTORY');

                foreach ($rows as $row) {
                    $record = Media::getInstance($row->m_id, $tree, $row->m_gedcom);
                    if ($record->canShow()) {
                        foreach ($record->mediaFiles() as $media_file) {
                            if (file_exists($media_file->getServerFilename())) {
                                $fp = fopen($media_file->getServerFilename(), 'rb');
                                $zip_filesystem->writeStream($path . $media_file->filename(), $fp);
                                fclose($fp);
                            }
                        }
                    }
                }
            }

            // The ZipArchiveAdapter may or may not close the stream.
            if (is_resource($tmp_stream)) {
                fclose($tmp_stream);
            }

            // Need to force-close the filesystem
            unset($zip_filesystem);

            // Use a stream, so that we do not have to load the entire file into memory.
            $stream   = app(StreamFactoryInterface::class)->createStreamFromFile($temp_zip_file);
            $filename = addcslashes($download_filename, '"') . '.zip';

            return response()
                ->withBody($stream)
                ->withHeader('Content-Type', 'application/zip')
                ->withHeader('Content-Disposition', 'attachment; filename="' . $filename . '"');
        }

        $resource = fopen('php://temp', 'wb+');
        FunctionsExport::exportGedcom($tree, $resource, $access_level, $media_path, $encoding);
        rewind($resource);

        $charset = $convert ? 'ISO-8859-1' : 'UTF-8';

        /** @var StreamFactoryInterface $response_factory */
        $stream_factory = app(StreamFactoryInterface::class);

        $stream = $stream_factory->createStreamFromStream($resource);

        /** @var ResponseFactoryInterface $response_factory */
        $response_factory = app(ResponseFactoryInterface::class);

        return $response_factory->createResponse()
            ->withBody($stream)
            ->withHeader('Content-Type', 'text/x-gedcom; charset=' . $charset)
            ->withHeader('Content-Disposition', 'attachment; filename="' . addcslashes($download_filename, '"') . '"');
    }