Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Invoice number race condition #669

Merged
merged 12 commits into from
Dec 5, 2023
168 changes: 109 additions & 59 deletions includes/class-wcpdf-main.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

use WPO\WC\UBL\Builders\SabreBuilder;
use WPO\WC\UBL\Documents\UblDocument;
use WPO\WC\PDF_Invoices\Updraft_Semaphore_3_0 as Semaphore;

if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
Expand All @@ -12,6 +13,39 @@

class Main {

/**
* Lock name
*
* @var string
*/
public $lock_name;
alexmigf marked this conversation as resolved.
Show resolved Hide resolved

/**
* Lock context
*
* @var array
*/
public $lock_context;

/**
* Lock time limit if the release doesn't happen before
*
* @var int
*/
public $lock_time;

/**
* Lock retries
*
* @var int
*/
public $lock_retries;

/**
* Temp subfolders
*
* @var array
*/
private $subfolders = array( 'attachments', 'fonts', 'dompdf' );

protected static $_instance = null;
Expand All @@ -24,6 +58,12 @@ public static function instance() {
}

public function __construct() {
// semaphore
$this->lock_name = 'wpo_wcpdf_main_lock';
$this->lock_context = array( 'source' => 'wpo-wcpdf-main' );
$this->lock_time = apply_filters( 'wpo_wcpdf_main_lock_time', 60 );
$this->lock_retries = apply_filters( 'wpo_wcpdf_main_lock_retries', 0 );

add_action( 'wp_ajax_generate_wpo_wcpdf', array( $this, 'generate_document_ajax' ) );
add_action( 'wp_ajax_nopriv_generate_wpo_wcpdf', array( $this, 'generate_document_ajax' ) );

Expand Down Expand Up @@ -131,72 +171,82 @@ public function attach_document_to_email( $attachments, $email_id, $order, $emai
}

$attach_to_document_types = $this->get_documents_for_email( $email_id, $order );
$lock = new Semaphore( $this->lock_name, $this->lock_time, array( wc_get_logger() ), $this->lock_context );

if ( $lock->lock( $this->lock_retries ) ) {

foreach ( $attach_to_document_types as $output_format => $document_types ) {
foreach ( $document_types as $document_type ) {
$email_order = apply_filters( 'wpo_wcpdf_email_attachment_order', $order, $email, $document_type );
$email_order_id = $email_order->get_id();

do_action( 'wpo_wcpdf_before_attachment_creation', $email_order, $email_id, $document_type );

try {
// log document generation to order notes
add_action( 'wpo_wcpdf_init_document', function( $document ) {
$this->log_document_creation_to_order_notes( $document, 'email_attachment' );
$this->log_document_creation_trigger_to_order_meta( $document, 'email_attachment' );
$this->mark_document_printed( $document, 'email_attachment' );
} );
foreach ( $attach_to_document_types as $output_format => $document_types ) {
foreach ( $document_types as $document_type ) {
$email_order = apply_filters( 'wpo_wcpdf_email_attachment_order', $order, $email, $document_type );
$email_order_id = $email_order->get_id();

do_action( 'wpo_wcpdf_before_attachment_creation', $email_order, $email_id, $document_type );

// prepare document
// we use ID to force to reloading the order to make sure that all meta data is up to date.
// this is especially important when multiple emails with the PDF document are sent in the same session
$document = wcpdf_get_document( $document_type, (array) $email_order_id, true );
if ( ! $document ) { // something went wrong, continue trying with other documents
try {
// log document generation to order notes
add_action( 'wpo_wcpdf_init_document', function( $document ) {
$this->log_document_creation_to_order_notes( $document, 'email_attachment' );
$this->log_document_creation_trigger_to_order_meta( $document, 'email_attachment' );
$this->mark_document_printed( $document, 'email_attachment' );
} );

// prepare document
// we use ID to force to reloading the order to make sure that all meta data is up to date.
// this is especially important when multiple emails with the PDF document are sent in the same session
$document = wcpdf_get_document( $document_type, (array) $email_order_id, true );
if ( ! $document ) { // something went wrong, continue trying with other documents
continue;
}

$tmp_path = $this->get_tmp_path( 'attachments' );
if ( ! @is_dir( $tmp_path ) || ! wp_is_writable( $tmp_path ) ) {
wcpdf_log_error( "Couldn't get the attachments temporary folder path.", 'critical', $e );
return $attachments;
}

// get attachment
$attachment = false;
switch ( $output_format ) {
default:
case 'pdf':
$attachment = $this->get_document_pdf_attachment( $document, $tmp_path );
break;
case 'ubl':
if ( true === apply_filters_deprecated( 'wpo_wcpdf_custom_ubl_attachment_condition', array( true, $order, $email_id, $document ), '3.6.0', 'wpo_wcpdf_custom_attachment_condition' ) ) {
$attachment = $this->get_document_ubl_attachment( $document, $tmp_path );
}
break;
}

if ( $attachment ) {
$attachments[] = $attachment;
} else {
continue;
}

do_action( 'wpo_wcpdf_email_attachment', $attachment, $document_type, $document, $output_format );

} catch ( \Exception $e ) {
wcpdf_log_error( $e->getMessage(), 'critical', $e );
continue;
}

$tmp_path = $this->get_tmp_path( 'attachments' );
if ( ! @is_dir( $tmp_path ) || ! wp_is_writable( $tmp_path ) ) {
wcpdf_log_error( "Couldn't get the attachments temporary folder path.", 'critical', $e );
return $attachments;
}

// get attachment
$attachment = false;
switch ( $output_format ) {
default:
case 'pdf':
$attachment = $this->get_document_pdf_attachment( $document, $tmp_path );
break;
case 'ubl':
if ( true === apply_filters_deprecated( 'wpo_wcpdf_custom_ubl_attachment_condition', array( true, $order, $email_id, $document ), '3.6.0', 'wpo_wcpdf_custom_attachment_condition' ) ) {
$attachment = $this->get_document_ubl_attachment( $document, $tmp_path );
}
break;
}

if ( $attachment ) {
$attachments[] = $attachment;
} else {
} catch ( \Dompdf\Exception $e ) {
wcpdf_log_error( 'DOMPDF exception: '.$e->getMessage(), 'critical', $e );
continue;
} catch ( \WPO\WC\UBL\Exceptions\FileWriteException $e ) {
wcpdf_log_error( 'UBL FileWrite exception: '.$e->getMessage(), 'critical', $e );
continue;
} catch ( \Error $e ) {
wcpdf_log_error( $e->getMessage(), 'critical', $e );
continue;
}

do_action( 'wpo_wcpdf_email_attachment', $attachment, $document_type, $document, $output_format );

} catch ( \Exception $e ) {
wcpdf_log_error( $e->getMessage(), 'critical', $e );
continue;
} catch ( \Dompdf\Exception $e ) {
wcpdf_log_error( 'DOMPDF exception: '.$e->getMessage(), 'critical', $e );
continue;
} catch ( \WPO\WC\UBL\Exceptions\FileWriteException $e ) {
wcpdf_log_error( 'UBL FileWrite exception: '.$e->getMessage(), 'critical', $e );
continue;
} catch ( \Error $e ) {
wcpdf_log_error( $e->getMessage(), 'critical', $e );
continue;

}
}

$lock->release();

} else {
$lock->log( sprintf( 'Couldn\'t get the lock for the order ID# %s!', $order_id ), 'critical' );
}

remove_filter( 'wcpdf_disable_deprecation_notices', '__return_true' );
Expand Down
8 changes: 4 additions & 4 deletions includes/class-wcpdf-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ public function __construct() {
$this->debug_settings = get_option( 'wpo_wcpdf_settings_debug' );
$this->ubl_tax_settings = get_option( 'wpo_wcpdf_settings_ubl_taxes' );

$this->lock_name = 'wpo_wcpdf_semaphore_lock';
$this->lock_context = array( 'source' => 'wpo-wcpdf-semaphore' );
$this->lock_time = apply_filters( 'wpo_wcpdf_semaphore_lock_time', 2 );
$this->lock_retries = apply_filters( 'wpo_wcpdf_semaphore_lock_retries', 0 );
$this->lock_name = 'wpo_wcpdf_settings_lock';
$this->lock_context = array( 'source' => 'wpo-wcpdf-settings' );
$this->lock_time = apply_filters( 'wpo_wcpdf_settings_lock_time', 60 );
$this->lock_retries = apply_filters( 'wpo_wcpdf_settings_lock_retries', 0 );

// Settings menu item
add_action( 'admin_menu', array( $this, 'menu' ), 999 ); // Add menu
Expand Down
8 changes: 4 additions & 4 deletions includes/documents/class-wcpdf-invoice.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public function __construct( $order = 0 ) {
// semaphore
$this->lock_name = "wpo_wcpdf_{$this->slug}_number_lock";
$this->lock_context = array( 'source' => "wpo-wcpdf-{$this->type}-semaphore" );
$this->lock_time = apply_filters( "wpo_wcpdf_{$this->type}_number_lock_time", 2 );
$this->lock_time = apply_filters( "wpo_wcpdf_{$this->type}_number_lock_time", 60 );
$this->lock_retries = apply_filters( "wpo_wcpdf_{$this->type}_number_lock_retries", 0 );

// call parent constructor
Expand Down Expand Up @@ -86,15 +86,15 @@ public function init() {
}

public function exists() {
return !empty( $this->data['number'] );
return ! empty( $this->data['number'] );
}

public function init_number() {
$logger = isset( $this->settings['log_number_generation'] ) ? [ wc_get_logger() ] : [];
$lock = new Semaphore( $this->lock_name, $this->lock_time, $logger, $this->lock_context );
$invoice_number = null;
$invoice_number = $this->exists() ? $this->data['number'] : null;

if ( $lock->lock( $this->lock_retries ) ) {
if ( $lock->lock( $this->lock_retries ) && empty( $invoice_number ) ) {

try {
// If a third-party plugin claims to generate invoice numbers, trigger this instead
Expand Down
Loading