journal = $journal; } /** Put $staged in place of the live item, keeping the live item (if any) in $aside. */ public function replace(string $group, string $item, string $staged, string $live, string $aside): void { if (file_exists($live) || is_link($live)) { $this->move('out', $group, $item, $live, $aside); } $this->move('in', $group, $item, $staged, $live); } public function move_aside(string $group, string $item, string $live, string $aside): void { $this->move('out', $group, $item, $live, $aside); } /** * Undo the moves in this journal after the first $keep, newest first; the journal then holds only those * $keep moves (and is deleted when none are left). */ public function rollback(int $keep = 0): void { $lines = self::lines($this->journal); foreach (array_reverse(array_slice($lines, $keep)) as $line) { if ((file_exists($line['t']) || is_link($line['t'])) && !file_exists($line['f'])) { Fs::move($line['t'], $line['f']); } } if ($keep <= 0 || $lines === []) { wp_delete_file($this->journal); return; } $kept = ''; foreach (array_slice($lines, 0, $keep) as $line) { $kept .= json_encode($line, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n"; } Fs::put($this->journal, $kept); } /** How many moves this journal holds. */ public function count(): int { return count(self::lines($this->journal)); } /** * Undo a finished swap recorded in $old_journal, recording the reverse moves in this journal: items that * came in go aside into $aside_root//, items that were moved aside come back. */ public function revert(string $old_journal, string $aside_root): void { foreach (array_reverse(self::lines($old_journal)) as $line) { if ($line['k'] === 'in') { if (file_exists($line['t']) || is_link($line['t'])) { $this->move('out', $line['g'], $line['r'], $line['t'], $aside_root . '/' . $line['g'] . '/' . $line['r']); } } elseif (file_exists($line['t']) || is_link($line['t'])) { $this->move('in', $line['g'], $line['r'], $line['t'], $line['f']); } } } /** @return array */ public static function lines(string $journal): array { $out = []; $raws = !is_file($journal) ? [] : (is_readable($journal) ? file($journal, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) : false); if ($raws === false) { // an undo that read nothing would leave the files it should put back throw new RelayError( 'files.journal_unreadable', esc_html__('SiteRelay could not read its record of the files it replaced.', 'siterelay'), esc_html__('Check that the web server can read SiteRelay\'s folder in wp-content/uploads (siterelay, or relay on a site an earlier version set up).', 'siterelay') ); } foreach ($raws as $raw) { $line = json_decode($raw, true); if (is_array($line) && isset($line['k'], $line['g'], $line['r'], $line['f'], $line['t'])) { $out[] = $line; } } return $out; } private function move(string $kind, string $group, string $item, string $from, string $to): void { Fs::ensure_dir(dirname($this->journal)); // Recorded first: a move the journal doesn't know of could not be undone, so none happens without it. Fs::put($this->journal, json_encode(['k' => $kind, 'g' => $group, 'r' => $item, 'f' => $from, 't' => $to], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n", true); Fs::move($from, $to); } }