PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / 2.6.2
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). v2.6.2
3.0.3 3.0.2 3.0.1 trunk 2.2.14 2.2.15 2.2.16 2.2.17 2.2.18 2.2.19 2.3.0 2.3.1 2.3.10 2.3.11 2.3.12 2.3.13 2.3.14 2.3.2 2.3.3 2.3.4 2.3.5 2.3.6 2.3.7 2.3.8 2.3.9 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.5.0 2.5.1 2.5.2 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.6.5 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.7.91 2.7.92 2.7.93 2.8.0 2.8.1 2.8.2 2.8.3 2.8.4 2.8.5 2.9.0 2.9.1 2.9.2 2.9.3 3.0.0
commercebird / commercebird.php
commercebird Last commit date
admin 6 months ago includes 6 months ago languages 1 year ago vendor 6 months ago LICENSE 1 year ago commercebird.php 6 months ago composer.json 6 months ago index.php 1 year ago readme.txt 6 months ago wp-dependencies.json 8 months ago
commercebird.php
445 lines
1 <?php
2 /**
3 * Plugin Name: CommerceBird
4 * Plugin URI: https://commercebird.com
5 * Author: CommerceBird
6 * Description: This plugin helps you get the most of CommerceBird by allowing you to upload product images, use integrations like Zoho Inventory, Zoho CRM, Exact Online and more. Requires a subscription at CommerceBird.com.
7 * Version: 2.6.2
8 * Requires PHP: 8.2
9 * Requires Plugins: woocommerce
10 * Requires at least: 6.5
11 * Tested up to: 6.9
12 * Text Domain: commercebird
13 * Domain Path: /languages
14 *
15 * License: GNU General Public License v3.0
16 * License URI: http://www.gnu.org/licenses/gpl-3.0.html
17 *
18 * @category Fulfillment
19 * @package CommerceBird
20 * @author Fawad Tiemoerie <info@commercebird.com>
21 * @copyright Copyright (c) 2024, CommerceBird
22 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL-3.0-or-later
23 *
24 * WC requires at least: 9.4.0
25 * WC tested up to: 10.4.2
26 */
27
28 if ( ! defined( 'ABSPATH' ) ) {
29 exit; // Exit if accessed directly.
30 }
31
32 if ( ! defined( 'CMBIRD_VERSION' ) ) {
33 define( 'CMBIRD_VERSION', '2.6.2' );
34 }
35 if ( ! defined( 'CMBIRD_PATH' ) ) {
36 define( 'CMBIRD_PATH', plugin_dir_path( __FILE__ ) );
37 }
38 if ( ! defined( 'CMBIRD_URL' ) ) {
39 define( 'CMBIRD_URL', plugin_dir_url( __FILE__ ) );
40 }
41 if ( ! defined( 'CMBIRD_MENU_SLUG' ) ) {
42 define( 'CMBIRD_MENU_SLUG', 'commercebird-app' );
43 }
44
45 require_once CMBIRD_PATH . 'includes/woo-functions.php';
46 require_once CMBIRD_PATH . 'includes/sync/order-backend.php';
47
48 require __DIR__ . '/vendor/autoload.php';
49
50 use Automattic\WooCommerce\Utilities\FeaturesUtil;
51 use CommerceBird\Admin\Actions\Sync\ExactOnlineSync;
52 use CommerceBird\Admin\Actions\Sync\ZohoCRMSync;
53 use CommerceBird\API\CMBird_APIs;
54 use CommerceBird\API\CreateOrderWebhook;
55 use CommerceBird\API\Exact;
56 use CommerceBird\API\ProductWebhook;
57 use CommerceBird\API\ShippingWebhook;
58 use CommerceBird\API\Zoho;
59 use CommerceBird\Plugin;
60
61 /*
62 |--------------------------------------------------------------------------
63 | Activation, deactivation and uninstall event.
64 |--------------------------------------------------------------------------
65 */
66
67 register_activation_hook( __FILE__, array( Plugin::class, 'activate' ) );
68 register_deactivation_hook( __FILE__, array( Plugin::class, 'deactivate' ) );
69 register_uninstall_hook( __FILE__, array( Plugin::class, 'uninstall' ) );
70
71 /*
72 |--------------------------------------------------------------------------
73 | Load the plugin translations
74 |--------------------------------------------------------------------------
75 | Note: WordPress.org automatically loads translations for plugins since WP 4.6.
76 | Manual loading not required for plugins hosted on WordPress.org.
77 */
78
79 /** Loading Purchase Order Class
80 *
81 * @since 1.0.0
82 */
83 function cmbird_purchase_order_class() {
84 if ( class_exists( 'WooCommerce' ) ) {
85 new CMBIRD_Purchase_Order();
86 }
87 }
88 add_action( 'woocommerce_init', 'cmbird_purchase_order_class' );
89 add_action( 'init', array( CMBIRD_PO_Admin_Manager::class, 'init' ), 11 );
90
91 /*
92 |--------------------------------------------------------------------------
93 | Start the plugin
94 |--------------------------------------------------------------------------
95 */
96 Plugin::init();
97
98 /**
99 * Declaring compatibility for WooCommerce HPOS
100 */
101 add_action(
102 'before_woocommerce_init',
103 function () {
104 if ( class_exists( FeaturesUtil::class ) ) {
105 FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );
106 }
107 }
108 );
109
110 /*
111 --------------------------------------------------------------------------
112 | Install required dependencies
113 |--------------------------------------------------------------------------
114 */
115 add_action(
116 'plugins_loaded',
117 static function () {
118 // Only if Premium plan is active via transient subscription_details.
119 $subscription = get_transient( 'subscription_details' );
120 // return if no subscription found.
121 if ( false === $subscription || empty( $subscription ) ) {
122 return;
123 }
124 if ( 'active' === $subscription['status'] ) {
125 $premium_plans = array( 'Premium', 'PremiumPlus' );
126 if ( in_array( $subscription['plan'], $premium_plans, true ) ) {
127 if ( class_exists( 'ACF' ) ) {
128 return;
129 }
130 WP_Dependency_Installer::instance( __DIR__ )->run();
131 }
132 }
133 }
134 );
135
136 /**
137 * Hooks for WC Action Scheduler to import or export products
138 */
139 $cmbird_import_products = new CMBIRD_Products_ZI();
140 $cmbird_import_pricelist = new CMBIRD_Pricelist_ZI();
141 $cmbird_product_class = new CMBIRD_Products_ZI_Export();
142 $cmbird_common_class = new CMBIRD_Common_Functions();
143 $cmbird_contact_class = new CMBIRD_Contact_ZI();
144 $cmbird_category_class = new CMBIRD_Categories_ZI();
145 $cmbird_import_pricelist->wc_b2b_groups();
146 add_action( 'import_group_items_cron', array( $cmbird_import_products, 'sync_groupitem_recursively' ), 10, 2 );
147 add_action( 'import_simple_items_cron', array( $cmbird_import_products, 'sync_item_recursively' ), 10, 2 );
148 add_action( 'import_variable_product_cron', array( $cmbird_import_products, 'import_variable_product_variations' ), 10, 2 );
149 add_action( 'sync_zi_product_cron', array( $cmbird_product_class, 'cmbird_zi_products_prepare_sync' ), 10, 2 );
150 add_action( 'sync_zi_pricelist', array( $cmbird_import_pricelist, 'zi_get_pricelist' ), 10, 2 );
151 add_action( 'sync_zi_order', array( $cmbird_common_class, 'cmbird_orders_prepare_sync' ), 10, 2 );
152 add_action( 'sync_zi_import_contacts', array( $cmbird_contact_class, 'cmbird_get_zoho_contacts' ), 10, 2 );
153 // add action to set the zoho rate limit option exceeded to false.
154 add_action( 'cmbird_common', array( CMBIRD_Common_Functions::class, 'set_zoho_rate_limit_option' ) );
155 // Zoho CRM Hooks.
156 add_action( 'sync_zcrm_order', array( $cmbird_common_class, 'cmbird_orders_prepare_sync' ) );
157 add_action( 'sync_zcrm_contact', array( ZohoCRMSync::class, 'cmbird_zcrm_contact_sync' ) );
158 // Exact Online Hooks.
159 add_action( 'cmbird_sync_eo', array( ExactOnlineSync::class, 'sync' ), 10, 3 );
160 add_action( 'cmbird_exact_online_sync_orders', array( ExactOnlineSync::class, 'sync_orders_via_cron' ) );
161 add_action( 'cmbird_payment_status', array( ExactOnlineSync::class, 'cmbird_payment_status' ), 10, 1 );
162 add_action( 'cmbird_eo_get_payment_statuses', array( ExactOnlineSync::class, 'get_payment_status_via_cron' ) );
163 // Callback functions for scheduled action to process product or customer chunk.
164 add_action(
165 'cmbird_process_product_chunk',
166 function ( $args ) {
167 if ( ! is_array( $args ) || empty( $args['transient_key'] ) ) {
168 return;
169 }
170 $transient_key = $args['transient_key'];
171 $import_products = $args['import_products'] ?? false;
172 $chunked_products = get_transient( $transient_key );
173 if ( $chunked_products ) {
174 $sync = new ExactOnlineSync();
175 $sync->sync( 'product', $chunked_products, (bool) $import_products );
176 // Remove transient after processing.
177 delete_transient( $transient_key );
178 }
179 },
180 10,
181 1
182 );
183 add_action(
184 'cmbird_process_customer_chunk',
185 function ( $args ) {
186 if ( ! is_array( $args ) || empty( $args['transient_key'] ) ) {
187 return;
188 }
189 $transient_key = $args['transient_key'];
190 $import_customers = $args['import_customers'] ?? false;
191 $chunked_customers = get_transient( $transient_key );
192 if ( $chunked_customers ) {
193 $sync = new ExactOnlineSync();
194 $sync->sync( 'customer', $chunked_customers, (bool) $import_customers );
195 // Remove transient after processing.
196 delete_transient( $transient_key );
197 }
198 },
199 10,
200 1
201 );
202 // Zoho CRM Hooks.
203 add_action( 'init', array( ZohoCRMSync::class, 'refresh_token' ) );
204
205 // add classes to REST API.
206 add_action(
207 'rest_api_init',
208 function () {
209 new Zoho();
210 new Exact();
211 new ProductWebhook();
212 new ShippingWebhook();
213 new CreateOrderWebhook();
214 new CMBird_APIs();
215 $po_controller = new CMBIRD_REST_Shop_Purchase_Controller();
216 $po_controller->register_routes();
217 }
218 );
219
220 add_action(
221 'save_post',
222 function ( $post_id, $post ) {
223 if ( 'wcb2b_group' === $post->post_type ) {
224 delete_transient( 'wc_b2b_groups' );
225 }
226 },
227 10,
228 2
229 );
230
231 /**
232 * Perform actions when the plugin is updated
233 *
234 * @param string $upgrader_object
235 * @param array $options
236 * @return void
237 */
238 add_action( 'upgrader_process_complete', 'cmbird_update_plugin_tasks', 10, 2 );
239
240 /**
241 * Perform tasks when the plugin is updated.
242 *
243 * @param \WP_Upgrader $upgrader_object - Upgrader object.
244 * @param array $options - Options array.
245 *
246 * @see https://developer.wordpress.org/reference/hooks/upgrader_process_complete/
247 */
248 function cmbird_update_plugin_tasks( $upgrader_object, $options ) {
249 $this_plugin = plugin_basename( __FILE__ );
250
251 if ( 'update' === $options['action'] && 'plugin' === $options['type'] ) {
252 foreach ( $options['plugins'] as $plugin ) {
253 if ( $plugin === $this_plugin ) {
254 // Perform tasks when the plugin is updated.
255 if ( defined( 'WP_CLI' ) && WP_CLI ) {
256 WP_CLI::add_command(
257 'cmbird:clean-zoho-images',
258 function () {
259 $result = cmbird_delete_zoho_orphaned_attachments();
260 WP_CLI::log( wp_json_encode( $result, JSON_PRETTY_PRINT ) );
261 }
262 );
263 }
264 }
265 }
266 }
267 }
268
269 /**
270 * Delete orphaned image attachments whose files no longer exist on disk.
271 * Checks all files in the uploads directory and its subfolders.
272 *
273 * @return array { deleted_posts, deleted_postmeta, deleted_terms, scanned, kept_exists }
274 */
275 function cmbird_delete_zoho_orphaned_attachments(): array {
276 global $wpdb;
277
278 // Only allow in admin/CLI for safety (optional but recommended).
279 if ( ! ( is_admin() || ( defined( 'WP_CLI' ) && WP_CLI ) ) ) {
280 return array(
281 'error' => 'Not allowed outside admin/CLI.',
282 );
283 }
284
285 // 1) Collect all attachment IDs (not just zoho_image).
286 // Try to get from cache first.
287 $cache_key = 'cmbird_attachment_ids';
288 $ids = wp_cache_get( $cache_key, 'commercebird' );
289
290 if ( false === $ids ) {
291 $ids = $wpdb->get_col(
292 $wpdb->prepare(
293 "SELECT p.ID
294 FROM {$wpdb->posts} p
295 INNER JOIN {$wpdb->postmeta} pm
296 ON pm.post_id = p.ID AND pm.meta_key = '_wp_attached_file'
297 WHERE p.post_type = 'attachment'
298 AND p.post_mime_type LIKE %s",
299 'image/%'
300 )
301 ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Cached above.
302 wp_cache_set( $cache_key, $ids, 'commercebird', 10 * MINUTE_IN_SECONDS );
303 }
304
305 if ( empty( $ids ) ) {
306 return array(
307 'deleted_posts' => 0,
308 'deleted_postmeta' => 0,
309 'deleted_terms' => 0,
310 'scanned' => 0,
311 'kept_exists' => 0,
312 'message' => 'No image attachments were found.',
313 );
314 }
315
316 // 2) Safety: keep only IDs where the actual file is missing on disk.
317 $uploads = wp_get_upload_dir();
318 $basedir = trailingslashit( $uploads['basedir'] );
319 $missing = array();
320 $kept_exist = 0;
321
322 // Fetch the relative paths for the candidate IDs.
323 $id_count = count( $ids );
324 if ( 0 === $id_count ) {
325 return array(
326 'deleted_posts' => 0,
327 'deleted_postmeta' => 0,
328 'deleted_terms' => 0,
329 'scanned' => 0,
330 'kept_exists' => 0,
331 'message' => 'No image attachments were found.',
332 );
333 }
334
335 $placeholders = implode( ',', array_fill( 0, $id_count, '%d' ) );
336
337 // Try to get from cache first.
338 $cache_key = 'cmbird_attachment_paths_' . md5( implode( ',', $ids ) );
339 $paths = wp_cache_get( $cache_key, 'commercebird' );
340
341 if ( false === $paths ) {
342 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Placeholders dynamically generated based on array size.
343 $paths = $wpdb->get_results(
344 $wpdb->prepare(
345 "SELECT pm.post_id AS id, pm.meta_value AS rel_path
346 FROM {$wpdb->postmeta} pm
347 WHERE pm.meta_key = '_wp_attached_file'
348 AND pm.post_id IN ($placeholders)", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Cached above.
349 ...$ids
350 ),
351 ARRAY_A
352 );
353 wp_cache_set( $cache_key, $paths, 'commercebird', 10 * MINUTE_IN_SECONDS );
354 }
355
356 // Map paths for quick lookup.
357 $map = array();
358 foreach ( $paths as $row ) {
359 $map[ (int) $row['id'] ] = $row['rel_path'];
360 }
361
362 foreach ( $ids as $id ) {
363 $rel = $map[ $id ] ?? '';
364 $abs = $rel ? $basedir . ltrim( $rel, '/\\' ) : '';
365 if ( empty( $rel ) || ! file_exists( $abs ) ) {
366 $missing[] = (int) $id;
367 } else {
368 ++$kept_exist;
369 }
370 }
371
372 if ( empty( $missing ) ) {
373 return array(
374 'deleted_posts' => 0,
375 'deleted_postmeta' => 0,
376 'deleted_terms' => 0,
377 'scanned' => count( $ids ),
378 'kept_exists' => $kept_exist,
379 'message' => 'All referenced files still exist; nothing to delete.',
380 );
381 }
382
383 // Deletion helpers.
384 $delete_in = function ( string $table, string $col, array $id_list ) use ( $wpdb ): int {
385 $id_list = array_map( 'intval', $id_list );
386 if ( empty( $id_list ) ) {
387 return 0;
388 }
389 // Sanitize table and column names.
390 $table = preg_replace( '/[^a-zA-Z0-9_]/', '', $table );
391 $col = preg_replace( '/[^a-zA-Z0-9_]/', '', $col );
392 $affected = 0;
393 $chunks = array_chunk( $id_list, 500 );
394 foreach ( $chunks as $chunk ) {
395 $placeholders = implode( ',', array_fill( 0, count( $chunk ), '%d' ) );
396 $query = "DELETE FROM `$table` WHERE `$col` IN ($placeholders)";
397 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Table and column names sanitized via preg_replace. DELETE operation not suitable for caching.
398 $res = $wpdb->query( $wpdb->prepare( $query, ...$chunk ) );
399 $affected += (int) $res;
400 }
401 return $affected;
402 };
403
404 $deleted_posts = 0;
405 $deleted_meta = 0;
406 $deleted_terms = 0;
407 $total_to_delete = count( $missing );
408 $in_transaction = false;
409
410 // 3) Transaction (optional, best effort; will no-op if MySQL engine doesn't support).
411 $wpdb->query( 'START TRANSACTION' );
412 $in_transaction = true;
413
414 try {
415 // Delete post meta.
416 $deleted_meta = $delete_in( $wpdb->postmeta, 'post_id', $missing );
417 // Delete term relationships (usually none for attachments, but safe).
418 $deleted_terms = $delete_in( $wpdb->term_relationships, 'object_id', $missing );
419 // Delete the posts.
420 $deleted_posts = $delete_in( $wpdb->posts, 'ID', $missing );
421
422 $wpdb->query( 'COMMIT' );
423 $in_transaction = false;
424
425 return array(
426 'deleted_posts' => $deleted_posts,
427 'deleted_postmeta' => $deleted_meta,
428 'deleted_terms' => $deleted_terms,
429 'scanned' => count( $ids ),
430 'kept_exists' => $kept_exist,
431 'deleted_requested' => $total_to_delete,
432 'message' => 'Deleted orphaned image attachments and related data.',
433 );
434
435 } catch ( \Throwable $e ) {
436 if ( $in_transaction ) {
437 $wpdb->query( 'ROLLBACK' );
438 }
439 return array(
440 'error' => 'Deletion failed: ' . $e->getMessage(),
441 'scanned' => count( $ids ),
442 );
443 }
444 }
445