*/ public function get_countries(): array { $cached = get_transient( self::TRANSIENT_KEY ); if ( is_array( $cached ) && count( $cached ) > 0 ) { /** @var array $cached */ return $cached; } $response = wp_remote_get( self::API_BASE . '/AvailableCountries', [ 'timeout' => 15, ] ); if ( is_wp_error( $response ) ) { throw new \RuntimeException( esc_html( $response->get_error_message() ) ); } $code = wp_remote_retrieve_response_code( $response ); if ( 200 !== $code ) { throw new \RuntimeException( // translators: %d is the HTTP status code. sprintf( esc_html__( 'Nager.Date API returned HTTP %d', 'agenticdaisy-calendar' ), (int) $code ) ); } $body = wp_remote_retrieve_body( $response ); /** @var list>|null $data */ $data = json_decode( $body, true ); if ( ! is_array( $data ) ) { throw new \RuntimeException( esc_html__( 'Invalid response from Nager.Date API', 'agenticdaisy-calendar' ) ); } $countries = []; foreach ( $data as $entry ) { if ( isset( $entry['countryCode'], $entry['name'] ) && is_string( $entry['countryCode'] ) && is_string( $entry['name'] ) ) { $countries[ $entry['countryCode'] ] = $entry['name']; } } asort( $countries ); set_transient( self::TRANSIENT_KEY, $countries, self::TRANSIENT_TTL ); return $countries; } public function fetch_holidays( string $country_code, int $year ): string { $url = sprintf( '%s/PublicHolidays/%d/%s', self::API_BASE, $year, rawurlencode( $country_code ) ); $response = wp_remote_get( $url, [ 'timeout' => 15, ] ); if ( is_wp_error( $response ) ) { throw new \RuntimeException( esc_html( $response->get_error_message() ) ); } $code = wp_remote_retrieve_response_code( $response ); if ( 200 !== $code ) { throw new \RuntimeException( // translators: 1: HTTP status code, 2: country code, 3: year. sprintf( esc_html__( 'Nager.Date API returned HTTP %1$d for %2$s/%3$d', 'agenticdaisy-calendar' ), (int) $code, esc_html( $country_code ), (int) $year ) ); } $body = wp_remote_retrieve_body( $response ); /** @var list>|null $holidays */ $holidays = json_decode( $body, true ); if ( ! is_array( $holidays ) ) { throw new \RuntimeException( esc_html__( 'Invalid holiday data from Nager.Date API', 'agenticdaisy-calendar' ) ); } return $this->to_ics( $holidays, $country_code, $year ); } /** * Convert Nager.Date JSON holiday array to ICS string. * * @param list> $holidays Holiday data. * @param string $country Country code. * @param int $year Year. * @return string ICS content. */ private function to_ics( array $holidays, string $country, int $year ): string { $lines = [ 'BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//WebCalendar//Nager.Date//EN', ]; foreach ( $holidays as $holiday ) { if ( ! isset( $holiday['date'], $holiday['name'] ) || ! is_string( $holiday['date'] ) || ! is_string( $holiday['name'] ) ) { continue; } $local_name = isset( $holiday['localName'] ) && is_string( $holiday['localName'] ) ? $holiday['localName'] : $holiday['name']; $date = str_replace( '-', '', $holiday['date'] ); $name = $local_name; $uid = sprintf( '%d-%s-%s@webcalendar', $year, $country, md5( $holiday['name'] ) ); $end_dt = new \DateTimeImmutable( $holiday['date'] ); $end = $end_dt->modify( '+1 day' )->format( 'Ymd' ); $lines[] = 'BEGIN:VEVENT'; $lines[] = 'DTSTART;VALUE=DATE:' . $date; $lines[] = 'DTEND;VALUE=DATE:' . $end; $lines[] = 'SUMMARY:' . $this->escape_ics_text( $name ); $lines[] = 'UID:' . $uid; $lines[] = 'END:VEVENT'; } $lines[] = 'END:VCALENDAR'; return implode( "\r\n", $lines ); } /** * Escape text for ICS property values. * * @param string $text Raw text. * @return string Escaped text. */ private function escape_ics_text( string $text ): string { $text = str_replace( '\\', '\\\\', $text ); $text = str_replace( ',', '\\,', $text ); $text = str_replace( ';', '\\;', $text ); $text = str_replace( "\n", '\\n', $text ); return $text; } }