PluginProbe ʕ •ᴥ•ʔ
WPML Multilingual & Multicurrency for WooCommerce / trunk
WPML Multilingual & Multicurrency for WooCommerce vtrunk
5.3.8 5.3.9 5.4.3 5.4.4 5.4.5 5.5.1 5.5.1.1 5.5.2.2 5.5.2.3 5.5.3 5.5.3.1 5.5.4 5.5.5 5.5.6 trunk 0.9 1.0 1.1 1.2 1.3 1.4 1.5 2.0 2.2 2.3 2.3.1 2.3.2 3.0 3.0.1 3.1 3.2 3.2.1 3.2.2 3.3 3.3.1 3.3.2 3.3.3 3.3.4 3.4 3.4.1 3.4.2 3.4.3 3.5 3.5.1 3.5.2 3.5.3 3.5.4 3.5.5 3.6 3.6.1 3.6.10 3.6.11 3.6.2 3.6.3 3.6.4 3.6.5 3.6.5.1 3.6.6 3.6.7 3.6.8 3.6.9 3.7 3.7.1 3.7.10 3.7.11 3.7.12 3.7.13 3.7.14 3.7.15 3.7.16 3.7.2 3.7.3 3.7.4 3.7.5 3.7.6 3.7.7 3.7.8 3.7.9 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.8.6 3.9.0 3.9.1 3.9.1.1 3.9.2 3.9.3 3.9.4 3.9.5 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4 4.10.0 4.10.1 4.10.2 4.10.3 4.10.4 4.11.2 4.11.3.2 4.11.5 4.11.6 4.12.1 4.12.4 4.12.5 4.12.6 4.2.0 4.2.0.1 4.2.1 4.2.1.1 4.2.10 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.2.7 4.2.7.1 4.2.8 4.2.8.1 4.2.9 4.3.0 4.3.1 4.3.2 4.3.2.1 4.3.3 4.3.4 4.3.5 4.3.6 4.3.7 4.4.0 4.4.1 4.4.2 4.4.2.1 4.5.0 4.6.0 4.6.1 4.6.2 4.6.2.1 4.6.3 4.6.5 4.6.6 4.6.7 4.7.0 4.7.1 4.7.2 4.7.3 4.7.4 4.7.5 4.7.6 4.7.7 4.7.8 4.7.9 4.8.0 4.9.0 4.9.1 5.0.1 5.0.2 5.1.1 5.1.2 5.1.3 5.2.0 5.2.1 5.3.2 5.3.3.1 5.3.4 5.3.5 5.3.6 5.3.7
woocommerce-multilingual / classes / Synchronization / Hooks.php
woocommerce-multilingual / classes / Synchronization Last commit date
Component 1 month ago Hooks.php 1 month ago Manager.php 1 month ago Store.php 1 year ago
Hooks.php
469 lines
1 <?php
2
3 namespace WCML\Synchronization;
4
5 use WCML\Utilities\SyncHash;
6 use WPML\FP\Obj;
7 use function WCML\functions\isCli;
8
9 //class Hooks implements \IWPML_Backend_Action, \IWPML_Frontend_Action, \IWPML_AJAX_Action, \IWPML_REST_Action, \IWPML_CLI_Action, \IWPML_DIC_Action {
10 class Hooks implements \IWPML_Backend_Action, \IWPML_Frontend_Action, \IWPML_DIC_Action {
11
12 const HOOK_SYNCHRONIZE_PRODUCT_TRANSLATIONS = 'wcml_synchronize_product_translations';
13 const HOOK_SYNCHRONIZE_PRODUCT_VARIATION_TRANSLATIONS = 'wcml_synchronize_product_variation_translations';
14 const HOOK_SYNCHRONIZE_PRODUCT_COMPONENT = 'wcml_synchronize_product_component';
15
16 const PRIORITY_BEFORE_STOCK_EMAIL_TRIGGER = 9;
17
18 /** @var \woocommerce_wpml */
19 protected $woocommerceWpml;
20
21 /** @var \SitePress */
22 protected $sitepress;
23
24 /** @var Manager */
25 private $manager;
26
27 public function __construct(
28 \woocommerce_wpml $woocommerceWpml,
29 \SitePress $sitepress,
30 \wpdb $wpdb,
31 SyncHash $syncHashManager
32 ) {
33 $this->woocommerceWpml = $woocommerceWpml;
34 $this->sitepress = $sitepress;
35 $this->manager = new Manager(
36 new Store(
37 $woocommerceWpml,
38 $sitepress,
39 $wpdb,
40 $syncHashManager
41 )
42 );
43 }
44
45 public function add_hooks() {
46 if ( is_admin() || isCli() ) {
47 add_action( 'save_post', [ $this, 'synchronizeProductTranslationsOnSave' ], PHP_INT_MAX, 2 ); // After WPML.
48 add_action( 'icl_make_duplicate', [ $this, 'synchronizeProductDuplication' ], 110, 4 );
49
50 add_action( 'woocommerce_product_quick_edit_save', [ $this, 'synchronizeOnEditSave' ] );
51 add_action( 'woocommerce_product_bulk_edit_save', [ $this, 'synchronizeOnEditSave' ] );
52
53 add_action( 'wpml_translation_update', [ $this, 'synchronizeConnectedTranslations' ] );
54 }
55
56 if ( is_admin() || wpml_is_rest_request() ) {
57 add_action( 'wpml_pro_translation_completed', [ $this, 'synchronizeProductTranslation' ] );
58 }
59
60 add_action( self::HOOK_SYNCHRONIZE_PRODUCT_TRANSLATIONS, [ $this, 'synchronizeProductTranslations' ], 10, 3 );
61 add_action( self::HOOK_SYNCHRONIZE_PRODUCT_VARIATION_TRANSLATIONS, [ $this, 'synchronizeProductVariationTranslations' ], 10, 3 );
62 add_action( self::HOOK_SYNCHRONIZE_PRODUCT_COMPONENT, [ $this, 'synchronizeProductComponent' ], 10, 4 );
63
64 add_action( 'woocommerce_ajax_save_product_variations', [ $this, 'synchronizeProductVariationsOnAjax' ], 11 );
65 add_action( 'woocommerce_bulk_edit_variations', [ $this, 'synchronizeProductVariationsOnBulkEdit' ], 10, 3 );
66
67 add_action( 'woocommerce_product_set_stock', [ $this, 'syncProductStock' ], self::PRIORITY_BEFORE_STOCK_EMAIL_TRIGGER );
68 add_action( 'woocommerce_variation_set_stock', [ $this, 'syncProductStock' ], self::PRIORITY_BEFORE_STOCK_EMAIL_TRIGGER );
69 }
70
71 /**
72 * @param \WP_Post $post
73 *
74 * @return bool
75 */
76 private function canRunProductSynchronization( $post ) {
77 if ( 'product' !== $post->post_type ) {
78 return false;
79 }
80
81 if ( 'auto-draft' === $post->post_status ) {
82 return false;
83 }
84
85 /* phpcs:ignore WordPress.VIP.SuperGlobalInputUsage.AccessDetected */
86 if ( isset( $_POST['autosave'] ) ) {
87 return false;
88 }
89
90 if ( isset( $_GET['action'] ) && 'trash' === $_GET['action'] ) {
91 return false;
92 }
93
94 return true;
95 }
96
97 /**
98 * @return bool
99 */
100 private function isProductSynchronizationValidContext() {
101 global $pagenow, $wp;
102 // exceptions.
103 /* phpcs:ignore WordPress.VIP.SuperGlobalInputUsage.AccessDetected */
104 $isDuplicating = ( ! empty( $_POST['icl_ajx_action'] ) && 'make_duplicates' === $_POST['icl_ajx_action'] );
105 $isApiRequest = ! empty( $wp->query_vars['wc-api-version'] );
106 $isValidContext = isCli()
107 || $isDuplicating
108 || $isApiRequest
109 || in_array( $pagenow, [ 'post.php', 'post-new.php', 'admin.php' ], true );
110
111 return apply_filters( 'wcml_product_synchronization_on_save_is_valid_context', $isValidContext );
112 }
113
114 /**
115 * @param int $postId
116 * @param \WP_Post $post
117 */
118 public function synchronizeProductTranslationsOnSave( $postId, $post ) {
119 if ( 'product_variation' === $post->post_type ) {
120 $this->setVariationLanguageDetails( $postId, $post );
121 return;
122 }
123
124 if ( ! $this->canRunProductSynchronization( $post ) ) {
125 return;
126 }
127 if ( ! $this->isProductSynchronizationValidContext() ) {
128 return;
129 }
130
131 $originalProduct = $this->manager->getOriginalProduct( $post );
132 if ( $this->woocommerceWpml->is_wpml_prior_4_2() ) {
133 $is_using_native_editor = ! $this->woocommerceWpml->settings['trnsl_interface'];
134 } else {
135 $is_using_native_editor = ! \WPML_TM_Post_Edit_TM_Editor_Mode::is_using_tm_editor( $this->sitepress, $originalProduct->ID );
136 }
137
138 if ( $is_using_native_editor ) {
139 // WARNING!!!
140 // This depends on the stored/selected setting, not the actual editor being used for editing a translation.
141 // Keeping it for backward compatibility since this is the original logic.
142 $this->synchronizeProductTranslationsOnSaveInNativeEditor( $postId, $post );
143 return;
144 }
145
146 $this->manager->setContext( $this->getContext() );
147 $this->manager->run( $post );
148 $this->manager->setContext( null );
149 }
150
151 /**
152 * @return string|null
153 */
154 private function getContext() {
155 $isUpdatingProductFromEditScreen = isset( $_POST['action'] ) && 'editpost' === sanitize_key( wp_unslash( $_POST['action'] ) );
156
157 if ( $isUpdatingProductFromEditScreen ) {
158 return Manager::CONTEXT_PRODUCT_EDIT_SCREEN_UPDATE;
159 }
160
161 return null;
162 }
163
164 /**
165 * @param int $variationId
166 * @param \WP_Post $variation
167 */
168 private function setVariationLanguageDetails( $variationId, $variation ) {
169 $productId = (int) $variation->post_parent;
170 $productLanguage = $this->sitepress->get_language_for_element( $productId, 'post_product' );
171 if ( ! $productLanguage ) {
172 return;
173 }
174 $productInOriginalLanguage = $this->woocommerceWpml->products->is_original_product( $productId );
175 if ( ! $productInOriginalLanguage ) {
176 return;
177 }
178 $variationLanguageDetails = $this->sitepress->get_element_language_details( $variationId, 'post_product_variation' );
179 if ( ! is_object( $variationLanguageDetails ) ) {
180 $this->sitepress->set_element_language_details( $variationId, 'post_product_variation', null, $productLanguage );
181 }
182 }
183
184 /**
185 * Legacy logic dealing with the native editor as the preferred translation editor.
186 *
187 * Copied verbatim from the original implementation.
188 *
189 * @param int $postId
190 * @param \WP_Post $post
191 */
192 private function synchronizeProductTranslationsOnSaveInNativeEditor( $postId, $post ) {
193 $originalProduct = $this->manager->getOriginalProduct( $post );
194 if ( $originalProduct->ID === $postId ) {
195 $this->manager->run( $post );
196 return;
197 }
198
199 $originalLanguage = $this->manager->getElementLanguage( $originalProduct->ID );
200 $currentLanguage = $this->sitepress->get_current_language();
201
202 if ( $originalLanguage === $currentLanguage ) {
203 $this->manager->run( $post );
204 return;
205 }
206
207 if ( ! empty( $_POST['wp-preview'] ) ) {
208 return;
209 }
210
211 $postId = apply_filters( 'wpml_object_id', $postId, 'product', false, $currentLanguage );
212
213 $this->manager->run( $originalProduct, [ $postId ], [ $postId => $currentLanguage ] );
214 }
215
216 /**
217 * @param int $productId
218 * @param string $language
219 * @param array $duplicatedPostData
220 * @param int $duplicatedProductId
221 */
222 public function synchronizeProductDuplication( $productId, $language, $duplicatedPostData, $duplicatedProductId ) {
223 if ( 'product' !== $duplicatedPostData['post_type'] ) {
224 return;
225 }
226
227 // Duplication should clone de variation description field.
228 global $iclTranslationManagement;
229 $customFieldSettings = $iclTranslationManagement->settings['custom_fields_translation'];
230 $variationDescriptionFieldSetting = Obj::prop( '_variation_description', $customFieldSettings );
231
232 $iclTranslationManagement->settings['custom_fields_translation']['_variation_description'] = WPML_COPY_CUSTOM_FIELD;
233
234 $product = get_post( $productId );
235 $originalProduct = $this->manager->getOriginalProduct( $product );
236
237 $this->manager->runProductComponents( $originalProduct, [ $duplicatedProductId ], [ $duplicatedProductId => $language ] );
238
239 // Restore the variation description field original setting.
240 if ( null === $variationDescriptionFieldSetting ) {
241 unset( $iclTranslationManagement->settings['custom_fields_translation']['_variation_description'] );
242 } else {
243 $iclTranslationManagement->settings['custom_fields_translation']['_variation_description'] = $variationDescriptionFieldSetting;
244 }
245 }
246
247 /**
248 * @param int $translatedProductId
249 */
250 public function synchronizeProductTranslation( $translatedProductId ) {
251 if ( 'product' !== get_post_type( $translatedProductId ) ) {
252 return;
253 }
254
255 $translatedProduct = get_post( $translatedProductId );
256 $originalProduct = $this->manager->getOriginalProduct( $translatedProduct );
257
258 if ( $originalProduct ) {
259 $translationsLanguages = [
260 $translatedProductId => $this->manager->getElementLanguage( $translatedProductId ),
261 ];
262 $this->manager->runProductComponents( $originalProduct, [ $translatedProductId ], $translationsLanguages );
263 }
264 }
265
266 /**
267 * @todo There is a regression here that we need to investigate.
268 *
269 * @see https://git.onthegosystems.com/glue-plugins/wpml/woocommerce-multilingual/-/merge_requests/1028
270 * @see https://git.onthegosystems.com/glue-plugins/wpml/woocommerce-multilingual/-/commit/e64378a8656bdf938f1e20565c2f65c94a26a3bb
271 */
272 public function synchronizeConnectedTranslations() {
273 if ( 'connect_translations' !== Obj::prop( 'icl_ajx_action', $_POST ) ) {
274 return;
275 }
276
277 $postType = Obj::prop( 'post_type', $_POST );
278 if ( 'product' !== $postType ) {
279 return;
280 }
281
282 $newTrid = Obj::prop( 'new_trid', $_POST );
283 $translations = $this->sitepress->get_element_translations( $newTrid, 'post_' . $postType );
284 if ( ! $translations ) {
285 return;
286 }
287
288 $postId = Obj::prop( 'post_id', $_POST );
289 $setAsSource = Obj::prop( 'set_as_source', $_POST );
290
291 remove_action( 'wpml_translation_update', [ $this, 'synchronizeConnectedTranslations' ] );
292 // Before the referenced commit:
293 // * If the translations being looped reached the original, syncing it to the post being sent to the AJAX call,
294 // unless there is a mandatory setting to set the post being sent as original, in which case we update the previous original to be in sync with the new (?)
295 // * For every other translation, if there is a mandatory setting to set the post being sent as original, sync this sent post into the translation
296 // After the referended commit:
297 // * We loop over the tranbslations but we act over just the first one that matches any of the two criteria.
298 foreach ( $translations as $translation ) {
299 if ( $setAsSource && ! $translation->original ) {
300 $productId = $postId;
301 $translatedProductId = $translation->element_id;
302 $language = $translation->language_code;
303 break;
304 } elseif ( ! $setAsSource && $translation->original ) {
305 $productId = $translation->element_id;
306 $translatedProductId = $postId;
307 $language = $this->sitepress->get_current_language();
308 break;
309 }
310 }
311
312 if ( isset( $productId, $translatedProductId, $language ) ) {
313 $product = get_post( $productId );
314 $this->manager->runProductComponents( $product, [ $translatedProductId ], [ $translatedProductId => $language ] );
315 $this->sitepress->copy_custom_fields( $productId, $translatedProductId );
316 $this->woocommerceWpml->translation_editor->create_product_translation_package( $productId, $newTrid, $language, ICL_TM_COMPLETE );
317 }
318 add_action( 'wpml_translation_update', [ $this, 'synchronizeConnectedTranslations' ] );
319 }
320
321 /**
322 * @param \WC_Product $productObject
323 */
324 public function synchronizeOnEditSave( $productObject ) {
325 $productId = $productObject->get_id();
326 $product = get_post( $productId );
327 $isOriginal = $this->manager->isOriginalProduct( $product );
328 $originalProduct = $this->manager->getOriginalProduct( $product );
329 $translations = $this->manager->getElementTranslations( $productId );
330
331 if ( ! $translations ) {
332 return;
333 }
334
335 $this->manager->setContext( Manager::CONTEXT_PRODUCT_BULK_OR_QUICK_EDIT );
336
337 if ( ! $isOriginal ) {
338 $language = $this->manager->getElementLanguage( $productId );
339 $this->manager->runProductComponents( $originalProduct, [ $productId ], [ $productId => $language ] );
340 } else {
341 $translationsLanguages = [];
342 foreach ( $translations as $index => $translation ) {
343 if ( $productId === (int) $translation ) {
344 unset( $translations[ $index ] );
345 } else {
346 $translationsLanguages[ $translation ] = $this->manager->getElementLanguage( $translation );
347 }
348 }
349
350 if ( ! empty( $translations ) ) {
351 $this->manager->runProductComponents( $originalProduct, $translations, $translationsLanguages );
352 }
353 }
354
355 $this->manager->setContext( null );
356 }
357
358 /**
359 * @param \WP_Post $product
360 * @param int[] $translationsIds
361 * @param array<int,string> $translationsLanguages
362 */
363 public function synchronizeProductTranslations( $product, $translationsIds = [], $translationsLanguages = [] ) {
364 if ( ! $this->canRunProductSynchronization( $product ) ) {
365 return;
366 }
367 $this->manager->run( $product, $translationsIds, $translationsLanguages );
368 }
369
370 /**
371 * @param \WP_Post $variation
372 * @param int[] $variationTranslations
373 * @param array<int,string> $variationTranslationsLanguages
374 */
375 public function synchronizeProductVariationTranslations( $variation, $variationTranslations, $variationTranslationsLanguages ) {
376 $this->manager->runProductVariationComponents( $variation, $variationTranslations, $variationTranslationsLanguages );
377 }
378
379 /**
380 * @param \WP_Post $product
381 * @param int[] $translationsIds
382 * @param array<int,string> $translationsLanguages
383 * @param string $componentName
384 */
385 public function synchronizeProductComponent( $product, $translationsIds, $translationsLanguages, $componentName ) {
386 if ( ! $this->canRunProductSynchronization( $product ) ) {
387 return;
388 }
389 $this->manager->runComponent( $product, $translationsIds, $translationsLanguages, $componentName );
390 }
391
392 /**
393 * @param int $productId
394 */
395 public function synchronizeProductVariationsOnAjax( $productId ) {
396 $product = get_post( $productId );
397 $isOriginal = $this->manager->isOriginalProduct( $product );
398
399 if ( ! $isOriginal ) {
400 return;
401 }
402
403 $translations = $this->manager->getElementTranslations( $productId );
404 if ( empty( $translations ) ) {
405 return;
406 }
407
408 $translationsLanguages = [];
409 foreach ( $translations as $index => $translation ) {
410 if ( $productId === (int) $translation ) {
411 unset( $translations[ $index ] );
412 } else {
413 $translationsLanguages[ $translation ] = $this->manager->getElementLanguage( $translation );
414 }
415 }
416
417 $this->manager->runComponent( $product, $translations, $translationsLanguages, Store::COMPONENT_VARIATIONS );
418 $this->manager->runComponent( $product, $translations, $translationsLanguages, Store::COMPONENT_ATTRIBUTES );
419 }
420
421 /**
422 * @param string $bulkAction
423 * @param array $data
424 * @param int $productId
425 */
426 public function synchronizeProductVariationsOnBulkEdit( $bulkAction, $data, $productId ) {
427 $this->synchronizeProductVariationsOnAjax( $productId );
428 }
429
430 /**
431 * @param \WC_Product $product
432 */
433 public function syncProductStock( $product ) {
434 $productId = $product->get_id();
435 $translations = $this->manager->getElementTranslations( $productId, false, false );
436 if ( empty( $translations ) ) {
437 return;
438 }
439
440 $translationsLanguages = [];
441 foreach ( $translations as $index => $translation ) {
442 if ( $productId === (int) $translation ) {
443 unset( $translations[ $index ] );
444 } else {
445 $translationsLanguages[ $translation ] = $this->manager->getElementLanguage( $translation );
446 }
447 }
448
449 $this->manager->runComponent( get_post( $productId ), $translations, $translationsLanguages, Store::COMPONENT_STOCK );
450
451 $wcml_data_store = wcml_product_data_store_cpt();
452 wp_cache_delete( $productId, 'post_meta' );
453 wp_cache_delete( 'product-' . $productId, 'products' );
454 delete_transient( 'wc_product_children_' . $productId );
455 $wcml_data_store->update_lookup_table_data( $productId );
456
457 foreach( $translations as $translation ) {
458 wp_cache_delete( $translation, 'post_meta' );
459 wp_cache_delete( 'product-' . $translation, 'products' );
460 delete_transient( 'wc_product_children_' . $translation );
461 $wcml_data_store->update_lookup_table_data( $translation );
462 }
463
464 delete_transient( 'wc_low_stock_count' );
465 delete_transient( 'wc_outofstock_count' );
466 }
467
468 }
469