GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Pull Request — develop (#42)
by Chris
01:49
created

namespace.php ➔ get_bgg_game()   B

Complexity

Conditions 6
Paths 2

Size

Total Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
cc 6
nc 2
nop 1
dl 0
loc 31
ccs 0
cts 18
cp 0
crap 42
rs 8.8017
c 0
b 0
f 0
1
<?php
2
/**
3
 * Games Collector BoardGameGeek API integration.
4
 *
5
 * Integrates BoardGameGeek's XML API into Games Collector to allow game data to be imported.
6
 *
7
 * @package GC\GamesCollector\BGG
8
 * @since   1.2.0
9
 */
10
11
namespace GC\GamesCollector\BGG;
12
13
/**
14
 * Return the BGG v1 API endpoint.
15
 *
16
 * @since  1.2.0
17
 * @return string The BGG v1 endpoint.
18
 */
19
function bgg_api() {
20 1
	return esc_url( 'https://www.boardgamegeek.com/xmlapi/' );
21
}
22
23
/**
24
 * Return the BGG v2 (beta) API endpoint.
25
 *
26
 * @since  1.2.0
27
 * @return string The BGG v2 endpoint.
28
 */
29
function bgg_api2() {
30 1
	return esc_url( 'https://www.boardgamegeek.com/xmlapi2/' );
31
}
32
33
/**
34
 * Return the BGG search endpoint for a particular query.
35
 *
36
 * @since  1.2.0
37
 * @param  string $query The search query.
38
 * @param  string $type  The type of search (optional). Allowed values are rpgitem, videogame, boardgame, boardgameaccessory or boardgameexpansion.
39
 * @return string        The BGG search API URL.
40
 */
41
function bgg_search( string $query, $type = 'boardgame' ) {
42 1
	$query = str_replace( ' ', '+', $query );
43 1
	$type  = in_array( $type, [ 'rpgitem', 'videogame', 'boardgame', 'boardgameaccessory', 'boardgameexpansion' ] ) ? $type : 'boardgame';
44
45 1
	return esc_url( sprintf(
46 1
		'%1$ssearch?search=%2$s&type=%3$s',
47 1
		bgg_api(),
48 1
		esc_html( $query ),
49 1
		esc_html( $type )
50
	) );
51
}
52
53
/**
54
 * Return the BGG API endpoint for a single game/entity.
55
 *
56
 * @since  1.2.0
57
 * @param  int $id The BGG entity ID.
58
 * @return string  The BGG URL.
59
 */
60
function bgg_game( int $id ) {
61 1
	return esc_url( bgg_api2() . 'thing?id=' . $id );
62
}
63
64
/**
65
 * Return the search results for a given query.
66
 *
67
 * @since  1.2.0
68
 * @param  string $query A search query for a game.
69
 * @return array         An array of possible matches.
70
 */
71
function get_bgg_search_results( $query ) {
72
	$response = wp_remote_get( bgg_search( $query ) );
73
	$results  = [];
74
75
	if ( isset( $response['response'] ) && 200 === $response['response']['code'] ) {
76
		$xml  = simplexml_load_string( wp_remote_retrieve_body( $response ) );
77
78
		if ( isset( $xml->boardgame ) ) {
79
			foreach ( $xml->boardgame as $game ) {
80
				$game = (array) $game;
81
82
				$results[] = [
83
					'id' => (int) $game['@attributes']['objectid'],
84
					'name' => $game['name'],
85
					'year' => $game['yearpublished'],
86
				];
87
			}
88
		}
89
	}
90
91
	return $results;
92
}
93
94
/**
95
 * Return the BGG data that maps to data used in Games Collector for a game.
96
 *
97
 * @since  1.2.0
98
 * @param  int $id The BGG game id.
99
 * @return array   An array of game information pulled from the entry on Board Game Geek.
100
 */
101
function get_bgg_game( $id ) {
102
	$response = wp_remote_get( bgg_game( $id ) );
103
	$data     = [];
104
105
	if ( isset( $response['response'] ) && 200 === $response['response']['code'] ) {
106
		$xml = simplexml_load_string( wp_remote_retrieve_body( $response ) );
107
		$game = $xml->item;
0 ignored issues
show
Bug introduced by
The property item does not seem to exist in SimpleXMLElement.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
108
		$data = [
109
			'title'       => (string) $game->name->attributes()['value'],
110
			'image'       => (string) $game->image,
111
			'minplayers'  => (int) $game->minplayers->attributes()['value'],
112
			'maxplayers'  => (int) $game->maxplayers->attributes()['value'],
113
			'minplaytime' => (int) $game->minplaytime->attributes()['value'],
114
			'maxplaytime' => (int) $game->maxplaytime->attributes()['value'],
115
			'minage'      => (int) $game->minage->attributes()['value'],
116
			'categories'  => [],
117
		];
118
119
		$categories = [];
120
121
		foreach ( $game->link as $metadata ) {
122
			if ( 'boardgamecategory' === (string) $metadata->attributes()['type'] ) {
123
				$categories[] = (string) $metadata->attributes()['value'];
124
			}
125
		}
126
127
		$data['categories'] = ! empty( $categories ) ? $categories : [];
128
	}
129
130
	return $data;
131
}
132
133
/**
134
 * CMB2 field for BGG Search.
135
 *
136
 * @since 1.2.0
137
 */
138
function fields() {
139
	$search_results = get_transient( 'gc_last_bgg_search' );
140
141
	// First run.
142
	if ( ! $search_results ) {
143
		$cmb = new_cmb2_box( array(
144
			'id'           => 'bgg-search',
145
			'title'        => __( 'Add game from Board Game Geek', 'games-collector' ),
146
			'object_types' => [ 'options-page' ],
147
			'option_key'   => 'add_from_bgg',
148
			'parent_slug'  => 'edit.php?post_type=gc_game',
149
			'menu_title'   => __( 'Add New From BGG', 'games-collector' ),
150
			'save_button'  => __( 'Search for Game', 'games-collector' ),
151
		) );
152
153
		$cmb->add_field( array(
154
			'name'       => __( 'Search', 'games-collector' ),
155
			'id'         => 'bgg_searchform',
156
			'type'       => 'bgg_search',
157
			'desc'       => __( 'Type in the title of a game to search for that game on Board Game Geek.', 'games-collector' ),
158
		) );
159
	} else {
160
		// Choose the right game.
161
		$cmb = new_cmb2_box( array(
162
			'id'           => 'bgg-search-2',
163
			'title'        => __( 'Add game from Board Game Geek &mdash; Step 2', 'games-collector' ),
164
			'object_types' => [ 'options-page' ],
165
			'option_key'   => 'add_from_bgg',
166
			'parent_slug'  => 'edit.php?post_type=gc_game',
167
			'menu_title'   => __( 'Add New From BGG', 'games-collector' ),
168
			'save_button'  => __( 'Add Game', 'games-collector' ),
169
		) );
170
171
		$cmb->add_field( array(
172
			'name'       => __( 'Search Results', 'games-collector' ),
173
			'id'         => 'bgg_search_results',
174
			'type'       => 'radio',
175
			'desc'       => __( 'Select the game that matches your search.', 'games-collector' ),
176
			'options'    => bgg_search_results_options( $search_results ),
177
		) );
178
179
		$cmb->add_field( array(
180
			'id'         => 'bgg_search_results_hidden',
181
			'type'       => 'hidden',
182
			'attributes' => [
183
				'name' => 'action',
184
				'value' => 'bgg_insert_game',
185
			],
186
		) );
187
	}
188
}
189
190
/**
191
 * Render the BGG search field in CMB2.
192
 *
193
 * This callback extends the built in "text" field type.
194
 *
195
 * @since  1.2.0
196
 * @param  string $field             Not used.
197
 * @param  string $escaped_value     Not used.
198
 * @param  int    $object_id         Not used.
199
 * @param  string $object_type       Not used.
200
 * @param  object $field_type_object The CMB2 field type object.
201
 */
202
function render_cmb2_bgg_search( $field, $escaped_value, $object_id, $object_type, $field_type_object ) {
0 ignored issues
show
Unused Code introduced by
The parameter $field is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $escaped_value is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $object_id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $object_type is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
203
	$description = '<p class="description">' . esc_html( $field_type_object->field->args()['desc'] ) . '</p>';
204
	$form = sprintf( '<input id="%1$s" class="regular-text" name="%2$s" value="" placeholder="%3$s" type="text">',
205
		esc_attr( $field_type_object->field->args()['id'] ),
206
		esc_attr( $field_type_object->field->args()['id'] ),
207
		__( 'A game title or search, e.g. &ldquo;betrayal house hill&rdquo;', 'usat' )
208
	);
209
	$hidden = '<input type="hidden" name="action" value="bgg_search_response">';
210
	$output = $hidden . $form . $description;
211
212
	echo wp_kses( $output, [
213
		'p'     => [
214
			'class'       => [],
215
		],
216
		'input' => [
217
			'id'          => [],
218
			'class'       => [],
219
			'name'        => [],
220
			'value'       => [],
221
			'type'        => [],
222
			'placeholder' => [],
223
		],
224
	] );
225
}
226
227
/**
228
 * Store the Board Game Geek search results in a transient so we can access it later.
229
 *
230
 * @since  1.2.0
231
 * @return void|wp_die
232
 */
233
function search_response() {
234
	if ( isset( $_POST['nonce_CMB2phpbgg-search'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce_CMB2phpbgg-search'] ) ), 'nonce_CMB2phpbgg-search' ) ) {
235
236
		$search_query = isset( $_POST['bgg_searchform'] ) ? sanitize_text_field( wp_unslash( $_POST['bgg_searchform'] ) ) : '';
237
		$results      = get_bgg_search_results( $search_query );
238
		set_transient( 'gc_last_bgg_search', $results, DAY_IN_SECONDS );
239
		wp_safe_redirect( admin_url( 'edit.php?post_type=gc_game&page=add_from_bgg&step=2' ) );
240
		return;
241
	}
242
243
	return wp_die( esc_html__( 'Security check failed. What were you doing?', 'games-collector' ), esc_html__( 'Nonce check failed', 'games-collector' ) );
244
}
245
246
/**
247
 * Dislplay the BGG search results in an option array for CMB2.
248
 *
249
 * @since  1.2.0
250
 * @param  array $results The array of BGG search results.
251
 * @return array          An array of options for CMB2.
252
 */
253
function bgg_search_results_options( $results ) {
254
	$options = [];
255
	foreach ( $results as $game ) {
256
		$options[ absint( $game['id'] ) ] = sprintf( '%1$s [%2$s] (%3$s)',
257
			'<strong>' . esc_html( $game['name'] ) . '</strong>',
258
			esc_html( $game['year'] ),
259
			esc_html( $game['id'] )
260
		);
261
	}
262
263
	return $options;
264
}
265
266
/**
267
 * Insert the game using BGG data from the API.
268
 *
269
 * @since  1.2.0
270
 * @return void|wp_die
271
 */
272
function insert_game() {
273
	if ( isset( $_POST['nonce_CMB2phpbgg-search-2'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce_CMB2phpbgg-search-2'] ) ), 'nonce_CMB2phpbgg-search-2' ) ) {
274
275
		$game_id      = isset( $_POST['bgg_search_results'] ) ? absint( wp_unslash( $_POST['bgg_search_results'] ) ) : false;
276
		$redirect_url = admin_url( 'edit.php?post_type=gc_game&page=add_from_bgg' );
277
278
		if ( $game_id ) {
279
			$game = get_bgg_game( $game_id );
280
281
			// Check if game already exists.
282
			if ( get_page_by_title( $game['title'], OBJECT, 'gc_game' ) ) {
283
				return wp_die(
284
					esc_html__( 'A game with that title already exists. Please try again.', 'games-collector' ),
285
					esc_html__( 'Duplicate game found', 'games-collector' ),
286
					[ 'back_link' => true ]
287
				);
288
			}
289
290
			$post_id = wp_insert_post( [
291
				'post_type'   => 'gc_game',
292
				'post_title'  => esc_html( $game['title'] ),
293
				'post_status' => 'draft',
294
			] );
295
296
			if ( ! is_wp_error( $post_id ) ) {
297
				$redirect_url = admin_url( sprintf( 'post.php?post=%d&action=edit', $post_id ) );
298
299
				// Add game meta.
300
				add_post_meta( $post_id, '_gc_min_players', absint( $game['minplayers'] ) );
301
				add_post_meta( $post_id, '_gc_max_players', absint( $game['maxplayers'] ) );
302
				add_post_meta( $post_id, '_gc_age', absint( $game['minage'] ) );
303
				add_post_meta( $post_id, '_gc_link', sprintf( 'https://www.boardgamegeek.com/boardgame/%d/', $game_id ) );
304
				add_post_meta( $post_id, '_gc_bgg_id', $game_id );
305
306
				if ( absint( $game['minplaytime'] ) === absint( $game['maxplaytime'] ) ) {
307
					add_post_meta( $post_id, '_gc_time', esc_html( $game['minplaytime'] ) );
308
				} else {
309
					add_post_meta( $post_id, '_gc_time', esc_html( $game['minplaytime'] . '-' . $game['maxplaytime'] ) );
310
				}
311
312
				if ( isset( $game['categories'] ) ) {
313
					foreach ( $game['categories'] as $game_attribute ) {
314
						$similar_attribute = get_attribute_like( $game_attribute );
315
316
						// If there's an existing attribute that matches the BGG category, use that.
317
						if ( $similar_attribute ) {
318
							wp_set_post_terms( $post_id, [ $similar_attribute ], 'gc_attribute', true );
319
						}
320
321
						// Otherwise insert a new term.
322
						wp_set_post_terms( $post_id, $game_attribute, 'gc_attribute', true );
323
					}
324
				}
325
326
				// Sideload the image from BGG.
327
				attach_bgg_image( $post_id, $game );
328
			}
329
		}
330
331
		// Delete the transient so we can do this again.
332
		delete_transient( 'gc_last_bgg_search' );
333
334
		// Redirect to the edit page for this game.
335
		wp_safe_redirect( esc_url_raw( $redirect_url ) );
336
		exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The function insert_game() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
337
	}
338
339
	return wp_die( esc_html__( 'Security check failed. What were you doing?', 'games-collector' ), esc_html__( 'Nonce check failed', 'games-collector' ) );
340
}
341
342
/**
343
 * Check if an existing game attribute term exists and return the ID if it does.
344
 *
345
 * @since  1.2.0
346
 * @param  string $search The game attribute name.
347
 * @return int|bool       The term ID if a matching term exists, false if it doesn't.
348
 */
349
function get_attribute_like( $search ) {
350
	// Check if a previously cached attribute for this term exists already.
351
	$cached_term_search = get_transient( 'gc_frequently_used_attributes' );
352
	if ( $cached_term_search && array_key_exists( $search, $cached_term_search ) ) {
353
		return $cached_term_search[ $search ];
354
	}
355
356
	$terms = get_terms( [
357
		'taxonomy'   => 'gc_attribute',
358
		'hide_empty' => true,
359
		'fields'     => 'ids',
360
		'name__like' => esc_html( $search ),
361
	] );
362
363
	if ( ! is_wp_error( $terms ) && count( $terms ) > 0 ) {
364
		// Cache this term combination so we can access it faster later.
365
		if ( ! $cached_term_search ) {
366
			set_transient( 'gc_frequently_used_attributes', [
367
				$search => $terms[0],
368
			], 999 * YEAR_IN_SECONDS );
369
		} else {
370
			$cached_term_search = array_merge( $cached_term_search, [ $search => $terms[0] ] );
371
			set_transient( 'gc_frequently_used_attributes', $cached_term_search, 999 * YEAR_IN_SECONDS );
372
		}
373
374
		return $terms[0];
375
	}
376
377
	return false;
378
}
379
380
/**
381
 * Sideload image for a BGG image.
382
 *
383
 * @since  1.2.0
384
 * @param  int   $post_id The game ID.
385
 * @param  array $game    The array of game data from BGG.
386
 */
387
function attach_bgg_image( $post_id, $game ) {
388
	$image_id = media_sideload_image( esc_url_raw( $game['image'] ), $post_id, esc_html( $game['title'] ), 'id' );
389
	set_post_thumbnail( $post_id, $image_id );
390
}
391