ifnex/03_WordPress/wp-includes/blocks/playlist.php
Kazem Alghasi f1d5667fe8 feat(deploy): preflight + smoke-test + UAT checklist (phases 3.9.1–3.11)
Prepare the project for production deployment and final client
acceptance testing. Adds preflight fail-fast command, hardened FX rate
importer, currency seeder, deploy scripts, E2E smoke test, and the
43-item UAT checklist document with sign-off form.

Key changes:

- Phase 3.9.1 — hotfix (AddressBookEntry):
  - Add $attributes defaults (use_count=0, is_default=false) so
    instance values are populated right after create() without
    refresh(). Resolves the red test
    `it_increments_use_count_and_sets_last_used_at`.

- Phase 3.10 — deploy infrastructure (Laravel):
  - Add `ifnex:preflight` command: fail-fast audit of production
    environment (APP_ENV, APP_DEBUG, APP_KEY, IFNEX_API_KEY,
    IFNEX_BRIDGE_API_KEY, ZARINPAL_SANDBOX, DB_CONNECTION, CORS,
    currencies seeded, PHP extensions, storage writable). Exits 1
    if any check fails; supports --skip-db for pre-migrate runs.
  - Add `ifnex:import-fx-rates` command: replacement for
    `ifnex:update-rates`. Fetches USD/AED from Exir.io + EUR/CNY
    from FreeCurrencyAPI. Supports --auto-approve (cron use) and
    --threshold (max change % for auto-approve). Logs critical on
    HTTP failure. URL configurable via EXIR_API_URL env.
  - Add `CurrencySeeder`: creates 5 base currencies (USD/AED/EUR/
    CNY/IRR) with rate=0 (IRR=1). Idempotent — does not overwrite
    real rates already set by admin or cron.
  - Update `DatabaseSeeder`: skip super_admin with password
    'password' in production (still created in local/testing).
  - Fix `bootstrap/app.php`: remove dead `use ApiKeyAuth` import
    (actual class is ApiKeyMiddleware). Latent fatal if api_key
    alias was used.
  - Add `config/ifnex.php` exir block + preflight config.
  - Harden `.env.example`: MySQL default, EXIR_API_URL,
    SANCTUM_STATEFUL_DOMAINS, APP_TIMEZONE, LOG_DAILY_DAYS, fix
    duplicate CURRENCY_API_KEY.

- Phase 3.10 — deploy scripts (Linux):
  - Add `scripts/deploy.sh`: idempotent 13-step deploy (composer
    install → key:generate → preflight → migrate → seed → FX import
    → cache → chown). Fail-fast with set -e.
  - Add `scripts/cron-exir-rates.sh`: daily cron wrapper for
    `ifnex:import-fx-rates --source=all --auto-approve`.

- Phase 3.10.1 — preflight hotfix:
  - Make expected DB connection configurable
    (ifnex.preflight.expected_db_connection) instead of hardcoded
    'mysql'. Prevents cascade test failure when tests would have to
    mutate database.default to mysql in a sqlite test environment
    (Unknown database ':memory:' → broken connection pool → 22
    previously-green tests fail with "There is already an active
    transaction").
  - Rewrite IfnexPreflightTest to use config() rather than mutating
    database.default.

- Phase 3.11 — UAT:
  - Add `ifnex:smoke-test` command: E2E post-deploy automated test
    (25+ checks: DB connection, core tables, currencies with
    non-zero rates, countries count, roles, admin user, system
    settings, storage writable, config/route cache, 4 API
    endpoints: /up, /countries, /track/{invalid}, /customer/profile
    without token).
  - Add 9 feature tests for smoke-test (PASS + 6 FAIL + custom URL
    + 422/500 handling + auth bypass detection).
  - Add `UAT-3.11.md`: 43-item Persian checklist for client
    acceptance testing across 15 feature areas. Includes sign-off
    form for both parties (client + VernaSoft).
  - Update `CLIENT_DELIVERY.md` to record phases 3.10 and 3.11.

Tests:
- Total: 51 → 82 tests, 150 → 220+ assertions, all green.
- New: IfnexPreflightTest (9), IfnexImportFxRatesTest (9),
  CurrencySeederTest (4), IfnexSmokeTestTest (9) = 31 new tests.

Documentation:
- DEPLOYMENT_HANDOFF.md §2.4: add CurrencySeeder + import-fx-rates.
- DEPLOYMENT_HANDOFF.md §2.8: update cron to use cron-exir-rates.sh.
- UAT-3.11.md: new 43-item client acceptance checklist + sign-off.
- CLIENT_DELIVERY.md: record phases 3.10 and 3.11 completion.
2026-10-31 22:18:15 +03:30

221 lines
6.8 KiB
PHP

<?php
/**
* Server-side rendering of the `core/playlist` block.
*
* @package WordPress
*/
/**
* Renders the `core/playlist` block on server.
*
* @since 7.1.0
*
* @param array $attributes The block attributes.
* @param string $content The block content.
* @param WP_Block $block The block instance.
*
* @return string Returns the Playlist.
*/
function render_block_core_playlist( $attributes, $content, $block ) {
$playlist_id = wp_unique_id( 'playlist-' );
$playlist_tracks = array();
$tracks_data = array();
$show_play_button_artwork = ! empty( $attributes['showPlayButtonArtwork'] );
// Parse inner blocks to extract track data.
// This approach avoids duplicating track data in the HTML output.
if ( ! empty( $block->inner_blocks ) ) {
foreach ( $block->inner_blocks as $inner_block ) {
if ( 'core/playlist-track' === $inner_block->name ) {
$track_attributes = $inner_block->attributes;
if ( empty( $track_attributes['id'] ) ) {
continue;
}
$track_id = 'track-' . count( $playlist_tracks );
$playlist_tracks[] = $track_id;
// Extract track metadata from block attributes.
$title = isset( $track_attributes['title'] ) && ! empty( $track_attributes['title'] ) ? $track_attributes['title'] : __( 'Unknown title' );
$artist = $track_attributes['artist'] ?? '';
$album = $track_attributes['album'] ?? '';
$image = $track_attributes['image'] ?? '';
$image_alt = $track_attributes['imageAlt'] ?? '';
$url = $track_attributes['src'] ?? '';
$aria_label = $title;
if ( $title && $artist && $album ) {
$aria_label = sprintf(
/* translators: %1$s: track title, %2$s: artist name, %3$s: album name. */
_x( '%1$s by %2$s from the album %3$s', 'track title, artist name, album name' ),
$title,
$artist,
$album
);
}
// Data is passed to wp_interactivity_state() which JSON-encodes it,
// so we use wp_strip_all_tags() instead of esc_html() to prevent
// HTML injection without double-encoding. URLs still use esc_url().
$tracks_data[ $track_id ] = array(
'url' => esc_url( $url ),
'title' => wp_strip_all_tags( $title ),
'artist' => wp_strip_all_tags( $artist ),
'album' => wp_strip_all_tags( $album ),
'image' => esc_url( $image ),
'imageAlt' => wp_strip_all_tags( $image_alt ),
'ariaLabel' => wp_strip_all_tags( $aria_label ),
);
}
}
}
if ( empty( $playlist_tracks ) ) {
return '';
}
wp_enqueue_script_module( '@wordpress/block-library/playlist/view' );
// Add the playlist tracks to the global state,
// but keep them isolated from other playlists with the help of playlistId.
wp_interactivity_state(
'core/playlist',
array(
'playlists' => array(
$playlist_id => array(
'tracks' => $tracks_data,
),
),
)
);
// Add waveform player container with translated button labels.
$label_play = esc_attr__( 'Play' );
$label_pause = esc_attr__( 'Pause' );
$label_seek = esc_attr__( 'Seek' );
/* translators: %1$s: current audio time, %2$s: total audio duration. */
$label_seek_value = esc_attr_x(
'%1$s of %2$s',
'audio current time of total duration'
);
$waveform_color_attribute = '';
$waveform_gradient_attribute = '';
$waveform_background_color_attribute = '';
$waveform_background_gradient_attribute = '';
if ( ! empty( $attributes['waveformColor'] ) ) {
$waveform_color_attribute = sprintf(
' data-waveform-player-color="%s"',
esc_attr( $attributes['waveformColor'] )
);
}
if ( ! empty( $attributes['waveformGradient'] ) ) {
$waveform_gradient_attribute = sprintf(
' data-waveform-player-gradient="%s"',
esc_attr( $attributes['waveformGradient'] )
);
}
if ( ! empty( $attributes['waveformBackgroundColor'] ) ) {
$waveform_background_color_attribute = sprintf(
' data-waveform-player-background-color="%s"',
esc_attr( $attributes['waveformBackgroundColor'] )
);
}
if ( ! empty( $attributes['waveformBackgroundGradient'] ) ) {
$waveform_background_gradient_attribute = sprintf(
' data-waveform-player-background-gradient="%s"',
esc_attr( $attributes['waveformBackgroundGradient'] )
);
}
$html = '<div class="wp-block-playlist__waveform-player"' .
$waveform_color_attribute .
$waveform_gradient_attribute .
$waveform_background_color_attribute .
$waveform_background_gradient_attribute . '
data-wp-watch="callbacks.initWaveformPlayer"
data-label-play="' . $label_play . '"
data-label-pause="' . $label_pause . '"
data-label-seek="' . $label_seek . '"
data-label-seek-value="' . $label_seek_value . '"
></div>';
// Add the waveform player container inside the figure.
$processor = new class( $content ) extends WP_HTML_Tag_Processor {
/**
* Inserts HTML immediately after the current token.
*
* @since 7.1.4
*
* @param string $html HTML to insert.
*/
public function block_core_playlist_insert_after( string $html ) {
$this->set_bookmark( 'here' );
$span = $this->bookmarks['here'];
$this->lexical_updates[] = new WP_HTML_Text_Replacement( $span->start + $span->length, 0, $html );
}
};
$waveform_style = $attributes['waveformStyle'] ?? 'bars';
if ( $processor->next_tag( 'figure' ) ) {
$processor->block_core_playlist_insert_after( $html );
$processor->set_attribute( 'data-wp-interactive', 'core/playlist' );
$processor->set_attribute(
'data-wp-context',
wp_json_encode(
array(
'playlistId' => $playlist_id,
'currentId' => $playlist_tracks[0],
'isPlaying' => false,
'tracks' => $playlist_tracks,
'waveformStyle' => $waveform_style,
'showPlayButtonArtwork' => $show_play_button_artwork,
'labelPauseTrack' => __( 'Pause' ),
'labelSelectTrack' => __( 'Play' ),
)
)
);
}
// Track IDs are render-time only. Add them after inner blocks have rendered
// so track buttons can update the Interactivity API state without storing
// persistent unique IDs in post content.
$track_index = 0;
while ( $processor->next_tag( array( 'class_name' => 'wp-block-playlist-track__button' ) ) ) {
$track_id = $playlist_tracks[ $track_index ] ?? null;
if ( null === $track_id ) {
break;
}
$processor->set_attribute(
'data-wp-context',
wp_json_encode(
array(
'trackId' => $track_id,
)
)
);
++$track_index;
}
return $processor->get_updated_html();
}
/**
* Registers the `core/playlist` block on server.
*
* @since 7.1.0
*/
function register_block_core_playlist() {
register_block_type_from_metadata(
__DIR__ . '/playlist',
array(
'render_callback' => 'render_block_core_playlist',
)
);
}
add_action( 'init', 'register_block_core_playlist' );