PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]; // When Query Monitor is active, return a tracing wrapper so PDO // queries appear in QM's database panel. if ( class_exists( 'QM_Collectors' ) ) { // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO -- TracingPdo extends PDO for Query Monitor observability. return new \WebCalendar\WP\Integrations\QueryMonitor\TracingPdo( $dsn, DB_USER, DB_PASSWORD, $options ); } return new PDO( $dsn, DB_USER, DB_PASSWORD, $options ); } /** * Resolve the connection charset from the configured DB_CHARSET. * * Mirrors wpdb::determine_charset(): WordPress silently upgrades 'utf8' * to 'utf8mb4' when the server supports it, so the webcal_* tables * (created via $wpdb->charset) are utf8mb4 even on the stock * wp-config-sample.php value of 'utf8'. Without the same upgrade here, * the PDO connection talks 3-byte utf8 and any 4-byte character (emoji) * fails with SQLSTATE 1366 on strict-mode servers. The plugin's MySQL * 5.7+/MariaDB 10.3+ floor always supports utf8mb4, so no capability * probe is needed. * * @param string $configured DB_CHARSET value ('' when undefined). * @return string */ public static function resolve_charset( string $configured ): string { if ( '' === $configured || 'utf8' === $configured || 'utf8mb3' === $configured ) { return 'utf8mb4'; } return $configured; } /** * Parse a WordPress DB_HOST value into connection components. * * Mirrors $wpdb::parse_db_host() so PDO connects the same way as mysqli. * Supports: * - "localhost" → unix socket (default path) * - "localhost:/var/run/sock" → unix socket at explicit path * - "host" → TCP, default port * - "host:3307" → TCP, explicit port * - "127.0.0.1" → TCP * - "[::1]:3307" → IPv6 + port * - ":/path/to/sock" → unix socket at explicit path * * @param string $db_host Raw DB_HOST value. * @return array{host:?string,port:?int,socket:?string} */ public static function parse_db_host( string $db_host ): array { $port = null; $socket = null; // IPv6 literals are bracketed; an optional trailing colon-port may follow the bracket. if ( str_starts_with( $db_host, '[' ) ) { $close = strpos( $db_host, ']' ); if ( false !== $close ) { $ipv6 = substr( $db_host, 1, $close - 1 ); $rest = substr( $db_host, $close + 1 ); if ( str_starts_with( $rest, ':' ) ) { $port_str = substr( $rest, 1 ); if ( ctype_digit( $port_str ) ) { $port = (int) $port_str; } } return [ 'host' => $ipv6, 'port' => $port, 'socket' => null, ]; } } // "host:something" — could be port or socket path. if ( str_contains( $db_host, ':' ) ) { $split = explode( ':', $db_host, 2 ); $hostpart = $split[0]; $tail = $split[1] ?? ''; if ( '' !== $tail && '/' === $tail[0] ) { $socket = $tail; $host = '' !== $hostpart ? $hostpart : 'localhost'; } elseif ( '' !== $tail && ctype_digit( $tail ) ) { $host = $hostpart; $port = (int) $tail; } else { $host = $hostpart; } } else { $host = $db_host; } // Bare "localhost" with no socket and no port → use unix socket convention. if ( null === $port && null === $socket && 'localhost' === $host ) { $socket = self::default_socket_path(); } return [ 'host' => $host, 'port' => $port, 'socket' => $socket, ]; } /** * Build a PDO MySQL DSN from parsed host components. * * @param array{host:?string,port:?int,socket:?string} $parts Parsed host. * @param string $dbname Database name. * @param string $charset Charset. * @return string */ public static function build_dsn( array $parts, string $dbname, string $charset ): string { $segments = []; if ( null !== $parts['socket'] && '' !== $parts['socket'] ) { $segments[] = 'unix_socket=' . $parts['socket']; } else { $segments[] = 'host=' . ( $parts['host'] ?? 'localhost' ); if ( null !== $parts['port'] && $parts['port'] > 0 ) { $segments[] = 'port=' . $parts['port']; } } $segments[] = 'dbname=' . $dbname; $segments[] = 'charset=' . $charset; return 'mysql:' . implode( ';', $segments ); } /** * Best-effort default unix socket path, falling back to common locations. * * @return string */ private static function default_socket_path(): string { $ini = ini_get( 'mysqli.default_socket' ); if ( is_string( $ini ) && '' !== $ini && file_exists( $ini ) ) { return $ini; } $ini = ini_get( 'pdo_mysql.default_socket' ); if ( is_string( $ini ) && '' !== $ini && file_exists( $ini ) ) { return $ini; } foreach ( [ '/var/run/mysqld/mysqld.sock', '/tmp/mysql.sock', '/var/lib/mysql/mysql.sock' ] as $candidate ) { if ( file_exists( $candidate ) ) { return $candidate; } } // Fall back to mysqli default. PDO will surface a clear error if missing. return is_string( $ini ) ? $ini : '/tmp/mysql.sock'; } }