Skip to content

Restore backup - #119

Open
tijmenbruggeman wants to merge 35 commits into
tinify:masterfrom
wcreateweb:restore-backup
Open

Restore backup#119
tijmenbruggeman wants to merge 35 commits into
tinify:masterfrom
wcreateweb:restore-backup

Conversation

@tijmenbruggeman

@tijmenbruggeman tijmenbruggeman commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

#98 added backups of the original image. This will add the restore functionality.

Screenshots
image

When a backup is created, we will check in the media library if the image has a backup available. If so, the customer can restore the image in the details modal. This will make a API call to the back-end (ajax action). This will attempt to restore the uncompressed uploaded image.

  • Automatic compression will not be triggered.

Additionally in this pull request:

  • moved backup functionality to tiny image so that we functionality is contained to the image domain
  • improved error handling with the file system, for when direct file manipulation is not possible due to write constraints

Summary by CodeRabbit

New Features

  • Added automatic backups of original images during compression.
  • Added options to view, restore, and delete image backups.
  • Added confirmation dialogs and progress feedback for restoring backups.
  • Restored images now refresh their media details, thumbnails, and metadata.

Bug Fixes

  • Backups are removed when attachments are deleted.
  • Improved handling of restored image sizes and compression information.
  • Improved error handling when filesystem access is unavailable.

Style

  • Updated compression details spacing and dialog styling.

@Sreini
Sreini requested review from Sreini and rkoopmans July 27, 2026 09:30
@rkoopmans

rkoopmans commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@rkoopmans Hi! How can I help with this PR?

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds original-image backup lifecycle methods, authenticated admin restoration, backup controls in compression details, filesystem failure handling, cleanup on attachment deletion, and unit and integration coverage.

Changes

Image backup restoration

Layer / File(s) Summary
Backup lifecycle
src/class-tiny-image.php, docs/hooks/tiny_image_before_compression.md
Adds backup creation, retrieval, restoration, deletion, metadata regeneration, and a second compression-hook argument for attachment metadata.
Plugin restoration endpoint
src/class-tiny-plugin.php
Delegates backup creation, registers authenticated restoration, exposes attachment IDs, cleans backups on deletion, and returns refreshed compression details.
Admin restore interface
src/views/compress-details.php, src/views/compress-details-backup.php, src/js/admin.js, src/css/admin.css
Adds backup status, restore dialogs, AJAX handling, refreshed media markup, and dialog styling.
Filesystem failure handling
src/class-tiny-helpers.php, src/class-tiny-diagnostics.php, src/class-tiny-logger.php
Returns a false filesystem result and stops diagnostic and logging operations when filesystem initialization fails.
Backup restoration validation
test/unit/TinyPluginBackupTest.php, test/integration/backup.spec.ts, test/integration/utils.ts, test/helpers/wordpress.php, test/wp-includes-for-tests/file.php
Tests backup restoration, metadata regeneration, cleanup, AJAX registration, integration behavior, and filesystem-backed image fixtures.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AdminUI
  participant admin.js
  participant restore_backup_image
  participant Tiny_Image
  participant WordPressFilesystem
  AdminUI->>admin.js: Confirm backup restoration
  admin.js->>restore_backup_image: POST tiny_restore_backup with attachment ID
  restore_backup_image->>Tiny_Image: Call restore_backup()
  Tiny_Image->>WordPressFilesystem: Copy backup over original image
  restore_backup_image-->>admin.js: Return compression details
  admin.js-->>AdminUI: Replace media container
Loading

Suggested reviewers: rkoopmans, sreini

Poem

A rabbit guards the backup byte,
Then hops to restore files just right.
Dialogs bloom and spinners spin,
Metadata starts fresh again.
“Thump-thump!” says Bun, “the tests all cheer!”

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the pull request's primary change: restoring image backups.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/class-tiny-image.php`:
- Around line 720-772: Handle exceptions from Tiny_Helpers::get_wp_filesystem()
in all backup methods: create_backup(), get_backup(), restore_backup(), and
delete_backup(). Wrap each filesystem initialization in try/catch and return
false when initialization fails, preserving normal behavior when it succeeds so
media rendering and compression degrade gracefully.
- Around line 774-825: Update restore_backup() to reuse get_original_image() for
the restored original image instead of duplicating the
ORIGINAL_UNSCALED/ORIGINAL lookup. After wp_generate_attachment_metadata()
succeeds, rebuild $this->sizes from the new attachment metadata so
get_image_sizes() and get_image_size() immediately reflect regenerated, added,
removed, or renamed thumbnails; retain the existing metadata update and success
behavior.

In `@src/js/admin.js`:
- Around line 22-64: Move the restore workflow from the dialog’s cancel listener
to the confirm button’s click handler, so Escape only dismisses the dialog and
never calls restoreBackup. In the relevant admin.js handler, explicitly remove
any previously registered confirm handler before adding or assigning the new
one, rather than relying on { once: true }, so cancelling and reopening cannot
accumulate listeners. Update the confirm button markup to use type="button" and
remove command="request-close" and commandfor.

In `@src/views/compress-details-backup.php`:
- Around line 18-49: Fix the conditional markup around the backup view in the
template: remove the paragraph wrapper that contains the flow-content dialog,
ensure the no-backup branch emits no orphaned closing tag, and keep the dialog
as valid sibling markup. Also update the “original upload” translation call to
use esc_html_e() without echo, add the tiny-compress-images text domain to “View
uncompressed file,” and use esc_url() for the $backup href.

In `@src/views/compress-details.php`:
- Around line 116-121: Update the modal_title construction and Thickbox name
attribute around $modal_title to escape the translated title exactly once for
its HTML attribute context. Keep the filename unescaped while formatting the
translated string, then rely on esc_attr() at output without changing the
displayed title or surrounding link behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6a9aa22b-2fe7-40ef-b27c-4d843c3c1e32

📥 Commits

Reviewing files that changed from the base of the PR and between f19bae2 and 70b68bb.

📒 Files selected for processing (9)
  • src/class-tiny-image.php
  • src/class-tiny-plugin.php
  • src/css/admin.css
  • src/js/admin.js
  • src/views/compress-details-backup.php
  • src/views/compress-details.php
  • test/helpers/wordpress.php
  • test/unit/TinyPluginBackupTest.php
  • test/wp-includes-for-tests/file.php

Comment thread src/class-tiny-image.php
Comment thread src/class-tiny-image.php
Comment thread src/js/admin.js Outdated
Comment thread src/views/compress-details-backup.php Outdated
Comment thread src/views/compress-details.php
get_wp_filesystem() now returns WP_Filesystem_Base|false and logs via error_log() on failure instead of throwing. Removes the separate get_filesystem() wrapper. All callers guard against false.
@tijmenbruggeman
tijmenbruggeman marked this pull request as ready for review August 3, 2026 10:17

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/js/admin.js (1)

1-12: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Propagate restore failures to the AJAX error path.

src/class-tiny-plugin.php, Lines 979-1001, emits validation and restore failures with echo ...; exit() but does not return a non-2xx status. The AJAX helper can therefore treat a failed restore as successful. Lines 40-50 then close the dialog and replace the container with plain error text, so the catch block is bypassed.

Return a structured error or a non-2xx response from restore_backup_image(), or make restoreBackup reject the failure response before resolving.

Also applies to: 40-50

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/js/admin.js` around lines 1 - 12, Update the restoreBackup flow and its
success/error handling so failures from restore_backup_image are rejected rather
than resolved as successful AJAX responses. Ensure the PHP endpoint returns a
structured error or non-2xx status, or validate the response in restoreBackup
and reject failure payloads, allowing the existing catch path to handle errors
instead of closing the dialog and replacing the container with plain text.
🧹 Nitpick comments (4)
test/unit/TinyPluginBackupTest.php (3)

174-174: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove unused stub parameters.

The two wp_create_image_subsizes callbacks do not read $id or $file. Declare the callbacks without these parameters to remove the PHPMD UnusedFormalParameter warnings.

Also applies to: 200-200

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/unit/TinyPluginBackupTest.php` at line 174, Remove the unused $id and
$file parameters from both wp_create_image_subsizes callback declarations in
TinyPluginBackupTest, including the callbacks at both referenced locations,
while preserving their existing stub behavior.

Source: Linters/SAST tools


195-242: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Assert that persisted Tiny metadata is cleared.

The test only checks the regenerated thumbnail file size. It can pass if restore_backup() skips clearing or persisting compression metadata. Seed the stored Tiny metadata, then assert that the Tiny_Image::ORIGINAL and thumbnail metadata are cleared after restoration. Also verify that regenerated attachment metadata is persisted.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/unit/TinyPluginBackupTest.php` around lines 195 - 242, Update
test_restore_backup_clears_all_sizes_metadata to seed the stored Tiny metadata
for both Tiny_Image::ORIGINAL and thumbnail, then assert those entries are
cleared after Tiny_Image::restore_backup(). Also assert that the regenerated
attachment metadata is persisted, while retaining the existing regenerated
thumbnail filesize assertion.

167-193: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Cover replacement of an existing original file.

This test creates only the upload directory. It does not prove that restore_backup() overwrites a compressed original. Create a small, writable testfile.png before restoration. The existing size assertion will then detect a failed replacement.

Suggested setup change
-		$this->wp->createImage( 1, '2026/04', '_placeholder.png' );
+		$this->wp->createImage( 1, '2026/04', 'testfile.png' );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/unit/TinyPluginBackupTest.php` around lines 167 - 193, Update
test_restore_backup_restores_file_from_backup to create a small, writable
original testfile.png in the 2026/04 uploads directory before invoking
restore_backup(), while retaining the existing backup file and final size
assertion so the test verifies replacement of existing content rather than
creation in an empty directory.
src/class-tiny-logger.php (1)

182-184: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid duplicate filesystem initialization during logging.

log() calls rotate_logs() before calling Tiny_Helpers::get_wp_filesystem() again. rotate_logs() performs the same lookup. When initialization fails, one log attempt emits the same PHP error-log message twice.

Initialize the filesystem once and pass the result to rotate_logs(), or suppress duplicate failure logging.

Also applies to: 207-209

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/class-tiny-logger.php` around lines 182 - 184, Update the logging flow in
log() and rotate_logs() to perform Tiny_Helpers::get_wp_filesystem() only once
per log attempt, passing the initialized filesystem result into rotate_logs()
for reuse. Preserve the existing early return when initialization fails and
ensure both affected call paths no longer emit duplicate failure messages.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/class-tiny-diagnostics.php`:
- Around line 209-214: Update the caller of create_diagnostic_zip() before
download_zip() to guard with is_wp_error( $zippath ). When it returns a
WP_Error, report its error message and stop before passing the value to
download_zip(); preserve the existing download flow for valid string paths.

In `@src/class-tiny-helpers.php`:
- Around line 136-138: Update the filesystem initialization flow around
WP_Filesystem() to capture its return value and require a true result before
returning the global $wp_filesystem object. Preserve the existing error logging
and false return path when initialization fails, even if $wp_filesystem contains
an object.

In `@src/css/admin.css`:
- Line 524: Update the fill declaration containing currentColor to use the
lowercase CSS keyword currentcolor, satisfying the value-keyword-case Stylelint
rule.

In `@src/js/admin.js`:
- Around line 34-39: Update the confirmButton.onclick handler to add an
in-flight guard and disable confirmButton before invoking restoreBackup,
preventing rapid duplicate requests. In the handler’s finally block, re-enable
the button only when the existing retry flow allows another attempt, while
preserving the current spinner and restore behavior.

---

Outside diff comments:
In `@src/js/admin.js`:
- Around line 1-12: Update the restoreBackup flow and its success/error handling
so failures from restore_backup_image are rejected rather than resolved as
successful AJAX responses. Ensure the PHP endpoint returns a structured error or
non-2xx status, or validate the response in restoreBackup and reject failure
payloads, allowing the existing catch path to handle errors instead of closing
the dialog and replacing the container with plain text.

---

Nitpick comments:
In `@src/class-tiny-logger.php`:
- Around line 182-184: Update the logging flow in log() and rotate_logs() to
perform Tiny_Helpers::get_wp_filesystem() only once per log attempt, passing the
initialized filesystem result into rotate_logs() for reuse. Preserve the
existing early return when initialization fails and ensure both affected call
paths no longer emit duplicate failure messages.

In `@test/unit/TinyPluginBackupTest.php`:
- Line 174: Remove the unused $id and $file parameters from both
wp_create_image_subsizes callback declarations in TinyPluginBackupTest,
including the callbacks at both referenced locations, while preserving their
existing stub behavior.
- Around line 195-242: Update test_restore_backup_clears_all_sizes_metadata to
seed the stored Tiny metadata for both Tiny_Image::ORIGINAL and thumbnail, then
assert those entries are cleared after Tiny_Image::restore_backup(). Also assert
that the regenerated attachment metadata is persisted, while retaining the
existing regenerated thumbnail filesize assertion.
- Around line 167-193: Update test_restore_backup_restores_file_from_backup to
create a small, writable original testfile.png in the 2026/04 uploads directory
before invoking restore_backup(), while retaining the existing backup file and
final size assertion so the test verifies replacement of existing content rather
than creation in an empty directory.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 26e35339-fd2b-4153-8c4f-e7269e7c66b5

📥 Commits

Reviewing files that changed from the base of the PR and between 70b68bb and 73d1a04.

📒 Files selected for processing (14)
  • docs/hooks/tiny_image_before_compression.md
  • src/class-tiny-diagnostics.php
  • src/class-tiny-helpers.php
  • src/class-tiny-image.php
  • src/class-tiny-logger.php
  • src/class-tiny-plugin.php
  • src/css/admin.css
  • src/js/admin.js
  • src/views/compress-details-backup.php
  • src/views/compress-details.php
  • test/helpers/wordpress.php
  • test/integration/backup.spec.ts
  • test/integration/utils.ts
  • test/unit/TinyPluginBackupTest.php
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/views/compress-details-backup.php
  • src/views/compress-details.php
  • src/class-tiny-image.php
  • src/class-tiny-plugin.php

Comment on lines +209 to +214
if ( false === $wp_filesystem ) {
return new WP_Error(
'filesystem_unavailable',
__( 'WordPress filesystem could not be initialized.', 'tiny-compress-images' )
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

echo "== candidate files =="
git ls-files | rg '(^|/)(src/class-tiny-diagnostics\.php|.*diagnostic.*|.*tiny.*diagnostic.*)$' || true

echo "== relevant symbols and call sites =="
rg -n -C 8 'create_diagnostic_zip|download_diagnostics|download_zip|filesystem_unavailable|is_wp_error' src/class-tiny-diagnostics.php

echo "== file outline =="
ast-grep outline src/class-tiny-diagnostics.php --lang php

Repository: tinify/wordpress-plugin

Length of output: 3042


🏁 Script executed:

#!/bin/bash
set -eu

echo "== download and archive methods =="
sed -n '176,295p' src/class-tiny-diagnostics.php

echo "== all create_diagnostic_zip callers =="
rg -n -C 6 'create_diagnostic_zip\s*\(' .

echo "== diagnostics tests and related error handling =="
git ls-files | rg '(^|/)(test|tests|spec|phpunit|.*diagnostic.*)' || true
rg -n -C 5 'wp_die\(|wp_send_json_error\(|is_wp_error\(' src | head -n 240

Repository: tinify/wordpress-plugin

Length of output: 10307


🏁 Script executed:

#!/bin/bash
set -eu

echo "== filesystem helper implementation =="
rg -n -C 12 'function\s+get_wp_filesystem|get_wp_filesystem\s*\(' src test | head -n 220

echo "== WP_Error definition and filesystem exists implementations in repository =="
rg -n -C 8 'class\s+WP_Error|function\s+exists\s*\(' . | head -n 260

echo "== diagnostics unit test context =="
sed -n '1,145p' test/unit/Tiny_Diagnostics_Test.php

echo "== exact diagnostic error-return paths =="
python3 - <<'PY'
from pathlib import Path
p = Path("src/class-tiny-diagnostics.php")
text = p.read_text()
start = text.index("public function create_diagnostic_zip")
end = text.index("\n\t/**", start)
for i, line in enumerate(text[start:end].splitlines(), start=text[:start].count("\n")+1):
    if "return new WP_Error" in line or "return $temp_path" in line:
        print(f"{i}: {line.strip()}")
PY

Repository: tinify/wordpress-plugin

Length of output: 18033


🏁 Script executed:

#!/bin/bash
set -eu

echo "== helper return contract =="
sed -n '114,145p' src/class-tiny-helpers.php

echo "== test filesystem path contract =="
sed -n '1,35p' test/wp-includes-for-tests/file.php

echo "== standalone PHP probe: WP_Error passed to file_exists =="
if command -v php >/dev/null 2>&1; then
	php -d display_errors=1 -r '
		class WP_Error {}
		$path = new WP_Error();
		var_dump(file_exists($path));
	'
else
	echo "php unavailable"
fi

echo "== read-only structural verifier for create_diagnostic_zip =="
python3 - <<'PY'
from pathlib import Path
import re

text = Path("src/class-tiny-diagnostics.php").read_text()
start = text.index("public function create_diagnostic_zip")
end = text.index("\n\t/**", start)
body = text[start:end]

errors = re.findall(r"return\s+new\s+WP_Error\s*\(\s*'([^']+)'", body)
print("WP_Error return codes:", errors)
print("success return present:", "return $temp_path;" in body)
print("caller guards result before download_zip:",
      bool(re.search(r"\$zippath\s*=\s*\$this->create_diagnostic_zip\(\);\s*if\s*\(\s*is_wp_error\s*\(\s*\$zippath\s*\)", text)))
PY

Repository: tinify/wordpress-plugin

Length of output: 2592


Handle every WP_Error from create_diagnostic_zip() before calling download_zip().

The error paths zip_not_available, filesystem_unavailable, and zip_create_failed pass a WP_Error object to a method that requires a string path. Add an is_wp_error( $zippath ) guard and report the error message.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/class-tiny-diagnostics.php` around lines 209 - 214, Update the caller of
create_diagnostic_zip() before download_zip() to guard with is_wp_error(
$zippath ). When it returns a WP_Error, report its error message and stop before
passing the value to download_zip(); preserve the existing download flow for
valid string paths.

Comment on lines +136 to +138
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
error_log( 'Tiny Compress: Unable to initialize WordPress filesystem.' );
return false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- target file context ---'
sed -n '100,155p' src/class-tiny-helpers.php

printf '%s\n' '--- related filesystem calls and declarations ---'
rg -n -C 3 'WP_Filesystem|wp_filesystem|WP_Filesystem_Base' src tests .github 2>/dev/null || true

printf '%s\n' '--- repository metadata and dependency references ---'
rg -n -C 2 'wordpress|WP_Filesystem' composer.json composer.lock package.json phpunit.xml* README.md 2>/dev/null || true

Repository: tinify/wordpress-plugin

Length of output: 16525


🌐 Web query:

WordPress core WP_Filesystem function source assigns global wp_filesystem before connect returns false

💡 Result:

In the WordPress core function WP_Filesystem, the global variable $wp_filesystem is indeed assigned an instance of the chosen filesystem class before the $wp_filesystem->connect method is called [1][2][3]. The implementation follows this sequence: 1. The filesystem method is determined via get_filesystem_method [1][4]. 2. The corresponding filesystem class is instantiated and assigned to the global variable: $wp_filesystem = new $method($args); [1][5]. 3. The function then proceeds to perform checks (such as verifying potential errors) and defines connection timeouts [1][5][3]. 4. Finally, it attempts to connect using $wp_filesystem->connect [1][5][3]. If $wp_filesystem->connect returns false, the function returns false [1][5][3]. Because the assignment to the global $wp_filesystem has already occurred, the global variable remains populated with the instantiated object even if the connection attempt fails [1][2]. Consequently, developers should verify both that the function returned true and that the filesystem object successfully connected or is ready for use, rather than relying solely on the existence of the global variable [6][7][8].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
import urllib.request

url = "https://raw.githubusercontent.com/WordPress/wordpress-develop/trunk/src/wp-admin/includes/file.php"
text = urllib.request.urlopen(url, timeout=10).read().decode()

start = text.index("function WP_Filesystem(")
end = text.find("\n}", start) + 2
print(text[start:end])
PY

Repository: tinify/wordpress-plugin

Length of output: 2447


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
import ssl
import urllib.request

url = "https://raw.githubusercontent.com/WordPress/wordpress-develop/trunk/src/wp-admin/includes/file.php"
context = ssl._create_unverified_context()
text = urllib.request.urlopen(url, context=context, timeout=10).read().decode()

start = text.index("function WP_Filesystem(")
# Print only the function body region, bounded by the next documented top-level function.
next_function = text.find("\nfunction ", start + 1)
print(text[start:next_function if next_function != -1 else start + 5000])
PY

Repository: tinify/wordpress-plugin

Length of output: 3341


Check the return value from WP_Filesystem().

A failed connection can leave a WP_Filesystem_Base object in $wp_filesystem while WP_Filesystem() returns false. Capture the return value and require true before returning the global object.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/class-tiny-helpers.php` around lines 136 - 138, Update the filesystem
initialization flow around WP_Filesystem() to capture its return value and
require a true result before returning the global $wp_filesystem object.
Preserve the existing error logging and false return path when initialization
fails, even if $wp_filesystem contains an object.

Comment thread src/css/admin.css

.tiny-icon-backup {
flex-shrink: 0;
fill: currentColor;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the CSS keyword casing required by Stylelint.

Stylelint reports currentColor at Line 524 for value-keyword-case. Change it to currentcolor to prevent a lint failure.

Suggested fix
-	fill: currentColor;
+	fill: currentcolor;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fill: currentColor;
fill: currentcolor;
🧰 Tools
🪛 Stylelint (17.14.1)

[error] 524-524: Expected "currentColor" to be "currentcolor" (value-keyword-case)

(value-keyword-case)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/css/admin.css` at line 524, Update the fill declaration containing
currentColor to use the lowercase CSS keyword currentcolor, satisfying the
value-keyword-case Stylelint rule.

Source: Linters/SAST tools

Comment thread src/js/admin.js
Comment on lines +34 to +39
confirmButton.onclick = async () => {
const spinner = dialog.querySelector('.spinner');
try {
if (spinner) {
spinner.style.visibility = 'visible';
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Prevent concurrent restore requests.

The confirm handler remains active while restoreBackup is pending. A rapid double-click can start two restore requests. Tiny_Image::restore_backup() rewrites the original file and regenerates sizes for each request, which can cause duplicate work and concurrent metadata updates.

Add an in-flight guard and disable confirmButton before the request starts. Re-enable it in finally when retry is allowed.

Suggested fix
+      let restoreInFlight = false;
+
       confirmButton.onclick = async () => {
+        if (restoreInFlight) {
+          return;
+        }
+        restoreInFlight = true;
+        confirmButton.disabled = true;
+
         const spinner = dialog.querySelector('.spinner');
         try {
           ...
         } finally {
+          restoreInFlight = false;
+          confirmButton.disabled = false;
           if (spinner) {
             spinner.style.visibility = 'hidden';
           }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
confirmButton.onclick = async () => {
const spinner = dialog.querySelector('.spinner');
try {
if (spinner) {
spinner.style.visibility = 'visible';
}
let restoreInFlight = false;
confirmButton.onclick = async () => {
if (restoreInFlight) {
return;
}
restoreInFlight = true;
confirmButton.disabled = true;
const spinner = dialog.querySelector('.spinner');
try {
if (spinner) {
spinner.style.visibility = 'visible';
}
// existing restore workflow remains unchanged
} finally {
restoreInFlight = false;
confirmButton.disabled = false;
if (spinner) {
spinner.style.visibility = 'hidden';
}
}
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/js/admin.js` around lines 34 - 39, Update the confirmButton.onclick
handler to add an in-flight guard and disable confirmButton before invoking
restoreBackup, preventing rapid duplicate requests. In the handler’s finally
block, re-enable the button only when the existing retry flow allows another
attempt, while preserving the current spinner and restore behavior.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants