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
02:20
created

namespace.php ➔ bgg_search()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 2
dl 0
loc 11
ccs 0
cts 7
cp 0
crap 6
rs 9.9
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
	$query = str_replace( ' ', '+', $query );
43
	$type  = in_array( $type, [ 'rpgitem', 'videogame', 'boardgame', 'boardgameaccessory', 'boardgameexpansion' ] ) ? $type : 'boardgame';
44
45
	return esc_url( sprintf(
46
		'%1$ssearch?search=%2$s&type=%3$s',
47
		bgg_api(),
48
		esc_html( $query ),
49
		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
	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
305
				if ( absint( $game['minplaytime'] ) === absint( $game['maxplaytime'] ) ) {
306
					add_post_meta( $post_id, '_gc_time', esc_html( $game['minplaytime'] ) );
307
				} else {
308
					add_post_meta( $post_id, '_gc_time', esc_html( $game['minplaytime'] . '-' . $game['maxplaytime'] ) );
309
				}
310
311
				if ( isset( $game['categories'] ) ) {
312
					foreach ( $game['categories'] as $game_attribute ) {
313
						$similar_attribute = get_attribute_like( $game_attribute );
314
315
						// If there's an existing attribute that matches the BGG category, use that.
316
						if ( $similar_attribute ) {
317
							wp_set_post_terms( $post_id, [ $similar_attribute ], 'gc_attribute', true );
318
						}
319
320
						// Otherwise insert a new term.
321
						wp_set_post_terms( $post_id, $game_attribute, 'gc_attribute', true );
322
					}
323
				}
324
325
				// Sideload the image from BGG.
326
				attach_bgg_image( $post_id, $game );
327
			}
328
		}
329
330
		// Delete the transient so we can do this again.
331
		delete_transient( 'gc_last_bgg_search' );
332
333
		// Redirect to the edit page for this game.
334
		wp_safe_redirect( esc_url_raw( $redirect_url ) );
335
		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...
336
	}
337
338
	return wp_die( esc_html__( 'Security check failed. What were you doing?', 'games-collector' ), esc_html__( 'Nonce check failed', 'games-collector' ) );
339
}
340
341
/**
342
 * Check if an existing game attribute term exists and return the ID if it does.
343
 *
344
 * @since  1.2.0
345
 * @param  string $search The game attribute name.
346
 * @return int|bool       The term ID if a matching term exists, false if it doesn't.
347
 */
348
function get_attribute_like( $search ) {
349
	// Check if a previously cached attribute for this term exists already.
350
	$cached_term_search = get_transient( 'gc_frequently_used_attributes' );
351
	if ( $cached_term_search && array_key_exists( $search, $cached_term_search ) ) {
352
		return $cached_term_search[ $search ];
353
	}
354
355
	$terms = get_terms( [
356
		'taxonomy'   => 'gc_attribute',
357
		'hide_empty' => true,
358
		'fields'     => 'ids',
359
		'name__like' => esc_html( $search ),
360
	] );
361
362
	if ( ! is_wp_error( $terms ) && count( $terms ) > 0 ) {
363
		// Cache this term combination so we can access it faster later.
364
		if ( ! $cached_term_search ) {
365
			set_transient( 'gc_frequently_used_attributes', [
366
				$search => $terms[0],
367
			], 999 * YEAR_IN_SECONDS );
368
		} else {
369
			$cached_term_search = array_merge( $cached_term_search, [ $search => $terms[0] ] );
370
			set_transient( 'gc_frequently_used_attributes', $cached_term_search, 999 * YEAR_IN_SECONDS );
371
		}
372
373
		return $terms[0];
374
	}
375
376
	return false;
377
}
378
379
/**
380
 * Sideload image for a BGG image.
381
 *
382
 * @since  1.2.0
383
 * @param  int   $post_id The game ID.
384
 * @param  array $game    The array of game data from BGG.
385
 */
386
function attach_bgg_image( $post_id, $game ) {
387
	$image_id = media_sideload_image( esc_url_raw( $game['image'] ), $post_id, esc_html( $game['title'] ), 'id' );
388
	set_post_thumbnail( $post_id, $image_id );
389
}
390