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