From 765aaf96c933cfc4a31f5bb6ab6eab3a9f636d59 Mon Sep 17 00:00:00 2001 From: artpi Date: Wed, 20 May 2026 13:44:09 +0200 Subject: [PATCH 1/2] Prepare Push MD for WordPress.org submission --- .github/workflows/publish.yml | 16 +-- .github/workflows/push-md-plugin-check.yml | 41 ++++++ .gitignore | 2 + bin/build-plugins.sh | 110 +++++++++++++++ bin/inspect-push-md-zip.sh | 57 ++++++++ bin/push-md-toolkit-manifest.txt | 6 +- .../Filesystem/Tests/WpdbFilesystemTest.php | 6 + .../Filesystem/class-wpdbfilesystem.php | 25 +++- .../Markdown/Tests/MarkdownConsumerTest.php | 43 ++++++ .../Markdown/class-markdownconsumer.php | 133 +++++++++++++++--- plugins/push-md/Tests/ci-mu-test-helper.php | 6 +- plugins/push-md/class-pmd-plugin.php | 6 +- plugins/push-md/class-pmd-seeder.php | 14 +- plugins/push-md/functions.php | 4 +- plugins/push-md/push-md-dev-bootstrap.php | 2 +- plugins/push-md/push-md-phar-bootstrap.php | 2 +- plugins/push-md/push-md-toolkit-bootstrap.php | 10 +- plugins/push-md/push-md-toolkit-loader.php | 12 +- plugins/push-md/readme.txt | 4 + plugins/push-md/uninstall.php | 16 +-- 20 files changed, 436 insertions(+), 79 deletions(-) create mode 100644 .github/workflows/push-md-plugin-check.yml create mode 100755 bin/inspect-push-md-zip.sh diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 93c9fd9d..b86c146e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -89,21 +89,7 @@ jobs: run: bash bin/build-plugins.sh - name: Inspect Push MD plugin zip - shell: bash - run: | - set -euo pipefail - - test -f dist/plugins/push-md.zip - zipinfo -1 dist/plugins/push-md.zip | tee /tmp/push-md-zip-contents.txt - - grep -Fxq 'push-md/push-md.php' /tmp/push-md-zip-contents.txt - grep -Fxq 'push-md/readme.txt' /tmp/push-md-zip-contents.txt - grep -Fxq 'push-md/php-toolkit/vendor/composer/ClassLoader.php' /tmp/push-md-zip-contents.txt - - ! grep -Eq '^push-md/(Tests|docker-demo|docs)/' /tmp/push-md-zip-contents.txt - ! grep -Fxq 'push-md/blueprint-e2e.json' /tmp/push-md-zip-contents.txt - ! grep -Fxq 'push-md/push-md-dev-bootstrap.php' /tmp/push-md-zip-contents.txt - ! grep -Fxq 'push-md/push-md-phar-bootstrap.php' /tmp/push-md-zip-contents.txt + run: bin/inspect-push-md-zip.sh dist/plugins/push-md.zip - name: Upload Push MD release asset env: diff --git a/.github/workflows/push-md-plugin-check.yml b/.github/workflows/push-md-plugin-check.yml new file mode 100644 index 00000000..16355871 --- /dev/null +++ b/.github/workflows/push-md-plugin-check.yml @@ -0,0 +1,41 @@ +name: Push MD Plugin Check + +on: + push: + branches: [ trunk ] + pull_request: + +jobs: + plugin-check: + name: Push MD Plugin Check + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.1' + extensions: mbstring, json + coverage: none + tools: composer:v2 + + - name: Install Composer dependencies + run: composer install --no-interaction --prefer-dist --no-progress --optimize-autoloader + + - name: Build Push MD plugin zip + run: bash bin/build-plugins.sh + + - name: Unpack Push MD plugin zip + run: | + rm -rf .plugin-check-build + mkdir .plugin-check-build + unzip -q dist/plugins/push-md.zip -d .plugin-check-build + + - name: Run Plugin Check + uses: wordpress/plugin-check-action@v1 + with: + build-dir: './.plugin-check-build/push-md' + slug: 'push-md' diff --git a/.gitignore b/.gitignore index e0f5561f..e42173c5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ vendor/ **/*/vendor/ +.DS_Store +**/.DS_Store opfs/ node_modules .cursor diff --git a/bin/build-plugins.sh b/bin/build-plugins.sh index 15d7f4c4..97628000 100644 --- a/bin/build-plugins.sh +++ b/bin/build-plugins.sh @@ -27,6 +27,9 @@ copy_pmd_toolkit_path() { if [ -d "$source_path" ]; then mkdir -p "$target_path" rsync -a \ + --exclude='.DS_Store' \ + --include='LICENSE.md' \ + --include='license.md' \ --exclude='*.dist' \ --exclude='*.json' \ --exclude='*.lock' \ @@ -47,9 +50,34 @@ copy_pmd_toolkit_path() { --exclude='tests/' \ --exclude='Tests/' \ --exclude='Test/' \ + --exclude='class-markdownimporter.php' \ --exclude='vendor-bin/' \ --exclude='vendor-patched/autoload.php' \ + --exclude='vendor-patched/bin/' \ --exclude='vendor-patched/composer/' \ + --exclude='vendor-patched/webuni/' \ + --exclude='vendor-patched/symfony/yaml/' \ + --exclude='vendor-patched/league/commonmark/src/Extension/Attributes/' \ + --exclude='vendor-patched/league/commonmark/src/Extension/DefaultAttributes/' \ + --exclude='vendor-patched/league/commonmark/src/Extension/DescriptionList/' \ + --exclude='vendor-patched/league/commonmark/src/Extension/Embed/' \ + --exclude='vendor-patched/league/commonmark/src/Extension/Footnote/' \ + --exclude='vendor-patched/league/commonmark/src/Extension/FrontMatter/' \ + --exclude='vendor-patched/league/commonmark/src/Extension/HeadingPermalink/' \ + --exclude='vendor-patched/league/commonmark/src/Extension/Mention/' \ + --exclude='vendor-patched/league/commonmark/src/Extension/SmartPunct/' \ + --exclude='vendor-patched/league/commonmark/src/Extension/TableOfContents/' \ + --exclude='vendor-patched/nette/utils/src/HtmlStringable.php' \ + --exclude='vendor-patched/nette/utils/src/SmartObject.php' \ + --exclude='vendor-patched/nette/utils/src/Translator.php' \ + --exclude='vendor-patched/nette/utils/src/Utils/FileSystem.php' \ + --exclude='vendor-patched/nette/utils/src/Utils/Finder.php' \ + --exclude='vendor-patched/nette/utils/src/Utils/Html.php' \ + --exclude='vendor-patched/nette/utils/src/Utils/Image*.php' \ + --exclude='vendor-patched/nette/utils/src/Utils/Json.php' \ + --exclude='vendor-patched/nette/utils/src/Utils/Paginator.php' \ + --exclude='vendor-patched/nette/utils/src/Utils/Random.php' \ + --exclude='vendor-patched/nette/utils/src/Iterators/' \ "$source_path/" \ "$target_path/" elif [ -f "$source_path" ]; then @@ -210,6 +238,85 @@ pmd_toolkit_write_files( $target_dir, $files ); PHP } +harden_pmd_bundled_php() { + local target_dir=$1 + + PMD_BUNDLE_TARGET="$target_dir" php <<'PHP' +getExtension() ) ) { + continue; + } + + $path = $file->getPathname(); + $relative_path = substr( $path, strlen( $target_dir ) + 1 ); + $code = file_get_contents( $path ); + if ( false === $code ) { + continue; + } + + if ( 0 === strpos( $relative_path, 'php-toolkit/' ) ) { + $code = push_md_add_phpcs_disable( $code, $phpcs_disable ); + } + + $code = push_md_add_direct_access_guard( $code, $guard ); + file_put_contents( $path, $code ); +} +PHP +} + copy_pmd_php_toolkit_bundle() { local target_dir=$1 local manifest_file="$PROJECT_DIR/bin/push-md-toolkit-manifest.txt" @@ -255,6 +362,7 @@ copy_pmd_php_toolkit_bundle() { mkdir -p $DIST_DIR/push-md rsync -a \ + --exclude='.DS_Store' \ --exclude='Tests/' \ --exclude='docker-demo/' \ --exclude='docs/' \ @@ -264,7 +372,9 @@ rsync -a \ "$PROJECT_DIR/plugins/push-md/" \ "$DIST_DIR/push-md/" copy_pmd_php_toolkit_bundle "$DIST_DIR/push-md/php-toolkit" +harden_pmd_bundled_php "$DIST_DIR/push-md" cd $DIST_DIR zip -r push-md.zip push-md/ +bash "$PROJECT_DIR/bin/inspect-push-md-zip.sh" "$DIST_DIR/push-md.zip" cd $PROJECT_DIR rm -rf $DIST_DIR/push-md diff --git a/bin/inspect-push-md-zip.sh b/bin/inspect-push-md-zip.sh new file mode 100755 index 00000000..e2004b3e --- /dev/null +++ b/bin/inspect-push-md-zip.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +set -euo pipefail + +ZIP_PATH="${1:-dist/plugins/push-md.zip}" + +if [ ! -f "$ZIP_PATH" ]; then + echo "Missing Push MD zip: $ZIP_PATH" >&2 + exit 1 +fi + +CONTENTS_FILE="$(mktemp)" +trap 'rm -f "$CONTENTS_FILE"' EXIT + +zipinfo -1 "$ZIP_PATH" > "$CONTENTS_FILE" + +require_file() { + local path="$1" + if ! grep -Fxq "$path" "$CONTENTS_FILE"; then + echo "Push MD zip is missing required file: $path" >&2 + exit 1 + fi +} + +reject_pattern() { + local pattern="$1" + local message="$2" + local matches + + matches="$(grep -E "$pattern" "$CONTENTS_FILE" || true)" + if [ -n "$matches" ]; then + echo "$message" >&2 + echo "$matches" >&2 + exit 1 + fi +} + +require_file 'push-md/push-md.php' +require_file 'push-md/readme.txt' +require_file 'push-md/php-toolkit/vendor/composer/ClassLoader.php' +require_file 'push-md/php-toolkit/components/Markdown/class-markdownconsumer.php' +require_file 'push-md/php-toolkit/components/Markdown/vendor-patched/league/commonmark/LICENSE' +require_file 'push-md/php-toolkit/components/Markdown/vendor-patched/league/config/LICENSE.md' +require_file 'push-md/php-toolkit/components/Markdown/vendor-patched/nette/schema/license.md' +require_file 'push-md/php-toolkit/components/Markdown/vendor-patched/nette/utils/license.md' + +reject_pattern '(^|/)\.DS_Store$' 'Push MD zip must not contain macOS metadata files.' +reject_pattern '\.phar$' 'Push MD zip must not contain PHAR archives.' +reject_pattern '^push-md/(Tests|docker-demo|docs)/' 'Push MD zip must not contain development-only plugin directories.' +reject_pattern '^push-md/(blueprint-e2e\.json|push-md-dev-bootstrap\.php|push-md-phar-bootstrap\.php)$' 'Push MD zip contains development or PHAR bootstrap files.' +reject_pattern '(^|/)(composer\.(json|lock)|package(-lock)?\.json|phpunit\.xml(\.dist)?|phpcs\.xml|rector\.php)$' 'Push MD zip contains source-only project metadata.' +reject_pattern 'components/Markdown/class-markdownimporter\.php$' 'Push MD zip contains the unused Markdown importer.' +reject_pattern 'vendor-patched/(bin/|composer/|webuni/|symfony/yaml/)' 'Push MD zip contains pruned vendor support files or front matter/YAML dependencies.' +reject_pattern 'vendor-patched/league/commonmark/src/Extension/(Attributes|DefaultAttributes|DescriptionList|Embed|Footnote|FrontMatter|HeadingPermalink|Mention|SmartPunct|TableOfContents)/' 'Push MD zip contains pruned CommonMark extensions.' +reject_pattern 'vendor-patched/nette/utils/src/Iterators/' 'Push MD zip contains pruned Nette iterator utilities.' + +echo "Push MD zip contents look submission-ready: $ZIP_PATH" diff --git a/bin/push-md-toolkit-manifest.txt b/bin/push-md-toolkit-manifest.txt index f64550c2..c576ff57 100644 --- a/bin/push-md-toolkit-manifest.txt +++ b/bin/push-md-toolkit-manifest.txt @@ -1,7 +1,7 @@ # Push MD ships a purpose-built toolkit bundle, not the entire monorepo. -# Keep Markdown vendored code intact for now; the rest of this list is the -# runtime surface Push MD uses for Git storage, Git Smart HTTP, and block -# markup conversion. +# The build script prunes unused Markdown/front matter vendor files from this +# readable runtime bundle. The rest of this list is the runtime surface Push MD +# uses for Git storage, Git Smart HTTP, and block markup conversion. components/Markdown/ diff --git a/components/Filesystem/Tests/WpdbFilesystemTest.php b/components/Filesystem/Tests/WpdbFilesystemTest.php index 206aa61b..0cb2f212 100644 --- a/components/Filesystem/Tests/WpdbFilesystemTest.php +++ b/components/Filesystem/Tests/WpdbFilesystemTest.php @@ -81,4 +81,10 @@ public function testGetMetaExposesTableNames() { $this->assertSame( 'pmd_files', $meta['files_table'] ); $this->assertSame( 'pmd_directory_entries', $meta['entries_table'] ); } + + public function testRejectsUnsafeTablePrefix() { + $this->expectException( InvalidArgumentException::class ); + + WpdbFilesystem::create( $this->wpdb, 'pmd_;DROP_' ); + } } diff --git a/components/Filesystem/class-wpdbfilesystem.php b/components/Filesystem/class-wpdbfilesystem.php index f9f3ffaa..92114453 100644 --- a/components/Filesystem/class-wpdbfilesystem.php +++ b/components/Filesystem/class-wpdbfilesystem.php @@ -2,9 +2,13 @@ namespace WordPress\Filesystem; -// Table names are configured per-instance and cannot be passed through -// $wpdb->prepare(). All user-supplied values use placeholders. +// Table names are configured per-instance, validated as SQL identifiers, and +// interpolated only after validation. All user-supplied values use placeholders. +// Direct queries are intentional here: this class is a private Git object store. // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared +// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange +// phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter +// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped use Exception; use WordPress\ByteStream\MemoryPipe; @@ -63,16 +67,25 @@ public static function create( $wpdb, $table_prefix ) { * import. The next call to `create()` will re-create the schema. */ public static function drop_tables( $wpdb, $table_prefix ) { - $files_table = $table_prefix . 'files'; - $entries_table = $table_prefix . 'directory_entries'; + $files_table = self::sanitize_table_identifier( $table_prefix . 'files' ); + $entries_table = self::sanitize_table_identifier( $table_prefix . 'directory_entries' ); $wpdb->query( "DROP TABLE IF EXISTS {$files_table}" ); $wpdb->query( "DROP TABLE IF EXISTS {$entries_table}" ); } private function __construct( $wpdb, $table_prefix ) { $this->wpdb = $wpdb; - $this->files_table = $table_prefix . 'files'; - $this->entries_table = $table_prefix . 'directory_entries'; + $this->files_table = self::sanitize_table_identifier( $table_prefix . 'files' ); + $this->entries_table = self::sanitize_table_identifier( $table_prefix . 'directory_entries' ); + } + + private static function sanitize_table_identifier( $table ) { + $table = (string) $table; + if ( ! preg_match( '/\A[A-Za-z0-9_]+\z/', $table ) ) { + throw new \InvalidArgumentException( 'Invalid database table identifier.' ); + } + + return $table; } public function get_root(): string { diff --git a/components/Markdown/Tests/MarkdownConsumerTest.php b/components/Markdown/Tests/MarkdownConsumerTest.php index 61007538..f88a1d28 100644 --- a/components/Markdown/Tests/MarkdownConsumerTest.php +++ b/components/Markdown/Tests/MarkdownConsumerTest.php @@ -209,6 +209,49 @@ public function test_frontmatter_extraction() { $this->assertEquals( $expected_metadata, $metadata ); } + public function test_frontmatter_subset_extraction() { + $markdown = <<consume(); + + $this->assertEquals( + array( + 'title' => array( "It's ready" ), + 'menu_order' => array( 0 ), + 'description' => array( '' ), + ), + $result->get_all_metadata() + ); + } + + public function test_frontmatter_nested_values_are_exposed_for_caller_validation() { + $markdown = <<consume(); + + $this->assertEquals( + array( + 'title' => array( array() ), + ), + $result->get_all_metadata() + ); + } + public function test_gutenberg_fence_is_preserved_as_block_markup() { $markdown = <<extract_frontmatter( $this->markdown ); + $environment = new Environment( array() ); $environment->addExtension( new CommonMarkCoreExtension() ); $environment->addExtension( new GithubFlavoredMarkdownExtension() ); - $environment->addExtension( - new \Webuni\FrontMatter\Markdown\FrontMatterLeagueCommonMarkExtension( - new \Webuni\FrontMatter\FrontMatter() - ) - ); - $parser = new MarkdownParser( $environment ); - $document = $parser->parse( $this->markdown ); - $this->frontmatter = array(); - foreach ( $document->data->export() as $key => $value ) { - if ( 'attributes' === $key && empty( $value ) ) { - // The Frontmatter extension adds an 'attributes' key to the document data - // even when there is no actual "attributes" key in the frontmatter. - // - // Let's skip it when the value is empty. - continue; - } - // Use an array as a value to comply with the WP_Block_Markup_Converter interface. - $this->frontmatter[ $key ] = array( $value ); - } + $parser = new MarkdownParser( $environment ); + $document = $parser->parse( $markdown ); $walker = $document->walker(); while ( true ) { @@ -377,6 +362,116 @@ private function convert_markdown_to_blocks() { } } + private function extract_frontmatter( $markdown ) { + $this->frontmatter = array(); + + if ( ! preg_match( '/\A---\r?\n/', $markdown, $opening ) ) { + return $markdown; + } + + $body_start = strlen( $opening[0] ); + $offset = $body_start; + $length = strlen( $markdown ); + + while ( $offset < $length ) { + $line_end = strpos( $markdown, "\n", $offset ); + if ( false === $line_end ) { + $line = substr( $markdown, $offset ); + $next_line = $length; + } else { + $line = substr( $markdown, $offset, $line_end - $offset ); + $next_line = $line_end + 1; + } + + if ( '---' === rtrim( $line, "\r" ) ) { + $frontmatter = substr( $markdown, $body_start, $offset - $body_start ); + foreach ( $this->parse_frontmatter_block( $frontmatter ) as $key => $value ) { + // Use an array as a value to comply with the WP_Block_Markup_Converter interface. + $this->frontmatter[ $key ] = array( $value ); + } + + return substr( $markdown, $next_line ); + } + + $offset = $next_line; + } + + return $markdown; + } + + private function parse_frontmatter_block( $frontmatter ) { + $metadata = array(); + $lines = preg_split( "/\r\n|\n|\r/", $frontmatter ); + $count = count( $lines ); + + for ( $i = 0; $i < $count; $i++ ) { + $line = $lines[ $i ]; + if ( '' === trim( $line ) || preg_match( '/^\s*#/', $line ) ) { + continue; + } + if ( preg_match( '/^\s+/', $line ) ) { + continue; + } + if ( ! preg_match( '/^([A-Za-z0-9_.-]+)\s*:(?:\s*(.*))?$/', $line, $matches ) ) { + continue; + } + + $key = $matches[1]; + $raw = isset( $matches[2] ) ? trim( $matches[2] ) : ''; + if ( '' === $raw ) { + $nested = false; + $j = $i + 1; + while ( $j < $count && preg_match( '/^\s+/', $lines[ $j ] ) ) { + if ( '' !== trim( $lines[ $j ] ) ) { + $nested = true; + } + ++$j; + } + + if ( $nested ) { + $metadata[ $key ] = array(); + $i = $j - 1; + continue; + } + + $metadata[ $key ] = ''; + continue; + } + + $metadata[ $key ] = $this->parse_frontmatter_scalar( $raw ); + } + + return $metadata; + } + + private function parse_frontmatter_scalar( $raw ) { + if ( preg_match( '/^\[|\{|- /', $raw ) ) { + return array(); + } + + $first = substr( $raw, 0, 1 ); + $last = substr( $raw, -1 ); + + if ( '"' === $first && '"' === $last ) { + $decoded = json_decode( $raw ); + if ( JSON_ERROR_NONE === json_last_error() && is_string( $decoded ) ) { + return $decoded; + } + + return stripcslashes( substr( $raw, 1, -1 ) ); + } + + if ( "'" === $first && "'" === $last ) { + return str_replace( "''", "'", substr( $raw, 1, -1 ) ); + } + + if ( preg_match( '/^-?(?:0|[1-9][0-9]*)$/', $raw ) ) { + return (int) $raw; + } + + return $raw; + } + /** * Naive slugification. * diff --git a/plugins/push-md/Tests/ci-mu-test-helper.php b/plugins/push-md/Tests/ci-mu-test-helper.php index 2807fa53..885a7bbb 100644 --- a/plugins/push-md/Tests/ci-mu-test-helper.php +++ b/plugins/push-md/Tests/ci-mu-test-helper.php @@ -12,20 +12,20 @@ exit; } -add_filter( 'pmd_seed_batch_size', static function () { +add_filter( 'push_md_seed_batch_size', static function () { return 5; } ); // Zero-second budget forces budget_exhausted() to fire after every // batch, so the seeder reschedules itself even if the host can run // the whole thing in one tick. -add_filter( 'pmd_seed_time_budget_seconds', static function () { +add_filter( 'push_md_seed_time_budget_seconds', static function () { return 0.0; } ); // Reschedule "in the future" with no delay so wp-cli's // `cron event run --due-now` picks up the next tick on the very next // invocation. -add_filter( 'pmd_seed_tick_reschedule_seconds', static function () { +add_filter( 'push_md_seed_tick_reschedule_seconds', static function () { return 0; } ); diff --git a/plugins/push-md/class-pmd-plugin.php b/plugins/push-md/class-pmd-plugin.php index 1ca8c233..168ce815 100644 --- a/plugins/push-md/class-pmd-plugin.php +++ b/plugins/push-md/class-pmd-plugin.php @@ -83,11 +83,11 @@ public static function on_activation() { } public static function install_default_agent_skill() { - if ( ! self::guidelines_available() || ! function_exists( 'wp_install_skill' ) ) { + if ( ! self::guidelines_available() || ! function_exists( 'push_md_install_skill' ) ) { return; } - wp_install_skill( + push_md_install_skill( self::AGENT_SKILL_SOURCE, self::AGENT_SKILL_TITLE, 'Guide for coding agents working in a Push MD checkout of a WordPress site.', @@ -97,7 +97,7 @@ public static function install_default_agent_skill() { ) ); - wp_install_skill( + push_md_install_skill( self::TEMPLATE_EDITOR_SKILL_SOURCE, self::TEMPLATE_EDITOR_SKILL_TITLE, 'Edit Push MD block theme templates and template parts as raw Gutenberg HTML while preserving Site Editor compatibility.', diff --git a/plugins/push-md/class-pmd-seeder.php b/plugins/push-md/class-pmd-seeder.php index e41425e6..faa682ac 100644 --- a/plugins/push-md/class-pmd-seeder.php +++ b/plugins/push-md/class-pmd-seeder.php @@ -45,7 +45,7 @@ class PMD_Seeder { const STATE_OPTION = 'pmd_seed_state'; const PROGRESS_OPTION = 'pmd_seed_progress'; const SEED_BRANCH = 'refs/heads/_pmd_seed'; - const CRON_HOOK = 'pmd_seed_tick'; + const CRON_HOOK = 'push_md_seed_tick'; const LOCK_TRANSIENT = 'pmd_seed_lock'; const STATE_PENDING = 'pending'; @@ -66,21 +66,21 @@ public static function bootstrap() { /** * Resolve budget knobs through filters. The defaults are the * production values; tests and unusual hosts can shrink them via - * `pmd_seed_batch_size`, - * `pmd_seed_time_budget_seconds`, and - * `pmd_seed_tick_reschedule_seconds` to force the seeder to + * `push_md_seed_batch_size`, + * `push_md_seed_time_budget_seconds`, and + * `push_md_seed_tick_reschedule_seconds` to force the seeder to * span multiple cron ticks. */ private static function batch_size() { - return (int) apply_filters( 'pmd_seed_batch_size', self::BATCH_SIZE ); + return (int) apply_filters( 'push_md_seed_batch_size', self::BATCH_SIZE ); } private static function time_budget_seconds() { - return (float) apply_filters( 'pmd_seed_time_budget_seconds', self::TIME_BUDGET_SECONDS ); + return (float) apply_filters( 'push_md_seed_time_budget_seconds', self::TIME_BUDGET_SECONDS ); } private static function tick_reschedule_seconds() { - return (int) apply_filters( 'pmd_seed_tick_reschedule_seconds', self::TICK_RESCHEDULE_SECONDS ); + return (int) apply_filters( 'push_md_seed_tick_reschedule_seconds', self::TICK_RESCHEDULE_SECONDS ); } public static function on_activation() { diff --git a/plugins/push-md/functions.php b/plugins/push-md/functions.php index 7e178d6d..a76683a7 100644 --- a/plugins/push-md/functions.php +++ b/plugins/push-md/functions.php @@ -4,8 +4,8 @@ exit; } -if ( ! function_exists( 'wp_install_skill' ) ) { - function wp_install_skill( string $source_identifier, string $title, string $excerpt, string $content, array $extras = array() ) { +if ( ! function_exists( 'push_md_install_skill' ) ) { + function push_md_install_skill( string $source_identifier, string $title, string $excerpt, string $content, array $extras = array() ) { if ( '' === $source_identifier ) { return new WP_Error( 'missing_source_identifier', 'A non-empty $source_identifier is required.' ); } diff --git a/plugins/push-md/push-md-dev-bootstrap.php b/plugins/push-md/push-md-dev-bootstrap.php index 4877a87e..be6b0705 100644 --- a/plugins/push-md/push-md-dev-bootstrap.php +++ b/plugins/push-md/push-md-dev-bootstrap.php @@ -40,5 +40,5 @@ ); foreach ( $pmd_files as $pmd_file ) { - pmd_require_toolkit_file( md5( 'push-md:' . $pmd_file ), $pmd_file ); + push_md_require_toolkit_file( md5( 'push-md:' . $pmd_file ), $pmd_file ); } diff --git a/plugins/push-md/push-md-phar-bootstrap.php b/plugins/push-md/push-md-phar-bootstrap.php index 2c73319c..1d23989d 100644 --- a/plugins/push-md/push-md-phar-bootstrap.php +++ b/plugins/push-md/push-md-phar-bootstrap.php @@ -42,5 +42,5 @@ ); foreach ( $pmd_files as $pmd_file ) { - pmd_require_toolkit_file( md5( 'push-md:' . $pmd_file ), $pmd_file ); + push_md_require_toolkit_file( md5( 'push-md:' . $pmd_file ), $pmd_file ); } diff --git a/plugins/push-md/push-md-toolkit-bootstrap.php b/plugins/push-md/push-md-toolkit-bootstrap.php index 87abe583..2af31f42 100644 --- a/plugins/push-md/push-md-toolkit-bootstrap.php +++ b/plugins/push-md/push-md-toolkit-bootstrap.php @@ -6,7 +6,7 @@ require_once __DIR__ . '/push-md-toolkit-loader.php'; -function pmd_load_core_html_api() { +function push_md_load_core_html_api() { if ( class_exists( 'WP_HTML_Tag_Processor' ) && class_exists( 'WP_HTML_Processor' ) ) { return; } @@ -41,10 +41,10 @@ function pmd_load_core_html_api() { } } -function pmd_load_toolkit_bundle() { +function push_md_load_toolkit_bundle() { $pmd_toolkit = __DIR__ . '/php-toolkit'; - pmd_load_core_html_api(); + push_md_load_core_html_api(); if ( ! class_exists( 'Composer\\Autoload\\ClassLoader' ) ) { require_once $pmd_toolkit . '/vendor/composer/ClassLoader.php'; @@ -68,8 +68,8 @@ function pmd_load_toolkit_bundle() { $pmd_files = require $pmd_toolkit . '/vendor/composer/autoload_files.php'; foreach ( $pmd_files as $pmd_file_identifier => $pmd_file ) { - pmd_require_toolkit_file( $pmd_file_identifier, $pmd_file ); + push_md_require_toolkit_file( $pmd_file_identifier, $pmd_file ); } } -pmd_load_toolkit_bundle(); +push_md_load_toolkit_bundle(); diff --git a/plugins/push-md/push-md-toolkit-loader.php b/plugins/push-md/push-md-toolkit-loader.php index be37e339..db976169 100644 --- a/plugins/push-md/push-md-toolkit-loader.php +++ b/plugins/push-md/push-md-toolkit-loader.php @@ -4,16 +4,16 @@ exit; } -function pmd_require_toolkit_file( $pmd_file_identifier, $pmd_file ) { - if ( ! isset( $GLOBALS['__composer_autoload_files'] ) ) { - $GLOBALS['__composer_autoload_files'] = array(); +function push_md_require_toolkit_file( $push_md_file_identifier, $push_md_file ) { + if ( ! isset( $GLOBALS['push_md_composer_autoload_files'] ) ) { + $GLOBALS['push_md_composer_autoload_files'] = array(); } - if ( ! empty( $GLOBALS['__composer_autoload_files'][ $pmd_file_identifier ] ) ) { + if ( ! empty( $GLOBALS['push_md_composer_autoload_files'][ $push_md_file_identifier ] ) ) { return; } - $GLOBALS['__composer_autoload_files'][ $pmd_file_identifier ] = true; + $GLOBALS['push_md_composer_autoload_files'][ $push_md_file_identifier ] = true; - require_once $pmd_file; + require_once $push_md_file; } diff --git a/plugins/push-md/readme.txt b/plugins/push-md/readme.txt index e198a50e..4545be71 100644 --- a/plugins/push-md/readme.txt +++ b/plugins/push-md/readme.txt @@ -109,6 +109,10 @@ No. Push MD does not enable or force any authentication method. It works with Wo The built-in WordPress Application Passwords feature is the default way to use Git over HTTPS when it is available on the site. Other REST authentication plugins may also work if they authenticate the request as a WordPress user before Push MD's permission checks run. += Does Push MD require Composer or a PHAR archive? = + +No. Push MD includes the readable PHP Toolkit runtime files it needs under `php-toolkit/` in the plugin package. The plugin does not install Composer dependencies on the site and does not load an opaque PHAR archive. + = Does this sync my site to GitHub or another Git host? = No. Push MD makes WordPress itself behave like a Git remote for supported content. You can add GitHub, GitLab, Bitbucket, or another host as a separate remote in your local clone if your workflow needs that. diff --git a/plugins/push-md/uninstall.php b/plugins/push-md/uninstall.php index fc286e38..b02b514b 100644 --- a/plugins/push-md/uninstall.php +++ b/plugins/push-md/uninstall.php @@ -12,12 +12,12 @@ exit; } -pmd_uninstall_cleanup(); +push_md_uninstall_cleanup(); /** * Clean up every site in multisite installs, or the current site otherwise. */ -function pmd_uninstall_cleanup() { +function push_md_uninstall_cleanup() { if ( is_multisite() && function_exists( 'get_sites' ) && function_exists( 'switch_to_blog' ) && function_exists( 'restore_current_blog' ) ) { $offset = 0; $number = 100; @@ -33,7 +33,7 @@ function pmd_uninstall_cleanup() { foreach ( $site_ids as $site_id ) { switch_to_blog( (int) $site_id ); - pmd_uninstall_cleanup_site(); + push_md_uninstall_cleanup_site(); restore_current_blog(); } @@ -43,24 +43,24 @@ function pmd_uninstall_cleanup() { return; } - pmd_uninstall_cleanup_site(); + push_md_uninstall_cleanup_site(); } /** * Clean up Push MD data for the current site. */ -function pmd_uninstall_cleanup_site() { +function push_md_uninstall_cleanup_site() { delete_option( 'pmd_seed_state' ); delete_option( 'pmd_seed_progress' ); delete_transient( 'pmd_seed_lock' ); - wp_clear_scheduled_hook( 'pmd_seed_tick' ); - pmd_uninstall_drop_repository_tables(); + wp_clear_scheduled_hook( 'push_md_seed_tick' ); + push_md_uninstall_drop_repository_tables(); } /** * Drop the per-site Git object-store tables. */ -function pmd_uninstall_drop_repository_tables() { +function push_md_uninstall_drop_repository_tables() { global $wpdb; $table_prefix = preg_replace( '/[^A-Za-z0-9_]/', '', $wpdb->prefix . 'pmd_' ); From 92cf886b561dbe61e622d8f6b742023abffd995e Mon Sep 17 00:00:00 2001 From: artpi Date: Wed, 20 May 2026 13:58:26 +0200 Subject: [PATCH 2/2] Avoid activation redirect in Push MD --- plugins/push-md/push-md.php | 33 +++++++++++---------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/plugins/push-md/push-md.php b/plugins/push-md/push-md.php index c6411ae3..ab843d4c 100644 --- a/plugins/push-md/push-md.php +++ b/plugins/push-md/push-md.php @@ -39,31 +39,20 @@ register_activation_hook( __FILE__, array( 'PMD_Plugin', 'on_activation' ) ); -// After the user clicks Activate, send them straight to the seeder -// progress page so they can watch the import without hunting for a -// menu item. Skipped for bulk activations and CLI/AJAX flows so we -// only intercept the one-click admin "Activate" link. -add_action( - 'activated_plugin', - function ( $plugin ) { - if ( plugin_basename( PMD_PLUGIN_FILE ) !== $plugin ) { - return; - } - if ( wp_doing_ajax() || ( defined( 'WP_CLI' ) && WP_CLI ) ) { - return; - } - $action = isset( $_REQUEST['action'] ) ? sanitize_key( wp_unslash( $_REQUEST['action'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended - if ( '' !== $action && 'activate' !== $action ) { - return; - } - wp_safe_redirect( admin_url( 'tools.php?page=' . PMD_Admin::PAGE_SLUG ) ); - exit; - } -); - add_filter( 'plugin_action_links_' . plugin_basename( PMD_PLUGIN_FILE ), function ( $actions ) { + $actions = array_merge( + array( + 'push_md_open' => sprintf( + '%s', + esc_url( admin_url( 'tools.php?page=' . PMD_Admin::PAGE_SLUG ) ), + esc_html__( 'Open Push MD', 'push-md' ) + ), + ), + $actions + ); + $actions['push_md_landing_page'] = sprintf( '%s', esc_url( 'https://pushmd.blog/' ),