Skip to content

Keep a font failure a render can't tell is stale - #2035

Merged
jakejackson1 merged 1 commit into
developmentfrom
fix/2021-render-clears-font-failure-by-size
Nov 12, 2026
Merged

jakejackson1 merged 1 commit into
developmentfrom
fix/2021-render-clears-font-failure-by-size

Conversation

@gravitypdf-bot

@gravitypdf-bot gravitypdf-bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A render could clear a recorded font download failure on the strength of a file's size alone. When another font's file sat at a core font's name with the size of a known build, only the background sync's hash check found it. The download to the font's hashed name then failed, and the next render cleared the failure again. So the retry never reached a give-up and the failed fonts notice never showed. A failure now records the size and modified time of the other font's file it found at the name, and nothing counts that unchanged file as the font's own copy.

  • A render's download() no longer clears a failure for the file the failed download found, while it is unchanged.
  • Give-ups are no longer cleared for that file either, so the notice lists the font once its retry fails.
  • A large font in that state is queued for the sync instead of being downloaded during the render.
  • A font copied in by hand still clears its failure on the next render, as Don't record a font failure when another request already saved the font #2008 intended. The render path does no hashing.

Try it

  1. Put a modified copy of a core font your PDFs use at its name: copy DejaVuSans.ttf into wp-content/uploads/PDF_EXTENDED_TEMPLATES/fonts/ and change its last byte (printf x | dd of=DejaVuSans.ttf bs=1 seek=$(( $(stat -f%z DejaVuSans.ttf) - 1 )) conv=notrunc).
  2. Block the download with define( 'WP_HTTP_BLOCK_EXTERNAL', true ); in wp-config.php.
  3. Run the sync: wp cron event run gfpdf_core_font_sync. wp option get gfpdf_core_font_failures --format=json lists the font with a conflict value.
  4. Generate a PDF that uses DejaVu Sans. The failure is still listed. Before this change it was removed.
  5. Run the sync again once the retry is due, within five minutes. The font gives up and the failed fonts notice shows.
  6. Copy the real DejaVuSans.ttf over the modified file and generate the PDF again. The failure and the notice are cleared.

Test plan

  • PHPUnit single site: 2785 tests, 4 failures/errors, none from this change. test_an_unwritable_folder_fails_without_a_request x2 (the container runs as root) and test_deep_copy_is_deprecated also fail on development. test_delete_item_with_font_reference_gone_success errored once in the full run but passes alone and in two runs of its class, and it doesn't touch the downloader.
  • PHPUnit multisite, font classes: 720 tests, only the two unwritable folder failures
  • The three new tests fail without the fix (the large-file one against the first version of the fix too)
  • PHPCS clean on the changed files, PHPStan clean on Model_Font_Downloader.php
  • Playwright (not run, as nothing it covers changed)
  • CI is green
More info

The issue listed three options. This is a variant of the mtime one that needs no stored failure time. record_failure() stamps the file at the font's plain name (size:mtime) into a new conflict field. A failed download leaves the name as it was, so any file there is another font's: the sync's hash check or a size mismatch found it. With nothing at the name, the stamp is '' and the record behaves as before.

is_failed_conflict() is true when the own copy get_own_copy() found by size is the plain-name file and its stamp still matches. It is checked in download() when not verifying, and via has_own_copy() in download_or_queue() and clear_installed_give_ups(). A hashed copy on disk is always the font's own, so it isn't affected. The check costs nothing extra on a render unless the failure has a stamp, as download() already reads the failure record.

A file copied in with its old modified time preserved (cp -p) and of the same size as the other font's file won't clear on a render. The sync's next verified pass still clears it.

Not changed here: Model_Required_Fonts (Site Health, the System Report and the Font Manager's install state) still goes by size, so while the failure stands it reports the font as installed even though the notice lists it as failed. delete_downloaded_font() can also treat the other font's file as the family's own copy while such a failure stands. Both predate this change and need the downloader's record passed into Model_Font_Sources or Model_Required_Fonts, so they're left for #2036.

When a download fails while another font's file is at the font's name,
the failure record now keeps that file's size and modified time. A
render's size-only own-copy check, and the clearing of give-ups for
files on disk, no longer count that same unchanged file as the font's
own copy. So a same-size file from another font, which only the sync's
hash check finds, no longer clears the failure on every render: the
retry gives up and the failed fonts notice lists it.

A font copied in by hand still clears its failure on the next render,
as the copy has a new modified time (or no file was at the name when
the download failed). The render path does no hashing.

- download() treats the recorded file as another font's on a render.
- download_or_queue() queues a large font in that case rather than
  downloading it during the render.
- Give-ups are only cleared for a file that isn't the recorded one.

Closes #2021

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Coverage report for commit: 445fa4d
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% │ █░░░░░░░░░░░░░░░░░░░░░░ │  3.1%
  80% │ ██░░░░░░░░░░░░░░░░░░░░░ │  4.7%
  90% │ ██████░░░░░░░░░░░░░░░░░ │ 18.8%
 100% │ ███████████████████████ │ 73.4%
      └─────────────────────────┘
 *Legend:* █ = Current Distribution 
Summary - Lines: 93.99% | Methods: 90.78% | Branches: 85.75%
FilesLinesMethodsBranches
src/assets/js/react/actions
   fontManager.js96.15%84.62%100.00%
   templates.js100.00%100.00%100.00%
src/assets/js/react/components/Alert
   Alert.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.js80.65%80.00%72.22%
   Spinner.js100.00%100.00%100.00%
src/assets/js/react/components/FontManager
   AddFont.js100.00%100.00%100.00%
   AddUpdateFontFooter.js100.00%100.00%91.67%
   AdvancedButton.js100.00%100.00%100.00%
   DeleteConfirm.js90.91%75.00%100.00%
   FontDeleteButton.js100.00%80.00%100.00%
   FontDetail.js100.00%100.00%100.00%
   FontInstallButton.js100.00%66.67%100.00%
   FontList.js100.00%50.00%65.22%
   FontListAlertMessage.js100.00%100.00%100.00%
   FontListItems.js96.00%83.33%88.24%
   FontListSkeleton.js100.00%100.00%100.00%
   FontManager.js73.68%50.00%50.00%
   FontManagerBody.js89.12%90.32%84.96%
   FontManagerHeader.js100.00%100.00%25.00%
   FontPreview.js100.00%100.00%100.00%
   FontStatus.js100.00%66.67%100.00%
   FontUsage.js100.00%75.00%100.00%
   FontVariant.js94.12%80.00%90.24%
   FontVariantLabel.js100.00%100.00%100.00%
   Icon.js100.00%100.00%100.00%
   InitialAddUpdateState.js100.00%100.00%100.00%
   SearchBox.js90.00%66.67%69.23%
   TemplateTooltip.js100.00%100.00%100.00%
   UpdateFont.js75.00%50.00%64.29%
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%
   TemplateDocsLink.js100.00%100.00%100.00%
   TemplateFooterActions.js100.00%100.00%100.00%
   TemplateHeaderNavigation.js82.35%85.71%70.00%
   TemplateHeaderTitle.js100.00%100.00%100.00%
   TemplateList.js100.00%100.00%100.00%
   TemplateListItem.js100.00%100.00%92.86%
   TemplateListItemComponents.js100.00%100.00%100.00%
   TemplateManagerClosed.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%95.00%
   TemplateUploaderContext.js100.00%100.00%100.00%
   TemplateUploaderTile.js100.00%100.00%100.00%
src/assets/js/react/reducers
   fontManagerReducer.js91.57%100.00%81.33%
   index.js100.00%100.00%100.00%
   templateReducer.js100.00%100.00%100.00%
src/assets/js/react/sagas
   fontManager.js91.18%93.75%89.74%
   index.js100.00%100.00%100.00%
   templates.js91.30%100.00%100.00%
src/assets/js/react/selectors
   getTemplates.js91.11%100.00%83.33%
src/assets/js/react/utilities/FontManager
   associatedFontManagerSelectBox.js94.44%100.00%77.78%
   fontManagerReducer.js100.00%100.00%94.12%
   toggleUpdateFont.js100.00%100.00%100.00%
src/assets/js/react/utilities/Template
   hasPendingUpload.js100.00%100.00%100.00%
src/assets/js/react/utilities
   withRouterHooks.js100.00%100.00%100.00%

🤖 Jest coverage report

@github-actions

Copy link
Copy Markdown
Contributor

Coverage report for commit: 445fa4d
File: tmp/coverage/report-xml/merged.xml

Cover ┌─────────────────────────┐ Freq.
   0% │ ██████░░░░░░░░░░░░░░░░░ │  7.1%
  10% │ ░░░░░░░░░░░░░░░░░░░░░░░ │  0.0%
  20% │ █░░░░░░░░░░░░░░░░░░░░░░ │  0.4%
  30% │ █░░░░░░░░░░░░░░░░░░░░░░ │  0.9%
  40% │ █░░░░░░░░░░░░░░░░░░░░░░ │  0.4%
  50% │ ███████░░░░░░░░░░░░░░░░ │  9.3%
  60% │ █░░░░░░░░░░░░░░░░░░░░░░ │  1.3%
  70% │ ██░░░░░░░░░░░░░░░░░░░░░ │  2.7%
  80% │ ████████████░░░░░░░░░░░ │ 16.4%
  90% │ ███████████████████████ │ 31.4%
 100% │ ███████████████████████ │ 30.1%
      └─────────────────────────┘
 *Legend:* █ = Current Distribution 
Summary - Lines: 90.47% | Methods: 96.03%
FilesLinesMethodsBranches
/var/www/html/wp-content/plugins/gravity-pdf
   api.php98.14%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.83%100.00%100.00%
   Controller_Custom_Fonts.php96.02%100.00%100.00%
   Controller_Debug.php100.00%100.00%100.00%
   Controller_Export_Entries.php96.77%100.00%100.00%
   Controller_Font_Downloader.php100.00%100.00%100.00%
   Controller_Font_Sources.php100.00%100.00%100.00%
   Controller_Form_Settings.php87.37%90.00%100.00%
   Controller_Install.php100.00%100.00%100.00%
   Controller_Mergetags.php100.00%100.00%100.00%
   Controller_PDF.php81.46%100.00%100.00%
   Controller_Pdf_Cache.php100.00%100.00%100.00%
   Controller_Pdf_Queue.php85.27%87.50%100.00%
   Controller_Save_Core_Fonts.php91.67%100.00%100.00%
   Controller_Settings.php86.90%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.php95.19%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%
   GravityPdfNotATemplateException.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.86%100.00%100.00%
   Field_Chainedselect.php66.67%75.00%100.00%
   Field_Checkbox.php94.44%100.00%100.00%
   Field_Consent.php95.00%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.php95.83%100.00%100.00%
   Field_Form.php90.91%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.php96.15%100.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.php92.31%100.00%100.00%
   Field_Products.php84.32%100.00%100.00%
   Field_Quantity.php84.62%100.00%100.00%
   Field_Quiz.php89.74%100.00%100.00%
   Field_Radio.php94.74%100.00%100.00%
   Field_Rank.php95.24%100.00%100.00%
   Field_Rating.php95.24%100.00%100.00%
   Field_Repeater.php97.39%100.00%100.00%
   Field_Section.php90.74%100.00%100.00%
   Field_Select.php94.12%100.00%100.00%
   Field_Shipping.php92.86%100.00%100.00%
   Field_Signature.php71.11%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.php91.18%100.00%100.00%
   Field_Time.php81.82%100.00%100.00%
   Field_Tos.php92.86%100.00%100.00%
   Field_Total.php88.00%100.00%100.00%
   Field_Website.php85.71%100.00%100.00%
/var/www/html/wp-content/plugins/gravity-pdf/src/Helper/Fonts
   FlushCache.php92.31%100.00%100.00%
   LocalFile.php88.89%100.00%100.00%
   LocalFilesystem.php66.67%100.00%100.00%
   SupportsOtl.php87.50%100.00%100.00%
   TtfFontValidation.php84.62%100.00%100.00%
/var/www/html/wp-content/plugins/gravity-pdf/src/Helper
   Helper_Abstract_Addon.php94.09%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.php96.10%95.83%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.php77.33%81.36%100.00%
   Helper_Abstract_Pdf_Shortcode.php94.67%95.65%100.00%
   Helper_Abstract_View.php96.77%100.00%100.00%
   Helper_Async_Notifications.php64.29%100.00%100.00%
   Helper_Data.php98.43%100.00%100.00%
   Helper_Field_Container.php92.31%100.00%100.00%
   Helper_Field_Container_Gf25.php92.00%100.00%100.00%
   Helper_Field_Container_Void.php16.67%-100.00%
   Helper_Form.php80.65%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_Deprecated_Features.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.php50.00%100.00%100.00%
   Helper_Interface_Url_Signer.php-100.00%100.00%
   Helper_Logger.php-100.00%100.00%
   Helper_Misc.php89.17%100.00%100.00%
   Helper_Mpdf.php50.00%100.00%100.00%
   Helper_Notices.php92.98%100.00%100.00%
   Helper_Options_Fields.php98.10%100.00%100.00%
   Helper_PDF.php88.61%97.56%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_Render_Health.php85.71%100.00%100.00%
   Helper_Sha256_Url_Signer.php90.91%100.00%100.00%
   Helper_Singleton.php100.00%100.00%100.00%
   Helper_Templates.php97.29%100.00%100.00%
   Helper_Trait_Logger.php25.00%-100.00%
   Helper_Trait_Removed_Methods.php100.00%100.00%100.00%
   Helper_Url_Signer.php84.82%100.00%100.00%
/var/www/html/wp-content/plugins/gravity-pdf/src/Helper/Licensing
Table truncated to fit comment

🤖 PHPUnit coverage 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.

2 participants