main_instance = $main_instance;
$this->data_helper = new PdfBuilderOrderDataHelper();
$canvas_helper = new PdfBuilderCanvasHelper();
$this->ajax_handler = new PdfBuilderWooCommerceAjax( $canvas_helper, $this->data_helper );
$this->renderer = new PdfBuilderMetaBoxRenderer();
$this->init_hooks();
}
/**
* Enregistre le script utilisé par la metabox WooCommerce.
*
* @return void
*/
public function enqueue_admin_scripts(): void {
if ( ! function_exists( 'get_current_screen' ) ) {
return;
}
$screen = get_current_screen();
if ( ! $screen || ! in_array( $screen->id, array( 'shop_order', 'woocommerce_page_wc-orders' ), true ) ) {
return;
}
wp_register_script(
'pdfib-metabox-blob',
PDFIB_PLUGIN_URL . 'assets/js/pdfib-metabox-blob.js',
array(),
PDFIB_VERSION,
true
);
wp_enqueue_script( 'pdfib-metabox-blob' );
wp_register_script( 'pdf-builder-wc-order-js', false, array( 'jquery', 'pdfib-metabox-blob' ), PDFIB_VERSION, true );
wp_enqueue_script( 'pdf-builder-wc-order-js' );
}
/**
* Enregistre les hooks AJAX WooCommerce.
*
* @return void
*/
public function register_ajax_hooks(): void {
add_action( 'wp_ajax_pdfib_generate_order_pdf', array( $this, 'ajax_generate_order_pdf' ) );
add_action( 'wp_ajax_pdfib_send_order_email', array( $this, 'ajax_send_order_email' ) );
add_action( 'wp_ajax_pdfib_save_order_canvas', array( $this->ajax_handler, 'ajax_save_order_canvas' ) );
add_action( 'wp_ajax_pdfib_load_order_canvas', array( $this->ajax_handler, 'ajax_load_order_canvas' ) );
add_action( 'wp_ajax_pdfib_get_canvas_elements', array( $this->ajax_handler, 'ajax_get_canvas_elements' ) );
add_action( 'wp_ajax_pdfib_get_order_data', array( $this->ajax_handler, 'ajax_get_order_data' ) );
add_action( 'wp_ajax_pdfib_get_company_data', array( $this->ajax_handler, 'ajax_get_company_data' ) );
add_action( 'wp_ajax_pdfib_validate_order_access', array( $this->ajax_handler, 'ajax_validate_order_access' ) );
add_action( 'wp_ajax_pdfib_pdf_queue_join', array( $this, 'ajax_pdf_queue_join' ) );
add_action( 'wp_ajax_pdfib_pdf_queue_poll', array( $this, 'ajax_pdf_queue_poll' ) );
add_action( 'wp_ajax_pdfib_pdf_queue_leave', array( $this, 'ajax_pdf_queue_leave' ) );
add_action( 'wp_ajax_pdfib_stream_pdf', array( $this, 'ajax_stream_pdf' ) );
}
/**
* Ajoute la metabox PDF Builder aux commandes WooCommerce.
*
* @return void
*/
public function add_woocommerce_order_meta_box(): void {
add_meta_box(
'pdf-builder-order-actions',
__( 'Advanced PDF Invoice Builder', 'advanced-pdf-invoice-builder' ),
array( $this, 'render_woocommerce_order_meta_box' ),
array( 'shop_order', 'woocommerce_page_wc-orders' ),
'side',
'high'
);
}
/**
* Rend la metabox de commande WooCommerce.
*
* @param object|\WP_Post|null $post_or_order Objet WooCommerce ou post.
* @return void
*/
public function render_woocommerce_order_meta_box( mixed $post_or_order ): void {
$table_templates = pdfib_db()->prefix . 'pdfib_templates';
$order = $this->resolve_order_from_param( $post_or_order );
if ( ! $order ) {
echo '
❌ ' . esc_html__( 'Commande introuvable.', 'advanced-pdf-invoice-builder' ) . '
';
return;
}
$is_premium = $this->check_is_premium();
$order_status = $order->get_status();
$status_key = 'wc-' . $order_status;
$order_statuses = function_exists( 'wc_get_order_statuses' ) ? \wc_get_order_statuses() : array();
$status_label = $order_statuses[ $status_key ] ?? ucfirst( $order_status );
if ( ! $is_premium && 'completed' !== $order_status ) {
?>
Terminées.', 'advanced-pdf-invoice-builder' ), array( 'strong' => array() ) ); ?>
get_id();
$selected_template = $this->resolve_template_for_status( $is_premium, $status_key, $table_templates );
$nonce = wp_create_nonce( 'pdfib_order_actions' );
$ajax_url = admin_url( 'admin-ajax.php' );
$this->renderer->render( $order_id, $selected_template, $is_premium, $status_label, $nonce, $ajax_url, $order );
}
/**
* Résout un objet WC_Order depuis un WC_Order, un WP_Post ou la requête courante.
*
* @param object|\WP_Post|null $post_or_order Objet reçu par la metabox.
* @return object|null
*/
private function resolve_order_from_param( ?object $post_or_order ): ?object {
if ( is_a( $post_or_order, 'WC_Order' ) ) {
return $post_or_order;
}
if ( is_a( $post_or_order, 'WP_Post' ) ) {
$order_id = $post_or_order->ID;
} else {
$order_id = absint( filter_input( INPUT_GET, 'id', FILTER_SANITIZE_NUMBER_INT ) ?? 0 );
}
$order = function_exists( 'wc_get_order' ) ? \wc_get_order( $order_id ) : null;
return ( $order instanceof \WC_Order ) ? $order : null;
}
/**
* Résout le template associé au statut courant.
*
* @param bool $is_premium Indique si l'utilisateur est Premium.
* @param string $status_key Clé de statut WooCommerce.
* @param string $table Nom de table du template.
* @return array|null
*/
private function resolve_template_for_status( bool $is_premium, string $status_key, string $table ): ?array {
$settings = pdfib_get_option( 'pdfib_settings', array() );
$status_templates = $settings['pdfib_order_status_templates'] ?? array();
$selected = null;
if ( $is_premium && ! empty( $status_templates[ $status_key ] ) ) {
$selected = pdfib_db()->get_row( pdfib_db()->prepare( self::SQL_SELECT_TEMPLATE_BY_ID, $table, (int) $status_templates[ $status_key ] ), ARRAY_A );
} elseif ( ! $is_premium && ! empty( $status_templates['wc-completed'] ) ) {
$selected = pdfib_db()->get_row( pdfib_db()->prepare( self::SQL_SELECT_TEMPLATE_BY_ID, $table, (int) $status_templates['wc-completed'] ), ARRAY_A );
}
if ( ! $selected ) {
$mapped_id = \apply_filters( 'pdfib_get_template_for_status', null, $status_key );
if ( $mapped_id ) {
$selected = pdfib_db()->get_row( pdfib_db()->prepare( self::SQL_SELECT_TEMPLATE_BY_ID, $table, (int) $mapped_id ), ARRAY_A );
}
}
if ( ! $selected ) {
$selected = pdfib_db()->get_row( pdfib_db()->prepare( 'SELECT id, name FROM %i ORDER BY id ASC LIMIT 1', $table ), ARRAY_A );
}
return $selected;
}
/**
* Retourne les slots actifs de la file PDF et nettoie les entrées expirées.
*
* @return array
*/
private function pdf_queue_get_slots(): array {
$slots = get_option( 'pdfib_free_pdf_slots', array() );
$now = time();
$slots = array_filter(
$slots,
static fn( $timestamp ) => ( $now - (int) $timestamp ) < 180
);
return $slots;
}
/**
* Sauvegarde les slots actifs de la file PDF.
*
* @param array $slots Slots à enregistrer.
* @return void
*/
private function pdf_queue_save_slots( array $slots ): void {
update_option( 'pdfib_free_pdf_slots', $slots, false );
}
/**
* Indique si l'utilisateur courant est Premium.
* Délègue au plugin PRO via filtre si disponible.
*
* @return bool
*/
private function check_is_premium(): bool {
$license_manager = apply_filters( 'pdfib_license_manager_instance', null );
if ( ! is_object( $license_manager ) && class_exists( '\PDFIB\Managers\PdfBuilderLicenseManager' ) ) {
$license_manager = \PDFIB\Managers\PdfBuilderLicenseManager::get_instance();
}
return is_object( $license_manager )
&& method_exists( $license_manager, 'is_premium' )
&& $license_manager->is_premium();
}
/**
* Retire l'utilisateur courant de la file après génération.
*
* @return void
*/
public function ajax_pdf_queue_leave(): void {
if ( ! current_user_can( 'edit_shop_orders' ) && ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => self::msg_permission_denied() ), 403 );
return;
}
$nonce = sanitize_text_field( wp_unslash( $GLOBALS['_POST']['nonce'] ?? '' ) );
$valid_nonce = wp_verify_nonce( $nonce, 'pdfib_order_actions' )
|| wp_verify_nonce( $nonce, 'pdfib_ajax' );
if ( ! $valid_nonce ) {
wp_send_json_error( array( 'message' => self::msg_nonce_invalid() ), 403 );
return;
}
$slots = $this->pdf_queue_get_slots();
$user_id = get_current_user_id();
unset( $slots[ $user_id ] );
$this->pdf_queue_save_slots( $slots );
wp_send_json_success(
array(
'message' => 'ok',
'queue_size' => count( $slots ),
)
);
}
/**
* Stream le PDF en binaire pour un fetch() depuis le navigateur.
*
* @return void
*/
public function ajax_stream_pdf(): void {
if ( ! current_user_can( 'edit_shop_orders' ) && ! current_user_can( 'manage_options' ) ) {
status_header( 403 );
echo esc_html( self::msg_permission_denied() );
exit;
}
$nonce = sanitize_text_field( wp_unslash( $GLOBALS['_POST']['nonce'] ?? '' ) );
if ( ! wp_verify_nonce( $nonce, 'pdfib_order_actions' ) ) {
status_header( 403 );
echo esc_html( self::msg_nonce_invalid() );
exit;
}
$order_id = absint( wp_unslash( $GLOBALS['_POST']['order_id'] ?? 0 ) );
$template_id = sanitize_text_field( wp_unslash( $GLOBALS['_POST']['template_id'] ?? '' ) );
if ( ! $order_id || ! $template_id ) {
status_header( 422 );
/* translators: 1: Order ID, 2: Template ID */
echo esc_html( sprintf( __( 'Paramètres manquants (order_id=%1$d template_id=%2$s)', 'advanced-pdf-invoice-builder' ), (int) $order_id, $template_id ) );
exit;
}
if ( ! class_exists( 'PdfBuilderUnifiedAjaxHandler' ) ) {
status_header( 500 );
echo esc_html( self::msg_handler_unavailable() );
exit;
}
$handler = \PdfBuilderUnifiedAjaxHandler::get_instance();
$pdf_content = $handler->get_pdf_buffer( $template_id, $order_id );
if ( false === $pdf_content || 0 === strlen( $pdf_content ) ) {
status_header( 500 );
echo esc_html__( 'Échec génération PDF', 'advanced-pdf-invoice-builder' );
exit;
}
$this->stream_pdf_to_output( $pdf_content, $order_id );
}
/**
* Envoie un buffer PDF en streaming HTTP et termine l'exécution.
*
* @param string $pdf_content Contenu PDF binaire.
* @param int $order_id Identifiant de la commande.
* @return void
*/
private function stream_pdf_to_output( string $pdf_content, int $order_id ): void {
while ( ob_get_level() > 0 ) {
ob_end_clean();
}
header_remove();
header( 'Content-Type: application/pdf' );
$order_number = $order_id;
if ( function_exists( 'wc_get_order' ) ) {
$order = \wc_get_order( $order_id );
if ( $order ) {
$order_number = $order->get_order_number();
}
}
header( 'Content-Disposition: inline; filename="commande-' . sanitize_file_name( (string) $order_number ) . '.pdf"' );
header( 'Content-Length: ' . strlen( $pdf_content ) );
header( 'Cache-Control: private, max-age=0, must-revalidate' );
header( 'X-Content-Type-Options: nosniff' );
pdfib_filesystem()->put_contents( 'php://output', $pdf_content );
exit;
}
/**
* Génère le PDF d'une commande et le renvoie directement au navigateur.
*
* @return void
*/
public function ajax_generate_order_pdf(): void {
if ( ! current_user_can( 'manage_options' ) && ! current_user_can( 'edit_shop_orders' ) ) {
\wp_send_json_error( array( 'message' => self::msg_permission_denied() ) );
return;
}
$nonce = \sanitize_text_field( wp_unslash( $GLOBALS['_POST']['nonce'] ?? $GLOBALS['_GET']['nonce'] ?? '' ) );
if ( ! \wp_verify_nonce( $nonce, 'pdfib_order_actions' ) ) {
\wp_send_json_error( array( 'message' => self::msg_nonce_invalid() ) );
return;
}
$order_id = absint( wp_unslash( $GLOBALS['_POST']['order_id'] ?? $GLOBALS['_GET']['order_id'] ?? 0 ) );
$template_id = sanitize_text_field( wp_unslash( $GLOBALS['_POST']['template_id'] ?? $GLOBALS['_GET']['template_id'] ?? '' ) );
if ( ! $order_id || ! $template_id ) {
\wp_send_json_error( array( 'message' => __( 'Paramètres manquants', 'advanced-pdf-invoice-builder' ) ) );
return;
}
if ( ! class_exists( 'PdfBuilderUnifiedAjaxHandler' ) ) {
\wp_send_json_error( array( 'message' => self::msg_handler_unavailable() ) );
return;
}
$handler = \PdfBuilderUnifiedAjaxHandler::get_instance();
$pdf_content = $handler->get_pdf_buffer( $template_id, $order_id );
if ( false === $pdf_content || '' === $pdf_content ) {
\wp_send_json_error( array( 'message' => self::msg_handler_unavailable() ) );
return;
}
$this->stream_pdf_to_output( $pdf_content, $order_id );
}
/**
* Envoie un PDF de commande par e-mail.
*
* @return void
*/
public function ajax_send_order_email(): void {
try {
[, $order_id, $template_id, $to, $subject, $message] = $this->validate_and_extract_email_request();
$this->send_pdf_by_email( $order_id, $template_id, $to, $subject, $message );
} catch ( \RuntimeException $e ) {
$error_code = (int) $e->getCode();
\wp_send_json_error( array( 'message' => $e->getMessage() ), 0 !== $error_code ? $error_code : 400 );
}
}
/**
* Valide une requête d'envoi e-mail et retourne les paramètres utiles.
*
* @throws PdfBuilderWooCommerceException Si la requête est invalide.
* @return array
*/
private function validate_and_extract_email_request(): array {
if ( ! current_user_can( 'manage_options' ) && ! current_user_can( 'edit_shop_orders' ) ) {
throw new PdfBuilderWooCommerceException( 'Permissions insuffisantes', 403 );
}
$nonce = \sanitize_text_field( wp_unslash( $GLOBALS['_POST']['nonce'] ?? '' ) );
if ( ! \wp_verify_nonce( $nonce, 'pdfib_order_actions' ) ) {
throw new PdfBuilderWooCommerceException( esc_html( self::msg_nonce_invalid() ), 403 );
}
$order_id = absint( wp_unslash( $GLOBALS['_POST']['order_id'] ?? 0 ) );
$template_id = \sanitize_text_field( wp_unslash( $GLOBALS['_POST']['template_id'] ?? '' ) );
$to = \sanitize_email( wp_unslash( $GLOBALS['_POST']['to'] ?? '' ) );
$subject = \sanitize_text_field( wp_unslash( $GLOBALS['_POST']['subject'] ?? '' ) );
$message = \sanitize_textarea_field( wp_unslash( $GLOBALS['_POST']['message'] ?? '' ) );
if ( ! $order_id || ! $template_id || ! $to || ! $subject ) {
throw new PdfBuilderWooCommerceException( 'Paramètres manquants (order_id, template_id, to, subject)', 422 );
}
if ( ! \is_email( $to ) ) {
throw new PdfBuilderWooCommerceException( 'Adresse e-mail invalide', 422 );
}
if ( ! function_exists( 'wc_get_order' ) ) {
throw new PdfBuilderWooCommerceException( 'WooCommerce non disponible', 503 );
}
$order = \wc_get_order( $order_id );
if ( ! $order ) {
throw new PdfBuilderWooCommerceException( esc_html( 'Commande #' . (int) $order_id . ' introuvable' ), 404 );
}
if ( ! class_exists( 'PdfBuilderUnifiedAjaxHandler' ) ) {
throw new PdfBuilderWooCommerceException( esc_html( self::msg_handler_unavailable() ), 503 );
}
return array( $order, $order_id, $template_id, $to, $subject, $message );
}
/**
* Génère le PDF, l'attache au courriel et l'envoie.
*
* @param int $order_id Identifiant de commande.
* @param string $template_id Identifiant de template.
* @param string $to Destinataire.
* @param string $subject Sujet du courriel.
* @param string $message Message du courriel.
* @return void
*/
private function send_pdf_by_email( int $order_id, string $template_id, string $to, string $subject, string $message ): void {
$handler = \PdfBuilderUnifiedAjaxHandler::get_instance();
$pdf_content = $handler->get_pdf_buffer( $template_id, $order_id );
if ( false === $pdf_content ) {
\wp_send_json_error( array( 'message' => __( "Erreur lors de la génération du PDF. Vérifiez qu'un moteur PDF est configuré.", 'advanced-pdf-invoice-builder' ) ) );
return;
}
$upload = \wp_upload_dir();
$tmp_dir = $upload['basedir'] . '/pdf-builder-tmp/';
if ( ! file_exists( $tmp_dir ) ) {
\wp_mkdir_p( $tmp_dir );
}
$tmp_file = $tmp_dir . 'order-' . $order_id . '-' . uniqid() . '.pdf';
pdfib_filesystem()->put_contents( $tmp_file, $pdf_content, FS_CHMOD_FILE );
$from_name = get_bloginfo( 'name' );
$from_email = get_option( 'admin_email' );
$headers = array(
'Content-Type: text/html; charset=UTF-8',
'From: ' . $from_name . ' <' . $from_email . '>',
);
$body = '' !== $message ? nl2br( \esc_html( $message ) ) : '';
$attachments = array( $tmp_file );
$sent = \wp_mail( $to, $subject, $body, $headers, $attachments );
wp_delete_file( $tmp_file );
if ( $sent ) {
// translators: %s: recipient email address.
$success_fmt = __( 'E-mail envoyé avec succès à %s', 'advanced-pdf-invoice-builder' );
\wp_send_json_success( array( 'message' => sprintf( $success_fmt, $to ) ) );
return;
}
\wp_send_json_error( array( 'message' => __( "Échec de l'envoi. Vérifiez la configuration SMTP de WordPress.", 'advanced-pdf-invoice-builder' ) ) );
}
/**
* Vérifie la file PDF, réserve un slot et renvoie la position courante.
*
* @return void
*/
public function ajax_pdf_queue_poll(): void {
if ( ! current_user_can( 'edit_shop_orders' ) && ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => self::msg_permission_denied() ), 403 );
return;
}
$nonce = sanitize_text_field( wp_unslash( $GLOBALS['_POST']['nonce'] ?? '' ) );
$valid_nonce = wp_verify_nonce( $nonce, 'pdfib_order_actions' )
|| wp_verify_nonce( $nonce, 'pdfib_ajax' );
if ( ! $valid_nonce ) {
wp_send_json_error( array( 'message' => self::msg_nonce_invalid() ), 403 );
return;
}
$slots = $this->pdf_queue_get_slots();
$user_id = get_current_user_id();
if ( ! isset( $slots[ $user_id ] ) ) {
$slots = array( $user_id => time() ) + $slots;
$this->pdf_queue_save_slots( $slots );
} else {
$slots[ $user_id ] = time();
$this->pdf_queue_save_slots( $slots );
}
$keys = array_keys( $slots );
$position = (int) array_search( $user_id, $keys, true );
wp_send_json_success(
array(
'position' => $position,
'slot_available' => ( 0 === $position ),
'queue_size' => count( $slots ),
)
);
}
/**
* Initialise les hooks du manager.
*
* @return void
*/
private function init_hooks(): void {
add_action( 'init', array( $this, 'register_ajax_hooks' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) );
add_action( 'add_meta_boxes', array( $this, 'add_woocommerce_order_meta_box' ) );
}
/**
* Ajoute l'utilisateur courant à la file PDF gratuite.
*
* @return void
*/
public function ajax_pdf_queue_join(): void {
if ( ! current_user_can( 'edit_shop_orders' ) && ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => self::msg_permission_denied() ), 403 );
return;
}
$nonce = sanitize_text_field( wp_unslash( $GLOBALS['_POST']['nonce'] ?? '' ) );
$valid_nonce = wp_verify_nonce( $nonce, 'pdfib_order_actions' )
|| wp_verify_nonce( $nonce, 'pdfib_ajax' );
if ( ! $valid_nonce ) {
wp_send_json_error( array( 'message' => self::msg_nonce_invalid() ), 403 );
return;
}
if ( $this->check_is_premium() ) {
wp_send_json_success(
array(
'is_premium' => true,
'position' => 0,
'slot_available' => true,
'queue_size' => 0,
)
);
return;
}
$slots = $this->pdf_queue_get_slots();
$user_id = get_current_user_id();
if ( ! isset( $slots[ $user_id ] ) ) {
$slots[ $user_id ] = time();
$this->pdf_queue_save_slots( $slots );
}
$keys = array_keys( $slots );
$position = (int) array_search( $user_id, $keys, true );
wp_send_json_success(
array(
'is_premium' => false,
'position' => $position,
'slot_available' => ( 0 === $position ),
'queue_size' => count( $slots ),
)
);
}
}