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
|
|
|
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; |
|
|
|
|
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 — 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 ) { |
|
|
|
|
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. “betrayal house hill”', '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; |
|
|
|
|
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
|
|
|
|
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.