Skip to content

Improve the Template Manager uploader - #1693

Draft
jakejackson1 wants to merge 1 commit into
developmentfrom
feat/template-manager-uploader-development
Draft

Improve the Template Manager uploader#1693
jakejackson1 wants to merge 1 commit into
developmentfrom
feat/template-manager-uploader-development

Conversation

@jakejackson1

Copy link
Copy Markdown
Member

Summary

Four fixes to how PDF templates get installed via the Template Manager.

Multiple zips can be selected or dropped together. Only the first one used to install. The saga used takeLatest, which cancelled every in-flight upload but the last, and the reducer kept a single success/error object that concurrent results overwrote. Uploads now use takeEvery, each result carries the filename it belongs to, and the component drains an append-only results array with a batch counter so nothing is lost when several results land in the same React render. Every file reports its own outcome, so one bad zip no longer hides the rest.

Zips with the templates inside a folder now install. Safari auto-extracts a template zip on download. People then re-zip that folder, which buries the PHP files a level deeper than the installer looked, and the upload failed with "No valid PDF template found in Zip archive." The installer now descends through single-directory wrappers to find the templates. Multiple directories in the root of the archive are still invalid, as #1336 asked for.

The upload limit goes from 10MB to 32MB. It is clamped by wp_max_upload_size() so we never accept a file PHP will reject. That clamp matters: a POST over post_max_size gets discarded before the request reaches us, and the user saw a nonce failure instead of a size error. The message now names the real limit, so a host capped at 8MB says so.

The drop target is the whole window. It used to be the small "Add New Template" box at the foot of the list. Dragging a zip anywhere over the Template Manager now shows a full-window drop overlay, and progress and results appear in a toast pinned to the modal so you see them wherever the list is scrolled.

Closes #1336 — Allow template zip to contain folder in root of archive
Closes #1337 — Process template zips up to 20MB (this raises it to 32MB)

Try it

yarn wp-env start && yarn dev:build

Open a form, add a PDF, and click Manage PDF Templates:

  1. Drag a zip anywhere over the window. The whole window highlights as a drop target.
  2. Select two or more template zips at once. All of them install.
  3. Unzip a template package, re-zip the resulting folder, and upload it. It installs.
  4. Try a non-zip and an oversized file in the same selection as a valid one. Each bad file is named in its own error and the good one still installs.

Test plan

  • Dragging a zip anywhere over the Template Manager shows the drop overlay
  • Selecting several zips at once installs all of them
  • A folder-wrapped (Safari re-zipped) template installs
  • A template package between 10MB and 32MB installs
  • On a host with a low upload_max_filesize, the error names that limit rather than 32MB
  • Invalid files are reported per file, and valid files in the same batch still install
  • Uploading a template that is already installed still reports it as updated
More info

Where the changes live

The nested-folder resolution and the size constant both live on Helper_Templates, which already owns template discovery, rather than on Model_Templates. That keeps Helper_Data (which needs the size for the localised script data) from reaching into a Model, and lets get_template_root_dir() reuse the same directory-listing helper as get_all_templates_in_folder() instead of hand-rolling a second FilesystemIterator walk.

unzip_and_verify_templates() now returns the resolved directory so the copy step reads from the right place. Hidden directories (.git, .idea) are skipped during the descent. Root-level __MACOSX needs no special handling because core's unzip_file() already refuses to extract it.

New gfpdf_template_max_upload_size filter for anyone who wants a different ceiling.

On the front end, TemplateUploader moved from being a leaf inside the template list to wrapping the whole manager (noClick/noKeyboard), sharing the react-dropzone open() handle and upload status with the new TemplateUploaderTile through TemplateUploaderContext. The composition stays inside TemplateList rather than moving up to the router so TemplateUploader and react-dropzone remain in the lazily-loaded chunk.

Note on concurrency

takeEvery fires the uploads in parallel, and browsers cap concurrent connections to roughly six. Combined with the higher size limit that is up to six large multipart POSTs, each holding a PHP worker through an unzip and a recursive copy. Previously takeLatest meant effectively one at a time. Templates are usually small so this is left parallel, but a bounded actionChannel pool drops in without touching the batch accounting if it turns out to matter.

Testing

  • 479 JS unit tests, 1567 PHPUnit tests, 93 Playwright tests
  • New PHPUnit coverage for the root-directory resolution (top level, single wrapper, nested twice, macOS-compressed, hidden folders, ambiguous, assets-only) and for the size clamp and its filter
  • Three new Playwright tests: multi-file selection, re-zipped folder, and the window-wide drop target
  • New E2E fixtures use a distinct template Group and their own slugs, since the whole suite shares one WordPress instance and the existing upload test asserts on the number of templates in the Custom group

🤖 Generated with AI

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown

Coverage report for commit: 25d9eb8
File: ./tmp/jest-coverage/clover.xml

Cover ┌─────────────────────────┐ Freq.
   0% │ ░░░░░░░░░░░░░░░░░░░░░░░ │  0.0%
  10% │ ░░░░░░░░░░░░░░░░░░░░░░░ │  0.0%
  20% │ ░░░░░░░░░░░░░░░░░░░░░░░ │  0.0%
  30% │ ░░░░░░░░░░░░░░░░░░░░░░░ │  0.0%
  40% │ ░░░░░░░░░░░░░░░░░░░░░░░ │  0.0%
  50% │ ░░░░░░░░░░░░░░░░░░░░░░░ │  0.0%
  60% │ ░░░░░░░░░░░░░░░░░░░░░░░ │  0.0%
  70% │ █░░░░░░░░░░░░░░░░░░░░░░ │  1.6%
  80% │ ███░░░░░░░░░░░░░░░░░░░░ │  7.8%
  90% │ ████████░░░░░░░░░░░░░░░ │ 21.9%
 100% │ ███████████████████████ │ 68.8%
      └─────────────────────────┘
 *Legend:* █ = Current Distribution 
Summary - Lines: 93.19% | Methods: 88.55% | Branches: 81.51%
FilesLinesMethodsBranches
src/assets/js/react/actions
   coreFonts.js100.00%100.00%100.00%
   fontManager.js100.00%100.00%100.00%
   templates.js100.00%100.00%-
src/assets/js/react/components/Alert
   Alert.js100.00%100.00%100.00%
src/assets/js/react/components/CoreFonts
   CoreFontContainer.js100.00%100.00%91.43%
   CoreFontCounter.js100.00%100.00%100.00%
   CoreFontListResults.js100.00%100.00%85.71%
   CoreFontListSpacer.js100.00%100.00%100.00%
src/assets/js/react/components
   CustomHashRouter.js100.00%100.00%100.00%
   Empty.js100.00%100.00%100.00%
   ShowMessage.js79.31%80.00%64.29%
   Spinner.js100.00%100.00%100.00%
src/assets/js/react/components/FontManager
   AddFont.js100.00%100.00%100.00%
   AddUpdateFontFooter.js85.37%50.00%88.89%
   AdvancedButton.js100.00%100.00%100.00%
   FontList.js100.00%50.00%65.22%
   FontListAlertMessage.js100.00%100.00%100.00%
   FontListHeader.js100.00%100.00%100.00%
   FontListIcon.js100.00%100.00%100.00%
   FontListItems.js85.39%64.00%68.66%
   FontListSkeleton.js100.00%100.00%100.00%
   FontManager.js77.78%57.14%50.00%
   FontManagerBody.js94.20%96.43%90.29%
   FontManagerHeader.js100.00%100.00%100.00%
   FontVariant.js90.00%60.00%70.00%
   FontVariantLabel.js100.00%100.00%100.00%
   InitialAddUpdateState.js100.00%100.00%100.00%
   SearchBox.js90.00%66.67%69.23%
   TemplateTooltip.js100.00%75.00%100.00%
   UpdateFont.js75.00%50.00%75.00%
src/assets/js/react/components/Modal
   CloseDialog.js93.33%66.67%70.59%
src/assets/js/react/components/Template
   TemplateActivateButton.js100.00%100.00%100.00%
   TemplateButton.js85.71%66.67%100.00%
   TemplateContainer.js71.43%66.67%25.00%
   TemplateDeleteButton.js100.00%100.00%70.00%
   TemplateFooterActions.js100.00%100.00%100.00%
   TemplateHeaderNavigation.js82.35%85.71%70.00%
   TemplateHeaderTitle.js100.00%100.00%100.00%
   TemplateList.js90.00%100.00%57.14%
   TemplateListItem.js100.00%100.00%92.86%
   TemplateListItemComponents.js100.00%100.00%100.00%
   TemplateScreenshot.js100.00%100.00%100.00%
   TemplateScreenshots.js100.00%100.00%50.00%
   TemplateSearch.js93.75%88.89%50.00%
   TemplateSingle.js100.00%100.00%100.00%
   TemplateSingleComponents.js100.00%100.00%75.00%
   TemplateUploader.js100.00%100.00%94.29%
   TemplateUploaderContext.js100.00%100.00%100.00%
   TemplateUploaderTile.js100.00%100.00%100.00%
src/assets/js/react/reducers
   coreFontReducer.js95.65%100.00%88.00%
   fontManagerReducer.js87.21%75.00%75.00%
   index.js100.00%100.00%100.00%
   templateReducer.js100.00%100.00%100.00%
src/assets/js/react/sagas
   coreFonts.js91.67%100.00%75.00%
   fontManager.js86.96%90.00%83.33%
   index.js100.00%100.00%100.00%
   templates.js84.21%100.00%100.00%
src/assets/js/react/selectors
   getTemplates.js91.11%100.00%83.33%
src/assets/js/react/utilities/FontManager
   adjustFontListHeight.js100.00%100.00%100.00%
   associatedFontManagerSelectBox.js94.44%100.00%66.67%
   fontManagerReducer.js100.00%100.00%100.00%
   getTabLocation.js100.00%100.00%100.00%
   toggleUpdateFont.js100.00%100.00%100.00%
src/assets/js/react/utilities
   withRouterHooks.js100.00%100.00%100.00%

🤖 Jest coverage report

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown

Coverage report for commit: 25d9eb8
File: tmp/coverage/report-xml/merged.xml

Cover ┌─────────────────────────┐ Freq.
   0% │ █████████░░░░░░░░░░░░░░ │ 11.1%
  10% │ █░░░░░░░░░░░░░░░░░░░░░░ │  1.0%
  20% │ █░░░░░░░░░░░░░░░░░░░░░░ │  0.5%
  30% │ █░░░░░░░░░░░░░░░░░░░░░░ │  1.0%
  40% │ █░░░░░░░░░░░░░░░░░░░░░░ │  0.5%
  50% │ ███████░░░░░░░░░░░░░░░░ │  8.7%
  60% │ ██░░░░░░░░░░░░░░░░░░░░░ │  1.4%
  70% │ █████░░░░░░░░░░░░░░░░░░ │  5.8%
  80% │ ███████████████░░░░░░░░ │ 19.8%
  90% │ ███████████████████████ │ 31.4%
 100% │ ██████████████░░░░░░░░░ │ 18.8%
      └─────────────────────────┘
 *Legend:* █ = Current Distribution 
Summary - Lines: 83.13% | Methods: 90.06%
FilesLinesMethodsBranches
/var/www/html/wp-content/plugins/gravity-pdf
   api.php96.55%100.00%100.00%
   gravity-pdf-updater.php53.97%100.00%100.00%
   pdf.php59.82%81.25%100.00%
/var/www/html/wp-content/plugins/gravity-pdf/src/Controller
   Controller_Actions.php100.00%100.00%100.00%
   Controller_Activation.php95.45%100.00%100.00%
   Controller_Custom_Fonts.php88.52%100.00%100.00%
   Controller_Debug.php100.00%100.00%100.00%
   Controller_Export_Entries.php96.67%100.00%100.00%
   Controller_Form_Settings.php86.05%90.00%100.00%
   Controller_Install.php100.00%100.00%100.00%
   Controller_Mergetags.php100.00%100.00%100.00%
   Controller_PDF.php82.05%100.00%100.00%
   Controller_Pdf_Queue.php83.72%77.78%100.00%
   Controller_Save_Core_Fonts.php66.67%100.00%100.00%
   Controller_Settings.php86.05%100.00%100.00%
   Controller_Shortcodes.php100.00%100.00%100.00%
   Controller_System_Report.php100.00%100.00%100.00%
   Controller_Templates.php100.00%100.00%100.00%
   Controller_Uninstaller.php83.33%77.78%100.00%
   Controller_Upgrade_Routines.php93.65%100.00%100.00%
   Controller_Webhooks.php100.00%100.00%100.00%
   Controller_Zapier.php100.00%100.00%100.00%
/var/www/html/wp-content/plugins/gravity-pdf/src/Exceptions
   GravityPdfDatabaseUpdateException.php50.00%100.00%100.00%
   GravityPdfDomainException.php50.00%100.00%100.00%
   GravityPdfException.php50.00%100.00%100.00%
   GravityPdfFontNotFoundException.php50.00%100.00%100.00%
   GravityPdfIdException.php50.00%100.00%100.00%
   GravityPdfModelNotUpdatedException.php50.00%100.00%100.00%
   GravityPdfRuntimeException.php50.00%100.00%100.00%
   GravityPdfShortcodeEntryIdException.php50.00%100.00%100.00%
   GravityPdfShortcodePdfConditionalLogicFailedException.php50.00%100.00%100.00%
   GravityPdfShortcodePdfConfigNotFoundException.php50.00%100.00%100.00%
   GravityPdfShortcodePdfInactiveException.php50.00%100.00%100.00%
/var/www/html/wp-content/plugins/gravity-pdf/src/Helper/Fields
   Field_Address.php92.16%100.00%100.00%
   Field_Chainedselect.php66.67%75.00%100.00%
   Field_Checkbox.php94.34%100.00%100.00%
   Field_Consent.php89.47%100.00%100.00%
   Field_Coupon.php--100.00%
   Field_Creditcard.php83.33%100.00%100.00%
   Field_Date.php83.33%100.00%100.00%
   Field_Default.php83.33%100.00%100.00%
   Field_Discount.php44.00%75.00%100.00%
   Field_Email.php83.33%100.00%100.00%
   Field_Fg_Ls_Consent.php92.86%100.00%100.00%
   Field_Fg_Ls_Signature.php73.08%66.67%100.00%
   Field_Fileupload.php94.23%100.00%100.00%
   Field_Form.php89.09%100.00%100.00%
   Field_Hidden.php81.82%100.00%100.00%
   Field_Html.php89.47%100.00%100.00%
   Field_Image_Choice.php86.67%100.00%100.00%
   Field_Likert.php97.22%100.00%100.00%
   Field_List.php92.41%100.00%100.00%
   Field_Multi_Choice.php50.00%100.00%100.00%
   Field_Multiselect.php92.59%100.00%100.00%
   Field_Name.php84.62%100.00%100.00%
   Field_Number.php83.33%100.00%100.00%
   Field_Option.php57.69%50.00%100.00%
   Field_Page.php83.33%100.00%100.00%
   Field_Phone.php94.55%100.00%100.00%
   Field_Poll.php93.75%100.00%100.00%
   Field_Post_Category.php85.00%100.00%100.00%
   Field_Post_Content.php82.35%100.00%100.00%
   Field_Post_Custom_Field.php50.00%100.00%100.00%
   Field_Post_Excerpt.php81.82%100.00%100.00%
   Field_Post_Image.php94.00%100.00%100.00%
   Field_Post_Tags.php90.91%100.00%100.00%
   Field_Post_Title.php81.82%100.00%100.00%
   Field_Product.php88.46%100.00%100.00%
   Field_Products.php85.41%100.00%100.00%
   Field_Quantity.php84.62%100.00%100.00%
   Field_Quiz.php89.74%100.00%100.00%
   Field_Radio.php95.35%100.00%100.00%
   Field_Rank.php95.24%100.00%100.00%
   Field_Rating.php95.24%100.00%100.00%
   Field_Repeater.php97.37%100.00%100.00%
   Field_Section.php90.74%100.00%100.00%
   Field_Select.php94.12%100.00%100.00%
   Field_Shipping.php75.00%66.67%100.00%
   Field_Signature.php67.39%100.00%100.00%
   Field_Slim.php82.35%100.00%100.00%
   Field_Slim_Post.php91.30%100.00%100.00%
   Field_Subtotal.php65.38%75.00%100.00%
   Field_Survey.php95.00%100.00%100.00%
   Field_Tax.php32.00%50.00%100.00%
   Field_Text.php81.82%100.00%100.00%
   Field_Textarea.php90.63%100.00%100.00%
   Field_Time.php81.82%100.00%100.00%
   Field_Tos.php92.59%100.00%100.00%
   Field_Total.php68.00%66.67%100.00%
   Field_V3_List.php92.86%100.00%100.00%
   Field_V3_Products.php73.68%100.00%100.00%
   Field_V3_Section.php77.78%100.00%100.00%
   Field_Website.php85.71%100.00%100.00%
/var/www/html/wp-content/plugins/gravity-pdf/src/Helper/Fonts
   FlushCache.php80.00%100.00%100.00%
   LocalFile.php90.91%100.00%100.00%
   LocalFilesystem.php66.67%100.00%100.00%
   SupportsOtl.php88.89%100.00%100.00%
   TtfFontValidation.php72.73%100.00%100.00%
/var/www/html/wp-content/plugins/gravity-pdf/src/Helper
   Helper_Abstract_Addon.php93.33%100.00%100.00%
   Helper_Abstract_Config_Settings.php75.00%100.00%100.00%
   Helper_Abstract_Controller.php-100.00%100.00%
   Helper_Abstract_Field_Products.php89.47%66.67%100.00%
   Helper_Abstract_Fields.php93.59%93.75%100.00%
   Helper_Abstract_Fields_Input_Type.php84.00%100.00%100.00%
   Helper_Abstract_Form.php--100.00%
   Helper_Abstract_Model.php100.00%100.00%100.00%
   Helper_Abstract_Options.php75.51%78.57%100.00%
   Helper_Abstract_Pdf_Shortcode.php89.52%91.67%100.00%
   Helper_Abstract_View.php95.83%100.00%100.00%
   Helper_Data.php96.62%100.00%100.00%
   Helper_Field_Container.php92.31%100.00%100.00%
   Helper_Field_Container_Gf25.php82.22%100.00%100.00%
   Helper_Field_Container_Void.php16.67%-100.00%
   Helper_Form.php70.97%64.29%100.00%
   Helper_Interface_Actions.php-100.00%100.00%
   Helper_Interface_Config.php50.00%100.00%100.00%
   Helper_Interface_Config_Settings.php50.00%100.00%100.00%
   Helper_Interface_Extension_Settings.php-100.00%100.00%
   Helper_Interface_Extension_Uninstaller.php-100.00%100.00%
   Helper_Interface_Field_Pdf_Config.php50.00%100.00%100.00%
   Helper_Interface_Filters.php-100.00%100.00%
   Helper_Interface_Setup_TearDown.php-100.00%100.00%
   Helper_Interface_Url_Signer.php-100.00%100.00%
   Helper_Logger.php-100.00%100.00%
   Helper_Misc.php73.28%90.91%100.00%
   Helper_Mpdf.php50.00%100.00%100.00%
   Helper_Notices.php79.31%78.57%100.00%
   Helper_Options_Fields.php96.87%100.00%100.00%
   Helper_PDF.php89.66%94.29%100.00%
   Helper_PDF_List_Table.php84.50%92.31%100.00%
   Helper_Pdf_Queue.php84.31%100.00%100.00%
   Helper_QueryPath.php80.00%100.00%100.00%
   Helper_Sha256_Url_Signer.php87.50%100.00%100.00%
   Helper_Singleton.php90.00%100.00%100.00%
   Helper_Templates.php96.27%100.00%100.00%
   Helper_Trait_Logger.php--100.00%
   Helper_Url_Signer.php82.86%100.00%100.00%
/var/www/html/wp-content/plugins/gravity-pdf/src/Helper/Licensing
   EDD_SL_Plugin_Updater.php94.74%100.00%100.00%
/var/www/html/wp-content/plugins/gravity-pdf/src/Helper/Log
   Logger.php86.89%100.00%100.00%
   Redact_Processor.php96.15%100.00%100.00%
/var/www/html/wp-content/plugins/gravity-pdf/src/Helper/Mpdf
   Cache.php66.67%100.00%100.00%
   Mpdf.php92.31%100.00%100.00%
Table truncated to fit comment

🤖 PHPUnit coverage report

Five changes to how PDF templates get installed.

Multiple zips can now be selected or dropped together. Previously only
the first survived: the saga used takeLatest, which cancelled every
in-flight upload but the last, and the reducer held a single
success/error object that concurrent results overwrote. Uploads now use
takeEvery, results carry the filename they belong to and are appended to
a templateUploadResults array, and the component drains that array with
a batch counter so results arriving in one React render can't be lost.
Each file reports its own outcome.

Zips with the templates nested inside a folder now install. Safari
auto-extracts a template zip on download; users then re-zip the folder,
which buries the PHP files one level deep where the non-recursive
get_all_templates_in_folder() couldn't see them. Helper_Templates now
descends through single-directory wrappers to find the templates.
Multiple directories in the root stay invalid, as #1336 specified.

The upload limit goes from 10MB to 32MB, clamped by wp_max_upload_size().
The clamp matters because a POST over post_max_size is discarded by PHP
before the request reaches us, which surfaced as a nonce failure rather
than a size error. One constant now feeds the server-side validator, the
JS pre-flight check and the error message, which reports the real limit
via size_format().

The drop target is the whole Template Manager window instead of the tile
at the foot of the list. TemplateUploader wraps the manager with
noClick/noKeyboard and shares the file picker with the "Add New Template"
tile through context. Dragging anywhere shows a full-viewport overlay,
and progress and results appear in a toast pinned to the modal so they're
visible wherever the list is scrolled.

The bundled upload library moves to 4.0, a security-hardening release. Storage
stages each upload and moves it into place rather than writing to the
destination directly, refuses traversal, dotfiles, control characters and
symlinked destinations, applies a default extension deny-list, and stores files
as 0640. Template zips move from Extension + Mimetype to the new
Validation\FileType: the old pair checked two independent allow-lists, so the
extension and the sniffed contents never had to describe the same format.
The octet-stream allowance is kept, because plenty of servers report a zip that
way. GFPDF\Helper\Fonts\LocalFile overrides isValid() wholesale to skip the
is-uploaded-file check, so it did not inherit 4.0's reset of the error list --
without it, upload() calling isValid() again reported every font validation
error twice.

Note for review: composer.json points at dev-hardening while
GravityPDF/Upload#20 is open. It needs repointing at ^4.0 once that is tagged,
before this can merge.

Closes #1336
Closes #1337

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jakejackson1
jakejackson1 force-pushed the feat/template-manager-uploader-development branch from 8a9f410 to 25d9eb8 Compare August 14, 2026 07:02
@jakejackson1
jakejackson1 marked this pull request as draft August 14, 2026 07:02
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatedcomposer/​gravitypdf/​upload@​3.1.0.0 ⏵ dev-hardening91 -9100100 +11100100

View full report

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Process template zips up to 20MB Allow template zip to contain folder in root of archive

1 participant