settings = $settings; $this->jobs = $jobs; } /** Hooked to relay_job_finished. */ public function finished(Job $job): void { $event = self::event($job); $s = $this->settings->get(); if ($event === null || empty($s['events'][$event]) || (!$s['email']['on'] && !$s['webhook']['on'])) { return; } $message = self::message($job, $event); foreach ($this->send($s, $message) as $channel => $result) { if ($result !== true) { /* translators: 1: "email" or "webhook", 2: error message */ $this->jobs->event($job->id, 'warning', 'notify.failed', sprintf(__('The %1$s notification could not be sent: %2$s', 'siterelay'), $channel, $result)); } } } /** A test message to the channels in $s (or the saved settings). @return array channel => true or the error */ public function test(?array $s = null): array { $s = $s ?? $this->settings->get(); $site = SiteInfo::plain_name((string) get_bloginfo('name')); $text = sprintf( /* translators: %s: site name */ __('Test from SiteRelay on %s: notifications work.', 'siterelay'), $site ); return $this->send($s, ['subject' => '[SiteRelay] ' . $text, 'body' => $text . "\n\n" . home_url(), 'payload' => ['text' => $text, 'event' => 'test', 'site' => ['name' => $site, 'url' => home_url()]]]); } public static function event(Job $job): ?string { if ($job->status === Job::COMPLETED) { return $job->role === 'receiver' ? 'incoming.' . ($job->direction === 'push' ? 'push' : 'pull') : 'job.completed'; } return in_array($job->status, [Job::FAILED, Job::ABANDONED], true) ? 'job.failed' : null; } /** @return array{subject: string, body: string, payload: array} */ public static function message(Job $job, string $event): array { $site = SiteInfo::plain_name((string) get_bloginfo('name')); $title = self::title($job); $url = \Relay\Admin\Menu::url('#/jobs/' . $job->id); $outcome = $job->status === Job::COMPLETED ? __('completed', 'siterelay') : ($job->status === Job::ABANDONED ? __('was abandoned', 'siterelay') : __('failed', 'siterelay')); /* translators: 1: job title, e.g. "Pull from example.com", 2: "completed" or "failed", 3: site name */ $line = sprintf(__('%1$s %2$s on %3$s', 'siterelay'), $title, $outcome, $site); $body = [$line, '', __('Site:', 'siterelay') . ' ' . home_url()]; if ($job->key_label !== '' && $job->role === 'receiver') { $body[] = __('Access key:', 'siterelay') . ' ' . $job->key_label; } if ($job->started_at !== null) { $body[] = __('Started:', 'siterelay') . ' ' . $job->started_at . ' UTC'; } if ($job->finished_at !== null) { $body[] = __('Finished:', 'siterelay') . ' ' . $job->finished_at . ' UTC'; } if (is_array($job->error)) { $body[] = ''; $body[] = (string) ($job->error['message'] ?? ''); if (!empty($job->error['hint'])) { $body[] = (string) $job->error['hint']; } } $body[] = ''; $body[] = __('Details:', 'siterelay') . ' ' . $url; return [ 'subject' => '[SiteRelay] ' . $line, 'body' => implode("\n", $body), 'payload' => [ 'text' => $line . ': ' . $url, 'event' => $event, 'site' => ['name' => $site, 'url' => home_url()], 'job' => [ 'id' => $job->id, 'type' => $job->type, 'role' => $job->role, 'direction' => $job->direction, 'status' => $job->status, 'title' => $title, 'peer_url' => $job->peer_url, 'key_label' => $job->key_label, 'started_at' => $job->started_at, 'finished_at' => $job->finished_at, 'error' => is_array($job->error) ? ['code' => $job->error['code'] ?? '', 'message' => $job->error['message'] ?? ''] : null, 'url' => $url, ], ], ]; } /** The job's title, as in History ("Pull from example.com"). */ public static function title(Job $job): string { $host = (string) wp_parse_url($job->peer_url, PHP_URL_HOST); switch ($job->type) { case 'pull': /* translators: %s: other site's host name */ return $job->role === 'receiver' ? sprintf(__('Incoming pull from %s', 'siterelay'), $host) : sprintf(__('Pull from %s', 'siterelay'), $host); case 'push': /* translators: %s: other site's host name */ return $job->role === 'receiver' ? sprintf(__('Incoming push from %s', 'siterelay'), $host) : sprintf(__('Push to %s', 'siterelay'), $host); case 'export': return __('Database export', 'siterelay'); case 'find_replace': return __('Find & Replace', 'siterelay'); case 'backup': return __('Backup', 'siterelay'); case 'backup_restore': return __('Restore from a backup', 'siterelay'); case 'import': return __('Database import', 'siterelay'); default: return __('Undo', 'siterelay'); } } /** @return array */ private function send(array $s, array $message): array { $out = []; if (!empty($s['email']['on'])) { $to = NotificationSettings::recipients((string) $s['email']['to']); $out['email'] = $to !== [] && wp_mail($to, $message['subject'], $message['body']) ? true : __('wp_mail() could not send the email. Check the site\'s mail setup.', 'siterelay'); } if (!empty($s['webhook']['on'])) { $response = wp_remote_post((string) $s['webhook']['url'], [ 'timeout' => 5, 'redirection' => 0, 'headers' => ['Content-Type' => 'application/json'], 'body' => (string) wp_json_encode($message['payload']), 'user-agent' => 'SiteRelay/' . RELAY_VERSION . '; ' . home_url(), ]); $code = is_wp_error($response) ? 0 : (int) wp_remote_retrieve_response_code($response); /* translators: %s: HTTP status code or error message */ $out['webhook'] = $code >= 200 && $code < 300 ? true : sprintf(__('the webhook answered %s', 'siterelay'), is_wp_error($response) ? $response->get_error_message() : 'HTTP ' . $code); } return $out; } }