diff --git a/src/Block.php b/src/Block.php index c08aad0..7ab6649 100644 --- a/src/Block.php +++ b/src/Block.php @@ -11,37 +11,109 @@ class Block { public array $attributes; public function __construct( array $attributes ) { - $this->attributes = $attributes; + $this->attributes = self::sanitize_attributes( $attributes ); } - protected function fetchData( string $url, string $keySuffix = '' ) { - $key = "blocks_for_github_$keySuffix"; + /** + * Sanitize block attributes before render and GitHub API requests. + * + * @param array $attributes Raw block attributes. + * @return array + */ + public static function sanitize_attributes( array $attributes ): array { + $block_type = $attributes['blockType'] ?? 'repository'; + $attributes['blockType'] = in_array( $block_type, [ 'repository', 'profile' ], true ) + ? $block_type + : 'repository'; + + $attributes['profileName'] = self::sanitize_github_username( + (string) ( $attributes['profileName'] ?? 'Octocat' ) + ); + $attributes['repoUrl'] = self::sanitize_repo_path( + (string) ( $attributes['repoUrl'] ?? 'DevinWalker/blocks-for-github' ) + ); + $attributes['customTitle'] = sanitize_text_field( (string) ( $attributes['customTitle'] ?? '' ) ); + $attributes['mediaUrl'] = esc_url_raw( (string) ( $attributes['mediaUrl'] ?? '' ) ); + $attributes['mediaId'] = absint( $attributes['mediaId'] ?? 0 ); + + foreach ( + [ + 'showTags', + 'showForks', + 'showSubscribers', + 'showOpenIssues', + 'showLastUpdate', + 'showBio', + 'showLocation', + 'showOrg', + 'showWebsite', + 'showTwitter', + 'preview', + ] as $bool_key + ) { + $attributes[ $bool_key ] = ! empty( $attributes[ $bool_key ] ); + } + + return $attributes; + } + + protected static function sanitize_github_username( string $username ): string { + $username = sanitize_text_field( $username ); + if ( preg_match( '/^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?$/', $username ) ) { + return $username; + } + + return 'Octocat'; + } + + protected static function sanitize_repo_path( string $repo_path ): string { + $repo_path = sanitize_text_field( $repo_path ); + if ( preg_match( '/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/', $repo_path ) ) { + return $repo_path; + } + + return 'DevinWalker/blocks-for-github'; + } + + /** + * @param string $suffix Transient key suffix. + */ + protected function transient_key( string $suffix ): string { + return 'blocks_for_github_' . md5( $suffix ); + } + + /** + * @param mixed $data Fetch result. + */ + protected function is_api_error( $data ): bool { + return is_string( $data ) && str_starts_with( $data, 'API error occurred:' ); + } + + protected function fetchData( string $url, string $key_suffix = '' ) { + $key = $this->transient_key( $key_suffix ); $data = get_transient( $key ); if ( empty( $data ) ) { - - $response = wp_remote_get( $url ); + $response = wp_remote_get( + $url, + [ + 'timeout' => 10, + 'headers' => [ + 'Accept' => 'application/vnd.github+json', + 'User-Agent' => 'WordPress Blocks-for-GitHub/1.0.0', + ], + ] + ); if ( is_wp_error( $response ) ) { - ob_start(); ?> -
-
- 🤷‍ -

-

-
-
get_error_message(); } $http_code = wp_remote_retrieve_response_code( $response ); - // Ensure no API errors. if ( $http_code >= 400 ) { - // Delete the transient if an error occurred. delete_transient( $key ); - // Parse the error message from the response body. $response_body = wp_remote_retrieve_body( $response ); $error_message = __( 'Unknown error occurred', 'blocks-for-github' ); if ( ! empty( $response_body ) ) { @@ -49,27 +121,36 @@ protected function fetchData( string $url, string $keySuffix = '' ) { $error_message = $response_data->message ?? $response_body; } - return "API error occurred: $error_message"; - } else { - // API request went through, so we can save the data. - $body = wp_remote_retrieve_body( $response ); - $data = json_decode( $body ); - set_transient( $key, $data, 24 * HOUR_IN_SECONDS ); + return 'API error occurred: ' . sanitize_text_field( (string) $error_message ); + } + + $body = wp_remote_retrieve_body( $response ); + $data = json_decode( $body ); + if ( ! is_object( $data ) ) { + return 'API error occurred: ' . __( 'Invalid response from GitHub.', 'blocks-for-github' ); } + + set_transient( $key, $data, 24 * HOUR_IN_SECONDS ); } return $data; } protected function fetchProfileRepos() { - $reposUrl = add_query_arg( [ - 'q' => 'user:' . $this->attributes['profileName'], - 'stars' => '>=0', - 'type' => 'Repositories', - 'per_page' => 5, - ], 'https://api.github.com/search/repositories' ); - - return $this->fetchData( $reposUrl, $this->attributes['profileName'] . '_repos' ); + $repos_url = add_query_arg( + [ + 'q' => 'user:' . $this->attributes['profileName'], + 'stars' => '>=0', + 'type' => 'Repositories', + 'per_page' => 5, + ], + 'https://api.github.com/search/repositories' + ); + + return $this->fetchData( + $repos_url, + $this->attributes['profileName'] . '_repos' + ); } /** @@ -77,35 +158,44 @@ protected function fetchProfileRepos() { */ public function render() { if ( $this->attributes['blockType'] === 'repository' ) { - $data = $this->fetchData( 'https://api.github.com/repos/' . $this->attributes['repoUrl'], $this->attributes['repoUrl'] ); + $data = $this->fetchData( + 'https://api.github.com/repos/' . $this->attributes['repoUrl'], + $this->attributes['repoUrl'] + ); - return $this->getOutputOrError( $data, $this->renderRepo( $data ) ); + return $this->get_output_or_error( $data, $this->renderRepo( $data ) ); } if ( $this->attributes['blockType'] === 'profile' ) { - $data = $this->fetchData( "https://api.github.com/users/{$this->attributes['profileName']}", $this->attributes['profileName'] ); + $data = $this->fetchData( + 'https://api.github.com/users/' . $this->attributes['profileName'], + $this->attributes['profileName'] + ); - return $this->getOutputOrError( $data, $this->renderProfile( $data ) ); + return $this->get_output_or_error( $data, $this->renderProfile( $data ) ); } return false; } - protected function getOutputOrError( $data, $output ) { - if ( is_string( $data ) && strpos( $data, 'error' ) ) { - // Output error message if an error occurred during the API request. - ob_start(); ?> + /** + * @param mixed $data API payload or error string. + * @param string $output Rendered block HTML. + */ + protected function get_output_or_error( $data, $output ) { + if ( $this->is_api_error( $data ) ) { + ob_start(); + ?>
👾

- +
- -
+ ob_start(); + ?> +
- description ) ) : ?> + description ) ) : ?>
-

description ); ?>

+

description ); ?>

- topics ) && $this->attributes['showTags'] ): ?> + topics ) && $this->attributes['showTags'] ) : ?>
    - topics as $topic ) : ?> -
  • - + topics as $topic ) : ?> +
  • +
- +
    - updated_at ) && $this->attributes['showLastUpdate'] ): ?> + updated_at ) && $this->attributes['showLastUpdate'] ) : ?>
  • updated_at ); - $formattedDate = $dateTime->format( 'm-d-Y' ); + $date_time = new DateTime( $data->updated_at ); + $formatted_date = $date_time->format( 'm-d-Y' ); echo file_get_contents( BLOCKS_FOR_GITHUB_DIR . '/assets/images/calendar.svg' ); - echo esc_html__( 'Last Update', 'blocks-for-github' ) . ' ' . $formattedDate; ?>
  • - - open_issues ) && $this->attributes['showOpenIssues'] ): ?> + echo esc_html__( 'Last Update', 'blocks-for-github' ) . ' ' . esc_html( $formatted_date ); + ?> + + open_issues ) && $this->attributes['showOpenIssues'] ) : ?>
  • open_issues ) ); ?>
  • - - subscribers_count ) && $this->attributes['showSubscribers'] ): ?> + echo esc_html__( 'Open Issues', 'blocks-for-github' ) . ' ' . esc_html( number_format_i18n( (int) $data->open_issues ) ); + ?> + + subscribers_count ) && $this->attributes['showSubscribers'] ) : ?>
  • subscribers_count ) ); ?>
  • + echo esc_html__( 'Subscribers', 'blocks-for-github' ) . ' ' . esc_html( number_format_i18n( (int) $data->subscribers_count ) ); + ?> - forks ) && $this->attributes['showForks'] ): ?> + forks ) && $this->attributes['showForks'] ) : ?>
  • forks ) ); ?>
  • - + echo esc_html__( 'Forks', 'blocks-for-github' ) . ' ' . esc_html( number_format_i18n( (int) $data->forks ) ); + ?> +
starga } /** - * Render the GitHub Profile output. - * - * @param $data - * + * @param object $data GitHub user payload. * @return false|string */ public function renderProfile( $data ) { - $reposData = $this->fetchProfileRepos(); + $repos_data = $this->fetchProfileRepos(); + + $header_image = ! empty( $this->attributes['mediaUrl'] ) + ? esc_url( $this->attributes['mediaUrl'] ) + : esc_url( BLOCKS_FOR_GITHUB_URL . 'assets/images/code-placeholder.jpg' ); - ob_start(); ?> + ob_start(); + ?> -
-
+
+
- <?php esc_html_e( $data->name ); ?>
@@ -241,94 +321,107 @@ class="bfg-avatar-url" />

attributes['customTitle'] ) ) { - esc_html_e( $this->attributes['customTitle'] ); + echo esc_html( $this->attributes['customTitle'] ); } else { - esc_html_e( $data->name ); - } ?> + echo esc_html( $data->name ); + } + ?>

- + - login ); ?> + login ); ?> - followers ) ); ?> + followers ) ); ?>
bio ) && $this->attributes['showBio'] ) : ?>
-

bio ); ?>

+

bio ); ?>

attributes['showOrg'] ) || ! empty( $this->attributes['showLocation'] ) || ! empty( $this->attributes['showWebsite'] ) || ! empty( $this->attributes['showTwitter'] ) ): ?> + if ( + ! empty( $this->attributes['showOrg'] ) + || ! empty( $this->attributes['showLocation'] ) + || ! empty( $this->attributes['showWebsite'] ) + || ! empty( $this->attributes['showTwitter'] ) + ) : + ?>
- items ) : ?> + items ) ) : ?>
    - items as $repo ) : ?> + items as $repo ) : ?>
  1. - name; ?> + name ); ?>
    archived ) : ?> - + - forks ); ?> - stargazers_count ); ?> + forks ) ); + ?> + stargazers_count ) ); + ?>
    description ) : ?>

    - description; ?> + description ); ?>

  2. @@ -339,8 +432,6 @@ class="bfg-top-repo-pill bfg-top-repo-pill--gold">