PluginProbe ʕ •ᴥ•ʔ
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). / 2.4.4
CommerceBird – AI Command Center, ERP Integrations & B2B for WooCommerce (Zoho, Exact Online). v2.4.4
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 10 months ago includes 10 months ago languages 1 year ago vendor 10 months ago LICENSE 1 year ago commercebird.php 10 months ago composer.json 1 year ago data-sync.php 1 year ago index.php 1 year ago readme.txt 10 months ago
commercebird.php
405 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.4
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.4' );
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 use WP_CLI;
62
63 /*
64 |--------------------------------------------------------------------------
65 | Activation, deactivation and uninstall event.
66 |--------------------------------------------------------------------------
67 */
68
69 register_activation_hook( __FILE__, array( Plugin::class, 'activate' ) );
70 register_deactivation_hook( __FILE__, array( Plugin::class, 'deactivate' ) );
71 register_uninstall_hook( __FILE__, array( Plugin::class, 'uninstall' ) );
72
73 /*
74 |--------------------------------------------------------------------------
75 | Load the plugin translations
76 |--------------------------------------------------------------------------
77 */
78 add_action( 'init', 'cmbird_load_textdomain' );
79 function cmbird_load_textdomain() {
80 load_plugin_textdomain( 'commercebird', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
81 }
82
83 /** Loading Purchase Order Class
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 * @param string $upgrader_object
211 * @param array $options
212 * @return void
213 */
214 add_action( 'upgrader_process_complete', 'cmbird_update_plugin_tasks', 10, 2 );
215
216 function cmbird_update_plugin_tasks( $upgrader_object, $options ) {
217 $this_plugin = plugin_basename( __FILE__ );
218
219 if ( 'update' === $options['action'] && 'plugin' === $options['type'] ) {
220 foreach ( $options['plugins'] as $plugin ) {
221 if ( $plugin === $this_plugin ) {
222 // Perform tasks when the plugin is updated
223 if ( defined( 'WP_CLI' ) && WP_CLI ) {
224 WP_CLI::add_command( 'cmbird:clean-zoho-images',
225 function () {
226 $result = cmbird_delete_zoho_orphaned_attachments();
227 WP_CLI::log( json_encode( $result, JSON_PRETTY_PRINT ) );
228 }
229 );
230 }
231 // change the post meta key name from "cost_price" to "_cost_price" using wpdb query
232 global $wpdb;
233 $wpdb->query( "UPDATE $wpdb->postmeta SET meta_key = '_cost_price' WHERE meta_key = 'cost_price'" );
234 // add 'cmbird_' to every option key name that starts with 'zoho_' using wpdb query
235 $zoho_options = $wpdb->get_results( "SELECT option_name FROM $wpdb->options WHERE option_name LIKE 'zoho_%' OR option_name LIKE 'zi_%'" );
236 foreach ( $zoho_options as $zoho_option ) {
237 if ( strpos( $zoho_option->option_name, 'cmbird_' ) !== 0 ) {
238 $new_option_name = 'cmbird_' . $zoho_option->option_name;
239 $wpdb->query( $wpdb->prepare( "UPDATE $wpdb->options SET option_name = %s WHERE option_name = %s", $new_option_name, $zoho_option->option_name ) );
240 }
241 }
242 }
243 }
244 }
245 }
246
247 /**
248 * Delete orphaned image attachments whose files no longer exist on disk.
249 * Checks all files in the uploads directory and its subfolders.
250 *
251 * @return array { deleted_posts, deleted_postmeta, deleted_terms, scanned, kept_exists }
252 */
253 function cmbird_delete_zoho_orphaned_attachments(): array {
254 global $wpdb;
255
256 // Only allow in admin/CLI for safety (optional but recommended).
257 if ( ! ( is_admin() || ( defined( 'WP_CLI' ) && WP_CLI ) ) ) {
258 return array(
259 'error' => 'Not allowed outside admin/CLI.',
260 );
261 }
262
263 // 1) Collect all attachment IDs (not just zoho_image)
264 $ids = $wpdb->get_col(
265 $wpdb->prepare(
266 "SELECT p.ID
267 FROM {$wpdb->posts} p
268 INNER JOIN {$wpdb->postmeta} pm
269 ON pm.post_id = p.ID AND pm.meta_key = '_wp_attached_file'
270 WHERE p.post_type = 'attachment'
271 AND p.post_mime_type LIKE %s",
272 'image/%'
273 )
274 );
275
276 if ( empty( $ids ) ) {
277 return array(
278 'deleted_posts' => 0,
279 'deleted_postmeta' => 0,
280 'deleted_terms' => 0,
281 'scanned' => 0,
282 'kept_exists' => 0,
283 'message' => 'No image attachments were found.',
284 );
285 }
286
287 // 2) Safety: keep only IDs where the actual file is missing on disk
288 $uploads = wp_get_upload_dir();
289 $basedir = trailingslashit( $uploads['basedir'] );
290 $missing = array();
291 $kept_exist = 0;
292
293 // Fetch the relative paths for the candidate IDs
294 $id_count = count( $ids );
295 if ( 0 === $id_count ) {
296 return array(
297 'deleted_posts' => 0,
298 'deleted_postmeta' => 0,
299 'deleted_terms' => 0,
300 'scanned' => 0,
301 'kept_exists' => 0,
302 'message' => 'No image attachments were found.',
303 );
304 }
305
306 $placeholders = implode( ',', array_fill( 0, $id_count, '%d' ) );
307 $query = "SELECT pm.post_id AS id, pm.meta_value AS rel_path
308 FROM {$wpdb->postmeta} pm
309 WHERE pm.meta_key = '_wp_attached_file'
310 AND pm.post_id IN ($placeholders)";
311
312 $paths = $wpdb->get_results(
313 $wpdb->prepare( $query, ...$ids ), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
314 ARRAY_A
315 );
316
317 // Map paths for quick lookup
318 $map = array();
319 foreach ( $paths as $row ) {
320 $map[ (int) $row['id'] ] = $row['rel_path'];
321 }
322
323 foreach ( $ids as $id ) {
324 $rel = $map[ $id ] ?? '';
325 $abs = $rel ? $basedir . ltrim( $rel, '/\\' ) : '';
326 if ( empty( $rel ) || ! file_exists( $abs ) ) {
327 $missing[] = (int) $id;
328 } else {
329 $kept_exist++;
330 }
331 }
332
333 if ( empty( $missing ) ) {
334 return array(
335 'deleted_posts' => 0,
336 'deleted_postmeta' => 0,
337 'deleted_terms' => 0,
338 'scanned' => count( $ids ),
339 'kept_exists' => $kept_exist,
340 'message' => 'All referenced files still exist; nothing to delete.',
341 );
342 }
343
344 // Deletion helpers
345 $delete_in = function (string $table, string $col, array $id_list) use ($wpdb): int {
346 $id_list = array_map( 'intval', $id_list );
347 if ( empty( $id_list ) ) {
348 return 0;
349 }
350 // Sanitize table and column names
351 $table = preg_replace( '/[^a-zA-Z0-9_]/', '', $table );
352 $col = preg_replace( '/[^a-zA-Z0-9_]/', '', $col );
353 $affected = 0;
354 $chunks = array_chunk( $id_list, 500 );
355 foreach ( $chunks as $chunk ) {
356 $placeholders = implode( ',', array_fill( 0, count( $chunk ), '%d' ) );
357 $query = "DELETE FROM `$table` WHERE `$col` IN ($placeholders)";
358 $res = $wpdb->query( $wpdb->prepare( $query, ...$chunk ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
359 $affected += (int) $res;
360 }
361 return $affected;
362 };
363
364 $deleted_posts = 0;
365 $deleted_meta = 0;
366 $deleted_terms = 0;
367 $total_to_delete = count( $missing );
368 $in_transaction = false;
369
370 // 3) Transaction (optional, best effort; will no-op if MySQL engine doesn't support)
371 $wpdb->query( 'START TRANSACTION' );
372 $in_transaction = true;
373
374 try {
375 // Delete post meta
376 $deleted_meta = $delete_in( $wpdb->postmeta, 'post_id', $missing );
377 // Delete term relationships (usually none for attachments, but safe)
378 $deleted_terms = $delete_in( $wpdb->term_relationships, 'object_id', $missing );
379 // Delete the posts
380 $deleted_posts = $delete_in( $wpdb->posts, 'ID', $missing );
381
382 $wpdb->query( 'COMMIT' );
383 $in_transaction = false;
384
385 return array(
386 'deleted_posts' => $deleted_posts,
387 'deleted_postmeta' => $deleted_meta,
388 'deleted_terms' => $deleted_terms,
389 'scanned' => count( $ids ),
390 'kept_exists' => $kept_exist,
391 'deleted_requested' => $total_to_delete,
392 'message' => 'Deleted orphaned image attachments and related data.',
393 );
394
395 } catch (\Throwable $e) {
396 if ( $in_transaction ) {
397 $wpdb->query( 'ROLLBACK' );
398 }
399 return array(
400 'error' => 'Deletion failed: ' . $e->getMessage(),
401 'scanned' => count( $ids ),
402 );
403 }
404 }
405