PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | vendor/wp-php-toolkit/reprint-server/src/class-wpdb-driver-pdo.php +286 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,286 @@
1 +<?php
2 +/**
3 + * wpdb-backed PDO adapter for MySQL exports on PDO-less hosts.
4 + *
5 + * MySQLDumpProducer expects a PDO connection — prepare(), query(), and the
6 + * statement methods fetch(), fetchAll(), fetchColumn(), execute().
7 + * On hosts without ext-pdo / ext-pdo_mysql, this adapter wraps WordPress's
8 + * global $wpdb so the dump producer can run unchanged.
9 + *
10 + * Surface area mirrors SqliteDriverPDO 1:1 — only the methods MySQLDumpProducer
11 + * and the export endpoints actually call. Behavior parity with real PDO is the
12 + * bar; behavioral divergence (wpdb's HTML error rendering, sticky last_error,
13 + * get_results null ambiguity) is normalized inside the adapter.
14 + *
15 + * Charset: create_db_connection() gives PDO and wpdb the same utf8mb4_bin
16 + * session context. The dump producer casts non-numeric result columns to
17 + * binary, while text primary key comparisons retain their stored collation.
18 + */
19 +
20 +namespace WordPress\Reprint\Server;
21 +
22 +/**
23 + * Wraps a WordPress wpdb instance to look like a PDO connection.
24 + */
25 +class WpdbDriverPDO
26 +{
27 + /** @var object */
28 + private $wpdb;
29 +
30 + /**
31 + * @param object $wpdb The global WordPress $wpdb. Type hint omitted because
32 + * wpdb subclasses (HyperDB, LudicrousDB, SQLite drop-in) are not
33 + * guaranteed to extend the canonical class in all environments,
34 + * and the test double is not a wpdb subclass.
35 + */
36 + public function __construct($wpdb)
37 + {
38 + $this->wpdb = $wpdb;
39 +
40 + // Prevent wpdb from echoing HTML error blocks into the response stream.
41 + // The export endpoints emit gzip multipart, so even one HTML chunk
42 + // would corrupt the output stream irrecoverably. No restore on
43 + // shutdown: export endpoints terminate the request after streaming.
44 + $wpdb->suppress_errors(true);
45 + $wpdb->hide_errors();
46 + }
47 +
48 + public function prepare(string $sql): WpdbDriverPDOStatement
49 + {
50 + return new WpdbDriverPDOStatement($this->wpdb, $sql);
51 + }
52 +
53 + public function query(string $sql): WpdbDriverPDOStatement
54 + {
55 + $stmt = new WpdbDriverPDOStatement($this->wpdb, $sql);
56 + $stmt->execute();
57 + return $stmt;
58 + }
59 +
60 + /**
61 + * Quotes a string for safe inclusion in a query.
62 + *
63 + * Calls $wpdb->_real_escape() — a public method on wpdb (and its
64 + * subclasses); the leading underscore is naming convention only, not
65 + * visibility. Calling it directly keeps the adapter free of any global
66 + * function dependency and HyperDB / LudicrousDB / SQLite-drop-in safe.
67 + */
68 + public function quote(string $value, $type = null): string
69 + {
70 + return "'" . $this->wpdb->_real_escape($value) . "'";
71 + }
72 +}
73 +
74 +/**
75 + * PDOStatement-shaped wrapper around a wpdb query result set.
76 + *
77 + * Substitutes positional (?) and named (:name) placeholders manually rather
78 + * than calling $wpdb->prepare() — wpdb's prepare uses %s/%d and adds quoting
79 + * itself, which would conflict with how MySQLDumpProducer builds queries.
80 + */
81 +class WpdbDriverPDOStatement
82 +{
83 + /** @var object */
84 + private $wpdb;
85 +
86 + /** @var string */
87 + private $sql;
88 +
89 + /** @var array Stored result rows after execution. */
90 + private $rows = [];
91 +
92 + /** @var int Current position for fetch(). */
93 + private $position = 0;
94 +
95 + /** @var array Parameters bound via bindValue(). */
96 + private $bound_params = [];
97 +
98 + /**
99 + * @param object $wpdb
100 + * @param string $sql
101 + */
102 + public function __construct($wpdb, string $sql)
103 + {
104 + $this->wpdb = $wpdb;
105 + $this->sql = $sql;
106 + }
107 +
108 + /**
109 + * Executes the prepared statement.
110 + *
111 + * Substitutes parameters into $this->sql, clears any sticky
112 + * $wpdb->last_error (which can survive from queries that ran before the
113 + * exporter took over the request), runs $wpdb->get_results($sql,
114 + * ARRAY_A), and disambiguates the null return: null + non-empty
115 + * last_error -> RuntimeException; null + empty last_error -> empty result
116 + * set.
117 + *
118 + * @param array|null $params Positional or named parameters.
119 + */
120 + public function execute($params = null): bool
121 + {
122 + $sql = $this->substitute_placeholders($this->sql, $params ?? $this->bound_params);
123 +
124 + // Clear sticky last_error so the post-dispatch check below can't
125 + // throw a phantom query error from a prior query.
126 + $this->clear_last_error();
127 +
128 + $rows = $this->wpdb->get_results($sql, 'ARRAY_A');
129 + $this->discard_query_log();
130 + $last_error = $this->get_last_error();
131 +
132 + if ($last_error !== '') {
133 + throw new \RuntimeException($last_error);
134 + }
135 +
136 + $this->rows = is_array($rows) ? $rows : [];
137 + $this->position = 0;
138 + return true;
139 + }
140 +
141 + /**
142 + * $mode is accepted for PDO compatibility but only FETCH_ASSOC is honored.
143 + * MySQLDumpProducer never asks for any other mode.
144 + */
145 + public function fetch($mode = null)
146 + {
147 + if ($this->position >= count($this->rows)) {
148 + return false;
149 + }
150 + return $this->rows[$this->position++];
151 + }
152 +
153 + /**
154 + * Honors FETCH_ASSOC (default) and FETCH_COLUMN. Other PDO modes are
155 + * unsupported — MySQLDumpProducer never asks for any other mode.
156 + */
157 + public function fetchAll($mode = null): array
158 + {
159 + $mode = $mode ?? PdoConstants::fetch_assoc();
160 + $remaining = array_slice($this->rows, $this->position);
161 + $this->position = count($this->rows);
162 +
163 + if ($mode === PdoConstants::fetch_column()) {
164 + return array_map(static function ($row) {
165 + return reset($row);
166 + }, $remaining);
167 + }
168 +
169 + return $remaining;
170 + }
171 +
172 + public function fetchColumn(int $column_number = 0)
173 + {
174 + $row = $this->fetch();
175 + if ($row === false) {
176 + return false;
177 + }
178 + $values = array_values($row);
179 + return $values[$column_number] ?? false;
180 + }
181 +
182 + public function bindValue($parameter, $value, $type = null): bool
183 + {
184 + $this->bound_params[$parameter] = $value;
185 + return true;
186 + }
187 +
188 + /**
189 + * Substitutes ? and :name placeholders with quoted parameter values.
190 + *
191 + * Walks $sql byte-by-byte tracking single/double-quoted string literals so
192 + * a literal '?' or ':name' inside a string is not treated as a placeholder.
193 + * Inside a literal, a backslash escapes the next byte (MySQL extension)
194 + * so `\'` does not close the string. Positional substitution runs right
195 + * to left so earlier offsets stay valid. Named substitution uses a regex
196 + * with a word-boundary tail (/:name(?![A-Za-z0-9_])/) so :foo does not
197 + * corrupt :foobar.
198 + */
199 + private function clear_last_error(): void
200 + {
201 + $this->wpdb->last_error = '';
202 + }
203 +
204 + /**
205 + * Drop the wpdb query log in case it's active.
206 + * We don't want these stacking up and consuming memory.
207 + */
208 + private function discard_query_log(): void
209 + {
210 + if (isset($this->wpdb->queries) && is_array($this->wpdb->queries)) {
211 + $this->wpdb->queries = [];
212 + }
213 + }
214 +
215 + private function get_last_error(): string
216 + {
217 + return (string) ($this->wpdb->last_error ?? '');
218 + }
219 +
220 + private function substitute_placeholders(string $sql, $params): string
221 + {
222 + if ($params === null || count($params) === 0) {
223 + return $sql;
224 + }
225 +
226 + $positions = [];
227 + $len = strlen($sql);
228 + $in_single = false;
229 + $in_double = false;
230 + $escape_next = false;
231 + for ($i = 0; $i < $len; $i++) {
232 + if ($escape_next) {
233 + $escape_next = false;
234 + continue;
235 + }
236 + $ch = $sql[$i];
237 + if (($in_single || $in_double) && $ch === '\\') {
238 + $escape_next = true;
239 + continue;
240 + }
241 + if ($ch === "'" && !$in_double) {
242 + $in_single = !$in_single;
243 + } elseif ($ch === '"' && !$in_single) {
244 + $in_double = !$in_double;
245 + } elseif ($ch === '?' && !$in_single && !$in_double) {
246 + $positions[] = $i;
247 + }
248 + }
249 +
250 + for ($i = count($positions) - 1; $i >= 0; $i--) {
251 + if (!array_key_exists($i, $params)) {
252 + continue;
253 + }
254 + $quoted = $this->quote_value($params[$i]);
255 + $sql = substr_replace($sql, $quoted, $positions[$i], 1);
256 + }
257 +
258 + foreach ($params as $key => $value) {
259 + if (!is_string($key)) {
260 + continue;
261 + }
262 + $name = ltrim($key, ':');
263 + if ($name === '') {
264 + continue;
265 + }
266 + $pattern = '/:' . preg_quote($name, '/') . '(?![A-Za-z0-9_])/';
267 + $sql = preg_replace_callback(
268 + $pattern,
269 + function () use ($value) {
270 + return $this->quote_value($value);
271 + },
272 + $sql
273 + );
274 + }
275 +
276 + return $sql;
277 + }
278 +
279 + private function quote_value($value): string
280 + {
281 + if ($value === null) {
282 + return 'NULL';
283 + }
284 + return "'" . $this->wpdb->_real_escape((string) $value) . "'";
285 + }
286 +}