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