*/ protected $args; /** * Constructor. * * @since 1.0.0 * @param array $args Facts (see the class comment). */ public function __construct( array $args ) { $this->args = array_merge( array( 'id' => '', 'group' => 'wordpress', 'label' => '', 'description' => '', 'consequence' => '', 'risk' => DevGuard::ADVANCED, 'object_cap' => '', 'slow' => false, 'tables' => array(), 'days' => null, 'min_days' => 1, 'backup' => false, 'unavailable' => null, 'setting' => '', ), $args ); } /** * {@inheritDoc} */ public function id(): string { return (string) $this->args['id']; } /** * {@inheritDoc} */ public function group(): string { return (string) $this->args['group']; } /** * {@inheritDoc} */ public function label(): string { return (string) $this->args['label']; } /** * {@inheritDoc} */ public function description(): string { return (string) $this->args['description']; } /** * {@inheritDoc} */ public function consequence(): string { return (string) $this->args['consequence']; } /** * {@inheritDoc} */ public function risk(): string { $risk = $this->args['risk']; return is_callable( $risk ) ? (string) $risk() : (string) $risk; } /** * {@inheritDoc} */ public function object_cap(): string { return (string) $this->args['object_cap']; } /** * {@inheritDoc} */ public function slow(): bool { return (bool) $this->args['slow']; } /** * {@inheritDoc} */ public function tables(): array { return array_values( array_filter( (array) $this->args['tables'], 'is_string' ) ); } /** * {@inheritDoc} */ public function requires_backup(): bool { return (bool) $this->args['backup']; } /** * The power-up setting this check's age threshold comes from, or ''. * * @since 1.0.0 * @return string */ public function setting(): string { return is_string( $this->args['setting'] ) ? $this->args['setting'] : ''; } /** * {@inheritDoc} The configured days, then the `apwc_database_cleanup_threshold` filter, then * the check's minimum. */ public function threshold(): ?int { if ( ! is_callable( $this->args['days'] ) ) { return null; } $days = call_user_func( $this->args['days'] ); if ( null === $days ) { return null; } /** * Filter how old (days) rows must be before a Database clean-up check counts them. * Clamped to the check's minimum. * * @since 1.0.0 * @param int $days Days from the power-up's settings (or the owner's setting). * @param string $check_id Check ID, e.g. `wp-revisions`. */ $days = apply_filters( 'apwc_database_cleanup_threshold', (int) $days, $this->id() ); $days = is_numeric( $days ) ? (int) $days : 0; return min( 36500, max( (int) $this->args['min_days'], $days ) ); } /** * {@inheritDoc} The configured reason first, then missing tables. */ public function unavailable(): string { if ( is_callable( $this->args['unavailable'] ) ) { $reason = (string) call_user_func( $this->args['unavailable'] ); if ( '' !== $reason ) { return $reason; } } $tables = $this->tables(); if ( $tables && ! self::table_exists( $tables[0] ) ) { return __( 'Its database table doesn’t exist on this site.', 'admin-powerups-for-woocommerce' ); } return ''; } /* ------------------------------------------------------------------ helpers */ /** * Whether a table exists (one query per request for all known tables). * * @since 1.0.0 * @param string $table Full table name. * @return bool */ public static function table_exists( string $table ): bool { return Sizes::exists( $table ); } /** * Whether there is time left in a budget. * * @since 1.0.0 * @param float $start microtime( true ) at the start. * @param float $budget Seconds. * @return bool */ protected static function time_left( float $start, float $budget ): bool { return microtime( true ) - $start < $budget; } /** * A batch for API-based deletes: the check's own size, scaled like the `apwc_database_cleanup_batch` filter. * * @since 1.0.0 * @param int $batch Batch the Runner passed (default 500). * @param int $own The check's own batch at the default. * @return int */ protected static function api_batch( int $batch, int $own ): int { return max( 1, (int) round( $own * $batch / Runner::DEFAULT_BATCH ) ); } /** * Why WooCommerce checks can't run: WooCommerce missing or waiting for a database update. * * @since 1.0.0 * @return string */ public static function woo_unavailable(): string { if ( ! function_exists( 'WC' ) || ! class_exists( '\WC_Install' ) ) { return __( 'Needs WooCommerce.', 'admin-powerups-for-woocommerce' ); } if ( \WC_Install::needs_db_update() ) { return __( 'WooCommerce is waiting for a database update. Run it first.', 'admin-powerups-for-woocommerce' ); } return ''; } /** * A value ready for a sample cell: text, invalid bytes replaced, cut to 200 characters. * * @since 1.0.0 * @param mixed $value Value from the database (never unserialized). * @return string */ public static function cell( $value ): string { if ( null === $value ) { return ''; } $value = SafeDump::to_array( Str::scalar( $value ), 1, self::CELL_CHARS ); return is_string( $value ) ? $value : ''; } /** * A value for a sample cell with personal data and secrets hidden: secret-looking content * (MetaMask::looks_like_secret()) is masked whole; email addresses (also %40-encoded), IP * addresses (IPv4 and IPv6) and phone numbers inside the text are masked. * * @since 1.0.0 * @param mixed $value Raw value (never unserialized). * @return string */ public static function value_cell( $value ): string { // Only the start is ever shown (200 characters), so only a bounded prefix is checked. $raw = substr( Str::scalar( $value ), 0, self::REDACT_BYTES ); if ( '' !== $raw && MetaMask::looks_like_secret( $raw ) ) { return self::MASK; } return self::cell( self::redact( $raw ) ); } /** * Text with email addresses, IP addresses and phone numbers masked. * * @since 1.0.0 * @param string $text Text. * @return string */ public static function redact( string $text ): string { // Emails, also URL-encoded (%40). $text = preg_replace( '/[^\s@"\'<>;:,%]{1,64}(?:@|%40)[^\s@"\'<>;:,]{1,255}\.[a-z]{2,24}/i', self::MASK, $text ); // IP addresses (IPv4 and IPv6, compressed forms and a :port included): bounded tokens that // validate as one. $text = null === $text ? null : preg_replace_callback( '/(? ltrim( substr( $hex, $at, 4 ), '0' ) ?: '0'; // phpcs:ignore Universal.Operators.DisallowShortTernary.Found -- "0" for an all-zero group. return $group( 0 ) . ':' . $group( 4 ) . '::'; } return self::MASK; } /** * A meta value for a sample: masked when its key looks sensitive or personal, otherwise * value_cell(). * * @since 1.0.0 * @param string $key Meta key. * @param mixed $value Raw value. * @return string */ public static function meta_cell( string $key, $value ): string { return MetaMask::matches( $key, self::sensitive_patterns() ) ? self::MASK : self::value_cell( $value ); } /** * Key patterns whose values samples never show: the shared sensitive patterns (context * `cleanup` of `apwc_sensitive_arg_patterns`) plus keys that carry hashes, passwords, * capabilities and keys, and personal data (contact details, phones, addresses, names, * companies, recipients, IPs, user agents). * * @since 1.0.0 * @return string[] */ public static function sensitive_patterns(): array { static $extra = array( '*hash*', '*pass*', '*capabilities*', '*activation*', '*key*', '*email*', '*phone*', '*mobile*', '*tel*', '*address*', '*billing*', '*shipping*', '*name*', '*nickname*', '*company*', '*city*', '*country*', '*postcode*', '*zip*', '*contact*', '*recipient*', '*user_agent*', '*_ip', '*_ip_*', 'ip', 'ip_*', '*ip_address*', '*description*' ); return array_values( array_unique( array_merge( MetaMask::arg_patterns( 'cleanup' ), $extra ) ) ); } /** * A GMT date for a sample cell, in the site's format. * * @since 1.0.0 * @param int $timestamp Unix time. * @return string */ public static function date_cell( int $timestamp ): string { if ( $timestamp <= 0 ) { return ''; } return (string) wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $timestamp ); } /** * Empty result of clean(). * * @since 1.0.0 * @param array $state State. * @param bool $done Finished. * @return array{deleted: int, skipped: int, notes: array, done: bool, state: array} */ protected static function cleaned( array $state = array(), bool $done = true ): array { return array( 'deleted' => 0, 'skipped' => 0, 'notes' => array(), 'done' => $done, 'state' => $state, ); } /** * An `IN (…)` list of %d placeholders. * * @since 1.0.0 * @param int $count Items. * @param string $type Placeholder (%d or %s). * @return string */ public static function placeholders( int $count, string $type = '%d' ): string { return implode( ',', array_fill( 0, max( 1, $count ), $type ) ); } }