settings = new Settings(); $this->requests = new Requests_Repository(); $this->events = new Events_Repository(); } /** * The settings every service of the plugin reads. * * @return Settings */ public function settings() { return $this->settings; } /** * The requests table. * * @return Requests_Repository */ public function requests() { return $this->requests; } /** * The event log. * * @return Events_Repository */ public function events() { return $this->events; } /** * The status registry. Its methods are static; the instance is the same * way in for code that holds only the accessor. * * @return Statuses */ public function statuses() { if ( null === $this->statuses ) { $this->statuses = new Statuses(); } return $this->statuses; } /** * The route registry. Its methods are static; the instance is the same way * in for code that holds only the accessor. * * @return Routes */ public function routes() { if ( null === $this->routes ) { $this->routes = new Routes(); } return $this->routes; } public function init() { // The shipped languages/ folder, for a locale WordPress has no file of // its own for. Before an add-on's fallback (10), so a file of Careback's // own comes first. add_filter( 'lang_dir_for_domain', array( __CLASS__, 'own_translations' ), 5, 3 ); add_action( 'init', array( $this, 'maybe_upgrade' ) ); add_action( 'init', array( $this, 'tidy_settings' ) ); // The emails are WooCommerce's (WooCommerce → Settings → Emails); the receipt // code of a request is shown again on its card. Emails::init(); Receipt::init(); // The moment an order gets the status the shop chose as "delivered" (N-76). Periods::init(); // Settings: when the periods start, deleting the plugin (N-76, N-104); Setup and the // notices: WooCommerce's own withdrawal form (N-81), "I have added the link" (N-02). Admin_Sections::init(); Woo_Withdrawal_Form::init(); Setup_Link::init(); // The form's rows on the settings screen: Legal wording, the returns // policy page and the consent with it (N-12, N-13). Form_Settings::init(); // The legal texts (N-114, N-116): who pays for sending the goods back, a // row on Settings read by every text on that cost; the model withdrawal // form, linked under the form, on a page of its own and as a shortcode. Return_Costs::init(); // The shop's own item fields shown under each item (X-04): their row // under Item variation details. Item_Fields::init(); Model_Form::init(); // The harmonised notice on the legal guarantee (N-115), as a shortcode. Legal_Guarantee::init(); $notifications = new Notifications( $this->settings, $this->events ); $submission = new Submission( $this->settings, $this->requests, $notifications ); $security = new Security( $this->settings ); $setup = new Setup( $this->settings, $this->requests ); $admin_requests = new Admin_Requests( $this->requests, $this->events, $this->settings ); ( new Rest( $this->settings, $submission, $this->requests, $this->events, $security ) )->init(); ( new Frontend( $this->settings, $security ) )->init(); ( new Retention( $this->settings, $this->events ) )->init(); $admin_requests->init(); ( new Admin( $this->settings, $admin_requests, $setup, $this->events ) )->init(); ( new Privacy( $this->requests, $this->settings ) )->init(); ( new Order_Link( $this->requests, $this->settings ) )->init(); // Optional tracking module. Registers its admin screen always so it can // be switched on; everything customer-facing stays behind track_enabled. ( new Track_Module( $this->settings, $this->requests, $this->events, $security ) )->init(); add_action( 'careback_status_changed', static function ( $id, $status, $previous, $request ) use ( $notifications ) { $notifications->status_update( $id, $status, $request ); }, 10, 4 ); } /** * Action fired once, after the free part has wired itself, with the API * version ( int $api, CAREBACK_API ). An add-on boots on it. See * api/core.md. */ const LOADED_ACTION = 'careback_loaded'; /** * Tells the add-ons the free part is ready: fires `careback_loaded` once, * from the main file right after init(). A second call does nothing. * * @return void */ public function loaded() { if ( did_action( self::LOADED_ACTION ) ) { return; } /** * Fires once the free part has wired itself; an add-on boots here. * * @param int $api Version of the API the free part offers (CAREBACK_API). */ do_action( self::LOADED_ACTION, CAREBACK_API ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- careback_loaded, the final prefix. } /** * The plugin's own languages folder (German in four variants, Ukrainian) * for the `careback` text domain, only while WordPress has no file of its * own for the locale (`lang_dir_for_domain`, WordPress 6.6+, asked whenever * WordPress loads the domain just in time). * * A language pack in WP_LANG_DIR always wins: WordPress names the folder it * found the locale's file in, and that folder is kept. Only when WordPress * found none and languages/ holds the locale is languages/ named. Any other * locale, and English, is left to WordPress. * * @param string|false $path Folder WordPress found for the domain and locale, false for none. * @param string $domain Text domain. * @param string $locale Locale. * @return string|false */ public static function own_translations( $path, $domain, $locale ) { if ( 'careback' !== $domain || ! is_string( $locale ) || 1 !== preg_match( '/^[a-z]{2,3}(_[A-Z]{2})?(_[a-z0-9]+)?$/', $locale ) ) { return $path; } if ( is_string( $path ) && '' !== $path && self::has_translation( $path, $locale ) ) { return $path; } $own = CAREBACK_PATH . 'languages/'; return self::has_translation( $own, $locale ) ? $own : $path; } /** * Whether a folder holds Careback's translation for a locale (.mo or .l10n.php). * * @param string $dir Folder. * @param string $locale Locale. * @return bool */ private static function has_translation( $dir, $locale ) { $dir = trailingslashit( $dir ); return is_readable( $dir . 'careback-' . $locale . '.mo' ) || is_readable( $dir . 'careback-' . $locale . '.l10n.php' ); } /** Name of the lock that keeps one upgrade from starting twice. */ const UPGRADE_LOCK = 'careback_upgrading'; /** * Filter: cron hooks queued once after a schema migration ( string[] $hooks ). * Each runs as a single event a minute after the migration finished. */ const UPGRADE_EVENTS = 'careback_upgrade_events'; /** * Hooks queued after a migration: none of this plugin's own, whatever an * add-on registers through `careback_upgrade_events`. * * @return string[] */ public static function upgrade_events() { $hooks = apply_filters( self::UPGRADE_EVENTS, array() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- careback_upgrade_events, the final prefix. return array_values( array_unique( array_filter( array_map( 'strval', (array) $hooks ) ) ) ); } /** * Creates or migrates the tables, moves the built-in reasons into the * merchant's own list, and queues the work an add-on asks for after a * migration (upgrade_events()), on a load where the stored schema version * differs from the shipped one, or where a part of the schema an add-on * declares has not been set up at its version (Database::parts_current()). * Returns without doing any of it while both match. * * Two things this had wrong until 0.66.0. * * It ran on any request at all. The moment a plugin is updated, the version * has moved and every visitor still on the site races into the migration: * several of them run dbDelta and the index replay at once, against the same * tables, and each of them pays for it in page time. It now runs on an admin * load, or on cron, where a slow request is nobody's checkout — and behind a * lock, so the second one that arrives leaves it to the first. * * And it aggregated thirteen months of history inline, on whichever request * happened to be first through the door. That is queued for cron now: the * counters are a report, and a report can be a minute late. */ public function maybe_upgrade() { // A part of the schema an add-on declares and that was never set up — the // add-on arrived after the tables were made — is set up the same way. if ( Database::parts_current() && get_option( 'careback_db_version' ) === CAREBACK_DB_VERSION ) { return; } // A customer's request is not where a schema migration belongs. WP-CLI // counts as neither an admin screen nor cron, and is exactly where a // careful host runs an upgrade, so it is allowed through by name. $allowed = is_admin() || wp_doing_cron() || ( defined( 'WP_CLI' ) && WP_CLI ); if ( ! $allowed ) { return; } // add_option() is the lock: it writes only when the row is absent, in one // statement, so of two requests arriving together exactly one succeeds. // A transient would not do — with no persistent object cache it is an // option too, but one that get_transient() will happily read as expired // and delete underneath the request holding it. if ( ! add_option( self::UPGRADE_LOCK, time(), '', false ) ) { $since = (int) get_option( self::UPGRADE_LOCK ); // A lock left behind by a request that died mid-migration would // otherwise stop the site from ever upgrading. Ten minutes is far // longer than the work takes and far shorter than a release cycle. if ( $since && ( time() - $since ) < 10 * MINUTE_IN_SECONDS ) { return; } update_option( self::UPGRADE_LOCK, time(), false ); } try { // A site of a network set up here for the first time is a new install. $fresh = false === get_option( 'careback_db_version' ); Database::create_tables(); Reasons::install( $this->settings ); if ( $fresh ) { Activator::seed_new_site(); } update_option( 'careback_db_version', CAREBACK_DB_VERSION ); // Work an add-on wants done after a migration — thirteen months of // aggregation, say — is queued rather than run: it is not something to // make anyone wait for. Scheduled a minute out so the migration is // committed before it reads the rows. foreach ( self::upgrade_events() as $hook ) { wp_schedule_single_event( time() + MINUTE_IN_SECONDS, $hook ); } } finally { delete_option( self::UPGRADE_LOCK ); } } /** * Drops the keys of earlier releases from the stored settings, moving the * two that hold a shop's choice first (Settings::tidy()). Only on an admin, * cron or command-line load, as the upgrade: a visitor's page writes * nothing. * * @return void */ public function tidy_settings() { if ( is_admin() || wp_doing_cron() || ( defined( 'WP_CLI' ) && WP_CLI ) ) { $this->settings->tidy(); } } public static function woocommerce_active() { return class_exists( 'WooCommerce' ); } }