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
188 changes: 128 additions & 60 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,46 @@

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;

/**
* If the lock log is enabled
*
* @var int
*/
public $lock_log_enabled;

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

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

public function __construct() {
// semaphore
$this->lock_name = 'wpo_wcpdf_main_semaphore_lock';
$this->lock_context = array( 'source' => 'wpo-wcpdf-semaphore' );
$this->lock_time = apply_filters( 'wpo_wcpdf_main_semaphore_lock_time', 60 );
$this->lock_retries = apply_filters( 'wpo_wcpdf_main_semaphore_lock_retries', 0 );
$this->lock_log_enabled = isset( WPO_WCPDF()->settings->debug_settings['semaphore_logs'] ) ? true : false;

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 All @@ -35,7 +83,7 @@ public function __construct() {
add_filter( 'wpo_wcpdf_document_is_allowed', array( $this, 'disable_free' ), 10, 2 );
add_filter( 'wp_mail', array( $this, 'set_phpmailer_validator'), 10, 1 );

if ( isset(WPO_WCPDF()->settings->debug_settings['enable_debug']) ) {
if ( isset( WPO_WCPDF()->settings->debug_settings['enable_debug'] ) ) {
$this->enable_debug();
}

Expand Down Expand Up @@ -131,72 +179,92 @@ 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 ) ) {

if ( $this->lock_log_enabled ) {
$lock->log( sprintf( 'Lock acquired for attach document to email for order ID# %s.', $order_id ), 'info' );
}

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 = $lock->release();

if ( $lock_release && $this->lock_log_enabled ) {
$lock->log( sprintf( 'Lock released for attach document to email for order ID# %s.', $order_id ), 'info' );
}

} else {
if ( $this->lock_log_enabled ) {
$lock->log( sprintf( 'Couldn\'t get the lock for attach document to email for order ID# %s.', $order_id ), 'critical' );
}
}

remove_filter( 'wcpdf_disable_deprecation_notices', '__return_true' );
Expand Down
41 changes: 34 additions & 7 deletions includes/class-wcpdf-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class Settings {
public $lock_context;
public $lock_time;
public $lock_retries;
public $lock_log_enabled;
private $installed_templates = array();
private $installed_templates_cache = array();
private $template_list_cache = array();
Expand All @@ -51,10 +52,11 @@ 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_name = 'wpo_wcpdf_settings_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_time = apply_filters( 'wpo_wcpdf_settings_semaphore_lock_time', 60 );
$this->lock_retries = apply_filters( 'wpo_wcpdf_settings_semaphore_lock_retries', 0 );
$this->lock_log_enabled = isset( $this->debug_settings['semaphore_logs'] ) ? true : false;

// Settings menu item
add_action( 'admin_menu', array( $this, 'menu' ), 999 ); // Add menu
Expand Down Expand Up @@ -810,7 +812,13 @@ public function schedule_yearly_reset_numbers() {

// if no concurrent actions sets the action
if ( $scheduled_actions < 1 ) {

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

if ( $this->lock_log_enabled ) {
$lock->log( 'Lock acquired for yearly reset numbers schedule.', 'info' );
}

try {
$action_id = as_schedule_single_action( $datetime->getTimestamp(), $hook );
if ( ! empty( $action_id ) ) {
Expand All @@ -830,10 +838,18 @@ public function schedule_yearly_reset_numbers() {
$lock->log( $e, 'critical' );
}

$lock->release();
$lock_release = $lock->release();

if ( $lock_release && $this->lock_log_enabled ) {
$lock->log( 'Lock released for yearly reset numbers schedule.', 'info' );
}

} else {
$lock->log( "Couldn't get the lock!", 'critical' );
if ( $this->lock_log_enabled ) {
$lock->log( 'Couldn\'t get the lock for yearly reset numbers schedule.', 'critical' );
}
}

} else {
wcpdf_log_error(
"Number of concurrent yearly document numbers reset actions found: {$scheduled_actions}",
Expand All @@ -853,6 +869,11 @@ public function yearly_reset_numbers() {
$lock = new Semaphore( $this->lock_name, $this->lock_time, array( wc_get_logger() ), $this->lock_context );

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

if ( $this->lock_log_enabled ) {
$lock->log( 'Lock acquired for yearly reset numbers.', 'info' );
}

try {
// reset numbers
$documents = WPO_WCPDF()->documents->get_documents( 'all' );
Expand Down Expand Up @@ -885,10 +906,16 @@ public function yearly_reset_numbers() {
$lock->log( $e, 'critical' );
}

$lock->release();
$lock_release = $lock->release();

if ( $lock_release && $this->lock_log_enabled ) {
$lock->log( 'Lock release for yearly reset numbers.', 'info' );
}

} else {
$lock->log( "Couldn't get the lock!", 'critical' );
if ( $this->lock_log_enabled ) {
$lock->log( 'Couldn\'t get the lock for yearly reset numbers.', 'critical' );
}
}

// reschedule the action for the next year
Expand Down
36 changes: 24 additions & 12 deletions includes/documents/class-wcpdf-invoice.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class Invoice extends Order_Document_Methods {
public $lock_context;
public $lock_time;
public $lock_retries;
public $lock_log_enabled;
public $output_formats;

/**
Expand All @@ -31,15 +32,16 @@ class Invoice extends Order_Document_Methods {
*/
public function __construct( $order = 0 ) {
// set properties
$this->type = 'invoice';
$this->title = __( 'Invoice', 'woocommerce-pdf-invoices-packing-slips' );
$this->icon = WPO_WCPDF()->plugin_url() . "/assets/images/invoice.svg";
$this->type = 'invoice';
$this->title = __( 'Invoice', 'woocommerce-pdf-invoices-packing-slips' );
$this->icon = WPO_WCPDF()->plugin_url() . "/assets/images/invoice.svg";

// 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_retries = apply_filters( "wpo_wcpdf_{$this->type}_number_lock_retries", 0 );
$this->lock_name = "wpo_wcpdf_{$this->slug}_semaphore_lock";
$this->lock_context = array( 'source' => "wpo-wcpdf-semaphore" );
$this->lock_time = apply_filters( "wpo_wcpdf_{$this->type}_semaphore_lock_time", 60 );
$this->lock_retries = apply_filters( "wpo_wcpdf_{$this->type}_semaphore_lock_retries", 0 );
$this->lock_log_enabled = isset( WPO_WCPDF()->settings->debug_settings['semaphore_logs'] ) ? true : false;

// call parent constructor
parent::__construct( $order );
Expand Down Expand Up @@ -86,15 +88,19 @@ 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 ) ) {

if ( $this->lock_log_enabled ) {
$lock->log( 'Lock acquired for the invoice number init.', 'info' );
}

try {
// If a third-party plugin claims to generate invoice numbers, trigger this instead
Expand Down Expand Up @@ -128,10 +134,16 @@ public function init_number() {
$lock->log( $e, 'critical' );
}

$lock->release();
$lock_release = $lock->release();

if ( $lock_release && $this->lock_log_enabled ) {
$lock->log( 'Lock released for the invoice number init.', 'info' );
}

} else {
$lock->log( "Couldn't get the Invoice Number lock!", 'critical' );
if ( $this->lock_log_enabled ) {
$lock->log( 'Couldn\'t get the lock for the invoice number init.', 'critical' );
}
}

return $invoice_number;
Expand Down
Loading
Loading