notices = $notices; $this->customers = $customers; $this->mailer = $mailer; $this->vars = $vars; $this->log = $log; } /** * Creates a notice. * * @since 0.1.0 * * @param array $raw Unslashed input. * @return array|WP_Error */ public function create( array $raw ) { $clean = $this->clean( $raw, null ); if ( $clean['errors'] ) { return Errors::invalid( $clean['errors'] ); } $data = $clean['data']; $data['created_by'] = get_current_user_id(); $id = $this->notices->insert( $data ); if ( 0 === $id ) { return Errors::failed( __( 'The notice could not be saved.', 'hostkhata' ) ); } $notice = $this->notices->find( $id ); $this->log->record( 'notice.created', 0, 'notice', $id, __( 'Notice created.', 'hostkhata' ) ); if ( 'published' === $notice['status'] ) { $this->announce( $notice ); } return $notice; } /** * Updates a notice. * * @since 0.1.0 * * @param int $id Notice id. * @param array $raw Unslashed input. * @return array|WP_Error */ public function update( int $id, array $raw ) { $notice = $this->notices->find( $id ); if ( null === $notice ) { return Errors::not_found( __( 'Notice not found.', 'hostkhata' ) ); } $clean = $this->clean( array_merge( $notice, $raw ), $notice ); if ( $clean['errors'] ) { return Errors::invalid( $clean['errors'] ); } $this->notices->update( $id, $clean['data'] ); $updated = $this->notices->find( $id ); if ( 'published' === $updated['status'] && 'published' !== $notice['status'] ) { $this->announce( $updated ); } return $updated; } /** * Deletes a notice that was never published. * * @since 0.1.0 * * @param int $id Notice id. * @return true|WP_Error */ public function delete( int $id ) { $notice = $this->notices->find( $id ); if ( null === $notice ) { return Errors::not_found( __( 'Notice not found.', 'hostkhata' ) ); } if ( 'published' === $notice['status'] ) { return Errors::state( 'hostkhata_notice_published', __( 'A published notice cannot be deleted. Archive it first.', 'hostkhata' ) ); } $this->notices->delete( $id ); return true; } /** * Cleans and validates a notice. * * @param array $raw Input (merged over the stored notice when updating). * @param array|null $existing Stored notice, or null when creating. * @return array{data: array, errors: array} */ private function clean( array $raw, ?array $existing ): array { $errors = array(); $title = Sanitizer::text( $raw['title'] ?? '' ); $body = Sanitizer::textarea( $raw['body'] ?? '' ); if ( '' === $title ) { $errors['title'] = __( 'Give the notice a title.', 'hostkhata' ); } elseif ( mb_strlen( $title ) > 255 ) { $errors['title'] = __( 'Use at most 255 characters.', 'hostkhata' ); } if ( '' === trim( $body ) ) { $errors['body'] = __( 'Write the message.', 'hostkhata' ); } elseif ( mb_strlen( $body ) > 10000 ) { $errors['body'] = __( 'Use at most 10000 characters.', 'hostkhata' ); } $type = Sanitizer::pick( $raw, 'notice_type', self::TYPES, 'info' ); $audience = Sanitizer::pick( $raw, 'audience', array( 'all', 'selected' ), 'all' ); $status = Sanitizer::pick( $raw, 'status', array( 'draft', 'published', 'archived' ), 'draft' ); foreach ( array( 'notice_type' => $type, 'audience' => $audience, 'status' => $status, ) as $field => $value ) { if ( null === $value ) { $errors[ $field ] = __( 'Choose one of the listed options.', 'hostkhata' ); } } $ids = array(); if ( 'selected' === $audience ) { $ids = array_values( array_unique( array_filter( array_map( 'absint', (array) ( $raw['audience_ids'] ?? array() ) ) ) ) ); if ( ! $ids ) { $errors['audience_ids'] = __( 'Choose at least one customer.', 'hostkhata' ); } } $starts = $this->boundary( $raw['starts_on'] ?? $raw['starts_at_gmt'] ?? '', false, 'starts_at_gmt', $errors ); $ends = $this->boundary( $raw['ends_on'] ?? $raw['ends_at_gmt'] ?? '', true, 'ends_at_gmt', $errors ); if ( '' !== $starts && '' !== $ends && $ends < $starts ) { $errors['ends_at_gmt'] = __( 'The end date cannot be before the start date.', 'hostkhata' ); } $order = array( 'draft' => 0, 'published' => 1, 'archived' => 2, ); if ( $existing && is_string( $status ) && isset( $order[ $status ], $order[ $existing['status'] ] ) && $order[ $status ] < $order[ $existing['status'] ] ) { $errors['status'] = __( 'A notice cannot go back to an earlier status.', 'hostkhata' ); } return array( 'data' => array( 'title' => $title, 'body' => $body, 'notice_type' => (string) $type, 'audience' => (string) $audience, 'audience_ids' => $ids, 'starts_at_gmt' => $starts, 'ends_at_gmt' => $ends, 'send_email' => Sanitizer::boolean( $raw['send_email'] ?? false ) ? 1 : 0, 'status' => (string) $status, ), 'errors' => $errors, ); } /** * Reads a start or end of the visible period. Accepts `YYYY-MM-DD` or a stored datetime. * * @param mixed $value Raw value. * @param bool $is_end Whether this is the end of the period (end of day). * @param string $field Field name for errors. * @param array $errors Errors, extended by this method. * @return string Datetime, or an empty string for "no limit". */ private function boundary( $value, bool $is_end, string $field, array &$errors ): string { $value = is_scalar( $value ) ? trim( (string) $value ) : ''; if ( '' === $value ) { return ''; } $day = substr( $value, 0, 10 ); if ( ! Date::is_valid_date( $day ) ) { $errors[ $field ] = __( 'Enter a valid date.', 'hostkhata' ); return ''; } return $day . ( $is_end ? ' 23:59:59' : ' 00:00:00' ); } /** * Emails a notice that was just published, when it asks for that. * * @param array $notice Notice. * @return void */ private function announce( array $notice ): void { $this->log->record( 'notice.published', 0, 'notice', $notice['id'], __( 'Notice published.', 'hostkhata' ) ); if ( ! $notice['send_email'] ) { return; } $sent = 0; $page = 1; $full_page = false; do { $batch = $this->customers->query( array( 'status' => CustomerStatus::ACTIVE, 'per_page' => 100, 'page' => $page, ) ); foreach ( $batch['items'] as $customer ) { if ( 'selected' === $notice['audience'] && ! in_array( (int) $customer['id'], $notice['audience_ids'], true ) ) { continue; } if ( empty( $customer['user_email'] ) || $sent >= self::MAX_RECIPIENTS ) { continue; } $vars = array_merge( $this->vars->base( $customer ), array( 'notice_title' => $notice['title'], 'notice_body' => $notice['body'], ) ); if ( $this->mailer->queue( 'notice', (string) $customer['user_email'], trim( $customer['first_name'] . ' ' . $customer['last_name'] ), $vars, 'notice', $notice['id'] ) > 0 ) { ++$sent; } } $full_page = 100 === count( $batch['items'] ); ++$page; } while ( $full_page && $sent < self::MAX_RECIPIENTS ); } }