mirror of
https://github.com/WordPress/create-block-theme.git
synced 2026-07-27 21:48:03 +08:00
* tests: add tiny.png and evil.php.txt fixtures for media validation * docs(agents): update test:php commands to test:unit:php * Add CBT_Theme_Media::is_allowed_media_url() extension allowlist * Add CBT_Theme_Media::is_allowed_media_file() MIME allowlist * Apply media URL + file allowlist in add_media_to_local * Add CBT_Theme_Fonts::is_allowed_font_url() extension allowlist * Add CBT_Theme_Fonts::is_allowed_font_file() MIME allowlist * Apply font URL + file allowlist in copy_font_assets_to_theme * Allow font/sfnt and application/vnd.ms-opentype for TTF/OTF MIME detection * Reject multi-extension polyglots in URL allowlist * Apply media+font allowlist to zip export sinks in theme-zip * Add positive end-to-end tests for media and font sinks * Verify downloaded fonts by magic bytes instead of libmagic MIME * Pass $font_src string to download_url instead of $font_face src array Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Stream downloaded media into zip and clean up tmp file Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Strip query string before deriving folder path from media URL Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Restore braces and is_wp_error check dropped by Copilot suggestions Two recent Copilot suggestion commits accidentally removed structural code while making single-line changes: -c045da7("Pass $font_src to download_url") dropped the `} else {` and the `is_wp_error( $tmp_file )` guard. -617f6ec("Stream downloaded media into zip") dropped the closing brace of the `foreach` in add_media_to_zip(). Both broke PHP parse on every CI matrix. Restoring the missing statements so the file is valid again. * lint: align assignment operators in get_media_folder_path_from_url * Strip query string from filename before renaming downloaded media Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Strip query string from font URL before deriving filename Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Strip query string from font URL before deriving filename in zip Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Restore loop brace and sanitize_title dropped by Copilot suggestions Two more Copilot suggestion commits dropped surrounding context lines: -ad31fe4("Strip query string from filename before renaming downloaded media") removed the closing brace of the foreach loop in add_media_to_local(), causing fatal lint errors. -860f1bc("Strip query string from font URL before deriving filename in zip") removed the `$font_family_dir_name = sanitize_title(...)` assignment, leaving an undefined variable used immediately below. * Remove dead pre-loop assignments in add_activated_fonts_to_zip `$font_filename = basename( $font_face['src'] )` threw TypeError on PHP 8+ when src was already an array (valid per Theme JSON spec) and was redefined inside the inner src loop anyway. The adjacent `$font_dir = wp_get_font_dir()` was likewise redundant — also redefined inside the inner loop. Removing both. * Verify downloaded media by magic bytes instead of libmagic MIME wp_check_filetype_and_ext() was inconsistent with the URL allowlist: SVG isn't in WP Core's default mime registry at all, and WP maps wmv to video/x-ms-wmv / avi to video/avi while the post-check allowlist had only video/x-msvideo. Legitimate SVG/WMV/AVI URLs passed preflight and then silently failed post-download. Switching media to magic-byte verification (matching the font sink) removes the dependency on WP Core's mime registry and libmagic versions. Recognised formats: JPEG, PNG, GIF, WebP, SVG, MP4/M4V/MOV/ 3GP/3G2 (via ftyp), WebM, OGV, WMV (ASF), AVI (RIFF), MPEG. * Add regression tests for media asset validation * Fix media validation bugs found by scruffian Three fixes for issues found via regression tests in8da66ff: 1. is_allowed_media_file() now requires the downloaded bytes to match the URL extension specifically — a .jpg URL with SVG/MP4/anything-else bytes is rejected. Previously any allowed magic was accepted, so a browser MIME-sniffing the on-disk .jpg containing SVG could render it as SVG (with all the script-execution surface that brings). 2. make_relative_media_url() now derives the filename from the parsed URL path instead of raw basename(). A URL like cat.jpg?/evil.php previously produced `/assets/images/evil.php` in exported template markup because basename() treats query-string slashes as path separators. 3. add_media_to_zip() reads the downloaded bytes into memory and unlinks the tmp file BEFORE adding to the archive via addFromStringToTheme(). The previous `addFileToTheme() + unlink` sequence broke because ZipArchive defers reading files added via addFile() until close() — by which time the tmp file was gone. * lint: suppress NoSilencedErrors warning on ZipArchive::close in test * Skip zip tests gracefully when ZipArchive extension is unavailable Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Guard localhost retry against missing host/port keys parse_url() omits the 'port' key when the URL has no explicit port (e.g. `http://localhost/foo`). The retry-on-localhost block in both add_media_to_zip() and add_media_to_local() read $parsed_url['port'] unconditionally, raising an undefined-index notice when the initial download failed for such a URL. Guard with isset() on both keys. * Match font magic bytes against URL extension specifically Previously any recognised font magic was accepted, so a .woff2 URL returning TTF/WOFF/OTF/EOT bytes would pass and be saved under the .woff2 filename. This defeated the extension+MIME allowlist intent and would produce broken assets in the exported theme/zip (browsers would fail to load a WOFF2-named file containing TTF content). The check now switches on the URL extension and validates the magic matches THAT specific format. Mirrors the same fix already applied to is_allowed_media_file(). * Align .mpeg handling across URL, folder, and file-content checks is_allowed_media_file() accepted both .mpg and .mpeg, but is_allowed_media_url() and get_media_folder_path_from_url() only listed .mpg. A template with a .mpeg video would get rewritten to a local asset URL, then skipped before download — leaving the exported theme pointing at a missing file. Add .mpeg to the URL allowlist and the folder mapping so all three lists agree (matching WP Core's wp_get_mime_types which maps mpeg|mpg|mpe to video/mpeg). Also drops a pre-existing duplicate 'ogv' entry in the folder mapping that was noticed while editing. * Stream font bytes into zip before unlinking tmp file Same fix as add_media_to_zip: ZipArchive::addFile() defers reading the file until close() is called, so unlinking the download_url tmp file immediately after addFileToTheme() left an empty entry in the final archive when the zip was finalised. Read the bytes into memory, unlink, then addFromStringToTheme() — mirrors the pattern in add_media_to_zip(). Only the remote-download branch is affected; the local-copy branch points at a permanent file in the font library so its addFileToTheme() call stays. * Tweak comment * Allow AVIF images alongside JPEG/PNG/GIF/WebP/SVG AVIF is a WordPress-supported image format but was missing from the URL allowlist, folder mapping, and magic-byte check. The result: make_template_images_local() rewrote .avif URLs to local references, but the post-rewrite download was rejected — leaving exported themes pointing at missing assets. AVIF uses the ISO BMFF container (like MP4) with 'avif' or 'avis' as the major brand at offset 8. Added the brand-specific magic check alongside the URL/folder allowlist entries, plus tests for both AVIF still-image ('avif') and image-sequence ('avis') brands. * Drop rejected font sources from exported families Previously a font source that failed the URL allowlist or post-download MIME check was skipped via continue, but the original $font_src stayed in $font_face['src']. copy_activated_fonts_to_theme() then merged that family into theme.json — leaving the user's activated font pointing at an uncopied (and possibly disallowed) source while the user custom settings were cleared. Both copy_font_assets_to_theme() and add_activated_fonts_to_zip() now build a fresh srcs list and only retain entries that successfully copied or were already file:./ asset paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Only rewrite media URLs after successful asset validation * Accept AVIF compatible-brands in addition to the major brand Some AVIF files (typically those emitted by HEIF-derived tooling) set the major brand to mif1/miaf and only list avif/avis in the compatible brands. Parsing only the major brand at offset 8 would false-reject them. Scan the full FileTypeBox brand list (major brand plus compatible brands at offsets 16+) for any AVIF brand. HEIC files (no avif/avis anywhere) are still rejected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Magic-byte check the local-copy font branch too Both copy_font_assets_to_theme() and add_activated_fonts_to_zip() treated a font src under the WP user-fonts directory as trusted — copying / zipping the bytes without ever inspecting them. Anything that ended up in that directory through a separate flow (a polyglot upload via another plugin, manual write, etc.) would be promoted to the exported theme as-is just because the URL extension was on the allowlist. Run the same is_allowed_font_file() check on the local source before copying / zipping. If the bytes don't match the URL extension's font format, drop the source from the returned families. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Localize media URLs via the block parser, not str_replace Plain str_replace on the whole template would substring-match a shorter validated URL inside a longer rejected URL when one is a prefix of the other (e.g. `photo.png` inside `photo.png.php`). That silently rewrote the rejected URL to a missing local asset and bypassed the validated-media guard. Walk the parsed block tree instead: WP_HTML_Tag_Processor handles img / video src+poster and inline `background-image:url(...)`, while direct array access handles block-comment JSON attrs. Replacements only touch values that exactly match a validated URL. The rewrites embed literal PHP open/close tags that must end up verbatim in the export, but both set_attribute() and wp_json_encode() (inside serialize_blocks) would escape `<` / `>` / quotes. Route the rewrites through opaque, URL-shaped placeholders so they survive both passes, then strtr() them back to the raw PHP at the end. The placeholder is a root-relative path because set_attribute prefixes schemeless values with `http://`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Ben Dwyer <ben@scruffian.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
97 lines
3.4 KiB
Markdown
97 lines
3.4 KiB
Markdown
# AGENTS.md
|
|
|
|
This file provides guidance to AI coding agents working in this repository.
|
|
|
|
## Repository Overview
|
|
|
|
This is a WordPress plugin that allows you to create block themes from within the WordPress Editor. The main purpose of the plugin is to provide additional functionality on top of the existing theme features in the Editor. See README.md for more details.
|
|
|
|
The main features include:
|
|
|
|
- Export the activated theme with all the user's changes made in the Editor.
|
|
- Create a new theme, blank theme, child theme, or style variation from the Editor.
|
|
- Option to add all images used in templates to the theme's `assets` folder.
|
|
- Option to ensure the block markup used in templates and patterns is export-ready.
|
|
- Option to make most strings used in templates and patterns translate-ready.
|
|
|
|
## Tech Stack
|
|
|
|
- WordPress
|
|
- PHP
|
|
- JavaScript
|
|
- HTML
|
|
- CSS
|
|
|
|
See CONTRIBUTING.md for more details on the tech stack and development setup.
|
|
|
|
## Directory Structure
|
|
|
|
- `src/`: Source code for the plugin, including the main JavaScript files and utilities.
|
|
- `plugin-sidebar.js`: Entry point for the Block Editor plugin sidebar (loads in the Site Editor only).
|
|
- `admin-landing-page.js`: Entry point for the React app under Appearance > Create Block Theme.
|
|
- `assets/`: Assets for the plugin, e.g. screenshots for documentation.
|
|
- `includes/`: Includes for the plugin. This is where the main plugin code is located.
|
|
- `includes/create-theme/`: All main PHP logic, organised as `CBT_`-prefixed static utility classes (e.g. `CBT_Theme_JSON`, `CBT_Theme_Templates`, `CBT_Theme_Fonts`).
|
|
- `src/test/`: JavaScript unit tests.
|
|
- `test/`: JavaScript Jest configuration.
|
|
- `tests/`: PHP unit tests for the plugin.
|
|
- `vendor/`: Vendor files for the plugin, including PHP dependencies.
|
|
|
|
## Commands
|
|
|
|
```bash
|
|
# Install Node dependencies
|
|
npm install
|
|
|
|
# Install Composer dependencies
|
|
composer install
|
|
|
|
# Build the plugin
|
|
npm run build
|
|
|
|
# Watch for changes and rebuild the plugin
|
|
npm run start
|
|
|
|
# Run the PHP unit tests (requires Docker; setup must succeed first)
|
|
npm run test:unit:php:setup
|
|
npm run test:unit:php
|
|
|
|
# Run the JavaScript unit tests
|
|
npm run test:unit
|
|
|
|
# Run the linters
|
|
npm run lint:php
|
|
npm run lint:js
|
|
npm run lint:css
|
|
npm run lint:md-docs
|
|
|
|
# Format code
|
|
npm run format
|
|
```
|
|
|
|
Before committing, run all linters and format. These are all enforced in CI.
|
|
|
|
## Conventions to Follow
|
|
|
|
- Use the WordPress coding standards.
|
|
- Use the WordPress block editor coding standards.
|
|
|
|
## Common Pitfalls
|
|
|
|
- Always keep in mind that anything in this plugin could be migrated to the WordPress Editor (Gutenberg).
|
|
- This plugin can be run on sites with or without the Gutenberg plugin installed.
|
|
- The plugin UI is gated using `wp_is_block_theme()`, which means nothing will appear on non-block themes.
|
|
- PHP code that touches theme.json must check for the `IS_GUTENBERG_PLUGIN` constant and use `WP_Theme_JSON_Gutenberg` when available, falling back to core `WP_Theme_JSON`.
|
|
|
|
## PR instructions
|
|
|
|
- Ensure build passes.
|
|
- Fix all formatting and linting issues; these are enforced through CI in PRs.
|
|
|
|
## Documentation and Links
|
|
|
|
- README.md for the plugin README.
|
|
- CONTRIBUTING.md for the plugin contributing guidelines.
|
|
- [Plugin Documentation](https://wordpress.org/plugins/create-block-theme/)
|
|
- [Plugin Repository](https://github.com/WordPress/create-block-theme)
|
|
- [Plugin Support](https://wordpress.org/support/plugin/create-block-theme/)
|