diff --git a/.github/workflows/build-plugin-with-ref.yml b/.github/workflows/build-plugin-with-ref.yml new file mode 100644 index 000000000..1124e973c --- /dev/null +++ b/.github/workflows/build-plugin-with-ref.yml @@ -0,0 +1,329 @@ +name: Build Accessibility Checker Plugin with Ref Param + +on: + workflow_dispatch: + inputs: + ref_param: + description: "Ref param string to set in EDAC_REF_PARAM (optional)" + required: false + type: string + pull_request: + types: [labeled] + release: + types: [created, published] + +permissions: + contents: write + pull-requests: write + +jobs: + build-plugin: + name: Build plugin zip + runs-on: ubuntu-latest + # Run on release, manual, or PR when a specific label is added + if: | + github.event_name == 'release' || + github.event_name == 'workflow_dispatch' || + (github.event_name == 'pull_request' && (contains(github.event.pull_request.labels.*.name, 'gha-build') || contains(github.event.pull_request.labels.*.name, 'gha-build-all'))) + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Determine mode and ref param for this run + id: setref + run: | + MODE="${{ github.event_name }}" + echo "mode=$MODE" >> $GITHUB_OUTPUT + # Manual run: take input if provided; else empty + if [ "$MODE" = "workflow_dispatch" ]; then + REF_INPUT="${{ github.event.inputs.ref_param }}" + echo "ref_param=${REF_INPUT}" >> $GITHUB_OUTPUT + # For manual mode: if ref is provided, skip primary build + if [ -n "$REF_INPUT" ]; then + echo "skip_primary=true" >> $GITHUB_OUTPUT + else + echo "skip_primary=false" >> $GITHUB_OUTPUT + fi + # Release: use fixed ref 'woocommerce' + elif [ "$MODE" = "release" ]; then + echo "ref_param=woocommerce" >> $GITHUB_OUTPUT + echo "skip_primary=false" >> $GITHUB_OUTPUT + # PR labeled: check which label + else + # Check if gha-build-all label is present (build both) + if echo "${{ github.event.pull_request.labels.*.name }}" | grep -q "gha-build-all"; then + echo "ref_param=woocommerce" >> $GITHUB_OUTPUT + echo "skip_primary=false" >> $GITHUB_OUTPUT + else + # gha-build label (build primary only) + echo "ref_param=" >> $GITHUB_OUTPUT + echo "skip_primary=false" >> $GITHUB_OUTPUT + fi + fi + + - name: Show selected mode/ref + run: | + echo "Triggered on: $GITHUB_EVENT_NAME" + echo "Mode: ${{ steps.setref.outputs.mode }}" + echo "Head ref: ${{ github.head_ref }}" + echo "PR number: ${{ github.event.pull_request.number }}" + echo "Release tag: ${{ github.event.release.tag_name }}" + echo "Ref param: ${{ steps.setref.outputs.ref_param }}" + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: package-lock.json + + - name: Cache node_modules + uses: actions/cache@v4 + with: + path: node_modules + key: node-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + node-${{ runner.os }}- + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + tools: composer + coverage: none + + - name: Install Composer dependencies + run: | + if [ -f composer.json ]; then composer install --no-dev --prefer-dist --no-progress --no-interaction; else echo "No composer.json"; fi + + - name: Install npm dependencies + run: | + if [ -f package.json ]; then + if [ -d node_modules ]; then + echo "node_modules cache hit, skipping install" + else + npm ci --prefer-offline --no-audit + fi + else + echo "No package.json" + fi + + - name: Extract plugin version and commit hash + id: version + run: | + VERSION=$(grep "Version:" accessibility-checker.php | head -1 | sed 's/.*Version:[[:space:]]*\([^ ]*\).*/\1/') + SHORT_SHA=$(git rev-parse --short HEAD) + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT + echo "Plugin version: $VERSION" + echo "Short commit hash: $SHORT_SHA" + + - name: Verify EDAC_REF_PARAM is empty before first build + if: steps.setref.outputs.skip_primary != 'true' + run: | + grep -n "define( 'EDAC_REF_PARAM', '' )" accessibility-checker.php || { echo "EDAC_REF_PARAM should be empty for first build"; exit 1; } + + - name: Build primary dist zip (empty ref) + if: steps.setref.outputs.skip_primary != 'true' + run: | + echo "Building primary zip with empty ref..." + npm run dist + ZIP_PATH=$(ls -1 dist/*.zip build/*.zip 2>/dev/null | head -n 1 || true) + if [ -z "$ZIP_PATH" ]; then + ZIP_PATH=$(ls -1 *.zip 2>/dev/null | head -n 1 || true) + fi + if [ -z "$ZIP_PATH" ]; then + echo "Error: Could not locate produced zip after npm run dist" >&2 + exit 1 + fi + mkdir -p builds + + # Construct the new filename based on trigger mode + VERSION="${{ steps.version.outputs.version }}" + SHORT_SHA="${{ steps.version.outputs.short_sha }}" + + if [ "${{ github.event_name }}" = "pull_request" ]; then + # PR: accessibility-checker-{version}-{prnumber}-{hash}.zip + PRIMARY_ZIP_NAME="accessibility-checker-${VERSION}-${{ github.event.pull_request.number }}-${SHORT_SHA}.zip" + elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + # Manual: accessibility-checker-{version}-{hash}.zip + PRIMARY_ZIP_NAME="accessibility-checker-${VERSION}-${SHORT_SHA}.zip" + elif [ "${{ github.event_name }}" = "release" ]; then + # Release: accessibility-checker-{version}.zip + PRIMARY_ZIP_NAME="accessibility-checker-${VERSION}.zip" + else + # Fallback + PRIMARY_ZIP_NAME="accessibility-checker-${VERSION}-${SHORT_SHA}.zip" + fi + + mv "$ZIP_PATH" "builds/$PRIMARY_ZIP_NAME" + PRIMARY_ZIP_BASENAME="${PRIMARY_ZIP_NAME%.zip}" + unzip -q "builds/$PRIMARY_ZIP_NAME" -d "builds/$PRIMARY_ZIP_BASENAME" + PRIMARY_ZIP_FOLDER="builds/$PRIMARY_ZIP_BASENAME" + PRIMARY_ZIP_NAME_NO_EXT="${PRIMARY_ZIP_NAME%.zip}" + echo "PRIMARY_ZIP_PATH=builds/$PRIMARY_ZIP_NAME" >> $GITHUB_ENV + echo "PRIMARY_ZIP_NAME=$PRIMARY_ZIP_NAME" >> $GITHUB_ENV + echo "PRIMARY_ZIP_NAME_NO_EXT=$PRIMARY_ZIP_NAME_NO_EXT" >> $GITHUB_ENV + echo "PRIMARY_ZIP_FOLDER=$PRIMARY_ZIP_FOLDER" >> $GITHUB_ENV + echo "Produced primary zip: builds/$PRIMARY_ZIP_NAME" + echo "Produced primary folder: $PRIMARY_ZIP_FOLDER" + + - name: Upload primary build artifact + if: steps.setref.outputs.skip_primary != 'true' + uses: actions/upload-artifact@v4 + with: + name: ${{ env.PRIMARY_ZIP_NAME_NO_EXT }} + path: ${{ env.PRIMARY_ZIP_FOLDER }} + if-no-files-found: error + + - name: Check if ref build is needed + id: check_ref + run: | + MODE="${{ steps.setref.outputs.mode }}" + REF_VALUE="${{ steps.setref.outputs.ref_param }}" + # For manual: build ref if ref is provided + # For release: always build ref (woocommerce) + # For PR: never build ref + if [ "$MODE" = "workflow_dispatch" ] && [ -n "$REF_VALUE" ]; then + echo "need_ref_build=true" >> $GITHUB_OUTPUT + echo "Ref build needed with value: $REF_VALUE" + elif [ "$MODE" = "release" ] && [ -n "$REF_VALUE" ]; then + echo "need_ref_build=true" >> $GITHUB_OUTPUT + echo "Ref build needed with value: $REF_VALUE" + else + echo "need_ref_build=false" >> $GITHUB_OUTPUT + echo "No ref build needed (mode=$MODE, ref='$REF_VALUE')" + fi + + - name: Update EDAC_REF_PARAM for second build + if: steps.check_ref.outputs.need_ref_build == 'true' + run: | + chmod +x ./scripts/update-ref-param.sh + ./scripts/update-ref-param.sh "${{ steps.setref.outputs.ref_param }}" + + - name: Verify EDAC_REF_PARAM change + if: steps.check_ref.outputs.need_ref_build == 'true' + run: | + grep -n "EDAC_REF_PARAM" accessibility-checker.php || { echo "EDAC_REF_PARAM not found"; exit 1; } + echo "Updated EDAC_REF_PARAM contents:" + grep "EDAC_REF_PARAM" accessibility-checker.php + + - name: Build ref dist zip (custom ref) + if: steps.check_ref.outputs.need_ref_build == 'true' + run: | + echo "Building ref zip with custom ref value..." + npm run dist + ZIP_PATH=$(ls -1 dist/*.zip build/*.zip 2>/dev/null | head -n 1 || true) + if [ -z "$ZIP_PATH" ]; then + ZIP_PATH=$(ls -1 *.zip 2>/dev/null | head -n 1 || true) + fi + if [ -z "$ZIP_PATH" ]; then + echo "Error: Could not locate produced zip after npm run dist" >&2 + exit 1 + fi + mkdir -p builds + + VERSION="${{ steps.version.outputs.version }}" + SHORT_SHA="${{ steps.version.outputs.short_sha }}" + REF_VALUE="${{ steps.setref.outputs.ref_param }}" + + # Ref build naming: accessibility-checker-{version}-ref-{refvalue}-{hash}.zip + REF_ZIP_NAME="accessibility-checker-${VERSION}-ref-${REF_VALUE}-${SHORT_SHA}.zip" + + mv "$ZIP_PATH" "builds/$REF_ZIP_NAME" + REF_ZIP_BASENAME="${REF_ZIP_NAME%.zip}" + unzip -q "builds/$REF_ZIP_NAME" -d "builds/$REF_ZIP_BASENAME" + REF_ZIP_FOLDER="builds/$REF_ZIP_BASENAME" + REF_ZIP_NAME_NO_EXT="${REF_ZIP_NAME%.zip}" + echo "REF_ZIP_PATH=builds/$REF_ZIP_NAME" >> $GITHUB_ENV + echo "REF_ZIP_NAME=$REF_ZIP_NAME" >> $GITHUB_ENV + echo "REF_ZIP_NAME_NO_EXT=$REF_ZIP_NAME_NO_EXT" >> $GITHUB_ENV + echo "REF_ZIP_FOLDER=$REF_ZIP_FOLDER" >> $GITHUB_ENV + echo "Produced ref zip: builds/$REF_ZIP_NAME" + echo "Produced ref folder: $REF_ZIP_FOLDER" + + - name: Upload ref build artifact + if: steps.check_ref.outputs.need_ref_build == 'true' + uses: actions/upload-artifact@v4 + with: + name: ${{ env.REF_ZIP_NAME_NO_EXT }} + path: ${{ env.REF_ZIP_FOLDER }} + if-no-files-found: error + + - name: Upload to release (both zips) + if: github.event_name == 'release' + uses: softprops/action-gh-release@v2 + with: + files: | + ${{ env.PRIMARY_ZIP_PATH }} + ${{ steps.check_ref.outputs.need_ref_build == 'true' && env.REF_ZIP_PATH || '' }} + fail_on_unmatched_files: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Comment on PR with primary build details + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const prNumber = context.payload.pull_request.number; + const runId = context.runId; + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; + const primaryName = process.env.PRIMARY_ZIP_NAME_NO_EXT; + + // Get the artifact ID for the download URL + const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: runId, + }); + + const artifact = artifacts.data.artifacts.find(a => a.name === primaryName); + const downloadUrl = artifact + ? `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}/artifacts/${artifact.id}` + : runUrl; + + const body = `โ Accessibility Checker build (primary only)\n\n- **Artifact**: [Download ${primaryName}.zip](${downloadUrl})\n- **Workflow run**: [View logs](${runUrl})`; + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body, + }); + + - name: Remove triggering label from PR + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const prNumber = context.payload.pull_request.number; + const labels = context.payload.pull_request.labels.map(l => l.name); + const labelToRemove = labels.includes('gha-build-all') ? 'gha-build-all' : 'gha-build'; + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + name: labelToRemove, + }); + console.log(`Removed label '${labelToRemove}' from PR #${prNumber}`); + } catch (e) { + console.log(`Could not remove label '${labelToRemove}' from PR #${prNumber}: ${e.message}`); + } + + - name: Summary + run: | + echo "=== Build Summary ===" + echo "Mode: ${{ steps.setref.outputs.mode }}" + echo "Primary zip (empty ref): ${{ env.PRIMARY_ZIP_PATH }}" + if [ "${{ steps.check_ref.outputs.need_ref_build }}" = "true" ]; then + echo "Ref zip (ref=${{ steps.setref.outputs.ref_param }}): ${{ env.REF_ZIP_PATH }}" + else + echo "Ref zip: Not built" + fi + if [ "${{ github.event_name }}" = "release" ]; then + echo "Uploaded to release: ${{ github.event.release.html_url }}" + fi diff --git a/.github/workflows/verify-hooks-docs.yml b/.github/workflows/verify-hooks-docs.yml index 030252883..7d4c5a842 100644 --- a/.github/workflows/verify-hooks-docs.yml +++ b/.github/workflows/verify-hooks-docs.yml @@ -1,12 +1,9 @@ name: Verify hooks docs on: - pull_request: - paths: - - '**.php' push: branches: - - develop + - 'release/**' jobs: verify-docs: diff --git a/accessibility-checker.php b/accessibility-checker.php index efe4c772a..911b19c99 100755 --- a/accessibility-checker.php +++ b/accessibility-checker.php @@ -10,7 +10,7 @@ * Plugin Name: Accessibility Checker * Plugin URI: https://a11ychecker.com * Description: Audit and check your website for accessibility before you hit publish. In-post accessibility scanner and guidance. - * Version: 1.34.0 + * Version: 1.35.0 * Requires PHP: 7.4 * Author: Equalize Digital * Author URI: https://equalizedigital.com @@ -36,7 +36,7 @@ // Current plugin version. if ( ! defined( 'EDAC_VERSION' ) ) { - define( 'EDAC_VERSION', '1.34.0' ); + define( 'EDAC_VERSION', '1.35.0' ); } // Current database version. @@ -72,6 +72,11 @@ define( 'EDAC_DEBUG', false ); } +// Default ref is empty - we don't ref links, but it can be filtered by providers. +if ( ! defined( 'EDAC_REF_PARAM' ) ) { + define( 'EDAC_REF_PARAM', '' ); +} + // SVG Icons. define( 'EDAC_SVG_IGNORE_ICON', file_get_contents( __DIR__ . '/assets/images/ignore-icon.svg' ) ); diff --git a/admin/class-admin.php b/admin/class-admin.php index 30c8b2545..7ad570e0d 100644 --- a/admin/class-admin.php +++ b/admin/class-admin.php @@ -50,6 +50,7 @@ public function init(): void { add_action( 'admin_enqueue_scripts', [ 'EDAC\Admin\Enqueue_Admin', 'enqueue' ] ); add_action( 'wp_trash_post', [ Purge_Post_Data::class, 'delete_post' ] ); add_action( 'save_post', [ Post_Save::class, 'delete_issue_data_on_post_trashing' ], 10, 3 ); + add_filter( 'edac_filter_generate_link_type_ref', [ $this, 'add_ref_param_to_links' ], 5, 1 ); $plugin_action_links = new Plugin_Action_Links(); $plugin_action_links->init_hooks(); @@ -96,4 +97,18 @@ private function init_ajax(): void { $frontend_highlight = new Frontend_Highlight(); $frontend_highlight->init_hooks(); } + + /** + * Add ref param in links that are used through the plugin link helpers. + * + * @param string $ref Ref param. + * @return string + */ + public function add_ref_param_to_links( string $ref ): string { + if ( defined( 'EDAC_REF_PARAM' ) && ! empty( EDAC_REF_PARAM ) ) { + return EDAC_REF_PARAM; + } else { + return $ref; + } + } } diff --git a/admin/class-ajax.php b/admin/class-ajax.php index ed946a045..b8ec8388c 100644 --- a/admin/class-ajax.php +++ b/admin/class-ajax.php @@ -428,14 +428,20 @@ function ( $a, $b ) { foreach ( $results as $row ) { - $id = (int) $row['id']; - $ignore = (int) $row['ignre']; - $ignore_class = $ignore ? ' active' : ''; - $ignore_label = $ignore ? 'Ignored' : 'Ignore'; - $ignore_user = (int) $row['ignre_user']; - $ignore_user_info = get_userdata( $ignore_user ); - $ignore_username = is_object( $ignore_user_info ) ? 'Username: ' . $ignore_user_info->user_login : ''; - $ignore_date = ( $row['ignre_date'] && '0000-00-00 00:00:00' !== $row['ignre_date'] ) ? 'Date: ' . gmdate( 'F j, Y g:i a', strtotime( esc_html( $row['ignre_date'] ) ) ) : ''; + $id = (int) $row['id']; + $ignore = (int) $row['ignre']; + $ignore_class = $ignore ? ' active' : ''; + $ignore_label = $ignore ? 'Ignored' : 'Ignore'; + $ignore_user = (int) $row['ignre_user']; + $ignore_user_info = get_userdata( $ignore_user ); + $ignore_username = is_object( $ignore_user_info ) + ? '' . esc_html__( 'Username:', 'accessibility-checker' ) . ' ' . esc_html( $ignore_user_info->user_login ) + : ''; + + $ignore_date_text = $row['ignre_date'] ? edac_format_datetime_from_utc( $row['ignre_date'] ) : ''; + $ignore_date = $ignore_date_text + ? '' . esc_html__( 'Date:', 'accessibility-checker' ) . ' ' . esc_html( $ignore_date_text ) + : ''; $ignore_comment = esc_html( $row['ignre_comment'] ); $ignore_action = $ignore ? 'disable' : 'enable'; $ignore_type = $rule['rule_type']; @@ -728,8 +734,8 @@ function ( $value ) { $ignre_user = ( 'enable' === $action ) ? get_current_user_id() : null; $ignre_user_info = ( 'enable' === $action ) ? get_userdata( $ignre_user ) : ''; $ignre_username = ( 'enable' === $action ) ? $ignre_user_info->user_login : ''; - $ignre_date = ( 'enable' === $action ) ? gmdate( 'Y-m-d H:i:s' ) : null; - $ignre_date_formatted = ( 'enable' === $action ) ? gmdate( 'F j, Y g:i a', strtotime( $ignre_date ) ) : ''; + $ignre_date = ( 'enable' === $action ) ? edac_get_current_utc_datetime() : null; + $ignre_date_formatted = ( 'enable' === $action ) ? edac_format_datetime_from_utc( $ignre_date ) : ''; $ignre_comment = ( 'enable' === $action && isset( $_REQUEST['comment'] ) ) ? sanitize_textarea_field( wp_unslash( $_REQUEST['comment'] ) ) : null; $ignore_global = ( 'enable' === $action && isset( $_REQUEST['ignore_global'] ) ) ? sanitize_textarea_field( wp_unslash( $_REQUEST['ignore_global'] ) ) : 0; diff --git a/admin/class-enqueue-admin.php b/admin/class-enqueue-admin.php index 32197dd00..77c3e449c 100644 --- a/admin/class-enqueue-admin.php +++ b/admin/class-enqueue-admin.php @@ -140,6 +140,7 @@ public static function maybe_enqueue_admin_and_editor_app_scripts() { 'scanUrl' => $scan_url, 'maxAltLength' => max( 1, absint( apply_filters( 'edac_max_alt_length', 300 ) ) ), 'version' => EDAC_VERSION, + 'postStatus' => get_post_status( $post_id ), 'restNonce' => wp_create_nonce( 'wp_rest' ), ] ); diff --git a/admin/class-scans-stats.php b/admin/class-scans-stats.php index d2bd08182..f8fcea954 100644 --- a/admin/class-scans-stats.php +++ b/admin/class-scans-stats.php @@ -299,9 +299,10 @@ function ( $item ) { JOIN ( SELECT DISTINCT postid FROM ' . $wpdb->prefix . 'accessibility_checker + WHERE ignre=%d AND ignre_global=%d ) AS distinct_posts ON ' . $wpdb->postmeta . '.post_id = distinct_posts.postid WHERE meta_key = %s', - [ '_edac_issue_density' ] + [ 0, 0, '_edac_issue_density' ] ) ); diff --git a/admin/class-widgets.php b/admin/class-widgets.php index a9977297f..2d75b4cbc 100644 --- a/admin/class-widgets.php +++ b/admin/class-widgets.php @@ -281,13 +281,16 @@ public function render_dashboard_scan_summary() { Edit Accessibility Checker Settings '; - $html .= ' + $meetup_html = edac_get_upcoming_meetups_html( 'wordpress-accessibility-meetup-group', 2, 4 ); + if ( ! empty( $meetup_html ) ) { + $html .= '