JezK
Edit File: Plugin.php
<?php namespace WPForms\Helpers; use Automatic_Upgrader_Skin; use WP_Error; /** * Source of truth for installing, activating, and upgrading plugins and WPForms addons. * * A plugin is identified by its main file relative to `WP_PLUGIN_DIR` * (e.g. `contact-form-7/wp-contact-form-7.php`). WPForms addons are plugins too: they * live in a `wpforms-{slug}` directory and are detected by that prefix, so their package * comes from the license API (gated by the site's license level) while every other plugin * downloads from WordPress.org. "Already installed" is a check for the file on disk; a * plugin that is already present is never re-downloaded, only activated or upgraded. * * @since 2.0.0 */ class Plugin { /** * Cache of plugin header data, keyed by plugin basename. * * @since 2.0.0 * * @var array */ private static $plugin_data_cache = []; /** * Install a plugin or WPForms addon by its main file, downloading it when missing. * * Runs the WP installer through `PluginSilentUpgrader` with a neutral upgrader skin so * the call works in AJAX, REST, CLI, and plain PHP contexts alike. Addons are detected * by the `wpforms-` directory prefix and resolved against the license API: one whose * tier is above the site's license is refused. A plugin already on disk is reported as * a `wpforms_install_plugin_exists` error (with the plugin file as error data) instead * of being re-downloaded, so the caller can decide whether to activate or upgrade it. * * @since 2.0.0 * * @param string $plugin_file Plugin main file relative to `WP_PLUGIN_DIR`: a WordPress.org * plugin (e.g. `contact-form-7/wp-contact-form-7.php`) or a * WPForms addon (e.g. `wpforms-stripe/wpforms-stripe.php`). * * @return array|WP_Error { * Install result on success, `WP_Error` on failure or when already present * (error code `wpforms_install_plugin_exists`). * * @type string $plugin Plugin main file. * } */ public function install( string $plugin_file ) { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks, Generic.Metrics.CyclomaticComplexity.TooHigh $generic_error = esc_html__( 'Could not install the plugin. Please download and install it manually.', 'wpforms-lite' ); if ( $plugin_file === '' ) { return new WP_Error( 'wpforms_install_plugin_empty', $generic_error ); } // Already on disk: report it so the caller can activate or upgrade it instead of re-downloading. if ( $this->is_installed( $plugin_file ) ) { return new WP_Error( 'wpforms_install_plugin_exists', esc_html__( 'The plugin is already installed.', 'wpforms-lite' ), $plugin_file ); } $is_addon = $this->is_addon( $plugin_file ); if ( ! wpforms_can_install( $is_addon ? 'addon' : 'plugin' ) ) { return new WP_Error( 'wpforms_install_plugin_forbidden', $generic_error ); } // Addons are gated by the site's license level. if ( $is_addon && ! $this->has_addon_access( $plugin_file ) ) { return new WP_Error( 'wpforms_install_plugin_access', esc_html__( 'Your license level does not include this addon. Please upgrade your plan to install it.', 'wpforms-lite' ) ); } $download_url = $this->get_download_url( $plugin_file ); if ( $download_url === '' ) { return new WP_Error( 'wpforms_install_plugin_url', $generic_error ); } $installed = $this->install_from_url( $plugin_file, $download_url ); if ( is_wp_error( $installed ) ) { return $installed; } return [ 'plugin' => $plugin_file ]; } /** * Install a plugin from an explicit download URL. * * For callers that already hold a resolved package URL (e.g. the Setup Wizard's * Lite-to-Pro upgrade, where the license API returns the URL). The catalog/license * checks `install()` runs are intentionally skipped: the caller-supplied URL * already encodes the access decision. The install-capability gate still applies, * so a `public` entry point can never install without the right permission. * * @since 2.0.0 * * @param string $plugin_file Plugin main file relative to `WP_PLUGIN_DIR`. * @param string $download_url Resolved package URL. * * @return true|WP_Error */ public function install_from_url( string $plugin_file, string $download_url ) { $generic_error = esc_html__( 'Could not install the plugin. Please download and install it manually.', 'wpforms-lite' ); $is_addon = $this->is_addon( $plugin_file ); if ( ! wpforms_can_install( $is_addon ? 'addon' : 'plugin' ) ) { return new WP_Error( 'wpforms_install_plugin_forbidden', $generic_error ); } if ( ! $this->prepare_filesystem() ) { return new WP_Error( 'wpforms_install_plugin_filesystem', $generic_error ); } $installer = new PluginSilentUpgrader( new Automatic_Upgrader_Skin() ); if ( ! method_exists( $installer, 'install' ) ) { return new WP_Error( 'wpforms_install_plugin_installer', $generic_error ); } $installed = $installer->install( $download_url ); if ( is_wp_error( $installed ) ) { return $installed; } wp_cache_flush(); if ( ! $this->is_installed( $plugin_file ) ) { return new WP_Error( 'wpforms_install_plugin_failed', $generic_error ); } return true; } /** * Activate an installed plugin by its main file. * * @since 2.0.0 * * @param string $plugin_file Plugin main file (e.g. `wp-mail-smtp/wp_mail_smtp.php`). * * @return true|WP_Error True on success, `WP_Error` on failure. */ public function activate( string $plugin_file ) { if ( ! current_user_can( 'activate_plugins' ) ) { return new WP_Error( 'wpforms_activate_plugin_forbidden', esc_html__( 'Plugin activation is disabled for you on this site.', 'wpforms-lite' ) ); } if ( ! function_exists( 'activate_plugin' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $activated = activate_plugin( $plugin_file ); if ( is_wp_error( $activated ) ) { return $activated; } /** This action is documented in includes/admin/ajax-actions.php. */ do_action( 'wpforms_plugin_activated', $plugin_file ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName return true; } /** * Upgrade an installed plugin or WPForms addon to its latest release. * * Overwrites the plugin on disk with the latest package by clearing the destination * first, but only when a genuinely newer version is available: when the installed build * is the same as or newer than the available one (e.g. a patched or beta build), the * call is a no-op that returns success without downgrading. Addons are detected by the * `wpforms-` directory prefix and pull the latest license build from the license API * (gated by the site's license level); every other plugin pulls the latest * WordPress.org build. * * @since 2.0.0 * * @param string $plugin_file Plugin main file relative to `WP_PLUGIN_DIR`: a WordPress.org * plugin (e.g. `contact-form-7/wp-contact-form-7.php`) or a * WPForms addon (e.g. `wpforms-stripe/wpforms-stripe.php`). * * @return array|WP_Error { * Upgrade result on success, `WP_Error` on failure. * * @type string $plugin Plugin main file. * } */ public function upgrade( string $plugin_file ) { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks $generic_error = esc_html__( 'Could not upgrade the plugin. Please update it manually.', 'wpforms-lite' ); if ( $plugin_file === '' || ! $this->is_installed( $plugin_file ) ) { return new WP_Error( 'wpforms_upgrade_plugin_missing', $generic_error ); } if ( ! current_user_can( 'update_plugins' ) || ! wp_is_file_mod_allowed( 'wpforms_can_install' ) ) { return new WP_Error( 'wpforms_upgrade_plugin_forbidden', $generic_error ); } // Addons are gated by the site's license level. if ( $this->is_addon( $plugin_file ) && ! $this->has_addon_access( $plugin_file ) ) { return new WP_Error( 'wpforms_upgrade_plugin_access', esc_html__( 'Your license level does not include this addon. Please upgrade your plan to update it.', 'wpforms-lite' ) ); } // Never downgrade: skip the overwrite unless a genuinely newer version is available. if ( ! $this->has_available_update( $plugin_file ) ) { return [ 'plugin' => $plugin_file ]; } $download_url = $this->get_download_url( $plugin_file ); if ( $download_url === '' ) { return new WP_Error( 'wpforms_upgrade_plugin_url', $generic_error ); } if ( ! $this->prepare_filesystem() ) { return new WP_Error( 'wpforms_upgrade_plugin_filesystem', $generic_error ); } $upgrader = new PluginSilentUpgrader( new Automatic_Upgrader_Skin() ); if ( ! method_exists( $upgrader, 'run' ) ) { return new WP_Error( 'wpforms_upgrade_plugin_upgrader', $generic_error ); } $result = $upgrader->run( [ 'package' => $download_url, 'destination' => WP_PLUGIN_DIR, 'clear_destination' => true, 'abort_if_destination_exists' => false, 'clear_working' => true, 'hook_extra' => [ 'type' => 'plugin', 'action' => 'update', 'plugin' => $plugin_file, ], ] ); if ( is_wp_error( $result ) ) { return $result; } if ( ! $result ) { return new WP_Error( 'wpforms_upgrade_plugin_failed', $generic_error ); } wp_cache_flush(); return [ 'plugin' => $plugin_file ]; } /** * Whether a plugin basename belongs to a genuine WPForms addon. * * Stricter than {@see self::is_addon()}: on top of the `wpforms-` directory * prefix it excludes the core plugin and reads the installed plugin header to * confirm the `WPForms` author, ruling out third-party forks that reuse the * prefix. It therefore needs the plugin present on disk, so it detects * already-installed addons — use `is_addon()` to route a not-yet-installed * download. Shared so the addon-detection logic lives in one place across both * editions instead of being duplicated per consumer. * * @since 2.0.0 * * @param string $plugin Plugin basename (e.g. `wpforms-stripe/wpforms-stripe.php`). * * @return bool */ public static function is_wpforms_addon( string $plugin ): bool { // The core plugin (Lite or Pro) is never an addon. if ( $plugin === 'wpforms/wpforms.php' || strpos( $plugin, 'wpforms-' ) !== 0 ) { return false; } // Forks reuse the `wpforms-` prefix, so confirm the author from the header. $author = strtolower( (string) ( self::get_plugin_data( $plugin )['AuthorName'] ?? '' ) ); return $author === 'wpforms'; } /** * Read and cache a plugin's header data by basename. * * @since 2.0.0 * * @param string $plugin Plugin basename (e.g. `wpforms-stripe/wpforms-stripe.php`). * * @return array Plugin header data, or an empty array when the file is absent. */ public static function get_plugin_data( string $plugin ): array { if ( isset( self::$plugin_data_cache[ $plugin ] ) ) { return self::$plugin_data_cache[ $plugin ]; } $plugin_file = trailingslashit( WP_PLUGIN_DIR ) . $plugin; if ( ! file_exists( $plugin_file ) ) { self::$plugin_data_cache[ $plugin ] = []; return []; } self::ensure_plugin_functions(); self::$plugin_data_cache[ $plugin ] = get_plugin_data( $plugin_file, false, false ); return self::$plugin_data_cache[ $plugin ]; } /** * Ensure the WordPress plugin admin functions are loaded. * * Available in admin requests, but not in REST/cron contexts where the wizard * and update routines also run. * * @since 2.0.0 */ public static function ensure_plugin_functions(): void { if ( ! function_exists( 'get_plugins' ) || ! function_exists( 'get_plugin_data' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } } /** * Prepare the WordPress filesystem and upgrader prerequisites. * * Loads the upgrader classes, resolves filesystem credentials without emitting * the credentials form, and prevents WordPress from fetching translations during * the run (which would break JS output). * * @since 2.0.0 * * @return bool Whether the filesystem is ready for an install or upgrade. */ private function prepare_filesystem(): bool { // phpcs:ignore WPForms.PHP.HooksMethod.InvalidPlaceForAddingHooks if ( ! function_exists( 'request_filesystem_credentials' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } // The core upgrader calls get_plugin_data() and wp_clean_plugins_cache() mid-run, // and neither is loaded on REST/cron requests. if ( ! function_exists( 'get_plugin_data' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } if ( ! class_exists( 'Plugin_Upgrader', false ) ) { require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; } // Suppress any output the credentials form might emit when run outside an admin screen. ob_start(); $credentials = request_filesystem_credentials( '', '', false, false ); ob_end_clean(); if ( $credentials === false || ! WP_Filesystem( $credentials ) ) { return false; } // Do not allow WordPress to search/download translations, as this will break JS output. remove_action( 'upgrader_process_complete', [ 'Language_Pack_Upgrader', 'async_upgrade' ], 20 ); return true; } /** * Whether a plugin main file belongs to a WPForms addon. * * Addons live in a `wpforms-{slug}` directory, so the directory prefix distinguishes * them from WordPress.org plugins. * * @since 2.0.0 * * @param string $plugin_file Plugin main file relative to `WP_PLUGIN_DIR`. * * @return bool */ private function is_addon( string $plugin_file ): bool { return strpos( dirname( $plugin_file ), 'wpforms-' ) === 0; } /** * Whether the site's license level grants access to an addon. * * Reads the `plugin_allow` flag the Addons handler computes for the addon (its * `has_access()` result, which maps `agency`/`ultimate` to `elite`). Returns false on * Lite and whenever the handler is unavailable, so addons fail closed. * * @since 2.0.0 * * @param string $plugin_file Addon main file relative to `WP_PLUGIN_DIR`. * * @return bool */ private function has_addon_access( string $plugin_file ): bool { $addons = wpforms()->obj( 'addons' ); if ( ! $addons ) { return false; } $addon = (array) $addons->get_addon( dirname( $plugin_file ) ); return ! empty( $addon['plugin_allow'] ); } /** * Whether a genuinely newer version of the plugin is available to install. * * Both WordPress core and the WPForms addon updater list a plugin in the * `update_plugins` transient's `response` only when the available version is newer than * the installed one, so a higher (e.g. patched or beta) build is absent from it. The * available version is re-compared against the build on disk as well, so a stale * transient entry can never trigger a downgrade. When the transient has never been * populated (a fresh site, or a REST/cron context) the update list is refreshed once, * so a genuine update is not mistaken for "already current". * * @since 2.0.0 * * @param string $plugin_file Plugin main file relative to `WP_PLUGIN_DIR`. * * @return bool */ private function has_available_update( string $plugin_file ): bool { $updates = get_site_transient( 'update_plugins' ); // An absent cache means "never checked", not "nothing to update": populate it once. // A populated cache that simply omits the plugin legitimately means it is current. if ( ! is_object( $updates ) || ! isset( $updates->last_checked ) ) { wp_update_plugins(); $updates = get_site_transient( 'update_plugins' ); } if ( ! is_object( $updates ) || ! isset( $updates->response[ $plugin_file ]->new_version ) ) { return false; } $installed = $this->get_installed_version( $plugin_file ); return $installed === '' || wpforms_version_compare( (string) $updates->response[ $plugin_file ]->new_version, $installed, '>' ); } /** * Read the installed version of a plugin from its header on disk. * * @since 2.0.0 * * @param string $plugin_file Plugin main file relative to `WP_PLUGIN_DIR`. * * @return string Version string, or an empty string when it cannot be determined. */ private function get_installed_version( string $plugin_file ): string { self::ensure_plugin_functions(); return get_plugins()[ $plugin_file ]['Version'] ?? ''; } /** * Whether a plugin's main file is already present under `WP_PLUGIN_DIR`. * * @since 2.0.0 * * @param string $plugin_file Plugin main file relative to `WP_PLUGIN_DIR`. * * @return bool */ private function is_installed( string $plugin_file ): bool { return file_exists( trailingslashit( WP_PLUGIN_DIR ) . $plugin_file ); } /** * Build the download URL for a plugin main file. * * For a WPForms addon the license-keyed package URL comes from the Addons handler * (empty on Lite or for an unknown/inaccessible addon). For every other plugin the * directory is the WordPress.org slug, so `contact-form-7/wp-contact-form-7.php` * resolves to `https://downloads.wordpress.org/plugin/contact-form-7.zip`. * * @since 2.0.0 * * @param string $plugin_file Plugin main file relative to `WP_PLUGIN_DIR`. * * @return string */ private function get_download_url( string $plugin_file ): string { $slug = dirname( $plugin_file ); if ( ! $this->is_addon( $plugin_file ) ) { return sprintf( 'https://downloads.wordpress.org/plugin/%s.zip', $slug ); } $addons = wpforms()->obj( 'addons' ); return $addons && method_exists( $addons, 'get_url' ) ? (string) $addons->get_url( $slug ) : ''; } }
BanaBridge News
https://validator.w3.org/feed/docs/rss2.html
Court Orders Ex-Weah Protocol Chief Jailed on US$8 Million Bail, Sends 15 Jurors to Prison Over Misconduct
Women Renew Call for War and Economic Crimes Court at Liberia’s Historic Peace Prayer Site
Finance Minister Ngafuan Urges Transparency, Accountability as Liberia Advances ARREST Agenda at UN Retreat
PPCC Donates ICT Equipment to 50 Government Institutions to Expand Electronic Procurement System
Liberia Commissions €100,000 Elemental Analyzer to Strengthen Environmental Research and Food Security
EPA Ends 16 Years of Renting with Purchase of Permanent Headquarters
West African Tax Bodies Deepen Partnership to Boost Revenue Mobilization, Launch Carbon Tax Study
Liberia Hosts Sierra Leone Procurement Delegation for E-GP Reform Study Tour
World Bank Praises Liberia’s Central Bank for Economic Stability and Private Sector Progress
Liberians See Slight Improvement in Corruption Fight, But Graft Still Viewed as Widespread, CENTAL Report Finds
Canada Afrique Care Foundation Set for Official Launch, Expanding Free Mental Health Access Across Canada and Africa
Weah Claims Liberia Has Become a “Narco State” as Opposition Rallies Behind Calls for Independent Probe into US$19 Million Cocaine Bust
EPA Boss Calls for Stronger Student Engagement, Unveils Push for Academic Partnership at UL Climate Symposium
Education Minister Urges Students to Embrace Tax Responsibility at National Student Tax Day
CDC Secretary General Urges U.S. Government to Review Support to Liberia National Police
Liberia Partners with CACFO to Expand Mental Health Services Ahead of Global Launch
Wreh-Toe Appointed Board Chair of CACFO as Foundation Strengthens Mental Health Push
GIABA Opens New Information Center Headquarters in Abidjan
Liberia Wins Fresh Backing as World Bank Group Executive Praises Progress, Pushes Faster Project Delivery
LRA Honors Liberia’s Top Taxpayers as Revenue Collections Hit Historic High
Root Cause in Question as Border Tensions Persist After Conakry Peace Deal
From Enforcement to Innovation: EPA Marks 2025 as a Defining Year for Liberia’s Environment
Weah Declares ‘Road to 2029 Begins Today’ as CDC Marks 22nd Anniversary in Zwedru
500 Documented Governance Failures: Activist Publishes Sweeping End-of-Year Indictment of Boakai Administration
CDC Calls for Probe into Border Crisis, Cites Alleged Mining Activities
Liberia Moves to Strengthen Small Businesses with $672,000 Packaging Solution Initiative
Former Public Works Minister Ruth Coker Collins Defends Zwedru Corridor Strategy, Calls for “Facts Over Rhetoric” in Infrastructure Debate
Power Shake-Up in CDC Diaspora: NEC Dissolves CDC-USA Interim Leadership, Appoints Rev. Muin to Lead U.S. Chapter
Tweah Reemerges, Sparks Fierce National Debate Over Governance and Liberia’s Future
Koijee Slams Tribal Politics and Political Manipulation in Lofa, Accuses Boakai Government
Mali and Burkina Faso Impose Reciprocal Travel Ban on U.S. Citizens
Guinea Election: A Turn Toward Continuity Amid Questions of Democratic Credibility
Liberia’s Revenue Grows Stronger as LRA Exceeds National Target for Second Year
Liberia’s Gender Legal Landscape: Laws in Place, Systems Lag Far Behind
Liberia’s Economy Gains Momentum as CBL Governor Highlights Progress at ECOWAS Meeting
Liberia Positions Itself for Eco Currency Entry as Economy Expands by 5.1%
From Overcrowded Classrooms to Renewal: NPA Mobilizes $1 Million to Transform UL
Ex-Public Works Minister Ruth Coker Collins Slams Ministry’s “Lack of Capacity,” Defends Her Infrastructure Record
CDC Expels Three Senior Members, Citing Alleged Alignment With Government and Undermining Party Ideology
UL to Celebrate 75 Years of Excellence
Saudi Fund Advances Liberia’s Push for Major Infrastructure Development
West African Financial Leaders Convene in Monrovia for ECOWAS Statutory Meetings
Unlocking Liberia’s Potential: Government Launches Major Revenue and Transparency Reforms
Incremental Gains, Enduring Impunity: Liberia’s CPI Improvement Fails the Test of Reform
Boakai’s Call for Unity Faces Scrutiny as Opposition, Citizens Question Actions Behind the Words
REMAPSEN Liberia Set to Strengthen Health and Environmental Reporting
Energy Sector Women Step Into the Spotlight at Liberia’s International Women’s Day Celebration
FeJAL Concludes Three-Day Retreat, Honors Outstanding Women Journalists
Dr. Rasha Kelej Named Among Africa’s 100 Most Influential Leaders
LAVA Honored for Strengthening Liberia–U.S. Ties at 2025 Military Ball