· 5 years ago · Mar 27, 2020, 12:26 PM
1
2/**
3 * @param string $root
4 * @param ClonerDBConn $conn
5 * @param int $timeout
6 * @param ClonerDBImportState $state
7 * @param int $maxCount
8 * @param ClonerImportFilter[] $filters
9 *
10 * @return ClonerDBImportState New state.
11 *
12 * @throws ClonerException
13 * @throws ClonerFSFunctionException
14 */
15function cloner_import_database($root, ClonerDBConn $conn, $timeout, ClonerDBImportState $state, $maxCount, array $filters)
16{
17 clearstatcache();
18 $maxPacket = $realMaxPacket = 0;
19 $firstRun = true;
20 foreach ($state->files as $file) {
21 if ($file->processed > 0) {
22 $firstRun = false;
23 break;
24 }
25 }
26 if ($firstRun) {
27 try {
28 cloner_clear_database_processlist($conn);
29 } catch (ClonerException $e) {
30 trigger_error($e->getMessage());
31 }
32 }
33 if (is_array($maxPacketResult = $conn->query("SHOW VARIABLES LIKE 'max_allowed_packet'")->fetch())) {
34 $maxPacket = $realMaxPacket = (int)end($maxPacketResult);
35 }
36 if (!$maxPacket) {
37 $maxPacket = 128 << 10;
38 } elseif ($maxPacket > 512 << 10) {
39 $maxPacket = 512 << 10;
40 }
41 $deadline = new ClonerDeadline($timeout);
42 $shifts = 0;
43 while (($dump = $state->next()) !== null) {
44 if (strlen($dump->encoding)) {
45 $conn->execute("SET NAMES {$dump->encoding}");
46 }
47 $stat = cloner_fs_stat("$root/$dump->path");
48 if ($stat->getSize() !== $dump->size) {
49 throw new ClonerException(sprintf("Inconsistent table dump file size, file %s transferred %d bytes, but on the disk it's %d bytes", $dump->path, $dump->size, $stat->getSize()), "different_size");
50 }
51 $scanner = new ClonerDBDumpScanner("$root/$dump->path");
52 if ($dump->processed !== 0) {
53 $scanner->seek($dump->processed);
54 }
55 $charsetFixer = new ClonerDBCharsetFixer($conn);
56 while (strlen($statements = $scanner->scan($maxCount, $maxPacket))) {
57 if ($realMaxPacket && strlen($statements) + 20 > $realMaxPacket) {
58 throw new ClonerException(sprintf("A query in the backup (%d bytes) is too big for the SQL server to process (max %d bytes); please set the server's variable 'max_allowed_packet' to at least %d and retry the process", strlen($statements), $realMaxPacket, strlen($statements) + 20), 'db_max_packet_size_reached', strlen($statements));
59 }
60 try {
61 $statements = cloner_filter_statement($statements, $filters);
62 $conn->execute($statements);
63 $shifts = 0;
64 if (strncmp($statements, 'DROP TABLE IF EXISTS ', 21) === 0) {
65 $state->pushNextToEnd();
66 // We just dropped a table; switch to next file if available.
67 // This way we will drop all tables before importing new data.
68 // That helps with foreign key constraints.
69 break;
70 }
71 } catch (ClonerException $e) {
72 // Super-powerful recovery switch, un-document it to secure your job.
73 switch ($e->getInternalError()) {
74 case "1005": // SQLSTATE[HY000]: General error: 1005 Can't create table 'dbname.wp_wlm_email_queue' (errno: 150)
75 // This looks like an issue specific to InnoDB storage engine.
76 case "1451": // SQLSTATE[23000]: Integrity constraint violation: 1451 Cannot delete or update a parent row: a foreign key constraint fails
77 // For "DROP TABLE IF EXISTS..." queries. Sometimes they DO exist.
78 case "1217": // Cannot delete or update a parent row: a foreign key constraint fails
79 // @todo we could drop keys before dropping the database, but we would have to parse SQL :/
80 case "1146": // Table '%s' doesn't exist
81 case "1215": // Cannot add foreign key constraint
82 // Possible table reference error, we should suspend this import and go to next file.
83 // Push the currently imported file to end if and only if we're certain that the number of pushes
84 // without a successful statement execution doesn't exceed the number of files being imported;
85 // that would mean that we rotated all the files and would enter an infinite loop.
86 if ($shifts + 1 < count($state->files)) {
87 // Switch to next file.
88 $state->pushNextToEnd();
89 $scanner->close();
90 $shifts++;
91 continue 3;
92 }
93 throw new ClonerException(cloner_format_query_error($e->getMessage(), $statements, $dump->path, $dump->processed, $scanner->tell(), $dump->size), 'db_query_error', $e->getInternalError());
94 case "1115":
95 case "1273":
96 $newStatements = preg_replace_callback('{utf8mb4[a-z0-9_]*}', array($charsetFixer, 'replaceCharsetOrCollation'), $statements, -1, $count);
97 if ($count) {
98 try {
99 $conn->execute($newStatements);
100 break;
101 } catch (ClonerException $e2) {
102 }
103 }
104 throw new ClonerException(cloner_format_query_error($e->getMessage(), $statements, $dump->path, $dump->processed, $scanner->tell(), $dump->size), 'db_query_error', $e->getInternalError());
105 case "2013":
106 // 2013 Lost connection to MySQL server during query
107 case "2006":
108 // 2006 MySQL server has gone away
109 case "1153":
110 // SQLSTATE[08S01]: Communication link failure: 1153 Got a packet bigger than 'max_allowed_packet' bytes
111 $attempt = 1;
112 $maxAttempts = 4;
113 while (++$attempt <= $maxAttempts) {
114 usleep(100000 * pow($attempt, 2));
115 try {
116 $conn = cloner_db_connection($conn->getConfiguration(), true);
117 if ($realMaxPacket && (strlen($statements) * 1.2) > $realMaxPacket) {
118 // We are certain that the packet size is too big.
119 $conn->execute(sprintf("SET GLOBAL max_allowed_packet=%d", strlen($statements) + 1024 * 1024));
120 }
121 $conn->execute($statements);
122 break 2;
123 } catch (Exception $e2) {
124 trigger_error(sprintf('Could not increase max_allowed_packet: %s for file %s at offset %d', $e2->getMessage(), $dump->path, $scanner->tell()));
125 }
126 }
127 // We aren't certain of what happened here. Maybe reconnect once?
128 throw new ClonerException(cloner_format_query_error($e->getMessage(), $statements, $dump->path, $dump->processed, $scanner->tell(), $dump->size), 'db_query_error', $e->getInternalError());
129 case "1231":
130 // Ignore errors like this:
131 // SQLSTATE[42000]: Syntax error or access violation: 1231 Variable 'character_set_client' can't be set to the value of 'NULL'
132 // We don't save the SQL variable state between imports since we only care about the relevant ones (encoding, timezone).
133 break;
134 //case 1065:
135 // Ignore error "[1065] Query was empty"
136 // break;
137 case "1067": // SQLSTATE[42000]: Syntax error or access violation: 1067 Invalid default value for 'access_granted'
138 // Most probably NO_ZERO_DATE is ON and the default value is something like 0000-00-00.
139 $currentMode = $conn->query("SELECT @@sql_mode")->fetch();
140 $currentMode = @end($currentMode);
141 if (strlen($currentMode)) {
142 $modes = explode(',', $currentMode);
143 $removeModes = array('NO_ZERO_DATE', 'NO_ZERO_IN_DATE');
144 foreach ($modes as $i => $mode) {
145 if (!in_array($mode, $removeModes)) {
146 continue;
147 }
148 unset($modes[$i]);
149 }
150 $newMode = implode(',', $modes);
151 try {
152 $conn->execute("SET SESSION sql_mode = '$newMode'");
153 $conn->execute($statements);
154 // Recovered.
155 break;
156 } catch (Exception $e2) {
157 trigger_error($e2->getMessage());
158 }
159 }
160 throw new ClonerException(cloner_format_query_error($e->getMessage(), $statements, $dump->path, $dump->processed, $scanner->tell(), $dump->size), 'db_query_error', $e->getInternalError());
161 case "1064":
162 // MariaDB compatibility cases.
163 // This is regarding the PAGE_CHECKSUM property.
164 case "1286":
165 // ... and this is regarding the unknown storage engine, e.g.:
166 // CREATE TABLE `name` ( ... ) ENGINE=Aria DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci PAGE_CHECKSUM=1;
167 // results in
168 // SQLSTATE[42000]: Syntax error or access violation: 1286 Unknown storage engine 'Aria'
169 if (strpos($statements, 'PAGE_CHECKSUM') !== false) {
170 // MariaDB's CREATE TABLE statement has some options
171 // that MySQL doesn't recognize.
172 $conn->query(strtr($statements, array(
173 ' ENGINE=Aria ' => ' ENGINE=MyISAM ',
174 ' PAGE_CHECKSUM=1' => '',
175 ' PAGE_CHECKSUM=0' => '',
176 )));
177 break;
178 }
179 throw new ClonerException(cloner_format_query_error($e->getMessage(), $statements, $dump->path, $dump->processed, $scanner->tell(), $dump->size), 'db_query_error', $e->getInternalError());
180 case "1298":
181 // 1298 Unknown or incorrect time zone
182 break;
183 case "1419":
184 // Triggers require super-user permissions.
185 //
186 // Query:
187 // /*!50003 CREATE*/ /*!50003 TRIGGER wp_hmenu_mega_list BEFORE UPDATE ON wp_hmenu_mega_list FOR EACH ROW SET NEW.lastModified = NOW() */;
188 //
189 // Error:
190 // SQLSTATE[HY000]: General error: 1419 You do not have the SUPER privilege and binary logging is enabled (you *might* want to use the less safe log_bin_trust_function_creators variable)
191 $state->skipStatement($statements);
192 break;
193 case "1227":
194 if (strncmp($statements, 'SET @@SESSION.', 14) === 0 || strncmp($statements, 'SET @@GLOBAL.', 13) === 0) {
195 // SET @@SESSION.SQL_LOG_BIN= 0;
196 // SET @@GLOBAL.GTID_PURGED='';
197 break;
198 }
199 // Remove strings like DEFINER=`user`@`localhost`, because they generate errors like this:
200 // "[1227] Access denied; you need (at least one of) the SUPER privilege(s) for this operation"
201 // Example of a problematic query:
202 //
203 // /*!50003 CREATE*/ /*!50017 DEFINER=`user`@`localhost`*/ /*!50003 TRIGGER `wp_hlogin_default_storage_table` BEFORE UPDATE ON `wp_hlogin_default_storage_table`
204 $newStatements = preg_replace('{(/\*!\d+) DEFINER=`[^`]+`@`[^`]+`(\*/ )}', '', $statements, 1, $count);
205 if ($count) {
206 try {
207 $conn->execute($newStatements);
208 break;
209 } catch (ClonerException $e) {
210 }
211 }
212 throw new ClonerException(cloner_format_query_error($e->getMessage(), $statements, $dump->path, $dump->processed, $scanner->tell(), $dump->size), 'db_query_error', $e->getInternalError());
213 case "3167":
214 if (strpos($statements, '@is_rocksdb_supported') !== false) {
215 // RocksDB support handling for the following case:
216 //
217 // /*!50112 SELECT COUNT(*) INTO @is_rocksdb_supported FROM INFORMATION_SCHEMA.SESSION_VARIABLES WHERE VARIABLE_NAME='rocksdb_bulk_load' */;
218 // /*!50112 SET @save_old_rocksdb_bulk_load = IF (@is_rocksdb_supported, 'SET @old_rocksdb_bulk_load = @@rocksdb_bulk_load', 'SET @dummy_old_rocksdb_bulk_load = 0') */;
219 // /*!50112 PREPARE s FROM @save_old_rocksdb_bulk_load */;
220 // /*!50112 EXECUTE s */;
221 // /*!50112 SET @enable_bulk_load = IF (@is_rocksdb_supported, 'SET SESSION rocksdb_bulk_load = 1', 'SET @dummy_rocksdb_bulk_load = 0') */;
222 // /*!50112 PREPARE s FROM @enable_bulk_load */;
223 // /*!50112 EXECUTE s */;
224 // /*!50112 DEALLOCATE PREPARE s */;
225 // ... table creation and insert statements ...
226 // /*!50112 SET @disable_bulk_load = IF (@is_rocksdb_supported, 'SET SESSION rocksdb_bulk_load = @old_rocksdb_bulk_load', 'SET @dummy_rocksdb_bulk_load = 0') */;
227 // /*!50112 PREPARE s FROM @disable_bulk_load */;
228 // /*!50112 EXECUTE s */;
229 // /*!50112 DEALLOCATE PREPARE s */;
230 //
231 // Error on the first statement:
232 // #3167 - The 'INFORMATION_SCHEMA.SESSION_VARIABLES' feature is disabled; see the documentation for 'show_compatibility_56'
233 try {
234 $conn->execute('SET @is_rocksdb_supported = 0');
235 } catch (ClonerException $e2) {
236 throw new ClonerException(cloner_format_query_error('Could not recover from RocksDB support patch: '.$e2->getMessage(), $statements, $dump->path, $dump->processed, $scanner->tell(), $dump->size), 'db_query_error', $e2->getInternalError());
237 }
238 break;
239 }
240 throw new ClonerException(cloner_format_query_error($e->getMessage(), $statements, $dump->path, $dump->processed, $scanner->tell(), $dump->size), 'db_query_error', $e->getInternalError());
241 default:
242 throw new ClonerException(cloner_format_query_error($e->getMessage(), $statements, $dump->path, $dump->processed, $scanner->tell(), $dump->size), 'db_query_error', $e->getInternalError());
243 }
244 }
245 $dump->processed = $scanner->tell();
246 if ($deadline->done()) {
247 // If there are any locked tables we might hang forever with the next query, unlock them.
248 $conn->execute("UNLOCK TABLES");
249 // We're cutting the import here - remember the encoding!!!
250 $charset = $conn->query("SHOW VARIABLES LIKE 'character_set_client'")->fetch();
251 $dump->encoding = (string)end($charset);
252 break 2;
253 }
254 }
255 $dump->processed = $scanner->tell();
256 $scanner->close();
257 }
258
259 return $state;
260}
261
262/**
263 * @param string $statement
264 * @param ClonerImportFilter[] $filters
265 *
266 * @return string
267 *
268 * @throws ClonerException
269 */
270function cloner_filter_statement($statement, array $filters)
271{
272 foreach ($filters as $filter) {
273 $statement = $filter->filter($statement);
274 }
275 return $statement;
276}
277
278
279
280
281
282class ClonerAction
283{
284 public $id = '';
285 public $action = '';
286 public $params;
287
288 /**
289 * @param string $name
290 * @param mixed $params
291 *
292 * @see cloner_run_action
293 */
294 public function __construct($name, $params)
295 {
296 $this->id = md5(uniqid('', true));
297 $this->action = $name;
298 $this->params = $params;
299 }
300}
301
302/**
303 * @param ClonerURL $url
304 * @param ClonerAction $action
305 *
306 * @return array
307 *
308 * @throws ClonerActionException
309 */
310function cloner_send_action(ClonerURL $url, ClonerAction $action)
311{
312 if (isset($_SERVER['HTTP_USER_AGENT']) && is_string($_SERVER['HTTP_USER_AGENT'])) {
313 $ua = $_SERVER['HTTP_USER_AGENT'];
314 } else {
315 $ua = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36';
316 }
317 $retried = false;
318 while (true) {
319 try {
320 $payload = cloner_base64_rotate(json_encode($action));
321 $req = cloner_http_open_request('POST', $url, array(
322 'Content-Type' => 'application/json',
323 'Content-Length' => strlen($payload),
324 'Connection' => 'close',
325 'Host' => $url->getHTTPHost(),
326 // Imitate a standard browser request.
327 'User-Agent' => $ua,
328 'Referer' => $url->__toString(),
329 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
330 'Accept-Language' => 'en-US,en;q=0.9,sr;q=0.8,bs;q=0.7',
331 ), 10);
332 socket_set_timeout($req, 10);
333 if (@fwrite($req, $payload) === false) {
334 throw new ClonerNetSocketException('fwrite', $req);
335 }
336 $res = cloner_http_get_response_headers($req, 120);
337 $result = null;
338 $body = $res->read(120);
339 $offset = 0;
340 while ($offset < strlen($body)) {
341 $lineEnd = strpos($body, "\n", $offset);
342 if ($lineEnd === false) {
343 $lineEnd = strlen($body);
344 } else {
345 // Capture \n
346 $lineEnd++;
347 }
348 $line = substr($body, $offset, $lineEnd - $offset);
349 $offset += $lineEnd;
350 if (strncmp('{"', $line, 2) !== 0) {
351 continue;
352 }
353 $result = json_decode($line, true);
354 if (empty($result['id']) || $result['id'] !== $action->id) {
355 $result = null;
356 continue;
357 }
358 break;
359 }
360 @fclose($res->body);
361 if (!isset($result)) {
362 throw new ClonerActionResultNotFoundException($res->statusCode, $res->status, $res->headers, $body);
363 }
364
365 // See cloner_send_success_response/cloner_send_error_response for expected structure.
366 if (isset($result['error']['error'], $result['error']['file'], $result['error']['line'])) {
367 $message = sprintf('%s in %s:%d', $result['error']['message'], $result['error']['file'], $result['error']['line']);
368 throw new ClonerRemoteErrorException($message, $result['error']['error'], $result['error']['internalError']);
369 }
370 } catch (ClonerException $e) {
371 if (!$retried) {
372 $retried = true;
373 try {
374 cloner_http_do('GET', $url->__toString(), '', '', 20);
375 } catch (Exception $e2) {
376 trigger_error('GET request after failed POST action failed: '.$e2->getMessage());
377 throw new ClonerActionException($action->action, $url->__toString(), $e);
378 }
379 // Retry initial request.
380 continue;
381 }
382 throw new ClonerActionException($action->action, $url->__toString(), $e);
383 }
384 break;
385 }
386 /** @noinspection PhpUndefinedVariableInspection */
387 return $result['result'];
388}
389
390class ClonerRemoteErrorException extends ClonerException
391{
392 public function __construct($message, $code, $internalError)
393 {
394 if (strlen($internalError)) {
395 $internalError = '; internal error: '.$internalError;
396 }
397 parent::__construct(sprintf('error code %s: %s%s', $code, $message, $internalError), 'remote_fatal_error');
398 }
399}
400
401class ClonerActionException extends ClonerException
402{
403 public $action = '';
404 public $target = '';
405 public $error;
406
407 public function __construct($action, $target, Exception $exception)
408 {
409 $this->action = $action;
410 $this->target = $target;
411 $this->error = $exception->getMessage();
412 parent::__construct(sprintf('action %s->%s failed: %s', $target, $action, $exception->getMessage()), 'action_error');
413 }
414}
415
416class ClonerActionResultNotFoundException extends ClonerException
417{
418 public $action = '';
419 public $url = '';
420 public $statusCode = 0;
421 public $status = '';
422 public $headers = array();
423 public $body = '';
424
425 /**
426 * @param int $statusCode
427 * @param string $status
428 * @param array $headers
429 * @param string $body
430 */
431 public function __construct($statusCode, $status, array $headers, $body)
432 {
433 $this->statusCode = $statusCode;
434 $this->status = $status;
435 $this->headers = $headers;
436 $this->body = $body;
437 $excerpt = trim(substr(preg_replace('{\s+}', ' ', strip_tags($body)), 0, 1024));
438 $message = sprintf('result not found, got status "%d %s"; excerpt: "%s"', $statusCode, $status, $excerpt);
439 parent::__construct($message, 'reaction_not_found');
440 }
441}
442
443
444
445
446class ClonerNetException extends ClonerException
447{
448}
449
450/**
451 * @param string $peerName
452 * @param string $cert
453 *
454 * @return array
455 * @throws ClonerNoTransportStreamsException
456 * @throws ClonerFSFunctionException
457 */
458function cloner_tls_transport_self_signed($peerName, $cert)
459{
460 static $transport, $certPath;
461
462 $available = stream_get_transports();
463 $attempted = array('ssl', 'tls', 'tlsv1.2', 'tlsv1.1', 'tlsv1.0');
464 if (!$transport) {
465 foreach ($attempted as $attempt) {
466 $index = array_search($attempt, $available);
467 if ($index !== false) {
468 $transport = $available[$index];
469 break;
470 }
471 }
472 }
473 if (!$transport) {
474 throw new ClonerNoTransportStreamsException($available, $attempted);
475 }
476 if (!$certPath) {
477 $certHash = md5($cert);
478 $tempPath = sys_get_temp_dir().'/cloner-cert-'.$certHash;
479 if (!file_exists($tempPath) || @md5_file($tempPath) !== $certHash) {
480 if (!file_put_contents($tempPath, $cert)) {
481 throw new ClonerFSFunctionException('file_put_contents', $tempPath);
482 }
483 }
484 $certPath = $tempPath;
485 }
486
487 // Temporarily disable SSL peer check.
488 $ctx = stream_context_create(array('ssl' => array(
489 'allow_self_signed' => true,
490 'CN_match' => $peerName,
491 'verify_peer' => true,
492 'SNI_enabled' => true,
493 'SNI_server_name' => $peerName,
494 'peer_name' => $peerName,
495 'cafile' => $certPath,
496 )));
497 return array($transport, $ctx);
498}
499
500/**
501 * @param string $peerName Peer name to verify.
502 *
503 * @return array Transport stream to use and initialized context.
504 *
505 * @throws ClonerNoTransportStreamsException
506 */
507function cloner_tls_transport($peerName = '')
508{
509 static $transport;
510
511 $available = stream_get_transports();
512 $attempted = array('ssl', 'tls', 'tlsv1.2', 'tlsv1.1', 'tlsv1.0');
513 foreach ($attempted as $attempt) {
514 $index = array_search($attempt, $available);
515 if ($index !== false) {
516 $transport = $available[$index];
517 break;
518 }
519 }
520 if ($transport === null) {
521 throw new ClonerNoTransportStreamsException($available, $attempted);
522 }
523
524 // Temporarily disable SSL peer check.
525 $ctx = stream_context_create(array('ssl' => array(
526 'verify_peer' => false,
527 'verify_peer_name' => false,
528 'allow_self_signed' => true,
529 )));
530 return array($transport, $ctx);
531
532 $cachedCertsPath = sys_get_temp_dir().'/managewp-worker-v2.crt';
533 $tlsOptions = array(
534 'verify_peer' => true,
535 'verify_peer_name' => true,
536 'allow_self_signed' => false,
537 // Attempt system's CAFILE.
538 );
539 if (is_file($cachedCertsPath)) {
540 $tlsOptions['cafile'] = $cachedCertsPath;
541 }
542 if (strlen($peerName)) {
543 if (PHP_VERSION_ID >= 50600) {
544 $tlsOptions['peer_name'] = $peerName;
545 } else {
546 $tlsOptions['CN_match'] = $peerName;
547 }
548 }
549
550 $ctx = stream_context_create(array('ssl' => $tlsOptions));
551
552 if ($transport !== null) {
553 return array($transport, $ctx);
554 }
555}
556
557/**
558 * Certificates used to fetch latest certificates from https://curl.haxx.se/ca/cacert.pem
559 * when the system is missing them.
560 *
561 * @return resource
562 * @throws Exception
563 */
564function cloner_tls_transport_context_curl()
565{
566 // Respectively:
567 // - From curl.haxx.se:
568 // /C=US/ST=California/L=San Francisco/O=Fastly, Inc./CN=c.sni.fastly.net
569 // /C=BE/O=GlobalSign nv-sa/CN=GlobalSign Organization Validation CA - SHA256 - G2
570 // - From the cacert.pem itself:
571 // /CN=GlobalSign Root CA
572 $certs = <<<CRT
573-----BEGIN CERTIFICATE-----
574MIIFOzCCBCOgAwIBAgIMO4pgQymgER+m0k6OMA0GCSqGSIb3DQEBCwUAMGYxCzAJ
575BgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMTwwOgYDVQQDEzNH
576bG9iYWxTaWduIE9yZ2FuaXphdGlvbiBWYWxpZGF0aW9uIENBIC0gU0hBMjU2IC0g
577RzIwHhcNMTcwMjA3MjI0MTA0WhcNMTkwMjA4MjI0MTA0WjBsMQswCQYDVQQGEwJV
578UzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEV
579MBMGA1UEChMMRmFzdGx5LCBJbmMuMRkwFwYDVQQDExBjLnNuaS5mYXN0bHkubmV0
580MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApbUevfFREAfvUH18oW27
581BVLbkWJnbZ69dQCCchcuaXJ8Jq/I6plgKwW2yWUG/ynp7dp+0BwoWnzbiQHqZTsW
5826Pqf0le2uENc8sSxLyILATG2Ct/s36XxXNfuH8388uOfiVvwoEAoDBD1VEXcI/4r
583ei2KGwVx8PGLb60jitLDPLYOXW/kMu+WNg/+btjJ4khs30UeHh10UUrXjuRO3iga
5846hOgKpvbkX03nfHH/+zc+sDfJerH0bmTwvZLwWupRW5x65hx2O2voUVbb27nnbqZ
585zR57FZATEHyvqQghHsHjI8bwBI1azDuCz7vXIIKyYkXNO1eRrXzT46Tx7mHtanOL
586bwIDAQABo4IB4TCCAd0wDgYDVR0PAQH/BAQDAgWgMIGgBggrBgEFBQcBAQSBkzCB
587kDBNBggrBgEFBQcwAoZBaHR0cDovL3NlY3VyZS5nbG9iYWxzaWduLmNvbS9jYWNl
588cnQvZ3Nvcmdhbml6YXRpb252YWxzaGEyZzJyMS5jcnQwPwYIKwYBBQUHMAGGM2h0
589dHA6Ly9vY3NwMi5nbG9iYWxzaWduLmNvbS9nc29yZ2FuaXphdGlvbnZhbHNoYTJn
590MjBWBgNVHSAETzBNMEEGCSsGAQQBoDIBFDA0MDIGCCsGAQUFBwIBFiZodHRwczov
591L3d3dy5nbG9iYWxzaWduLmNvbS9yZXBvc2l0b3J5LzAIBgZngQwBAgIwCQYDVR0T
592BAIwADBJBgNVHR8EQjBAMD6gPKA6hjhodHRwOi8vY3JsLmdsb2JhbHNpZ24uY29t
593L2dzL2dzb3JnYW5pemF0aW9udmFsc2hhMmcyLmNybDAbBgNVHREEFDASghBjLnNu
594aS5mYXN0bHkubmV0MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNV
595HQ4EFgQUtKi05Nur72AEab/ueagsP+smrGAwHwYDVR0jBBgwFoAUlt5h8b0cFilT
596HMDMfTuDAEDmGnwwDQYJKoZIhvcNAQELBQADggEBAIO4QcnKsWyMvfjZj4QMg1ao
59731XuY7jRiG2/a+S39JYEIS+16GXiTRfKJMk5dNKK30kRU+uPxBal5HS/i43ZRmY2
5980iQG/tMLoVoTPUzxbgiIvgFIvjNG6vefiza+C83AY1Vz8HOcAAE3AM7efqYo0XdV
599xlvOkdinqGDwERkZyKQ4mIDqEeU6wPHLTKf+wLnqcYxyeA4DK6Cd7v0NHMBm02L2
600ZMf8iW1OZSy+uKswqSIedmmyko/tuO6gNA7Zs/pS5rjs6VH0OE6TlMIxvH/w0dj7
601n8F/e1mhjp73CMV77MAIyxnnorM/Z58reWF/VGgOU89y4OdUugHIZ4F7fDTfpTU=
602-----END CERTIFICATE-----
603-----BEGIN CERTIFICATE-----
604MIIEaTCCA1GgAwIBAgILBAAAAAABRE7wQkcwDQYJKoZIhvcNAQELBQAwVzELMAkG
605A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv
606b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw0xNDAyMjAxMDAw
607MDBaFw0yNDAyMjAxMDAwMDBaMGYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i
608YWxTaWduIG52LXNhMTwwOgYDVQQDEzNHbG9iYWxTaWduIE9yZ2FuaXphdGlvbiBW
609YWxpZGF0aW9uIENBIC0gU0hBMjU2IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IB
610DwAwggEKAoIBAQDHDmw/I5N/zHClnSDDDlM/fsBOwphJykfVI+8DNIV0yKMCLkZc
611C33JiJ1Pi/D4nGyMVTXbv/Kz6vvjVudKRtkTIso21ZvBqOOWQ5PyDLzm+ebomchj
612SHh/VzZpGhkdWtHUfcKc1H/hgBKueuqI6lfYygoKOhJJomIZeg0k9zfrtHOSewUj
613mxK1zusp36QUArkBpdSmnENkiN74fv7j9R7l/tyjqORmMdlMJekYuYlZCa7pnRxt
614Nw9KHjUgKOKv1CGLAcRFrW4rY6uSa2EKTSDtc7p8zv4WtdufgPDWi2zZCHlKT3hl
6152pK8vjX5s8T5J4BO/5ZS5gIg4Qdz6V0rvbLxAgMBAAGjggElMIIBITAOBgNVHQ8B
616Af8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUlt5h8b0cFilT
617HMDMfTuDAEDmGnwwRwYDVR0gBEAwPjA8BgRVHSAAMDQwMgYIKwYBBQUHAgEWJmh0
618dHBzOi8vd3d3Lmdsb2JhbHNpZ24uY29tL3JlcG9zaXRvcnkvMDMGA1UdHwQsMCow
619KKAmoCSGImh0dHA6Ly9jcmwuZ2xvYmFsc2lnbi5uZXQvcm9vdC5jcmwwPQYIKwYB
620BQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwOi8vb2NzcC5nbG9iYWxzaWduLmNv
621bS9yb290cjEwHwYDVR0jBBgwFoAUYHtmGkUNl8qJUC99BM00qP/8/UswDQYJKoZI
622hvcNAQELBQADggEBAEYq7l69rgFgNzERhnF0tkZJyBAW/i9iIxerH4f4gu3K3w4s
62332R1juUYcqeMOovJrKV3UPfvnqTgoI8UV6MqX+x+bRDmuo2wCId2Dkyy2VG7EQLy
624XN0cvfNVlg/UBsD84iOKJHDTu/B5GqdhcIOKrwbFINihY9Bsrk8y1658GEV1BSl3
62530JAZGSGvip2CTFvHST0mdCF/vIhCPnG9vHQWe3WVjwIKANnuvD58ZAWR65n5ryA
626SOlCdjSXVWkkDoPWoC209fN5ikkodBpBocLTJIg1MGCUF7ThBCIxPTsvFwayuJ2G
627K1pp74P1S8SqtCr4fKGxhZSM9AyHDPSsQPhZSZg=
628-----END CERTIFICATE-----
629-----BEGIN CERTIFICATE-----
630MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG
631A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv
632b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw
633MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i
634YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT
635aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ
636jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp
637xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp
6381Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG
639snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ
640U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8
6419iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E
642BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B
643AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz
644yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE
64538NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP
646AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad
647DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME
648HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A==
649-----END CERTIFICATE-----
650CRT;
651
652 $certsPath = sys_get_temp_dir().'/managewp-curl.crt';
653 if (@filesize($certsPath) !== strlen($certs)) {
654 if (@file_put_contents($certsPath, $certs) === false) {
655 throw new ClonerFSFunctionException('file_put_contents', $certsPath);
656 }
657 }
658
659 return stream_context_create(array(
660 'ssl' => array(
661 'verify_peer' => true,
662 'verify_peer_name' => true,
663 'allow_self_signed' => false,
664 'cafile' => $certsPath,
665 ),
666 ));
667}
668
669/**
670 * Certificates used to contact managewp.com, godaddy.com, managewp.test
671 *
672 * @param string $peerName Peer name to verify.
673 *
674 * @return resource
675 * @throws ClonerFSFunctionException
676 * @throws ClonerNetException
677 * @throws ClonerURLException
678 */
679function cloner_tls_transport_context_fallback($peerName = '')
680{
681 $certsPath = sys_get_temp_dir().'/managewp-worker-v2.crt';
682 if (!file_exists($certsPath)) {
683 $certs = cloner_http_do('GET', ClonerURL::fromString('https://curl.haxx.se/ca/cacert.pem'));
684
685 // Append managewp.test certificate:
686 // /C=RS/ST=Serbia/L=Belgrade/O=GoDaddy LLC/OU=ManageWP/CN=managewp.test/emailAddress=devops@managewp.test
687 $certs .= <<<CRT
688
689-----BEGIN CERTIFICATE-----
690MIIDrDCCApQCCQD3rCnOu1cdeTANBgkqhkiG9w0BAQUFADCBlzELMAkGA1UEBhMC
691UlMxDzANBgNVBAgMBlNlcmJpYTERMA8GA1UEBwwIQmVsZ3JhZGUxFDASBgNVBAoM
692C0dvRGFkZHkgTExDMREwDwYDVQQLDAhNYW5hZ2VXUDEWMBQGA1UEAwwNbWFuYWdl
693d3AudGVzdDEjMCEGCSqGSIb3DQEJARYUZGV2b3BzQG1hbmFnZXdwLnRlc3QwHhcN
694MTgwMTA5MDk1NjI4WhcNMjgwMTA3MDk1NjI4WjCBlzELMAkGA1UEBhMCUlMxDzAN
695BgNVBAgMBlNlcmJpYTERMA8GA1UEBwwIQmVsZ3JhZGUxFDASBgNVBAoMC0dvRGFk
696ZHkgTExDMREwDwYDVQQLDAhNYW5hZ2VXUDEWMBQGA1UEAwwNbWFuYWdld3AudGVz
697dDEjMCEGCSqGSIb3DQEJARYUZGV2b3BzQG1hbmFnZXdwLnRlc3QwggEiMA0GCSqG
698SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDj8dWERZXoFV2uzQodgAwj5yCfR6fK6gAU
699hc86TYHyFIBAqq5GEsUW48svmjKAlg2PydTu5/Uld1Q73VYR3eX5dDxRGwIVwfnI
700TdCsEmseCFidr24BLZzdxO3cc0m/iGGLlcQSF47d4kD9Qcu6F+hzkv4zTRSH6aY+
701kSD5i1aIzapUiQOroD5sfQZP1fe1N0CLuqKvpT5LDPqnz6/RaItqmsJL6sZaS01d
702wrBNLvU3M4flZzkILJ7t97Xamdwjr9qzyEJZTaSKBR7dhy5kHa8jZoJzvm2ym02j
703SvmyXI9og7v63PjRCYQOZdnohR8/y/aDX1nyuRnSNOGB+Y2dwXrXAgMBAAEwDQYJ
704KoZIhvcNAQEFBQADggEBAAqDHAUZXgYci3h9sUNwDcTnHPEWmcY+oC+vBnZBWhhM
705ZAYR1nRCf70GZBJ3hLzepN8cGCkE6EZQoDS7uT57F1/A8mDcHbYjOu1CwLSzwyKT
706U20WYLTcgp+unegAqQTDGw92sFohj7UFxU1n+jO1ygKENiUp3KVcgbjgFZqAbv4B
707gELCoRGJRBPBjwCrDXMCS8pfIQNSTWMByj03W4ZXDk6SDPWUhTcGxlfvpdampMI9
708Fi3CNNkU3AdKj4uuNxE8ymTpoDFmI35FY4lleQE71VZhoAH/wg0r8aXMEuOhB6j6
709t3/3q0NiQH8BiH+ZXxHTPLc7hRfwOiv/wkIU2ZmqDkA=
710-----END CERTIFICATE-----
711
712CRT;
713 if (@file_put_contents($certsPath, $certs) === false) {
714 throw new ClonerFSFunctionException('file_put_contents', $certsPath);
715 }
716 }
717
718 $tlsOptions = array(
719 'verify_peer' => true,
720 'verify_peer_name' => true,
721 'allow_self_signed' => false,
722 'cafile' => $certsPath,
723 );
724 if (strlen($peerName)) {
725 if (PHP_VERSION_ID >= 50600) {
726 $tlsOptions['peer_name'] = $peerName;
727 } else {
728 $tlsOptions['CN_match'] = $peerName;
729 }
730 }
731 return stream_context_create(array(
732 'ssl' => $tlsOptions,
733 ));
734}
735
736class ClonerNetSocketException extends ClonerNetException
737{
738 public $fn = '';
739 public $error = '';
740 public $timeout = false;
741 public $eof = false;
742
743 /**
744 * @param string $fn
745 * @param resource $sock
746 */
747 public function __construct($fn, $sock)
748 {
749 $this->fn = $fn;
750 $this->error = cloner_last_error_for($fn);
751 $meta = @stream_get_meta_data($sock);
752 if ($meta !== false) {
753 $this->timeout = $meta['timed_out'];
754 $this->eof = $meta['eof'];
755 }
756 if ($this->timeout) {
757 parent::__construct(sprintf('%s socket timeout: %s', $fn, $this->error));
758 return;
759 } elseif ($this->eof) {
760 parent::__construct(sprintf('%s socket eof: %s', $fn, $this->error));
761 return;
762 }
763 parent::__construct(sprintf('%s socket error: %s', $fn, $this->error));
764 }
765}
766
767class ClonerClonerNetFunctionException extends ClonerNetException
768{
769 public $fn = '';
770 public $host = '';
771 public $error = '';
772
773 /**
774 * @param string $fn One of stream_socket_client, fread (on socket), etc.
775 * @param string $host Remote host address.
776 * @param string|null $error Error message, will automatically fetch from error_get_last() if null.
777 */
778 public function __construct($fn, $host, $error = null)
779 {
780 $this->fn = $fn;
781 $this->host = $host;
782 if ($error === null) {
783 $error = cloner_last_error_for($fn);
784 }
785 $this->error = $error;
786 parent::__construct(sprintf('%s error for host %s: %s', $fn, $host, $this->error));
787 }
788}
789
790class ClonerSocketClientException extends ClonerNetException
791{
792 public $fn = 'stream_socket_client';
793 public $transport = '';
794 public $host = '';
795 public $error = '';
796 public $errno = 0;
797 public $errstr = '';
798
799 /**
800 * @param string $transport
801 * @param string $host
802 * @param int $errno
803 * @param string $errstr
804 */
805 public function __construct($transport, $host, $errno, $errstr)
806 {
807 $this->host = $host;
808 $this->error = cloner_last_error_for($this->fn);
809 $this->errno = $errno;
810 $this->errstr = $errstr;
811 parent::__construct(sprintf('%s error for host %s://%s: %s; errno: %d; errstr: %s', $this->fn, $transport, $host, $this->error, $errno, $errstr));
812 }
813}
814
815/**
816 * @param string $host
817 * @param int $timeout
818 * @param bool $secure
819 * @param string $peerName Peer name to verify when $tls is true.
820 * @param string $cert Certificate to use when $tls is true.
821 *
822 * @return resource Stream socket resources ready for response reading.
823 *
824 * @throws ClonerSocketClientException
825 * @throws ClonerNoTransportStreamsException
826 * @throws ClonerFSFunctionException
827 */
828function cloner_tcp_socket_dial($host, $timeout = 10, $secure = false, $peerName = '', $cert = '')
829{
830 $transport = 'tcp';
831 // Null is not allowed in stream_socket_client.
832 $ctx = stream_context_create();
833 if ($secure) {
834 if (strlen($cert)) {
835 list($transport, $ctx) = cloner_tls_transport_self_signed($peerName, $cert);
836 } else {
837 list($transport, $ctx) = cloner_tls_transport($peerName);
838 }
839 }
840 $fallback = false;
841 while (true) {
842 $sock = @stream_socket_client($transport.'://'.$host, $errno, $errstr, $timeout, STREAM_CLIENT_CONNECT, $ctx);
843 if ($sock === false) {
844 $e = new ClonerSocketClientException($transport, $host, $errno, $errstr);
845 // Temporarily disable SSL peer check.
846 if (false && $errno === 0 && $secure && !$fallback) {
847 try {
848 $ctx = ($host === 'curl.haxx.se:443') ? cloner_tls_transport_context_curl() : cloner_tls_transport_context_fallback($peerName);
849 $fallback = true;
850 continue;
851 } catch (Exception $e2) {
852 trigger_error(sprintf('Fallback TLS context error: %s', $e2->getMessage()));
853 }
854 }
855 throw $e;
856 }
857 break;
858 }
859 return $sock;
860}
861
862/**
863 * @param resource $sock
864 *
865 * @return bool
866 */
867function cloner_socket_is_timeout($sock)
868{
869 $data = @stream_get_meta_data($sock);
870 return !empty($data['timed_out']);
871}
872
873function cloner_socket_is_eof($sock)
874{
875 $data = @stream_get_meta_data($sock);
876 if ($data === false) {
877 return false;
878 }
879 return empty($data['unread_bytes']) && !empty($data['eof']);
880}
881
882/** @noinspection PhpDeprecationInspection */
883
884
885
886class ClonerMySQLStmt implements ClonerDBStmt
887{
888 private $result;
889
890 private $numRows = null;
891
892 /**
893 * @param resource|null $result
894 * @param int|null $numRows
895 *
896 * @throws ClonerException
897 */
898 public function __construct($result = null, $numRows = null)
899 {
900 if ($result === null && $numRows === null) {
901 throw new ClonerException("Either MySQL result or number of affected rows must be provided.", 'db_query_error');
902 }
903 $this->result = $result;
904 $this->numRows = $numRows;
905 }
906
907 public function fetch()
908 {
909 if (!is_resource($this->result)) {
910 throw new ClonerException("Only read-only queries can yield results.", 'db_query_error');
911 }
912 $row = @mysql_fetch_assoc($this->result);
913 if ($row === false) {
914 return null;
915 } else {
916 return $row;
917 }
918 }
919
920 public function fetchAll()
921 {
922 if (!is_resource($this->result)) {
923 throw new ClonerException("Only read-only queries can yield results.", 'db_query_error');
924 }
925 $rows = array();
926 while ($row = $this->fetch()) {
927 $rows[] = $row;
928 }
929 return $rows;
930 }
931
932 public function getNumRows()
933 {
934 if ($this->numRows !== null) {
935 return $this->numRows;
936 }
937 return mysql_num_rows($this->result);
938 }
939
940 public function free()
941 {
942 if (!is_resource($this->result)) {
943 return true;
944 }
945 return mysql_free_result($this->result);
946 }
947}
948
949class ClonerMySQLConn implements ClonerDBConn
950{
951 private $conn;
952 private $conf;
953
954 /**
955 * @param ClonerDBInfo $conf
956 *
957 * @throws ClonerException
958 */
959 public function __construct(ClonerDBInfo $conf)
960 {
961 $this->conf = $conf;
962 if (!extension_loaded('mysql')) {
963 throw new ClonerException("Mysql extension is not loaded.", 'mysql_disabled');
964 }
965
966 $this->conn = @mysql_connect($conf->host, $conf->user, $conf->password);
967 if (!is_resource($this->conn)) {
968 // Attempt to recover from "[2002] No such file or directory" error.
969 $errno = mysql_errno();
970 if ($errno !== 2002 || strtolower($conf->getHostname()) !== 'localhost' || !is_resource($this->conn = @mysql_connect('127.0.0.1', $conf->user, $conf->password))) {
971 throw new ClonerException(mysql_error(), 'db_connect_error', (string)$errno);
972 }
973 }
974 if (!@mysql_set_charset('utf8', $this->conn)) {
975 throw new ClonerException(mysql_error($this->conn), 'db_connect_error', (string)mysql_errno($this->conn));
976 }
977 if (mysql_select_db($conf->name, $this->conn) === false) {
978 throw new ClonerException(mysql_error($this->conn), 'db_connect_error', (string)mysql_errno($this->conn));
979 }
980 }
981
982 public function getConfiguration()
983 {
984 return $this->conf;
985 }
986
987 public function query($query, array $parameters = array(), $unbuffered = false)
988 {
989 $query = cloner_bind_query_params($this, $query, $parameters);
990
991 if ($unbuffered) {
992 $result = mysql_unbuffered_query($query, $this->conn);
993 } else {
994 $result = mysql_query($query, $this->conn);
995 }
996
997 if ($result === false) {
998 throw new ClonerException(mysql_error($this->conn), 'db_query_error', (string)mysql_errno($this->conn));
999 } elseif ($result === true) {
1000 // This is one of INSERT, UPDATE, DELETE, DROP statements.
1001 return new ClonerMySQLStmt(null, mysql_affected_rows($this->conn));
1002 } else {
1003 // This is one of SELECT, SHOW, DESCRIBE, EXPLAIN statements.
1004 return new ClonerMySQLStmt($result);
1005 }
1006 }
1007
1008 public function execute($query)
1009 {
1010 $this->query($query);
1011 }
1012
1013 public function escape($value)
1014 {
1015 return $value === null ? 'null' : "'".mysql_real_escape_string($value, $this->conn)."'";
1016 }
1017}
1018
1019
1020
1021
1022class ClonerMySQLiStmt implements ClonerDBStmt
1023{
1024 private $result;
1025
1026 /**
1027 * @param mysqli_result|bool $result
1028 */
1029 public function __construct($result)
1030 {
1031 $this->result = $result;
1032 }
1033
1034 /**
1035 * @return array|null
1036 */
1037 public function fetch()
1038 {
1039 if (is_bool($this->result)) {
1040 return null;
1041 }
1042 return $this->result->fetch_assoc();
1043 }
1044
1045 /**
1046 * @return array|null
1047 */
1048 public function fetchAll()
1049 {
1050 if (is_bool($this->result)) {
1051 return null;
1052 }
1053 $rows = array();
1054 while ($row = $this->fetch()) {
1055 $rows[] = $row;
1056 }
1057 return $rows;
1058 }
1059
1060 /**
1061 * @return int
1062 */
1063 public function getNumRows()
1064 {
1065 if (is_bool($this->result)) {
1066 return 0;
1067 }
1068 return $this->result->num_rows;
1069 }
1070
1071 /**
1072 * @return bool
1073 */
1074 public function free()
1075 {
1076 if (is_bool($this->result)) {
1077 return false;
1078 }
1079 mysqli_free_result($this->result);
1080 return true;
1081 }
1082}
1083
1084class ClonerMySQLiConn implements ClonerDBConn
1085{
1086 private $conn;
1087 private $conf;
1088
1089 /**
1090 * @param ClonerDBInfo $conf
1091 *
1092 * @throws ClonerException
1093 */
1094 public function __construct(ClonerDBInfo $conf)
1095 {
1096 $this->conf = $conf;
1097 if (!extension_loaded('mysqli')) {
1098 throw new ClonerException("Mysqli extension is not enabled.", 'mysqli_disabled');
1099 }
1100
1101 // Silence possible warnings thrown by mysqli
1102 // e.g. Warning: mysqli::mysqli(): Headers and client library minor version mismatch. Headers:50540 Library:50623
1103 $this->conn = @new mysqli($conf->getHostname(), $conf->user, $conf->password, $conf->name, $conf->getPort());
1104
1105 if ($this->conn->connect_errno === 2002 && strtolower($conf->getHost()) === 'localhost') {
1106 // Attempt to recover from "[2002] No such file or directory" error.
1107 $this->conn = @new mysqli('127.0.0.1', $conf->getUsername(), $conf->getPassword(), $conf->getDatabase(), $conf->getPort());
1108 }
1109 $this->conn->set_charset('utf8');
1110 if (!$this->conn->ping()) {
1111 throw new ClonerException($this->conn->connect_error, 'db_connect_error', $this->conn->connect_errno);
1112 }
1113 }
1114
1115 public function getConfiguration()
1116 {
1117 return $this->conf;
1118 }
1119
1120 public function query($query, array $parameters = array(), $unbuffered = false)
1121 {
1122 $query = cloner_bind_query_params($this, $query, $parameters);
1123
1124 $resultMode = $unbuffered ? MYSQLI_USE_RESULT : 0;
1125 $result = $this->conn->query($query, $resultMode);
1126
1127 // There are certain warnings that result in $result being false, eg. PHP Warning: mysqli::query(): Empty query,
1128 // but the error number is 0.
1129 if ($result === false && $this->conn->errno !== 0) {
1130 throw new ClonerException($this->conn->error, 'db_query_error', $this->conn->errno);
1131 }
1132
1133 return new ClonerMySQLiStmt($result);
1134 }
1135
1136 public function execute($query)
1137 {
1138 $this->query($query);
1139 }
1140
1141 public function escape($value)
1142 {
1143 return $value === null ? 'null' : "'".$this->conn->real_escape_string($value)."'";
1144 }
1145}
1146
1147
1148
1149
1150class ClonerPDOStmt implements ClonerDBStmt
1151{
1152 private $statement;
1153
1154 public function __construct(PDOStatement $statement)
1155 {
1156 $this->statement = $statement;
1157 }
1158
1159 public function fetch()
1160 {
1161 return $this->statement->fetch();
1162 }
1163
1164 public function fetchAll()
1165 {
1166 return $this->statement->fetchAll();
1167 }
1168
1169 public function getNumRows()
1170 {
1171 return $this->statement->rowCount();
1172 }
1173
1174 public function free()
1175 {
1176 return $this->statement->closeCursor();
1177 }
1178}
1179
1180class ClonerPDOConn implements ClonerDBConn
1181{
1182 /**
1183 * @param bool $attEmulatePrepares
1184 */
1185 public function setAttEmulatePrepares($attEmulatePrepares)
1186 {
1187 $this->conn->setAttribute(PDO::ATTR_EMULATE_PREPARES, $attEmulatePrepares);
1188 }
1189
1190 private $conn;
1191 private $conf;
1192 private $unbuffered = false;
1193
1194 /**
1195 * @param ClonerDBInfo $conf
1196 *
1197 * @throws ClonerException
1198 */
1199 public function __construct(ClonerDBInfo $conf)
1200 {
1201 $this->conf = $conf;
1202 $options = array(
1203 PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
1204 PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
1205 );
1206 try {
1207 $this->conn = new PDO(self::getDsn($conf), $conf->user, $conf->password, $options);
1208 } catch (PDOException $e) {
1209 if ((int)$e->getCode() === 2002 && strtolower($conf->getHostname()) === 'localhost') {
1210 try {
1211 $conf = clone $conf;
1212 $conf->host = '127.0.0.1';
1213 $this->conn = new PDO(self::getDsn($conf), $conf->user, $conf->password, $options);
1214 } catch (PDOException $e2) {
1215 throw new ClonerException($e->getMessage(), 'db_connect_error', (string)$e2->getCode());
1216 }
1217 } else {
1218 throw new ClonerException($e->getMessage(), 'db_connect_error', (string)$e->getCode());
1219 }
1220 }
1221 $this->conn->exec('SET NAMES utf8');
1222 }
1223
1224 public function getConfiguration()
1225 {
1226 return $this->conf;
1227 }
1228
1229 public function query($query, array $parameters = array(), $unbuffered = false)
1230 {
1231 if ($this->unbuffered !== $unbuffered) {
1232 $this->unbuffered = $unbuffered;
1233 $this->conn->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, !$unbuffered);
1234 }
1235
1236 try {
1237 $statement = $this->conn->prepare($query);
1238 $statement->execute($parameters);
1239 return new ClonerPDOStmt($statement);
1240 } catch (PDOException $e) {
1241 $internalErrorCode = isset($e->errorInfo[1]) ? (string)$e->errorInfo[1] : '';
1242 throw new ClonerException($e->getMessage(), 'db_query_error', $internalErrorCode);
1243 }
1244 }
1245
1246 public function execute($query)
1247 {
1248 try {
1249 $this->conn->exec($query);
1250 } catch (PDOException $e) {
1251 $internalErrorCode = isset($e->errorInfo[1]) ? (string)$e->errorInfo[1] : '';
1252 throw new ClonerException($e->getMessage(), 'db_query_error', $internalErrorCode);
1253 }
1254 }
1255
1256 public function escape($value)
1257 {
1258 return $value === null ? 'null' : $this->conn->quote($value);
1259 }
1260
1261 public static function getDsn(ClonerDBInfo $conf)
1262 {
1263 $pdoParameters = array(
1264 'dbname' => $conf->name,
1265 'charset' => 'utf8',
1266 );
1267 $socket = $conf->getSocket();
1268 if ($socket !== '') {
1269 $pdoParameters['host'] = $conf->getHostname();
1270 $pdoParameters['unix_socket'] = $socket;
1271 } else {
1272 $pdoParameters['host'] = $conf->getHostname();
1273 $pdoParameters['port'] = $conf->getPort();
1274 }
1275 $parameters = array();
1276 foreach ($pdoParameters as $name => $value) {
1277 $parameters[] = $name.'='.$value;
1278 }
1279 $dsn = sprintf('mysql:%s', implode(';', $parameters));
1280 return $dsn;
1281 }
1282}
1283
1284
1285class ClonerDBInfo
1286{
1287 public $user = '';
1288 public $password = '';
1289 /** @var string https://codex.wordpress.org/Editing_wp-config.php#Possible_DB_HOST_values */
1290 public $host = '';
1291 public $name = '';
1292
1293 public function __construct($user, $password, $host, $name)
1294 {
1295 if (strlen((string)$host) === 0) {
1296 $host = 'localhost';
1297 }
1298 $this->user = $user;
1299 $this->password = $password;
1300 $this->host = $host;
1301 $this->name = $name;
1302 }
1303
1304 public static function fromArray(array $info)
1305 {
1306 return new self($info['dbUser'], $info['dbPassword'], $info['dbHost'], $info['dbName']);
1307 }
1308
1309 public function getHostname()
1310 {
1311 $parts = explode(':', $this->host, 2);
1312 if ($parts[0] === '') {
1313 return 'localhost';
1314 }
1315 return $parts[0];
1316 }
1317
1318 public function getPort()
1319 {
1320 if (strpos($this->host, '/') !== false) {
1321 return 0;
1322 }
1323 $parts = explode(':', $this->host, 2);
1324 if (count($parts) === 2) {
1325 return (int)$parts[1];
1326 }
1327 return 0;
1328 }
1329
1330 public function getSocket()
1331 {
1332 if (strpos($this->host, '/') === false) {
1333 return '';
1334 }
1335 $parts = explode(':', $this->host, 2);
1336 if (count($parts) === 2) {
1337 return $parts[1];
1338 }
1339 return '';
1340 }
1341}
1342
1343
1344
1345
1346
1347class ClonerDeadline
1348{
1349 private $deadline = 0;
1350
1351 /**
1352 * @param $timeout int Timeout in seconds; 0 to never time out; -1 to time out immediately.
1353 */
1354 public function __construct($timeout)
1355 {
1356 if ($timeout === 0 || $timeout === -1) {
1357 $this->deadline = $timeout;
1358 return;
1359 }
1360 $this->deadline = microtime(true) + (float)$timeout;
1361 }
1362
1363 /**
1364 * @return bool True if deadline is reached.
1365 */
1366 public function done()
1367 {
1368 if ($this->deadline === 0) {
1369 return false;
1370 } elseif ($this->deadline === -1) {
1371 return true;
1372 }
1373 return microtime(true) > $this->deadline;
1374 }
1375}
1376
1377class ClonerURLReplacer
1378{
1379 private $fullURL;
1380 private $shortURL;
1381
1382 /**
1383 * @param string $url
1384 */
1385 public function __construct($url)
1386 {
1387 $this->fullURL = $url;
1388 $this->shortURL = preg_replace('{^https?:}', '', $url);
1389 }
1390
1391 /**
1392 * @param array $matches First match is http: or https:, second match is trailing slash.
1393 *
1394 * @return string
1395 */
1396 public function replace(array $matches)
1397 {
1398 if (strlen($matches[1])) {
1399 // Scheme is present.
1400 return $this->fullURL.$matches[2];
1401 }
1402 // Empty scheme.
1403 return $this->shortURL.$matches[2];
1404 }
1405}
1406
1407class ClonerErrorHandler
1408{
1409 private $logFile;
1410 private $reservedMemory;
1411 private static $lastError;
1412 private $requestID;
1413
1414 public function __construct($logFile)
1415 {
1416 $this->logFile = $logFile;
1417 }
1418
1419 public function setRequestID($requestID)
1420 {
1421 $this->requestID = $requestID;
1422 }
1423
1424 public function register()
1425 {
1426 $this->reservedMemory = str_repeat('x', 10240);
1427 register_shutdown_function(array($this, 'handleFatalError'));
1428 set_error_handler(array($this, 'handleError'));
1429 set_exception_handler(array($this, 'handleException'));
1430 }
1431
1432 public function refresh()
1433 {
1434 set_error_handler(array($this, 'handleError'));
1435 set_exception_handler(array($this, 'handleException'));
1436 }
1437
1438 /**
1439 * @return array
1440 */
1441 public static function lastError()
1442 {
1443 return self::$lastError;
1444 }
1445
1446 public function handleError($type, $message, $file, $line)
1447 {
1448 self::$lastError = compact('message', 'type', 'file', 'line');
1449 if (error_reporting() === 0) {
1450 // Muted error.
1451 return;
1452 }
1453 if (!strlen($message)) {
1454 $message = 'empty error message';
1455 }
1456 $args = func_get_args();
1457 if (count($args) >= 6 && $args[5] !== null && $type & E_ERROR) {
1458 // 6th argument is backtrace.
1459 // E_ERROR fatal errors are triggered on HHVM when
1460 // hhvm.error_handling.call_user_handler_on_fatals=1
1461 // which is the way to get their backtrace.
1462 $this->handleFatalError(compact('type', 'message', 'file', 'line'));
1463
1464 return;
1465 }
1466 list($file, $line) = self::getFileLine($file, $line);
1467 $this->log(sprintf("%s: %s in %s on line %d", self::codeToString($type), $message, $file, $line));
1468 }
1469
1470 private static function getFileLine($file, $line)
1471 {
1472 if (!function_exists('__bundler_sourcemap')) {
1473 return array($file, $line);
1474 }
1475 if (__FILE__ !== $file) {
1476 return array($file, $line);
1477 }
1478 $globalOffset = 0;
1479 foreach (__bundler_sourcemap() as $offsetPath) {
1480 list($offset, $path) = $offsetPath;
1481 if ($line <= $offset) {
1482 return array($path, $line - $globalOffset + 1);
1483 }
1484 $globalOffset = $offset;
1485 }
1486 return array($file, $line);
1487 }
1488
1489 /**
1490 * @param Exception|Error $e
1491 */
1492 public function handleException($e)
1493 {
1494 $errorCode = 'exception';
1495 $internalError = '';
1496 $context = array();
1497 if ($e instanceof ClonerException && strlen($e->getErrorCode())) {
1498 $errorCode = $e->getErrorCode();
1499 $internalError = $e->getInternalError();
1500 foreach (get_object_vars($e) as $key => $val) {
1501 if (!is_scalar($val)) {
1502 continue;
1503 }
1504 $context[$key] = (string)$val;
1505 }
1506 }
1507 $message = sprintf("%s in file %s on line %d", $e->getMessage(), $e->getFile(), $e->getLine());
1508 list($file, $line) = self::getFileLine($e->getFile(), $e->getLine());
1509 cloner_send_error_response($this->requestID, $message, $errorCode, $internalError, $file, $line, $e->getTraceAsString(), $context);
1510 exit;
1511 }
1512
1513 public function handleFatalError(array $error = null)
1514 {
1515 $this->reservedMemory = null;
1516 if ($error === null) {
1517 // Since default PHP implementation doesn't call error handlers on fatal errors, the self::$lastError
1518 // variable won't be updated. That's why this is the only place where we call error_get_last() directly.
1519 $error = error_get_last();
1520 }
1521 if (!$error) {
1522 return;
1523 }
1524 if (!in_array($error['type'], array(E_PARSE, E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR))) {
1525 return;
1526 }
1527 $message = sprintf("%s: %s in %s on line %d", self::codeToString($error['type']), $error['message'], $error['file'], $error['line']);
1528 $this->log($message);
1529 list($file, $line) = self::getFileLine($error['file'], $error['line']);
1530
1531 cloner_send_error_response($this->requestID, $message, 'fatal_error', $error['message'], $file, $line);
1532 exit;
1533 }
1534
1535 private function log($message)
1536 {
1537 if (($fp = fopen($this->logFile, 'a')) === false) {
1538 return;
1539 }
1540 if (flock($fp, LOCK_EX) === false) {
1541 return;
1542 }
1543 if (fwrite($fp, sprintf("[%s] %s\n", date("Y-m-d H:i:s"), $message)) === false) {
1544 return;
1545 }
1546 fclose($fp);
1547 }
1548
1549 private static function codeToString($code)
1550 {
1551 switch ($code) {
1552 case E_ERROR:
1553 return 'E_ERROR';
1554 case E_WARNING:
1555 return 'E_WARNING';
1556 case E_PARSE:
1557 return 'E_PARSE';
1558 case E_NOTICE:
1559 return 'E_NOTICE';
1560 case E_CORE_ERROR:
1561 return 'E_CORE_ERROR';
1562 case E_CORE_WARNING:
1563 return 'E_CORE_WARNING';
1564 case E_COMPILE_ERROR:
1565 return 'E_COMPILE_ERROR';
1566 case E_COMPILE_WARNING:
1567 return 'E_COMPILE_WARNING';
1568 case E_USER_ERROR:
1569 return 'E_USER_ERROR';
1570 case E_USER_WARNING:
1571 return 'E_USER_WARNING';
1572 case E_USER_NOTICE:
1573 return 'E_USER_NOTICE';
1574 case E_STRICT:
1575 return 'E_STRICT';
1576 case E_RECOVERABLE_ERROR:
1577 return 'E_RECOVERABLE_ERROR';
1578 case E_DEPRECATED:
1579 return 'E_DEPRECATED';
1580 case E_USER_DEPRECATED:
1581 return 'E_USER_DEPRECATED';
1582 }
1583 if (defined('PHP_VERSION_ID') && PHP_VERSION_ID >= 50300) {
1584 switch ($code) {
1585 case E_DEPRECATED:
1586 return 'E_DEPRECATED';
1587 case E_USER_DEPRECATED:
1588 return 'E_USER_DEPRECATED';
1589 }
1590 }
1591 return 'E_UNKNOWN';
1592 }
1593}
1594
1595class ClonerLoader
1596{
1597 private $hookedAdmin = false;
1598
1599 private $hookedAdminWpdb = false;
1600
1601 private $errorHandler;
1602
1603 public function __construct(ClonerErrorHandler $errorHandler)
1604 {
1605 $this->errorHandler = $errorHandler;
1606 }
1607
1608 public function hook()
1609 {
1610 global $wp_filter;
1611 $wp_filter['all'][-999999][0] = array('function' => array($this, 'hookTick'), 'accepted_args' => 0);
1612 $wp_filter['set_auth_cookie'][-999999][0] = array('function' => array($this, 'setAuthCookie'), 'accepted_args' => 5);
1613 $wp_filter['set_logged_in_cookie'][-999999][0] = array('function' => array($this, 'setAuthCookie'), 'accepted_args' => 5);
1614 }
1615
1616 public function setAuthCookie($cookie, $expire, $expiration, $userId, $scheme)
1617 {
1618 switch ($scheme) {
1619 case 'auth':
1620 if (!defined('AUTH_COOKIE')) {
1621 return;
1622 }
1623 $_COOKIE[AUTH_COOKIE] = $cookie;
1624 break;
1625 case 'secure_auth':
1626 if (!defined('SECURE_AUTH_COOKIE')) {
1627 return;
1628 }
1629 $_COOKIE[SECURE_AUTH_COOKIE] = $cookie;
1630 break;
1631 case 'logged_in':
1632 if (!defined('LOGGED_IN_COOKIE')) {
1633 return;
1634 }
1635 $_COOKIE[LOGGED_IN_COOKIE] = $cookie;
1636 break;
1637 }
1638 }
1639
1640 /**
1641 * @throws ClonerException
1642 */
1643 public function hookTick()
1644 {
1645 global $wpdb, $pagenow;
1646 $this->errorHandler->refresh();
1647 if ($this->hookedAdmin || !function_exists('admin_url')) {
1648 if ($wpdb) {
1649 $this->adminHookWpdb($wpdb);
1650 }
1651
1652 return;
1653 }
1654 $this->hookedAdmin = true;
1655
1656 @ini_set('memory_limit', '512M');
1657 define('DOING_AJAX', true);
1658 $pagenow = 'options-general.php';
1659 $_SERVER['PHP_SELF'] = parse_url(admin_url('options-general.php'), PHP_URL_PATH);
1660 $_COOKIE['redirect_count'] = 10; // hack for the WordPress HTTPS plugin, so it doesn't redirect us
1661 if (force_ssl_admin()) {
1662 $_SERVER['HTTPS'] = 'on';
1663 $_SERVER['SERVER_PORT'] = '443';
1664 }
1665 }
1666
1667 /**
1668 * @param wpdb $wpdb
1669 *
1670 * @throws ClonerException
1671 */
1672 private function adminHookWpdb($wpdb)
1673 {
1674 if ($this->hookedAdminWpdb || !function_exists('wp_set_current_user')) {
1675 return;
1676 }
1677 $this->hookedAdminWpdb = true;
1678 $users = get_users(array('role' => 'administrator', 'number' => 1, 'orderby' => 'ID'));
1679 $user = reset($users);
1680 if (!$user) {
1681 throw new ClonerException('Could not find an administrator user to use.', 'no_admin_user');
1682 }
1683 wp_set_current_user($user);
1684 wp_set_auth_cookie($user->ID);
1685 update_user_meta($user->ID, 'last_login_time', current_time('mysql'));
1686 do_action('wp_login', $user->user_login, $user);
1687 }
1688}
1689
1690function cloner_url_slug($url)
1691{
1692 $parts = parse_url($url);
1693 return strtolower(rtrim(sprintf('%s%s%s', preg_replace('/^www\./i', '', $parts['host']), isset($parts['port']) ? ':'.$parts['port'] : '', isset($parts['path']) ? $parts['path'] : ''), '/'));
1694}
1695
1696function cloner_update_multisite_url($oldRootHost, $oldRootPath, $newRootHost, $newRootPath, $host, $path)
1697{
1698 $replaced = false;
1699 if (substr($host, -strlen('.'.$oldRootHost)) === '.'.$oldRootHost) {
1700 // Hosts starts like domain.old-url.com; migrate it to domain.new-url.com
1701 $host = substr($host, 0, strlen($host) - strlen('.'.$oldRootHost)).'.'.$newRootHost;
1702 $replaced = true;
1703 } elseif ($oldRootHost === $host) {
1704 // Host is the same as the root host; use the new one.
1705 $host = $newRootHost;
1706 $replaced = true;
1707 }
1708 if (strlen($oldRootPath) > 1 && substr($path, 0, strlen($oldRootPath)) === $oldRootPath) {
1709 // Path starts like /old-root/blog-name/, strip the old-root prefix.
1710 $path = '/'.ltrim(substr($path, strlen($oldRootPath)), '/');
1711 $replaced = true;
1712 }
1713 $path = rtrim($newRootPath, '/').'/'.ltrim($path, '/');
1714 return array($host, $path, $replaced);
1715}
1716
1717/**
1718 * Update {$newPrefix}usermeta table by prefixing each meta_key in $keys with $oldPrefix
1719 * and changing its prefix to $newPrefix. Intended to be used as an atomic operation, so
1720 * pass $limit for tighter control over migration.
1721 *
1722 * @param ClonerDBConn $conn
1723 * @param array $keys Meta keys to migrate, without prefix.
1724 * @param string $oldPrefix Old table prefix.
1725 * @param string $newPrefix New table prefix.
1726 * @param int $limit Update limit for keys with old prefix, must be positive.
1727 *
1728 * @return int Number of updated fields.
1729 *
1730 * @throws ClonerException If database query fails.
1731 */
1732function cloner_update_usermeta_prefix(ClonerDBConn $conn, array $keys, $oldPrefix, $newPrefix, $limit)
1733{
1734 $oldPrefixLength = strlen($oldPrefix) + 1;
1735 $oldKeys = "'{$oldPrefix}".implode("', '{$oldPrefix}", $keys)."'";
1736
1737 $sql = <<<SQL
1738UPDATE `{$newPrefix}usermeta`
1739 SET `meta_key` = CONCAT({$conn->escape($newPrefix)}, SUBSTR(`meta_key`, {$oldPrefixLength}))
1740 WHERE `meta_key` IN ({$oldKeys})
1741 LIMIT {$limit}
1742SQL;
1743 return $conn->query($sql)->getNumRows();
1744}
1745
1746function cloner_update_field_prefix(ClonerDBConn $conn, $table, $field, $oldPrefix, $newPrefix, $limit, $where = null)
1747{
1748 if ($where !== null) {
1749 $where = 'AND '.$where;
1750 }
1751 $escapedOldPrefix = cloner_escape_like($oldPrefix);
1752 // +1 is intentional.
1753 // https://dev.mysql.com/doc/refman/5.0/en/string-functions.html#function_substring
1754 $oldPrefixLength = strlen($oldPrefix) + 1;
1755 $sql = <<<SQL
1756UPDATE `{$table}`
1757 SET `{$field}` = CONCAT({$conn->escape($newPrefix)}, SUBSTR(`{$field}`, {$oldPrefixLength}))
1758 WHERE `{$field}` LIKE '{$escapedOldPrefix}%'
1759 {$where}
1760LIMIT {$limit}
1761SQL;
1762 /** @var TYPE_NAME $conn */
1763 $result = $conn->query($sql);
1764 $count = $result->getNumRows();
1765 $result->free();
1766 return $count;
1767}
1768
1769// Functions that are present in more recent WP versions, but not in earlier ones.
1770// Keep them here for BC reasons.
1771function cloner_wp_polyfill()
1772{
1773 if (!function_exists('is_multisite')) {
1774 function is_multisite()
1775 {
1776 if (defined('MULTISITE')) {
1777 return MULTISITE;
1778 }
1779
1780 if (defined('SUBDOMAIN_INSTALL') || defined('VHOST') || defined('SUNRISE')) {
1781 return true;
1782 }
1783 return false;
1784 }
1785 }
1786}
1787
1788/**
1789 * @param string $id
1790 * @param mixed $data
1791 */
1792function cloner_send_success_response($id, $data)
1793{
1794 $response = json_encode($data);
1795 if ($response === false) {
1796 cloner_send_error_response($id, "Could not JSON-encode result.", "json_encode_error");
1797 } else {
1798 echo "\n", '{"id":"', (string)$id, '","result":', $response, '}', "\n";
1799 }
1800}
1801
1802function cloner_send_error_response($id, $errorMessage, $errorCode = null, $internalError = null, $file = null, $line = null, $trace = null, array $context = null)
1803{
1804 if ($context) {
1805 foreach ($context as $key => &$val) {
1806 if (!is_scalar($val)) {
1807 unset($context[$key]);
1808 continue;
1809 }
1810 $val = (string)$val;
1811 }
1812 }
1813 if (!$context) {
1814 $context = null;
1815 }
1816 $data = array(
1817 'error' => (string)$errorCode,
1818 'message' => (string)$errorMessage,
1819 'internalError' => (string)$internalError,
1820 'file' => (string)$file,
1821 'line' => (int)$line,
1822 'trace' => (string)$trace,
1823 'context' => $context,
1824 );
1825 $response = json_encode($data);
1826 if ($response === false) {
1827 echo "\n", '{"id":"', (string)$id, '","error":{"error":"json_encode_error","message":"Could not JSON-encode error."}}', "\n";
1828 } else {
1829 echo "\n", '{"id":"', (string)$id, '","error":', $response, '}', "\n";
1830 }
1831}
1832
1833/**
1834 * @param ClonerDBConn $conn
1835 * @param string $prefix
1836 * @param string $username
1837 *
1838 * @return int|null
1839 *
1840 * @throws ClonerException
1841 */
1842function cloner_get_user_id_by_username(ClonerDBConn $conn, $prefix, $username)
1843{
1844 $query = <<<SQL
1845SELECT u.ID
1846 FROM {$prefix}users u
1847 WHERE
1848 u.user_login = :user_login
1849 ORDER BY ID ASC
1850 LIMIT 1
1851SQL;
1852
1853 $existingUser = $conn->query($query, array('user_login' => $username))->fetch();
1854
1855 if ($existingUser) {
1856 return (int)$existingUser['ID'];
1857 }
1858
1859 return null;
1860}
1861
1862/**
1863 * @param string $username
1864 * @param bool $strict
1865 *
1866 * @return mixed|string
1867 *
1868 * @see sanitize_user() from WordPress core.
1869 */
1870function cloner_sanitize_user($username, $strict = false)
1871{
1872 $username = strip_tags($username);
1873 // Kill octets
1874 $username = preg_replace('|%([a-fA-F0-9][a-fA-F0-9])|', '', $username);
1875 $username = preg_replace('/&.+?;/', '', $username); // Kill entities
1876
1877 // If strict, reduce to ASCII for max portability.
1878 if ($strict) {
1879 $username = preg_replace('|[^a-z0-9 _.\-@]|i', '', $username);
1880 }
1881
1882 $username = trim($username);
1883 // Consolidate contiguous whitespace
1884 $username = preg_replace('|\s+|', ' ', $username);
1885
1886 return $username;
1887}
1888
1889/**
1890 * @param string|null $path File path for which to clear the cache.
1891 *
1892 * @see clearstatcache()
1893 */
1894function cloner_clear_stat_cache($path = null)
1895{
1896 if (PHP_VERSION_ID < 50300 || $path === null) {
1897 clearstatcache();
1898 return;
1899 }
1900 clearstatcache(true, $path);
1901}
1902
1903/**
1904 * Creates a directory similarly to mkdir -p, but calls chmod 0777 to each directory in $path before creating
1905 * it if the parent directory (starting with $root) is not writable.
1906 *
1907 * @param string $root Absolute path to the root directory, should already exist and will not be checked.
1908 * @param string $path Relative path of directory to create.
1909 *
1910 * @return string Error message; empty message means "no error".
1911 */
1912function cloner_make_dir($root, $path)
1913{
1914 $dir = strtok($path, '/');
1915 do {
1916 $root .= '/'.$dir;
1917 if ($dir === '..') {
1918 continue;
1919 }
1920 if (is_dir($root)) {
1921 if (!is_writable($root)) {
1922 @chmod($root, 0777);
1923 }
1924 continue;
1925 }
1926 $dirMade = @mkdir($root, 0777, true);
1927 // Verify that the dir was not made by another process in a race condition.
1928 if ($dirMade === false) {
1929 $lastError = cloner_last_error_for('mkdir');
1930 cloner_clear_stat_cache($root);
1931 if (!is_dir($root)) {
1932 return $lastError;
1933 }
1934 }
1935 } while (is_string($dir = strtok('/')));
1936
1937 if (!is_writable($root)) {
1938 return "directory $root is not writable";
1939 }
1940
1941 return '';
1942}
1943
1944/**
1945 * Writes the file $content located on $path at $offset. If $offset does not match file size at $path,
1946 * the function will fail. Uses retries with exponential back-off.
1947 *
1948 * @param string $path Path to file to create and/or write content to.
1949 * @param int $offset File offset, must match the existing file size if greater than 0.
1950 * @param string $content Content to write to the file at $offset.
1951 *
1952 * @return string Error message, empty message means "no error".
1953 */
1954function cloner_write_file($path, $offset, $content)
1955{
1956 $attempt = 0;
1957 $length = strlen($content);
1958 $total = $offset + $length;
1959 $err = '';
1960 $fp = null;
1961 do {
1962 cloner_clear_stat_cache($path);
1963 if ($attempt > 0) {
1964 // Sleep for 200ms, 400ms, 800s, 1.6s etc.
1965 usleep(100000 * pow(2, $attempt));
1966 trigger_error("$err (file: $path, attempt: $attempt, offset: $offset, length: $length)");
1967 }
1968 // Check file size if appending content.
1969 if ($offset && (($size = filesize($path)) < $offset)) {
1970 if ($size === false) {
1971 $err = cloner_last_error_for('filesize');
1972 } else {
1973 $err = "corrupt file; wrote $offset bytes, but file is $size bytes";
1974 }
1975 continue;
1976 }
1977 $err = '';
1978 if (is_resource($fp)) {
1979 @fclose($fp);
1980 }
1981 if (is_dir($path)) {
1982 if (strlen($err = cloner_remove_file_or_dir($path))) {
1983 break;
1984 }
1985 }
1986 $fp = @fopen($path, $offset ? 'cb' : 'wb');
1987 if ($fp === false) {
1988 $err = cloner_last_error_for('fopen');
1989 continue;
1990 }
1991 if ($offset) {
1992 if (@fseek($fp, $offset) !== 0) {
1993 $err = cloner_last_error_for('fseek');
1994 continue;
1995 }
1996 }
1997 if (@fwrite($fp, $content) === false) {
1998 $err = cloner_last_error_for('fwrite');
1999 continue;
2000 }
2001 if (@fclose($fp) === false) {
2002 $err = cloner_last_error('fclose');
2003 continue;
2004 }
2005 $fp = null;
2006 // This is mandatory before stat-ing the file.
2007 cloner_clear_stat_cache($path);
2008 if (($size = @filesize($path)) !== $total) {
2009 if ($size === false) {
2010 $err = cloner_last_error_for('filesize');
2011 continue;
2012 }
2013 $err = "file size after write is $size; expected $offset+$length=$total";
2014 continue;
2015 }
2016 break;
2017 } while (strlen($err) && ++$attempt < 3);
2018
2019 return $err;
2020}
2021
2022/**
2023 * Checks to see if a string is utf8 encoded.
2024 * This function checks for 5-Byte sequences, UTF8
2025 * has Bytes Sequences with a maximum length of 4.
2026 *
2027 * @param string $str The string to be checked
2028 *
2029 * @return bool True if $str fits a UTF-8 model, false otherwise.
2030 */
2031function cloner_seems_utf8($p)
2032{
2033 static $first;
2034 if ($first === null) {
2035 $xx = 0xF1; // invalid: size 1
2036 $as = 0xF0; // ASCII: size 1
2037 $s1 = 0x02; // accept 0, size 2
2038 $s2 = 0x13; // accept 1, size 3
2039 $s3 = 0x03; // accept 0, size 3
2040 $s4 = 0x23; // accept 2, size 3
2041 $s5 = 0x34; // accept 3, size 4
2042 $s6 = 0x04; // accept 0, size 4
2043 $s7 = 0x44; // accept 4, size 4
2044 $first = array(
2045 // 1 2 3 4 5 6 7 8 9 A B C D E F
2046 $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, // 0x00-0x0F
2047 $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, // 0x10-0x1F
2048 $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, // 0x20-0x2F
2049 $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, // 0x30-0x3F
2050 $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, // 0x40-0x4F
2051 $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, // 0x50-0x5F
2052 $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, // 0x60-0x6F
2053 $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, // 0x70-0x7F
2054 // 1 2 3 4 5 6 7 8 9 A B C D E F
2055 $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, // 0x80-0x8F
2056 $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, // 0x90-0x9F
2057 $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, // 0xA0-0xAF
2058 $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, // 0xB0-0xBF
2059 $xx, $xx, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, // 0xC0-0xCF
2060 $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, // 0xD0-0xDF
2061 $s2, $s3, $s3, $s3, $s3, $s3, $s3, $s3, $s3, $s3, $s3, $s3, $s3, $s4, $s3, $s3, // 0xE0-0xEF
2062 $s5, $s6, $s6, $s6, $s7, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, // 0xF0-0xFF
2063 );
2064 }
2065 static $xx = 0xF1;
2066 static $locb = 0x80;
2067 static $hicb = 0xBF;
2068 static $acceptRanges;
2069 if ($acceptRanges === null) {
2070 $acceptRanges = array(
2071 0 => array($locb, $hicb),
2072 1 => array(0xA0, $hicb),
2073 2 => array($locb, 0x9F),
2074 3 => array(0x90, $hicb),
2075 4 => array($locb, 0x8F),
2076 );
2077 }
2078 $n = strlen($p);
2079 for ($i = 0; $i < $n;) {
2080 $pi = ord($p[$i]);
2081 if ($pi < 0x80) {
2082 $i++;
2083 continue;
2084 }
2085 $x = $first[$pi];
2086 if ($x === $xx) {
2087 return false; // Illegal starter byte.
2088 }
2089 $size = $x & 7;
2090 if ($i + $size > $n) {
2091 return false; // Short or invalid.
2092 }
2093 $accept = $acceptRanges[$x >> 4];
2094 if ((($c = ord($p[$i + 1])) < $accept[0]) || ($accept[1] < $c)) {
2095 return false;
2096 } elseif ($size === 2) {
2097 } elseif ((($c = ord($p[$i + 2])) < $locb) || ($hicb < $c)) {
2098 return false;
2099 } elseif ($size === 3) {
2100 } elseif ((($c = ord($p[$i + 3])) < $locb) || ($hicb < $c)) {
2101 return false;
2102 }
2103 $i += $size;
2104 }
2105 return true;
2106}
2107
2108function cloner_encode_non_utf8($p)
2109{
2110 static $first;
2111 if ($first === null) {
2112 $xx = 0xF1; // invalid: size 1
2113 $as = 0xF0; // ASCII: size 1
2114 $s1 = 0x02; // accept 0, size 2
2115 $s2 = 0x13; // accept 1, size 3
2116 $s3 = 0x03; // accept 0, size 3
2117 $s4 = 0x23; // accept 2, size 3
2118 $s5 = 0x34; // accept 3, size 4
2119 $s6 = 0x04; // accept 0, size 4
2120 $s7 = 0x44; // accept 4, size 4
2121 $first = array(
2122 // 1 2 3 4 5 6 7 8 9 A B C D E F
2123 $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, // 0x00-0x0F
2124 $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, // 0x10-0x1F
2125 $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, // 0x20-0x2F
2126 $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, // 0x30-0x3F
2127 $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, // 0x40-0x4F
2128 $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, // 0x50-0x5F
2129 $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, // 0x60-0x6F
2130 $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, $as, // 0x70-0x7F
2131 // 1 2 3 4 5 6 7 8 9 A B C D E F
2132 $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, // 0x80-0x8F
2133 $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, // 0x90-0x9F
2134 $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, // 0xA0-0xAF
2135 $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, // 0xB0-0xBF
2136 $xx, $xx, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, // 0xC0-0xCF
2137 $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, $s1, // 0xD0-0xDF
2138 $s2, $s3, $s3, $s3, $s3, $s3, $s3, $s3, $s3, $s3, $s3, $s3, $s3, $s4, $s3, $s3, // 0xE0-0xEF
2139 $s5, $s6, $s6, $s6, $s7, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, $xx, // 0xF0-0xFF
2140 );
2141 }
2142 static $xx = 0xF1;
2143 static $locb = 0x80;
2144 static $hicb = 0xBF;
2145 static $acceptRanges;
2146 if ($acceptRanges === null) {
2147 $acceptRanges = array(
2148 0 => array($locb, $hicb),
2149 1 => array(0xA0, $hicb),
2150 2 => array($locb, 0x9F),
2151 3 => array(0x90, $hicb),
2152 4 => array($locb, 0x8F),
2153 );
2154 }
2155 $percent = ord('%');
2156 $plus = ord('+');
2157 $encoded = false;
2158 $fixed = '';
2159 $n = strlen($p);
2160 $invalid = false;
2161 for ($i = 0; $i < $n;) {
2162 if ($invalid) {
2163 if (!$encoded) {
2164 // Make sure that "urldecode" call transforms the string to its original form.
2165 // We don't encode printable characters, only invalid UTF-8; but these characters
2166 // will always be processed by URL-decoder.
2167 $fixed = strtr($fixed, array('%' => '%25', '+' => '%2B'));
2168 }
2169 $encoded = true;
2170 $fixed .= urlencode($p[$i]);
2171 $invalid = false;
2172 $i++;
2173 continue;
2174 }
2175 $pi = ord($p[$i]);
2176 if ($pi < 0x80) {
2177 if ($encoded && $pi === $percent) {
2178 $fixed .= '%25';
2179 } elseif ($encoded && $pi === $plus) {
2180 $fixed .= '%2B';
2181 } else {
2182 $fixed .= $p[$i];
2183 }
2184 $i++;
2185 continue;
2186 }
2187 $x = $first[$pi];
2188 if ($x === $xx) {
2189 $invalid = true;
2190 continue;
2191 }
2192 $size = $x & 7;
2193 if ($i + $size > $n) {
2194 $invalid = true;
2195 continue;
2196 }
2197 $accept = $acceptRanges[$x >> 4];
2198 if ((($c = ord($p[$i + 1])) < $accept[0]) || ($accept[1] < $c)) {
2199 $invalid = true;
2200 continue;
2201 } elseif ($size === 2) {
2202 } elseif ((($c = ord($p[$i + 2])) < $locb) || ($hicb < $c)) {
2203 $invalid = true;
2204 continue;
2205 } elseif ($size === 3) {
2206 } elseif ((($c = ord($p[$i + 3])) < $locb) || ($hicb < $c)) {
2207 $invalid = true;
2208 continue;
2209 }
2210 $fixed .= substr($p, $i, $size);
2211 $i += $size;
2212 }
2213 return $fixed;
2214}
2215
2216function cloner_format_query_error($message, $statements, $path, $processed, $cursor, $size)
2217{
2218 $max = 2 * 1024;
2219 $len = strlen($statements);
2220 if ($len > $max) {
2221 $statements = substr($statements, 0, $max / 2).sprintf('[truncated %d bytes]', strlen($statements) - $max).substr($statements, -$max / 2);
2222 }
2223 if (!cloner_seems_utf8($statements)) {
2224 if (function_exists('mb_convert_encoding')) {
2225 // http://php.net/manual/en/function.iconv.php#108643
2226 ini_set('mbstring.substitute_character', 'none');
2227 $statements = mb_convert_encoding($statements, 'UTF-8', 'UTF-8');
2228 } else {
2229 $statements = 'base64:'.base64_encode($statements);
2230 }
2231 }
2232 return sprintf('%s; query: %s (file %s at %d-%d out of %d bytes)', $message, $statements, $path, $processed, $cursor, $size);
2233}
2234
2235// Actions.
2236
2237class ClonerNoTransportStreamsException extends ClonerException
2238{
2239 public function __construct(array $attempted, array $available)
2240 {
2241 parent::__construct(sprintf(
2242 "could not find available transport stream to use; attempted: %s; available: %s",
2243 implode(',', $attempted),
2244 implode(',', $available)
2245 ), 'no_tls');
2246 }
2247}
2248
2249class ClonerSyncState
2250{
2251 /**
2252 * @var string Address to connect to in host:port format.
2253 */
2254 public $host = '';
2255
2256 /**
2257 * @var string[] Established connection IDs.
2258 */
2259 public $haveConns = array();
2260
2261 /**
2262 * @var string[] Connection IDs that we need to create.
2263 */
2264 public $wantConns = array();
2265
2266 /**
2267 * @var int
2268 */
2269 public $timestamp;
2270
2271 public static function fromArray(array $data)
2272 {
2273 $state = new self();
2274 $state->host = isset($data['host']) ? $data['host'] : '';
2275 $state->haveConns = isset($data['haveConns']) ? $data['haveConns'] : array();
2276 $state->wantConns = isset($data['wantConns']) ? $data['wantConns'] : array();
2277 $state->timestamp = time();
2278 return $state;
2279 }
2280}
2281
2282/**
2283 * @param string $code Error code.
2284 * @param string $message Error message.
2285 *
2286 * @return string JSON-encoded error result.
2287 */
2288function cloner_error_result($code, $message)
2289{
2290 if (!strlen($code)) {
2291 $code = 'unexpected_error';
2292 }
2293 return json_encode(array(
2294 'ok' => false,
2295 'error' => $code,
2296 'message' => $message,
2297 ));
2298}
2299
2300function cloner_ok_result(array $props = array())
2301{
2302 return json_encode(array_merge(array('ok' => true), $props));
2303}
2304
2305function cloner_page_state_poll()
2306{
2307 try {
2308 $state = cloner_state_poll(cloner_constant('CLONER_STATE'));
2309 } catch (ClonerException $e) {
2310 trigger_error($e->getMessage());
2311 exit(cloner_error_result($e->getErrorCode(), $e->getMessage()));
2312 } catch (Exception $e) {
2313 trigger_error($e->getMessage());
2314 exit(cloner_error_result('', $e->getMessage()));
2315 }
2316
2317 exit(cloner_ok_result(array('state' => $state)));
2318}
2319
2320function cloner_page_connect($host, $connID)
2321{
2322 try {
2323 cloner_action_connect_back($host, '/api/ws', $connID, 5, 120, '');
2324 } catch (ClonerException $e) {
2325 trigger_error($e->getMessage());
2326 exit(cloner_error_result($e->getErrorCode(), $e->getMessage()));
2327 } catch (Exception $e) {
2328 trigger_error($e->getMessage());
2329 exit(cloner_error_result('', $e->getMessage()));
2330 }
2331}
2332
2333class ClonerURLException extends ClonerException
2334{
2335 public $url = '';
2336 public $error = '';
2337
2338 /**
2339 * @param string $url
2340 * @param string $error
2341 */
2342 public function __construct($url, $error)
2343 {
2344 $this->url = $url;
2345 $this->error = $error;
2346 parent::__construct(sprintf('url %s is not valid: %s', $url, $error));
2347 }
2348}
2349
2350/**
2351 * @param string $url
2352 *
2353 * @return ClonerSyncState
2354 *
2355 * @throws ClonerNetException
2356 * @throws ClonerURLException
2357 * @throws Exception
2358 */
2359function cloner_state_poll($url)
2360{
2361 $json = cloner_http_do('GET', $url);
2362 $data = json_decode($json, true);
2363 if (!is_array($data)) {
2364 throw new ClonerException("non-json get-state response: $json");
2365 }
2366 if (!empty($data['error'])) {
2367 throw new ClonerException('Poll error', $data['error']);
2368 }
2369 return ClonerSyncState::fromArray($data);
2370}
2371
2372/**
2373 * Runs appropriate route and outputs result.
2374 */
2375function cloner_sync_main()
2376{
2377 $root = dirname(__FILE__);
2378 $uri = '';
2379 if (isset($_GET['q']) && is_string($_GET['q']) && strlen($_GET['q'])) {
2380 $uri = $_GET['q'];
2381 }
2382 switch ($uri) {
2383 case '':
2384 cloner_page_index();
2385 return;
2386 case 'state_poll':
2387 cloner_page_state_poll();
2388 return;
2389 case 'connect':
2390 cloner_page_connect((string)@$_GET['host'], (string)@$_GET['conn_id']);
2391 return;
2392 case 'cleanup':
2393 echo json_encode(cloner_action_delete_files($root, array('cloner.php')));
2394 return;
2395 default:
2396 cloner_page_404();
2397 return;
2398 }
2399}
2400
2401/**
2402 * Compiles an array of glob-like patterns into a single regular expression that
2403 * matches any of them in case-insensitive mode. Supported syntax:
2404 * * matches zero or more non-/ characters
2405 * + matches one or more non-/ characters
2406 * ? matches any single non-/ character
2407 * [abc] matches one character given in the bracket
2408 * [abc*] matches zero or more characters given in the bracket
2409 * [abc+] matches one or more characters given in the bracket
2410 * [a-z] matches one character from the range given in the bracket
2411 * [a-z*] matches zero or more characters from the range given in the bracket
2412 * [a-z+] matches one or more characters from the range given in the bracket
2413 * [a-z*2] matches exactly 2 characters from the range given in the bracket
2414 * [a-z*,2] matches up to 2 characters from the range given in the bracket
2415 * [a-z*2,] matches 2 or more characters from the range given in the bracket
2416 * [a-z*2,6] matches 2 to 6 characters from the range given in the bracket
2417 * {foo,bar} matches any of the given literal strings
2418 *
2419 * @param string $prefix Prefix for all matches.
2420 * @param string[] $globs Glob patterns.
2421 * @param string $delimiter Delimiter to use when preg-quoting.
2422 *
2423 * @return string Regexp that matches any of the glob patterns from $globs. It always starts
2424 * with `$prefix(?:` and ends with `)` and does not include ^ nor $ nor delimiters.
2425 */
2426function cloner_globs_to_regexp($prefix, array $globs, $delimiter)
2427{
2428 $regexps = array();
2429 foreach ($globs as $glob) {
2430 $regexp = '';
2431 $i = 0;
2432 $len = strlen($glob);
2433 while ($i < $len) {
2434 switch ($glob[$i]) {
2435 case '*':
2436 $regexp .= '[^/]*';
2437 break;
2438 case '+':
2439 $regexp .= '[^/]+';
2440 break;
2441 case '?':
2442 $regexp .= '[^/]';
2443 break;
2444 case '[':
2445 $end = strpos(substr($glob, $i + 1), ']');
2446 if ($end === false || $end === 0) {
2447 // No enclosing ], ignore the opening [.
2448 $regexp .= preg_quote($glob[$i], $delimiter);
2449 break;
2450 }
2451 $match = substr($glob, $i + 1, $end);
2452 $flag = '';
2453 if (strlen($match) > 1) {
2454 preg_match('{(\\+|\\*|\\*\\d+|\\*\\d+,\\d*|\\*\\d+,|\\*,\\d+)$}', $match, $flags);
2455 // One of "+", "*", "*32", "*16,32", "*,32", "*32,"
2456 if (count($flags)) {
2457 $match = substr($match, 0, -strlen($flags[0]));
2458 $flag = substr($flags[0], 0, 1);
2459 if (strlen($flags[0]) > 1) {
2460 $flag = '{'.substr($flags[0], 1).'}';
2461 }
2462 }
2463 }
2464 $regexp .= '['.$match.']'.$flag;
2465 $i += $end + 1;
2466 break;
2467 case '{':
2468 $end = strpos(substr($glob, $i + 1), '}');
2469 if ($end === false || $end === 0) {
2470 // No enclosing }, ignore the opening {.
2471 $regexp .= preg_quote($glob[$i], $delimiter);
2472 break;
2473 }
2474 $matches = substr($glob, $i + 1, $end);
2475 $flag = '';
2476 $regexp .= '(?:'.strtr($matches, ',', '|').')'.$flag;
2477 $i += $end + 1;
2478 break;
2479 default:
2480 $regexp .= preg_quote($glob[$i], $delimiter);
2481 break;
2482 }
2483 $i++;
2484 }
2485 $regexps[] = $regexp;
2486 }
2487 return preg_quote($prefix, $delimiter).'(?:'.implode('|', $regexps).')';
2488}
2489
2490class ClonerStatCollector implements ClonerFSVisitor
2491{
2492 public $result;
2493 private $maxCount = 0;
2494 private $maxPayload = 0;
2495 private $deadline = 0;
2496 private $contentDir = '';
2497 private $pluginsDir = '';
2498 private $muPluginsDir = '';
2499 private $uploadsDir = '';
2500 private $skipPaths = array();
2501
2502 private $payload = 0;
2503
2504 const ANY_DIR = '';
2505 const ONLY_FILE = 'file';
2506 const ONLY_DIR = 'dir';
2507 const CONTENT_DIR = 'wp-content';
2508 const PLUGINS_DIR = 'wp-content/plugins';
2509 const MU_PLUGINS_DIR = 'wp-content/mu-plugins';
2510 const UPLOADS_DIR = 'wp-content/uploads';
2511
2512 /** @var string[] Core files are matched by their full path and are invisible to the process. */
2513 private static $ignoreFiles = array('cloner.php', 'cloner_error_log', 'mwp_db');
2514
2515 /**
2516 * @see cloner_globs_to_regexp for available patterns.
2517 * @var string[]
2518 */
2519 private $includeList = array(
2520 // Core WP files.
2521 //'wp-admin', 'wp-admin/*', 'wp-includes', 'wp-includes/*', 'index.php', 'license.txt', 'readme.html',
2522 //'wp-activate.php', 'wp-blog-header.php', 'wp-comments-post.php', 'wp-config.php', 'wp-config-sample.php', 'wp-cron.php', 'wp-links-opml.php',
2523 //'wp-load.php', 'wp-login.php', 'wp-mail.php', 'wp-settings.php', 'wp-signup.php', 'wp-trackback.php', 'xmlrpc.php',
2524 // Extra files.
2525 '*.ico', 'robots.txt', 'gd-config.php', '.htaccess',
2526 // Backup v1 rule:
2527 'wp-*', 'index.php', 'license.txt', 'readme.html', 'xmlrpc.php',
2528 );
2529
2530 /**
2531 * Filters used to skip files in their respected locations.
2532 * @see cloner_globs_to_regexp for available patterns.
2533 * @var array[]
2534 */
2535 private $excludeLists = array(
2536 // File name filters, check only files by name, the rest of the path is ignored.
2537 self::ANY_DIR => array('.svn', '.cvs', '.idea', '.DS_Store', '.git', '.hg', '*.hprof', '*.pyc',
2538 '404.log.txt', '.ftpquota', '.listing', '.mt_backup*', 'timthumb.txt', '.tweetcache', '.wpress', '.tmp',
2539 '.lwbak', '.X1-unix', 'process.log', 'errors.log', '.pf_debug_output.txt', 'php-errors.log',
2540 '.sass-cache', '__wpe_admin_ajax.log', 'cgi-bin', 'debug.log', 'error.log', 'php-cgi.core', 'core.[0-9+]',
2541 'wp-retina-2x.log', 'log.txt', 'php_errorlog', 'php_mail.log', 'sess[0-9a-z*32]', 'timthumb[0-9a-z*]',
2542 'vp-uploaded-restore-*', 'wc-logs', 'WS_FTP.LOG', '*-wprbackups', '_private', '_sucuribackup*',
2543 '_vti_[a-z*]', 'node_modules', '__MACOSX',
2544 '{backup,snapshot,restore,akeebabackupwp}.{tar,gz,zip,rar,7z,jpa,sql}', 'pclzip-*'),
2545 self::ONLY_FILE => array('error_log'),
2546 self::ONLY_DIR => array(),
2547 // Full path filter for descendents of "wp-content/".
2548 self::CONTENT_DIR => array('backupbuddy_backups', 'cmscommander/backups', 'wflogs', 'ithemes-security/backups',
2549 'mainwp/backup', 'managewp/backups', 'mgm/exports', 'mwpbackups', 'updraft/*.zip', 'LocalGiant_WPF/session_store',
2550 'security.log', 'bte-wb', 'dml-logs', 'nfwlog', 'mysql.sql', 'backup-[0-9a-z*]', 'backups.*', 'backupwordpress',
2551 'bps-backup', 'codeguard_backups', 'infinitewp', 'old-cache', 'updraft', 'tCapsule/backups', 'sedlex/backup-scheduler',
2552 'uploads.zip', 's3bubblebackups', 'w3tc', 'wfcache', 'upgrade', 'wpbackitup_backups', 'wishlist-backup', 'cache-remove',
2553 'pep-vn', '[0-9a-z*]-backups', 'cache', 'wp-snapshots', 'ics-importer-cache', 'gt-cache', 'backwpup', 'backwpup-*', 'backwpups',
2554 'CRSbackup'),
2555 // Full path filter for descendents of "wp-content/plugins/".
2556 self::PLUGINS_DIR => array('wordfence/tmp', 'worker/log.html', 'all-in-one-wp-migration/storage', 'easywplocalhost_migrator/temp',
2557 'wp-rss-aggregator/log-[0-9+].txt', 'wponlinebackup/tmp', 'wptouch-data/infinity-cache', 'si-captcha-for-wordpress/captcha/cache',
2558 'wp-content/mu-plugins/gd-system-plugin*' /*dir and .php file*/),
2559 // Full path filter for descendents of "wp-content/mu-plugins/".
2560 self::MU_PLUGINS_DIR => array(),
2561 // Full path filter for descendents of "wp-content/uploads/".
2562 self::UPLOADS_DIR => array('aiowps_backups', '*-backups', 'broken-link-checker', 'mainwp', 'backupbuddy_temp',
2563 'snapshots', 'wp-clone', 'sucuri', 'wp_system', 'wpcf7_captcha', 'wpallimport/uploads', 'bfi_thumb', 'wp-clone', 'essb_cache', 'ewpt_cache',
2564 'wp-migrate-db', 'wp-backup-plus', 'tCapsule/backups', 'awb', 'essb_cache', 'abovethefold', 'ninja-forms/tmp', 'backupwordpress.*',
2565 'backupbuddy*', 'pb_backupbuddy', 'backwpup', 'backwpup-*', 'backwpups', 'pp/cache', 'cache', 'elementor/tmp'),
2566 );
2567
2568 /** @var string */
2569 private $compiledIncludeList = '';
2570 /** @var string */
2571 private $compiledSystemExcludeList = '';
2572 /** @var string */
2573 private $compiledUserExcludeList = '';
2574 /** @var string */
2575 private $compiledFileExcludeList = '';
2576 /** @var string */
2577 private $compiledDirExcludeList = '';
2578
2579 public $prefix = '';
2580
2581 /**
2582 * ClonerStatCollector constructor.
2583 *
2584 * @param ClonerStatResult $result
2585 * @param int $maxCount
2586 * @param int $maxPayload
2587 * @param int $timeout
2588 * @param string $contentDir
2589 * @param string $pluginsDir
2590 * @param string $muPluginsDir
2591 * @param string $uploadsDir
2592 * @param string[] $addPaths
2593 * @param string[] $skipPaths
2594 *
2595 * @throws ClonerException
2596 */
2597 public function __construct(ClonerStatResult $result, $maxCount, $maxPayload, $timeout, $contentDir, $pluginsDir, $muPluginsDir, $uploadsDir, array $addPaths, array $skipPaths)
2598 {
2599 if (strlen($contentDir) === 0 || strlen($pluginsDir) === 0 || strlen($muPluginsDir) === 0 || strlen($uploadsDir) === 0 ||
2600 cloner_is_path_absolute($contentDir) || cloner_is_path_absolute($pluginsDir) || cloner_is_path_absolute($muPluginsDir) || cloner_is_path_absolute($uploadsDir)) {
2601 throw new ClonerException('WordPress dir is not relative to root', 'stat_error');
2602 }
2603 foreach ($addPaths as $path) {
2604 $this->includeList[] = $path;
2605 }
2606 $this->result = $result;
2607 $this->maxCount = $maxCount;
2608 $this->maxPayload = $maxPayload;
2609 $this->deadline = $timeout ? time() + $timeout : 0;
2610 $this->contentDir = $contentDir;
2611 $this->pluginsDir = $pluginsDir;
2612 $this->muPluginsDir = $muPluginsDir;
2613 $this->uploadsDir = $uploadsDir;
2614 $this->skipPaths = $skipPaths;
2615 $this->build();
2616 }
2617
2618 private function build()
2619 {
2620 $delimiter = '{}';
2621 $this->compiledIncludeList = '{^'.cloner_globs_to_regexp('', array_merge($this->includeList, array(
2622 $this->contentDir, $this->pluginsDir, $this->muPluginsDir, $this->uploadsDir)), $delimiter).'(?:$|/)}i';
2623 $this->compiledSystemExcludeList = '{(?:'
2624 .'(?:^|/)'.cloner_globs_to_regexp('', $this->excludeLists[self::ANY_DIR], $delimiter).'$|'
2625 .'^'.cloner_globs_to_regexp($this->contentDir.'/', $this->excludeLists[self::CONTENT_DIR], $delimiter).'(?:$|/)|'
2626 .'^'.cloner_globs_to_regexp($this->pluginsDir.'/', $this->excludeLists[self::PLUGINS_DIR], $delimiter).'(?:$|/)|'
2627 .'^'.cloner_globs_to_regexp($this->muPluginsDir.'/', $this->excludeLists[self::MU_PLUGINS_DIR], $delimiter).'(?:$|/)|'
2628 .'^'.cloner_globs_to_regexp($this->uploadsDir.'/', $this->excludeLists[self::UPLOADS_DIR], $delimiter).'(?:$|/))}i';
2629 if ($this->skipPaths) {
2630 $this->compiledUserExcludeList = '{^'.cloner_globs_to_regexp('', $this->skipPaths, $delimiter).'(?:$|/)}i';
2631 }
2632 if ($this->excludeLists[self::ONLY_FILE]) {
2633 $this->compiledFileExcludeList = '{^'.cloner_globs_to_regexp('', $this->excludeLists[self::ONLY_FILE], $delimiter).'$}i';
2634 }
2635 if ($this->excludeLists[self::ONLY_DIR]) {
2636 $this->compiledDirExcludeList = '{^'.cloner_globs_to_regexp('', $this->excludeLists[self::ONLY_DIR], $delimiter).'(?:$|/)}i';
2637 }
2638 }
2639
2640 public function visit($path, ClonerStatInfo $stat, Exception $e = null)
2641 {
2642 // Core files should be invisible to the collector.
2643 if (in_array($path, self::$ignoreFiles)) {
2644 throw new ClonerSkipVisitException();
2645 }
2646
2647 if (strlen($path) === 0) {
2648 $fullPath = $this->prefix;
2649 if (strlen($fullPath) === 0) {
2650 $fullPath = '.';
2651 }
2652 } elseif (strlen($this->prefix) > 0) {
2653 $fullPath = $this->prefix."/".$path;
2654 } else {
2655 $fullPath = $path;
2656 }
2657 $encoded = false;
2658 $encodedPath = cloner_encode_non_utf8($fullPath);
2659 if ($fullPath !== $encodedPath) {
2660 $fullPath = $encodedPath;
2661 $encoded = true;
2662 }
2663
2664 if (empty($e)) {
2665 $status = $this->includePath($fullPath, $stat->isDir());
2666 } else {
2667 $status = ClonerStatus::ERROR;
2668 }
2669
2670 $len = 0;
2671 if ($this->maxPayload) {
2672 $len = $this->payloadLen($path, $status, $stat, $e);
2673 }
2674
2675 if (count($this->result->stats)) {
2676 if ($this->deadline && $this->deadline <= time()) {
2677 return false;
2678 }
2679 if ($this->maxCount && count($this->result->stats) >= $this->maxCount) {
2680 return false;
2681 }
2682 if ($this->maxPayload && $this->payload + $len >= $this->maxPayload) {
2683 return false;
2684 }
2685 }
2686
2687 $this->payload += $len;
2688 if ($e !== null) {
2689 $this->result->appendError($fullPath, $encoded, ClonerStatus::ERROR, $e->getMessage());
2690 } elseif ($status) {
2691 $this->result->appendError($fullPath, $encoded, $status);
2692 throw new ClonerSkipVisitException();
2693 } else {
2694 if ($stat->isDir()) {
2695 $this->result->appendDir($fullPath, $encoded, $stat->getMTime(), $stat->getPermissions());
2696 } elseif ($stat->isLink()) {
2697 $this->result->appendLink($fullPath, $encoded, $stat->link, $stat->getPermissions());
2698 } else {
2699 $this->result->appendFile($fullPath, $encoded, $stat->getMTime(), $stat->getSize(), $stat->getPermissions());
2700 }
2701 }
2702
2703 return true;
2704 }
2705
2706 private function includePath($path, $isDir)
2707 {
2708 if ($path === '.') {
2709 return ClonerStatus::OK;
2710 }
2711 if (!preg_match($this->compiledIncludeList, $path)) {
2712 return ClonerStatus::SKIPPED;
2713 }
2714 if (preg_match($this->compiledSystemExcludeList, $path, $m)) {
2715 return ClonerStatus::SKIPPED;
2716 }
2717 if ($this->compiledUserExcludeList && preg_match($this->compiledUserExcludeList, $path)) {
2718 return ClonerStatus::USER_SKIPPED;
2719 }
2720 if ($this->compiledFileExcludeList && !$isDir && preg_match($this->compiledFileExcludeList, $path)) {
2721 return ClonerStatus::SKIPPED;
2722 }
2723 if ($this->compiledDirExcludeList && $isDir && preg_match($this->compiledDirExcludeList, $path)) {
2724 return ClonerStatus::SKIPPED;
2725 }
2726 return ClonerStatus::OK;
2727 }
2728
2729 private function payloadLen($path, $status, ClonerStatInfo $stat = null, Exception $e = null)
2730 {
2731 if ($status) {
2732 // {"path":"","status":1}
2733 return 8 + strlen($path) + 13;
2734 }
2735 if ($e !== null) {
2736 // {"path:"","status":1,"error":""}
2737 return 8 + strlen($path) + 22 + strlen($e->getMessage()) + 2;
2738 }
2739 // {"path":"","mtime":0,"size":0,"dir":0},
2740 return 9 + strlen($path) + 10 + cloner_int_len($stat->getMTime()) + 8 + cloner_int_len($stat->getSize()) + 7 + 1 + 2;
2741 }
2742}
2743
2744/**
2745 * @param int $int
2746 *
2747 * @return int
2748 */
2749function cloner_int_len($int)
2750{
2751 return (int)floor(log10($int)) + 1;
2752}
2753
2754/**
2755 * @property string $path File path, may not be UTF-8.
2756 * @property string $path64 Base64-encoded path.
2757 * @property int $size File size.
2758 * @property int $dir Directory if $dir===1.
2759 * @property int $mtime File modification time.
2760 * @property int $offset Offset for r/w ops.
2761 * @property string $hash Full file content hash.
2762 * @property string $hashes Transient hashes.
2763 * @property int $status Op status, defaults to ClonerStatus::OK.
2764 * @property string $error Op error, when $status === ClonerStatus::ERROR.
2765 * @property string $data Op data.
2766 * @property string $data64 Base64-encoded op data.
2767 * @property bool $eof End-of-file if true.
2768 * @property array $result Underlying data structure.
2769 * @property int $written Upstream/downstream report.
2770 * @property bool $isLink Symlink if $isLink===1.
2771 * @property string $link Symlink path reference.
2772 * @property int $perms Permissions.
2773 */
2774class ClonerFileInfo
2775{
2776 private $file;
2777
2778 public function __construct(array $file)
2779 {
2780 $this->file = $file;
2781 }
2782
2783 /**
2784 * @param $name
2785 *
2786 * @return int|string|array
2787 *
2788 * @throws ClonerException
2789 */
2790 public function __get($name)
2791 {
2792 switch ($name) {
2793 case 'path':
2794 if (!empty($this->file['encoded'])) {
2795 return urldecode(base64_decode($this->file['path']));
2796 }
2797 return base64_decode($this->file['path']);
2798 case 'path64':
2799 if (!empty($this->file['encoded'])) {
2800 return urldecode($this->file['path']);
2801 }
2802 return $this->file['path'];
2803 case 'size':
2804 return $this->file['size'];
2805 case 'dir':
2806 return $this->file['type'] === 1;
2807 case 'isLink':
2808 return $this->file['type'] === 2;
2809 case 'link':
2810 return isset($this->file['link64']) ? base64_decode($this->file['link64']) : '';
2811 case 'mtime':
2812 return $this->file['mtime'];
2813 case 'offset':
2814 return isset($this->file['offset']) ? $this->file['offset'] : 0;
2815 case 'hash':
2816 return isset($this->file['hash']) ? $this->file['hash'] : '';
2817 case 'hashes':
2818 return isset($this->file['hashes']) ? $this->file['hashes'] : '';
2819 case 'status':
2820 return isset($this->file['status']) ? $this->file['status'] : 0;
2821 case 'error':
2822 return isset($this->file['error']) ? $this->file['error'] : '';
2823 case 'data':
2824 return isset($this->file['data64']) ? base64_decode($this->file['data64']) : '';
2825 case 'data64':
2826 return isset($this->file['data64']) ? $this->file['data64'] : '';
2827 case 'written':
2828 return isset($this->file['written']) ? $this->file['written'] : 0;
2829 case 'eof':
2830 return isset($this->file['eof']) ? $this->file['eof'] : false;
2831 case 'result':
2832 return $this->file;
2833 default:
2834 throw new ClonerException("Unrecognized file property: $name");
2835 }
2836 }
2837}
2838
2839/**
2840 * @param string $action Action name.
2841 * @param array $params Action parameters.
2842 *
2843 * @return mixed Whatever is returned by the action called.
2844 *
2845 * @throws Exception
2846 */
2847function cloner_run_action($action, array $params)
2848{
2849 switch ($action = (string)$action) {
2850 case 'ping':
2851 return cloner_action_ping();
2852 case 'stat':
2853 return cloner_action_stat($params['root'], $params['cursor'], $params['cursorEncoded'], $params['maxCount'], $params['maxPayload'], $params['configPath'], $params['contentDir'], $params['pluginsDir'], $params['muPluginsDir'], $params['uploadsDir'], $params['timeout'], $params['addPaths'], $params['skipPaths']);
2854 case 'hash':
2855 return cloner_action_hash($params['root'], $params['files'], $params['tempHashes'], $params['timeout'], $params['maxHashSize'], $params['chunkSize'], $params['hashBufSize']);
2856 case 'touch':
2857 return cloner_action_touch($params['root'], $params['files'], $params['timeout']);
2858 case 'read':
2859 return cloner_action_read($params['root'], $params['id'], $params['files'], $params['lastOffset'], $params['limit']);
2860 case 'write':
2861 return cloner_action_write($params['root'], $params['files'], $params['lastOffset']);
2862 case 'push':
2863 return cloner_action_push($params['root'], $params['remoteRoot'], $params['id'], $params['remoteID'], $params['files'], $params['url'], $params['lastOffset'], $params['limit']);
2864 case 'pull':
2865 return cloner_action_pull($params['root'], $params['remoteRoot'], $params['remoteID'], $params['files'], $params['url'], $params['lastOffset'], $params['limit']);
2866 case 'list_tables':
2867 return cloner_action_list_tables($params['db']);
2868 case 'hash_tables':
2869 return cloner_action_hash_tables($params['db'], $params['tables'], $params['timeout']);
2870 case 'dump_tables':
2871 return cloner_action_dump_tables($params['root'], $params['id'], $params['db'], $params['state'], $params['timeout']);
2872 case 'delete_files':
2873 return cloner_action_delete_files($params['root'], $params['files'], $params['id'], $params['errorLogSize']);
2874 case 'flush_rewrite_rules':
2875 return cloner_action_flush_rewrite_rules($params['db'], $params['id'], $params['prefix'], $params['timeout']);
2876 case 'heartbeat':
2877 return cloner_action_heartbeat($params['db'], $params['prefix'], $params['id']);
2878 case 'import_database':
2879 return cloner_action_import_database($params['root'], $params['db'], $params['state'], $params['oldPrefix'], $params['newPrefix'], $params['maxCount'], $params['timeout']);
2880 case 'set_admin':
2881 return cloner_action_set_admin($params['db'], $params['prefix'], $params['username'], $params['password'], $params['email']);
2882 case 'set_options':
2883 return cloner_action_set_options($params['db'], $params['prefix'], $params['options']);
2884 case 'migrate_database':
2885 return cloner_action_migrate_database($params['db'], $params['timeout'], $params['state']);
2886 case 'connect_back':
2887 return cloner_action_connect_back($params['addr'], $params['path'], $params['origin'], $params['connTimeout'], $params['rwTimeout'], $params['cert']);
2888 case 'cleanup':
2889 return cloner_action_cleanup($params['root']);
2890 case 'get_local_env':
2891 return cloner_action_get_local_env();
2892 case 'get_static_env':
2893 return cloner_action_get_static_env($params['root']);
2894 case 'get_ftp_env':
2895 return cloner_action_get_ftp_env($params['db'], $params['tablePrefix'], $params['wpConfig']);
2896 default:
2897 throw new ClonerException("Action \"$action\" not found", 'action_not_found');
2898 }
2899}
2900
2901/** @noinspection SqlNoDataSourceInspection */
2902
2903
2904
2905
2906
2907
2908function cloner_action_ping()
2909{
2910 return "pong";
2911}
2912
2913function cloner_action_delete_files($root, array $files, $id, $errorLogSize = 0)
2914{
2915 $ok = true;
2916 $errs = "Could not remove the following files:";
2917 $errorLog = '';
2918 if ($errorLogSize !== 0) {
2919 $errorLog = @file_get_contents('cloner_error_log', false, null, 0, $errorLogSize);
2920 if ($errorLog === false) {
2921 $errorLog = sprintf('unable to read error log: %s', cloner_last_error_for('file_get_contents'));
2922 }
2923 }
2924 foreach ($files as $file) {
2925 $err = cloner_remove_file_or_dir($root.'/'.$file);
2926 if (strlen($err)) {
2927 $errs .= " $file ($err);";
2928 $ok = false;
2929 }
2930 }
2931 if (strlen($id)) {
2932 $temp = sys_get_temp_dir()."/mwp_db$id";
2933 $err = cloner_remove_file_or_dir($temp);
2934 if (strlen($err)) {
2935 $errs .= " $temp ($err);";
2936 $ok = false;
2937 }
2938 }
2939 return array(
2940 'ok' => $ok,
2941 'error' => $ok ? null : $errs,
2942 'errorLog' => base64_encode($errorLog),
2943 );
2944}
2945
2946/**
2947 * @param string $root
2948 * @param ClonerDBConn|array $db
2949 * @param string $id
2950 * @param string $prefix
2951 * @param int $timeout
2952 *
2953 * @return array
2954 *
2955 * @throws ClonerException
2956 */
2957function cloner_action_flush_rewrite_rules($db, $id, $prefix, $timeout)
2958{
2959 $conn = cloner_db_connection($db);
2960 if (function_exists('w3tc_pgcache_flush')) {
2961 w3tc_pgcache_flush();
2962 }
2963 if (function_exists('w3tc_dbcache_flush')) {
2964 w3tc_dbcache_flush();
2965 }
2966 if (function_exists('w3tc_objectcache_flush')) {
2967 w3tc_objectcache_flush();
2968 }
2969 if (isset($_SERVER['GD_PHP_HANDLER'], $_SERVER['GD_ERROR_DOC'])) {
2970 // GoDaddy managed wordpress hosting, manually flush cache.
2971 do_action('flush_cache', array('ban' => 1, 'urls' => array()));
2972 }
2973 if (class_exists('ESSBDynamicCache') && is_callable(array('ESSBDynamicCache', 'flush'))) {
2974 /** @noinspection PhpUndefinedClassInspection */
2975 ESSBDynamicCache::flush();
2976 }
2977 if (function_exists('purge_essb_cache_static_cache')) {
2978 purge_essb_cache_static_cache();
2979 }
2980 if (is_multisite()) {
2981 /** @noinspection PhpUndefinedFunctionInspection */
2982 add_filter('mod_rewrite_rules', array(__CLASS__, 'msRewriteRules'));
2983 }
2984 /** @noinspection PhpUndefinedFunctionInspection */
2985 flush_rewrite_rules(true);
2986 $ok = false;
2987 if ($timeout) {
2988 $done = time();
2989 while ($timeout) {
2990 $row = $conn->query("SELECT option_value FROM {$prefix}options WHERE option_name = 'clone_heartbeat_{$id}'")->fetch();
2991 $heartbeat = is_array($row) ? (int)end($row) : 0;
2992 if ($heartbeat - 1 > $done) {
2993 // We hit PHP after the creation of rewrite rules - everything looks ok.
2994 $ok = true;
2995 break;
2996 }
2997 $timeout--;
2998 sleep(1);
2999 }
3000 $conn->query("DELETE FROM {$prefix}options WHERE option_name = 'clone_heartbeat_{$id}'");
3001 if (!$ok) {
3002 foreach (array(ABSPATH.'.htaccess', ABSPATH.'../.htaccess') as $path) {
3003 unlink($path);
3004 }
3005 }
3006 }
3007 // From wp-admin/admin.php
3008 global $wp_db_version;
3009 /** @noinspection PhpUndefinedFunctionInspection */
3010 if ($ok && get_option('db_version') != $wp_db_version) {
3011 if (!function_exists('wp_upgrade')) {
3012 /** @noinspection PhpIncludeInspection */
3013 require_once cloner_constant('ABSPATH').'wp-admin/includes/upgrade.php';
3014 }
3015 ob_start();
3016 /** @noinspection PhpUndefinedFunctionInspection */
3017 wp_upgrade();
3018 do_action('after_db_upgrade');
3019 ob_end_clean();
3020 }
3021 return array('ok' => $ok);
3022}
3023
3024/**
3025 * @param string $root
3026 * @param ClonerDBConn|array $db
3027 * @param array $state
3028 * @param string $oldPrefix
3029 * @param string $newPrefix
3030 * @param int $maxCount
3031 * @param int $timeout
3032 *
3033 * @return ClonerDBImportState
3034 *
3035 * @throws ClonerException
3036 * @throws ClonerFSFunctionException
3037 */
3038function cloner_action_import_database($root, $db, array $state, $oldPrefix, $newPrefix, $maxCount, $timeout)
3039{
3040 $conn = cloner_db_connection($db);
3041 $state = ClonerDBImportState::fromArray($state, 10 << 10);
3042 $filters = array();
3043 $lowerPrefix = strtolower($newPrefix);
3044 if ($oldPrefix !== $newPrefix) {
3045 $filters[] = new ClonerPrefixFilter($oldPrefix, $newPrefix);
3046 } elseif ($lowerPrefix !== $newPrefix) {
3047 // Prefix contains uppercase characters, meaning that if the origin is Windows the tables may actually have
3048 // lowercase names, since on Windows MySQL internally normalizes them all to lowercase.
3049 // To be safe, transform lowercase versions of table names (if any exist) to uppercase.
3050 $filters[] = new ClonerPrefixFilter($lowerPrefix, $newPrefix);
3051 }
3052 $env = cloner_env_info($root);
3053 if ($env->goDaddyPro === 2) {
3054 $filters[] = new ClonerDBStorageFilter();
3055 }
3056 return cloner_import_database($root, $conn, $timeout, $state, $maxCount, $filters);
3057}
3058
3059/**
3060 * @param ClonerDBConn|array $db
3061 * @param string $prefix
3062 * @param string $id
3063 *
3064 * @return array
3065 * @throws ClonerException
3066 */
3067function cloner_action_heartbeat($db, $prefix, $id)
3068{
3069 $conn = cloner_db_connection($db);
3070 $conn->query("INSERT INTO {$prefix}options SET option_name = 'clone_heartbeat_{$id}', option_value = :value ON DUPLICATE KEY UPDATE option_value = :value", array(
3071 'value' => time(),
3072 ));
3073
3074 return array('ok' => true);
3075}
3076
3077/**
3078 * @param ClonerDBConn|array $db
3079 * @param string $prefix
3080 * @param string $username
3081 * @param string $password
3082 * @param string $email
3083 *
3084 * @return array
3085 * @throws ClonerException
3086 */
3087function cloner_action_set_admin($db, $prefix, $username, $password, $email)
3088{
3089 $conn = cloner_db_connection($db);
3090 $adminID = cloner_get_user_id_by_username($conn, $prefix, $username);
3091 $conn->query("UPDATE {$prefix}options SET option_value = :email WHERE option_name = 'admin_email'", array(
3092 'email' => $email,
3093 ));
3094 if ($adminID) {
3095 $conn->query("UPDATE {$prefix}users SET user_pass = :password, user_email = :email WHERE ID = :user_id", array(
3096 'user_id' => $adminID,
3097 'password' => $password,
3098 'email' => $email,
3099 ));
3100 } else {
3101 /** @noinspection SqlDialectInspection */
3102 $conn->query("INSERT INTO {$prefix}users (user_login, user_pass, user_email, user_nicename, user_registered, display_name)
3103VALUES (:username, :password, :email, :slug, :now, :display_name)", array(
3104 'username' => $username,
3105 'password' => $password,
3106 'email' => $email,
3107 'slug' => cloner_sanitize_user($username),
3108 'now' => date('Y-m-d H:i:s'),
3109 'display_name' => $username,
3110 ));
3111 $newID = cloner_get_user_id_by_username($conn, $prefix, $username);
3112 if (!$newID) {
3113 throw new ClonerException('Admin user could not be saved to the database.', 'admin_not_saved');
3114 }
3115 $options = array(
3116 $prefix.'capabilities' => serialize(array('administrator' => true)),
3117 'rich_editing' => 'true',
3118 'show_admin_bar_front' => true,
3119 );
3120 foreach ($options as $name => $value) {
3121 /** @noinspection SqlDialectInspection */
3122 $conn->query("INSERT INTO {$prefix}usermeta SET user_id = :user_id, meta_key = :meta_key, meta_value = :meta_value", array(
3123 'user_id' => $newID,
3124 'meta_key' => $name,
3125 'meta_value' => $value,
3126 ));
3127 }
3128 }
3129 return array();
3130}
3131
3132/**
3133 * @param ClonerDBConn|array $db
3134 * @param string $prefix
3135 * @param array $options
3136 *
3137 * @return array
3138 *
3139 * @throws ClonerException
3140 */
3141function cloner_action_set_options($db, $prefix, array $options)
3142{
3143 $conn = cloner_db_connection($db);
3144 foreach ($options as $key => $value) {
3145 if ($value === null) {
3146 $conn->query("DELETE FROM {$prefix}options WHERE option_name = :option_name", array(
3147 'option_name' => $key,
3148 ));
3149 } else {
3150 $conn->query("INSERT INTO {$prefix}options SET option_name = :option_name, option_value = :option_value ON DUPLICATE KEY UPDATE option_value = :option_value", array(
3151 'option_name' => $key,
3152 'option_value' => $value,
3153 ));
3154 }
3155 }
3156 return array();
3157}
3158
3159/**
3160 * @param ClonerDBConn|array $db
3161 * @param float $timeout
3162 * @param array $state
3163 *
3164 * @return array
3165 * @throws Exception
3166 */
3167function cloner_action_migrate_database($db, $timeout, array $state)
3168{
3169 $conn = cloner_db_connection($db);
3170 foreach ($state as $key => &$migration) {
3171 if ($migration['done']) {
3172 continue;
3173 }
3174 switch ($migration['fn']) {
3175 case 'cloner_migrate_table_prefix':
3176 list($migration['state'], $migration['done']) = cloner_migrate_table_prefix($conn, $timeout, (array)@$migration['args'], @$migration['state']);
3177 break;
3178 case 'cloner_migrate_site_url':
3179 list($migration['state'], $migration['done']) = cloner_migrate_site_url($conn, $timeout, (array)@$migration['args'], @$migration['state']);
3180 break;
3181 default:
3182 throw new ClonerException(sprintf('Unknown migration function: %s', $migration['fn']));
3183 }
3184 if ($migration['done']) {
3185 // Timeout reached on a migration.
3186 break;
3187 }
3188 }
3189 return array('state' => $state);
3190}
3191
3192function cloner_return_false()
3193{
3194 return false;
3195}
3196
3197/**
3198 * @param string $addr Address in host:port format.
3199 * @param string $path HTTP handshake path.
3200 * @param string $origin HTTP handshake "Origin" header value.
3201 * @param string $connTimeout HTTP connect+handshake timeout.
3202 * @param string $rwTimeout Read/write timeout for single actions.
3203 * @param string $cert Certificate to use for TLS.
3204 *
3205 * @return array Nothing of use.
3206 *
3207 * @throws ClonerException
3208 */
3209function cloner_action_connect_back($addr, $path, $origin, $connTimeout, $rwTimeout, $cert)
3210{
3211 set_time_limit(12 * 3600);
3212 set_error_handler('cloner_return_false');
3213 $ws = new ClonerWebSocket($addr, $path, $connTimeout, $rwTimeout, 'localhost', $origin, $cert);
3214 $ws->connect();
3215 while (cloner_run_ws_transaction($ws)) {
3216 if (function_exists('gc_collect_cycles')) {
3217 gc_collect_cycles();
3218 }
3219 }
3220 return array();
3221}
3222
3223/**
3224 * Read message from the WebSocket, execute the action, write result.
3225 * If there was a "2006 MySQL server has gone away" it will additionally clear MySQL connection cache
3226 * before sending the error.
3227 *
3228 * @param ClonerWebSocket $ws
3229 *
3230 * @return bool
3231 *
3232 * @throws ClonerException
3233 */
3234function cloner_run_ws_transaction(ClonerWebSocket $ws)
3235{
3236 list($message, $eof) = $ws->readMessage();
3237 if ($eof) {
3238 return false;
3239 }
3240 $data = json_decode($message, true);
3241 if ($data === null || $data === false) {
3242 throw new ClonerException(sprintf("Invalid JSON payload: %s", base64_encode($message)), 'ws_invalid_json', function_exists('json_last_error') ? json_last_error() : 0);
3243 }
3244 unset($message);
3245 $id = $data['id'];
3246 try {
3247 $result = cloner_run_action($data['action'], (array)@$data['params']);
3248 unset($data);
3249 $send = json_encode(array('id' => $id, 'result' => $result));
3250 if ($send === null || $send === false) {
3251 throw new ClonerException(sprintf("JSON encode error: %s", base64_encode(serialize($result))), 'json_encode_error', function_exists('json_last_error') ? json_last_error() : 0);
3252 }
3253 } catch (ClonerException $e) {
3254 unset($send);
3255 if ($e->getInternalError() === "2006") {
3256 // MySQL server has gone away; use hacky new way to clear cache.
3257 /** @noinspection PhpUnhandledExceptionInspection */
3258 cloner_db_connection(null, true);
3259 }
3260 $context = null;
3261 foreach (get_object_vars($e) as $key => $val) {
3262 if (!is_scalar($val)) {
3263 continue;
3264 }
3265 if ($context === null) {
3266 $context = array();
3267 }
3268 $context[$key] = (string)$val;
3269 }
3270 $send = json_encode(array(
3271 'id' => $id,
3272 'error' => array(
3273 'error' => $e->getErrorCode(),
3274 'message' => $e->getMessage(),
3275 'internalError' => $e->getInternalError(),
3276 'file' => $e->getFile(),
3277 'line' => $e->getLine(),
3278 'context' => $context,
3279 ),
3280 ));
3281 }
3282 $ws->writeMessage($send);
3283 return true;
3284}
3285
3286/**
3287 * Callback for filtering out dot-files from results of scandir and such.
3288 *
3289 * @param string $value
3290 *
3291 * @return bool True if the file is NOT a dot-file.
3292 */
3293function cloner_is_not_dot($value)
3294{
3295 return $value !== '.' && $value !== '..';
3296}
3297
3298/**
3299 * Remove file or directory at $path recursively.
3300 *
3301 * @param string $path Path to the file or directory.
3302 *
3303 * @return string Error string, or empty string if there was no error.
3304 */
3305function cloner_remove_file_or_dir($path)
3306{
3307 $attempt = 0;
3308 $maxAttempts = 3;
3309 $err = '';
3310 while (true) {
3311 $attempt++;
3312 if ($attempt > $maxAttempts) {
3313 break;
3314 }
3315 $err = '';
3316 if ($attempt > 1) {
3317 usleep(100000 * pow(2, $attempt));
3318 cloner_clear_stat_cache($path);
3319 }
3320 if (!file_exists($path)) {
3321 break;
3322 }
3323 if (!is_writable($path)) {
3324 @chmod($path, 0777);
3325 }
3326 if (is_dir($path)) {
3327 foreach (@scandir($path) as $child) {
3328 if ($child === '.' || $child === '..') {
3329 continue;
3330 }
3331 if (strlen($err = cloner_remove_file_or_dir("$path/$child"))) {
3332 return $err;
3333 }
3334 }
3335 if (@rmdir($path) === false) {
3336 $err = cloner_last_error_for('rmdir');
3337 continue;
3338 }
3339 } else {
3340 if (@unlink($path) === false) {
3341 $err = cloner_last_error_for('unlink');
3342 continue;
3343 }
3344 }
3345 break;
3346 }
3347 return $err;
3348}
3349
3350/**
3351 * @param string $root Absolute path to the website root, where cloner.php (this file) resides.
3352 *
3353 * @return array
3354 */
3355function cloner_action_cleanup($root)
3356{
3357 $errs = array();
3358 $errorLog = @file_get_contents("$root/cloner_error_log", false, null, 0, 1 << 20);
3359 if ($errorLog === false) {
3360 $errorLog = "could not fetch cloner_error_log: ".cloner_last_error_for('file_get_contents');
3361 }
3362 $dumpDir = $root.'/mwp_db';
3363 $dumps = is_dir($dumpDir) ? @scandir($dumpDir) : array();
3364 if (!is_array($dumps)) {
3365 $dumps = array();
3366 $errs[] = array('mwp_db', cloner_last_error_for('scandir'));
3367 } else {
3368 $dumps = array_values(array_filter($dumps, 'cloner_is_not_dot'));
3369 foreach ($dumps as $i => $dump) {
3370 $dumps[$i] = "mwp_db/$dump";
3371 }
3372 // Remove the directory itself.
3373 $dumps[] = 'mwp_db';
3374 }
3375 $files = array_merge($dumps, array('cloner_error_log', 'cloner.php'));
3376 foreach ($files as $file) {
3377 $err = cloner_remove_file_or_dir($root.'/'.$file);
3378 if (strlen($err)) {
3379 $errs[] = array($file, $err);
3380 }
3381 }
3382 return array(
3383 'ok' => empty($errs),
3384 'errors' => $errs,
3385 'errorLog' => $errorLog,
3386 );
3387}
3388
3389class ClonerStatResult
3390{
3391 public $stats = array();
3392
3393 public function appendFile($path, $encoded, $modTime, $size, $permissions)
3394 {
3395 $this->stats[] = array('p' => base64_encode($path), 'n' => $encoded, 's' => $size, 'm' => $modTime, 'i' => $permissions);
3396 }
3397
3398 public function appendDir($path, $encoded, $modTime, $permissions)
3399 {
3400 $this->stats[] = array('p' => base64_encode($path), 'n' => $encoded, 'd' => 1, 'm' => $modTime, 'i' => $permissions);
3401 }
3402
3403 public function appendLink($path, $encoded, $reference, $permissions)
3404 {
3405 $this->stats[] = array('p' => base64_encode($path), 'n' => $encoded, 'd' => 2, 'k' => base64_encode($reference), 'i' => $permissions);
3406 }
3407
3408 public function appendError($path, $encoded, $status, $error = '')
3409 {
3410 $this->stats[] = array('p' => base64_encode($path), 'n' => $encoded, 'o' => $status, 'e' => cloner_encode_non_utf8($error));
3411 }
3412}
3413
3414/**
3415 * Eliminate paths that are located under any other path. For example:
3416 * - "wp-content" and "changed-content-dir/uploads" are located under the "" (empty, root) path; remove them as they will be traversed
3417 * - "../wp-content/mu-plugins" and "../../plugins" are located above the "" path; they will be traversed separately
3418 * - if content dir is "../wp-content" and plugins dir is "../wp-content/plugins", only the first one will be traversed, as the second
3419 * one is located under the first one.
3420 * This will make sure that all WordPress files (including directory roots) are traversed exactly once.
3421 *
3422 * @param string $cursor
3423 * @param string[] $paths
3424 *
3425 * @return string[] Unique paths that should be traversed.
3426 */
3427function cloner_remove_nested_paths($cursor, array $paths)
3428{
3429 foreach ($paths as $i => $path) {
3430 if ($path === '') {
3431 continue;
3432 }
3433 foreach ($paths as $checkPath) {
3434 if ($checkPath === '') {
3435 if (strncmp($path, '../', 3) !== 0) {
3436 // Directory is not going up, hence is located under ''.
3437 unset($paths[$i]);
3438 break;
3439 }
3440 continue;
3441 }
3442 if (strncmp($path, $checkPath, strlen($checkPath)) === 0 && substr($path, strlen($checkPath), 1) === '/') {
3443 unset($paths[$i]);
3444 break;
3445 }
3446 }
3447 }
3448 $paths = array_values(array_unique($paths));
3449 foreach ($paths as $i => $path) {
3450 $paths[$i] = strtr($path, array('../' => chr(127)));
3451 }
3452 sort($paths);
3453 foreach ($paths as $i => $path) {
3454 $paths[$i] = strtr($path, array(chr(127) => '../'));
3455 }
3456
3457 for ($i = count($paths) - 1; $i > 0; $i--) {
3458 if (strlen($cursor) && strncmp($paths[$i], $cursor, strlen($paths[$i])) === 0) {
3459 break;
3460 }
3461 }
3462 $paths = array_slice($paths, $i);
3463
3464 return $paths;
3465}
3466
3467/**
3468 * @param string $root
3469 * @param string $cursor
3470 * @param bool $cursorEncoded
3471 * @param int $maxCount
3472 * @param int $maxPayload
3473 * @param string $configPath
3474 * @param string $contentDir
3475 * @param string $pluginsDir
3476 * @param string $muPluginsDir
3477 * @param string $uploadsDir
3478 * @param string[] $addPaths
3479 * @param string[] $skipPaths
3480 * @param int $timeout
3481 *
3482 * @return array
3483 *
3484 * @throws ClonerFSException
3485 * @throws ClonerException
3486 */
3487function cloner_action_stat($root, $cursor, $cursorEncoded, $maxCount, $maxPayload, $configPath, $contentDir, $pluginsDir, $muPluginsDir, $uploadsDir, $timeout, array $addPaths = null, array $skipPaths = null)
3488{
3489 if ($cursorEncoded) {
3490 // TODO(mcolakovic): fix utf-8 issues on protocol level
3491 $cursor = urldecode($cursor);
3492 }
3493 $addPaths = (array)$addPaths;
3494 $skipPaths = (array)$skipPaths;
3495 $addPaths[] = $configPath;
3496 // We will always traverse these directories, even if they are above our rooted path.
3497 // If we are continuing traversal, cursor will start with the next path to be traversed.
3498 $traverse = cloner_remove_nested_paths($cursor, array('', $configPath, $contentDir, $pluginsDir, $muPluginsDir, $uploadsDir));
3499 if (isset($traverse[0])) {
3500 $cursor = substr($cursor, strlen($traverse[0]));
3501 }
3502
3503 $result = new ClonerStatResult();
3504 $visitor = new ClonerStatCollector($result, $maxCount, $maxPayload, $timeout, $contentDir, $pluginsDir, $muPluginsDir, $uploadsDir, $addPaths, $skipPaths);
3505 $newCursor = '';
3506 foreach ($traverse as $path) {
3507 $visitor->prefix = $path;
3508 $rootPath = rtrim("$root/$path", '/');
3509 if (strlen($rootPath) === 0) {
3510 $rootPath = '/';
3511 }
3512 $newCursor = cloner_fs_walk($rootPath, $visitor, $cursor, true);
3513 if (strlen($newCursor) !== 0) {
3514 // We hit a deadline.
3515 $newCursor = ltrim("$path/$newCursor", '/');
3516 break;
3517 }
3518 $cursor = '';
3519 // Cursor is empty, go to next path, or return.
3520 }
3521 $newCursorEncoded = cloner_encode_non_utf8($newCursor);
3522 $cursorEncoded = false;
3523 if ($newCursorEncoded !== $newCursor) {
3524 $newCursor = $newCursorEncoded;
3525 $cursorEncoded = true;
3526 }
3527 return array(
3528 'stats' => $result->stats,
3529 'cursor' => $newCursor,
3530 'cursorEncoded' => $cursorEncoded,
3531 );
3532}
3533
3534class ClonerStatus
3535{
3536 const OK = 0;
3537 const IN_PROGRESS = 1;
3538 const NO_PARENT = 2;
3539 const IS_DIR = 3;
3540 const IS_FILE = 4;
3541 const SKIPPED = 5;
3542 const ERROR = 6;
3543 const REMOTE_ERROR = 7;
3544 const NOT_UTF8 = 8;
3545 const NO_FILE = 9;
3546 const HASH_MISSING = 10;
3547 const USER_SKIPPED = 11;
3548}
3549
3550class ClonerTouchResult
3551{
3552 public $files = array();
3553
3554 public function appendOK()
3555 {
3556 // Force default status so it doesn't json_encode into an array.
3557 $this->files[] = array('s' => 0);
3558 }
3559
3560 public function appendError($error)
3561 {
3562 $this->files[] = array('e' => $error);
3563 }
3564}
3565
3566function cloner_action_touch($root, array $files, $timeout)
3567{
3568 $result = new ClonerTouchResult();
3569 $deadline = new ClonerDeadline($timeout);
3570 foreach ($files as $i => $file) {
3571 if (count($result->files) !== 0 && $deadline->done()) {
3572 break;
3573 }
3574 $file = new ClonerFileInfo($file);
3575 $fullPath = "$root/$file->path";
3576 if (@touch($fullPath, $file->mtime) === false) {
3577 $result->appendError(cloner_last_error_for('touch'));
3578 continue;
3579 }
3580 $result->appendOK();
3581 }
3582 return $result;
3583}
3584
3585class ClonerHashResult
3586{
3587 public $hashes = array();
3588 public $tempHashes = '';
3589
3590 public function appendOK($hash)
3591 {
3592 $this->hashes[] = array('h' => $hash);
3593 }
3594
3595 public function appendTempHashes($hashes)
3596 {
3597 $this->hashes[] = array('t' => true);
3598 $this->tempHashes = $hashes;
3599 }
3600
3601 public function appendError($error)
3602 {
3603 $this->hashes[] = array('o' => ClonerStatus::ERROR, 'e' => $error);
3604 }
3605}
3606
3607function cloner_action_hash($root, array $files, $tempHashes, $timeout, $maxHashSize, $chunkSize, $hashBufSize)
3608{
3609 $hashLen = 32;
3610 $result = new ClonerHashResult();
3611 $deadline = new ClonerDeadline($timeout);
3612
3613 foreach ($files as $file) {
3614 $file = new ClonerFileInfo($file);
3615 if ($file->dir) {
3616 $result->appendError('cannot hash dir');
3617 continue;
3618 }
3619 if (count($result->hashes) !== 0 && $deadline->done()) {
3620 break;
3621 }
3622 $hashes = $tempHashes;
3623 $tempHashes = '';
3624 try {
3625 $filePath = "$root/$file->path";
3626 $stat = cloner_fs_stat($filePath);
3627 if ($stat->getSize() !== $file->size) {
3628 throw new ClonerException(sprintf("size changed, was %d, now is %d", $file->size, $stat->getSize()));
3629 }
3630 $hash = 'd41d8cd98f00b204e9800998ecf8427e'; // md5('')
3631 if ($file->size === 0) {
3632 // Zero-length file.
3633 $result->appendOK($hash);
3634 continue;
3635 } elseif ($file->size <= $maxHashSize) {
3636 // Single-chunk file.
3637 $hash = md5_file($filePath);
3638 if ($hash === false) {
3639 $result->appendError(cloner_last_error_for('md5_file'));
3640 continue;
3641 }
3642 $result->appendOK($hash);
3643 continue;
3644 }
3645 // Chunk hash.
3646 $parts = (int)ceil($stat->getSize() / $chunkSize);
3647 for ($i = strlen($hashes) / $hashLen; $i < $parts; $i++) {
3648 if (($fh = @fopen($filePath, 'rb')) === false) {
3649 throw new ClonerFSFunctionException('fopen', $filePath);
3650 }
3651 $limit = ($parts === 1) ? $file->size : min($chunkSize, $file->size - $i * $chunkSize);
3652 if ($i !== 0 && (@fseek($fh, $i * $chunkSize) === false)) {
3653 throw new ClonerFSFunctionException('fseek', $filePath);
3654 }
3655
3656 if (($ctx = @hash_init('md5')) === false) {
3657 throw new ClonerFunctionException('hash_init');
3658 }
3659 while ($limit > 0) {
3660 // Limit chunk size to either our remaining chunk or max chunk size
3661 $read = min($limit, $hashBufSize);
3662 $limit -= $read;
3663 if (($chunk = @fread($fh, $read)) === false) {
3664 throw new ClonerFSException('fread', $filePath);
3665 }
3666 if (@hash_update($ctx, $chunk) === false) {
3667 throw new ClonerFunctionException('hash_update');
3668 }
3669 }
3670 @fclose($fh);
3671 if (($hash = @hash_final($ctx)) === false) {
3672 throw new ClonerFunctionException('hash_final');
3673 }
3674
3675 if ($i + 1 === $parts) {
3676 // Last (and maybe only) part.
3677 if (strlen($hashes) !== 0) {
3678 // End of multipart hash.
3679 $hash = md5($hashes.$hash);
3680 }
3681 // Break will happen here.
3682 } else {
3683 $hashes .= $hash;
3684 // Need to hash more parts.
3685 if ($deadline->done()) {
3686 $result->appendTempHashes($hashes);
3687 return $result;
3688 }
3689 }
3690 }
3691 $result->appendOK($hash);
3692 } catch (Exception $e) {
3693 if (isset($fh) && is_resource($fh)) {
3694 @fclose($fh);
3695 }
3696 $result->appendError($e->getMessage());
3697 }
3698 }
3699 return $result;
3700}
3701
3702class ClonerReadResult
3703{
3704 public $files = array();
3705 public $lastOffset = 0;
3706
3707 public function appendEOF($data)
3708 {
3709 $this->files[] = array('b' => base64_encode($data), 'f' => true);
3710 }
3711
3712 public function appendChunk($data)
3713 {
3714 $this->files[] = array('b' => base64_encode($data));
3715 }
3716
3717 public function appendError($status, $error)
3718 {
3719 $this->files[] = array('o' => $status, 'e' => $error);
3720 }
3721}
3722
3723/**
3724 * @param string $root
3725 * @param string $id
3726 * @param array $files
3727 * @param int $lastOffset
3728 * @param int $limit
3729 *
3730 * @return ClonerReadResult
3731 *
3732 * @throws ClonerException
3733 */
3734function cloner_action_read($root, $id, array $files, $lastOffset, $limit)
3735{
3736 $result = new ClonerReadResult();
3737 $cursor = 0;
3738 if ($limit <= 0) {
3739 throw new ClonerException('Limit must be greater than zero');
3740 }
3741 foreach ($files as $file) {
3742 $offset = $lastOffset;
3743 $lastOffset = 0;
3744 $file = new ClonerFileInfo($file);
3745 if ($cursor >= $limit && count($result->files) !== 0) {
3746 break;
3747 }
3748 if ($file->dir) {
3749 $result->appendEOF('');
3750 continue;
3751 }
3752 $fullPath = "$root/$file->path";
3753 if (strncmp($file->path, 'mwp_db/', 7) === 0) {
3754 $tryFullPath = sys_get_temp_dir()."/mwp_db$id/".substr($file->path, 7);
3755 if (@filesize($tryFullPath) === $file->size) {
3756 $fullPath = $tryFullPath;
3757 }
3758 }
3759 if (($size = @filesize($fullPath)) === false) {
3760 $result->appendError(ClonerStatus::ERROR, cloner_last_error_for('filesize'));
3761 continue;
3762 }
3763 if ($size !== $file->size) {
3764 $result->appendError(ClonerStatus::ERROR, "file size changed to $size bytes, expected $file->size bytes");
3765 continue;
3766 }
3767 $maxLen = min($limit - $cursor, $file->size);
3768 list($content, $eof, $err) = cloner_get_file_chunk($fullPath, $offset, $maxLen);
3769 if (strlen($err)) {
3770 $result->appendError(ClonerStatus::ERROR, $err);
3771 continue;
3772 }
3773 if ($eof) {
3774 if (strlen($content) + $offset !== $file->size) {
3775 $error = sprintf('expected to read %d bytes at offset %d, but got only %d', $file->size - $offset, $offset, strlen($content));
3776 $result->appendError(ClonerStatus::ERROR, $error);
3777 continue;
3778 }
3779 $result->appendEOF($content);
3780 $cursor += strlen($content);
3781 continue;
3782 }
3783 if ($file->size <= $offset + strlen($content)) {
3784 $result->appendError(ClonerStatus::ERROR, sprintf('file size was %d bytes, but %d bytes were read', $file->size, $offset + strlen($content)));
3785 continue;
3786 }
3787 $result->appendChunk($content);
3788 $result->lastOffset = $offset + strlen($content);
3789 break;
3790 }
3791 return $result;
3792}
3793
3794/**
3795 * @param string $path
3796 * @param int $offset
3797 * @param int $limit
3798 *
3799 * @return array Three elements, chunk (string), eof (bool), error (string).
3800 * @link https://www.ibm.com/developerworks/library/os-php-readfiles/index.html
3801 *
3802 */
3803function cloner_get_file_chunk($path, $offset, $limit)
3804{
3805 $fp = @fopen($path, 'rb');
3806 if ($fp === false) {
3807 return array('', false, cloner_last_error_for('fopen'));
3808 }
3809 if ($offset) {
3810 if (@fseek($fp, $offset) !== 0) {
3811 return array('', false, cloner_last_error_for('fseek'));
3812 }
3813 }
3814
3815 $content = '';
3816 $need = $limit;
3817 $eof = false;
3818 while (!$eof && $need > 0) {
3819 $chunk = @fread($fp, $need);
3820 if ($chunk === false) {
3821 $err = cloner_last_error_for('fread');
3822 @fclose($fp);
3823 return array('', false, $err);
3824 }
3825 $content .= $chunk;
3826 $eof = @feof($fp);
3827 $need -= strlen($chunk);
3828 }
3829 if (!$eof) {
3830 // Buffer full; peek 1 byte to see if we reached eof.
3831 @fread($fp, 1);
3832 $eof = @feof($fp);
3833 }
3834 @fclose($fp);
3835
3836 return array($content, $eof, '');
3837}
3838
3839class ClonerWriteResult
3840{
3841 public $files = array();
3842 public $lastOffset = 0;
3843
3844 public function appendOK($written)
3845 {
3846 $this->files[] = array('w' => $written);
3847 }
3848
3849 public function appendError($status, $error)
3850 {
3851 $this->files[] = array('o' => $status, 'e' => $error);
3852 }
3853}
3854
3855/**
3856 * @param string $root
3857 * @param array $files
3858 * @param int $lastOffset
3859 *
3860 * @return ClonerWriteResult
3861 */
3862function cloner_action_write($root, array $files, $lastOffset)
3863{
3864 $result = new ClonerWriteResult();
3865 foreach ($files as $file) {
3866 $file = new ClonerFileInfo($file);
3867 $offset = $lastOffset;
3868 $lastOffset = 0;
3869 if ($file->dir) {
3870 if (strlen($error = cloner_make_dir($root, $file->path))) {
3871 $result->appendError(ClonerStatus::ERROR, $error);
3872 continue;
3873 }
3874 $result->appendOK(0);
3875 continue;
3876 }
3877 if (strlen($error = cloner_make_dir($root, dirname($file->path)))) {
3878 $result->appendError(ClonerStatus::ERROR, $error);
3879 continue;
3880 }
3881 $filePath = "$root/$file->path";
3882 if ($file->isLink) {
3883 @unlink($filePath);
3884 if (@symlink($file->link, $filePath) === false) {
3885 $result->appendError(ClonerStatus::ERROR, cloner_last_error_for('symlink'));
3886 } else {
3887 $result->appendOK(0);
3888 }
3889 continue;
3890 }
3891 $data = $file->data;
3892 $error = cloner_write_file($filePath, $offset, $data);
3893 if (strlen($error)) {
3894 $result->appendError(ClonerStatus::ERROR, $error);
3895 continue;
3896 }
3897 $result->appendOk(strlen($data));
3898 if (!$file->eof) {
3899 $lastOffset = $offset + strlen($data);
3900 continue;
3901 }
3902 if ($file->mtime) {
3903 @touch($filePath, $file->mtime);
3904 }
3905 }
3906 $result->lastOffset = $lastOffset;
3907 return $result;
3908}
3909
3910/**
3911 * @param string $root
3912 * @param string $remoteRoot
3913 * @param string $id
3914 * @param string $remoteID
3915 * @param array $files
3916 * @param string $url
3917 * @param int $lastOffset
3918 * @param int $limit
3919 *
3920 * @return array Result of remote cloner_action_write call.
3921 *
3922 * @throws ClonerActionException
3923 * @throws ClonerException
3924 * @throws ClonerURLException
3925 */
3926function cloner_action_push($root, $remoteRoot, $id, $remoteID, array $files, $url, $lastOffset, $limit)
3927{
3928 $results = array();
3929 $payload = array();
3930 $sent = array();
3931 $readResult = cloner_action_read($root, $id, $files, $lastOffset, $limit);
3932 foreach ($readResult->files as $i => $readOp) {
3933 if (!empty($readOp['o'])) {
3934 $results[] = $readOp;
3935 continue;
3936 }
3937 $writeOp = $files[$i] + array(
3938 'data64' => $readOp['b'],
3939 'eof' => empty($readOp['f']) ? false : true,
3940 );
3941 $payload[] = $writeOp;
3942 $sent[] = $i;
3943 $results[] = null;
3944 }
3945 $action = new ClonerAction('write', array('files' => $payload, 'lastOffset' => $lastOffset, 'root' => $remoteRoot, 'id' => $remoteID));
3946 $result = cloner_send_action(ClonerURL::fromString($url), $action);
3947 foreach ($result['files'] as $i => $writeOpResult) {
3948 $results[$sent[$i]] = $writeOpResult;
3949 }
3950 return array(
3951 'files' => $results,
3952 'lastOffset' => $result['lastOffset'],
3953 );
3954}
3955
3956/**
3957 * @param string $root
3958 * @param string $remoteRoot
3959 * @param string $remoteID
3960 * @param array $files
3961 * @param string $url
3962 * @param int $lastOffset
3963 * @param int $limit
3964 *
3965 * @return array Appends status, error.
3966 *
3967 * @throws ClonerActionException
3968 * @throws ClonerURLException
3969 */
3970function cloner_action_pull($root, $remoteRoot, $remoteID, array $files, $url, $lastOffset, $limit)
3971{
3972 $results = array();
3973 $payload = array();
3974 $sent = array();
3975 $action = new ClonerAction('read', array('files' => $files, 'lastOffset' => $lastOffset, 'limit' => $limit, 'root' => $remoteRoot, 'id' => $remoteID));
3976 /** @var ClonerReadResult $reaction */
3977 $reaction = cloner_send_action(ClonerURL::fromString($url), $action);
3978 foreach ($reaction['files'] as $i => $readOp) {
3979 // See ClonerReadResult structure.
3980 if (!empty($readOp['o'])) {
3981 $results[] = $readOp;
3982 continue;
3983 }
3984 $writeOp = $files[$i] + array(
3985 'data64' => $readOp['b'],
3986 'eof' => empty($readOp['f']) ? false : true,
3987 );
3988 $payload[] = $writeOp;
3989 $sent[] = $i;
3990 $results[] = null;
3991 }
3992 $result = cloner_action_write($root, $payload, $lastOffset);
3993 foreach ($result->files as $i => $writeOpResult) {
3994 $results[$sent[$i]] = $writeOpResult;
3995 }
3996 return array(
3997 'files' => $results,
3998 'lastOffset' => $result->lastOffset,
3999 );
4000}
4001
4002/**
4003 * @return array
4004 *
4005 * @throws ClonerException
4006 */
4007function cloner_action_get_local_env()
4008{
4009 if (!function_exists('__cloner_get_state')) {
4010 throw new ClonerException('Environment unavailable', 'env_unavailable');
4011 }
4012 return __cloner_get_state();
4013}
4014
4015/**
4016 * @param array|ClonerDBInfo|null $db
4017 * @param string $tablePrefix
4018 * @param string $wpConfig
4019 *
4020 * @return array
4021 *
4022 * @throws ClonerException
4023 */
4024function cloner_action_get_ftp_env($db, $tablePrefix, $wpConfig)
4025{
4026 $root = dirname(__FILE__);
4027 $wpConfig = base64_decode($wpConfig);
4028
4029 if (!empty($db)) {
4030 $clonerDbInfo = ClonerDBInfo::fromArray($db);
4031 $wpConfigPath = 'wp-config.php';
4032 } else {
4033 if ($wpConfig) {
4034 $wpConfigPath = 'wp-config.php';
4035 } else {
4036 list($wpConfigPath, $wpConfig) = cloner_env_read_wp_config($root, false);
4037 }
4038 $wpConfigInfo = cloner_env_parse_wp_config($wpConfig);
4039 $clonerDbInfo = new ClonerDBInfo($wpConfigInfo->dbUser, $wpConfigInfo->dbPassword, $wpConfigInfo->dbHost, $wpConfigInfo->dbName);
4040 $tablePrefix = $wpConfigInfo->wpTablePrefix;
4041 }
4042
4043 $conn = cloner_db_connection($clonerDbInfo);
4044 try {
4045 $url = cloner_get_option($conn, $tablePrefix, 'siteurl');
4046 } catch (ClonerException $e) {
4047 $url = '';
4048 }
4049 $clonerWpInfo = new ClonerWPInfo($url, $root, $tablePrefix, $wpConfigPath, $wpConfig, '', '', '', '', '');
4050
4051 $result = new ClonerSetupResult($clonerDbInfo, $clonerWpInfo, cloner_env_info($root));
4052 return array('ok' => true) + $result->toArray();
4053}
4054
4055/**
4056 * @param ClonerDBConn|array $db
4057 *
4058 * @return array
4059 *
4060 * @throws ClonerException
4061 */
4062function cloner_action_list_tables($db)
4063{
4064 $conn = cloner_db_connection($db);
4065 $tables = $conn->query('SELECT `table_name` AS `name`, `data_length` AS `dataSize`
4066FROM information_schema.TABLES WHERE table_schema = :db_name AND table_type = :table_type AND engine IS NOT NULL', array(
4067 // The NULL `engine` tables usually have `table_comment` == "Table 'forrestl_wrdp1.wp_wpgmza_categories' doesn't exist in engine".
4068 'db_name' => $conn->getConfiguration()->name,
4069 'table_type' => 'BASE TABLE', // as opposed to VIEW
4070 ))->fetchAll();
4071 foreach ($tables as &$table) {
4072 $table['dataSize'] = (int)$table['dataSize'];
4073 $table['noData'] = cloner_is_schema_only($table['name']);
4074 }
4075 return $tables;
4076}
4077
4078/**
4079 * @param ClonerDBConn|array $db
4080 * @param array $tables
4081 * @param float $timeout
4082 *
4083 * @return array
4084 *
4085 * @throws ClonerException
4086 */
4087function cloner_action_hash_tables($db, array $tables, $timeout)
4088{
4089 $conn = cloner_db_connection($db);
4090 $deadline = new ClonerDeadline($timeout);
4091 $result = new ClonerHashResult();
4092 foreach ($tables as $tableData) {
4093 $table = ClonerTable::fromArray($tableData);
4094 if ($table->noData) {
4095 $row = $conn->query("SHOW CREATE TABLE `{$table->name}`")->fetch();
4096 $createTable = $row['Create Table'];
4097 if (empty($createTable)) {
4098 throw new ClonerException(sprintf('SHOW CREATE TABLE did not return expected result for table %s', $table->name), 'no_create_table');
4099 }
4100 $result->appendOK(md5($createTable));
4101 } else {
4102 $rows = $conn->query("CHECKSUM TABLE `{$table->name}`")->fetchAll();
4103 if (count($rows) !== 1) {
4104 throw new ClonerException(sprintf('Expected exactly one CHECKSUM TABLE result, got %d', count($rows)), 'table_checksum_empty');
4105 }
4106 $result->appendOK(md5($rows[0]['Checksum']));
4107 }
4108 if ($deadline->done()) {
4109 break;
4110 }
4111 }
4112 return $result->hashes;
4113}
4114
4115/**
4116 * @param string $root
4117 * @param string $id
4118 * @param ClonerDBConn|array $db
4119 * @param ClonerDBDumpState|array $state
4120 * @param float $timeout
4121 *
4122 * @return ClonerDBDumpState
4123 *
4124 * @throws ClonerFSFunctionException
4125 * @throws ClonerException
4126 */
4127function cloner_action_dump_tables($root, $id, $db, $state, $timeout)
4128{
4129 set_time_limit(max($timeout * 5, 900));
4130 $conn = cloner_db_connection($db);
4131 $state = ClonerDBDumpState::fromArray($state);
4132 $deadline = new ClonerDeadline($timeout);
4133 $suffix = '/mwp_db';
4134 if (strlen($err = cloner_make_dir($root, 'mwp_db')) || strlen($err = cloner_write_file("$root$suffix/index.php", 0, ''))) {
4135 $root = sys_get_temp_dir();
4136 $suffix = "/mwp_db$id";
4137 if (strlen(cloner_make_dir($root, "mwp_db$id")) || strlen(cloner_write_file("$root$suffix/index.php", 0, ''))) {
4138 throw new ClonerException($err);
4139 }
4140 }
4141 $count = 0;
4142 foreach ($state->list as $table) {
4143 if ($count > 0 && $deadline->done()) {
4144 return $state;
4145 }
4146 if ($table->done) {
4147 continue;
4148 }
4149 if (!$table->listed) {
4150 $table->columns = cloner_get_table_columns($conn, $table->name);
4151 $table->listed = true;
4152 }
4153 $table->path = "mwp_db/$table->name.sql.php";
4154 $table->size = cloner_dump_table($conn, $table->name, $table->columns, "$root$suffix/$table->name.sql.php", $table->noData);
4155 $table->done = true;
4156 $count++;
4157 }
4158 $state->done = true;
4159 return $state;
4160}
4161
4162/**
4163 * @param string $root
4164 *
4165 * @return array
4166 *
4167 * @throws ClonerException
4168 */
4169function cloner_action_get_static_env($root)
4170{
4171 list($configPath, $wpConfig) = cloner_env_read_wp_config($root, false);
4172 $wpConfigInfo = cloner_env_parse_wp_config($wpConfig);
4173 $dbInfo = new ClonerDBInfo($wpConfigInfo->dbUser, $wpConfigInfo->dbPassword, $wpConfigInfo->dbHost, $wpConfigInfo->dbName);
4174 $conn = cloner_db_connection($dbInfo);
4175 try {
4176 $url = cloner_get_option($conn, $wpConfigInfo->wpTablePrefix, 'siteurl');
4177 } catch (ClonerException $e) {
4178 $url = '';
4179 }
4180 $wpInfo = new ClonerWPInfo($url, $root, $wpConfigInfo->wpTablePrefix, $configPath, $wpConfig, '', '', '', '', '');
4181 $result = new ClonerSetupResult($dbInfo, $wpInfo, cloner_env_info($root));
4182 return $result->toArray();
4183}
4184
4185/** @noinspection SqlDialectInspection */
4186
4187/** @noinspection SqlNoDataSourceInspection */
4188
4189
4190
4191class ClonerDBDumpScanner
4192{
4193 const INSERT_REPLACEMENT_PATTERN = '#^INSERT\\s+INTO\\s+(`?)[^\\s`]+\\1\\s+(?:\([^)]+\)\\s+)?VALUES\\s*#';
4194 // File handle.
4195 private $handle;
4196 // 0 - unknown ending
4197 // 1 - \n ending
4198 // 2 - \r\n ending
4199 private $rn = 0;
4200 private $cursor = 0;
4201 // Buffer that holds up to one statement.
4202 private $buffer = "";
4203
4204 /**
4205 * @param string $path
4206 *
4207 * @throws ClonerException
4208 */
4209 public function __construct($path)
4210 {
4211 $this->handle = @fopen($path, 'rb');
4212 if (!is_resource($this->handle)) {
4213 throw new ClonerException("Could not open database dump file", "db_dump_open", cloner_last_error_for('fopen'));
4214 }
4215 }
4216
4217 /**
4218 * @param int $maxCount
4219 * @param int $maxSize
4220 *
4221 * @return string Up to $maxCount statements or until half of $maxSize (in bytes) is reached.
4222 *
4223 * @throws ClonerException
4224 */
4225 public function scan($maxCount, $maxSize)
4226 {
4227 $lineBuffer = "";
4228 $buffer = "";
4229 $delimited = false;
4230 $count = 0;
4231 $inserts = false;
4232 while (true) {
4233 if (strlen($this->buffer)) {
4234 $line = $this->buffer;
4235 $this->buffer = "";
4236 } else {
4237 $line = fgets($this->handle);
4238 if ($line === false) {
4239 $error = cloner_last_error_for('fgets');
4240 if (feof($this->handle)) {
4241 // So, this is needed...
4242 break;
4243 }
4244 throw new ClonerException("Could not read database dump line", "db_dump_read_line", $error);
4245 }
4246 $this->cursor += strlen($line);
4247 }
4248 $len = strlen($line);
4249 if ($this->rn === 0) {
4250 // Run only once - detect line ending.
4251 if (substr_compare($line, "\r\n", $len - 2) === 0) {
4252 $this->rn = 2;
4253 } else {
4254 $this->rn = 1;
4255 }
4256 }
4257
4258 if (strlen($lineBuffer) === 0) {
4259 // Detect comments.
4260 if ($len <= 2 + $this->rn) {
4261 if ($this->rn === 2) {
4262 if ($line === "--\r\n" || $line === "\r\n") {
4263 continue;
4264 }
4265 } else {
4266 if ($line === "--\n" || $line === "\n") {
4267 continue;
4268 }
4269 }
4270 }
4271 if (strncasecmp($line, '-- ', 3) === 0) {
4272 continue;
4273 }
4274 if (preg_match('{^\s*$}', $line)) {
4275 continue;
4276 }
4277 }
4278
4279 if (($len >= 2 && $this->rn === 1 && substr_compare($line, ";\n", $len - 2) === 0)
4280 || ($len >= 3 && $this->rn === 2 && substr_compare($line, ";\r\n", $len - 3) === 0)
4281 ) {
4282 // Statement did end - fallthrough. This logic just makes more sense to write.
4283 } else {
4284 $lineBuffer .= $line;
4285 continue;
4286 }
4287 if (strlen($lineBuffer)) {
4288 $line = $lineBuffer.$line;
4289 $lineBuffer = "";
4290 }
4291 // Hack, but it's all for the greater good. The mysqldump command dumps statements
4292 // like "/*!50013 DEFINER=`user`@`localhost` SQL SECURITY DEFINER */" which require
4293 // super-privileges. That's way too troublesome, so just skip those statements.
4294 if (strncmp($line, '/*!50013 DEFINER=`', 18) === 0) {
4295 continue;
4296 }
4297 // /*!50003 CREATE*/ /*!50017 DEFINER=`foo`@`localhost`*/ /*!50003 TRIGGER `wp_hplugin_root` BEFORE UPDATE ON `wp_hplugin_root` FOR EACH ROW SET NEW.last_modified = NOW() */;
4298 if (strncmp($line, '/*!50003 CREATE*/ /*!50017 DEFINER=', 35) === 0) {
4299 $line = preg_replace('{/\*!50017 DEFINER=.*?(\*/)}', '', $line, 1);
4300 }
4301 if (strncmp($line, '/*!50001 CREATE ALGORITHM=', 26) === 0) {
4302 continue;
4303 }
4304 if (strncmp($line, '/*!50001 VIEW', 13) === 0) {
4305 continue;
4306 }
4307 $count++;
4308 if ($delimited) {
4309 // We're inside a block that looks like this:
4310 //
4311 // DELIMITER ;;
4312 // /*!50003 CREATE*/ /*!50017 DEFINER=`user`@`localhost`*/ /*!50003 TRIGGER `wp_hlogin_default_storage_table` BEFORE UPDATE ON `wp_hlogin_default_storage_table`
4313 // FOR EACH ROW SET NEW.last_modified = NOW() */;;
4314 // DELIMITER ;
4315 //
4316 // Since the DELIMITER statement does nothing when not in the CLI context, we need to merge the delimited statements
4317 // manually into a single statement.
4318 if (strncmp($line, 'DELIMITER ;', 11) === 0) {
4319 break;
4320 }
4321 // Replace the new delimiter with the default one (remove one semicolon).
4322 if (($this->rn === 1 && substr_compare($line, ";;\n", -3, 3) === 0)
4323 || ($this->rn === 2 && substr_compare($line, ";;\r\n", -4, 4) === 0)
4324 ) {
4325 $line = substr($line, 0, -($this->rn + 1)); // strip ";\n" or ";\r\n" at the end.
4326 }
4327 $buffer .= $line."\n";
4328 continue;
4329 } elseif (strncmp($line, 'DELIMITER ;;', 12) === 0) {
4330 $delimited = true;
4331 continue;
4332 }
4333 if (strncmp($line, 'INSERT INTO ', 12) === 0) {
4334 $inserts = true;
4335 if (strlen($buffer) === 0) {
4336 $buffer = 'INSERT IGNORE INTO '.substr($line, strlen('INSERT INTO '), -(1 + $this->rn)); // Strip the ";\n" or ";\r\n" at the end
4337 } else {
4338 if (strlen($buffer) + strlen($line) >= max(1, $maxSize / 2)) {
4339 $this->buffer = $line;
4340 break;
4341 }
4342 $newLine = preg_replace(self::INSERT_REPLACEMENT_PATTERN, ', ', $line, 1, $c);
4343 $newLine = substr($newLine, 0, -(1 + $this->rn));
4344 if ($c !== 1) {
4345 throw new ClonerException(sprintf("Could not parse INSERT line: %s", $line), "parse_insert_line");
4346 }
4347 $buffer .= $newLine;
4348 }
4349 if ($count >= $maxCount) {
4350 break;
4351 }
4352 continue;
4353 } elseif ($inserts) {
4354 // $buffer is not empty and we aren't inserting anything - break.
4355 $this->buffer = $line;
4356 } else {
4357 $buffer = $line;
4358 }
4359 break;
4360 }
4361 if ($inserts) {
4362 $buffer .= ';';
4363 }
4364 return $buffer;
4365 }
4366
4367 /**
4368 * @param int $offset
4369 *
4370 * @throws ClonerException
4371 */
4372 public function seek($offset)
4373 {
4374 if (@fseek($this->handle, $offset) === false) {
4375 throw new ClonerException("Could not seek database dump file", "seek_file", cloner_last_error_for('fseek'));
4376 }
4377 $this->cursor = $offset;
4378 }
4379
4380 public function tell()
4381 {
4382 return $this->cursor - strlen($this->buffer);
4383 }
4384
4385 public function close()
4386 {
4387 fclose($this->handle);
4388 }
4389}
4390
4391class ClonerDBImportState
4392{
4393 /** @var string Collects skipped statements up to a certain buffer length. */
4394 public $skip = "";
4395 /** @var int Counts skipped statements. */
4396 public $skipCount = 0;
4397 /** @var int Keeps skipped statements' total size. */
4398 public $skipSize = 0;
4399 /** @var ClonerImportDump[] File dumps that should be imported. */
4400 public $files = array();
4401
4402 /** @var int Maximum buffer size for skipped statements. */
4403 private $skipBuffer = 0;
4404
4405 /**
4406 * @param array $data State array; empty state means there's nothing to process. Every file that should be imported
4407 * must contain the props $state['files'][$i]['path'] and $state['files'][$i]['size'].
4408 * @param int $skipBuffer Maximum buffer size for skipped statement logging.
4409 *
4410 * @return ClonerDBImportState
4411 */
4412 public static function fromArray(array $data, $skipBuffer = 0)
4413 {
4414 $state = new self;
4415 $state->skipBuffer = $skipBuffer;
4416 foreach ((array)@$data['files'] as $i => $dump) {
4417 $state->files[$i] = new ClonerImportDump($dump['size'], $dump['processed'], $dump['path'], $dump['encoding']);
4418 }
4419 $state->skip = (string)@$data['skip'];
4420 $state->skipCount = (int)@$data['skipCount'];
4421 $state->skipSize = (int)@$data['skipSize'];
4422 return $state;
4423 }
4424
4425 /**
4426 * @return ClonerImportDump|null The next dump in the queue, or null if there are none left.
4427 */
4428 public function next()
4429 {
4430 foreach ($this->files as $file) {
4431 if ($file->processed < $file->size) {
4432 return $file;
4433 }
4434 }
4435 return null;
4436 }
4437
4438 /**
4439 * Pushes the first available file dump to the end of the queue.
4440 */
4441 public function pushNextToEnd()
4442 {
4443 $carry = null;
4444 foreach ($this->files as $i => $file) {
4445 if ($file->size === $file->processed) {
4446 continue;
4447 }
4448 $carry = $file;
4449 unset($this->files[$i]);
4450 $this->files = array_values($this->files);
4451 break;
4452 }
4453
4454 if ($carry === null) {
4455 return;
4456 }
4457
4458 $this->files[] = $carry;
4459 }
4460
4461 /**
4462 * Add a "skipped statement" to the state if there's any place left in state's "skipped statement" buffer.
4463 * Also updates state's "skipped statement" count and size.
4464 *
4465 * @param string $statements Statements that were skipped.
4466 */
4467 public function skipStatement($statements)
4468 {
4469 $length = strlen($statements);
4470 if (strlen($this->skip) + $length <= $this->skipBuffer / 2) {
4471 // Only write full statements to the buffer if it won't exceed half the buffer.
4472 $this->skip .= $statements;
4473 } elseif ($length + 200 <= $this->skipBuffer) {
4474 // We have enough space in the buffer to log the excerpt, but don't overflow the buffer, skip logging
4475 // when we reach its limit.
4476 $this->skip .= sprintf('/* query too big (%d bytes), excerpt: %s */;', $length, substr($statements, 0, 100));
4477 }
4478
4479 $this->skipCount++;
4480 $this->skipSize += $length;
4481 }
4482}
4483
4484class ClonerImportDump
4485{
4486 public $size = 0;
4487 public $processed = 0;
4488 public $path = "";
4489 public $encoding = "";
4490
4491 public function __construct($size, $processed, $path, $encoding)
4492 {
4493 $this->size = (int)$size;
4494 $this->processed = (int)$processed;
4495 $this->path = (string)$path;
4496 $this->encoding = (string)$encoding;
4497 }
4498}
4499
4500class ClonerDBColumn
4501{
4502 public $name = '';
4503 public $type = '';
4504
4505 public static function fromArray(array $data)
4506 {
4507 $column = new self;
4508 if (isset($data['name'])) {
4509 $column->name = $data['name'];
4510 }
4511 if (isset($data['type'])) {
4512 $column->type = $data['type'];
4513 }
4514 return $column;
4515 }
4516}
4517
4518class ClonerDBTable
4519{
4520 public $name = '';
4521 public $size = 0;
4522 public $dataSize = 0;
4523 public $storage = '';
4524 public $done = false;
4525 public $listed = false;
4526 /** @var ClonerDBColumn[] */
4527 public $columns = array();
4528 public $path = '';
4529 public $noData = false;
4530
4531 public static function fromArray(array $data)
4532 {
4533 $table = new self;
4534 if (isset($data['name'])) {
4535 $table->name = $data['name'];
4536 }
4537 if (isset($data['size'])) {
4538 $table->size = $data['size'];
4539 }
4540 if (isset($data['dataSize'])) {
4541 $table->dataSize = $data['dataSize'];
4542 }
4543 if (isset($data['storage'])) {
4544 $table->storage = $data['storage'];
4545 }
4546 if (isset($data['done'])) {
4547 $table->done = $data['done'];
4548 }
4549 if (isset($data['listed'])) {
4550 $table->listed = $data['listed'];
4551 }
4552 if (isset($data['columns'])) {
4553 foreach ($data['columns'] as $column) {
4554 $table->columns[] = ClonerDBColumn::fromArray($column);
4555 }
4556 }
4557 if (isset($data['path'])) {
4558 $table->path = $data['path'];
4559 }
4560 if (isset($data['noData'])) {
4561 $table->noData = $data['noData'];
4562 }
4563 return $table;
4564 }
4565}
4566
4567class ClonerDBDumpState
4568{
4569 public $listed = false;
4570 /** @var ClonerDBTable[] */
4571 public $list = array();
4572 public $done = false;
4573
4574 public static function fromArray($data)
4575 {
4576 if ($data instanceof self) {
4577 return $data;
4578 }
4579 $state = new self;
4580 if (isset($data['listed'])) {
4581 $state->listed = $data['listed'];
4582 }
4583 if (isset($data['list'])) {
4584 foreach ($data['list'] as $item) {
4585 $state->list[] = ClonerDBTable::fromArray($item);
4586 }
4587 }
4588 if (isset($data['done'])) {
4589 $state->done = $data['done'];
4590 }
4591 return $state;
4592 }
4593}
4594
4595/**
4596 * @param ClonerDBConn $conn
4597 * @param string $table
4598 *
4599 * @return array
4600 *
4601 * @throws ClonerException
4602 */
4603function cloner_get_table_columns(ClonerDBConn $conn, $table)
4604{
4605 $columnList = $conn->query("SHOW COLUMNS IN `$table`")->fetchAll();
4606
4607 $columns = array();
4608 foreach ($columnList as $columnData) {
4609 $column = new ClonerDBColumn();
4610 $column->name = $columnData['Field'];
4611 $type = strtolower($columnData['Type']);
4612 if (($openParen = strpos($type, '(')) !== false) {
4613 // Transform "int(11)" to "int", etc.
4614 $type = substr($type, 0, $openParen);
4615 }
4616 $column->type = $type;
4617 $columns[] = $column;
4618
4619 if ($conn instanceof ClonerPDOConn && strpos($column->name, '?') !== false) {
4620 $conn->setAttEmulatePrepares(false);
4621 }
4622 }
4623
4624 return $columns;
4625}
4626
4627/**
4628 * @param ClonerDBConn $conn
4629 * @param string $tableName
4630 * @param ClonerDBColumn[] $columns
4631 * @param string $tablePath
4632 * @param bool $noData
4633 *
4634 * @return int Number of bytes written.
4635 *
4636 * @throws ClonerException
4637 * @throws ClonerFSFunctionException
4638 */
4639function cloner_dump_table(ClonerDBConn $conn, $tableName, array $columns, $tablePath, $noData)
4640{
4641 $written = 0;
4642 $result = $conn->query("SHOW CREATE TABLE `$tableName`")->fetch();
4643 $createTable = $result['Create Table'];
4644 if (empty($createTable)) {
4645 throw new ClonerException(sprintf('SHOW CREATE TABLE did not return expected result for table %s', $tableName), 'no_create_table');
4646 }
4647
4648 if (($fp = @fopen($tablePath, 'wb')) === false) {
4649 throw new ClonerFSFunctionException('fopen', $tablePath);
4650 }
4651 $time = date('c');
4652 $fetchAllQuery = cloner_create_select_query($tableName, $columns);
4653 $haltCompiler = "<?php exit; __halt_compiler();";
4654 $dumper = get_class($conn);
4655 $phpVersion = phpversion();
4656 $header = <<<SQL
4657-- $haltCompiler // Protect the file from being visited via web
4658-- Orion backup format
4659-- Generated at: $time by $dumper; PHP v$phpVersion
4660-- Selected via: $fetchAllQuery
4661
4662/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
4663/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
4664/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
4665/*!40101 SET NAMES utf8 */;
4666/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
4667/*!40103 SET TIME_ZONE='+00:00' */;
4668/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
4669/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
4670/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
4671/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
4672
4673DROP TABLE IF EXISTS `$tableName`;
4674
4675/*!40101 SET @saved_cs_client = @@character_set_client */;
4676/*!40101 SET character_set_client = utf8 */;
4677
4678$createTable;
4679
4680/*!40101 SET character_set_client = @saved_cs_client */;
4681
4682SQL;
4683 if (!$noData) {
4684 $header .= <<<SQL
4685LOCK TABLES `$tableName` WRITE;
4686/*!40000 ALTER TABLE `$tableName` DISABLE KEYS */;
4687
4688SQL;
4689 }
4690 if (($w = @fwrite($fp, $header)) === false) {
4691 @fclose($fp);
4692 throw new ClonerFSFunctionException('fwrite', $tablePath);
4693 }
4694 $written += $w;
4695
4696 if (!$noData) {
4697 $flushSize = 8 << 20;
4698 $buf = '';
4699 $fetchAll = $conn->query($fetchAllQuery, array(), true);
4700 while ($row = $fetchAll->fetch()) {
4701 $buf .= cloner_create_insert_query($conn, $tableName, $columns, $row);
4702 if (strlen($buf) < $flushSize) {
4703 continue;
4704 }
4705 if (($w = @fwrite($fp, $buf)) === false) {
4706 $e = new ClonerFSFunctionException('fwrite', $tablePath);
4707 @fclose($fp);
4708 throw $e;
4709 }
4710 $buf = '';
4711 $written += $w;
4712 }
4713 if (strlen($buf)) {
4714 if (($w = @fwrite($fp, $buf)) === false) {
4715 $e = new ClonerFSFunctionException('fwrite', $tablePath);
4716 @fclose($fp);
4717 throw $e;
4718 }
4719 unset($buf);
4720 $written += $w;
4721 }
4722 $fetchAll->free();
4723 }
4724
4725 $footer = <<<SQL
4726
4727/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
4728/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
4729/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
4730/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
4731/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
4732/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
4733/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
4734/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
4735
4736SQL;
4737 if (!$noData) {
4738 $footer = <<<SQL
4739
4740/*!40000 ALTER TABLE `$tableName` ENABLE KEYS */;
4741UNLOCK TABLES;
4742SQL
4743 .$footer;
4744 }
4745 if (($w = @fwrite($fp, $footer)) === false) {
4746 $e = new ClonerFSFunctionException('fwrite', $tablePath);
4747 @fclose($fp);
4748 throw $e;
4749 }
4750 $written += $w;
4751
4752 if (@fclose($fp) === false) {
4753 throw new ClonerFSFunctionException('fclose', $tablePath);
4754 }
4755 cloner_clear_stat_cache($tablePath);
4756 if (($size = @filesize($tablePath)) !== $written) {
4757 if ($size === false) {
4758 throw new ClonerFSFunctionException('filesize', $tablePath);
4759 }
4760 throw new ClonerException(sprintf('table %s dumped %d bytes, but on the disk is %d bytes', $tableName, $written, $size));
4761 }
4762
4763 return $written;
4764}
4765
4766/**
4767 * @param string $tableName
4768 * @param ClonerDBColumn[] $columns
4769 *
4770 * @return string
4771 */
4772function cloner_create_select_query($tableName, array $columns)
4773{
4774 $select = 'SELECT ';
4775 foreach ($columns as $i => $column) {
4776 if ($i > 0) {
4777 $select .= ', ';
4778 }
4779 switch ($column->type) {
4780 case 'tinyblob':
4781 case 'mediumblob':
4782 case 'blob':
4783 case 'longblob':
4784 case 'binary':
4785 case 'varbinary':
4786 $select .= "HEX(`$column->name`)";
4787 break;
4788 default:
4789 $select .= "`$column->name`";
4790 break;
4791 }
4792 }
4793 $select .= " FROM `$tableName`;";
4794
4795 return $select;
4796}
4797
4798/**
4799 * @param ClonerDBConn $conn
4800 * @param string $tableName
4801 * @param ClonerDBColumn[] $columns
4802 * @param array $row
4803 *
4804 * @return string
4805 *
4806 * @throws ClonerException
4807 */
4808function cloner_create_insert_query(ClonerDBConn $conn, $tableName, array $columns, array $row)
4809{
4810 $insert = "INSERT INTO `$tableName` VALUES (";
4811 $i = 0;
4812 foreach ($row as $value) {
4813 $column = $columns[$i];
4814 if ($i > 0) {
4815 $insert .= ',';
4816 }
4817 $i++;
4818 if ($value === null) {
4819 $insert .= 'null';
4820 continue;
4821 }
4822 switch ($column->type) {
4823 case 'tinyint':
4824 case 'smallint':
4825 case 'mediumint':
4826 case 'int':
4827 case 'bigint':
4828 case 'decimal':
4829 case 'float':
4830 case 'double':
4831 $insert .= $value;
4832 break;
4833 case 'tinyblob':
4834 case 'mediumblob':
4835 case 'blob':
4836 case 'longblob':
4837 case 'binary':
4838 case 'varbinary':
4839 if (strlen($value) === 0) {
4840 $insert .= "''";
4841 } else {
4842 $insert .= "0x$value";
4843 }
4844 break;
4845 case 'bit':
4846 $insert .= $value ? "b'1'" : "b'0'";
4847 break;
4848 default:
4849 $insert .= $conn->escape($value);
4850 break;
4851 }
4852 }
4853 $insert .= ");\n";
4854
4855 return $insert;
4856}
4857
4858
4859
4860
4861class ClonerURL
4862{
4863 public $secure = false;
4864 public $host = '';
4865 public $port = 0;
4866 public $scheme = '';
4867 public $path = '';
4868 public $query = '';
4869 public $fragment = '';
4870 public $user = '';
4871 public $pass = '';
4872
4873 public function __toString()
4874 {
4875 return sprintf("http%s://%s%s%s", $this->secure ? 's' : '', $this->host, $this->port ? ":$this->port" : '', strlen($this->path) ? $this->path : '/');
4876 }
4877
4878 public function getHTTPHost()
4879 {
4880 if (!$this->port) {
4881 return $this->host;
4882 }
4883 if ($this->secure && $this->port === 443) {
4884 return $this->host;
4885 }
4886 if (!$this->secure && $this->scheme === 80) {
4887 return $this->host;
4888 }
4889 return "$this->host:$this->port";
4890 }
4891
4892 /**
4893 * @return string In host:port format. Applies default port numbers where none are set.
4894 */
4895 public function getHostPort()
4896 {
4897 $port = $this->port;
4898 if ($this->port === 0) {
4899 $port = $this->secure ? 443 : 80;
4900 }
4901 return "$this->host:$port";
4902 }
4903
4904 /**
4905 * @param string $url
4906 *
4907 * @return ClonerURL
4908 *
4909 * @throws ClonerURLException If the URL is not valid.
4910 */
4911 public static function fromString($url)
4912 {
4913 $u = new ClonerURL();
4914 $parts = parse_url($url);
4915 if ($parts === false) {
4916 throw new ClonerURLException($url, 'url_invalid');
4917 }
4918 if (!array_key_exists('host', $parts)) {
4919 throw new ClonerURLException($url, 'missing_host');
4920 }
4921 $u->host = strtolower($parts['host']);
4922 if (array_key_exists('scheme', $parts) && strtolower($parts['scheme']) === 'https') {
4923 $u->secure = true;
4924 }
4925 if (array_key_exists('port', $parts)) {
4926 $u->port = $parts['port'];
4927 }
4928 if (array_key_exists('path', $parts) && strlen($parts['path'])) {
4929 $u->path = $parts['path'];
4930 } else {
4931 $u->path = '/';
4932 }
4933 if (array_key_exists('query', $parts)) {
4934 $u->query = $parts['query'];
4935 }
4936 if (array_key_exists('fragment', $parts)) {
4937 $u->fragment = $parts['fragment'];
4938 }
4939 if (array_key_exists('user', $parts)) {
4940 $u->user = $parts['user'];
4941 }
4942 if (array_key_exists('pass', $parts)) {
4943 $u->pass = $parts['pass'];
4944 }
4945 return $u;
4946 }
4947}
4948
4949/**
4950 * Sends HTTP request to $url and leaves the connection open for reading and/or writing.
4951 * It is caller's responsibility to use the connection properly. No default headers are
4952 * sent in this method, set "host" and "content-length" manually if desired.
4953 *
4954 * @param string $method
4955 * @param ClonerURL $url As a special case, if the URL contains the hash fragment it will be used as the resolved IP.
4956 * User and password are also automatically added as part of the authorization header.
4957 * @param array $header Additional headers to send beside "Host", that is inferred from the URL.
4958 * @param int $timeout
4959 * @param string $cert Custom certificate to use for TLS.
4960 *
4961 * @return resource
4962 *
4963 * @throws ClonerNetSocketException
4964 * @throws ClonerSocketClientException
4965 * @throws ClonerNoTransportStreamsException
4966 * @throws ClonerFSFunctionException
4967 */
4968function cloner_http_open_request($method, ClonerURL $url, array $header = array(), $timeout = 60, $cert = '')
4969{
4970 $hostPort = $url->getHostPort();
4971 if (strlen($url->fragment)) {
4972 $port = empty($url->port) ? ($url->secure ? 443 : 80) : $url->port;
4973 $hostPort = "$url->fragment:$port";
4974 }
4975 $sock = cloner_tcp_socket_dial($hostPort, $timeout, $url->secure, $url->host, $cert);
4976 $request = array(
4977 sprintf("%s %s%s HTTP/1.1", $method, $url->path, strlen($url->query) ? "?$url->query" : ''),
4978 );
4979 if (strlen($url->user)) {
4980 $header['authorization'] = sprintf('Basic %s', base64_encode("$url->user:$url->pass"));
4981 }
4982 foreach ($header as $key => $value) {
4983 $request[] = sprintf('%s: %s', $key, $value);
4984 }
4985 array_push($request, '', ''); // Output \r\n\r\n at the end after implode.
4986 stream_set_timeout($sock, $timeout);
4987 if (@fwrite($sock, implode("\r\n", $request)) === false) {
4988 throw new ClonerNetSocketException('fwrite', $sock);
4989 }
4990 return $sock;
4991}
4992
4993class ClonerHTTPResponse
4994{
4995 public $statusCode = 0;
4996 public $status = '';
4997
4998 /**
4999 * @var string[] In key => value format.
5000 */
5001 public $headers = array();
5002 /**
5003 * @var resource
5004 */
5005 public $body;
5006
5007 public static function fromParts($statusCode, $status, array $headers, $body)
5008 {
5009 $self = new self();
5010 $self->statusCode = $statusCode;
5011 $self->status = $status;
5012 $self->headers = $headers;
5013 $self->body = $body;
5014 return $self;
5015 }
5016
5017 /**
5018 * @param int $timeout
5019 *
5020 * @return string
5021 *
5022 * @throws ClonerException
5023 * @throws ClonerNetException
5024 */
5025 public function read($timeout)
5026 {
5027 if (isset($this->headers['transfer-encoding']) && strtolower($this->headers['transfer-encoding']) === 'chunked') {
5028 return cloner_chunked_read($this->body, $timeout);
5029 }
5030
5031 if (isset($this->headers['connection']) && strtolower($this->headers['connection']) === 'close') {
5032 $data = @stream_get_contents($this->body);
5033 if ($data === false) {
5034 throw new ClonerNetSocketException('stream_get_contents', $this->body);
5035 }
5036 return $data;
5037 }
5038
5039 if (isset($this->headers['content-length']) || ctype_digit($this->headers['content-length'])) {
5040 $length = (int)$this->headers['content-length'];
5041 return cloner_limit_read($this->body, $length, $timeout);
5042 }
5043
5044 throw new ClonerException("got unrecognized HTTP response format");
5045 }
5046}
5047
5048/**
5049 * Reads HTTP response headers from $sock that's expecting them.
5050 * The response body is then ready to be read, after which features
5051 * like keep-alive or web sockets can be utilized.
5052 *
5053 * @param resource $sock Usually a result of cloner_http_request_open.
5054 * @param int $timeout
5055 *
5056 * @return ClonerHTTPResponse With headers and status present, and unread body.
5057 *
5058 * @throws ClonerNetException
5059 * @throws ClonerException If HTTP response is not valid.
5060 */
5061function cloner_http_get_response_headers($sock, $timeout = 60)
5062{
5063 $res = new ClonerHTTPResponse();
5064 $res->body = $sock;
5065 while (true) {
5066 stream_set_timeout($sock, $timeout);
5067 if (($line = @fgets($sock)) === false) {
5068 throw new ClonerNetSocketException('fgets', $sock);
5069 }
5070 if ($line === "\n" || $line === "\r\n") {
5071 if ($res->statusCode === 0) {
5072 throw new ClonerNetException('newline encountered before HTTP response');
5073 }
5074 break;
5075 }
5076 if ($res->statusCode === 0) {
5077 if (!preg_match('{^HTTP/\d\.\d (\d{3}) (.*)$}', $line, $matches)) {
5078 throw new ClonerException(sprintf('invalid first response line: %s', $line));
5079 }
5080 $res->statusCode = (int)$matches[1];
5081 $res->status = trim($matches[2]);
5082 continue;
5083 }
5084 $parts = explode(':', $line, 2);;
5085 if (count($parts) !== 2) {
5086 throw new ClonerException(sprintf('invalid header line: %s', $line));
5087 }
5088 $res->headers[strtolower(trim($parts[0]))] = trim($parts[1]);
5089 }
5090 return $res;
5091}
5092
5093/**
5094 * @param string $method
5095 * @param string $url
5096 * @param string $contentType
5097 * @param string $body
5098 * @param int $timeout In seconds.
5099 *
5100 * @return string Raw (and dechunked) response body.
5101 *
5102 * @throws ClonerNetException
5103 * @throws ClonerURLException
5104 * @throws Exception
5105 */
5106function cloner_http_do($method, $url, $contentType = '', $body = '', $timeout = 60)
5107{
5108 $deadline = time() + $timeout;
5109 $url = ClonerURL::fromString($url);
5110 $headers = array(
5111 'content-type' => $contentType,
5112 'connection' => 'close',
5113 'content-length' => (string)strlen($body),
5114 'host' => $url->getHTTPHost(),
5115 );
5116 $sock = cloner_http_open_request($method, $url, $headers, $timeout);
5117 if (strlen($body)) {
5118 stream_set_timeout($sock, max(1, $deadline - time()));
5119 $n = @fwrite($sock, $body);
5120 if ($n === false) {
5121 @fclose($sock);
5122 throw new ClonerClonerNetFunctionException('fwrite', $url);
5123 }
5124 }
5125 try {
5126 $response = cloner_http_get_response_headers($sock, max(1, $deadline - time()))->read(max(1, $deadline - time()));
5127 } catch (Exception $e) {
5128 @fclose($sock);
5129 throw $e;
5130 }
5131 @fclose($sock);
5132 return $response;
5133}
5134
5135class ClonerHTTPResponseLine
5136{
5137 public $protocol = ''; // 1.0 or 1.1
5138 public $statusCode = 0; // Whatever server returns.
5139 public $status = ''; // Whatever server returns.
5140
5141 /**
5142 * @param string $protocol
5143 * @param int $statusCode
5144 * @param string $status
5145 *
5146 * @return ClonerHTTPResponseLine
5147 */
5148 public static function create($protocol, $statusCode, $status)
5149 {
5150 $self = new self();
5151 $self->protocol = $protocol;
5152 $self->statusCode = $statusCode;
5153 $self->status = $status;
5154 return $self;
5155 }
5156}
5157
5158class ClonerHTTPParseException extends ClonerException
5159{
5160 public function __construct($message)
5161 {
5162 parent::__construct($message, 'http_parse');
5163 }
5164}
5165
5166/**
5167 * Waits for $timeout seconds on $sock to optionally receive an HTTP error response, eg. "413 Request Entity Too Large".
5168 * This should only be used on HTTP requests that are waiting for the body to be written.
5169 *
5170 * @param resource $sock Stream with HTTP request headers already written to it, and ready to accept the body.
5171 * @param float $timeout Timeout in seconds.
5172 *
5173 * @return ClonerHTTPResponseLine Response status code.
5174 *
5175 * @throws ClonerHTTPParseException
5176 * @throws ClonerNetSocketException
5177 */
5178function cloner_http_get_response_line($sock, $timeout)
5179{
5180 $bufferLimit = 256;
5181 list($sec, $usec) = cloner_split_usec($timeout);
5182 stream_set_timeout($sock, $sec, $usec);
5183 $line = @fgets($sock, $bufferLimit);
5184 if ($line === false) {
5185 throw new ClonerNetSocketException('fgets', $sock);
5186 }
5187 if (!preg_match('{^HTTP/(\d\.\d) (\d{3}) ([^$]+)$}', $line, $matches)) {
5188 throw new ClonerHTTPParseException(sprintf('invalid HTTP response first line: %s', $line));
5189 }
5190 return ClonerHTTPResponseLine::create($matches[1], (int)$matches[2], $matches[3]);
5191}
5192
5193/**
5194 * @param resource $sock Stream waiting for HTTP header reading, meaning it should go after http_get_response_line.
5195 * @param float $timeout Timeout in seconds.
5196 *
5197 * @return array Map of lowercase-header-name => header value, both strings.
5198 *
5199 * @throws Exception If HTTP response could not be parsed.
5200 * @throws ClonerNetSocketException
5201 */
5202function cloner_http_get_headers($sock, $timeout)
5203{
5204 $bufferLimit = 8 * (1 << 10);
5205 $headers = array();
5206 while (true) {
5207 list($sec, $usec) = cloner_split_usec($timeout);
5208 stream_set_timeout($sock, $sec, $usec);
5209 $line = @fgets($sock, $bufferLimit);
5210 if ($line === false) {
5211 throw new ClonerNetSocketException('fgets', $sock);
5212 }
5213 if ($line === "\n" || $line === "\r\n") {
5214 break;
5215 }
5216 $parts = explode(':', $line, 2);;
5217 if (count($parts) !== 2) {
5218 throw new ClonerHTTPParseException(sprintf('invalid HTTP header line: %s', $line));
5219 }
5220 $headers[strtolower(trim($parts[0]))] = trim($parts[1]);
5221 }
5222 return $headers;
5223}
5224
5225/**
5226 * Splits seconds and microseconds to two integers, to be used in system calls that require them.
5227 *
5228 * @param float|int $time
5229 *
5230 * @return int[] Array of two int elements, $seconds and $microseconds.
5231 */
5232function cloner_split_usec($time)
5233{
5234 $sec = floor($time);
5235 $usec = ($time - $sec) * 1000000;
5236 return array($sec, $usec);
5237}
5238
5239/**
5240 * @param $sock
5241 * @param $limit
5242 * @param $timeout
5243 *
5244 * @return string Read result until $limit is reached.
5245 *
5246 * @throws ClonerNetSocketException If reading from stream fails.
5247 */
5248function cloner_limit_read($sock, $limit, $timeout)
5249{
5250 stream_set_timeout($sock, $timeout);
5251 $body = '';
5252 while (strlen($body) < $limit) {
5253 $chunk = @fread($sock, $limit - strlen($body));
5254 if ($chunk === false) {
5255 throw new ClonerNetSocketException('fread', $this->body);
5256 }
5257 $body .= $chunk;
5258 }
5259 return $body;
5260}
5261
5262/**
5263 * @param resource $sock
5264 * @param int $timeout
5265 *
5266 * @return string Read result until terminating chunk (0\r\n).
5267 *
5268 * @throws Exception If chunked encoding is not valid.
5269 * @throws ClonerNetSocketException If reading from stream fails.
5270 */
5271function cloner_chunked_read($sock, $timeout)
5272{
5273 stream_set_timeout($sock, $timeout);
5274 $body = '';
5275 while (true) {
5276 $length = @fgets($sock);
5277 if ($length === false) {
5278 throw new ClonerNetSocketException('fgets', $sock);
5279 }
5280 $length = rtrim($length, "\r\n");
5281 if (!ctype_xdigit($length)) {
5282 throw new ClonerException(sprintf('Did not get hex chunk length: %s', $length));
5283 }
5284 $length = hexdec($length);
5285 $got = 0;
5286 while ($got < $length) {
5287 $chunk = @fread($sock, $length - $got);
5288 if ($chunk === false) {
5289 throw new ClonerNetSocketException('fread', $sock);
5290 }
5291 $got += strlen($chunk);
5292 $body .= $chunk;
5293 }
5294 // Every chunk (including final) is followed up by an additional \r\n.
5295 if (($tmp = @fgets($sock, 3)) === false) {
5296 throw new ClonerNetSocketException('fgets', $sock);
5297 }
5298 if ($tmp !== "\r\n") {
5299 throw new ClonerException('Did not get expected CRLF');
5300 }
5301 if ($length === 0) {
5302 break;
5303 }
5304 }
5305 return $body;
5306}
5307
5308
5309
5310
5311/**
5312 * Throw to skip file in ClonerFSVisitor implementation.
5313 */
5314class ClonerSkipVisitException extends ClonerException
5315{
5316 public function __construct()
5317 {
5318 parent::__construct("Internal exception, skip file");
5319 }
5320}
5321
5322interface ClonerFSVisitor
5323{
5324 /**
5325 * @param string $path Path relative to root.
5326 * @param ClonerStatInfo $stat Stat result of path.
5327 * @param Exception|null $e Error during stat or readdir of $path.
5328 *
5329 * @return bool True to continue iteration, false to stop and return the file's path as cursor to potentially continue from.
5330 *
5331 * @throws ClonerSkipVisitException If the directory should not be traversed. No real effect if visiting a file, since its sibling comes next.
5332 * @throws Exception To abort execution and propagate the exception.
5333 */
5334 public function visit($path, ClonerStatInfo $stat, Exception $e = null);
5335}
5336
5337class ClonerFSFileInfo
5338{
5339 /** @var string */
5340 private $path;
5341 /** @var ClonerStatInfo */
5342 private $stat;
5343 /** @var string[]|null */
5344 private $children;
5345
5346 /**
5347 * @param string $relPath
5348 * @param ClonerStatInfo $stat
5349 * @param string[]|null $children
5350 */
5351 public function __construct($relPath, ClonerStatInfo $stat, array $children = null)
5352 {
5353 $this->path = $relPath;
5354 $this->stat = $stat;
5355 $this->children = $children;
5356 }
5357
5358 /**
5359 * @return string
5360 */
5361 public function getPath()
5362 {
5363 return $this->path;
5364 }
5365
5366 /**
5367 * @return ClonerStatInfo
5368 */
5369 public function getStat()
5370 {
5371 return $this->stat;
5372 }
5373
5374 /**
5375 * @param string[]|null $children
5376 */
5377 public function setChildren(array $children = null)
5378 {
5379 $this->children = $children;
5380 }
5381
5382 /**
5383 * @return string[]|null
5384 */
5385 public function getChildren()
5386 {
5387 return $this->children;
5388 }
5389}
5390
5391/**
5392 * Creates stack for file iteration from cursor.
5393 * The cursor is normalized and split to directory nodes, for example:
5394 * '' => ['']
5395 * 'foo' => ['','foo']
5396 * '/foo\\/bar///' => ['','foo','bar']
5397 *
5398 * Each node holds its own name and names of children that alphabetically go after the node that is next in path.
5399 * For example, the path 'foo/bar', will be split to the following nodes:
5400 * 1. '' - Directory and its children that come after 'foo'.
5401 * 2. 'foo' - Directory and its children that come after 'bar'.
5402 * 3. 'bar' - If and only if bar is a directory.
5403 *
5404 * Note that 'bar' is not in the stack, since the last node is always skipped.
5405 * Cursor always points to the next processed file, so the iteration should continue from
5406 * 'bar', which is known in the 'foo' node (info 2. above).
5407 *
5408 * @param string $root Root on the filesystem.
5409 * @param string $cursor Cursor path that points to the next file to be processed.
5410 *
5411 * @return ClonerFSFileInfo[] Stack of file info with at least one (root) element.
5412 *
5413 * @throws ClonerFSException Only if the root cannot be stat-ed, or is not a directory.
5414 */
5415function cloner_fs_make_stack($root, $cursor = '')
5416{
5417 /** @var ClonerFSFileInfo[] $stack */
5418 $stack = array();
5419 // Split cursor to paths
5420 $paths = explode('/', preg_replace('{[\\\\/]+}', '/', trim($cursor, '\\/')));
5421 if ($paths[0] === '.') {
5422 $paths[0] = '';
5423 }
5424 if ($paths[0] !== '') {
5425 array_unshift($paths, '');
5426 }
5427 for ($i = 0, $pathCount = count($paths); $i < $pathCount; $i++) {
5428 $current = $paths[$i];
5429 // $current[$i+1] holds path-to-skip-to in current directory.
5430 // First time $current is an empty string.
5431 $path = isset($path) ? $path.'/'.$current : $current;
5432 $nextChild = isset($paths[$i + 1]) ? $paths[$i + 1] : null;
5433 $children = null;
5434 try {
5435 $stat = cloner_fs_stat($root.$path);
5436 if (!$stat->isDir()) {
5437 if (count($stack) === 0) {
5438 return array(new ClonerFSFileInfo($path, $stat));
5439 }
5440 return $stack;
5441 }
5442 if ($nextChild !== null) {
5443 $children = cloner_fs_list_children($root.$path, $nextChild);
5444 }
5445 if (isset($stack[$i - 1])) {
5446 $parent = $stack[$i - 1];
5447 $siblings = $parent->getChildren();
5448 if (isset($siblings[0]) && $siblings[0] === $current) {
5449 array_shift($siblings);
5450 $parent->setChildren($siblings);
5451 }
5452 }
5453 $stack[] = new ClonerFSFileInfo(ltrim($path, '/'), $stat, $children);
5454 } catch (ClonerFSException $e) {
5455 if (count($stack) > 0) {
5456 // We have root at least.
5457 break;
5458 }
5459 throw $e;
5460 }
5461 }
5462 return $stack;
5463}
5464
5465/**
5466 * Iterate through all $root paths (including $root itself) after $cursor and call $visitor for each file path
5467 * encountered. Directory traversal is done in depth-first mode (meaning directory children are visited recursively
5468 * before their siblings). The flag $parentsFirst decides whether parent directories themselves are visited before
5469 * their children (useful for reading) or after (useful for deleting).
5470 *
5471 * @param string $root Root path to traverse.
5472 * @param ClonerFSVisitor $visitor Visitor to invoke for each file encountered.
5473 * @param string $cursor Cursor points to the next node to be processed, defaults to working directory.
5474 * @param bool $parentsFirst True to visit parents right before their children, false to visit them right after.
5475 *
5476 * @return string $cursor Cursor path (relative to root) to continue from when visitor stops the traversal. Empty string signifies end.
5477 *
5478 * @throws Exception Propagated from $visitor.
5479 * @throws ClonerFSException If $root is not a directory.
5480 */
5481function cloner_fs_walk($root, ClonerFSVisitor $visitor, $cursor = '', $parentsFirst = false)
5482{
5483 try {
5484 $stack = cloner_fs_make_stack($root, $cursor);
5485 } catch (Exception $e) {
5486 $visitor->visit($cursor, ClonerStatInfo::makeEmpty(), $e);
5487 return '';
5488 }
5489 /** @var ClonerFSFileInfo $current */
5490 $current = array_pop($stack);
5491 if (!$current->getStat()->isDir()) {
5492 $visitor->visit($current->getPath(), $current->getStat());
5493 return '';
5494 }
5495 // Flag that is set to true every time we traverse down into a new directory, and false when going up.
5496 // If last node in cursor is a directory with un-stat-ed children, that means the cursor ended up on it,
5497 // and that directory was the last one to get visited.
5498 // If last part of cursor is a file, $current will have its siblings as children.
5499 $goDown = true;
5500 if ($parentsFirst) {
5501 if ($current->getChildren() !== null) {
5502 $goDown = null;
5503 }
5504 } else {
5505 if ($current->getChildren() === null) {
5506 $goDown = false;
5507 }
5508 }
5509
5510 while (true) {
5511 $e = null;
5512 $children = $current->getChildren();
5513 if ($children === null) {
5514 try {
5515 $children = cloner_fs_list_children($root.'/'.$current->getPath());
5516 } catch (Exception $e) {
5517 // Capture $e for call below.
5518 $children = array();
5519 }
5520 $current->setChildren($children);
5521 }
5522 if ($goDown === $parentsFirst) {
5523 try {
5524 if (!$visitor->visit($current->getPath(), $current->getStat(), $e)) {
5525 return $current->getPath();
5526 }
5527 } catch (ClonerSkipVisitException $e) {
5528 $goDown = false;
5529 }
5530 }
5531 if ($goDown === false) {
5532 $current = array_pop($stack);
5533 if ($current === null) {
5534 break;
5535 }
5536 }
5537 foreach ($current->getChildren() as $i => $child) {
5538 $childPath = ltrim($current->getPath().'/'.$child, '/');
5539 try {
5540 $stat = cloner_fs_stat($root.'/'.$childPath);
5541 if ($stat->isDir()) {
5542 $current->setChildren(array_slice($current->getChildren(), $i + 1));
5543 $stack[] = $current;
5544 $current = new ClonerFSFileInfo($childPath, $stat);
5545 $goDown = true;
5546 continue 2;
5547 }
5548 } catch (Exception $e) {
5549 if (!$visitor->visit($childPath, ClonerStatInfo::makeEmpty(), $e)) {
5550 return $childPath;
5551 }
5552 continue;
5553 }
5554 try {
5555 if (!$visitor->visit($childPath, $stat)) {
5556 return $childPath;
5557 }
5558 } catch (ClonerSkipVisitException $e) {
5559 // Go to next sibling.
5560 continue;
5561 }
5562 }
5563 $goDown = false;
5564 }
5565 return '';
5566}
5567
5568/**
5569 * Attempts to run lstat or stat syscall and return results.
5570 *
5571 * @param string $path File path to stat.
5572 *
5573 * @return ClonerStatInfo
5574 *
5575 * @throws ClonerFSFunctionException If both lstat and stat fail.
5576 * @throws ClonerNoFileException
5577 */
5578function cloner_fs_stat($path)
5579{
5580 if (function_exists('lstat')) {
5581 $stat = @lstat($path);
5582 if ($stat) {
5583 $info = ClonerStatInfo::fromArray($stat);
5584 if ($info->isLink()) {
5585 $link = readlink($path);
5586 if ($link === false) {
5587 throw new ClonerFSFunctionException('readlink', $path);
5588 }
5589 $info->link = $link;
5590 }
5591 return $info;
5592 }
5593 $error = error_get_last();
5594 if (empty($error['message']) || strncmp($error['message'], 'lstat(', 0) !== 0) {
5595 throw new ClonerNoFileException($path);
5596 }
5597 }
5598
5599 if (function_exists('stat')) {
5600 $stat = @stat($path);
5601 if ($stat) {
5602 $info = ClonerStatInfo::fromArray($stat);;
5603 if (@is_link($path)) {
5604 $link = $link = readlink($path);
5605 if ($link === false) {
5606 throw new ClonerFSFunctionException('readlink', $path);
5607 }
5608 $info->link = $link;
5609 }
5610 return $info;
5611 }
5612 throw new ClonerFSFunctionException('stat', $path);
5613 } else {
5614 throw new ClonerFSFunctionException('lstat', $path);
5615 }
5616}
5617
5618class ClonerStatInfo
5619{
5620 // https://unix.superglobalmegacorp.com/Net2/newsrc/sys/stat.h.html
5621 const S_IFMT = 0170000; /* type of file */
5622 const S_IFIFO = 0010000; /* named pipe (fifo) */
5623 const S_IFCHR = 0020000; /* character special */
5624 const S_IFDIR = 0040000; /* directory */
5625 const S_IFBLK = 0060000; /* block special */
5626 const S_IFREG = 0100000; /* regular */
5627 const S_IFLNK = 0120000; /* symbolic link */
5628 const S_IFSOCK = 0140000; /* socket */
5629
5630 private $stat;
5631 public $link = '';
5632
5633 private function __construct(array $stat)
5634 {
5635 $this->stat = $stat;
5636 }
5637
5638 /**
5639 * @return bool
5640 */
5641 public function isDir()
5642 {
5643 return ($this->stat['mode'] & self::S_IFDIR) === self::S_IFDIR;
5644 }
5645
5646 public function isLink()
5647 {
5648 return ($this->stat['mode'] & self::S_IFLNK) === self::S_IFLNK;
5649 }
5650
5651 public function getPermissions()
5652 {
5653 return ($this->stat['mode'] & 0777);
5654 }
5655
5656 /**
5657 * @return int
5658 */
5659 public function getSize()
5660 {
5661 return $this->isDir() ? 0 : $this->stat['size'];
5662 }
5663
5664 /**
5665 * @return int
5666 */
5667 public function getMTime()
5668 {
5669 return $this->stat['mtime'];
5670 }
5671
5672 /**
5673 * @param array $stat Result of lstat() or stat() function call.
5674 *
5675 * @return ClonerStatInfo
5676 */
5677 public static function fromArray(array $stat)
5678 {
5679 return new self($stat);
5680 }
5681
5682 public static function makeEmpty()
5683 {
5684 return new self(array('size' => 0, 'mode' => 0, 'mtime' => 0));
5685 }
5686}
5687
5688/**
5689 * As a special case, this function URL-encodes non-valid-UTF-8 strings
5690 * before sorting them, to maintain consistency between platforms.
5691 * The file names themselves are left intact, it only affects sorting.
5692 *
5693 * @param string $path
5694 * @param string $offset
5695 *
5696 * @return string[] Children of $path that do not come before $offset.
5697 *
5698 * @throws ClonerFSFunctionException
5699 */
5700function cloner_fs_list_children($path, $offset = '')
5701{
5702 $files = @scandir($path);
5703 if ($files === false) {
5704 throw new ClonerFSFunctionException('scandir', $path);
5705 }
5706 $children = array();
5707 foreach ($files as $file) {
5708 $encoded = false;
5709 if ($file === '.' || $file === '..') {
5710 continue;
5711 }
5712 if (!cloner_seems_utf8($file)) {
5713 $encoded = true;
5714 $file = cloner_encode_non_utf8($file);
5715 }
5716 if (strlen($offset) && (strcmp($file, $offset) < 0)) {
5717 continue;
5718 }
5719 $children[] = array($encoded, $file);
5720 }
5721 if (PHP_VERSION_ID < 0) {
5722 // Hack to include usort function during build.
5723 cloner_sort_encoded_files('', '');
5724 }
5725 usort($children, 'cloner_sort_encoded_files');
5726 $result = array();
5727 foreach ($children as $file) {
5728 if ($file[0]) {
5729 $result[] = urldecode($file[1]);
5730 continue;
5731 }
5732 $result[] = $file[1];
5733 }
5734 return $result;
5735}
5736
5737function cloner_sort_encoded_files($f1, $f2)
5738{
5739 return strcmp($f1[1], $f2[1]);
5740}
5741
5742class ClonerFSException extends ClonerException
5743{
5744}
5745
5746class ClonerNoFileException extends ClonerFSException
5747{
5748 public $path = '';
5749
5750 /**
5751 * @param string $path
5752 */
5753 public function __construct($path)
5754 {
5755 $this->path = $path;
5756 parent::__construct("File $path does not exist");
5757 }
5758}
5759
5760class ClonerFSFunctionException extends ClonerFSException
5761{
5762 public $fn = '';
5763 public $path = '';
5764 public $error = '';
5765
5766 /**
5767 * @param string $fn One of fopen, fread, flock, etc.
5768 * @param string $path Path on the filesystem.
5769 */
5770 public function __construct($fn, $path)
5771 {
5772 $this->fn = $fn;
5773 $this->path = $path;
5774 $this->error = cloner_last_error_for($fn);
5775 parent::__construct(sprintf('%s error for path %s: %s', $fn, $path, $this->error));
5776 }
5777}
5778
5779class ClonerFSNotDirFunctionException extends ClonerFSException
5780{
5781 public function __construct($path)
5782 {
5783 parent::__construct(sprintf('%s is not a directory', $path));
5784 }
5785}
5786
5787
5788/**
5789 * PHP's error_get_last() returns last error whose message is prefixed with the called function name.
5790 *
5791 * @param string $fnName Prefix that will be looked up.
5792 *
5793 * @return string Error message with the function name, or '$fnName(): unknown error' if it cannot be determined.
5794 */
5795function cloner_last_error_for($fnName)
5796{
5797 $error = error_get_last();
5798 if (!is_array($error) || !isset($error['message']) || !is_string($error['message'])) {
5799 return $fnName.'(): unknown error';
5800 }
5801 $message = $error['message'];
5802 if (strncmp($message, $fnName.'(', strlen($fnName) + 1)) {
5803 // Message not prefixed with $fnName.
5804 return $fnName.'(): unknown error';
5805 }
5806 if (PHP_VERSION_ID >= 70000) {
5807 error_clear_last();
5808 }
5809 return $message;
5810}
5811
5812class ClonerNoConstantException extends ClonerException
5813{
5814 public $constant = '';
5815
5816 public function __construct($constant, $code = self::ERROR_UNEXPECTED)
5817 {
5818 $this->constant = $constant;
5819 parent::__construct("The required constant $constant is not defined", $code);
5820 }
5821}
5822
5823class ClonerRealPathException extends ClonerException
5824{
5825 public $path;
5826
5827 public function __construct($path)
5828 {
5829 $this->path = $path;
5830 parent::__construct("The path $path could not be resolved on the filesystem", 'realpath_empty');
5831 }
5832}
5833
5834class ClonerException extends Exception
5835{
5836 private $error = '';
5837 private $errorCode = '';
5838 private $internalError = '';
5839
5840 const ERROR_UNEXPECTED = 'error_unexpected';
5841
5842 /**
5843 * @param string $error
5844 * @param string $code
5845 * @param string $internalError
5846 */
5847 public function __construct($error, $code = self::ERROR_UNEXPECTED, $internalError = '')
5848 {
5849 $this->message = sprintf('[%s]: %s', $code, $error);
5850 if (strlen($internalError)) {
5851 $this->message .= ": $internalError";
5852 }
5853 $this->error = $error;
5854 $this->errorCode = $code;
5855 $this->internalError = $internalError;
5856 }
5857
5858 public function getError()
5859 {
5860 return $this->error;
5861 }
5862
5863 public function getErrorCode()
5864 {
5865 return $this->errorCode;
5866 }
5867
5868 public function getInternalError()
5869 {
5870 return $this->internalError;
5871 }
5872}
5873
5874class ClonerFunctionException extends ClonerException
5875{
5876 private $fn = '';
5877
5878 public function __construct($fnName, $code = self::ERROR_UNEXPECTED)
5879 {
5880 $this->fn = $fnName;
5881 parent::__construct("Error calling $fnName", $code, cloner_last_error_for($fnName));
5882 }
5883}
5884
5885
5886
5887
5888/**
5889 * WebSocket/Hybi v13 implementation, RFC6455.
5890 *
5891 * @link https://tools.ietf.org/html/rfc6455#section-5.2
5892 */
5893class ClonerWebSocket
5894{
5895 private $addr = "";
5896 private $path = "";
5897 private $connTimeout = 10;
5898 private $rwTimeout = 30;
5899 private $host = "";
5900 private $cert = "";
5901 private $origin = "";
5902 private $proto = "";
5903 private $mask = true;
5904 /** @var resource|null */
5905 private $conn;
5906 private $maxPayload = 134217728; // 128 << 20
5907 private $wsVersion = 13;
5908
5909 static $opContinuation = 0x0;
5910 static $opText = 0x1;
5911 static $opBinary = 0x2;
5912 static $opClose = 0x8;
5913 static $opPing = 0x9;
5914 static $opPong = 0xA;
5915
5916 const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
5917
5918 public function __construct($addr, $path = "", $connTimeout = 10, $rwTimeout = 30, $host = "localhost", $origin = "localhost", $cert = "", $proto = "", $mask = true)
5919 {
5920 $this->addr = $addr;
5921 $this->path = $path;
5922 $this->connTimeout = $connTimeout;
5923 $this->rwTimeout = $rwTimeout;
5924 $this->host = $host;
5925 $this->origin = $origin;
5926 $this->cert = $cert;
5927 $this->proto = $proto;
5928 $this->mask = $mask;
5929 }
5930
5931 /**
5932 * @throws ClonerException
5933 * @throws ClonerNetException
5934 * @throws ClonerURLException
5935 */
5936 public function connect()
5937 {
5938 if ($this->conn !== null) {
5939 return;
5940 }
5941 $key = base64_encode(md5(uniqid("", true), true));
5942 $expectKey = base64_encode(sha1($key.self::GUID, true));
5943 $path = $this->path ? $this->path : "/";
5944 $headers = array(
5945 'Host' => $this->host,
5946 'Connection' => 'upgrade',
5947 'Upgrade' => 'WebSocket',
5948 'Origin' => $this->origin,
5949 'Sec-WebSocket-Key' => $key,
5950 'Sec-WebSocket-Version' => $this->wsVersion,
5951 'Sec-WebSocket-Protocol' => $this->proto,
5952 );
5953 $this->conn = cloner_http_open_request('GET', ClonerURL::fromString($this->addr.$path), $headers, $this->connTimeout, $this->cert);
5954 $res = cloner_http_get_response_headers($this->conn, 10);
5955 if ($res->headers["sec-websocket-accept"] !== $expectKey) {
5956 throw new ClonerException(sprintf("Got WS key %s, expected %s", $res->headers["sec-websocket-accept"], $expectKey), 'invalid_ws_key', $res->headers["sec-websocket-accept"]);
5957 }
5958 if (strlen($this->proto)) {
5959 $protos = array_map('trim', explode(",", $res->headers["sec-websocket-protocol"]));
5960 if (!in_array($this->proto, $protos)) {
5961 throw new ClonerException(sprintf("Need protocol %s, got %s", $this->proto, $res->headers["sec-websocket-protocol"]), 'invalid_ws_key', $res->headers["sec-websocket-protocol"]);
5962 }
5963 }
5964 }
5965
5966 /**
5967 * @param int $code
5968 * @param string $reason
5969 *
5970 * @throws ClonerException
5971 */
5972 public function disconnect($code, $reason)
5973 {
5974 if ($this->conn === null) {
5975 return;
5976 }
5977 $this->writeFrame(true, self::$opClose, pack("n", $code).$reason);
5978 if (@fclose($this->conn) === false) {
5979 throw new ClonerClonerNetFunctionException('fclose', $this->host);
5980 }
5981 $this->conn = null;
5982 }
5983
5984 /**
5985 * @return array 1st element is null if pong, string otherwise. Second is true if the connection is closed.
5986 *
5987 * @throws ClonerException
5988 */
5989 public function readMessage()
5990 {
5991 if ($this->conn === null) {
5992 throw new ClonerException("Socket not connected");
5993 }
5994 $message = null;
5995 $fin = false;
5996 $messageOp = 0x0;
5997 while (!$fin) {
5998 list($fin, $op, $frame) = $this->readFrame();
5999 switch ($op) {
6000 case self::$opContinuation:
6001 if (!$messageOp) {
6002 throw new ClonerException("Continuation frame sent before initial frame", 'ws_protocol_error');
6003 }
6004 if (strlen($message) + strlen($frame) > $this->maxPayload) {
6005 throw new ClonerException(sprintf("Read buffer full, message length: %d", strlen($message) + strlen($frame)), 'ws_read_buffer_full');
6006 }
6007 $message .= $frame;
6008 break;
6009 case self::$opClose:
6010 return array($frame, true);
6011 break;
6012 case self::$opBinary:
6013 case self::$opText:
6014 $messageOp = $op;
6015 $message = $frame;
6016 break;
6017 case self::$opPong:
6018 break;
6019 default:
6020 throw new ClonerException(sprintf("Read failed, invalid op: %d", $op), 'ws_protocol_error');
6021 }
6022 }
6023 return array($message, false);
6024 }
6025
6026 /**
6027 * @return array Triplet of fin:bool, op:int, message:string.
6028 *
6029 * @throws ClonerException
6030 */
6031 private function readFrame()
6032 {
6033 stream_set_timeout($this->conn, $this->rwTimeout);
6034 // $b1 = | FIN |RSV1 |RSV2 |RSV3 | OP1 | OP2 | OP3 | OP4 |
6035 // | 0/1 | 0 | 0 | 0 | n1 | n2 | n3 | n4 |
6036 if (($b1 = @fread($this->conn, 1)) === false) {
6037 throw new ClonerNetSocketException('fread', $this->conn);
6038 }
6039 $meta = stream_get_meta_data($this->conn);
6040 if (!empty($meta["timed_out"])) {
6041 throw new ClonerException("First byte read timeout", 'ws_read_timeout');
6042 }
6043 if (!empty($meta["eof"])) {
6044 throw new ClonerException("Connection closed", 'ws_closed');
6045 }
6046 $b1 = ord($b1);
6047 $fin = (bool)($b1 & 0x80 /*10000000*/);
6048 if (($b1 & 0x70 /*01110000*/) !== 0) {
6049 throw new ClonerException("Reserved bits present", 'ws_protocol_error');
6050 }
6051 $op = $b1 & 0xF; // 00001111
6052 // $b2 = |MASK | Payload length (7 bits) |
6053 // | 0/1 | n1 | n2 | n3 | n4 | n5 | n6 | n7 |
6054 if (($b2 = @fread($this->conn, 1)) === false) {
6055 throw new ClonerNetSocketException('fread', $this->conn);
6056 }
6057 $b2 = ord($b2);
6058 $masked = $b2 & 0x80; // 10000000
6059 $len = $b2 & 0x7F; // 01111111
6060 if ($len === 126 /*01111110*/) {
6061 if (($payloadLen = @fread($this->conn, 2)) === false) {
6062 throw new ClonerNetSocketException('fread', $this->conn);
6063 }
6064 $unpacked = unpack("n", $payloadLen);
6065 $len = end($unpacked);
6066 } elseif ($len === 127 /*01111111*/) {
6067 if (($payloadLen = @fread($this->conn, 8)) === false) {
6068 throw new ClonerNetSocketException('fread', $this->conn);
6069 }
6070 $len = $this->unmarshalUInt64($payloadLen);
6071 }
6072 if ($len > $this->maxPayload) {
6073 throw new ClonerException(sprintf("Read buffer full, frame length: %d", $len), 'ws_read_buffer_full');
6074 }
6075 $mask = "";
6076 if ($masked && (($mask = @fread($this->conn, 4)) === false)) {
6077 throw new ClonerNetSocketException('fread', $this->conn);
6078 }
6079 $message = "";
6080 $toRead = $len;
6081 while ($toRead > 0) {
6082 $chunk = @fread($this->conn, $toRead);
6083 if ($chunk === false) {
6084 throw new ClonerNetSocketException('fread', $this->conn);
6085 }
6086 if ($mask !== "") {
6087 for ($i = 0; $i < strlen($chunk); $i++) {
6088 $chunk[$i] ^= $mask[$i % 4];
6089 }
6090 }
6091 $message .= $chunk;
6092 $toRead -= strlen($chunk);
6093 }
6094 $meta = stream_get_meta_data($this->conn);
6095 if (!empty($meta["timed_out"])) {
6096 throw new ClonerException("Chunk read timeout", 'ws_read_timeout');
6097 }
6098 if (!empty($meta["eof"])) {
6099 throw new ClonerException("Connection closed", 'ws_closed');
6100 }
6101 return array($fin, $op, $message);
6102 }
6103
6104 private function marshalUInt64($value)
6105 {
6106 if (strlen(PHP_INT_MAX) === 19) {
6107 $higher = ($value & 0xffffffff00000000) >> 32;
6108 $lower = $value & 0x00000000ffffffff;
6109 } else {
6110 $higher = 0;
6111 $lower = $value;
6112 }
6113 return pack('NN', $higher, $lower);
6114 }
6115
6116 /**
6117 * @param int $packed
6118 *
6119 * @return int
6120 *
6121 * @throws ClonerException
6122 */
6123 private function unmarshalUInt64($packed)
6124 {
6125 list($higher, $lower) = array_values(unpack('N2', $packed));
6126 if ($higher !== 0 && strlen(PHP_INT_MAX) !== 19) {
6127 throw new ClonerException("Payload too big for 32bit architecture", 'no_64bit_support');
6128 }
6129 $value = $higher << 32 | $lower;
6130 if ($value < 0) {
6131 throw new ClonerException('no_uint64_support');
6132 }
6133 return $value;
6134 }
6135
6136 /**
6137 * @param string $message
6138 *
6139 * @throws ClonerException
6140 */
6141 public function writeMessage($message)
6142 {
6143 if ($this->conn === null) {
6144 throw new ClonerException("Socket not connected");
6145 }
6146 $offset = 0;
6147 $len = strlen($message);
6148 while ($offset < $len) {
6149 $frame = substr($message, $offset, min($len - $offset, 1 << 20));
6150 $op = $offset === 0 ? 0x1 : 0x0;
6151 $offset += strlen($frame);
6152 $fin = $offset >= $len;
6153 $this->writeFrame($fin, $op, $frame);
6154 }
6155 }
6156
6157 /**
6158 * @param bool $fin
6159 * @param int $op
6160 * @param string $frame
6161 *
6162 * @throws ClonerException
6163 */
6164 private function writeFrame($fin, $op, $frame)
6165 {
6166 $mask = $lenLen = "";
6167 $b1 = ($fin ? 0x80 : 0x00) | $op;
6168 $b2 = $this->mask ? 0x80 : 0x00;
6169 $len = strlen($frame);
6170 if ($len > 65535) {
6171 $b2 |= 0x7f;
6172 $lenLen = $this->marshalUInt64($len);
6173 } elseif ($len >= 126) {
6174 $b2 |= 0x7e;
6175 $lenLen = pack("n", $len);
6176 } else {
6177 $b2 |= $len;
6178 }
6179 if ($this->mask) {
6180 $mask = pack("nn", mt_rand(0, 0xffff), mt_rand(0, 0xffff));
6181 for ($i = 0; $i < strlen($frame); $i++) {
6182 $frame[$i] = $frame[$i] ^ $mask[$i % 4];
6183 }
6184 }
6185 $send = pack("CC", $b1, $b2).$lenLen.$mask.$frame;
6186 unset($frame);
6187 stream_set_timeout($this->conn, $this->rwTimeout);
6188 if (($written = @fwrite($this->conn, $send)) === false) {
6189 throw new ClonerNetSocketException('fwrite', $this->conn);
6190 }
6191 }
6192
6193 public function __destruct()
6194 {
6195 if ($this->conn === null) {
6196 return;
6197 }
6198 try {
6199 $code = 1000;
6200 $reason = 'disconnected by client';
6201 $error = error_get_last();
6202 if (!empty($error['message']) && in_array($error['type'], array(E_PARSE, E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR))) {
6203 $code = 1001;
6204 $reason = $error['message'];
6205 }
6206 $this->disconnect($code, $reason);
6207 } catch (ClonerException $e) {
6208 }
6209 }
6210}
6211
6212
6213/**
6214 * Prints a 404 page.
6215 */
6216function cloner_page_404() {
6217?><!doctype html>
6218<html lang="en">
6219<head>
6220 <meta charset="UTF-8">
6221 <meta name="viewport"
6222 content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
6223 <meta http-equiv="X-UA-Compatible" content="ie=edge">
6224 <title>404 Page Not Found</title>
6225</head>
6226<body>
6227404 page not found.
6228</body>
6229</html><?php
6230}
6231
6232/**
6233 * Prints main page HTML content to STDOUT.
6234 */
6235function cloner_page_index() {
6236?><!doctype html>
6237<html lang="en">
6238<head>
6239 <meta charset="UTF-8">
6240 <meta name="viewport"
6241 content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
6242 <meta http-equiv="X-UA-Compatible" content="ie=edge">
6243 <title>Sync</title>
6244 <style>
6245 body {
6246 color: #333;
6247 margin: 0;
6248 height: 100vh;
6249 background-color: #2C454C;
6250 font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
6251 }
6252
6253 .logo {
6254 height: 150px;
6255 background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0ZWQgYnkgSWNvTW9vbi5pbyAtLT4KPCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj4KPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KPHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE2LjA2My0wLjAwNWMtOC44MzYgMC0xNiA3LjE2NC0xNiAxNS45OTkgMCA4LjgzOSA3LjE2NCAxNi4wMDEgMTYgMTYuMDAxIDguODM5IDAgMTYtNy4xNjMgMTYtMTYuMDAxIDAtOC44MzUtNy4xNjEtMTUuOTk5LTE2LTE1Ljk5OXpNMTYuMDYzIDMwLjUyMWMtOC4wMjMgMC0xNC41MjctNi41MDUtMTQuNTI3LTE0LjUyOCAwLTguMDIwIDYuNTA0LTE0LjUyNSAxNC41MjctMTQuNTI1czE0LjUyNSA2LjUwNSAxNC41MjUgMTQuNTI1YzAgOC4wMjMtNi41MDMgMTQuNTI4LTE0LjUyNSAxNC41Mjh6Ij48L3BhdGg+CjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0yNi42NTIgNy45NDNsLTYuMjg5IDYuNzQ1LTMuMTI1LTUuNDMyLTQuODU2IDUuODc2LTIuNTEzLTMuODA3LTYuMjQzIDkuMDgzYzEuODYxIDUuMDQ5IDYuNzE3IDguNjUyIDEyLjQxMyA4LjY1MiA3LjMwNSAwIDEzLjIyNC01LjkyMSAxMy4yMjQtMTMuMjI3IDAtMi45NTctMC45NzEtNS42ODgtMi42MTEtNy44OTF6Ij48L3BhdGg+Cjwvc3ZnPgo=) no-repeat center center;
6256 background-size: 100px 100px;
6257 }
6258
6259 .content {
6260 line-height: 1.4;
6261 background: #fff;
6262 border-radius: 15px;
6263 padding: 15px;
6264 max-width: 650px;
6265 margin: 0 auto;
6266 }
6267
6268 #feedback {
6269 font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace;
6270 word-wrap: break-word;
6271 max-height: 200px;
6272 color: #666;
6273 overflow-y: auto;
6274 }
6275 </style>
6276</head>
6277<body>
6278
6279<div class="logo"></div>
6280
6281<div class="content">
6282 <p>
6283 This window closes automatically when the synchronization finishes. Please, do not close it.
6284 </p>
6285 <p>
6286 Debug info:
6287 </p>
6288 <div id="feedback"></div>
6289</div>
6290
6291<script>
6292 /**
6293 * Creates response for XMLHttpRequest. Safe to call only from onerror and onload, when the request is already finished.
6294 *
6295 * @param {XMLHttpRequest} xhr
6296 * @return {{status: number, response: string|null, headers: string|null}}
6297 */
6298 function createXHRResult(xhr) {
6299 return {
6300 status: xhr.status,
6301 response: xhr.responseText,
6302 headers: xhr.getAllResponseHeaders()
6303 };
6304 }
6305
6306 function request(method, url, callback, payload) {
6307 var xhr = new XMLHttpRequest();
6308 xhr.open(method, url);
6309 xhr.onerror = function(e) {
6310 callback(e, createXHRResult(xhr))
6311 };
6312 xhr.onload = function() {
6313 var result = createXHRResult(xhr);
6314 if (xhr.status !== 200) {
6315 callback(new Error("Non-200 response status code"), result);
6316 return;
6317 }
6318 callback(null, result);
6319 };
6320 xhr.send(payload);
6321 return xhr.abort;
6322 }
6323
6324 function DivFeedback(container) {
6325 this.feedbackContainer = container;
6326 this.lastFeedback = '';
6327 this.lastFeedbackLen = 0;
6328 this.lastFeedbackCount = 0;
6329 this.maxFeedbackLen = 10 << 10;
6330 }
6331
6332 DivFeedback.prototype.send = function(text) {
6333 var prepend = '', cut = 0;
6334 var prefix = '[' + new Date().toJSON() + '] ';
6335 if (this.lastFeedback === '') {
6336 prepend = prefix + text;
6337 } else if (this.lastFeedback === text) {
6338 this.lastFeedbackCount++;
6339 cut = this.lastFeedbackLen;
6340 prepend = prefix + text + Array(this.lastFeedbackCount + 1).join(' .') + "\n";
6341 } else {
6342 this.lastFeedbackCount = 1;
6343 prepend = prefix + text + "\n";
6344 }
6345 this.lastFeedback = text;
6346 this.lastFeedbackLen = prepend.length;
6347 this.feedbackContainer.innerText = prepend + this.feedbackContainer.innerText.substr(cut, Math.max(this.maxFeedbackLen - prepend.length - cut, 0));
6348 };
6349
6350 var feedback = new DivFeedback(document.getElementById('feedback'));
6351
6352 /**
6353 * @param {number} id
6354 * @param {function} abort Connection cancelation function.
6355 */
6356 function RemoteConn(id, abort) {
6357 this.id = id;
6358 this.close = abort;
6359 this.createdAt = new Date();
6360 }
6361
6362 /**
6363 * @param {Array<T>} a
6364 *
6365 * @return {Array<T>}
6366 */
6367 Array.prototype.diff = function(a) {
6368 return this.filter(function(i) {
6369 return a.indexOf(i) === -1;
6370 });
6371 };
6372
6373 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find#Polyfill
6374 if (!Array.prototype.find) {
6375 Object.defineProperty(Array.prototype, 'find', {
6376 value: function(predicate) {
6377 if (this == null) {
6378 throw new TypeError('"this" is null or not defined');
6379 }
6380 var o = Object(this);
6381 var len = o.length >>> 0;
6382 if (typeof predicate !== 'function') {
6383 throw new TypeError('predicate must be a function');
6384 }
6385 var thisArg = arguments[1];
6386 var k = 0;
6387 while (k < len) {
6388 var kValue = o[k];
6389 if (predicate.call(thisArg, kValue, k, o)) {
6390 return kValue;
6391 }
6392 k++;
6393 }
6394 return undefined;
6395 },
6396 configurable: true,
6397 writable: true
6398 });
6399 }
6400
6401 var errorCounter = 0;
6402
6403 function finish() {
6404 window.close();
6405 }
6406
6407 function cleanup() {
6408 request("GET", location.pathname + "?q=cleanup", finish);
6409 }
6410
6411 function main() {
6412 var haveConns = [];
6413 var poll = function() {
6414 var url = location.pathname + "?q=state_poll";
6415 request("GET", url, pollResult)
6416 };
6417 var pollResult = function(e, result) {
6418 if (e) {
6419 if (result && result.status === 404) {
6420 feedback.send("Closing this window");
6421 finish();
6422 return;
6423 }
6424 feedback.send("Error polling endpoint: " + JSON.stringify(e) + "; result: " + JSON.stringify(result));
6425 return;
6426 }
6427 /** @var {{ok: boolean, error: string, state: {host: string, haveConns: Array<string>, wantConns: Array<string>, cached: boolean, age: number} }} pollData */
6428 try {
6429 var pollData = JSON.parse(result.response);
6430 } catch (e) {
6431 feedback.send("Poll error: " + JSON.stringify(e));
6432 setTimeout(poll, 2000);
6433 return;
6434 }
6435 if (!pollData.ok) {
6436 if (pollData.error === 'not_found') {
6437 feedback.send("Task not found");
6438 cleanup();
6439 return;
6440 } else if (pollData.error === 'done') {
6441 feedback.send("Task completed");
6442 cleanup();
6443 return;
6444 }
6445
6446 errorCounter++;
6447 var message = pollData.error;
6448 if (pollData.message) {
6449 message += ': ' + pollData.message;
6450 }
6451 feedback.send(message);
6452 if (errorCounter < 30) {
6453 setTimeout(poll, 2000);
6454 return
6455 }
6456 feedback.send("Aborting after too many retries");
6457 return;
6458 }
6459 errorCounter = 0;
6460 var state = pollData.state;
6461 var createConns = state.wantConns.diff(state.haveConns);
6462 var closeConns = state.haveConns.diff(state.wantConns);
6463 createConns.map(function(connID) {
6464 feedback.send("Opening connection " + connID);
6465 spawn(state.host, connID);
6466 });
6467 closeConns.map(function(connID) {
6468 var conn = haveConns.find(function(conn) {
6469 return conn.id === connID;
6470 });
6471 if (!conn) {
6472 return;
6473 }
6474 feedback.send("Closing connection " + connID);
6475 conn.close();
6476 });
6477 setTimeout(poll, 3000);
6478 };
6479 var spawn = function(host, id) {
6480 var url = location.pathname + "?q=connect&host=" + encodeURIComponent(host) + "&conn_id=" + encodeURIComponent(id);
6481 var abort = request("GET", url, connClosed);
6482 var conn = new RemoteConn(id, abort);
6483 haveConns.push(conn);
6484 };
6485 var connClosed = function(e, result) {
6486 if (e) {
6487 feedback.send("Error spawning connection: " + JSON.stringify(e));
6488 return;
6489 }
6490 try {
6491 /** @var {{ok: boolean, error: string}} connData */
6492 var connData = JSON.parse(result.response);
6493 } catch (e) {
6494 feedback.send("Invalid response: " + JSON.stringify(result.response));
6495 return;
6496 }
6497 if (connData.error) {
6498 feedback.send("Connection closed: " + JSON.stringify(connData.error));
6499 return;
6500 }
6501 feedback.send("Connection closed")
6502 };
6503 poll();
6504 }
6505
6506 main();
6507</script>
6508
6509</body>
6510</html><?php
6511}
6512
6513
6514
6515
6516
6517
6518
6519interface ClonerDBStmt
6520{
6521 /**
6522 * @return int
6523 */
6524 public function getNumRows();
6525
6526 /**
6527 * @return array|null
6528 *
6529 * @throws ClonerException
6530 */
6531 public function fetch();
6532
6533 /**
6534 * @return array|null
6535 *
6536 * @throws ClonerException
6537 */
6538 public function fetchAll();
6539
6540 /**
6541 * @return bool
6542 */
6543 public function free();
6544}
6545
6546interface ClonerDBConn
6547{
6548 /**
6549 * @return ClonerDBInfo
6550 */
6551 public function getConfiguration();
6552
6553 /**
6554 * @param string $query
6555 * @param array $parameters
6556 * @param bool $unbuffered Set to true to not fetch all results into memory and to incrementally read from SQL server.
6557 * See http://php.net/manual/en/mysqlinfo.concepts.buffering.php
6558 *
6559 * @return ClonerDBStmt
6560 * @throws ClonerException
6561 *
6562 */
6563 public function query($query, array $parameters = array(), $unbuffered = false);
6564
6565 /**
6566 * No-return-value version of the query() method. Allows adapters
6567 * to optionally optimize the operation.
6568 *
6569 * @param string $query
6570 *
6571 * @throws ClonerException
6572 */
6573 public function execute($query);
6574
6575 /**
6576 * Escapes string for safe use in statements; quotes are included.
6577 *
6578 * @param string $value
6579 *
6580 * @return string
6581 *
6582 * @throws ClonerException
6583 */
6584 public function escape($value);
6585}
6586
6587/**
6588 * Kill all SQL processes run by the current user on the current database.
6589 *
6590 * @param ClonerDBConn $conn
6591 *
6592 * @throws ClonerException
6593 */
6594function cloner_clear_database_processlist(ClonerDBConn $conn)
6595{
6596 // Use a random identifier so we don't pick up the current process.
6597 $rand = md5(uniqid('', true));
6598 /** @noinspection SqlDialectInspection */
6599 /** @noinspection SqlNoDataSourceInspection */
6600 $list = $conn->query("SELECT ID, INFO FROM information_schema.PROCESSLIST WHERE `USER` = :user AND `DB` = :db AND `INFO` NOT LIKE '%{$rand}%'", array(
6601 'user' => $conn->getConfiguration()->user,
6602 'db' => $conn->getConfiguration()->name,
6603 ))->fetchAll();
6604 foreach ($list as $process) {
6605 $conn->execute("KILL {$process['ID']}");
6606 }
6607 $conn->execute('UNLOCK TABLES');
6608}
6609
6610/**
6611 * @param ClonerDBConn $conn
6612 *
6613 * @return array
6614 *
6615 * @throws ClonerException
6616 */
6617function cloner_db_info(ClonerDBConn $conn)
6618{
6619 $info = array(
6620 'collation' => array(),
6621 'charset' => array(),
6622 );
6623 $list = $conn->query("SHOW COLLATION")->fetchAll();
6624 foreach ($list as $row) {
6625 $info['collation'][$row['Collation']] = true;
6626 $info['charset'][$row['Charset']] = true;
6627 }
6628 return $info;
6629}
6630
6631/**
6632 * @param ClonerDBInfo|array $conf If array, it gets passed to ClonerDBInfo::fromArray.
6633 * @param bool $skipCache True to skip connection cache, used for forcing new connection.
6634 *
6635 * @return ClonerDBConn
6636 *
6637 * @throws ClonerException
6638 */
6639function cloner_db_connection($conf, $skipCache = false)
6640{
6641 if ($conf instanceof ClonerDBConn) {
6642 if (!$skipCache) {
6643 return $conf;
6644 }
6645 $conf = $conf->getConfiguration();
6646 }
6647 static $cache = array();
6648 if ($conf === null && $skipCache) {
6649 // todo: rework this hack to clear cache
6650 $cache = array();
6651 /** @noinspection PhpInconsistentReturnPointsInspection */
6652 return;
6653 }
6654 if (!$conf instanceof ClonerDBInfo) {
6655 $conf = ClonerDBInfo::fromArray($conf);
6656 }
6657 $key = sprintf("%s@%s/%s", $conf->user, $conf->host, $conf->name);
6658 if (!$skipCache && array_key_exists($key, $cache)) {
6659 return $cache[$key];
6660 }
6661 if (extension_loaded('pdo_mysql') && PHP_VERSION_ID > 50206) {
6662 // We need PHP 5.2.6 because of this nasty PDO bug: https://bugs.php.net/bug.php?id=44251
6663 $conn = new ClonerPDOConn($conf);
6664 } elseif (extension_loaded('mysqli')) {
6665 $conn = new ClonerMySQLiConn($conf);
6666 } elseif (extension_loaded('mysql')) {
6667 $conn = new ClonerMySQLConn($conf);
6668 } else {
6669 throw new ClonerException("No drivers available for php mysql connection.", 'no_db_drivers');
6670 }
6671 $cache[$key] = $conn;
6672 return $conn;
6673}
6674
6675/**
6676 * @param ClonerDBInfo $expect Expected configuration.
6677 * @param ClonerDBInfo $got Gotten configuration.
6678 *
6679 * @throws ClonerException If the configuration mismatches.
6680 */
6681function cloner_verify_db_credentials(ClonerDBInfo $expect, ClonerDBInfo $got)
6682{
6683 if ($expect->name !== $got->name) {
6684 throw cloner_exception_db_credentials_differ('name', $expect->name, $got->name);
6685 }
6686 if ($expect->user !== $got->user) {
6687 throw cloner_exception_db_credentials_differ('user', $expect->user, $got->name);
6688 }
6689 if ($expect->getSocket() !== $got->getSocket()
6690 || $expect->getHostname() !== $got->getHostname()
6691 || $expect->getPort() !== $got->getPort()) {
6692 throw cloner_exception_db_credentials_differ('host', $expect->host, $got->host);
6693 }
6694}
6695
6696function cloner_exception_db_credentials_differ($field, $expect, $got)
6697{
6698 return new ClonerException(sprintf('Database %s differs and wp-config.php is read-only; user-provided %s is "%s", but wp-config.php contains the value "%s". Please, either update the wp-config.php file with the right credentials, or make the file writable.', $field, $field, $expect, $got), 'wp_config_readonly_diff');
6699}
6700
6701/**
6702 * @param ClonerDBConn $conn Adapter is used for value escaping.
6703 * @param string $query Query optionally containing :placeholders.
6704 * @param array $params Parameters in format ['placeholder' => "any value to escape"].
6705 *
6706 * @return string Compiled and escaped query.
6707 * @throws ClonerException
6708 */
6709function cloner_bind_query_params(ClonerDBConn $conn, $query, array $params)
6710{
6711 if (count($params) === 0) {
6712 return $query;
6713 }
6714 $replacements = array();
6715 foreach ($params as $name => $value) {
6716 $replacements[":$name"] = $conn->escape($value);
6717 }
6718 return strtr($query, $replacements);
6719}
6720
6721/**
6722 * @param ClonerDBConn $conn
6723 * @param string $prefix WordPress table prefix.
6724 * @param string $option Option name to fetch.
6725 *
6726 * @return string The 'siteurl' option value.
6727 * @throws ClonerException
6728 */
6729function cloner_get_option(ClonerDBConn $conn, $prefix, $option)
6730{
6731 $option = $conn->query(sprintf('SELECT option_value FROM %soptions WHERE option_name=%s', $prefix, $conn->escape($option)))->fetch();
6732 if (!isset($option['option_value'])) {
6733 throw new ClonerException(sprintf('The "%s" option could not be found', $option), "no_option_$option");
6734 }
6735 return $option['option_value'];
6736}
6737
6738
6739
6740
6741
6742
6743
6744
6745
6746
6747
6748
6749// Entry point.
6750
6751if (PHP_SAPI === 'cli') {
6752 return;
6753}
6754
6755$clonerRoot = dirname(__FILE__);
6756$errorHandler = new ClonerErrorHandler($clonerRoot.'/cloner_error_log');
6757$errorHandler->register();
6758if (strlen(session_id())) {
6759 session_write_close();
6760}
6761
6762error_reporting(E_ALL);
6763ini_set('display_errors', 0);
6764ini_set('log_errors', 0);
6765date_default_timezone_set('UTC');
6766ini_set('memory_limit', '512M');
6767set_time_limit(1800);
6768
6769$obLevel = ob_get_level();
6770while ($obLevel) {
6771 $obLevel--;
6772 ob_end_clean();
6773}
6774
6775if (defined('CLONER_STATE')) {
6776 cloner_sync_main();
6777 return;
6778}
6779
6780$requestBody = file_get_contents('php://input');
6781if (defined('CLONER_KEY') && strlen(CLONER_KEY)) {
6782 if (!isset($_GET['key']) || $_GET['key'] !== CLONER_KEY) {
6783 $request = json_decode($requestBody, true);
6784 $message = "Key mismatches.";
6785 if (isset($request['id']) && is_string($request['id'])) {
6786 cloner_send_error_response($request['id'], $message, 'key_mismatch');
6787 return;
6788 }
6789 echo $message;
6790 return;
6791 }
6792}
6793
6794function cloner_base64_rotate($encoded)
6795{
6796 $encode = '][ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/{}';
6797 $reverse = strrev($encode);
6798 return strtr($encoded, $encode, $reverse);
6799}
6800
6801if (strlen($requestBody) !== 0 && strncmp($requestBody, '{', 1) !== 0) {
6802 $requestBody = cloner_base64_rotate($requestBody);
6803}
6804$request = json_decode($requestBody, true);
6805if (empty($request['action']) || !is_string($request['action'])) {
6806 cloner_send_error_response(@$request['id'], "Action name not provided", 'no_action', '', __FILE__, __LINE__);
6807 return;
6808}
6809$errorHandler->setRequestID(@$request['id']);
6810
6811if ($request['action'] === 'flush_rewrite_rules') {
6812 // It's important to bootstrap WordPress while in the global scope!
6813 define('WP_DEBUG', false);
6814 define('MWP_SKIP_BOOTSTRAP', true);
6815 define('AUTOMATIC_UPDATER_DISABLED', true);
6816 define('WP_MEMORY_LIMIT', '256M');
6817 define('WP_MAX_MEMORY_LIMIT', '256M');
6818 define('DISABLE_WP_CRON', true);
6819 $loader = new ClonerLoader($errorHandler);
6820 $loader->hook();
6821 // /wp-admin/admin.php file cuts us off and redirects us to /wp-admin/upgrade.php
6822 // if a database update has to be performed AND $_POST is empty. Avoid that.
6823 if (is_array($_POST)) {
6824 // Avoid E_COMPILE_ERROR: Cannot re-assign auto-global variable _POST
6825 $_POST['foo'] = 'bar';
6826 } else {
6827 $_POST = array('foo' => 'bar');
6828 }
6829
6830 $adminScript = $clonerRoot.'/wp-admin/admin.php';
6831 if (!is_file($adminScript)) {
6832 /** @noinspection PhpUnhandledExceptionInspection */
6833 throw new ClonerException('Could not find the /wp-admin/admin.php file required to bootstrap WordPress.', 'wp_admin_not_found');
6834 }
6835 /** @noinspection PhpIncludeInspection */
6836 require $adminScript;
6837
6838 if (!defined('ABSPATH') || !defined('WPINC')) {
6839 /** @noinspection PhpUnhandledExceptionInspection */
6840 throw new ClonerException('ABSPATH and WPINC must be defined after initialization of admin context.');
6841 }
6842
6843 $absPath = cloner_constant('ABSPATH');
6844 $wpInc = cloner_constant('WPINC');
6845 $pluggable = $absPath.$wpInc.'/pluggable.php';
6846 if (is_file($pluggable)) {
6847 /** @noinspection PhpIncludeInspection */
6848 require_once $pluggable;
6849 }
6850 cloner_wp_polyfill();
6851}
6852
6853$result = cloner_run_action($request['action'], (array)@$request['params'], $clonerRoot);
6854cloner_send_success_response((string)@$request['id'], $result);
6855
6856function __bundler_sourcemap() { return array(array(225,'cloner/serializer.php'),array(314,'cloner/parser.php'),array(717,'cloner/db_migration.php'),array(872,'cloner/setup.php'),array(1342,'cloner/env.php'),array(1688,'cloner/db_importer.php'),array(1853,'cloner/client.php'),array(2292,'cloner/net.php'),array(2429,'cloner/db_mysql.php'),array(2557,'cloner/db_mysqli.php'),array(2694,'cloner/db_pdo.php'),array(2753,'cloner/db_info.php'),array(4311,'cloner/common.php'),array(5595,'cloner/actions.php'),array(6268,'cloner/db_scanner.php'),array(6718,'cloner/http.php'),array(7197,'cloner/fs.php'),array(7295,'cloner/errors.php'),array(7622,'cloner/websocket.php'),array(7923,'cloner/sync.php'),array(8148,'cloner/db.php'),array(8266,'cloner/cloner.php'),); }
6857
6858function __cloner_get_state() {
6859 return array (
6860 'wpURL' => 'http://pawel.rybysiedlce.pl/test',
6861 'wpAbsPath' => '/home/pawelryb/public_html/test',
6862 'wpContentPath' => 'wp-content',
6863 'wpPluginsPath' => 'wp-content/plugins',
6864 'wpMuPluginsPath' => 'wp-content/mu-plugins',
6865 'wpUploadsPath' => 'wp-content/uploads',
6866 'wpConfigPath' => 'wp-config.php',
6867 'wpConfig' => 'PD9waHAKLyoqCiAqIFRoZSBiYXNlIGNvbmZpZ3VyYXRpb24gZm9yIFdvcmRQcmVzcwogKgogKiBUaGUgd3AtY29uZmlnLnBocCBjcmVhdGlvbiBzY3JpcHQgdXNlcyB0aGlzIGZpbGUgZHVyaW5nIHRoZQogKiBpbnN0YWxsYXRpb24uIFlvdSBkb24ndCBoYXZlIHRvIHVzZSB0aGUgd2ViIHNpdGUsIHlvdSBjYW4KICogY29weSB0aGlzIGZpbGUgdG8gIndwLWNvbmZpZy5waHAiIGFuZCBmaWxsIGluIHRoZSB2YWx1ZXMuCiAqCiAqIFRoaXMgZmlsZSBjb250YWlucyB0aGUgZm9sbG93aW5nIGNvbmZpZ3VyYXRpb25zOgogKgogKiAqIE15U1FMIHNldHRpbmdzCiAqICogU2VjcmV0IGtleXMKICogKiBEYXRhYmFzZSB0YWJsZSBwcmVmaXgKICogKiBBQlNQQVRICiAqCiAqIEBsaW5rIGh0dHBzOi8vY29kZXgud29yZHByZXNzLm9yZy9FZGl0aW5nX3dwLWNvbmZpZy5waHAKICoKICogQHBhY2thZ2UgV29yZFByZXNzCiAqLwoKLy8gKiogTXlTUUwgc2V0dGluZ3MgLSBZb3UgY2FuIGdldCB0aGlzIGluZm8gZnJvbSB5b3VyIHdlYiBob3N0ICoqIC8vCi8qKiBUaGUgbmFtZSBvZiB0aGUgZGF0YWJhc2UgZm9yIFdvcmRQcmVzcyAqLwpkZWZpbmUoICdEQl9OQU1FJywgJ3Bhd2VscnliX3Rlc3QyJyApOwoKLyoqIE15U1FMIGRhdGFiYXNlIHVzZXJuYW1lICovCmRlZmluZSggJ0RCX1VTRVInLCAncGF3ZWxyeWJfdGVzdDInICk7CgovKiogTXlTUUwgZGF0YWJhc2UgcGFzc3dvcmQgKi8KZGVmaW5lKCAnREJfUEFTU1dPUkQnLCAncW0xZk5HMVpDc25hJyApOwoKLyoqIE15U1FMIGhvc3RuYW1lICovCmRlZmluZSggJ0RCX0hPU1QnLCAnbG9jYWxob3N0JyApOwoKLyoqIERhdGFiYXNlIENoYXJzZXQgdG8gdXNlIGluIGNyZWF0aW5nIGRhdGFiYXNlIHRhYmxlcy4gKi8KZGVmaW5lKCAnREJfQ0hBUlNFVCcsICd1dGY4JyApOwoKLyoqIFRoZSBEYXRhYmFzZSBDb2xsYXRlIHR5cGUuIERvbid0IGNoYW5nZSB0aGlzIGlmIGluIGRvdWJ0LiAqLwpkZWZpbmUoICdEQl9DT0xMQVRFJywgJycgKTsKCi8qKgogKiBBdXRoZW50aWNhdGlvbiBVbmlxdWUgS2V5cyBhbmQgU2FsdHMuCiAqCiAqIENoYW5nZSB0aGVzZSB0byBkaWZmZXJlbnQgdW5pcXVlIHBocmFzZXMhCiAqIFlvdSBjYW4gZ2VuZXJhdGUgdGhlc2UgdXNpbmcgdGhlIHtAbGluayBodHRwczovL2FwaS53b3JkcHJlc3Mub3JnL3NlY3JldC1rZXkvMS4xL3NhbHQvIFdvcmRQcmVzcy5vcmcgc2VjcmV0LWtleSBzZXJ2aWNlfQogKiBZb3UgY2FuIGNoYW5nZSB0aGVzZSBhdCBhbnkgcG9pbnQgaW4gdGltZSB0byBpbnZhbGlkYXRlIGFsbCBleGlzdGluZyBjb29raWVzLiBUaGlzIHdpbGwgZm9yY2UgYWxsIHVzZXJzIHRvIGhhdmUgdG8gbG9nIGluIGFnYWluLgogKgogKiBAc2luY2UgMi42LjAKICovCmRlZmluZSggJ0FVVEhfS0VZJywgICAgICAgICAgJy0uR0t9WSp5KS96JW8lbUV7R01+WnJmQ0UmXmkvdGRnM04oPiFfPEZXZFU3ZFBfdns5LDNqbnBnJntXdkR8dSknICk7CmRlZmluZSggJ1NFQ1VSRV9BVVRIX0tFWScsICAgJ3tnVDlKakNMNVQpPlBnRzhXLWtSUltTL0FyVEVZfT08M3F+bENMQW9eakx4YnMqS2gsKSVdUGxmJXR1IGJVKyEnICk7CmRlZmluZSggJ0xPR0dFRF9JTl9LRVknLCAgICAgJyZzXW1wbVlvZk9aVzx8MS5Ib3xKYkZQQSRWNjdxd3xSaEAoRWxASyhaLHhsaEMlJnpsdztANGVdezQkU2lHPXAnICk7CmRlZmluZSggJ05PTkNFX0tFWScsICAgICAgICAgJ0pRO2ZlZ01OPSpnYksyUj9JTUhwQUFvZUhJZXY0S1U8LFt9TktqTyggfDBfOFZEX1hyVUlMXWphPENTbSh+QXUnICk7CmRlZmluZSggJ0FVVEhfU0FMVCcsICAgICAgICAgJ0JRL1heUW1ITyMgRDlaIDRqU3NxPV1rZm51WWAxbjhDTTheVEwjcElCcywmRkQ0JWRvdE8gfmZaVkkpdmBPVHknICk7CmRlZmluZSggJ1NFQ1VSRV9BVVRIX1NBTFQnLCAgJ2JyK3F6eSlfaThpdXh2ey9BNF0lSmVkbmMpNmErQyVfV3BSbG82XmpLJWhDPnVrWDBjWkozezJfWF4oNENDdk0nICk7CmRlZmluZSggJ0xPR0dFRF9JTl9TQUxUJywgICAgJ005XmV7MDY6Zzdwb1hdVGBydH5naDMoKms6KTJoQTwkPiU4YHUsYFdgSG0xUDNJQWBwZlAxL3NudDgjOSZ+ei8nICk7CmRlZmluZSggJ05PTkNFX1NBTFQnLCAgICAgICAgJ0RQNm5pJVN+RGNYXyg8Uzp6VCYhOTFqKmM2VmdjL3d4QXl6OzgrTz9+TFJOUio+a2YyeFhuYVFjaSM/clRNXUEnICk7CmRlZmluZSggJ1dQX0NBQ0hFX0tFWV9TQUxUJywgJz1BamQxKDVCR2U0NUR3SEJRYjtQKix7M01RRiRkNTlVTVNVd3hnO3hSLnssJDBfNVFjRU8lOXg/dzk3d2AtXS4nICk7CgovKioKICogV29yZFByZXNzIERhdGFiYXNlIFRhYmxlIHByZWZpeC4KICoKICogWW91IGNhbiBoYXZlIG11bHRpcGxlIGluc3RhbGxhdGlvbnMgaW4gb25lIGRhdGFiYXNlIGlmIHlvdSBnaXZlIGVhY2gKICogYSB1bmlxdWUgcHJlZml4LiBPbmx5IG51bWJlcnMsIGxldHRlcnMsIGFuZCB1bmRlcnNjb3JlcyBwbGVhc2UhCiAqLwokdGFibGVfcHJlZml4ID0gJ3dwXyc7CgoKCgovKiBUaGF0J3MgYWxsLCBzdG9wIGVkaXRpbmchIEhhcHB5IHB1Ymxpc2hpbmcuICovCgovKiogQWJzb2x1dGUgcGF0aCB0byB0aGUgV29yZFByZXNzIGRpcmVjdG9yeS4gKi8KaWYgKCAhIGRlZmluZWQoICdBQlNQQVRIJyApICkgewoJZGVmaW5lKCAnQUJTUEFUSCcsIGRpcm5hbWUoIF9fRklMRV9fICkgLiAnLycgKTsKfQoKLyoqIFNldHMgdXAgV29yZFByZXNzIHZhcnMgYW5kIGluY2x1ZGVkIGZpbGVzLiAqLwpyZXF1aXJlX29uY2UgQUJTUEFUSCAuICd3cC1zZXR0aW5ncy5waHAnOwo=',
6868 'wpTablePrefix' => 'wp_',
6869 'wpInstallDir' => '/test',
6870 'dbUser' => 'pawelryb_test2',
6871 'dbPassword' => 'qm1fNG1ZCsna',
6872 'dbHost' => 'localhost',
6873 'dbName' => 'pawelryb_test2',
6874 'envFlywheel' => false,
6875 'envOpenShift' => false,
6876 'envGoDaddyVersion' => 0,
6877 'envPHPVersion' => 70133,
6878 'keepOptions' =>
6879 array (
6880 '_worker_public_key' => 'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF1bjFHQ3IvQkV2eHpWS1NxRU5sNQpRK0p0cVZJRWs3NGU4ZEFOcHJtaFdaQ20zbktkbm5ETXI0Wi96aCtkdWVlV2JqSEU4WWhUd0gvWjE1TUZtdGYzCkxpOUJNd0c4b2FiYVpZSGtDK1pGQXRqRUxSQnlBQWlkTFc1V1BScTJ0SmlhK1Q3c0xFYW5DVGtoa3JVT3BFcWMKSU4vU0lpSXNTSHpNMGFNNDFQK0FpVVg5OWc3YUhoN1FmY2U2b1JUQlRRL2J0dW0xdWRtcTlMWTZUaTRiQWFvUgpBeWNpVnNxNGJoSDNqeWI4NEV2RG5ISTIvWGJDNm1zOHdJdlR6QzUxQ1V5dllseTVlM0ptMis3eC9scmxETDA2CjhlSHlRbXQ1TWQrbHd1MFN1dkh4bEJCRGJxZjU5OFVwYWpRQW5rTnVoVDlYaFVWZkVTai93V21BU1BIQ2ppb04KMHdJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==',
6881 'mwp_worker_configuration' => 'a:9:{s:10:"master_url";s:21:"https://managewp.com/";s:15:"master_cron_url";s:75:"https://managewp.com/wp-content/plugins/master/mwp-notifications-handle.php";s:20:"noti_cache_life_time";s:5:"86400";s:27:"noti_treshold_spam_comments";s:2:"10";s:30:"noti_treshold_pending_comments";s:1:"0";s:31:"noti_treshold_approved_comments";s:1:"0";s:19:"noti_treshold_posts";s:1:"0";s:20:"noti_treshold_drafts";s:1:"0";s:8:"key_name";s:8:"managewp";}',
6882 'mwp_service_key' => 'd97c4921-572f-4c70-936f-123bde31bce8',
6883 'mwp_potential_key' => '4e28fe53-884c-4720-ba12-8c99013dcf6a',
6884 'mwp_potential_key_time' => '1585311522',
6885 'mwp_container_site_parameters' => 'a:0:{}',
6886 'mwp_container_parameters' => 'a:0:{}',
6887 'mwp_communication_keys' => 'a:1:{i:5945648;a:2:{s:3:"key";s:36:"ed282bf0-3c74-4ee4-b7ef-27e922b08e59";s:5:"added";i:1585311520;}}',
6888 'mwp_public_keys' => 'a:8:{i:0;a:6:{s:2:"id";s:19:"managewp_1582941902";s:7:"service";s:8:"managewp";s:9:"validFrom";s:19:"2020-03-15 01:26:15";s:7:"validTo";s:19:"2020-04-16 01:26:15";s:9:"publicKey";s:451:"-----BEGIN PUBLIC KEY-----
6889MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtTNoEJjcKD4JDeF6EciQ
6890ITUK5wEEs2tz04/HOY5xfpK6cVrtPITq5CLWx47kIxcZf7ECRA65Y91/tpalbwWy
6891pKo7sk5Bpke5xewkwZ56EtPV8F/cxMBeoe6WmaPIkioWC6+BTUCW1e4r347CXdEx
6892dg8fNy1NLSs6FxQpmd8WEVNDA8vqfjUN0Ky3XdrBX3rmi4aUwsJYwTQJzfyleiLa
6893LKoCPiCi8N6xuZ/poZJeqmHAtGoap/ZC+w8T+P112tSWL5MwfMpjjFGP0P8U1Edj
68944RgOkdkv6sCL6s5mCDHcg0YRR8pC1wDG+QHug1Z1fEFLeTP3mLdRMWbcI3+c5BNs
6895HwIDAQAB
6896-----END PUBLIC KEY-----
6897";s:13:"useServiceKey";b:0;}i:1;a:6:{s:2:"id";s:23:"managewp_dev_1582941902";s:7:"service";s:12:"managewp_dev";s:9:"validFrom";s:19:"2020-03-15 01:26:15";s:7:"validTo";s:19:"2020-04-16 01:26:15";s:9:"publicKey";s:451:"-----BEGIN PUBLIC KEY-----
6898MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwQYilYPwbcT9b1ca4beG
6899uYm8k4NiNzf/h4CBFH3IKGw9Orr8NGbClTK60pYqgJrQjxRsgacrnwHzPOOf5uTE
69002I45XqobJna75VMLrzM/dFkUWMBTxV+bVc85wOd23Y3uziAlYdXrhD2kIbt4TqpV
6901NNm21OXXyNhk4r7Qwz6ekcekbuzFs6sQtDm00nKZ+FY4n3iRbU2KTAJ9jXiLP9vc
6902m8sfjy3ATkl8XYgxXIDTdyvkp2iohgsvEAq3mmWrS6L+sk7iFHEXar+bkoN7vMaB
6903Rv+6tvmUsvakS8Psiy7QoC33FySG20W/zfJQmPoqPD6Ol5q4XhazGbbaDnFxHNcg
6904FwIDAQAB
6905-----END PUBLIC KEY-----
6906";s:13:"useServiceKey";b:0;}i:2;a:6:{s:2:"id";s:16:"mwp20_1582941902";s:7:"service";s:5:"mwp20";s:9:"validFrom";s:19:"2020-03-15 01:26:15";s:7:"validTo";s:19:"2020-04-16 01:26:15";s:9:"publicKey";s:451:"-----BEGIN PUBLIC KEY-----
6907MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq2GSCJn2Z1j/1TOjLwfm
6908IMt9sa+ujJxlhnhzCwbfRjncn5RLELGhk42wBvY6IcvnG3eTAOa6TMFvoeusQUIG
69094nVeB8yGvz0DXYG+cL4J7LOw7vpV5uRFEFdqnS5iOI8f8k5SOFpEB8Z9HBZ5DI+h
6910QdcAUviOjoWcbP1n6UC5xRtk0ZcZRlSngbjn1KHWdIhK45Id2wnyTdG2Tihlh0JS
6911zVUyJOsSP7Ggn+PKz+OZFWdAevoA3ewadxaaGlZXfZerDnr6u3dL5Wb99kRNap/M
6912FngJjF4oXFgSRQjvzRv59P6b6Nef0eJUI1aHts7t/nw5cljLauhak8aMfe/rXmWM
69137QIDAQAB
6914-----END PUBLIC KEY-----
6915";s:13:"useServiceKey";b:1;}i:3;a:6:{s:2:"id";s:15:"wpps_1582549502";s:7:"service";s:4:"wpps";s:9:"validFrom";s:19:"2020-03-10 13:05:01";s:7:"validTo";s:19:"2020-04-11 13:05:01";s:9:"publicKey";s:451:"-----BEGIN PUBLIC KEY-----
6916MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo6MUM6mEWiXEzY7Q52H2
6917XAm49w2P8EjddfCk5RzG8cG4zlq0obcpVgLnuXviov3KEhj2KFIOxXnxH58/oGu7
69189SVoxDP3qJR6+yL4oRuw9+Bp/ETCI04EcnBDQgbST+AEh0gnU6LlBdbdEbDarJf0
6919ocTLqsPcilsuAnEdQrXeLkFDX1Rlcqp0fwnqoQ+nhjcjYGIJgFwqK1K0jLa+F4wE
6920ZFqW6f8N7eyNiTLocMzOcuAnQVC42wDU+FLPoVE/flI/jX+z6dEy3Bj/NTLFTcbT
6921xVwuqvTj7vW5sAjjTfO9aaRcsaMR7yA1h7r36beYlu1weqE82X+3SIYAx3QbuRhI
6922DQIDAQAB
6923-----END PUBLIC KEY-----
6924";s:13:"useServiceKey";b:1;}i:4;a:6:{s:2:"id";s:15:"wpps_1585141501";s:7:"service";s:4:"wpps";s:9:"validFrom";s:19:"2020-04-09 13:05:01";s:7:"validTo";s:19:"2020-05-11 13:05:01";s:9:"publicKey";s:451:"-----BEGIN PUBLIC KEY-----
6925MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuuwAdHFarvLBpSZXZjr7
6926xeZTFnNyfX5cfFc6bt6VwNO79vIZ4qkgSwpnGyGnZ/L763VUMlEKhlU1Wi4LW92U
6927lCwNAhZORL19/Ki9DLgaMTYnYXiDo8yaRw+Tr81tLNcEB8wcsAqa3Z+wh28LEqBz
6928fu701WWoDn/oR7QUzEMWkzLXCZtX0Es6wIIjWGlg3iACbcrKwgWhR3V+TAY3xzY5
6929dVJkp2Ng3XuSZsSknAuySR8z3mByBHVOAMIAqjJdHM5XAzez4f3/5ZAxBzk4t6Yp
6930hl5U4E1qTn5QmRorfPo+0Z/CTCRI9JTfv1SQ8X8NzjVkCSJb0cqVH6cXLIfuMQdy
6931YwIDAQAB
6932-----END PUBLIC KEY-----
6933";s:13:"useServiceKey";b:1;}i:5;a:6:{s:2:"id";s:25:"cookie_service_1583773502";s:7:"service";s:14:"cookie_service";s:9:"validFrom";s:19:"2020-03-24 16:27:25";s:7:"validTo";s:19:"2020-04-25 16:27:25";s:9:"publicKey";s:451:"-----BEGIN PUBLIC KEY-----
6934MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7XxuRHhg57vP1dAcmT1a
6935UQ3yXQ9gepoLBalN9GsR8K5Jjv2rzRFNzjVtiRPG8eoKbtWN8S3+ZZt2NRNtOwRE
6936ulX2HNHH4U1RBvP4ci5rIgDqseMY4hY68DCv3Pdpgt0LuYfJPWadv1RR2QIInG5A
6937OssJstc9eNOY4KtWrZxzKgXqWKNCX0eh52LaKYTNLAQif9a672h8oVhaykRjV0rM
6938GbAsc69Oh8luKeg4uVVAwEI46wH7VUheqpWsi1sCBdyutvNuZIdF1QvUwz3il1+Z
693985sDdLho2HTrvPAqLl2WI4y0vwWlwmhse/N6D/3W6kxL0kiNW58M3eOPaQeoC3co
6940SQIDAQAB
6941-----END PUBLIC KEY-----
6942";s:13:"useServiceKey";b:1;}i:6;a:6:{s:2:"id";s:15:"mwp1_1582639502";s:7:"service";s:4:"mwp1";s:9:"validFrom";s:19:"2020-03-11 14:05:01";s:7:"validTo";s:19:"2020-04-12 14:05:01";s:9:"publicKey";s:451:"-----BEGIN PUBLIC KEY-----
6943MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuxNH/YY/nK35qm3SH9UJ
6944QGwJAyzKXwmzETqGgzaYO84OYFY0CHP/dqAsia2FfUH4fzFuttVcmI7A+5+hZN5j
6945DTrQ2CZkHFTB/Fo5HdOOAH8Lp9PuZUoSUo2leQ3+uwzFKSWWlnf23l0ZPHsnInDx
69461cDNeZEHMJc+dLUWqgTEc7hRPrUj6+arj1Fwxb/SrMdXI0hWCQOnLaUhArajBU1k
6947VbNJ+Bcu2Z3dK7Mc5Q72sWCEG4ZzCNehS2Rw9VAaIPjiBOD2vD+XZPpNyuGmbsgu
694830SrUsExNLS9Gi5HU06vy01cPVLdNbAnVJAK2Eqp2ZWvroL4LjR9+hLtnTqfgPwe
6949bwIDAQAB
6950-----END PUBLIC KEY-----
6951";s:13:"useServiceKey";b:1;}i:7;a:6:{s:2:"id";s:15:"mwp1_1585231502";s:7:"service";s:4:"mwp1";s:9:"validFrom";s:19:"2020-04-10 14:05:01";s:7:"validTo";s:19:"2020-05-12 14:05:01";s:9:"publicKey";s:451:"-----BEGIN PUBLIC KEY-----
6952MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/YKnapxCYCO2x6m71czC
6953G/HlDiWeOrXgPLZoodGp8e22XnM522bGfK5CyIXxyLzPjbESdbCTxtg1zZ9PWVxs
6954FLF3hE701Xq3ZkuUQ91jVKNXvhIehlgaRLP7zDccXQasMhQ63FFMY11hW0Me6JGC
6955lM47/5GNZWa6UPpQGZDdN0WxSvr3N/QZw8p/fwmr4XosIEFyP3747wieEum434Wj
6956xFv6wpxG49ecoyLCt6jpDoon7vG4R7f2OnCr6MlNvlM/BZdc3vu1alhbEWybgtPq
69574PR9ctBMw2CvV16yS0BwAdfzkvhZNSVFxbwx34LySTgZIFiRnbJXChw4QQyCykPO
6958bQIDAQAB
6959-----END PUBLIC KEY-----
6960";s:13:"useServiceKey";b:1;}}',
6961 'mwp_public_keys_refresh_time' => '1585311458',
6962 ),
6963 'noRelay' => false,
6964);
6965}