| 1 |
<?php |
| 2 |
/** |
| 3 |
* Ebook Store - bootstrap helpers, environment guards and upgrade routines. |
| 4 |
* |
| 5 |
* This file is loaded before anything else and must stay parseable on PHP 5.6, |
| 6 |
* so that a site on an ancient PHP version sees a helpful admin notice instead |
| 7 |
* of a white screen. |
| 8 |
* |
| 9 |
* @package EbookStore |
| 10 |
*/ |
| 11 |
|
| 12 |
if ( ! defined( 'ABSPATH' ) ) { |
| 13 |
exit; |
| 14 |
} |
| 15 |
|
| 16 |
/** |
| 17 |
* Minimum supported environment. |
| 18 |
*/ |
| 19 |
/** |
| 20 |
* Absolute path to the main plugin file. Used anywhere a path must be derived |
| 21 |
* without depending on __FILE__ of the including file. |
| 22 |
*/ |
| 23 |
if ( ! defined( 'EBOOK_STORE_PLUGIN_FILE' ) ) { |
| 24 |
define( 'EBOOK_STORE_PLUGIN_FILE', dirname( __DIR__ ) . '/ebook_store.php' ); |
| 25 |
} |
| 26 |
|
| 27 |
define( 'EBOOK_STORE_MIN_PHP', '7.4' ); |
| 28 |
define( 'EBOOK_STORE_MIN_WP', '5.8' ); |
| 29 |
|
| 30 |
/** |
| 31 |
* The plugin version, read once from the plugin header. |
| 32 |
* |
| 33 |
* @return string |
| 34 |
*/ |
| 35 |
function ebook_store_get_plugin_version() { |
| 36 |
static $version = null; |
| 37 |
|
| 38 |
if ( $version !== null ) { |
| 39 |
return $version; |
| 40 |
} |
| 41 |
|
| 42 |
$version = '0.0.0'; |
| 43 |
$file = dirname( __DIR__ ) . '/ebook_store.php'; |
| 44 |
|
| 45 |
if ( is_readable( $file ) ) { |
| 46 |
if ( ! function_exists( 'get_file_data' ) ) { |
| 47 |
require_once ABSPATH . 'wp-includes/functions.php'; |
| 48 |
} |
| 49 |
$data = get_file_data( $file, array( 'Version' => 'Version' ), 'plugin' ); |
| 50 |
if ( ! empty( $data['Version'] ) ) { |
| 51 |
$version = $data['Version']; |
| 52 |
} |
| 53 |
} |
| 54 |
|
| 55 |
return $version; |
| 56 |
} |
| 57 |
|
| 58 |
/** |
| 59 |
* Absolute path to the plugin directory, with a trailing slash. |
| 60 |
* |
| 61 |
* @return string |
| 62 |
*/ |
| 63 |
function ebook_store_path( $relative = '' ) { |
| 64 |
return dirname( __DIR__ ) . '/' . ltrim( $relative, '/' ); |
| 65 |
} |
| 66 |
|
| 67 |
/** |
| 68 |
* URL to a plugin asset. |
| 69 |
* |
| 70 |
* @param string $relative Path relative to the plugin root. |
| 71 |
* @return string |
| 72 |
*/ |
| 73 |
function ebook_store_url( $relative = '' ) { |
| 74 |
return plugins_url( ltrim( $relative, '/' ), dirname( __DIR__ ) . '/ebook_store.php' ); |
| 75 |
} |
| 76 |
|
| 77 |
/** |
| 78 |
* Does the current environment meet the plugin requirements? |
| 79 |
* |
| 80 |
* @return true|string True, or a translated error message. |
| 81 |
*/ |
| 82 |
function ebook_store_check_environment() { |
| 83 |
global $wp_version; |
| 84 |
|
| 85 |
if ( version_compare( PHP_VERSION, EBOOK_STORE_MIN_PHP, '<' ) ) { |
| 86 |
return sprintf( |
| 87 |
/* translators: 1: required PHP version, 2: current PHP version. */ |
| 88 |
__( 'Ebook Store needs PHP %1$s or newer. This site runs PHP %2$s. Ask your host to switch the site to a supported PHP version.', 'ebook-store' ), |
| 89 |
EBOOK_STORE_MIN_PHP, |
| 90 |
PHP_VERSION |
| 91 |
); |
| 92 |
} |
| 93 |
|
| 94 |
if ( isset( $wp_version ) && version_compare( $wp_version, EBOOK_STORE_MIN_WP, '<' ) ) { |
| 95 |
return sprintf( |
| 96 |
/* translators: 1: required WordPress version, 2: current WordPress version. */ |
| 97 |
__( 'Ebook Store needs WordPress %1$s or newer. This site runs WordPress %2$s.', 'ebook-store' ), |
| 98 |
EBOOK_STORE_MIN_WP, |
| 99 |
$wp_version |
| 100 |
); |
| 101 |
} |
| 102 |
|
| 103 |
return true; |
| 104 |
} |
| 105 |
|
| 106 |
/* ------------------------------------------------------------------------- |
| 107 |
* Upgrade routines |
| 108 |
* ---------------------------------------------------------------------- */ |
| 109 |
|
| 110 |
/** |
| 111 |
* Run one-time migrations when the stored version is older than the file version. |
| 112 |
*/ |
| 113 |
function ebook_store_maybe_upgrade() { |
| 114 |
$installed = (string) get_option( 'ebook_store_version', '' ); |
| 115 |
$current = ebook_store_get_plugin_version(); |
| 116 |
|
| 117 |
if ( $installed === $current ) { |
| 118 |
return; |
| 119 |
} |
| 120 |
|
| 121 |
// 6.00 removed the Square integration, which required PHP 8.1 and locked |
| 122 |
// out every site still on PHP 7.4. |
| 123 |
if ( $installed === '' || version_compare( $installed, '6.00', '<' ) ) { |
| 124 |
ebook_store_upgrade_remove_square(); |
| 125 |
} |
| 126 |
|
| 127 |
// 6.23 began storing the PayPal verification toggle as an explicit '1'/'0'. |
| 128 |
// A site upgrading from an earlier version can hold '', which the IPN reads |
| 129 |
// as "the owner never chose, so verify" — correct, but it left the setting |
| 130 |
// looking unset until the owner re-saved the PayPal tab. Write the value the |
| 131 |
// site has been running with so the stored setting matches its behaviour. |
| 132 |
if ( $installed !== '' && version_compare( $installed, '6.24', '<' ) ) { |
| 133 |
$paypal_verify = get_option( 'paypal_verify_transactions', null ); |
| 134 |
if ( $paypal_verify !== null && is_scalar( $paypal_verify ) && (string) $paypal_verify === '' ) { |
| 135 |
update_option( 'paypal_verify_transactions', '1' ); |
| 136 |
} |
| 137 |
} |
| 138 |
|
| 139 |
// Protection for the upload directories is (re)asserted on every upgrade, |
| 140 |
// so a site that lost its .htaccess gets it back. |
| 141 |
if ( function_exists( 'ebook_store_ensure_protected_dirs' ) ) { |
| 142 |
ebook_store_ensure_protected_dirs(); |
| 143 |
} |
| 144 |
|
| 145 |
update_option( 'ebook_store_version', $current, false ); |
| 146 |
} |
| 147 |
add_action( 'admin_init', 'ebook_store_maybe_upgrade' ); |
| 148 |
|
| 149 |
/** |
| 150 |
* Delete every option, transient and log file left behind by the removed |
| 151 |
* Square integration, so nothing lingers in the database. |
| 152 |
*/ |
| 153 |
function ebook_store_upgrade_remove_square() { |
| 154 |
global $wpdb; |
| 155 |
|
| 156 |
$options = array( |
| 157 |
'square_access_token', |
| 158 |
'square_location_id', |
| 159 |
'square_environment', |
| 160 |
'square_webhook_signature_key', |
| 161 |
'square_cancel_url', |
| 162 |
'square_currency', |
| 163 |
'square_currency_conversion_enabled', |
| 164 |
'square_integration_enabled', |
| 165 |
'square_allow_tipping', |
| 166 |
'square_ask_shipping_address', |
| 167 |
'square_merchant_support_email', |
| 168 |
'square_button_text', |
| 169 |
); |
| 170 |
|
| 171 |
foreach ( $options as $option ) { |
| 172 |
delete_option( $option ); |
| 173 |
} |
| 174 |
|
| 175 |
// Checkout-state transients and per-payment creation locks. |
| 176 |
$wpdb->query( |
| 177 |
$wpdb->prepare( |
| 178 |
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s OR option_name LIKE %s", |
| 179 |
$wpdb->esc_like( '_transient_ebook_store_square_' ) . '%', |
| 180 |
$wpdb->esc_like( '_transient_timeout_ebook_store_square_' ) . '%', |
| 181 |
$wpdb->esc_like( 'ebook_store_square_lock_' ) . '%' |
| 182 |
) |
| 183 |
); |
| 184 |
|
| 185 |
// The old integration wrote an uncapped log file inside the plugin folder, |
| 186 |
// which routinely grew past 100 MB. |
| 187 |
$log = ebook_store_path( 'square-webhook.log.txt' ); |
| 188 |
if ( file_exists( $log ) ) { |
| 189 |
@unlink( $log ); |
| 190 |
} |
| 191 |
} |
| 192 |
|
| 193 |
/* ------------------------------------------------------------------------- |
| 194 |
* Logging |
| 195 |
* ---------------------------------------------------------------------- */ |
| 196 |
|
| 197 |
/** |
| 198 |
* Maximum size a plugin log file may reach before it is rotated, in bytes. |
| 199 |
*/ |
| 200 |
define( 'EBOOK_STORE_LOG_MAX_BYTES', 2 * MB_IN_BYTES ); |
| 201 |
|
| 202 |
/** |
| 203 |
* Append a line to a plugin log file, with rotation and a hard size cap. |
| 204 |
* |
| 205 |
* Logging is off unless WP_DEBUG is on or the 'ebook_store_enable_logging' |
| 206 |
* filter returns true, so a production store never accumulates gigabytes of |
| 207 |
* webhook chatter inside wp-content/plugins. |
| 208 |
* |
| 209 |
* @param string $channel Log channel, e.g. 'stripe'. |
| 210 |
* @param string $message Message. |
| 211 |
* @param array $context Optional context. |
| 212 |
*/ |
| 213 |
function ebook_store_log( $channel, $message, $context = array() ) { |
| 214 |
$enabled = ( defined( 'WP_DEBUG' ) && WP_DEBUG ) || defined( 'EBOOK_STORE_DEBUG' ); |
| 215 |
|
| 216 |
if ( ! apply_filters( 'ebook_store_enable_logging', $enabled, $channel ) ) { |
| 217 |
return; |
| 218 |
} |
| 219 |
|
| 220 |
$dir = ebook_store_get_log_dir(); |
| 221 |
if ( $dir === '' ) { |
| 222 |
return; |
| 223 |
} |
| 224 |
|
| 225 |
$file = $dir . '/' . sanitize_file_name( $channel ) . '.log'; |
| 226 |
|
| 227 |
if ( file_exists( $file ) && filesize( $file ) > EBOOK_STORE_LOG_MAX_BYTES ) { |
| 228 |
@rename( $file, $file . '.1' ); |
| 229 |
} |
| 230 |
|
| 231 |
$entry = gmdate( '[Y-m-d H:i:s] ' ) . $message . "\n"; |
| 232 |
if ( ! empty( $context ) ) { |
| 233 |
$entry .= wp_json_encode( $context ) . "\n"; |
| 234 |
} |
| 235 |
$entry .= "----------------------------------------\n"; |
| 236 |
|
| 237 |
@file_put_contents( $file, $entry, FILE_APPEND | LOCK_EX ); |
| 238 |
} |
| 239 |
|
| 240 |
/** |
| 241 |
* A protected log directory inside wp-content/uploads, never inside the plugin |
| 242 |
* folder and never web-readable. |
| 243 |
* |
| 244 |
* @return string Absolute path without a trailing slash, or '' on failure. |
| 245 |
*/ |
| 246 |
function ebook_store_get_log_dir() { |
| 247 |
static $dir = null; |
| 248 |
|
| 249 |
if ( $dir !== null ) { |
| 250 |
return $dir; |
| 251 |
} |
| 252 |
|
| 253 |
$uploads = wp_upload_dir(); |
| 254 |
if ( ! empty( $uploads['error'] ) ) { |
| 255 |
$dir = ''; |
| 256 |
return $dir; |
| 257 |
} |
| 258 |
|
| 259 |
$dir = untrailingslashit( $uploads['basedir'] ) . '/ebook-store-logs'; |
| 260 |
|
| 261 |
if ( ! is_dir( $dir ) && ! wp_mkdir_p( $dir ) ) { |
| 262 |
$dir = ''; |
| 263 |
return $dir; |
| 264 |
} |
| 265 |
|
| 266 |
// Belt and braces: block direct HTTP access on both Apache and any server |
| 267 |
// that respects a directory index. |
| 268 |
if ( ! file_exists( $dir . '/.htaccess' ) ) { |
| 269 |
@file_put_contents( $dir . '/.htaccess', "Require all denied\n<IfModule !mod_authz_core.c>\nDeny from all\n</IfModule>\n" ); |
| 270 |
} |
| 271 |
if ( ! file_exists( $dir . '/index.php' ) ) { |
| 272 |
@file_put_contents( $dir . '/index.php', "<?php\n// Silence is golden.\n" ); |
| 273 |
} |
| 274 |
|
| 275 |
return $dir; |
| 276 |
} |
| 277 |
|