sites = $sites; $this->runs = $runs; $this->deliveries = $deliveries; $this->dispatcher = $dispatcher; } /** * {@inheritDoc} * * @return array */ protected function actions() { return array( 'approve_run' => 'approve', 'resend_run' => 'resend', 'rebuild_run' => 'rebuild', 'cancel_run' => 'cancel', 'save_review' => 'save_review', 'preview_email' => 'preview', 'download_pdf' => 'download', 'send_now' => 'send_now', 'view_pdf' => 'view_pdf', 'delete_runs' => 'delete_runs', ); } /** * The dispatcher. * * @return Dispatcher */ private function dispatcher() { return call_user_func( $this->dispatcher ); } /** * The posted run, or back to the list. * * @return array{0:array,1:\TurtleReport\Profiles\Profile} */ private function posted_run() { $run = $this->runs->find( absint( $this->text( 'run' ) ) ); $site = $run ? $this->sites->find( $run['site_id'] ) : null; if ( ! $run || ! $site ) { $this->finish( DeliveriesPage::url(), 'error', __( 'That report no longer exists.', 'turtlereport' ) ); } return array( $run, $site ); } /** * URL of a run's screen. * * @param array $run Run. * @return string */ private static function back( array $run ) { return DeliveriesPage::url( array( 'run' => (int) $run['id'] ) ); } /** * Notice after sending, with accepted and total recipient counts. * * @param array|null $run Run afterwards. * @return void */ private function finish_sent( $run ) { if ( ! $run ) { $this->finish( DeliveriesPage::url(), 'warning', __( 'Another process is sending this report right now. Reload in a minute.', 'turtlereport' ) ); } $tally = DeliveryRepository::tally( $this->deliveries->for_run( $run['id'] ) ); $total = array_sum( $tally ); $ok = $tally[ DeliveryRepository::ACCEPTED ]; if ( ! $total ) { $message = ! empty( $run['meta']['error'] ) ? (string) $run['meta']['error'] : __( 'Nothing was sent.', 'turtlereport' ); $this->finish( self::back( $run ), 'error', $message ); } $this->finish( self::back( $run ), $ok === $total ? 'success' : ( $ok ? 'warning' : 'error' ), /* translators: 1: accepted count, 2: total recipients. */ sprintf( __( '%1$d of %2$d emails accepted by the mail system.', 'turtlereport' ), $ok, $total ) ); } /** * Approves a report waiting for review (or held) and sends it. * * @return void */ public function approve() { $this->guard( 'approve_run' ); list( $run ) = $this->posted_run(); $this->finish_sent( $this->dispatcher()->approve( $run['id'], get_current_user_id() ) ); } /** * Resends a report to its failed recipients (or to one of them). * * @return void */ public function resend() { $this->guard( 'resend_run' ); list( $run ) = $this->posted_run(); $one = absint( $this->text( 'delivery' ) ); $this->finish_sent( $this->dispatcher()->resend_failed( $run['id'], $one ? $one : null ) ); } /** * Fetches a held or reviewed report's numbers again. * * @return void */ public function rebuild() { $this->guard( 'rebuild_run' ); list( $run ) = $this->posted_run(); $after = $this->dispatcher()->rebuild( $run['id'] ); $this->finish( self::back( $run ), 'success', $after && 'ready' === $after['readiness'] ? __( 'Prepared again: every part loaded.', 'turtlereport' ) : __( 'Prepared again. See the data checks below.', 'turtlereport' ) ); } /** * Skips a report. * * @return void */ public function cancel() { $this->guard( 'cancel_run' ); list( $run ) = $this->posted_run(); $ok = $this->dispatcher()->cancel( $run['id'] ); $this->finish( self::back( $run ), $ok ? 'success' : 'error', $ok ? __( 'Report skipped.', 'turtlereport' ) : __( 'This report has already gone out, so it can\'t be skipped.', 'turtlereport' ) ); } /** * Saves edited highlights and the period's notes. * * @return void */ public function save_review() { $this->guard( 'save_review' ); list( $run ) = $this->posted_run(); if ( $this->deliveries->for_run( $run['id'] ) ) { $this->finish( self::back( $run ), 'error', __( 'This report has already gone out, so its highlights can\'t change.', 'turtlereport' ) ); } // phpcs:disable WordPress.Security.NonceVerification.Missing -- Checked in guard(). $posted = isset( $_POST['insights'] ) && is_array( $_POST['insights'] ) ? wp_unslash( $_POST['insights'] ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitised in Insights::from_form(). $notes = isset( $_POST['notes'] ) && is_string( $_POST['notes'] ) ? sanitize_textarea_field( wp_unslash( $_POST['notes'] ) ) : ''; // phpcs:enable $meta = (array) $run['meta']; $meta['insights_edited'] = time(); $this->runs->update( $run['id'], array( 'insights' => Insights::from_form( $posted ), 'notes' => Text::substr( $notes, 0, 2000 ), 'meta' => $meta, ) ); $this->finish( self::back( $run ) . '#tly-review', 'success', __( 'Highlights and notes saved.', 'turtlereport' ) ); } /** * Shows the report email as recipients will get it. * * @return void */ public function preview() { $this->guard( 'preview_email' ); list( $run, $site ) = $this->posted_run(); if ( ! is_array( $run['snapshot'] ) ) { $this->finish( self::back( $run ), 'error', __( 'This report\'s numbers are no longer stored, so it can\'t be previewed.', 'turtlereport' ) ); } // The preview shows the email; building the PDF isn't needed for that. $site->delivery['attach_pdf'] = false; $message = $this->dispatcher()->mailer()->compose( $site, $run, RunRepository::KIND_TEST === $run['kind'] ); nocache_headers(); header( 'Content-Type: text/html; charset=UTF-8' ); header( 'X-Robots-Tag: noindex', true ); echo $message['html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Template output, escaped inside templates/email/report.php. exit; } /** * Sends a site's latest report to its recipients now, outside the schedule, * and returns to the referring screen with the result. * * @return void */ public function send_now() { $this->guard( 'send_now' ); $site = $this->sites->find( sanitize_key( $this->text( 'site' ) ) ); $back = wp_get_referer(); $back = $back ? $back : DeliveriesPage::url(); if ( ! $site ) { $this->finish( $back, 'error', __( 'That site no longer exists.', 'turtlereport' ) ); } $run = $this->dispatcher()->send_now( $site ); if ( is_wp_error( $run ) ) { $this->finish( $back, 'error', $run->get_error_message() ); } $label = ReportBuilder::label( RunRepository::period( $run ), $site->email['frequency'] ); $tally = DeliveryRepository::tally( $this->deliveries->for_run( $run['id'] ) ); $total = array_sum( $tally ); $ok = $tally[ DeliveryRepository::ACCEPTED ]; if ( RunRepository::HELD === $run['status'] ) { /* translators: 1: site name, 2: report period, e.g. "September 2026". */ $this->finish( $back, 'warning', sprintf( __( '%1$s: the %2$s report was held because some data couldn\'t be loaded. Open it under Email deliveries to see why, then send it anyway or fix it.', 'turtlereport' ), $site->display_name(), $label ) ); } if ( ! $total ) { $this->finish( $back, 'error', ! empty( $run['meta']['error'] ) ? (string) $run['meta']['error'] : __( 'Nothing was sent.', 'turtlereport' ) ); } $this->finish( $back, $ok === $total ? 'success' : ( $ok ? 'warning' : 'error' ), /* translators: 1: site name, 2: report period, 3: accepted count, 4: total recipients. */ sprintf( __( '%1$s: the %2$s report was handed to the mail system. %3$d of %4$d emails accepted. Details are under Email deliveries.', 'turtlereport' ), $site->display_name(), $label, $ok, $total ) ); } /** * Deletes reports from the history: one (the row's or the report screen's * Delete button) or every ticked row of the list. * * @return void */ public function delete_runs() { $this->guard( 'delete_runs' ); $one = absint( $this->text( 'one' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Checked in guard(). $ticked = isset( $_POST['runs'] ) && is_array( $_POST['runs'] ) ? array_map( 'absint', wp_unslash( $_POST['runs'] ) ) : array(); $ids = $one ? array( $one ) : $ticked; // Back to the list the user came from, but never to the screen of a report that's now gone. $back = wp_get_referer(); $back = $back && false === strpos( $back, 'run=' ) ? $back : DeliveriesPage::url(); if ( ! array_filter( $ids ) ) { $this->finish( $back, 'warning', __( 'Tick at least one report to delete.', 'turtlereport' ) ); } $result = $this->runs->delete( $ids ); if ( $result['busy'] && ! $result['deleted'] ) { $this->finish( $back, 'error', __( 'That report is being prepared or sent right now, so it can\'t be deleted yet. Try again in a few minutes.', 'turtlereport' ) ); } if ( ! $result['deleted'] ) { $this->finish( $back, 'warning', __( 'Nothing was deleted: the report no longer exists.', 'turtlereport' ) ); } /* translators: %d: number of reports deleted. */ $message = sprintf( _n( '%d report deleted from the history.', '%d reports deleted from the history.', $result['deleted'], 'turtlereport' ), $result['deleted'] ); if ( $result['busy'] ) { /* translators: %d: number of reports left alone. */ $message .= ' ' . sprintf( _n( '%d was being sent right now and was kept.', '%d were being sent right now and were kept.', $result['busy'], 'turtlereport' ), $result['busy'] ); } $this->finish( $back, $result['busy'] ? 'warning' : 'success', $message ); } /** * Shows a report's PDF in the browser (a link from Deliveries, so GET): * the same two pages the recipients got, rebuilt from the numbers that * were sent. * * @return void */ public function view_pdf() { $this->guard( 'view_pdf' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Checked in guard(). $run = $this->runs->find( isset( $_GET['run'] ) ? absint( $_GET['run'] ) : 0 ); $site = $run ? $this->sites->find( $run['site_id'] ) : null; if ( ! $run || ! $site ) { wp_die( esc_html__( 'That report no longer exists.', 'turtlereport' ), '', array( 'response' => 404 ) ); } $pdf = $this->dispatcher()->mailer()->builder()->pdf( $site, $run ); if ( is_wp_error( $pdf ) ) { wp_die( esc_html( $pdf->get_error_message() ), esc_html__( 'Report PDF', 'turtlereport' ), array( 'response' => 410 ) ); } nocache_headers(); header( 'Content-Type: application/pdf' ); header( 'Content-Disposition: inline; filename="' . ReportBuilder::pdf_name( $site, $run ) . '"' ); header( 'Content-Length: ' . strlen( $pdf ) ); header( 'X-Content-Type-Options: nosniff' ); echo $pdf; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Binary PDF. exit; } /** * Downloads a report's PDF. * * @return void */ public function download() { $this->guard( 'download_pdf' ); list( $run, $site ) = $this->posted_run(); $pdf = $this->dispatcher()->mailer()->builder()->pdf( $site, $run ); if ( is_wp_error( $pdf ) ) { $this->finish( self::back( $run ), 'error', $pdf->get_error_message() ); } nocache_headers(); header( 'Content-Type: application/pdf' ); header( 'Content-Disposition: attachment; filename="' . ReportBuilder::pdf_name( $site, $run ) . '"' ); header( 'Content-Length: ' . strlen( $pdf ) ); echo $pdf; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Binary PDF. exit; } }