ifnex/03_WordPress/wp-admin/includes/class-wp-privacy-data-export-requests-list-table.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

176 lines
5.9 KiB
PHP

<?php
/**
* List Table API: WP_Privacy_Data_Export_Requests_List_Table class
*
* @package WordPress
* @subpackage Administration
* @since 4.9.6
*/
if ( ! class_exists( 'WP_Privacy_Requests_Table' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-privacy-requests-table.php';
}
/**
* WP_Privacy_Data_Export_Requests_Table class.
*
* @since 4.9.6
*/
class WP_Privacy_Data_Export_Requests_List_Table extends WP_Privacy_Requests_Table {
/**
* Action name for the requests this table will work with.
*
* @since 4.9.6
*
* @var string $request_type Name of action.
*/
protected $request_type = 'export_personal_data';
/**
* Post type for the requests.
*
* @since 4.9.6
*
* @var string $post_type The post type.
*/
protected $post_type = 'user_request';
/**
* Actions column.
*
* @since 4.9.6
*
* @param WP_User_Request $item Item being shown.
* @return string Email column markup.
*/
public function column_email( $item ) {
/** This filter is documented in wp-admin/includes/ajax-actions.php */
$exporters = apply_filters( 'wp_privacy_personal_data_exporters', array() );
$exporters_count = count( $exporters );
$status = $item->status;
$request_id = $item->ID;
$nonce = wp_create_nonce( 'wp-privacy-export-personal-data-' . $request_id );
$download_data_markup = '<span class="export-personal-data" ' .
'data-exporters-count="' . esc_attr( $exporters_count ) . '" ' .
'data-request-id="' . esc_attr( $request_id ) . '" ' .
'data-nonce="' . esc_attr( $nonce ) .
'">';
$download_data_markup .= '<span class="export-personal-data-idle"><button type="button" class="button-link export-personal-data-handle">' . __( 'Download personal data' ) . '</button></span>' .
'<span class="export-personal-data-processing hidden">' . __( 'Downloading data...' ) . ' <span class="export-progress"></span></span>' .
'<span class="export-personal-data-success hidden"><button type="button" class="button-link export-personal-data-handle">' . __( 'Download personal data again' ) . '</button></span>' .
'<span class="export-personal-data-failed hidden">' . __( 'Download failed.' ) . ' <button type="button" class="button-link export-personal-data-handle">' . __( 'Retry' ) . '</button></span>';
$download_data_markup .= '</span>';
$row_actions['download-data'] = $download_data_markup;
if ( 'request-completed' !== $status ) {
$complete_request_markup = '<span>';
$complete_request_markup .= sprintf(
'<a href="%s" class="complete-request" aria-label="%s">%s</a>',
esc_url(
wp_nonce_url(
add_query_arg(
array(
'action' => 'complete',
'request_id' => array( $request_id ),
),
admin_url( 'export-personal-data.php' )
),
'bulk-privacy_requests'
)
),
esc_attr(
sprintf(
/* translators: %s: Request email. */
__( 'Mark export request for &#8220;%s&#8221; as completed.' ),
$item->email
)
),
__( 'Complete request' )
);
$complete_request_markup .= '</span>';
}
if ( ! empty( $complete_request_markup ) ) {
$row_actions['complete-request'] = $complete_request_markup;
}
return sprintf( '<a href="%1$s">%2$s</a> %3$s', esc_url( 'mailto:' . $item->email ), esc_html( $item->email ), $this->row_actions( $row_actions ) );
}
/**
* Displays the next steps column.
*
* @since 4.9.6
*
* @param WP_User_Request $item Item being shown.
*/
public function column_next_steps( $item ) {
$status = $item->status;
switch ( $status ) {
case 'request-pending':
esc_html_e( 'Waiting for confirmation' );
break;
case 'request-confirmed':
/** This filter is documented in wp-admin/includes/ajax-actions.php */
$exporters = apply_filters( 'wp_privacy_personal_data_exporters', array() );
$exporters_count = count( $exporters );
$request_id = $item->ID;
$nonce = wp_create_nonce( 'wp-privacy-export-personal-data-' . $request_id );
echo '<div class="export-personal-data" ' .
'data-send-as-email="1" ' .
'data-exporters-count="' . esc_attr( $exporters_count ) . '" ' .
'data-request-id="' . esc_attr( $request_id ) . '" ' .
'data-nonce="' . esc_attr( $nonce ) .
'">';
?>
<span class="export-personal-data-idle"><button type="button" class="button-link export-personal-data-handle"><?php _e( 'Send export link' ); ?></button></span>
<span class="export-personal-data-processing hidden"><?php _e( 'Sending email...' ); ?> <span class="export-progress"></span></span>
<span class="export-personal-data-success success-message hidden"><?php _e( 'Email sent.' ); ?></span>
<span class="export-personal-data-failed hidden"><?php _e( 'Email could not be sent.' ); ?> <button type="button" class="button-link export-personal-data-handle"><?php _e( 'Retry' ); ?></button></span>
<?php
echo '</div>';
break;
case 'request-failed':
echo '<button type="submit" class="button-link" name="privacy_action_email_retry[' . $item->ID . ']" id="privacy_action_email_retry[' . $item->ID . ']">' . __( 'Retry' ) . '</button>';
break;
case 'request-completed':
echo '<a href="' . esc_url(
wp_nonce_url(
add_query_arg(
array(
'action' => 'delete',
'request_id' => array( $item->ID ),
),
admin_url( 'export-personal-data.php' )
),
'bulk-privacy_requests'
)
) . '">' . esc_html__( 'Remove request' ) . '</a>';
break;
}
}
/**
* Returns a clean label for the primary (Requester) column's row header `aria-label`.
*
* Provides screen readers with just the item email as the row header name,
* preventing them from computing the name from the full cell content.
*
* @since 7.1.0
*
* @param WP_User_Request $item Item being shown.
* @return string The user request item email.
*/
protected function get_primary_column_aria_label( $item ) {
return $item->email;
}
}