PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / 2.6.0
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). v2.6.0
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 7 months ago includes 7 months ago languages 1 year ago vendor 7 months ago LICENSE 1 year ago commercebird.php 7 months ago composer.json 9 months ago index.php 1 year ago readme.txt 7 months ago wp-dependencies.json 8 months ago
commercebird.php
428 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.0
8 * Requires PHP: 8.2
9 * Requires Plugins: woocommerce
10 * Requires at least: 6.5
11 * Tested up to: 6.8.3
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.3.5
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.0' );
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 */
76 add_action( 'init', 'cmbird_load_textdomain' );
77 function cmbird_load_textdomain() {
78 load_plugin_textdomain( 'commercebird', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
79 }
80
81 /** Loading Purchase Order Class
82 *
83 * @since 1.0.0
84 */
85 function cmbird_purchase_order_class() {
86 if ( class_exists( 'WooCommerce' ) ) {
87 new CMBIRD_Purchase_Order();
88 }
89 }
90 add_action( 'woocommerce_init', 'cmbird_purchase_order_class' );
91 add_action( 'init', array( CMBIRD_PO_Admin_Manager::class, 'init' ), 11 );
92
93 /*
94 |--------------------------------------------------------------------------
95 | Start the plugin
96 |--------------------------------------------------------------------------
97 */
98 Plugin::init();
99
100 /**
101 * Declaring compatibility for WooCommerce HPOS
102 */
103 add_action(
104 'before_woocommerce_init',
105 function () {
106 if ( class_exists( FeaturesUtil::class ) ) {
107 FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );
108 }
109 }
110 );
111
112 /*
113 --------------------------------------------------------------------------
114 | Install required dependencies
115 |--------------------------------------------------------------------------
116 */
117 add_action(
118 'plugins_loaded',
119 static function () {
120 // Only if Premium plan is active via transient subscription_details.
121 $subscription = get_transient( 'subscription_details' );
122 if ( 'active' === $subscription['status'] ) {
123 $premium_plans = array( 'Premium', 'PremiumPlus' );
124 if ( in_array( $subscription['plan'], $premium_plans, true ) ) {
125 if ( class_exists( 'ACF' ) ) {
126 return;
127 }
128 WP_Dependency_Installer::instance( __DIR__ )->run();
129 }
130 }
131 }
132 );
133
134 /**
135 * Hooks for WC Action Scheduler to import or export products
136 */
137 $import_products = new CMBIRD_Products_ZI();
138 $import_pricelist = new CMBIRD_Pricelist_ZI();
139 $product_class = new CMBIRD_Products_ZI_Export();
140 $common_class = new CMBIRD_Common_Functions();
141 $contact_class = new CMBIRD_Contact_ZI();
142 $category_class = new CMBIRD_Categories_ZI();
143 $import_pricelist->wc_b2b_groups();
144 add_action( 'import_group_items_cron', array( $import_products, 'sync_groupitem_recursively' ), 10, 2 );
145 add_action( 'import_simple_items_cron', array( $import_products, 'sync_item_recursively' ), 10, 2 );
146 add_action( 'import_variable_product_cron', array( $import_products, 'import_variable_product_variations' ), 10, 2 );
147 add_action( 'sync_zi_product_cron', array( $product_class, 'cmbird_zi_products_prepare_sync' ), 10, 2 );
148 add_action( 'sync_zi_pricelist', array( $import_pricelist, 'zi_get_pricelist' ), 10, 2 );
149 add_action( 'sync_zi_order', array( $common_class, 'cmbird_orders_prepare_sync' ), 10, 2 );
150 add_action( 'sync_zi_import_contacts', array( $contact_class, 'cmbird_get_zoho_contacts' ), 10, 2 );
151 add_action( 'cmbird_zi_category_cron', array( $category_class, 'cmbird_zi_category_sync_call' ), 10 );
152 // add action to set the zoho rate limit option exceeded to false.
153 add_action( 'cmbird_common', array( CMBIRD_Common_Functions::class, 'set_zoho_rate_limit_option' ) );
154 // Zoho CRM Hooks.
155 add_action( 'sync_zcrm_order', array( $common_class, 'cmbird_orders_prepare_sync' ) );
156 add_action( 'sync_zcrm_contact', array( ZohoCRMSync::class, 'cmbird_zcrm_contact_sync' ) );
157 // Exact Online Hooks.
158 add_action( 'cmbird_sync_eo', array( ExactOnlineSync::class, 'sync' ), 10, 3 );
159 add_action( 'cmbird_exact_online_sync_orders', array( ExactOnlineSync::class, 'sync_orders_via_cron' ) );
160 add_action( 'cmbird_payment_status', array( ExactOnlineSync::class, 'cmbird_payment_status' ), 10, 1 );
161 add_action( 'cmbird_eo_get_payment_statuses', array( ExactOnlineSync::class, 'get_payment_status_via_cron' ) );
162 // Callback functions for scheduled action to process product or customer chunk.
163 add_action(
164 'cmbird_process_product_chunk',
165 function ( $args ) {
166 if ( ! is_array( $args ) || empty( $args['transient_key'] ) ) {
167 return;
168 }
169 $transient_key = $args['transient_key'];
170 $import_products = $args['import_products'] ?? false;
171 $chunked_products = get_transient( $transient_key );
172 if ( $chunked_products ) {
173 $sync = new ExactOnlineSync();
174 $sync->sync( 'product', $chunked_products, (bool) $import_products );
175 // Remove transient after processing.
176 delete_transient( $transient_key );
177 }
178 },
179 10,
180 1
181 );
182 add_action(
183 'cmbird_process_customer_chunk',
184 function ( $args ) {
185 if ( ! is_array( $args ) || empty( $args['transient_key'] ) ) {
186 return;
187 }
188 $transient_key = $args['transient_key'];
189 $import_customers = $args['import_customers'] ?? false;
190 $chunked_customers = get_transient( $transient_key );
191 if ( $chunked_customers ) {
192 $sync = new ExactOnlineSync();
193 $sync->sync( 'customer', $chunked_customers, (bool) $import_customers );
194 // Remove transient after processing.
195 delete_transient( $transient_key );
196 }
197 },
198 10,
199 1
200 );
201 // Zoho CRM Hooks.
202 add_action( 'init', array( ZohoCRMSync::class, 'refresh_token' ) );
203
204 // add classes to REST API.
205 add_action(
206 'rest_api_init',
207 function () {
208 new Zoho();
209 new Exact();
210 new ProductWebhook();
211 new ShippingWebhook();
212 new CreateOrderWebhook();
213 new CMBird_APIs();
214 $po_controller = new CMBIRD_REST_Shop_Purchase_Controller();
215 $po_controller->register_routes();
216 }
217 );
218
219 add_action(
220 'save_post',
221 function ( $post_id, $post ) {
222 if ( 'wcb2b_group' === $post->post_type ) {
223 delete_transient( 'wc_b2b_groups' );
224 }
225 },
226 10,
227 2
228 );
229
230 /**
231 * Perform actions when the plugin is updated
232 *
233 * @param string $upgrader_object
234 * @param array $options
235 * @return void
236 */
237 add_action( 'upgrader_process_complete', 'cmbird_update_plugin_tasks', 10, 2 );
238
239 /**
240 * Perform tasks when the plugin is updated.
241 *
242 * @param \WP_Upgrader $upgrader_object - Upgrader object.
243 * @param array $options - Options array.
244 *
245 * @see https://developer.wordpress.org/reference/hooks/upgrader_process_complete/
246 */
247 function cmbird_update_plugin_tasks( $upgrader_object, $options ) {
248 $this_plugin = plugin_basename( __FILE__ );
249
250 if ( 'update' === $options['action'] && 'plugin' === $options['type'] ) {
251 foreach ( $options['plugins'] as $plugin ) {
252 if ( $plugin === $this_plugin ) {
253 // Perform tasks when the plugin is updated.
254 if ( defined( 'WP_CLI' ) && WP_CLI ) {
255 WP_CLI::add_command(
256 'cmbird:clean-zoho-images',
257 function () {
258 $result = cmbird_delete_zoho_orphaned_attachments();
259 WP_CLI::log( wp_json_encode( $result, JSON_PRETTY_PRINT ) );
260 }
261 );
262 }
263 }
264 }
265 }
266 }
267
268 /**
269 * Delete orphaned image attachments whose files no longer exist on disk.
270 * Checks all files in the uploads directory and its subfolders.
271 *
272 * @return array { deleted_posts, deleted_postmeta, deleted_terms, scanned, kept_exists }
273 */
274 function cmbird_delete_zoho_orphaned_attachments(): array {
275 global $wpdb;
276
277 // Only allow in admin/CLI for safety (optional but recommended).
278 if ( ! ( is_admin() || ( defined( 'WP_CLI' ) && WP_CLI ) ) ) {
279 return array(
280 'error' => 'Not allowed outside admin/CLI.',
281 );
282 }
283
284 // 1) Collect all attachment IDs (not just zoho_image).
285 $ids = $wpdb->get_col(
286 $wpdb->prepare(
287 "SELECT p.ID
288 FROM {$wpdb->posts} p
289 INNER JOIN {$wpdb->postmeta} pm
290 ON pm.post_id = p.ID AND pm.meta_key = '_wp_attached_file'
291 WHERE p.post_type = 'attachment'
292 AND p.post_mime_type LIKE %s",
293 'image/%'
294 )
295 );
296
297 if ( empty( $ids ) ) {
298 return array(
299 'deleted_posts' => 0,
300 'deleted_postmeta' => 0,
301 'deleted_terms' => 0,
302 'scanned' => 0,
303 'kept_exists' => 0,
304 'message' => 'No image attachments were found.',
305 );
306 }
307
308 // 2) Safety: keep only IDs where the actual file is missing on disk.
309 $uploads = wp_get_upload_dir();
310 $basedir = trailingslashit( $uploads['basedir'] );
311 $missing = array();
312 $kept_exist = 0;
313
314 // Fetch the relative paths for the candidate IDs.
315 $id_count = count( $ids );
316 if ( 0 === $id_count ) {
317 return array(
318 'deleted_posts' => 0,
319 'deleted_postmeta' => 0,
320 'deleted_terms' => 0,
321 'scanned' => 0,
322 'kept_exists' => 0,
323 'message' => 'No image attachments were found.',
324 );
325 }
326
327 $placeholders = implode( ',', array_fill( 0, $id_count, '%d' ) );
328
329 $paths = $wpdb->get_results(
330 $wpdb->prepare(
331 "SELECT pm.post_id AS id, pm.meta_value AS rel_path
332 FROM {$wpdb->postmeta} pm
333 WHERE pm.meta_key = '_wp_attached_file'
334 AND pm.post_id IN ($placeholders)", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
335 ...$ids
336 ),
337 ARRAY_A
338 );
339
340 // Map paths for quick lookup.
341 $map = array();
342 foreach ( $paths as $row ) {
343 $map[ (int) $row['id'] ] = $row['rel_path'];
344 }
345
346 foreach ( $ids as $id ) {
347 $rel = $map[ $id ] ?? '';
348 $abs = $rel ? $basedir . ltrim( $rel, '/\\' ) : '';
349 if ( empty( $rel ) || ! file_exists( $abs ) ) {
350 $missing[] = (int) $id;
351 } else {
352 ++$kept_exist;
353 }
354 }
355
356 if ( empty( $missing ) ) {
357 return array(
358 'deleted_posts' => 0,
359 'deleted_postmeta' => 0,
360 'deleted_terms' => 0,
361 'scanned' => count( $ids ),
362 'kept_exists' => $kept_exist,
363 'message' => 'All referenced files still exist; nothing to delete.',
364 );
365 }
366
367 // Deletion helpers.
368 $delete_in = function ( string $table, string $col, array $id_list ) use ( $wpdb ): int {
369 $id_list = array_map( 'intval', $id_list );
370 if ( empty( $id_list ) ) {
371 return 0;
372 }
373 // Sanitize table and column names.
374 $table = preg_replace( '/[^a-zA-Z0-9_]/', '', $table );
375 $col = preg_replace( '/[^a-zA-Z0-9_]/', '', $col );
376 $affected = 0;
377 $chunks = array_chunk( $id_list, 500 );
378 foreach ( $chunks as $chunk ) {
379 $placeholders = implode( ',', array_fill( 0, count( $chunk ), '%d' ) );
380 $query = "DELETE FROM `$table` WHERE `$col` IN ($placeholders)";
381 $res = $wpdb->query( $wpdb->prepare( $query, ...$chunk ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared.
382 $affected += (int) $res;
383 }
384 return $affected;
385 };
386
387 $deleted_posts = 0;
388 $deleted_meta = 0;
389 $deleted_terms = 0;
390 $total_to_delete = count( $missing );
391 $in_transaction = false;
392
393 // 3) Transaction (optional, best effort; will no-op if MySQL engine doesn't support).
394 $wpdb->query( 'START TRANSACTION' );
395 $in_transaction = true;
396
397 try {
398 // Delete post meta.
399 $deleted_meta = $delete_in( $wpdb->postmeta, 'post_id', $missing );
400 // Delete term relationships (usually none for attachments, but safe).
401 $deleted_terms = $delete_in( $wpdb->term_relationships, 'object_id', $missing );
402 // Delete the posts.
403 $deleted_posts = $delete_in( $wpdb->posts, 'ID', $missing );
404
405 $wpdb->query( 'COMMIT' );
406 $in_transaction = false;
407
408 return array(
409 'deleted_posts' => $deleted_posts,
410 'deleted_postmeta' => $deleted_meta,
411 'deleted_terms' => $deleted_terms,
412 'scanned' => count( $ids ),
413 'kept_exists' => $kept_exist,
414 'deleted_requested' => $total_to_delete,
415 'message' => 'Deleted orphaned image attachments and related data.',
416 );
417
418 } catch ( \Throwable $e ) {
419 if ( $in_transaction ) {
420 $wpdb->query( 'ROLLBACK' );
421 }
422 return array(
423 'error' => 'Deletion failed: ' . $e->getMessage(),
424 'scanned' => count( $ids ),
425 );
426 }
427 }
428