Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 613
0.00% covered (danger)
0.00%
0 / 35
CRAP
n/a
0 / 0
zeroBSCRM_migrations_markComplete
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
zeroBSCRM_migrations_getCompleted
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
jpcrm_migrations_get_migration
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
zeroBSCRM_migrations_run
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
552
zeroBSCRM_migrations_checks_postsettings
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
zeroBSCRM_adminNotices_majorMigrationError
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
zeroBSCRM_migration_288
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
zeroBSCRM_migration_2963
0.00% covered (danger)
0.00%
0 / 68
0.00% covered (danger)
0.00%
0 / 1
156
zeroBSCRM_migration_29999
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
zeroBSCRM_migration_411
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
zeroBSCRM_migration_50
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
zeroBSCRM_migration_53
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
272
zeroBSCRM_migration_54
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
56
zeroBSCRM_migration_543
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
56
zeroBSCRM_migration_544
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
zeroBSCRM_migration_55
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
2
zeroBSCRM_migration_55a
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
zeroBSCRM_migration_551
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
zeroBSCRM_migration_560_move_custom_file_upload_box
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
72
zeroBSCRM_migration_560_move_file_array
0.00% covered (danger)
0.00%
0 / 62
0.00% covered (danger)
0.00%
0 / 1
240
zeroBSCRM_migration_560
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
zeroBSCRM_migration_create_workflows_table
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
zeroBSCRM_migration_task_offset_fix
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
zeroBSCRM_migration_refresh_user_roles
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
zeroBSCRM_migration_regenerate_tag_slugs
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
zeroBSCRM_migration_invoice_language_fixes
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
132
zeroBSCRM_migration_gh3465_increase_city_field_size
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
zeroBSCRM_migration_tax_rate_precision_fix
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
zeroBSCRM_migration_ensure_notifications_table
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
20
zeroBSCRM_migration_addErrToStack
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
zeroBSCRM_migration_tableHasColumn
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
jpcrm_migration_table_has_index
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
zeraBSCRM_migration_get_column_data_type
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
jpcrm_migration_load_wp_filesystem_direct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
jpcrm_migration_regenerate_tag_slugs_for_obj_type
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
90
1<?php
2/*
3 * Jetpack CRM
4 * https://jetpackcrm.com
5 * V1.1.18
6 *
7 * Copyright 2020 Automattic
8 *
9 * Date: 30/08/16
10 */
11
12defined( 'ZEROBSCRM_PATH' ) || exit( 0 );
13
14/*
15======================================================
16    MIGRATION FUNCS
17    ====================================================== */
18
19global $zeroBSCRM_migrations; $zeroBSCRM_migrations = array(
20    '288', // build client portal page (moved to shortcodes) if using
21    '2963', // 2.96.3 - installs page templates
22    '29999', // Flush permalinks
23    '411', // 4.11.0 - Ensure upload folders are secure
24    '50', // 5.0 - Alter external sources table for existing users (added origin)
25    '53', // 5.3 - Migrate all encrypted data to new encryption endpoints
26    '54', // 5.4 - Support pinned logs, migrate all log meta stored in old dehydrated fashion
27    '543', // 5.4.3 - Deletes unwanted .htaccess files
28    '544', // 5.4.2 Forces re-install of default fonts
29    '55', // 5.5 Deletes orphaned rows linked to invoices in the objlinks table
30    '55a', // 5.5a Recompiles segments after wp_loaded
31    '551', // 5.5.1 Deletes orphaned aka rows linked to contacts since deleted
32    '560', // 5.6.0 Moves old folder structure (zbscrm-store) to new (jpcrm-storage)
33    'task_offset_fix', // removes task timezone offsets from database
34    'refresh_user_roles', // Refresh user roles
35    'regenerate_tag_slugs', // Regenerate tag slugs
36    'create_workflows_table', // Create "workflows" table.
37    'invoice_language_fixes', // Store invoice statuses and mappings consistently
38    'gh3465_increase_city_field_size',  // from gh issue 3465, increases the city field size to 200
39    'tax_rate_precision_fix', // increase tax rate precision from 2 to 10 decimal places
40    'ensure_notifications_table', // Ensure notifications table exists
41);
42
43global $zeroBSCRM_migrations_requirements; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
44$zeroBSCRM_migrations_requirements = array( // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
45    '288'  => array( 'postsettings' ),
46    '53'   => array( 'postsettings' ),
47    '5402' => array( 'postsettings' ),
48    '55a'  => array( 'wp_loaded' ),
49);
50
51// mark's a migration complete
52function zeroBSCRM_migrations_markComplete( $migrationKey = -1, $logObj = false ) {
53
54    global $zeroBSCRM_migrations;
55
56    if ( ! empty( $migrationKey ) && in_array( $migrationKey, $zeroBSCRM_migrations ) ) {
57
58        $completedMigrations   = zeroBSCRM_migrations_getCompleted();
59        $completedMigrations[] = $migrationKey;
60
61        // we're using wp options because they're reliable OUTSIDE of the scope of our settings model
62        // ... which has changed through versions
63        // the separation here is key, at 2.88 WH discovered much re-running + pain due to this.
64        // stick to a separate migration system (away from zbssettings)
65        update_option( 'zbsmigrations', $completedMigrations, false );
66
67        // log opt?
68        update_option(
69            'zbsmigration' . $migrationKey,
70            array(
71                'completed' => time(),
72                'meta'      => $logObj,
73            ),
74            false
75        );
76
77    }
78}
79
80// gets the list of completed migrations
81function zeroBSCRM_migrations_getCompleted() {
82
83    // we're using wp options because they're reliable OUTSIDE of the scope of our settings model
84    // ... which has changed through versions
85    // the separation here is key, at 2.88 WH discovered much re-running + pain due to this.
86    // stick to a separate migration system (away from zbssettings)
87
88    // BUT WAIT! hilariously, for those who already have finished migrations, this'll re-run them
89    // ... so here we 'MIGRATE' the migrations :o ffs
90    global $zbs;
91    $migrations = $zbs->settings->get( 'migrations' ); if ( isset( $migrations ) && is_array( $migrations ) && count( $migrations ) > 0 ) {
92
93        $existingMigrationsMigration = get_option( 'zbsmigrationsdal', -1 );
94
95        if ( $existingMigrationsMigration == -1 ) {
96            // copy over +
97            // to stop this ever rerunning + confusing things, we set an option to say migrated the migrations, LOL
98            update_option( 'zbsmigrations', $migrations, false );
99            update_option( 'zbsmigrationsdal', 2, false );
100        }
101    }
102
103    // normal return
104    return get_option( 'zbsmigrations', array() );
105}
106
107// gets details on a migration
108function jpcrm_migrations_get_migration( $migrationKey = '' ) {
109
110    // we're using wp options because they're reliable OUTSIDE of the scope of our settings model
111    // ... which has changed through versions
112    // the separation here is key, at 2.88 WH discovered much re-running + pain due to this.
113    // stick to a separate migration system (away from zbssettings)
114    $finished   = false;
115    $migrations = zeroBSCRM_migrations_getCompleted();
116    if ( in_array( $migrationKey, $migrations ) ) {
117        $finished = true;
118    }
119
120    return array( $finished, get_option( 'zbsmigration' . $migrationKey, false ) );
121}
122
123function zeroBSCRM_migrations_run( $settingsArr = false, $run_at = 'init' ) {
124
125    global $zeroBSCRM_migrations, $zeroBSCRM_migrations_requirements;
126
127        // catch migration block removal (can be run from system status):
128    if ( jpcrm_perms_manage_options() && isset( $_GET['resetmigrationblock'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ?? '' ) ), 'resetmigrationblock' ) ) {
129
130        // unblock migration blocks
131        delete_option( 'zbsmigrationpreloadcatch' );
132        delete_option( 'zbsmigrationblockerrors' );
133
134        // flag
135        $migrationBlocksRemoved = true;
136    }
137
138    #} Check if we've been stumped by blocking errs, and STOP migrating if so
139    $blockingErrs = get_option( 'zbsmigrationblockerrors', false );
140    if ( $blockingErrs !== false && ! empty( $blockingErrs ) ) {
141        return false;
142    }
143
144    #} load migrated list if not loaded
145    $migratedAlreadyArr = zeroBSCRM_migrations_getCompleted();
146
147    #} Run count
148    $migrationRunCount = 0;
149
150    #} cycle through any migrations + fire if not fired.
151    if ( count( $zeroBSCRM_migrations ) > 0 ) {
152        foreach ( $zeroBSCRM_migrations as $migration ) {
153
154            if ( ! in_array( $migration, $migratedAlreadyArr ) && function_exists( 'zeroBSCRM_migration_' . $migration ) ) {
155
156                $run = true;
157
158                // check reached state
159                if ( isset( $zeroBSCRM_migrations_requirements[ $migration ] ) ) {
160
161                    // 'preload' requirement means this migration needs to run AFTER a reload AFTER the previous migration
162                    // ... so if preload here, we kill this loop, if prev migrations have run
163                    if ( in_array( 'preload', $zeroBSCRM_migrations_requirements[ $migration ] ) && $migrationRunCount > 0 ) {
164
165                        // ... as a catch to stop infinite reloads, we check whether more than 3 of these have run in a row, and we stop that.
166                        $previousAttempts = get_option( 'zbsmigrationpreloadcatch', array() );
167                        if ( ! is_array( $previousAttempts ) ) {
168                            $previousAttempts = array();
169                        }
170                        if ( ! isset( $previousAttempts[ $migration ] ) ) {
171                            $previousAttempts[ $migration ] = 1;
172                        }
173                        if ( $previousAttempts[ $migration ] < 5 ) {
174
175                            // update count
176                            ++$previousAttempts[ $migration ];
177                            update_option( 'zbsmigrationpreloadcatch', $previousAttempts, false );
178
179                            // stop running migrations, reload the page
180                            header( 'Refresh:0' );
181                            exit( 0 );
182
183                        } else {
184
185                            // set a global which'll show up on systemstatus if this state occurs.
186                            update_option( 'zbsmigrationblockerrors', $migration, false );
187
188                            // expose an error that the world's about to rupture
189                            add_action( 'after-zerobscrm-admin-init', 'zeroBSCRM_adminNotices_majorMigrationError' );
190                            add_action( 'admin_notices', 'zeroBSCRM_adminNotices_majorMigrationError' );
191
192                        }
193                    }
194
195                    // assume func
196                    foreach ( $zeroBSCRM_migrations_requirements[ $migration ] as $check ) {
197
198                        // skip 'preload', dealt with above
199                        // skip 'wp_loaded', dealt with in second run
200                        if ( $check !== 'preload' && $check !== 'wp_loaded' ) {
201
202                            $checkFuncName = 'zeroBSCRM_migrations_checks_' . $check;
203                            if ( ! call_user_func( $checkFuncName ) ) {
204                                $run = false;
205                            }
206                        }
207                    }
208
209                    // wp_loaded
210                    if ( in_array( 'wp_loaded', $zeroBSCRM_migrations_requirements[ $migration ] ) ) {
211
212                        $run = false;
213
214                        if ( $run_at == 'wp_loaded' ) {
215                            $run = true;
216                        }
217                    }
218                }
219
220                // go
221                if ( $run ) {
222
223                    // run migration
224                    call_user_func( 'zeroBSCRM_migration_' . $migration );
225
226                    // update count
227                    ++$migrationRunCount;
228
229                }
230            }
231        }
232    }
233}
234
235function zeroBSCRM_migrations_checks_postsettings() {
236
237    global $zbs;
238    /*
239    didn't work:
240    if (isset($zbs->settings) && method_exists($zbs->settings,'get')){
241        $possiblyInstalled = $zbs->settings->get('settingsinstalled',true);
242        if (isset($possiblyInstalled) && $possiblyInstalled > 0) return true;
243    } */
244    // HARD DB settings check
245    try {
246        $potentialDBSetting = $zbs->DAL->getSetting(
247            array(
248                'key'         => 'settingsinstalled',
249                'fullDetails' => false,
250            )
251        );
252
253        if ( isset( $potentialDBSetting ) && $potentialDBSetting > 0 ) {
254
255            return true;
256
257        }
258    } catch ( Exception $e ) {
259
260    }
261
262    return false;
263}
264
265// general migration mechanism error
266function zeroBSCRM_adminNotices_majorMigrationError() {
267
268    // pop in a Notify Me Notification here instead....?
269    if ( get_current_user_id() > 0 ) {
270
271        // already sent?
272        $msgSent = get_transient( 'zbs-migration-general-errors' );
273        if ( ! $msgSent ) {
274
275            zeroBSCRM_notifyme_insert_notification( get_current_user_id(), -999, -1, 'migration.blocked.errors', 'migration.blocked.errors' );
276            set_transient( 'zbs-migration-general-errors', 20, 24 * 7 * HOUR_IN_SECONDS );
277
278        }
279    }
280}
281
282/*
283======================================================
284    / MIGRATION FUNCS
285    ====================================================== */
286
287/*
288======================================================
289    MIGRATIONS
290    ====================================================== */
291
292    /*
293    * Migration 2.88 - build client portal page (moved to shortcodes) if using
294    */
295function zeroBSCRM_migration_288() {
296
297    global $zbs;
298
299    zeroBSCRM_portal_checkCreatePage();
300
301    zeroBSCRM_migrations_markComplete( '288', array( 'updated' => '1' ) );
302}
303
304    /*
305    * Migration 2.4 - Refresh user roles
306    *  Previously this was a number of template related migrations
307    *  for v5 we combined these, though in time the need for this method of install should be done away with
308    *  Previously, migrations: 2.96.3, 2.96.4, 2.96.6, 2.97.4, 4.0.7, 4.0.8
309    */
310function zeroBSCRM_migration_2963() {
311
312    global $zbs, $wpdb, $ZBSCRM_t;
313
314    #} Check + create
315    zeroBSCRM_checkTablesExist();
316
317    #} Make the DB emails...
318    zeroBSCRM_populateEmailTemplateList();
319
320    // ===== Previously: Migration 2.96.3 - adds new template for 'client portal pw reset'
321
322    #} default is admin email and CRM name
323    // now all done via zeroBSCRM_mailDelivery_defaultFromname
324    $from_name = zeroBSCRM_mailDelivery_defaultFromname();
325
326    /*
327        This wasn't used in end, switched to default mail delivery opt
328    $from_address = zeroBSCRM_mailDelivery_defaultEmail();; //default WordPress admin email ?
329    $reply_to = '';
330    $cc = ''; */
331    $deliveryMethod = zeroBSCRM_getMailDeliveryDefault();
332
333    $ID       = 6;
334    $reply_to = '';
335    $cc       = '';
336    $bcc      = '';
337
338    #} The email stuff...
339    $subject = __( 'Your Client Portal Password', 'zero-bs-crm' );
340    $content = zeroBSCRM_mail_retrieveDefaultBodyTemplate( 'clientportalpwreset' );
341    $active  = 1; // 1 = true..
342    if ( zeroBSCRM_mailTemplate_exists( $ID ) == 0 ) {
343        $content = zeroBSCRM_mailTemplate_processEmailHTML( $content );
344        // zeroBSCRM_insertEmailTemplate($ID,$from_name,$from_address,$reply_to,$cc,$bcc,$subject,$content,$active);
345        zeroBSCRM_insertEmailTemplate( $ID, $deliveryMethod, $bcc, $subject, $content, $active );
346    }
347
348    // ===== / Previously: Migration 2.96.3
349
350    // ===== Previously: last one hadn't got the html file, this ADDS file proper :)
351
352    #} default is admin email and CRM name
353    // now all done via zeroBSCRM_mailDelivery_defaultFromname
354    $from_name = zeroBSCRM_mailDelivery_defaultFromname();
355
356    /*
357        This wasn't used in end, switched to default mail delivery opt
358    $from_address = zeroBSCRM_mailDelivery_defaultEmail();; //default WordPress admin email ?
359    $reply_to = '';
360    $cc = ''; */
361    $deliveryMethod = zeroBSCRM_getMailDeliveryDefault();
362
363    $ID       = 6;
364    $reply_to = '';
365    $cc       = '';
366    $bcc      = '';
367
368    // BRUTAL DELETE old one
369    $wpdb->delete( $ZBSCRM_t['system_mail_templates'], array( 'zbsmail_id' => $ID ) );
370
371    #} The email stuff...
372    $subject = __( 'Your Client Portal Password', 'zero-bs-crm' );
373    $content = zeroBSCRM_mail_retrieveDefaultBodyTemplate( 'clientportalpwreset' );
374
375    $active = 1; // 1 = true..
376    if ( zeroBSCRM_mailTemplate_exists( $ID ) == 0 ) {
377        $content = zeroBSCRM_mailTemplate_processEmailHTML( $content );
378        // zeroBSCRM_insertEmailTemplate($ID,$from_name,$from_address,$reply_to,$cc,$bcc,$subject,$content,$active);
379        zeroBSCRM_insertEmailTemplate( $ID, $deliveryMethod, $bcc, $subject, $content, $active );
380    }
381
382    // ===== / Previously: last one hadn't got the html file, this ADDS file proper :)
383
384    // ===== Previously: adds template for 'invoice summary statement sent'
385
386    #} default is admin email and CRM name
387    // now all done via zeroBSCRM_mailDelivery_defaultFromname
388    $from_name = zeroBSCRM_mailDelivery_defaultFromname();
389
390    /*
391        This wasn't used in end, switched to default mail delivery opt
392    $from_address = zeroBSCRM_mailDelivery_defaultEmail();; //default WordPress admin email ?
393    $reply_to = '';
394    $cc = ''; */
395    $deliveryMethod = zeroBSCRM_getMailDeliveryDefault();
396
397    $ID       = 7;
398    $reply_to = '';
399    $cc       = '';
400    $bcc      = '';
401
402    #} The email stuff...
403    $subject = __( 'Your Statement', 'zero-bs-crm' );
404    $content = zeroBSCRM_mail_retrieveDefaultBodyTemplate( 'invoicestatementsent' );
405
406    // BRUTAL DELETE old one
407    $wpdb->delete( $ZBSCRM_t['system_mail_templates'], array( 'zbsmail_id' => $ID ) );
408
409    $active = 1; // 1 = true..
410    if ( zeroBSCRM_mailTemplate_exists( $ID ) == 0 ) {
411        $content = zeroBSCRM_mailTemplate_processEmailHTML( $content );
412        // zeroBSCRM_insertEmailTemplate($ID,$from_name,$from_address,$reply_to,$cc,$bcc,$subject,$content,$active);
413        zeroBSCRM_insertEmailTemplate( $ID, $deliveryMethod, $bcc, $subject, $content, $active );
414    }
415
416    // ===== / Previously: adds template for 'invoice summary statement sent'
417
418    // ===== Previously: 2.97.4 - fixes duplicated email templates (found on 2 installs so far)
419
420    // 7 template emails up to here :)
421    for ( $i = 0; $i <= 7; $i++ ) {
422
423        // count em
424        $sql = $wpdb->prepare( 'SELECT ID FROM ' . $ZBSCRM_t['system_mail_templates'] . ' WHERE zbsmail_id = %d GROUP BY ID ORDER BY zbsmail_id DESC, zbsmail_lastupdated DESC', $i );
425        $r   = $wpdb->get_results( $sql, ARRAY_A );
426
427            // if too many, delete oldest (few?)
428        if ( is_array( $r ) && count( $r ) > 1 ) {
429
430            $count = 0;
431
432            // first stays, as the above selects in order by last updated
433            foreach ( $r as $x ) {
434
435                // if already got one, delete this (extra)
436                if ( $count > 0 ) {
437
438                    // BRUTAL DELETE old one
439                    $wpdb->delete( $ZBSCRM_t['system_mail_templates'], array( 'ID' => $x['ID'] ) );
440
441                }
442
443                ++$count;
444
445            }
446        }
447    }
448
449    // ===== / Previously: 2.97.4 - fixes duplicated email templates (found on 2 installs so far)
450
451    // ===== Previously: 4.0.7 - corrects outdated task notification template
452
453    // retrieve existing template - hardtyped
454    $existingTemplate = $wpdb->get_var( 'SELECT zbsmail_body FROM ' . $ZBSCRM_t['system_mail_templates'] . ' WHERE ID = 6' );
455
456    // load new
457    $newTemplate = zeroBSCRM_mail_retrieveDefaultBodyTemplate( 'tasknotification' ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
458
459    // back it up into a WP option if was different
460    if ( $existingTemplate !== $newTemplate ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
461        update_option( 'jpcrm_tasknotificationtemplate', $existingTemplate, false ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
462    }
463
464    // overwrite
465    $sql = 'UPDATE ' . $ZBSCRM_t['system_mail_templates'] . ' SET zbsmail_body = %s WHERE ID = 6';
466    $q   = $wpdb->prepare( $sql, array( $newTemplate ) );
467    $wpdb->query( $q );
468
469    // ===== / Previously: 4.0.7 - corrects outdated task notification template
470
471    // ===== Previously: 4.0.8 - Set the default reference type for invoices & Update the existing template for email notifications (had old label)
472
473    if ( $zbs->DAL->invoices->getFullCount() > 0 ) {
474        // The user has used the invoice module. Default reference type = manual
475        $zbs->settings->update( 'reftype', 'manual' );
476    }
477
478    // Update the existing template for email notifications (had old label)
479    global $ZBSCRM_t, $wpdb;
480
481    // retrieve existing template - hardtyped
482    $existingTemplate = $wpdb->get_var( 'SELECT zbsmail_body FROM ' . $ZBSCRM_t['system_mail_templates'] . ' WHERE ID = 4' );
483
484    // load new
485    $newTemplate = zeroBSCRM_mail_retrieveDefaultBodyTemplate( 'invoicesent' );
486
487    // back it up into a WP option if was different
488    if ( $existingTemplate !== $newTemplate ) {
489        update_option( 'jpcrm_invnotificationtemplate', $existingTemplate, false );
490    }
491
492    // overwrite
493    $sql = 'UPDATE ' . $ZBSCRM_t['system_mail_templates'] . ' SET zbsmail_body = %s WHERE ID = 4';
494    $q   = $wpdb->prepare( $sql, array( $newTemplate ) );
495    $wpdb->query( $q );
496
497    // ===== / Previously: 4.0.8 - Set the default reference type for invoices & Update the existing template for email notifications (had old label)
498
499    zeroBSCRM_migrations_markComplete( '2963', array( 'updated' => '1' ) );
500}
501
502    /*
503    * Migration 2.99.99 - set permalinks to flush (was used with v3.0 migration, left in tact as portal may be dependent)
504    */
505function zeroBSCRM_migration_29999() {
506
507    // set permalinks to flush, this'll cause them to be refreshed on 3000 migration
508    // ... as that has preload setting
509    jpcrm_flag_for_flush_rewrite();
510
511    // fini
512    zeroBSCRM_migrations_markComplete( '29999', array( 'updated' => 1 ) );
513}
514
515    /*
516    * Migration 4.11.0 - secure upload folders
517    *  previously:
518    *  4.5.0 - Adds indexing protection to directories with potentially sensitive .html files
519    *  4.11.0 - secure upload folders
520    */
521function zeroBSCRM_migration_411() {
522
523    $wp_uploads_dir = wp_upload_dir();
524
525    // directories to secure
526    // if these ever expand beyond this we should move the list to core & manage periodic checks
527    $directories = array(
528
529        ZEROBSCRM_PATH . 'templates/',
530        ZEROBSCRM_PATH . 'templates/emails/',
531        ZEROBSCRM_PATH . 'templates/invoices/',
532        ZEROBSCRM_PATH . 'templates/quotes/',
533
534        $wp_uploads_dir['basedir'] . '/' . 'zbscrm-store/_wip/',
535
536    );
537
538    // secure them!
539    foreach ( $directories as $directory ) {
540        jpcrm_create_and_secure_dir_from_external_access( $directory, true );
541    }
542
543    jpcrm_create_and_secure_dir_from_external_access( $wp_uploads_dir['basedir'] . '/' . 'zbscrm-store/', false );
544
545    // mark complete
546    zeroBSCRM_migrations_markComplete( '411', array( 'updated' => 1 ) );
547}
548
549    /*
550    * Migration 5.0 - Alter external sources table for existing users (added origin)
551    */
552function zeroBSCRM_migration_50() {
553
554    global $zbs, $wpdb, $ZBSCRM_t;
555
556    // external source tweak
557    if ( ! zeroBSCRM_migration_tableHasColumn( $ZBSCRM_t['externalsources'], 'zbss_origin' ) ) {
558
559        $sql = 'ALTER TABLE ' . $ZBSCRM_t['externalsources'] . ' ADD COLUMN `zbss_origin` VARCHAR(400) NULL DEFAULT NULL AFTER `zbss_uid`, ADD INDEX (zbss_origin);';
560        $wpdb->query( $sql );
561
562    }
563
564    // add transaction status
565
566    // build string
567    $transaction_statuses = zeroBSCRM_getTransactionsStatuses( true );
568    $deleted_string       = __( 'Deleted', 'zero-bs-crm' );
569    if ( ! in_array( $deleted_string, $transaction_statuses ) ) {
570        $transaction_statuses[] = $deleted_string;
571    }
572    $transaction_statuses_str = implode( ',', $transaction_statuses );
573
574    // update
575    $customisedFields                              = $zbs->settings->get( 'customisedfields' );
576    $customisedFields['transactions']['status'][1] = $transaction_statuses_str;
577    $zbs->settings->update( 'customisedfields', $customisedFields );
578
579    // mark complete
580    zeroBSCRM_migrations_markComplete( '50', array( 'updated' => 1 ) );
581}
582
583    /*
584    * 5.3 - Migrate all encrypted data to new encryption endpoints
585    */
586function zeroBSCRM_migration_53() {
587
588    global $zbs;
589
590    // load libs
591
592    // ~5.3
593    if ( ! function_exists( 'zeroBSCRM_encrypt' ) ) {
594        require ZEROBSCRM_INCLUDE_PATH . 'ZeroBSCRM.Encryption.php';
595    }
596
597    // 5.3~
598    $zbs->load_encryption();
599
600    // count
601    $successful_recryptions = 0;
602
603    // Mail Delivery methods (if any):
604
605    // previous decrypt key
606    $decryption_key = hex2bin( zeroBSCRM_getSetting( 'smtpkey' ) );
607
608    // retrieve existing
609    $existing_mail_delivery_methods = zeroBSCRM_getSetting( 'smtpaccs' );
610    if ( ! is_array( $existing_mail_delivery_methods ) ) {
611        $existing_mail_delivery_methods = array();
612    }
613
614    // cycle through them and re-encrypt
615    $replacement_delivery_methods = array();
616    foreach ( $existing_mail_delivery_methods as $method_key => $method_array ) {
617
618            $updated_method_array = $method_array;
619
620        if ( isset( $method_array['pass'] ) ) {
621
622            // decrypt (hiding deprecation notices via param)
623            $password = zeroBSCRM_encryption_unsafe_process( 'decrypt', $method_array['pass'], $decryption_key, zeroBSCRM_get_iv( true ), true );
624
625            // This is used as a fallback because some users can still have passwords
626            // that were encrypted using the wrong IV.
627            if ( ! $password ) {
628                $password = zeroBSCRM_encryption_unsafe_process( 'decrypt', $method_array['pass'], $decryption_key, $decryption_key, true );
629            }
630
631            if ( $password ) {
632
633                // encrypt password:
634                $updated_method_array['pass'] = $zbs->encryption->encrypt( $password, 'smtp' );
635
636            } else {
637
638                // keep existing ciphertext; likely already updated but otherwise corrupt
639                $updated_method_array['pass'] = $method_array['pass'];
640
641            }
642
643            ++$successful_recryptions;
644
645        }
646
647            $replacement_delivery_methods[ $method_key ] = $updated_method_array;
648
649    }
650
651    // update em
652    $zbs->settings->update( 'smtpaccs', $replacement_delivery_methods );
653
654    // There was some old usage of pwmanager on companys with CPTs, for now we're skipping support.
655    // $pws = get_post_meta($id,$zbsPasswordManager['dbkey'],true);
656
657    // hash secret if not already hashed
658    $api_secret = $zbs->DAL->setting( 'api_secret' ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
659    if ( str_starts_with( $api_secret, 'zbscrm_' ) ) {
660        $hashed_api_secret = $zbs->encryption->hash( $api_secret );
661        $zbs->DAL->updateSetting( 'api_secret', $hashed_api_secret ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
662    }
663
664    global $wpdb, $ZBSCRM_t;
665    // add indexes for performance
666    if ( jpcrm_database_server_has_ability( 'fulltext_index' ) && ! jpcrm_migration_table_has_index( $ZBSCRM_t['customfields'], 'search' ) ) {
667        $sql = 'ALTER TABLE ' . $ZBSCRM_t['customfields'] . ' ADD FULLTEXT INDEX `search` (`zbscf_objval`);';
668        $wpdb->query( $sql );
669    }
670    if ( ! jpcrm_migration_table_has_index( $ZBSCRM_t['taglinks'], 'zbstl_tagid+zbstl_objtype' ) ) {
671        $sql = 'ALTER TABLE ' . $ZBSCRM_t['taglinks'] . ' ADD INDEX `zbstl_tagid+zbstl_objtype` (`zbstl_tagid`,`zbstl_objtype`) USING BTREE;';
672        $wpdb->query( $sql );
673    }
674    if ( ! jpcrm_migration_table_has_index( $ZBSCRM_t['externalsources'], 'zbss_uid+zbss_source+zbss_objtype' ) ) {
675        $sql = 'ALTER TABLE ' . $ZBSCRM_t['externalsources'] . ' ADD INDEX `zbss_uid+zbss_source+zbss_objtype` (`zbss_uid`,`zbss_source`,`zbss_objtype`) USING BTREE;';
676        $wpdb->query( $sql );
677    }
678    if ( ! jpcrm_migration_table_has_index( $ZBSCRM_t['meta'], 'zbsm_objid+zbsm_key+zbsm_objtype' ) ) {
679        $sql = 'ALTER TABLE ' . $ZBSCRM_t['meta'] . ' ADD INDEX `zbsm_objid+zbsm_key+zbsm_objtype` (`zbsm_objid`,`zbsm_key`,`zbsm_objtype`) USING BTREE;';
680        $wpdb->query( $sql );
681    }
682    if ( ! jpcrm_migration_table_has_index( $ZBSCRM_t['logs'], 'zbsl_created' ) ) {
683        $sql = 'ALTER TABLE ' . $ZBSCRM_t['logs'] . ' ADD INDEX `zbsl_created` (`zbsl_created`) USING BTREE;';
684        $wpdb->query( $sql );
685    }
686    if ( ! jpcrm_migration_table_has_index( $ZBSCRM_t['contacts'], 'zbsc_status' ) ) {
687        $sql = 'ALTER TABLE ' . $ZBSCRM_t['contacts'] . ' ADD INDEX `zbsc_status` (`zbsc_status`) USING BTREE;';
688        $wpdb->query( $sql );
689    }
690
691    // remove errant .htaccess file
692    $wp_uploads_dir  = wp_upload_dir();
693    $errant_htaccess = $wp_uploads_dir['basedir'] . '/' . 'zbscrm-store/.htaccess';
694    if ( file_exists( $errant_htaccess ) ) {
695        unlink( $errant_htaccess );
696    }
697    // mark complete
698    zeroBSCRM_migrations_markComplete( '53', array( 'updated' => $successful_recryptions ) );
699}
700
701    /*
702    * Migration 5.4
703 * - Support pinned logs.
704 * - Migrate all log meta stored in old dehydrated fashion. (Will do in 1k chunks until finished.)
705    */
706function zeroBSCRM_migration_54() {
707
708    global $zbs, $wpdb, $ZBSCRM_t;
709
710    // add zbsl_pinned to log table if not existing
711    if ( ! zeroBSCRM_migration_tableHasColumn( $ZBSCRM_t['logs'], 'zbsl_pinned' ) ) {
712        $sql = 'ALTER TABLE ' . $ZBSCRM_t['logs'] . ' ADD `zbsl_pinned` int(1) NULL AFTER `zbsl_longdesc`;';
713        $wpdb->query( $sql );
714    }
715
716    // get outdated log meta count
717    $outdated_log_meta_count = (int) $wpdb->get_var( 'SELECT COUNT(ID) FROM ' . $ZBSCRM_t['meta'] . ' WHERE zbsm_objtype = 8 AND zbsm_key = "logmeta"' );
718
719    if ( $outdated_log_meta_count > 0 ) {
720
721        // get outdated meta records
722        $outdated_log_meta_records = $wpdb->get_results( 'SELECT * FROM ' . $ZBSCRM_t['meta'] . ' WHERE zbsm_objtype = 8 AND zbsm_key = "logmeta" ORDER BY ID DESC LIMIT 5000' );
723
724        foreach ( $outdated_log_meta_records as $log_record ) {
725
726            // hydrate - Note that `[]` doesn't hydrate into array with this
727            $log_meta = $zbs->DAL->decodeIfJSON( $zbs->DAL->stripSlashes( $log_record->zbsm_val ) );
728
729            // insert new line foreach meta
730            if ( is_array( $log_meta ) ) {
731
732                foreach ( $log_meta as $key => $value ) {
733
734                    $zbs->DAL->updateMeta( ZBS_TYPE_LOG, $log_record->zbsm_objid, $zbs->DAL->makeSlug( $key ), $value );
735
736                }
737            }
738
739            // delete old 'dehydrated whole' line
740            zeroBSCRM_db2_deleteGeneric( $log_record->ID, 'meta' );
741
742        }
743
744        // any left?
745        $outdated_log_meta_count = (int) $wpdb->get_var( 'SELECT COUNT(ID) FROM ' . $ZBSCRM_t['meta'] . ' WHERE zbsm_objtype = 8 AND zbsm_key = "logmeta"' );
746
747    }
748
749    if ( $outdated_log_meta_count == 0 ) {
750
751        // mark complete
752        zeroBSCRM_migrations_markComplete( '54', array( 'updated' => 1 ) );
753
754    }
755}
756
757    /*
758    * Migration 5.4.3
759    * - Removes unwanted .htaccess files
760    */
761function zeroBSCRM_migration_543() {
762    // recursively deletes all .htaccess files starting from the root storage folder
763    $root_storage = zeroBSCRM_privatisedDirCheck();
764    if ( $root_storage !== false ) {
765        $recursive_file_iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $root_storage['path'] ) );
766        $htaccess_files          = array();
767
768        foreach ( $recursive_file_iterator as $file ) {
769            if ( $file->isDir() || $file->getBasename() != '.htaccess' ) {
770                continue;
771            }
772            $htaccess_files[] = $file->getPathname();
773        }
774
775        foreach ( $htaccess_files as $errant_htaccess ) {
776            if ( is_file( $errant_htaccess ) ) {
777                    unlink( $errant_htaccess );
778            }
779        }
780    }
781
782    // mark this migration as complete
783    zeroBSCRM_migrations_markComplete( '543', array( 'updated' => 1 ) );
784}
785
786    /*
787    * Migration 5.4.4
788    * - Forces re-install of default fonts (moved to new JPCRM storage folder)
789    */
790function zeroBSCRM_migration_544() {
791
792    global $zbs;
793
794    // font reinstall
795    $shouldBeInstalled = zeroBSCRM_getSetting( 'feat_pdfinv' );
796    if ( $shouldBeInstalled == '1' ) {
797
798        // force reinstall of fonts
799        $fonts = $zbs->get_fonts();
800        if ( ! $fonts->extract_and_install_default_fonts() ) {
801            return false;
802        }
803    }
804
805    // mark complete
806    zeroBSCRM_migrations_markComplete( '544', array( 'updated' => 1 ) );
807}
808
809    /*
810    * Migration 5.5
811    * - Deletes orphaned rows linked to invoices in the objlinks table
812    */
813function zeroBSCRM_migration_55() {
814
815    global $zbs, $wpdb, $ZBSCRM_t;
816
817    // Deletes links when missing invoices are the 'to' object
818    $wpdb->query(
819        ' DELETE FROM ' . $ZBSCRM_t['objlinks']
820        . ' WHERE '
821        . ' zbsol_objtype_to = ' . ZBS_TYPE_INVOICE
822        . ' AND zbsol_objid_to NOT IN ( SELECT ID from ' . $ZBSCRM_t['invoices'] . ' ) '
823    );
824
825    // Deletes links when missing invoices are the 'from' object
826    $wpdb->query(
827        ' DELETE FROM ' . $ZBSCRM_t['objlinks']
828        . ' WHERE '
829        . ' zbsol_objtype_from = ' . ZBS_TYPE_INVOICE
830        . ' AND zbsol_objid_from NOT IN ( SELECT ID from ' . $ZBSCRM_t['invoices'] . ' ) '
831    );
832
833    // Deletes orphaned line items
834    $wpdb->query(
835        ' DELETE FROM ' . $ZBSCRM_t['lineitems'] . ' WHERE ID NOT IN'
836        . ' ('
837        . '   SELECT zbsol_objid_from FROM ' . $ZBSCRM_t['objlinks']
838        . '   WHERE '
839        . '    zbsol_objtype_from = ' . ZBS_TYPE_LINEITEM
840        . ' )'
841    );
842
843    // mark complete
844    zeroBSCRM_migrations_markComplete( '55', array( 'updated' => 1 ) );
845}
846
847    /*
848    * Migration 5.5a
849    * Recompiles segments, runs on later schedule (wp_loaded)
850    */
851function zeroBSCRM_migration_55a() {
852
853    global $zbs;
854
855    // recompile segments with new condition names
856    $zbs->DAL->segments->compile_all_segments();
857
858    // mark complete
859    zeroBSCRM_migrations_markComplete( '55a', array( 'updated' => 1 ) );
860}
861
862    /*
863    * Migration 5.5.1
864    * - Deletes orphaned aka rows linked to contacts since deleted
865    */
866function zeroBSCRM_migration_551() {
867
868    global $zbs, $wpdb, $ZBSCRM_t;
869
870    // Deletes orphaned aka rows
871    $wpdb->query(
872        'DELETE FROM ' . $ZBSCRM_t['aka']
873            . ' WHERE aka_type = ' . ZBS_TYPE_CONTACT . ' AND aka_id NOT IN'
874            . ' (SELECT id FROM ' . $ZBSCRM_t['contacts'] . ')'
875    );
876
877    // mark complete
878    zeroBSCRM_migrations_markComplete( '551', array( 'updated' => 1 ) );
879}
880
881/**
882 * Migration 5.6.0
883 * Moves files from the old file structure (zbscrm-store) to the new
884 * one (jpcrm-storage) for rows from the meta table that have their object type
885 * equals to ZBS_TYPE_CONTACT and a file in the zbsm_val column.
886 *
887 * @param object $meta_row Row from the meta table that needs to be updated.
888 *
889 * @return void
890 */
891function zeroBSCRM_migration_560_move_custom_file_upload_box( $meta_row ) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
892    global $wpdb, $ZBSCRM_t; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
893    jpcrm_migration_load_wp_filesystem_direct();
894
895    // Skip if this is not a custom file for a contact
896    // (the only type that should exist, but we are being extra careful here).
897    if ( $meta_row->zbsm_objtype !== ZBS_TYPE_CONTACT ) {
898        return;
899    }
900    $file_path = $meta_row->zbsm_val;
901    // Skip if this file doesn't exist (user may have deleted using the filesystem).
902    if ( ! file_exists( $file_path ) ) {
903        error_log( sprintf( 'JPCRM migration error while searching for upload box file %s', $file_path ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
904        return;
905    }
906    $new_dir = jpcrm_storage_dir_info_for_contact( $meta_row->zbsm_objid );
907    // Skip if there is no information for the files subfolder.
908    if ( $new_dir === false || ! isset( $new_dir['files'] ) ) {
909        error_log( sprintf( 'JPCRM migration error missing subfolder files for contact ID %s', $meta_row->zbsm_objid ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
910        return;
911    }
912    $new_dir_info         = $new_dir['files'];
913    $upload_folder_exists = jpcrm_create_and_secure_dir_from_external_access( $new_dir_info['path'], false );
914    if ( $upload_folder_exists === false ) {
915        // We shouldn't have any errors here, but if we do we log it and skip this one.
916        error_log( sprintf( 'JPCRM migration error while creating upload box folder %s ', $new_dir_info['path'] ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
917        return;
918    }
919    $file_name            = basename( $file_path );
920    $new_file_path        = $new_dir_info['path'] . '/' . $file_name;
921    $wp_filesystem_direct = new WP_Filesystem_Direct( false );
922    // Moving the file.
923    if ( ! $wp_filesystem_direct->move( $file_path, $new_file_path, true ) ) {
924        // We shouldn't have any errors here, but if we do we log it and skip this one.
925        error_log( sprintf( 'JPCRM migration error while moving upload box %s to %s', $file_path, $new_file_path ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
926        return;
927    }
928
929    // Updates the database.
930    $update_result = $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
931        $ZBSCRM_t['meta'], // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
932        array(
933            'zbsm_val'         => $new_file_path,
934            'zbsm_lastupdated' => time(),
935        ),
936        array( 'ID' => $meta_row->ID ),
937        array( // Field data types.
938            '%s',
939            '%d',
940        ),
941        array( // Where data types.
942            '%d',
943        )
944    );
945
946    if ( $update_result === false ) {
947        error_log( sprintf( 'JPCRM migration error while updating upload box meta %s to %s', $meta_row->ID ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
948    }
949}
950
951/**
952 * Migration 5.6.0
953 * Moves files from the old file structure (zbscrm-store) to the new
954 * one (jpcrm-storage) for rows from the meta table that have the key 'files'.
955 *
956 * @param object $meta_row Row from the meta table that needs to be updated.
957 *
958 * @return void
959 */
960function zeroBSCRM_migration_560_move_file_array( $meta_row ) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
961    global $wpdb, $ZBSCRM_t; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
962    jpcrm_migration_load_wp_filesystem_direct();
963
964    // Before we move the files from the array we must discover its type and
965    // update its dir_info information (contains information for several
966    // subfolders, we will use the 'files' subfolder).
967    $new_dir = false;
968    switch ( $meta_row->zbsm_objtype ) {
969        case ZBS_TYPE_CONTACT:
970            $new_dir = jpcrm_storage_dir_info_for_contact( $meta_row->zbsm_objid );
971            break;
972
973        case ZBS_TYPE_COMPANY:
974            $new_dir = jpcrm_storage_dir_info_for_company( $meta_row->zbsm_objid );
975            break;
976
977        case ZBS_TYPE_QUOTE:
978            $new_dir = jpcrm_storage_dir_info_for_quotes( $meta_row->zbsm_objid );
979            break;
980
981        case ZBS_TYPE_INVOICE:
982            $new_dir = jpcrm_storage_dir_info_for_invoices( $meta_row->zbsm_objid );
983            break;
984    }
985
986    // Skip if any other type (we are only moving these four types).
987    if ( $new_dir === false || ! isset( $new_dir['files'] ) ) {
988        return;
989    }
990
991    $new_dir_info        = $new_dir['files'];
992    $outdated_file_array = json_decode( $meta_row->zbsm_val, true );
993    // If we can't decode it neither the CRM can when it shows files, so
994    // we can skip it.
995    if ( $outdated_file_array === null ) {
996        return;
997    }
998
999    // This was the hard-coded value in JPCRM < 5.4.x.
1000    $previous_folder = 'zbscrm-store';
1001    $new_file_array  = array();
1002    foreach ( $outdated_file_array as $outdated_file_meta ) {
1003        // Skip if this has an unknown format.
1004        // Skip if this isn't an outdate file.
1005        // Skip if this file doesn`t exist (user may have deleted using the filesystem).
1006        if (
1007            ! isset( $outdated_file_meta['file'] )
1008            || ! str_contains( $outdated_file_meta['file'], "/$previous_folder/" )
1009            || ! file_exists( $outdated_file_meta['file'] )
1010        ) {
1011            $new_file_array[] = $outdated_file_meta;
1012            continue;
1013        }
1014
1015        $upload_folder_exists = jpcrm_create_and_secure_dir_from_external_access( $new_dir_info['path'], false );
1016        if ( $upload_folder_exists === false ) {
1017            // We shouldn't have any errors here, but if we do we log it and skip this one.
1018            error_log( sprintf( 'JPCRM migration error while creating folder %s ', $new_dir_info['path'] ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1019            $new_file_array[] = $outdated_file_meta;
1020            return;
1021        }
1022
1023        $file_name            = basename( $outdated_file_meta['file'] );
1024        $new_file_path        = $new_dir_info['path'] . '/' . $file_name;
1025        $wp_filesystem_direct = new WP_Filesystem_Direct( false );
1026        // Moving the file.
1027        if ( ! $wp_filesystem_direct->move( $outdated_file_meta['file'], $new_file_path, true ) ) {
1028            // We shouldn't have any errors here, but if we do we log it and skip this one.
1029            error_log( sprintf( 'JPCRM migration error while moving %s to %s', $outdated_file_meta['file'], $new_file_path ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1030            $new_file_array[] = $outdated_file_meta;
1031            continue;
1032        }
1033
1034        // Updating references to save in the database.
1035        $outdated_file_meta['file'] = $new_file_path;
1036        $outdated_file_meta['url']  = $new_dir_info['url'] . '/' . $file_name;
1037        $new_file_array[]           = $outdated_file_meta;
1038    }
1039    // Updates the database.
1040    $update_result = $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
1041        $ZBSCRM_t['meta'], // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1042        array(
1043            'zbsm_val'         => wp_json_encode( $new_file_array, JSON_UNESCAPED_SLASHES ),
1044            'zbsm_lastupdated' => time(),
1045        ),
1046        array( 'ID' => $meta_row->ID ),
1047        array( // Field data types.
1048            '%s',
1049            '%d',
1050        ),
1051        array( // Where data types.
1052            '%d',
1053        )
1054    );
1055
1056    if ( $update_result === false ) {
1057        error_log( sprintf( 'JPCRM migration error while updating file array meta %s to %s', $meta_row->ID ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1058    }
1059}
1060
1061/**
1062 * Migration 5.6.0
1063 * Moves the old folder structure (zbscrm-store) to the new one (jpcrm-storage).
1064 *
1065 * @return void
1066 */
1067function zeroBSCRM_migration_560() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
1068    global $wpdb, $ZBSCRM_t; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1069
1070    // This was the hard-coded value in JPCRM < 5.4.x.
1071    $previous_folder = 'zbscrm-store';
1072    // We only store files in the meta table.
1073    $query         = sprintf( "SELECT * FROM `%s` WHERE `zbsm_val` LIKE '%s'", $ZBSCRM_t['meta'], '%%%s%%' ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1074    $outdated_rows = $wpdb->get_results( $wpdb->prepare( $query, $previous_folder ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
1075
1076    if ( is_array( $outdated_rows ) ) {
1077        foreach ( $outdated_rows as $outdated_row ) {
1078            // The first type of row we have to migrate has they key 'files' and
1079            // has an array of files attached to an object of type `zbsm_objtype`.
1080            if ( $outdated_row->zbsm_key === 'files' ) {
1081                zeroBSCRM_migration_560_move_file_array( $outdated_row );
1082            } else {
1083                zeroBSCRM_migration_560_move_custom_file_upload_box( $outdated_row );
1084            }
1085        }
1086    }
1087
1088    // Mark as complete.
1089    zeroBSCRM_migrations_markComplete( '560', array( 'updated' => 1 ) );
1090}
1091
1092/**
1093 * Migration create_workflows_table
1094 *
1095 * This migration will:
1096 * - Make sure all tables are up-to-date. Practically speaking, then we're creating a new "workflows" table.
1097 *
1098 * @return void
1099 */
1100function zeroBSCRM_migration_create_workflows_table() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
1101    // Check tables if exist and create if not.
1102    zeroBSCRM_checkTablesExist();
1103
1104    // Mark migration as complete.
1105    zeroBSCRM_migrations_markComplete( 'create_workflows_table', array( 'updated' => 1 ) );
1106}
1107
1108/**
1109 * Removes errant task timezone offsets from database
1110 */
1111function zeroBSCRM_migration_task_offset_fix() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
1112    global $wpdb, $ZBSCRM_t; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1113
1114    $timezone_offset_in_secs = jpcrm_get_wp_timezone_offset_in_seconds();
1115
1116    if ( ! empty( $timezone_offset_in_secs ) ) {
1117        // remove offset from stored task dates
1118        $sql = sprintf( 'UPDATE %s SET zbse_start = zbse_start - %d;', $ZBSCRM_t['events'], $timezone_offset_in_secs ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1119        $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
1120        $sql = sprintf( 'UPDATE %s SET zbse_end = zbse_end - %d;', $ZBSCRM_t['events'], $timezone_offset_in_secs ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1121        $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
1122    }
1123
1124    zeroBSCRM_migrations_markComplete( 'task_offset_fix', array( 'updated' => 1 ) );
1125}
1126
1127/**
1128 * Refresh user roles after tightening restrictions
1129 */
1130function zeroBSCRM_migration_refresh_user_roles() {
1131    // remove roles
1132    zeroBSCRM_clearUserRoles();
1133
1134    // add roles anew
1135    zeroBSCRM_addUserRoles();
1136
1137    zeroBSCRM_migrations_markComplete( 'refresh_user_roles', array( 'updated' => 1 ) );
1138}
1139
1140/**
1141 * Regenerate tag slugs
1142 */
1143function zeroBSCRM_migration_regenerate_tag_slugs() {
1144    $obj_ids = array( ZBS_TYPE_CONTACT, ZBS_TYPE_COMPANY, ZBS_TYPE_QUOTE, ZBS_TYPE_INVOICE, ZBS_TYPE_TRANSACTION, ZBS_TYPE_TASK, ZBS_TYPE_FORM );
1145    foreach ( $obj_ids as $obj_id ) {
1146        jpcrm_migration_regenerate_tag_slugs_for_obj_type( $obj_id );
1147    }
1148    zeroBSCRM_migrations_markComplete( 'regenerate_tag_slugs', array( 'updated' => 1 ) );
1149}
1150
1151/**
1152 * Convert invoice statuses and mappings to English
1153 */
1154function zeroBSCRM_migration_invoice_language_fixes() {
1155
1156    // if already English, can't auto-migrate and probably no need
1157    $cur_locale = get_locale();
1158    if ( $cur_locale === 'en_US' ) {
1159        zeroBSCRM_migrations_markComplete( 'invoice_language_fixes', array( 'updated' => 1 ) );
1160        return;
1161    }
1162
1163    global $zbs, $wpdb, $ZBSCRM_t; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1164
1165    $invoice_statuses = array(
1166        'Draft'   => __( 'Draft', 'zero-bs-crm' ),
1167        'Unpaid'  => __( 'Unpaid', 'zero-bs-crm' ),
1168        'Paid'    => __( 'Paid', 'zero-bs-crm' ),
1169        'Overdue' => __( 'Overdue', 'zero-bs-crm' ),
1170        'Deleted' => __( 'Deleted', 'zero-bs-crm' ),
1171    );
1172
1173    // get WooSync settings
1174    $woosync_settings = $zbs->settings->get( 'zbscrm_dmz_ext_woosync' );
1175
1176    $settings_to_update = array();
1177
1178    foreach ( $invoice_statuses as $invoice_status => $translated_status ) {
1179
1180        // if the "translation" is the same as English, continue
1181        if ( $translated_status === $invoice_status ) {
1182            continue;
1183        }
1184
1185        // if there are settings, we may need to update mappings too
1186        if ( $woosync_settings ) {
1187
1188            foreach ( $woosync_settings as $setting => $value ) {
1189                // if not a setting we care about, continue
1190                if ( ! str_starts_with( $setting, 'order_invoice_map_' ) ) {
1191                    continue;
1192                }
1193
1194                // if no translated status matches, continue
1195                if ( $value !== $translated_status ) {
1196                    continue;
1197                }
1198
1199                // flag setting for update
1200                $settings_to_update[ $setting ] = $invoice_status;
1201            }
1202        }
1203
1204        // see if there are any matches on translated status
1205        $query = $wpdb->prepare( 'SELECT COUNT(ID) FROM ' . $ZBSCRM_t['invoices'] . ' WHERE zbsi_status=%s', $translated_status ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1206        $count = $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
1207
1208        // if no matches, nothing to update
1209        if ( $count === 0 ) {
1210            continue;
1211        }
1212
1213        // update status to English
1214        $query = $wpdb->prepare( 'UPDATE ' . $ZBSCRM_t['invoices'] . ' SET zbsi_status=%s WHERE zbsi_status=%s', $invoice_status, $translated_status ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1215        $wpdb->query( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
1216    }
1217
1218    // if there are mapping settings to update, do it
1219    if ( $woosync_settings && ! empty( $settings_to_update ) ) {
1220
1221        // make a backup of settings
1222        $zbs->settings->update( 'zbscrm_dmz_ext_woosync.bak', $woosync_settings );
1223
1224        // update WooSync settings
1225        $updated_woosync_settings = array_merge( $woosync_settings, $settings_to_update );
1226        $zbs->settings->update( 'zbscrm_dmz_ext_woosync', $updated_woosync_settings );
1227    }
1228
1229    zeroBSCRM_migrations_markComplete( 'invoice_language_fixes', array( 'updated' => 1 ) );
1230}
1231
1232/**
1233 * From Gh Issue 3465, this migration increases the city field size to 200 chars
1234 */
1235function zeroBSCRM_migration_gh3465_increase_city_field_size() {
1236
1237    // SQLite doesn't support column modification.
1238    $db_engine = jpcrm_database_engine();
1239    if ( $db_engine === 'sqlite' ) {
1240        return;
1241    }
1242
1243    global $wpdb, $ZBSCRM_t; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1244
1245    $sql = 'ALTER TABLE ' . $ZBSCRM_t['contacts'] . ' MODIFY COLUMN `zbsc_city` VARCHAR(200);'; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1246    $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
1247
1248    $sql = 'ALTER TABLE ' . $ZBSCRM_t['contacts'] . ' MODIFY COLUMN `zbsc_seccity` VARCHAR(200);'; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1249    $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
1250
1251    $sql = 'ALTER TABLE ' . $ZBSCRM_t['companies'] . ' MODIFY COLUMN `zbsco_city` VARCHAR(200);'; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1252    $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
1253
1254    $sql = 'ALTER TABLE ' . $ZBSCRM_t['companies'] . ' MODIFY COLUMN `zbsco_seccity` VARCHAR(200);'; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1255    $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
1256
1257    zeroBSCRM_migrations_markComplete( 'gh3465_increase_city_field_size', array( 'updated' => 1 ) );
1258}
1259
1260/**
1261 * Increase tax rate precision from 2 to 10 decimal places for WooCommerce sync compatibility
1262 */
1263function zeroBSCRM_migration_tax_rate_precision_fix() {
1264
1265    global $wpdb, $ZBSCRM_t; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1266
1267    $db_engine = jpcrm_database_engine();
1268
1269    if ( $db_engine === 'sqlite' ) {
1270        // SQLite has dynamic typing - column types are flexible and can store high precision decimals
1271        // No schema modification needed, but we can mark the migration as complete
1272        zeroBSCRM_migrations_markComplete(
1273            'tax_rate_precision_fix',
1274            array(
1275                'updated' => 0,
1276                'note'    => 'SQLite dynamic typing - no schema change needed',
1277            )
1278        );
1279        return;
1280    }
1281
1282    // For MySQL/MariaDB - check if the column exists and get its current data type.
1283    $column_type = zeraBSCRM_migration_get_column_data_type( $ZBSCRM_t['tax'], 'zbsc_rate' ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
1284    if ( $column_type && strpos( $column_type, 'decimal(20,10)' ) === false ) {
1285        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1286        $wpdb->query( 'ALTER TABLE `' . esc_sql( $ZBSCRM_t['tax'] ) . '` MODIFY zbsc_rate DECIMAL(20,10) NOT NULL DEFAULT 0.0000000000' );
1287    }
1288
1289    zeroBSCRM_migrations_markComplete( 'tax_rate_precision_fix', array( 'updated' => 1 ) );
1290}
1291
1292/**
1293 * Ensure the notifications table exists
1294 *
1295 * This migration ensures the notifications table is created using the dedicated
1296 * table creation function in ZeroBSCRM.Database.php
1297 */
1298function zeroBSCRM_migration_ensure_notifications_table() {
1299    global $wpdb;
1300
1301    // Include the Database file if needed
1302    if ( ! function_exists( 'jpcrm_create_notifications_table' ) ) {
1303        require_once ZEROBSCRM_INCLUDE_PATH . 'ZeroBSCRM.Database.php';
1304    }
1305
1306    // Check if table exists first
1307    $table_name = $wpdb->prefix . 'zbs_notifications';
1308
1309    // phpcs:disable
1310    $table_exists = $wpdb->get_var( "SHOW TABLES LIKE '$table_name'" ) === $table_name;
1311    // phpcs:enable 
1312
1313    if ( ! $table_exists ) {
1314        // Table doesn't exist, create just the notifications table
1315        $result = jpcrm_create_notifications_table();
1316
1317        if ( $result ) {
1318            zeroBSCRM_migrations_markComplete(
1319                'ensure_notifications_table',
1320                array(
1321                    'updated' => 1,
1322                    'note'    => 'Successfully created notifications table',
1323                )
1324            );
1325            return true;
1326        } else {
1327            // Log error if table creation failed
1328            zeroBSCRM_migrations_markComplete(
1329                'ensure_notifications_table',
1330                array(
1331                    'updated' => 0,
1332                    'note'    => 'Failed to create notifications table',
1333                    'error'   => true,
1334                )
1335            );
1336            return false;
1337        }
1338    }
1339
1340    // Table already exists
1341    zeroBSCRM_migrations_markComplete(
1342        'ensure_notifications_table',
1343        array(
1344            'updated' => 0,
1345            'note'    => 'Notifications table already exists',
1346        )
1347    );
1348    return true;
1349}
1350
1351/*
1352======================================================
1353    / MIGRATIONS
1354    ====================================================== */
1355
1356/*
1357======================================================
1358    MIGRATION Helpers
1359    ====================================================== */
1360
1361    // simplistic arr manager
1362function zeroBSCRM_migration_addErrToStack( $err = array(), $errKey = '' ) {
1363
1364    if ( $errKey !== '' ) {
1365
1366            $existing = get_option( $errKey, array() );
1367
1368            // catch err in err stack.
1369        if ( ! is_array( $existing ) ) {
1370            $existing = array();
1371        }
1372
1373            // add + update
1374            $existing[] = $err;
1375            update_option( $errKey, $existing, false );
1376
1377            return true;
1378
1379    }
1380
1381    return false;
1382}
1383
1384    // checks if a column already exists
1385    // note $tableName is used unchecked
1386function zeroBSCRM_migration_tableHasColumn( $table_name, $column_name ) {
1387
1388    global $wpdb;
1389
1390    if ( ! empty( $table_name ) && ! empty( $column_name ) ) {
1391
1392            $query = $wpdb->prepare( 'SHOW COLUMNS FROM ' . $table_name . ' LIKE %s', $column_name );
1393
1394            $row = $wpdb->get_results( $query );
1395
1396        if ( is_array( $row ) && count( $row ) > 0 ) {
1397
1398            return true;
1399
1400        }
1401    }
1402
1403    return false;
1404}
1405
1406    /*
1407    * Verifies if a mysql table has an index named X
1408    */
1409function jpcrm_migration_table_has_index( $table_name, $index_name ) {
1410
1411    global $wpdb;
1412
1413    $query = $wpdb->prepare( 'SHOW INDEX FROM ' . $table_name . ' WHERE Key_name = %s', $index_name );
1414    $row   = $wpdb->get_results( $query );
1415
1416    if ( is_array( $row ) && count( $row ) > 0 ) {
1417
1418            return true;
1419
1420    }
1421
1422    return false;
1423}
1424
1425    /**
1426     * Retrieves the data typo of the given colemn name in the given table name.
1427     * It's worth noting that it will have the size of the field too, so `int(10)`
1428     * rather than just `int`.
1429     *
1430     * @param $table_name string The table name to query.
1431     * @param $column_name string The column name to query.
1432     *
1433     * @return string|false The column type as a string, or `false` on failure.
1434     */
1435function zeraBSCRM_migration_get_column_data_type( $table_name, $column_name ) {
1436    global $wpdb;
1437
1438    $column = $wpdb->get_row(
1439        $wpdb->prepare(
1440            "SHOW COLUMNS FROM $table_name LIKE %s",
1441            $column_name
1442        )
1443    );
1444    return empty( $column ) ? false : $column->Type;
1445}
1446
1447/**
1448 * Loads everything needed to use the WP_Filesystem_Direct class.
1449 *
1450 * @return void
1451 */
1452function jpcrm_migration_load_wp_filesystem_direct() {
1453    require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
1454    require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php';
1455}
1456
1457/**
1458 * Regenerates tag slugs for a given object type.
1459 *
1460 * @param int $obj_type_id Object type ID.
1461 */
1462function jpcrm_migration_regenerate_tag_slugs_for_obj_type( int $obj_type_id ) {
1463    global $zbs;
1464
1465    // get tags for object type
1466    $tags = $zbs->DAL->getTagsForObjType( // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
1467        array(
1468            'objtypeid'    => $obj_type_id,
1469            'excludeEmpty' => false,
1470        )
1471    );
1472
1473    // store slugs we've used so we prevent duplicates
1474    $used_slugs = array();
1475
1476    foreach ( $tags as $tag ) {
1477
1478        // generate a potential slug
1479        $potential_slug = sanitize_key( $tag['name'] );
1480
1481        // this will be empty if Chinese or Cyrillic or symbols, so use `tag` fallback
1482        if ( empty( $potential_slug ) ) {
1483            if ( preg_match( '/^tag-\d+$/', $tag['slug'] ) && ! in_array( $tag['slug'], $used_slugs, true ) ) {
1484                // if we had a fallback slug before and it hasn't been claimed by another tag, use it
1485                $potential_slug = $tag['slug'];
1486            } else {
1487                // get a new fallback slug
1488                $potential_slug = $zbs->DAL->get_new_tag_slug( $obj_type_id, 'tag', true ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
1489            }
1490        } elseif ( in_array( $potential_slug, $used_slugs, true ) ) {
1491            // this needs an iteration
1492            if ( preg_match( '/^' . $potential_slug . '-\d+$/', $tag['slug'] ) && ! in_array( $tag['slug'], $used_slugs, true ) ) {
1493                // use old slug iteration
1494                $potential_slug = $tag['slug'];
1495            } else {
1496                // generate a new slug iteration
1497                $potential_slug = $zbs->DAL->get_new_tag_slug( $obj_type_id, $potential_slug ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
1498            }
1499        }
1500
1501        // if the new slug is different than the old one, update the database
1502        if ( $potential_slug !== $tag['slug'] ) {
1503            $zbs->DAL->addUpdateTag( // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
1504                array(
1505                    'id'   => $tag['id'],
1506                    'data' => array(
1507                        'objtype' => $obj_type_id,
1508                        'name'    => $tag['name'],
1509                        'slug'    => $potential_slug,
1510                    ),
1511                )
1512            );
1513        }
1514
1515        // store in index of used slugs
1516        $used_slugs[] = $potential_slug;
1517    }
1518}
1519
1520/*
1521======================================================
1522    / MIGRATION Helpers
1523    ====================================================== */