Save an edited media file.

@param ServerRequestInterface $request
@param Tree                   $tree

@return ResponseInterface
16394
public function editMediaFileAction(ServerRequestInterface $request, Tree $tree): ResponseInterface
    {
        $xref     = $request->get('xref', '');
        $fact_id  = $request->get('fact_id', '');
        $folder   = $request->get('folder', '');
        $new_file = $request->get('new_file', '');
        $remote   = $request->get('remote', '');
        $title    = $request->get('title', '');
        $type     = $request->get('type', '');
        $media    = Media::getInstance($xref, $tree);

        // Tidy whitespace
        $type  = trim(preg_replace('/\s+/', ' ', $type));
        $title = trim(preg_replace('/\s+/', ' ', $title));

        // Media object oes not exist?  Media object is read-only?
        if ($media === null || $media->isPendingDeletion() || !$media->canEdit()) {
            return redirect(route('tree-page', ['ged' => $tree->name()]));
        }

        // Find the fact we are editing.
        $media_file = null;
        foreach ($media->mediaFiles() as $tmp) {
            if ($tmp->factId() === $fact_id) {
                $media_file = $tmp;
            }
        }

        // Media file does not exist?
        if ($media_file === null) {
            return redirect(route('tree-page', ['ged' => $tree->name()]));
        }

        // We can edit the file as either a URL or a folder/file
        if ($remote !== '') {
            $file = $remote;
        } else {
            $new_file = str_replace('\\', '/', $new_file);
            $folder   = str_replace('\\', '/', $folder);
            $folder   = trim($folder, '/');

            if ($folder === '') {
                $file = $new_file;
            } else {
                $file = $folder . '/' . $new_file;
            }
            if (strpos($file, '../') !== false) {
                $file = '';
            }
        }

        // Invalid filename?  Do not change it.
        if ($file === '') {
            $file = $media_file->filename();
        }

        $MEDIA_DIRECTORY = $media->tree()->getPreference('MEDIA_DIRECTORY');
        $old             = $MEDIA_DIRECTORY . $media_file->filename();
        $new             = $MEDIA_DIRECTORY . $file;

        // Update the filesystem, if we can.
        if ($old !== $new && !$media_file->isExternal()) {
            // Don't overwrite existing file
            if (file_exists(WT_DATA_DIR . $new) && file_exists(WT_DATA_DIR . $old)) {
                FlashMessages::addMessage(I18N::translate('The media file %1$s could not be renamed to %2$s.', Html::filename($media_file->filename()), Html::filename($file)), 'info');
                $file = $media_file->filename();
            } else {
                try {
                    // The "old" file may not exist.  For example, if the file was renamed on disk,
                    // and we are now renaming the GEDCOM data to match.
                    if (file_exists(WT_DATA_DIR . $old)) {
                        File::mkdir(WT_DATA_DIR . $MEDIA_DIRECTORY . $folder);
                        rename(WT_DATA_DIR . $old, WT_DATA_DIR . $new);
                    }
                    FlashMessages::addMessage(I18N::translate('The media file %1$s has been renamed to %2$s.', Html::filename($media_file->filename()), Html::filename($file)), 'info');
                } catch (Throwable $ex) {
                    FlashMessages::addMessage($ex, 'info');
                    FlashMessages::addMessage(I18N::translate('The media file %1$s could not be renamed to %2$s.', Html::filename($media_file->filename()), Html::filename($file)), 'info');
                    $file = $media_file->filename();
                }
            }
        }

        $gedcom = $this->createMediaFileGedcom($file, $type, $title);

        $media->updateFact($fact_id, $gedcom, true);

        // Accept the changes, to keep the filesystem in sync with the GEDCOM data.
        if ($old !== $new && !$media_file->isExternal()) {
            FunctionsImport::acceptAllChanges($media->xref(), $tree);
        }

        return redirect($media->url());
    }
515415
public function rejectChanges(ServerRequestInterface $request, Tree $tree): ResponseInterface
    {
        $xref = $request->get('xref', '');

        $record = GedcomRecord::getInstance($xref, $tree);

        Auth::checkRecordAccess($record);

        if (Auth::isModerator($tree)) {
            DB::table('change')
                ->where('gedcom_id', '=', $record->tree()->id())
                ->where('xref', '=', $record->xref())
                ->where('status', '=', 'pending')
                ->update(['status' => 'rejected']);

            /* I18N: %s is the name of an individual, source or other record */
            FlashMessages::addMessage(I18N::translate('The changes to “%s” have been rejected.', $record->fullName()));
        }

        return response();
    }
448915
public function getPersonsAction(ServerRequestInterface $request, Tree $tree): ResponseInterface
    {
        $q        = $request->get('q', '');
        $instance = $request->get('instance', '');
        $treeview = new TreeView($instance);

        return response($treeview->getPersons($tree, $q));
    }
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);
    }
473386
public function addChildAction(ServerRequestInterface $request, Tree $tree): ResponseInterface
    {
        $xref = $request->get('xref', '');

        $family = Family::getInstance($xref, $tree);

        Auth::checkFamilyAccess($family, true);

        $PEDI      = $request->get('PEDI', '');
        $keep_chan = (bool) $request->get('keep_chan');

        $this->glevels = $request->get('glevels', []);
        $this->tag     = $request->get('tag', []);
        $this->text    = $request->get('text', []);
        $this->islink  = $request->get('islink', []);

        $this->splitSource();
        $gedrec = '0 @@ INDI';
        $gedrec .= $this->addNewName($request, $tree);
        $gedrec .= $this->addNewSex($request);
        if (preg_match_all('/([A-Z0-9_]+)/', $tree->getPreference('QUICK_REQUIRED_FACTS'), $matches)) {
            foreach ($matches[1] as $match) {
                $gedrec .= $this->addNewFact($request, $tree, $match);
            }
        }
        $gedrec .= "\n" . GedcomCodePedi::createNewFamcPedi($PEDI, $xref);
        if ((bool) $request->get('SOUR_INDI')) {
            $gedrec = $this->handleUpdates($gedrec);
        } else {
            $gedrec = $this->updateRest($gedrec);
        }

        // Create the new child
        $new_child = $tree->createIndividual($gedrec);

        // Insert new child at the right place
        $done = false;
        foreach ($family->facts(['CHIL']) as $fact) {
            $old_child = $fact->target();
            if ($old_child instanceof Individual && Date::compare($new_child->getEstimatedBirthDate(), $old_child->getEstimatedBirthDate()) < 0) {
                // Insert before this child
                $family->updateFact($fact->id(), '1 CHIL @' . $new_child->xref() . "@\n" . $fact->gedcom(), !$keep_chan);
                $done = true;
                break;
            }
        }
        if (!$done) {
            // Append child at end
            $family->createFact('1 CHIL @' . $new_child->xref() . '@', !$keep_chan);
        }

        if ($request->get('goto') === 'new') {
            return redirect($new_child->url());
        }

        return redirect($family->url());
    }
676429
public function treePrivacyUpdate(ServerRequestInterface $request, Tree $tree): ResponseInterface
    {
        $delete_default_resn_id = (array) $request->get('delete');

        DB::table('default_resn')
            ->whereIn('default_resn_id', $delete_default_resn_id)
            ->delete();

        $xrefs     = (array) $request->get('xref');
        $tag_types = (array) $request->get('tag_type');
        $resns     = (array) $request->get('resn');

        foreach ($xrefs as $n => $xref) {
            $tag_type = $tag_types[$n];
            $resn     = $resns[$n];

            // Delete any existing data
            if ($tag_type !== '' && $xref !== '') {
                DB::table('default_resn')
                    ->where('gedcom_id', '=', $tree->id())
                    ->where('tag_type', '=', $tag_type)
                    ->where('xref', '=', $xref)
                    ->delete();
            }

            if ($tag_type !== '' && $xref === '') {
                DB::table('default_resn')
                    ->where('gedcom_id', '=', $tree->id())
                    ->where('tag_type', '=', $tag_type)
                    ->whereNull('xref')
                    ->delete();
            }

            if ($tag_type === '' && $xref !== '') {
                DB::table('default_resn')
                    ->where('gedcom_id', '=', $tree->id())
                    ->whereNull('tag_type')
                    ->where('xref', '=', $xref)
                    ->delete();
            }

            // Add (or update) the new data
            if ($tag_type !== '' || $xref !== '') {
                DB::table('default_resn')->insert([
                    'gedcom_id' => $tree->id(),
                    'xref'      => $xref === '' ? null : $xref,
                    'tag_type'  => $tag_type === '' ? null : $tag_type,
                    'resn'      => $resn,
                ]);
            }
        }

        $tree->setPreference('HIDE_LIVE_PEOPLE', $request->get('HIDE_LIVE_PEOPLE'));
        $tree->setPreference('KEEP_ALIVE_YEARS_BIRTH', $request->get('KEEP_ALIVE_YEARS_BIRTH', '0'));
        $tree->setPreference('KEEP_ALIVE_YEARS_DEATH', $request->get('KEEP_ALIVE_YEARS_DEATH', '0'));
        $tree->setPreference('MAX_ALIVE_AGE', $request->get('MAX_ALIVE_AGE', '100'));
        $tree->setPreference('REQUIRE_AUTHENTICATION', $request->get('REQUIRE_AUTHENTICATION'));
        $tree->setPreference('SHOW_DEAD_PEOPLE', $request->get('SHOW_DEAD_PEOPLE'));
        $tree->setPreference('SHOW_LIVING_NAMES', $request->get('SHOW_LIVING_NAMES'));
        $tree->setPreference('SHOW_PRIVATE_RELATIONSHIPS', $request->get('SHOW_PRIVATE_RELATIONSHIPS'));

        FlashMessages::addMessage(I18N::translate('The preferences for the family tree “%s” have been updated.', e($tree->title()), 'success'));

        // Coming soon...
        if ((bool) $request->get('all_trees')) {
            FlashMessages::addMessage(I18N::translate('The preferences for all family trees have been updated.', e($tree->title())), 'success');
        }
        if ((bool) $request->get('new_trees')) {
            FlashMessages::addMessage(I18N::translate('The preferences for new family trees have been updated.', e($tree->title())), 'success');
        }

        return redirect(route('admin-trees', ['ged' => $tree->name()]));
    }
138991
public function mediaDownload(ServerRequestInterface $request, Tree $tree): ResponseInterface
    {
        $xref    = $request->get('xref', '');
        $fact_id = $request->get('fact_id');
        $media   = Media::getInstance($xref, $tree);

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

        if (!$media->canShow()) {
            throw new AccessDeniedHttpException();
        }

        foreach ($media->mediaFiles() as $media_file) {
            if ($media_file->factId() === $fact_id) {
                if ($media_file->isExternal()) {
                    return redirect($media_file->filename());
                }

                if (!$media_file->isImage() && $media_file->fileExists()) {
                    $data = file_get_contents($media_file->getServerFilename());

                    return response($data, StatusCodeInterface::STATUS_OK, [
                        'Content-Type' => $media_file->mimeType(),
                        'Content-Disposition' => 'attachment; filename="' . addcslashes($media_file->filename(), '"') . '"',
                    ]);
                }
            }
        }

        throw new NotFoundHttpException();
    }
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, '"') . '"');
    }
56259
public function getAddSourceAction(ServerRequestInterface $request, Tree $tree): ResponseInterface
    {
        $xref = $request->get('xref', '');

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

        if ($source === null) {
            throw new SourceNotFoundException();
        }

        $options = $this->sourceOptions($source);

        $title = I18N::translate('Add %s to the clippings cart', $source->fullName());

        return $this->viewResponse('modules/clippings/add-options', [
            'options' => $options,
            'default' => key($options),
            'record'  => $source,
            'title'   => $title,
            'tree'    => $tree,
        ]);
    }
601732
public function editNameAction(ServerRequestInterface $request, Tree $tree): ResponseInterface
    {
        $xref = $request->get('xref', '');

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

        Auth::checkIndividualAccess($individual, true);

        // @TODO - Move the name-specific code to this function?
        return app(EditGedcomRecordController::class)->updateFact($request, $tree);
    }
714388
public function editRawFact(ServerRequestInterface $request, Tree $tree): ResponseInterface
    {
        $xref    = $request->get('xref', '');
        $fact_id = $request->get('fact_id');
        $record  = GedcomRecord::getInstance($xref, $tree);

        Auth::checkRecordAccess($record, true);

        $title = I18N::translate('Edit the raw GEDCOM') . ' - ' . $record->fullName();

        foreach ($record->facts() as $fact) {
            if (!$fact->isPendingDeletion() && $fact->id() === $fact_id) {
                return $this->viewResponse('edit/raw-gedcom-fact', [
                    'pattern' => self::GEDCOM_FACT_REGEX,
                    'fact'    => $fact,
                    'title'   => $title,
                ]);
            }
        }

        return redirect($record->url());
    }
491786
public function editName(ServerRequestInterface $request, Tree $tree): ResponseInterface
    {
        $fact_id = $request->get('fact_id', '');
        $xref    = $request->get('xref', '');

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

        Auth::checkIndividualAccess($individual, true);

        // Find the fact to edit
        foreach ($individual->facts() as $fact) {
            if ($fact->id() === $fact_id && $fact->canEdit()) {
                return $this->viewResponse('edit/new-individual', [
                    'tree'       => $tree,
                    'title'      => I18N::translate('Edit the name'),
                    'nextaction' => 'update',
                    'individual' => $individual,
                    'family'     => null,
                    'name_fact'  => $fact,
                    'famtag'     => '',
                    'gender'     => $individual->sex(),
                ]);
            }
        }

        throw new NotFoundHttpException();
    }
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,
        ]);
    }
700901
public function createMediaObjectFromFileAction(ServerRequestInterface $request, Tree $tree): ResponseInterface
    {
        $file  = $request->get('file');
        $type  = $request->get('type');
        $title = $request->get('title');
        $note  = $request->get('note');

        if (preg_match('/\.([a-zA-Z0-9]+)$/', $file, $match)) {
            $format = ' ' . $match[1];
        } else {
            $format = '';
        }

        $gedcom = "0 @@ OBJE\n1 FILE " . $file . "\n2 FORM " . $format;

        if ($type !== '') {
            $gedcom .= "\n3 TYPE " . $type;
        }

        if ($title !== '') {
            $gedcom .= "\n2 TITL " . $title;
        }

        if ($note !== '') {
            $gedcom .= "\n1 NOTE " . preg_replace('/\r?\n/', "\n2 CONT ", $note);
        }

        $media_object = $tree->createRecord($gedcom);
        // Accept the new record.  Rejecting it would leave the filesystem out-of-sync with the genealogy
        FunctionsImport::acceptAllChanges($media_object->xref(), $tree);

        return redirect($media_object->url());
    }
331349
public function postAdminAction(ServerRequestInterface $request, UserInterface $user, Tree $tree = null): ResponseInterface
    {
        // We need a tree to work with.
        if ($tree === null) {
            throw new NotFoundHttpException();
        }

        // Admins only.
        if (!Auth::isAdmin($user)) {
            throw new AccessDeniedHttpException();
        }

        $plugin = $request->get('plugin', '');
        $update = $request->get('update', '');
        $xref   = $request->getParsedBody()['xref'] ?? '';

        $plugins = $this->getPluginList();
        $plugin  = $plugins[$plugin] ?? null;

        if ($plugin === null) {
            throw new NotFoundHttpException();
        }
        $plugin->getOptions($request);

        $all_data = $this->allData($plugin, $tree);

        $parameters = $request->getParsedBody();
        unset($parameters['csrf']);
        unset($parameters['update']);

        switch ($update) {
            case 'one':
                $record = $this->getRecord($all_data[$xref], $tree);
                if ($plugin->doesRecordNeedUpdate($record)) {
                    $new_gedcom = $plugin->updateRecord($record);
                    $record->updateRecord($new_gedcom, false);
                }

                $parameters['xref'] = $this->findNextXref($plugin, $xref, $all_data, $tree);
                break;

            case 'all':
                foreach ($all_data as $xref => $value) {
                    $record = $this->getRecord($value, $tree);
                    if ($plugin->doesRecordNeedUpdate($record)) {
                        $new_gedcom = $plugin->updateRecord($record);
                        $record->updateRecord($new_gedcom, false);
                    }
                }
                $parameters['xref'] = '';
                break;
        }

        $url = route('module', $parameters);

        return redirect($url);
    }
1321101
public function reorderChildrenAction(ServerRequestInterface $request, Tree $tree): ResponseInterface
    {
        $xref   = $request->get('xref', '');
        $order  = (array) $request->get('order', []);
        $family = Family::getInstance($xref, $tree);

        Auth::checkFamilyAccess($family, true);

        $dummy_facts = ['0 @' . $family->xref() . '@ FAM'];
        $sort_facts  = [];
        $keep_facts  = [];

        // Split facts into FAMS and other
        foreach ($family->facts() as $fact) {
            if ($fact->getTag() === 'CHIL') {
                $sort_facts[$fact->id()] = $fact->gedcom();
            } else {
                $keep_facts[] = $fact->gedcom();
            }
        }

        // Sort the facts
        uksort($sort_facts, static function ($x, $y) use ($order) {
            return array_search($x, $order, true) - array_search($y, $order, true);
        });

        // Merge the facts
        $gedcom = implode("\n", array_merge($dummy_facts, $sort_facts, $keep_facts));

        $family->updateRecord($gedcom, false);

        return redirect($family->url());
    }
825655
public function placesAction(ServerRequestInterface $request, Tree $tree): ResponseInterface
    {
        $search  = $request->get('search', '');
        $replace = $request->get('replace', '');

        $changes = $this->changePlacesUpdate($tree, $search, $replace);

        $feedback = I18N::translate('The following places have been changed:') . '<ul>';
        foreach ($changes as $old_place => $new_place) {
            $feedback .= '<li>' . e($old_place) . ' &rarr; ' . e($new_place) . '</li>';
        }
        $feedback .= '</ul>';

        FlashMessages::addMessage($feedback, 'success');

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

        return redirect($url);
    }