JezK
Edit File: cloud-auth-callback.trait.php
<?php /** * Cloud auth callback trait * * @package LiteSpeed * @since 7.8 */ namespace LiteSpeed; defined( 'WPINC' ) || exit(); /** * Trait Cloud_Auth_Callback * * Handles QUIC.cloud activation callbacks, status updates, and cryptographic operations. */ trait Cloud_Auth_Callback { /** * Successfully authorized REST requests retained for WordPress's repeated permission check. * * @var array<string,array<string,mixed>> */ private $_validated_callback_requests = []; /** * Encrypt data for cloud req * * @since 7.0 * * @param string|int $data Data to sign. * @return string|false */ private function _sign_b64( $data ) { $sk = $this->_local_sign_sk(); if ( false === $sk ) { self::debugErr( 'No usable local sign sk.' ); if ( ! empty( $this->_summary['pk_b64'] ) || ! empty( $this->_summary['sk_b64'] ) ) { $this->save_summary( [ 'pk_b64' => '', 'sk_b64' => '', ], true ); self::debug( 'Cleared the local sign pk/sk pair.' ); } return false; } $signature = sodium_crypto_sign_detached( (string) $data, $sk ); return base64_encode( $signature ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode } /** * Decode a base64 value of an exact byte length. * * Every key and signature here is fixed-length, so strict decode plus length is the whole validation. * * @since 7.9.1 * * @param mixed $value Base64 value. * @param int $expected_bytes Required decoded length. * @return string|false Binary value or false. */ private function _decode_b64( $value, $expected_bytes ) { if ( empty( $value ) || ! is_string( $value ) ) { return false; } $decoded = base64_decode( $value, true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode return false !== $decoded && strlen( $decoded ) === $expected_bytes ? $decoded : false; } /** * Load a matching local signing key pair. * * @since 7.9.1 * @return string|false Binary secret key or false. */ private function _local_sign_sk() { $secret_key = $this->_decode_b64( isset( $this->_summary['sk_b64'] ) ? $this->_summary['sk_b64'] : '', SODIUM_CRYPTO_SIGN_SECRETKEYBYTES ); $public_key = $this->_decode_b64( isset( $this->_summary['pk_b64'] ) ? $this->_summary['pk_b64'] : '', SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES ); if ( false === $secret_key || false === $public_key ) { return false; } try { return hash_equals( $public_key, sodium_crypto_sign_publickey_from_secretkey( $secret_key ) ) ? $secret_key : false; } catch ( \SodiumException $e ) { return false; } } /** * Binary public keys a server signature may be verified against. * * @since 7.9.1 * * @param bool $from_wpapi Use the WP API ring instead of the QUIC.cloud ring. * @param string $key_id Optional pinned key ID. * @return array<int,string> Binary public keys. */ private function _trusted_server_pks( $from_wpapi = false, $key_id = '' ) { $environment = false !== strpos( $this->_cloud_server, 'preview.' ) ? 'preview' : 'prod'; $source = $from_wpapi ? 'wpapi' : 'qc'; $ring = isset( self::SERVER_SIGN_KEYS[ $environment ][ $source ] ) ? self::SERVER_SIGN_KEYS[ $environment ][ $source ] : []; if ( '' !== $key_id ) { $ring = isset( $ring[ $key_id ] ) ? [ $ring[ $key_id ] ] : []; } $keys = []; foreach ( $ring as $key_b64 ) { $public_key = $this->_decode_b64( $key_b64, SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES ); if ( $public_key ) { $keys[] = $public_key; } } return $keys; } /** * WPAPI echo back to notify the sealed databox * * @since 7.0 */ public function wp_rest_echo() { $site_pk = isset( $this->_summary['pk_b64'] ) && is_string( $this->_summary['pk_b64'] ) ? $this->_summary['pk_b64'] : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $ts_input = isset( $_POST['wpapi_ts'] ) ? wp_unslash( $_POST['wpapi_ts'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $sig_input = isset( $_POST['wpapi_signature_b64'] ) ? wp_unslash( $_POST['wpapi_signature_b64'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $key_id_input = isset( $_POST['wpapi_key_id'] ) ? wp_unslash( $_POST['wpapi_key_id'] ) : ''; $ts = is_string( $ts_input ) ? sanitize_text_field( $ts_input ) : ''; $sig = is_string( $sig_input ) ? sanitize_text_field( $sig_input ) : ''; $key_id = is_string( $key_id_input ) ? sanitize_text_field( $key_id_input ) : ''; if ( '' === $site_pk || ! preg_match( '/^\d{10}$/D', $ts ) || '' === $sig || ( '' !== $key_id && ! preg_match( '/^[a-z0-9-]{1,64}$/D', $key_id ) ) ) { return self::err( 'No echo data' ); } $echo_data = "echo\n" . $site_pk . "\n" . $ts; $diff = time() - (int) $ts; if ( abs( $diff ) > self::SIGN_MAX_AGE ) { self::debugErr( 'WPAPI echo data timeout [diff] ' . $diff ); return self::err( 'Echo data expired' ); } if ( ! $this->_validate_signature( $sig, $echo_data, true, $key_id ) ) { return self::err( 'Data validation from WPAPI REST Echo failed' ); } $signature_b64 = $this->_sign_b64( $echo_data ); if ( false === $signature_b64 ) { return self::err( 'Failed to sign echo data' ); } return self::ok( [ 'signature_b64' => $signature_b64 ] ); } /** * Validate cloud data * * @since 7.0 * @since 7.9.1 Uses a source-specific pinned key ring. * * @param string $signature_b64 Base64 signature. * @param string $data Data to validate. * @param bool $from_wpapi Whether the signature is from WP API server. * @param string $key_id Optional pinned key ID. * @return bool */ private function _validate_signature( $signature_b64, $data, $from_wpapi = false, $key_id = '' ) { $signature = $this->_decode_b64( $signature_b64, SODIUM_CRYPTO_SIGN_BYTES ); if ( false === $signature ) { return false; } // Both operands are already length-checked by _decode_b64(), so sodium cannot raise on argument size here. try { foreach ( $this->_trusted_server_pks( $from_wpapi, $key_id ) as $cloud_pk ) { if ( sodium_crypto_sign_verify_detached( $signature, (string) $data, $cloud_pk ) ) { return true; } } } catch ( \SodiumException $e ) { self::debugErr( 'Signature validation failed: ' . esc_html( $e->getMessage() ) ); } return false; } /** * Build a stable REST authorization error for a signed callback. * * @since 7.9.1 * * @param string $code Machine-readable error code. * @param string $log Private debug-log detail. * @param bool $retryable Whether a sender may retry with fresh signature metadata. * @param int $status HTTP status. * @return \WP_Error */ private function _callback_error( $code, $log, $retryable = false, $status = 403 ) { self::debugErr( $log ); return new \WP_Error( $code, __( 'QUIC.cloud callback authorization failed.', 'litespeed-cache' ), [ 'status' => $status, 'retryable' => (bool) $retryable, ] ); } /** * Atomically claim a callback nonce in the current site's options table. * * @since 7.9.1 * * @param string $nonce Callback nonce. * @param int $qc_ts Signed timestamp. * @return true|\WP_Error */ private function _claim_callback_nonce( $nonce, $qc_ts ) { global $wpdb; $prefix = self::name( self::ITEM_SIGN_NONCE ); $option_name = $prefix . hash( 'sha256', $this->_summary['pk_b64'] . "\n" . $nonce ); $now = time(); $expires_at = (int) $qc_ts + self::SIGN_MAX_AGE + 1; $cleanup = "DELETE FROM `$wpdb->options` WHERE option_name LIKE %s AND CAST( option_value AS UNSIGNED ) < %d"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $cleaned = $wpdb->query( $wpdb->prepare( $cleanup, [ $wpdb->esc_like( $prefix ) . '%', $now ] ) ); if ( false === $cleaned ) { return $this->_callback_error( self::CALLBACK_ERR_STORAGE, 'Failed to clean the callback replay cache: ' . (string) $wpdb->last_error, true, 503 ); } $q = "INSERT INTO `$wpdb->options` ( option_name, option_value, autoload ) VALUES ( %s, %d, 'no' ) ON DUPLICATE KEY UPDATE option_value = IF( CAST( option_value AS UNSIGNED ) < %d, VALUES( option_value ), option_value )"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $claimed = $wpdb->query( $wpdb->prepare( $q, [ $option_name, $expires_at, $now ] ) ); if ( false === $claimed ) { return $this->_callback_error( self::CALLBACK_ERR_STORAGE, 'Failed to write the callback replay cache: ' . (string) $wpdb->last_error, true, 503 ); } if ( 0 === $claimed ) { return $this->_callback_error( self::CALLBACK_ERR_REPLAY, 'Replayed callback signature.' ); } $count_q = "SELECT COUNT(*) FROM `$wpdb->options` WHERE option_name LIKE %s AND CAST( option_value AS UNSIGNED ) >= %d"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $count = $wpdb->get_var( $wpdb->prepare( $count_q, [ $wpdb->esc_like( $prefix ) . '%', $now ] ) ); if ( null === $count && ! empty( $wpdb->last_error ) ) { return $this->_callback_error( self::CALLBACK_ERR_STORAGE, 'Failed to read the callback replay cache: ' . (string) $wpdb->last_error, true, 503 ); } $count = (int) $count; if ( $count > self::SIGN_NONCE_MAX ) { $delete_q = "DELETE FROM `$wpdb->options` WHERE option_name = %s AND option_value = %d"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared $deleted = $wpdb->query( $wpdb->prepare( $delete_q, [ $option_name, $expires_at ] ) ); if ( false === $deleted ) { return $this->_callback_error( self::CALLBACK_ERR_STORAGE, 'Failed to roll back an over-capacity callback nonce: ' . (string) $wpdb->last_error, true, 503 ); } return $this->_callback_error( self::CALLBACK_ERR_CAPACITY, 'The callback replay cache is full.', true, 429 ); } return true; } /** * Verify a signature and claim its nonce. * * Shared tail of every signed surface: freshness, signature, and single use. Callers own field extraction and site binding. * * @since 7.9.1 * * @param string $signed_bytes Exact bytes the signature covers. * @param string $signature_b64 Base64 detached signature. * @param mixed $qc_ts Callback timestamp. * @param mixed $qc_nonce Callback nonce. * @param bool $from_wpapi True when the WP API server signed it. * @param string $key_id Optional pinned key ID. * @return true|\WP_Error */ private function _verify_signed( $signed_bytes, $signature_b64, $qc_ts, $qc_nonce, $from_wpapi, $key_id = '' ) { if ( empty( $this->_summary['pk_b64'] ) || ! is_string( $this->_summary['pk_b64'] ) || ! is_string( $signed_bytes ) || ! is_string( $signature_b64 ) || '' === $signature_b64 || ! is_string( $qc_nonce ) || ! preg_match( '/^[a-zA-Z0-9_-]{16,128}$/D', $qc_nonce ) || ! is_string( $key_id ) || ( '' !== $key_id && ! preg_match( '/^[a-z0-9-]{1,64}$/D', $key_id ) ) ) { return $this->_callback_error( self::CALLBACK_ERR_METADATA, 'Invalid callback signature metadata.' ); } if ( ! is_string( $qc_ts ) || ! preg_match( '/^\d{10}$/D', $qc_ts ) ) { return $this->_callback_error( self::CALLBACK_ERR_METADATA, 'Invalid callback timestamp metadata.' ); } $diff = time() - (int) $qc_ts; if ( abs( $diff ) > self::SIGN_MAX_AGE ) { return $this->_callback_error( self::CALLBACK_ERR_TIMESTAMP, 'Invalid or expired callback timestamp.', true ); } $site_pk = (string) $this->_summary['pk_b64']; if ( ! $this->_validate_signature( $signature_b64, $signed_bytes, $from_wpapi, $key_id ) ) { return $this->_callback_error( self::CALLBACK_ERR_SIGNATURE, 'Invalid callback signature.' ); } self::reload_summary(); if ( empty( $this->_summary['pk_b64'] ) || ! hash_equals( (string) $this->_summary['pk_b64'], $site_pk ) ) { return $this->_callback_error( self::CALLBACK_ERR_SITE, 'The site key changed during signature validation.' ); } $claimed = $this->_claim_callback_nonce( $qc_nonce, $qc_ts ); if ( true === $claimed ) { self::debug( 'Validated callback [request] ' . substr( hash( 'sha256', $qc_nonce ), 0, 12 ) ); } return $claimed; } /** * Validate and claim a signed REST callback. * * QUIC.cloud signs the exact raw JSON body and sends the detached signature in `X-QC-Signature-B64`. * * @since 7.9.1 * * @param \WP_REST_Request $request REST request. * @param string $action Expected self::SIGN_ACTION_* value. * @return true|\WP_Error */ public function validate_signed_callback( $request, $action ) { if ( ! is_object( $request ) || ! method_exists( $request, 'get_body' ) || ! method_exists( $request, 'get_header' ) || ! is_string( $action ) ) { return $this->_callback_error( self::CALLBACK_ERR_REQUEST, 'Invalid signed callback request.' ); } $body = $request->get_body(); $content_type = $request->get_header( 'content-type' ); $content_encoding = $request->get_header( 'content-encoding' ); $signature_b64 = $request->get_header( 'x-qc-signature-b64' ); $content_encoding = null === $content_encoding ? '' : $content_encoding; $content_type_log = is_string( $content_type ) ? strtolower( trim( (string) strtok( $content_type, ';' ) ) ) : gettype( $content_type ); $content_type_log = substr( str_replace( [ "\r", "\n" ], '', $content_type_log ), 0, 128 ); $transport_error = ''; if ( ! is_string( $body ) ) { $transport_error = 'Request body is not a string.'; } elseif ( '' === $body ) { $transport_error = 'Request body is empty.'; } elseif ( self::SIGN_BODY_MAX_BYTES < strlen( $body ) ) { $transport_error = 'Request body is too large [bytes] ' . strlen( $body ); } elseif ( ! is_string( $content_type ) ) { $transport_error = 'Content-Type header is missing or invalid.'; } elseif ( ! is_string( $content_encoding ) ) { $transport_error = 'Content-Encoding header is invalid.'; } elseif ( ! is_string( $signature_b64 ) ) { $transport_error = 'X-QC-Signature-B64 header is missing or invalid.'; } elseif ( 'application/json' !== strtolower( trim( (string) strtok( $content_type, ';' ) ) ) ) { $transport_error = 'Content-Type is not application/json.'; } elseif ( '' !== $content_encoding && 'identity' !== strtolower( trim( $content_encoding ) ) ) { $transport_error = 'Content-Encoding is not identity.'; } if ( $transport_error ) { return $this->_callback_error( self::CALLBACK_ERR_REQUEST, 'Invalid signed callback transport [action] ' . $action . ' [content-type] ' . $content_type_log . ' [reason] ' . $transport_error ); } $payload = json_decode( $body, true, 32 ); if ( ! is_array( $payload ) || ! isset( $payload['qc_sig_v'] ) || ! is_int( $payload['qc_sig_v'] ) || self::SIGN_VERSION !== $payload['qc_sig_v'] || empty( $payload['qc_action'] ) || ! is_string( $payload['qc_action'] ) || ! isset( $payload['wp_pk_sha256'] ) || ! is_string( $payload['wp_pk_sha256'] ) || ! preg_match( '/^[a-f0-9]{64}$/D', $payload['wp_pk_sha256'] ) || ! isset( $payload['qc_ts'] ) || ! is_string( $payload['qc_ts'] ) || ! isset( $payload['qc_nonce'] ) || ! is_string( $payload['qc_nonce'] ) || ( isset( $payload['qc_key_id'] ) && ! is_string( $payload['qc_key_id'] ) ) ) { return $this->_callback_error( self::CALLBACK_ERR_ENVELOPE, 'Invalid callback envelope [action] ' . $action ); } if ( ! hash_equals( $action, $payload['qc_action'] ) ) { return $this->_callback_error( self::CALLBACK_ERR_ACTION, 'Callback action does not match its route.' ); } $request_key = spl_object_hash( $request ) . ':' . hash( 'sha256', $action ); $fingerprint = hash( 'sha256', $body ) . hash( 'sha256', $content_type ) . hash( 'sha256', $content_encoding ) . hash( 'sha256', $signature_b64 ); if ( isset( $this->_validated_callback_requests[ $request_key ] ) && $this->_validated_callback_requests[ $request_key ]['request'] === $request && hash_equals( $this->_validated_callback_requests[ $request_key ]['fingerprint'], $fingerprint ) ) { return true; } if ( empty( $this->_summary['pk_b64'] ) || ! is_string( $this->_summary['pk_b64'] ) ) { return $this->_callback_error( self::CALLBACK_ERR_REQUEST, 'Invalid signed callback request.' ); } if ( ! hash_equals( hash( 'sha256', (string) $this->_summary['pk_b64'] ), $payload['wp_pk_sha256'] ) ) { return $this->_callback_error( self::CALLBACK_ERR_SITE, 'Callback site-key digest does not match this site.' ); } $key_id = isset( $payload['qc_key_id'] ) ? $payload['qc_key_id'] : ''; $verified = $this->_verify_signed( $body, trim( $signature_b64 ), $payload['qc_ts'], $payload['qc_nonce'], self::SIGN_ACTION_NOTIFY_IMG === $action, $key_id ); if ( true === $verified ) { $this->_validated_callback_requests[ $request_key ] = [ 'request' => $request, 'fingerprint' => $fingerprint, ]; } return $verified; } /** * Read a sanitized query parameter. * * @since 7.9.1 * * @param string $key Parameter name. * @return string */ private function _get_param( $key ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $value = isset( $_GET[ $key ] ) ? wp_unslash( $_GET[ $key ] ) : ''; return is_string( $value ) ? sanitize_text_field( $value ) : ''; } /** * Validate a signed activation redirect from QUIC.cloud. * * The signature covers a newline-delimited canonical string and binds the callback to the local site key. * * @since 7.9.1 * * @return array|false Validated fields or false. */ private function _validate_signed_redirect() { if ( empty( $this->_summary['pk_b64'] ) || ! is_string( $this->_summary['pk_b64'] ) ) { return false; } $qc_activated = $this->_get_param( 'qc_activated' ); $qc_ts = $this->_get_param( 'qc_ts' ); $qc_nonce = $this->_get_param( 'qc_nonce' ); $qc_sig = $this->_get_param( 'qc_signature_v2_b64' ); $qc_key_id = $this->_get_param( 'qc_key_id' ); $main_domain = $this->_get_param( 'main_domain' ); if ( ! in_array( $qc_activated, [ 'anonymous', 'linked', 'cdn' ], true ) ) { self::debugErr( 'Failed to parse qc activation status' ); return false; } if ( '' !== $main_domain && ! preg_match( '/^[A-Za-z0-9.-]{1,253}$/D', $main_domain ) ) { self::debugErr( 'Invalid main domain in the activation redirect.' ); return false; } $signed = implode( "\n", [ 'main_domain=' . $main_domain, 'qc_activated=' . $qc_activated, 'qc_nonce=' . $qc_nonce, 'qc_ts=' . $qc_ts, 'wp_pk_b64=' . $this->_summary['pk_b64'], ] ); if ( true !== $this->_verify_signed( $signed, strtr( $qc_sig, '-_', '+/' ), $qc_ts, $qc_nonce, false, $qc_key_id ) ) { return false; } return [ 'qc_activated' => $qc_activated, 'main_domain' => '' !== $main_domain ? $main_domain : false, ]; } /** * Finish qc activation after redirection back from QC * * @since 7.0 * * @param string|false $ref Ref slug. */ public function finish_qc_activation( $ref = false ) { if ( '' === $this->_get_param( 'qc_activated' ) ) { return; } $activation = $this->_validate_signed_redirect(); if ( ! $activation ) { Admin_Display::error( sprintf( __( 'Failed to validate %s activation data.', 'litespeed-cache' ), 'QUIC.cloud' ) ); return; } self::debug( 'QC activation status: ' . $activation['qc_activated'] ); $this->update_qc_activation( $activation['qc_activated'], $activation['main_domain'] ); wp_safe_redirect( $this->_get_ref_url( $ref ) ); exit; } /** * Finish qc activation process * * @since 7.0 * * @param string $qc_activated Activation status. * @param string|bool $main_domain Main domain. * @param bool $quite Quiet flag. */ public function update_qc_activation( $qc_activated, $main_domain = false, $quite = false ) { $this->_summary['qc_activated'] = $qc_activated; if ( $main_domain ) { $this->_summary['main_domain'] = $main_domain; } $this->save_summary(); $msg = sprintf( __( 'Congratulations, %s successfully set this domain up for the anonymous online services.', 'litespeed-cache' ), 'QUIC.cloud' ); if ( 'linked' === $qc_activated ) { $msg = sprintf( __( 'Congratulations, %s successfully set this domain up for the online services.', 'litespeed-cache' ), 'QUIC.cloud' ); // Sync possible partner info $this->sync_usage(); } if ( 'cdn' === $qc_activated ) { $msg = sprintf( __( 'Congratulations, %s successfully set this domain up for the online services with CDN service.', 'litespeed-cache' ), 'QUIC.cloud' ); // Turn on CDN option $this->cls( 'Conf' )->update_confs( [ self::O_CDN_QUIC => true ] ); } if ( ! $quite ) { Admin_Display::success( '🎊 ' . $msg ); } $this->_clear_reset_qc_reg_msg(); $this->clear_cloud(); } /** * Update QC status * * @since 7.0 * @since 7.9.1 Reads the status from the verified raw body instead of $_POST. * * @param string|null $raw_body Verified raw JSON body from the REST request. * @return array */ public function update_cdn_status( $raw_body = null ) { // Read the status from the signed body, never $_POST. $payload = is_string( $raw_body ) ? json_decode( $raw_body, true, 32 ) : false; if ( ! is_array( $payload ) ) { return self::err( 'invalid data' ); } $qc_activated = ! empty( $payload['qc_activated'] ) && is_string( $payload['qc_activated'] ) ? $payload['qc_activated'] : ''; if ( ! in_array( $qc_activated, [ 'anonymous', 'linked', 'cdn', 'deleted' ], true ) ) { return self::err( 'lack_of_params' ); } self::debug( 'update_cdn_status request hash: ' . $qc_activated ); if ( 'deleted' === $qc_activated ) { $this->_reset_qc_reg(); } else { $this->_summary['qc_activated'] = $qc_activated; $this->save_summary(); } if ( 'cdn' === $qc_activated ) { $msg = sprintf( __( 'Congratulations, %s successfully set this domain up for the online services with CDN service.', 'litespeed-cache' ), 'QUIC.cloud' ); Admin_Display::success( '🎊 ' . $msg ); $this->_clear_reset_qc_reg_msg(); // Turn on CDN option $this->cls( 'Conf' )->update_confs( [ self::O_CDN_QUIC => true ] ); $this->cls( 'CDN\Quic' )->try_sync_conf( true ); } return self::ok( [ 'qc_activated' => $qc_activated ] ); } /** * Clear QC linked status * * @since 5.0 */ private function _reset_qc_reg() { $this->save_summary( [ 'qc_activated' => '', 'partner' => '', ], true ); $msg = $this->_reset_qc_reg_content(); Admin_Display::error( $msg, false, true ); } /** * Build reset QC registration content. * * @since 7.0 * @return string */ private function _reset_qc_reg_content() { $msg = __( 'Site not recognized. QUIC.cloud deactivated automatically. Please reactivate your QUIC.cloud account.', 'litespeed-cache' ); $msg .= Doc::learn_more( admin_url( 'admin.php?page=litespeed' ), __( 'Click here to proceed.', 'litespeed-cache' ), true, false, true ); $msg .= Doc::learn_more( 'https://docs.litespeedtech.com/lscache/lscwp/general/', false, false, false, true ); return $msg; } /** * Clear reset QC reg msg if exist * * @since 7.0 */ private function _clear_reset_qc_reg_msg() { self::debug( 'Removed pinned reset QC reg content msg' ); $msg = $this->_reset_qc_reg_content(); Admin_Display::dismiss_pin_by_content( $msg, Admin_Display::NOTICE_RED, true ); } }
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