summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/Http/RequestHandlers/CheckTree.php4
-rw-r--r--app/Http/RequestHandlers/ExportGedcomClient.php109
-rw-r--r--app/Http/RequestHandlers/ExportGedcomPage.php22
-rw-r--r--app/Module/ClippingsCartModule.php111
-rw-r--r--app/Services/GedcomExportService.php145
-rw-r--r--phpstan-baseline.neon24
-rw-r--r--resources/views/admin/gedcom-export-options.phtml53
-rw-r--r--resources/views/admin/trees-export.phtml31
-rw-r--r--resources/views/modules/clippings/download.phtml10
-rw-r--r--tests/app/Http/RequestHandlers/UpgradeWizardStepTest.php23
-rw-r--r--tests/app/TreeTest.php3
11 files changed, 265 insertions, 270 deletions
diff --git a/app/Http/RequestHandlers/CheckTree.php b/app/Http/RequestHandlers/CheckTree.php
index 2db2af8cdb..1cd7167dfc 100644
--- a/app/Http/RequestHandlers/CheckTree.php
+++ b/app/Http/RequestHandlers/CheckTree.php
@@ -20,6 +20,7 @@ declare(strict_types=1);
namespace Fisharebest\Webtrees\Http\RequestHandlers;
use Fisharebest\Webtrees\Elements\AbstractXrefElement;
+use Fisharebest\Webtrees\Elements\MultimediaFileReference;
use Fisharebest\Webtrees\Elements\MultimediaFormat;
use Fisharebest\Webtrees\Elements\SubmitterText;
use Fisharebest\Webtrees\Elements\UnknownElement;
@@ -296,6 +297,9 @@ class CheckTree implements RequestHandlerInterface
$message = I18N::translate('webtrees cannot create thumbnails for this file format.');
$warnings[] = $this->lineError($tree, $record->type, $record->xref, $line_number, $line, $message);
}
+ } elseif ($element instanceof MultimediaFileReference && $value === 'gedcom.ged') {
+ $message = I18N::translate('This filename is not compatible with the GEDZIP file format.');
+ $errors[] = $this->lineError($tree, $record->type, $record->xref, $line_number, $line, $message);
}
}
diff --git a/app/Http/RequestHandlers/ExportGedcomClient.php b/app/Http/RequestHandlers/ExportGedcomClient.php
index 79b870b4f0..0980406c4f 100644
--- a/app/Http/RequestHandlers/ExportGedcomClient.php
+++ b/app/Http/RequestHandlers/ExportGedcomClient.php
@@ -19,36 +19,19 @@ declare(strict_types=1);
namespace Fisharebest\Webtrees\Http\RequestHandlers;
-use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Encodings\ANSEL;
use Fisharebest\Webtrees\Encodings\ASCII;
use Fisharebest\Webtrees\Encodings\UTF16BE;
use Fisharebest\Webtrees\Encodings\UTF8;
use Fisharebest\Webtrees\Encodings\Windows1252;
-use Fisharebest\Webtrees\GedcomRecord;
use Fisharebest\Webtrees\Http\ViewResponseTrait;
-use Fisharebest\Webtrees\Registry;
use Fisharebest\Webtrees\Services\GedcomExportService;
use Fisharebest\Webtrees\Validator;
-use Illuminate\Database\Capsule\Manager as DB;
-use League\Flysystem\Filesystem;
use League\Flysystem\FilesystemException;
-use League\Flysystem\ZipArchive\FilesystemZipArchiveProvider;
-use League\Flysystem\ZipArchive\ZipArchiveAdapter;
-use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
-use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Server\RequestHandlerInterface;
-use function addcslashes;
-use function fclose;
-use function pathinfo;
-use function strtolower;
-use function tmpfile;
-
-use const PATHINFO_EXTENSION;
-
/**
* Download a GEDCOM file to the client.
*/
@@ -58,108 +41,30 @@ class ExportGedcomClient implements RequestHandlerInterface
private GedcomExportService $gedcom_export_service;
- private ResponseFactoryInterface $response_factory;
-
- private StreamFactoryInterface $stream_factory;
-
/**
* ExportGedcomServer constructor.
*
- * @param GedcomExportService $gedcom_export_service
- * @param ResponseFactoryInterface $response_factory
- * @param StreamFactoryInterface $stream_factory
+ * @param GedcomExportService $gedcom_export_service
*/
- public function __construct(
- GedcomExportService $gedcom_export_service,
- ResponseFactoryInterface $response_factory,
- StreamFactoryInterface $stream_factory
- ) {
+ public function __construct(GedcomExportService $gedcom_export_service)
+ {
$this->gedcom_export_service = $gedcom_export_service;
- $this->response_factory = $response_factory;
- $this->stream_factory = $stream_factory;
}
/**
* @param ServerRequestInterface $request
*
* @return ResponseInterface
- * @throws FilesystemException
*/
public function handle(ServerRequestInterface $request): ResponseInterface
{
- $tree = Validator::attributes($request)->tree();
-
- $data_filesystem = Registry::filesystem()->data();
-
- $format = Validator::parsedBody($request)->isInArray(['gedcom', 'zip'])->string('format');
+ $tree = Validator::attributes($request)->tree();
+ $filename = Validator::parsedBody($request)->string('filename');
+ $format = Validator::parsedBody($request)->isInArray(['gedcom', 'zip', 'zipmedia', 'gedzip'])->string('format');
$privacy = Validator::parsedBody($request)->isInArray(['none', 'gedadmin', 'user', 'visitor'])->string('privacy');
$encoding = Validator::parsedBody($request)->isInArray([UTF8::NAME, UTF16BE::NAME, ANSEL::NAME, ASCII::NAME, Windows1252::NAME])->string('encoding');
$line_endings = Validator::parsedBody($request)->isInArray(['CRLF', 'LF'])->string('line_endings');
- $media_path = Validator::parsedBody($request)->string('media_path', '');
-
- $access_levels = [
- 'gedadmin' => Auth::PRIV_NONE,
- 'user' => Auth::PRIV_USER,
- 'visitor' => Auth::PRIV_PRIVATE,
- 'none' => Auth::PRIV_HIDE,
- ];
-
- $access_level = $access_levels[$privacy];
-
- // What to call the downloaded file
- $download_filename = $tree->name();
-
- // Force a ".ged" suffix
- if (strtolower(pathinfo($download_filename, PATHINFO_EXTENSION)) !== 'ged') {
- $download_filename .= '.ged';
- }
-
- if ($format === 'zip') {
- $resource = $this->gedcom_export_service->export($tree, true, $encoding, $access_level, $media_path, $line_endings);
-
- $path = $tree->getPreference('MEDIA_DIRECTORY');
-
- // Create a new/empty .ZIP file
- $temp_zip_file = stream_get_meta_data(tmpfile())['uri'];
- $zip_provider = new FilesystemZipArchiveProvider($temp_zip_file, 0755);
- $zip_adapter = new ZipArchiveAdapter($zip_provider);
- $zip_filesystem = new Filesystem($zip_adapter);
- $zip_filesystem->writeStream($download_filename, $resource);
- fclose($resource);
-
- $media_filesystem = $tree->mediaFilesystem($data_filesystem);
-
- $records = DB::table('media')
- ->where('m_file', '=', $tree->id())
- ->get()
- ->map(Registry::mediaFactory()->mapper($tree))
- ->filter(GedcomRecord::accessFilter());
-
- foreach ($records as $record) {
- foreach ($record->mediaFiles() as $media_file) {
- $from = $media_file->filename();
- $to = $path . $media_file->filename();
- if (!$media_file->isExternal() && $media_filesystem->fileExists($from) && !$zip_filesystem->fileExists($to)) {
- $zip_filesystem->writeStream($to, $media_filesystem->readStream($from));
- }
- }
- }
-
- $stream = $this->stream_factory->createStreamFromFile($temp_zip_file);
- $filename = addcslashes($download_filename, '"') . '.zip';
-
- return $this->response_factory->createResponse()
- ->withBody($stream)
- ->withHeader('content-type', 'application/zip')
- ->withHeader('content-disposition', 'attachment; filename="' . $filename . '"');
- }
-
- $resource = $this->gedcom_export_service->export($tree, true, $encoding, $access_level, $media_path);
- $stream = $this->stream_factory->createStreamFromResource($resource);
- return $this->response_factory->createResponse()
- ->withBody($stream)
- ->withHeader('content-type', 'text/x-gedcom; charset=' . UTF8::NAME)
- ->withHeader('content-disposition', 'attachment; filename="' . addcslashes($download_filename, '"') . '"');
+ return $this->gedcom_export_service->downloadResponse($tree, true, $encoding, $privacy, $line_endings, $filename, $format);
}
}
diff --git a/app/Http/RequestHandlers/ExportGedcomPage.php b/app/Http/RequestHandlers/ExportGedcomPage.php
index 73cebda5aa..0d75560ce7 100644
--- a/app/Http/RequestHandlers/ExportGedcomPage.php
+++ b/app/Http/RequestHandlers/ExportGedcomPage.php
@@ -26,9 +26,12 @@ use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
+use function date;
use function e;
+use function extension_loaded;
use function pathinfo;
use function strtolower;
+use function substr;
use const PATHINFO_EXTENSION;
@@ -54,14 +57,23 @@ class ExportGedcomPage implements RequestHandlerInterface
$filename = $tree->name();
// Force a ".ged" suffix
- if (strtolower(pathinfo($filename, PATHINFO_EXTENSION)) !== 'ged') {
- $filename .= '.ged';
+ if (strtolower(pathinfo($filename, PATHINFO_EXTENSION)) === 'ged') {
+ $download_filename = substr($filename, 0, -4);
+ } else {
+ $download_filename = $filename;
}
+ $download_filenames = [
+ $download_filename => $download_filename,
+ $download_filename . date('-Y-m-d') => $download_filename . date('-Y-m-d'),
+ ];
+
return $this->viewResponse('admin/trees-export', [
- 'filename' => $filename,
- 'title' => $title,
- 'tree' => $tree,
+ 'download_filenames' => $download_filenames,
+ 'filename' => $filename,
+ 'title' => $title,
+ 'tree' => $tree,
+ 'zip_available' => extension_loaded('zip'),
]);
}
}
diff --git a/app/Module/ClippingsCartModule.php b/app/Module/ClippingsCartModule.php
index a6acc75ad1..db08e13b51 100644
--- a/app/Module/ClippingsCartModule.php
+++ b/app/Module/ClippingsCartModule.php
@@ -52,14 +52,9 @@ use Fisharebest\Webtrees\Submitter;
use Fisharebest\Webtrees\Tree;
use Fisharebest\Webtrees\Validator;
use Illuminate\Support\Collection;
-use League\Flysystem\Filesystem;
use League\Flysystem\FilesystemException;
-use League\Flysystem\ZipArchive\FilesystemZipArchiveProvider;
-use League\Flysystem\ZipArchive\ZipArchiveAdapter;
-use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
-use Psr\Http\Message\StreamFactoryInterface;
use function app;
use function array_filter;
@@ -67,7 +62,8 @@ use function array_keys;
use function array_map;
use function array_search;
use function assert;
-use function fclose;
+use function date;
+use function extension_loaded;
use function in_array;
use function is_array;
use function is_string;
@@ -75,8 +71,6 @@ use function preg_match_all;
use function redirect;
use function route;
use function str_replace;
-use function stream_get_meta_data;
-use function tmpfile;
use function uasort;
use function view;
@@ -118,28 +112,18 @@ class ClippingsCartModule extends AbstractModule implements ModuleMenuInterface
private LinkedRecordService $linked_record_service;
- private ResponseFactoryInterface $response_factory;
-
- private StreamFactoryInterface $stream_factory;
-
/**
* ClippingsCartModule constructor.
*
- * @param GedcomExportService $gedcom_export_service
- * @param LinkedRecordService $linked_record_service
- * @param ResponseFactoryInterface $response_factory
- * @param StreamFactoryInterface $stream_factory
+ * @param GedcomExportService $gedcom_export_service
+ * @param LinkedRecordService $linked_record_service
*/
public function __construct(
GedcomExportService $gedcom_export_service,
- LinkedRecordService $linked_record_service,
- ResponseFactoryInterface $response_factory,
- StreamFactoryInterface $stream_factory
+ LinkedRecordService $linked_record_service
) {
$this->gedcom_export_service = $gedcom_export_service;
$this->linked_record_service = $linked_record_service;
- $this->response_factory = $response_factory;
- $this->stream_factory = $stream_factory;
}
/**
@@ -257,10 +241,17 @@ class ClippingsCartModule extends AbstractModule implements ModuleMenuInterface
$title = I18N::translate('Family tree clippings cart') . ' — ' . I18N::translate('Download');
+ $download_filenames = [
+ 'clippings' => 'clippings',
+ 'clippings-' . date('Y-m-d') => 'clippings-' . date('Y-m-d'),
+ ];
+
return $this->viewResponse('modules/clippings/download', [
- 'module' => $this->name(),
- 'title' => $title,
- 'tree' => $tree,
+ 'download_filenames' => $download_filenames,
+ 'module' => $this->name(),
+ 'title' => $title,
+ 'tree' => $tree,
+ 'zip_available' => extension_loaded('zip'),
]);
}
@@ -274,25 +265,22 @@ class ClippingsCartModule extends AbstractModule implements ModuleMenuInterface
{
$tree = Validator::attributes($request)->tree();
- $data_filesystem = Registry::filesystem()->data();
+ if (Auth::isAdmin()) {
+ $privacy_options = ['none', 'gedadmin', 'user', 'visitor'];
+ } elseif (Auth::isManager($tree)) {
+ $privacy_options = ['gedadmin', 'user', 'visitor'];
+ } elseif (Auth::isMember($tree)) {
+ $privacy_options = ['user', 'visitor'];
+ } else {
+ $privacy_options = ['visitor'];
+ }
- $format = Validator::parsedBody($request)->isInArray(['gedcom', 'zip'])->string('format');
- $privacy = Validator::parsedBody($request)->isInArray(['none', 'gedadmin', 'user', 'visitor'])->string('privacy');
+ $filename = Validator::parsedBody($request)->string('filename');
+ $format = Validator::parsedBody($request)->isInArray(['gedcom', 'zip', 'zipmedia', 'gedzip'])->string('format');
+ $privacy = Validator::parsedBody($request)->isInArray($privacy_options)->string('privacy');
$encoding = Validator::parsedBody($request)->isInArray([UTF8::NAME, UTF16BE::NAME, ANSEL::NAME, ASCII::NAME, Windows1252::NAME])->string('encoding');
$line_endings = Validator::parsedBody($request)->isInArray(['CRLF', 'LF'])->string('line_endings');
- if ($privacy === 'none' && !Auth::isManager($tree)) {
- $privacy = 'member';
- }
-
- if ($privacy === 'gedadmin' && !Auth::isManager($tree)) {
- $privacy = 'member';
- }
-
- if ($privacy === 'user' && !Auth::isMember($tree)) {
- $privacy = 'visitor';
- }
-
$cart = Session::get('cart');
$cart = is_array($cart) ? $cart : [];
@@ -346,51 +334,8 @@ class ClippingsCartModule extends AbstractModule implements ModuleMenuInterface
}
}
- // Media file prefix
- $path = $tree->getPreference('MEDIA_DIRECTORY');
-
// We have already applied privacy filtering, so do not do it again.
- $resource = $this->gedcom_export_service->export($tree, false, $encoding, Auth::PRIV_HIDE, $path, $line_endings, $records);
-
- if ($format === 'gedcom') {
- return $this->response_factory->createResponse()
- ->withBody($this->stream_factory->createStreamFromResource($resource))
- ->withHeader('content-type', 'text/x-gedcom; charset=' . $encoding)
- ->withHeader('content-disposition', 'attachment; filename="clippings.ged');
- }
-
- // Create a new/empty .ZIP file
- $temp_zip_file = stream_get_meta_data(tmpfile())['uri'];
- $zip_provider = new FilesystemZipArchiveProvider($temp_zip_file, 0755);
- $zip_adapter = new ZipArchiveAdapter($zip_provider);
- $zip_filesystem = new Filesystem($zip_adapter);
-
- $media_filesystem = $tree->mediaFilesystem($data_filesystem);
-
- foreach ($records as $record) {
- if ($record instanceof Media) {
- // Add the media files to the archive
- foreach ($record->mediaFiles() as $media_file) {
- $from = $media_file->filename();
- $to = $path . $media_file->filename();
- if (!$media_file->isExternal() && $media_filesystem->fileExists($from)) {
- $zip_filesystem->writeStream($to, $media_filesystem->readStream($from));
- }
- }
- }
- }
-
- // Finally, add the GEDCOM file to the .ZIP file.
- $zip_filesystem->writeStream('clippings.ged', $resource);
- fclose($resource);
-
- // Use a stream, so that we do not have to load the entire file into memory.
- $resource = $this->stream_factory->createStreamFromFile($temp_zip_file);
-
- return $this->response_factory->createResponse()
- ->withBody($resource)
- ->withHeader('content-type', 'application/zip')
- ->withHeader('content-disposition', 'attachment; filename="clippings.zip');
+ return $this->gedcom_export_service->downloadResponse($tree, false, $encoding, 'none', $line_endings, $filename, $format, $records);
}
/**
diff --git a/app/Services/GedcomExportService.php b/app/Services/GedcomExportService.php
index 859988fb2c..8aeaedace0 100644
--- a/app/Services/GedcomExportService.php
+++ b/app/Services/GedcomExportService.php
@@ -36,23 +36,35 @@ use Illuminate\Database\Capsule\Manager as DB;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\Expression;
use Illuminate\Support\Collection;
+use League\Flysystem\Filesystem;
+use League\Flysystem\FilesystemOperator;
+use League\Flysystem\ZipArchive\FilesystemZipArchiveProvider;
+use League\Flysystem\ZipArchive\ZipArchiveAdapter;
+use Psr\Http\Message\ResponseFactoryInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\StreamFactoryInterface;
use RuntimeException;
+use function addcslashes;
use function date;
use function explode;
+use function fclose;
use function fopen;
use function fwrite;
use function pathinfo;
+use function preg_match_all;
use function rewind;
use function str_contains;
-use function str_starts_with;
use function stream_filter_append;
+use function stream_get_meta_data;
use function strlen;
use function strpos;
use function strtolower;
use function strtoupper;
+use function tmpfile;
use const PATHINFO_EXTENSION;
+use const PREG_SET_ORDER;
use const STREAM_FILTER_WRITE;
/**
@@ -60,16 +72,105 @@ use const STREAM_FILTER_WRITE;
*/
class GedcomExportService
{
+ private const ACCESS_LEVELS = [
+ 'gedadmin' => Auth::PRIV_NONE,
+ 'user' => Auth::PRIV_USER,
+ 'visitor' => Auth::PRIV_PRIVATE,
+ 'none' => Auth::PRIV_HIDE,
+ ];
+
+ private ResponseFactoryInterface $response_factory;
+
+ private StreamFactoryInterface $stream_factory;
+
+ /**
+ * @param ResponseFactoryInterface $response_factory
+ * @param StreamFactoryInterface $stream_factory
+ */
+ public function __construct(ResponseFactoryInterface $response_factory, StreamFactoryInterface $stream_factory)
+ {
+ $this->response_factory = $response_factory;
+ $this->stream_factory = $stream_factory;
+ }
+
+ /**
+ * @param Tree $tree - Export data from this tree
+ * @param bool $sort_by_xref - Write GEDCOM records in XREF order
+ * @param string $encoding - Convert from UTF-8 to other encoding
+ * @param string $privacy - Filter records by role
+ * @param string $filename - Name of download file, without an extension
+ * @param string $format - One of: gedcom, zip, zipmedia, gedzip
+ *
+ * @return ResponseInterface
+ */
+ public function downloadResponse(
+ Tree $tree,
+ bool $sort_by_xref,
+ string $encoding,
+ string $privacy,
+ string $line_endings,
+ string $filename,
+ string $format,
+ Collection $records = null
+ ): ResponseInterface {
+ $access_level = self::ACCESS_LEVELS[$privacy];
+
+ if ($format === 'gedcom') {
+ $resource = $this->export($tree, $sort_by_xref, $encoding, $access_level, $line_endings, $records);
+ $stream = $this->stream_factory->createStreamFromResource($resource);
+
+ return $this->response_factory->createResponse()
+ ->withBody($stream)
+ ->withHeader('content-type', 'text/x-gedcom; charset=' . UTF8::NAME)
+ ->withHeader('content-disposition', 'attachment; filename="' . addcslashes($filename, '"') . '.ged"');
+ }
+
+ // Create a new/empty .ZIP file
+ $temp_zip_file = stream_get_meta_data(tmpfile())['uri'];
+ $zip_provider = new FilesystemZipArchiveProvider($temp_zip_file, 0755);
+ $zip_adapter = new ZipArchiveAdapter($zip_provider);
+ $zip_filesystem = new Filesystem($zip_adapter);
+
+ if ($format === 'zipmedia') {
+ $media_path = $tree->getPreference('MEDIA_DIRECTORY');
+ } elseif ($format === 'gedzip') {
+ $media_path = '';
+ } else {
+ // Don't add media
+ $media_path = null;
+ }
+
+ $resource = $this->export($tree, $sort_by_xref, $encoding, $access_level, $line_endings, $records, $zip_filesystem, $media_path);
+
+ if ($format === 'gedzip') {
+ $zip_filesystem->writeStream('gedcom.ged', $resource);
+ $extension = '.gdz';
+ } else {
+ $zip_filesystem->writeStream($filename . '.ged', $resource);
+ $extension = '.zip';
+ }
+
+ fclose($resource);
+
+ $stream = $this->stream_factory->createStreamFromFile($temp_zip_file);
+
+ return $this->response_factory->createResponse()
+ ->withBody($stream)
+ ->withHeader('content-type', 'application/zip')
+ ->withHeader('content-disposition', 'attachment; filename="' . addcslashes($filename, '"') . $extension . '"');
+ }
+
/**
* Write GEDCOM data to a stream.
*
- * @param Tree $tree - Export data from this tree
- * @param bool $sort_by_xref - Write GEDCOM records in XREF order
- * @param string $encoding - Convert from UTF-8 to other encoding
- * @param int $access_level - Apply privacy filtering
- * @param string $media_path - Prepend path to media filenames
- * @param string $line_endings - CRLF or LF
- * @param Collection<int,string>|null $records - Just export these records
+ * @param Tree $tree - Export data from this tree
+ * @param bool $sort_by_xref - Write GEDCOM records in XREF order
+ * @param string $encoding - Convert from UTF-8 to other encoding
+ * @param int $access_level - Apply privacy filtering
+ * @param string $line_endings - CRLF or LF
+ * @param Collection<int,string>|null $records - Just export these records
+ * @param FilesystemOperator|null $zip_filesystem - Write media files to this filesystem
+ * @param string|null $media_path - Location within the zip filesystem
*
* @return resource
*/
@@ -78,9 +179,10 @@ class GedcomExportService
bool $sort_by_xref = false,
string $encoding = UTF8::NAME,
int $access_level = Auth::PRIV_HIDE,
- string $media_path = '',
string $line_endings = 'CRLF',
- Collection $records = null
+ Collection $records = null,
+ FilesystemOperator $zip_filesystem = null,
+ string $media_path = null
) {
$stream = fopen('php://memory', 'wb+');
@@ -125,6 +227,8 @@ class GedcomExportService
];
}
+ $media_filesystem = Registry::filesystem()->media($tree);
+
foreach ($data as $rows) {
foreach ($rows as $datum) {
if (is_string($datum)) {
@@ -140,8 +244,16 @@ class GedcomExportService
$datum->o_gedcom;
}
- if ($media_path !== '') {
- $gedcom = $this->convertMediaPath($gedcom, $media_path);
+ if ($media_path !== null && $zip_filesystem !== null && preg_match('/0 @' . Gedcom::REGEX_XREF . '@ OBJE/', $gedcom) === 1) {
+ preg_match_all('/\n1 FILE (.+)/', $gedcom, $matches, PREG_SET_ORDER);
+
+ foreach ($matches as $match) {
+ $media_file = $match[1];
+
+ if ($media_filesystem->fileExists($media_file)) {
+ $zip_filesystem->writeStream($media_path . $media_file, $media_filesystem->readStream($media_file));
+ }
+ }
}
$gedcom = $this->wrapLongLines($gedcom, Gedcom::LINE_LENGTH) . "\n";
@@ -235,14 +347,7 @@ class GedcomExportService
// Don’t modify external links
if (!str_contains($filename, '://')) {
- // Convert separators to match new path.
- if (str_contains($media_path, '\\')) {
- $filename = strtr($filename, ['/' => '\\']);
- }
-
- if (!str_starts_with($filename, $media_path)) {
- $filename = $media_path . $filename;
- }
+ $filename = $media_path . $filename;
}
return "\n1 FILE " . $filename;
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index 0f76568697..53e39fdb07 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -512,12 +512,12 @@ parameters:
-
message: "#^Cannot access property \\$type on mixed\\.$#"
- count: 46
+ count: 47
path: app/Http/RequestHandlers/CheckTree.php
-
message: "#^Cannot access property \\$xref on mixed\\.$#"
- count: 45
+ count: 46
path: app/Http/RequestHandlers/CheckTree.php
-
@@ -591,16 +591,6 @@ parameters:
path: app/Http/RequestHandlers/EditNoteAction.php
-
- message: "#^Cannot call method mediaFiles\\(\\) on mixed\\.$#"
- count: 1
- path: app/Http/RequestHandlers/ExportGedcomClient.php
-
- -
- message: "#^Parameter \\#1 \\$fp of function stream_get_meta_data expects resource, resource\\|false given\\.$#"
- count: 1
- path: app/Http/RequestHandlers/ExportGedcomClient.php
-
- -
message: "#^Parameter \\#1 \\$content of function response expects array\\|object\\|string, string\\|false given\\.$#"
count: 1
path: app/Http/RequestHandlers/FaviconIco.php
@@ -1236,11 +1226,6 @@ parameters:
path: app/Module/CensusAssistantModule.php
-
- message: "#^Parameter \\#1 \\$fp of function stream_get_meta_data expects resource, resource\\|false given\\.$#"
- count: 1
- path: app/Module/ClippingsCartModule.php
-
- -
message: "#^Argument of an invalid type array\\<int, string\\>\\|false supplied for foreach, only iterables are supported\\.$#"
count: 1
path: app/Module/CloudsTheme.php
@@ -2571,6 +2556,11 @@ parameters:
path: app/Services/GedcomExportService.php
-
+ message: "#^Parameter \\#1 \\$fp of function stream_get_meta_data expects resource, resource\\|false given\\.$#"
+ count: 1
+ path: app/Services/GedcomExportService.php
+
+ -
message: "#^Method Fisharebest\\\\Webtrees\\\\Services\\\\GedcomImportService\\:\\:createMediaObject\\(\\) should return string but returns mixed\\.$#"
count: 1
path: app/Services/GedcomImportService.php
diff --git a/resources/views/admin/gedcom-export-options.phtml b/resources/views/admin/gedcom-export-options.phtml
index 2ba1f8d77a..0cefd2aa55 100644
--- a/resources/views/admin/gedcom-export-options.phtml
+++ b/resources/views/admin/gedcom-export-options.phtml
@@ -7,9 +7,12 @@ use Fisharebest\Webtrees\Encodings\UTF16BE;
use Fisharebest\Webtrees\Encodings\UTF8;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Tree;
+use Fisharebest\Webtrees\View;
/**
- * @var Tree $tree
+ * @var array<string> $download_filenames
+ * @var Tree $tree
+ * @var bool $zip_available
*/
?>
@@ -21,7 +24,7 @@ use Fisharebest\Webtrees\Tree;
<div class="col-sm-9">
<div class="form-check">
- <input class="form-check-input" type="radio" name="format" id="format-gedcom" value="gedcom" checked="checked">
+ <input class="form-check-input" type="radio" name="format" id="format-gedcom" value="gedcom" checked="checked" data-wt-extension=".ged">
<label class="form-check-label" for="format-gedcom">
<?= I18N::translate('GEDCOM') ?>
@@ -29,16 +32,47 @@ use Fisharebest\Webtrees\Tree;
</div>
<div class="form-check">
- <input class="form-check-input" type="radio" name="format" id="format-zip" value="zip">
+ <input class="form-check-input" type="radio" name="format" id="format-zip" value="zip" data-wt-extension=".zip" <?= $zip_available ? '' : 'disabled="disabled"'?>>
<label class="form-check-label" for="format-zip">
- <?= /* I18N: ZIP = file format */ I18N::translate('ZIP (includes media files)') ?>
+ <?= /* I18N: ZIP = file format */ I18N::translate('ZIP') ?>
+ </label>
+ </div>
+
+ <div class="form-check">
+ <input class="form-check-input" type="radio" name="format" id="format-zipmedia" value="zipmedia" data-wt-extension=".zip" <?= $zip_available ? '' : 'disabled="disabled"'?>>
+
+ <label class="form-check-label" for="format-zipmedia">
+ <?= /* I18N: ZIP = file format */ I18N::translate('ZIP') ?>
+ <?= I18N::translate('(includes media files)') ?>
+ </label>
+ </div>
+
+ <div class="form-check">
+ <input class="form-check-input" type="radio" name="format" id="format-gdz" value="gedzip" data-wt-extension=".gdz" <?= $zip_available ? '' : 'disabled="disabled"'?>>
+
+ <label class="form-check-label" for="format-gdz">
+ <?= /* I18N: GEDZIP = file format */ I18N::translate('GEDZIP') ?>
+ <?= I18N::translate('(includes media files)') ?>
</label>
</div>
</div>
</div>
<div class="row mb-3">
+ <label class="col-sm-3" for="filename">
+ <?= I18N::translate('Filename') ?>
+ </label>
+
+ <div class="col-sm-9">
+ <div class="input-group">
+ <?= view('components/select', ['name' => 'filename', 'id' => 'filename', 'selected' => 0, 'options' => $download_filenames]) ?>
+ <span class="input-group-text" id="extension">.ged</span>
+ </div>
+ </div>
+</div>
+
+<div class="row mb-3">
<label class="col-sm-3" for="encoding">
<?= I18N::translate('Character encoding') ?>
</label>
@@ -76,3 +110,14 @@ use Fisharebest\Webtrees\Tree;
</div>
</div>
+<?php View::push('javascript') ?>
+<script>
+ 'use strict';
+
+ document.querySelectorAll('[name=format]').forEach(element => element.addEventListener('change', event => document.getElementById('extension').innerText = event.target.dataset.wtExtension));
+
+ // Firefox will reload the page and change the selected item.
+ document.getElementById('extension').innerText = document.querySelector('[name=format]:checked').dataset.wtExtension;
+
+</script>
+<?php View::endpush(); ?>
diff --git a/resources/views/admin/trees-export.phtml b/resources/views/admin/trees-export.phtml
index 8fbc4e51a9..94515445ae 100644
--- a/resources/views/admin/trees-export.phtml
+++ b/resources/views/admin/trees-export.phtml
@@ -8,9 +8,11 @@ use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Tree;
/**
- * @var string $filename
- * @var string $title
- * @var Tree $tree
+ * @var array<string> $download_filenames
+ * @var string $filename
+ * @var string $title
+ * @var Tree $tree
+ * @var bool $zip_available
*/
?>
@@ -38,7 +40,7 @@ use Fisharebest\Webtrees\Tree;
<button type="submit" class="btn btn-primary">
<?= view('icons/save') ?>
<?= /* I18N: A button label. */
- I18N::translate('continue') ?>
+ I18N::translate('save') ?>
</button>
</form>
</div>
@@ -59,29 +61,12 @@ use Fisharebest\Webtrees\Tree;
<?= I18N::translate('Export preferences') ?>
</p>
- <?= view('admin/gedcom-export-options', ['tree' => $tree]) ?>
-
- <?php if ($tree->getPreference('GEDCOM_MEDIA_PATH') !== '') : ?>
- <div class="row mb-3">
- <label class="col-sm-3" for="media-path">
- <?= /* I18N: A media path (e.g. C:\aaa\bbb\ccc\) in a GEDCOM file */ I18N::translate('Add the GEDCOM media path to filenames') ?>
- </label>
-
- <div class="col-sm-9">
- <div class="form-check">
- <input class="form-check-input" type="checkbox" id="media-path" name="media_path">
- <label class="form-check-label" for="media-path">
- <?= /* I18N: %s is the name of a folder. */ I18N::translate('Media filenames will be prefixed by %s.', '<code dir="ltr">' . e($tree->getPreference('GEDCOM_MEDIA_PATH')) . '</code>') ?>
- </label>
- </div>
- </div>
- </div>
- <?php endif ?>
+ <?= view('admin/gedcom-export-options', ['download_filenames' =>$download_filenames, 'tree' => $tree, 'zip_available' => $zip_available]) ?>
<button type="submit" class="btn btn-primary">
<?= view('icons/download') ?>
<?= /* I18N: A button label. */
- I18N::translate('continue') ?>
+ I18N::translate('download') ?>
</button>
</form>
</div>
diff --git a/resources/views/modules/clippings/download.phtml b/resources/views/modules/clippings/download.phtml
index e411ee2d56..695e11130b 100644
--- a/resources/views/modules/clippings/download.phtml
+++ b/resources/views/modules/clippings/download.phtml
@@ -4,9 +4,11 @@ use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Tree;
/**
- * @var string $module
- * @var string $title
- * @var Tree $tree
+ * @var array<string> $download_filenames
+ * @var string $module
+ * @var string $title
+ * @var Tree $tree
+ * @var bool $zip_available
*/
?>
@@ -21,7 +23,7 @@ use Fisharebest\Webtrees\Tree;
<?= I18N::translate('Export preferences') ?>
</div>
<div class="col-sm-9 wt-page-options-value">
- <?= view('admin/gedcom-export-options', ['tree' => $tree]) ?>
+ <?= view('admin/gedcom-export-options', ['download_filenames' =>$download_filenames, 'tree' => $tree, 'zip_available' => $zip_available]) ?>
</div>
</div>
diff --git a/tests/app/Http/RequestHandlers/UpgradeWizardStepTest.php b/tests/app/Http/RequestHandlers/UpgradeWizardStepTest.php
index b8f7425699..059e4b4eb6 100644
--- a/tests/app/Http/RequestHandlers/UpgradeWizardStepTest.php
+++ b/tests/app/Http/RequestHandlers/UpgradeWizardStepTest.php
@@ -32,6 +32,7 @@ use Fisharebest\Webtrees\Services\UpgradeService;
use Fisharebest\Webtrees\Services\UserService;
use Fisharebest\Webtrees\TestCase;
use Illuminate\Support\Collection;
+use Nyholm\Psr7\Factory\Psr17Factory;
/**
* Test UpgradeController class.
@@ -48,7 +49,7 @@ class UpgradeWizardStepTest extends TestCase
public function testIgnoreStepInvalid(): void
{
$handler = new UpgradeWizardStep(
- new GedcomExportService(),
+ new GedcomExportService(new Psr17Factory(), new Psr17Factory()),
new TreeService(new GedcomImportService()),
new UpgradeService(new TimeoutService())
);
@@ -68,7 +69,7 @@ class UpgradeWizardStepTest extends TestCase
$mock_upgrade_service = $this->createMock(UpgradeService::class);
$mock_upgrade_service->method('latestVersion')->willReturn('999.999.999');
$handler = new UpgradeWizardStep(
- new GedcomExportService(),
+ new GedcomExportService(new Psr17Factory(), new Psr17Factory()),
new TreeService(new GedcomImportService()),
$mock_upgrade_service
);
@@ -89,7 +90,7 @@ class UpgradeWizardStepTest extends TestCase
$mock_upgrade_service = $this->createMock(UpgradeService::class);
$mock_upgrade_service->method('latestVersion')->willReturn('');
$handler = new UpgradeWizardStep(
- new GedcomExportService(),
+ new GedcomExportService(new Psr17Factory(), new Psr17Factory()),
new TreeService(new GedcomImportService()),
$mock_upgrade_service
);
@@ -108,7 +109,7 @@ class UpgradeWizardStepTest extends TestCase
$mock_upgrade_service = $this->createMock(UpgradeService::class);
$mock_upgrade_service->method('latestVersion')->willReturn('0.0.0');
$handler = new UpgradeWizardStep(
- new GedcomExportService(),
+ new GedcomExportService(new Psr17Factory(), new Psr17Factory()),
new TreeService(new GedcomImportService()),
$mock_upgrade_service
);
@@ -123,7 +124,7 @@ class UpgradeWizardStepTest extends TestCase
public function testStepPrepare(): void
{
$handler = new UpgradeWizardStep(
- new GedcomExportService(),
+ new GedcomExportService(new Psr17Factory(), new Psr17Factory()),
new TreeService(new GedcomImportService()),
new UpgradeService(new TimeoutService())
);
@@ -140,7 +141,7 @@ class UpgradeWizardStepTest extends TestCase
public function testStepPending(): void
{
$handler = new UpgradeWizardStep(
- new GedcomExportService(),
+ new GedcomExportService(new Psr17Factory(), new Psr17Factory()),
new TreeService(new GedcomImportService()),
new UpgradeService(new TimeoutService())
);
@@ -164,7 +165,7 @@ class UpgradeWizardStepTest extends TestCase
$tree->createIndividual("0 @@ INDI\n1 NAME Joe Bloggs");
$handler = new UpgradeWizardStep(
- new GedcomExportService(),
+ new GedcomExportService(new Psr17Factory(), new Psr17Factory()),
new TreeService(new GedcomImportService()),
new UpgradeService(new TimeoutService())
);
@@ -186,7 +187,7 @@ class UpgradeWizardStepTest extends TestCase
$tree_service->method('all')->willReturn($all_trees);
$handler = new UpgradeWizardStep(
- new GedcomExportService(),
+ new GedcomExportService(new Psr17Factory(), new Psr17Factory()),
$tree_service,
new UpgradeService(new TimeoutService())
);
@@ -212,7 +213,7 @@ class UpgradeWizardStepTest extends TestCase
$mock_upgrade_service = $this->createMock(UpgradeService::class);
$mock_upgrade_service->method('downloadFile')->will(self::throwException(new Exception()));
$handler = new UpgradeWizardStep(
- new GedcomExportService(),
+ new GedcomExportService(new Psr17Factory(), new Psr17Factory()),
new TreeService(new GedcomImportService()),
$mock_upgrade_service
);
@@ -229,7 +230,7 @@ class UpgradeWizardStepTest extends TestCase
$mock_upgrade_service = $this->createMock(UpgradeService::class);
$mock_upgrade_service->method('downloadFile')->willReturn(123456);
$handler = new UpgradeWizardStep(
- new GedcomExportService(),
+ new GedcomExportService(new Psr17Factory(), new Psr17Factory()),
new TreeService(new GedcomImportService()),
$mock_upgrade_service
);
@@ -248,7 +249,7 @@ class UpgradeWizardStepTest extends TestCase
$mock_upgrade_service = $this->createMock(UpgradeService::class);
$mock_upgrade_service->method('webtreesZipContents')->willReturn(new Collection());
$handler = new UpgradeWizardStep(
- new GedcomExportService(),
+ new GedcomExportService(new Psr17Factory(), new Psr17Factory()),
new TreeService(new GedcomImportService()),
$mock_upgrade_service
);
diff --git a/tests/app/TreeTest.php b/tests/app/TreeTest.php
index 71d2143ca8..7d6dff01f2 100644
--- a/tests/app/TreeTest.php
+++ b/tests/app/TreeTest.php
@@ -26,6 +26,7 @@ use Fisharebest\Webtrees\Services\GedcomImportService;
use Fisharebest\Webtrees\Services\TreeService;
use Fisharebest\Webtrees\Services\UserService;
use InvalidArgumentException;
+use Nyholm\Psr7\Factory\Psr17Factory;
use Symfony\Component\Cache\Adapter\NullAdapter;
use function fclose;
@@ -358,7 +359,7 @@ class TreeTest extends TestCase
{
$tree = $this->importTree('demo.ged');
- $gedcom_export_service = new GedcomExportService();
+ $gedcom_export_service = new GedcomExportService(new Psr17Factory(), new Psr17Factory());
$resource = $gedcom_export_service->export($tree, true);
$original = file_get_contents(__DIR__ . '/../data/demo.ged');