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