PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.1
Jetpack – WP Security, Backup, Speed, & Growth v16.1
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 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 14.4.2 All 500 releases
jetpack / vendor / wp-php-toolkit / data-liberation / EntityReader / class-databaserowsentityreader.php
class-databaserowsentityreader.php
327 lines 7.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WordPress\DataLiberation\EntityReader;
4
5 use PDO;
6 use PDOStatement;
7 use WordPress\DataLiberation\DataLiberationException;
8 use WordPress\DataLiberation\ImportEntity;
9
10 /**
11 * Reads the database rows, one table at a time, from the first row to the last row.
12 * Enables efficiently exporting large databases into an SQL file, but it's not useful
13 * for content dumps – the latter need to emit resources in a "topological content order,"
14 * e.g. post 1, post 1 meta, post 1 categories, post 1 comments, then post 2 (child of 1) etc.
15 *
16 * Note this is just a reader. It doesn't import any data into WordPress. It
17 * only reads rows from the database.
18 *
19 * @since WP_VERSION
20 */
21 class DatabaseRowsEntityReader implements EntityReader {
22
23 /**
24 * State constants for the finite state machine
25 */
26 const STATE_INIT = 'init';
27 const STATE_NEXT_ROW = 'next_row';
28 const STATE_NEXT_TABLE = 'next_table';
29 const STATE_CREATE_TABLE = 'create_table';
30 const STATE_FINISHED = 'finished';
31
32 /**
33 * The database connection used to fetch records.
34 *
35 * @since WP_VERSION
36 * @var PDO
37 */
38 private $db;
39
40 /**
41 * The current entity being processed.
42 *
43 * @since WP_VERSION
44 * @var ImportEntity|null
45 */
46 private $current_entity = null;
47
48 /**
49 * The ID of the last processed record.
50 *
51 * @since WP_VERSION
52 * @var int|null
53 */
54 private $last_record_id = 0;
55
56 /**
57 * The number of entities read so far.
58 *
59 * @since WP_VERSION
60 * @var int
61 */
62 private $entities_read_so_far = 0;
63
64 /**
65 * The current table being processed.
66 *
67 * @since WP_VERSION
68 * @var string|null
69 */
70 private $current_table = null;
71
72 /**
73 * The current query result set.
74 *
75 * @since WP_VERSION
76 * @var PDOStatement|null
77 */
78 private $current_result_set = null;
79
80 /**
81 * The list of tables to process.
82 *
83 * @since WP_VERSION
84 * @var array
85 */
86 private $tables_to_process;
87
88 /**
89 * Whether to export the CREATE TABLE query for each table.
90 *
91 * @since WP_VERSION
92 * @var bool
93 */
94 private $create_table_query;
95
96 /**
97 * The current state of the reader.
98 *
99 * @since WP_VERSION
100 * @var string
101 */
102 private $state = self::STATE_INIT;
103
104 /**
105 * The type of the database.
106 *
107 * One of: "sqlite", "mysql"
108 *
109 * @since WP_VERSION
110 * @var string
111 */
112 private $db_type;
113
114 public static function create( PDO $db, $options = array() ) {
115 return new DatabaseRowsEntityReader( $db, $options );
116 }
117
118 /**
119 * Constructor.
120 *
121 * @param PDO $db The database connection to use.
122 * @param array $options The options to configure the reader.
123 *
124 * @since WP_VERSION
125 */
126 public function __construct( PDO $db, $options = array() ) {
127 $this->db = $db;
128 $this->tables_to_process = $options['tables_to_process'] ?? null;
129 $this->create_table_query = $options['create_table_query'] ?? false;
130 $this->db_type = $db->getAttribute( PDO::ATTR_DRIVER_NAME );
131 if ( ! in_array( $this->db_type, array( 'sqlite', 'mysql' ), true ) ) {
132 throw new DataLiberationException( esc_html( 'Unsupported database type: ' . $this->db_type ) );
133 }
134 if ( isset( $options['cursor'] ) ) {
135 $this->initialize_from_cursor( $options['cursor'] );
136 }
137 }
138
139 /**
140 * Gets the data for the current entity.
141 *
142 * @return ImportEntity The entity.
143 * @since WP_VERSION
144 */
145 public function get_entity(): ImportEntity {
146 return $this->current_entity;
147 }
148
149 /**
150 * Gets the ID of the last processed record.
151 *
152 * @return int|null The record ID, or null if no records have been processed.
153 * @since WP_VERSION
154 */
155 public function get_last_record_id() {
156 return $this->last_record_id;
157 }
158
159 public function is_finished(): bool {
160 return self::STATE_FINISHED === $this->state;
161 }
162
163 /**
164 * Advances to the next entity in the database.
165 *
166 * @return bool Whether another entity was found.
167 * @since WP_VERSION
168 */
169 public function next_entity() {
170 if ( $this->is_finished() ) {
171 return false;
172 }
173
174 if ( self::STATE_INIT === $this->state ) {
175 if ( null === $this->tables_to_process ) {
176 $this->initialize_tables_to_process();
177 }
178 $this->state = self::STATE_NEXT_TABLE;
179 }
180
181 while ( true ) {
182 switch ( $this->state ) {
183 case self::STATE_NEXT_TABLE:
184 if ( $this->move_to_next_table() ) {
185 $this->state = $this->create_table_query ? self::STATE_CREATE_TABLE : self::STATE_NEXT_ROW;
186 } else {
187 $this->state = self::STATE_FINISHED;
188
189 return false;
190 }
191 break;
192
193 case self::STATE_CREATE_TABLE:
194 $this->export_create_table_query();
195 $this->state = self::STATE_NEXT_ROW;
196
197 return true;
198
199 case self::STATE_NEXT_ROW:
200 if ( $this->read_next_entity() ) {
201 return true;
202 }
203 $this->state = self::STATE_NEXT_TABLE;
204
205 return $this->next_entity();
206
207 case self::STATE_FINISHED:
208 return false;
209 }
210 }
211
212 return false;
213 }
214
215 /**
216 * Advances to the next entity in the current table.
217 *
218 * @return bool Whether another entity was found.
219 * @since WP_VERSION
220 */
221 private function read_next_entity() {
222 if ( ! $this->current_result_set ) {
223 $this->current_result_set = $this->db->query( "SELECT * FROM {$this->current_table} WHERE ID > {$this->last_record_id}" );
224 }
225
226 $record = $this->current_result_set->fetch( PDO::FETCH_ASSOC );
227 if ( ! $record ) {
228 $this->current_result_set = null;
229
230 return false;
231 }
232
233 $this->current_entity = new ImportEntity(
234 'database_row',
235 array(
236 'table' => $this->current_table,
237 'record' => $record,
238 )
239 );
240 $this->last_record_id = $record['ID'] ?? null;
241 ++$this->entities_read_so_far;
242
243 return true;
244 }
245
246 /**
247 * Moves to the next table in the list of tables to process.
248 *
249 * @return bool Whether there is another table to process.
250 * @since WP_VERSION
251 */
252 private function move_to_next_table() {
253 if ( ! $this->current_table ) {
254 $this->current_table = reset( $this->tables_to_process );
255 } else {
256 $this->current_table = next( $this->tables_to_process );
257 }
258 $this->last_record_id = 0;
259
260 return (bool) $this->current_table;
261 }
262
263 /**
264 * Exports the CREATE TABLE query for the current table.
265 *
266 * @since WP_VERSION
267 */
268 private function export_create_table_query() {
269 switch ( $this->db_type ) {
270 case 'sqlite':
271 $result = $this->db->query( "SELECT sql FROM sqlite_master WHERE type='table' AND name='{$this->current_table}'" );
272 $row = $result->fetch( PDO::FETCH_ASSOC );
273 $sql = $row ? $row['sql'] . ';' : null;
274 break;
275 case 'mysql':
276 $result = $this->db->query( "SHOW CREATE TABLE {$this->current_table}" );
277 $row = $result->fetch( PDO::FETCH_ASSOC );
278 $sql = $row ? $row['Create Table'] : null;
279 break;
280 }
281
282 $this->current_entity = new ImportEntity( 'sql_query', $sql );
283 ++$this->entities_read_so_far;
284 }
285
286 /**
287 * Initializes the list of tables to process by fetching all tables from the database
288 * and sorting them alphabetically.
289 *
290 * @since WP_VERSION
291 */
292 private function initialize_tables_to_process() {
293 $this->tables_to_process = array();
294 $result = $this->db->query( 'SHOW TABLES' );
295 while ( $row = $result->fetch( PDO::FETCH_NUM ) ) { // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
296 $this->tables_to_process[] = $row[0];
297 }
298 sort( $this->tables_to_process );
299 }
300
301 public function get_reentrancy_cursor() {
302 return json_encode(
303 array(
304 'last_record_id' => $this->last_record_id,
305 'current_table' => $this->current_table,
306 'state' => $this->state,
307 )
308 );
309 }
310
311 /**
312 * Initializes the reader from a cursor.
313 *
314 * @param string $cursor The cursor to initialize from.
315 *
316 * @since WP_VERSION
317 */
318 private function initialize_from_cursor( $cursor ) {
319 $cursor_data = json_decode( $cursor, true );
320 if ( $cursor_data ) {
321 $this->last_record_id = $cursor_data['last_record_id'] ?? null;
322 $this->current_table = $cursor_data['current_table'] ?? null;
323 $this->state = $cursor_data['state'] ?? self::STATE_INIT;
324 }
325 }
326 }
327