1
<?php
2

3
namespace Pressbooks\Metadata;
4

5
use function \Pressbooks\L10n\get_book_language;
6
use function \Pressbooks\L10n\get_locale;
7
use function \Pressbooks\Sanitize\is_valid_timestamp;
8
use function \Pressbooks\Utility\get_contents;
9
use function \Pressbooks\Utility\is_assoc;
10
use function \Pressbooks\Utility\oxford_comma;
11
use function \Pressbooks\Utility\oxford_comma_explode;
12
use Pressbooks\Book;
13
use Pressbooks\Licensing;
14
use Pressbooks\Metadata;
15

16
/**
17
 * Returns an html blob of meta elements based on what is set in 'Book Information'
18
 *
19
 * @deprecated 5.7.0
20
 *
21
 * @return string
22
 */
23
function get_seo_meta_elements() {
24
	// map items that are already captured
25
	$meta_mapping = [
26 1
		'author' => 'pb_authors',
27
		'description' => 'pb_about_50',
28
		'keywords' => 'pb_keywords_tags',
29
		'publisher' => 'pb_publisher',
30
	];
31 1
	$html = "<meta name='application-name' content='Pressbooks'>\n";
32 1
	$metadata = Book::getBookInformation();
33

34
	// create meta elements
35 1
	foreach ( $meta_mapping as $name => $content ) {
36 1
		if ( array_key_exists( $content, $metadata ) ) {
37 1
			$html .= "<meta name='" . $name . "' content='" . $metadata[ $content ] . "'>\n";
38
		}
39
	}
40

41 1
	return $html;
42
}
43

44
/**
45
 * Returns an html blob of microdata elements based on what is set in 'Book Information'
46
 *
47
 * @deprecated 5.7.0
48
 *
49
 * @return string
50
 */
51
function get_microdata_elements() {
52 1
	$html = '';
53
	// map items that are already captured
54
	$micro_mapping = [
55 1
		'about' => 'pb_bisac_subject',
56
		'alternativeHeadline' => 'pb_subtitle',
57
		'author' => 'pb_authors',
58
		'contributor' => 'pb_contributors',
59
		'copyrightHolder' => 'pb_copyright_holder',
60
		'copyrightYear' => 'pb_copyright_year',
61
		'datePublished' => 'pb_publication_date',
62
		'description' => 'pb_about_50',
63
		'editor' => 'pb_editors',
64
		'image' => 'pb_cover_image',
65
		'inLanguage' => 'pb_language',
66
		'keywords' => 'pb_keywords_tags',
67
		'publisher' => 'pb_publisher',
68
		'isBasedOn' => 'pb_is_based_on',
69
	];
70 1
	$metadata = Book::getBookInformation();
71

72
	// create microdata elements
73 1
	foreach ( $micro_mapping as $itemprop => $content ) {
74 1
		if ( array_key_exists( $content, $metadata ) ) {
75 1
			if ( 'pb_publication_date' === $content ) {
76 0
				$content = date( 'Y-m-d', (int) $metadata[ $content ] );
77
			} else {
78 1
				$content = $metadata[ $content ];
79
			}
80 1
			$html .= "<meta itemprop='" . $itemprop . "' content='" . $content . "' id='" . $itemprop . "'>\n";
81
		}
82
	}
83

84 1
	if ( ! array_key_exists( 'pb_copyright_year', $metadata ) && array_key_exists( 'pb_publication_date', $metadata ) && is_valid_timestamp( $metadata['pb_publication_date'] ) ) {
85 0
		$itemprop = 'copyrightYear';
86 0
		$content = strftime( '%Y', (int) $metadata['pb_publication_date'] );
87 0
		$html .= "<meta itemprop='" . $itemprop . "' content='" . $content . "' id='" . $itemprop . "'>\n";
88
	}
89

90 1
	return $html;
91
}
92

93
/**
94
 * @param \WP_Post $post
95
 */
96
function add_expanded_metadata_box( $post ) {
97

98 0
	if ( $post->post_type !== 'metadata' ) {
99 0
		return;
100
	}
101

102 0
	if ( isset( $_GET['pressbooks_show_expanded_metadata'] ) && check_admin_referer( 'pb-expanded-metadata' ) ) {
103 0
		update_option( 'pressbooks_show_expanded_metadata', $_GET['pressbooks_show_expanded_metadata'] );
104
	}
105

106 0
	$show_expanded_metadata = show_expanded_metadata();
107 0
	$has_expanded_metadata = has_expanded_metadata();
108

109 0
	$url = get_edit_post_link( $post->ID );
110 0
	if ( $show_expanded_metadata ) {
111 0
		$text = __( 'Hide Additional Book Information', 'pressbooks' );
112 0
		$href = wp_nonce_url( $url . '&pressbooks_show_expanded_metadata=0', 'pb-expanded-metadata' );
113
	} else {
114 0
		$text = __( 'Show Additional Book Information', 'pressbooks' );
115 0
		$href = wp_nonce_url( $url . '&pressbooks_show_expanded_metadata=1', 'pb-expanded-metadata' );
116
	}
117

118
	?>
119 0
	<div id="expanded-metadata-panel" class="postbox">
120
		<div class="inside">
121
			<p><?php _e( 'The book information you enter here appears on your book’s cover and title pages and in the metadata of your webbook and exported files.', 'pressbooks' ); ?></p>
122
			<?php if ( ! $show_expanded_metadata && ! $has_expanded_metadata ) { ?>
123
				<p><?php _e( 'If you need to enter additional information, click the button below to see all available fields.', 'pressbooks' ); ?></p>
124
			<?php } ?>
125
			<?php if ( ! $has_expanded_metadata ) { ?>
126
				<p><a class="button" href="<?php echo $href; ?>"><?php echo $text; ?></a></p>
127
			<?php } ?>
128 0
		</div>
129
	</div>
130
	<?php
131
}
132

133
/**
134
 * Should we show expanded metadata fields or not?
135
 *
136
 * @return bool
137
 */
138
function show_expanded_metadata() {
139 1
	if ( isset( $_GET['pressbooks_show_expanded_metadata'] ) && check_admin_referer( 'pb-expanded-metadata' ) ) {
140 0
		if ( ! empty( $_GET['pressbooks_show_expanded_metadata'] ) ) {
141 0
			return true;
142
		} else {
143 0
			return false;
144
		}
145 1
	} elseif ( ! empty( get_option( 'pressbooks_show_expanded_metadata' ) ) ) {
146 1
		return true;
147 1
	} elseif ( has_expanded_metadata() ) {
148 0
		update_option( 'pressbooks_show_expanded_metadata', 1 );
149 0
		return true;
150
	}
151 1
	return false;
152
}
153

154
/**
155
 * Is expanded metadata present in this book?
156
 *
157
 * @return bool
158
 */
159
function has_expanded_metadata() {
160 1
	$metadata = Book::getBookInformation();
161
	$additional_fields = [
162 1
		'pb_onsale_date',
163
		'pb_copyright_year',
164
		'pb_series_title',
165
		'pb_series_number',
166
		'pb_keywords_tags',
167
		'pb_hashtag',
168
		'pb_list_price_print',
169
		'pb_list_price_pdf',
170
		'pb_list_price_epub',
171
		'pb_list_price_web',
172
		'pb_audience',
173
		'pb_bisac_subject',
174
		'pb_bisac_regional_theme',
175
	];
176 1
	foreach ( $additional_fields as $field ) {
177 1
		if ( isset( $metadata[ $field ] ) && ! empty( $metadata[ $field ] ) ) {
178 1
			return true;
179
		}
180
	}
181

182 1
	return false;
183
}
184

185
/**
186
 * Convert Pressbooks Book Information to Schema.org-compatible metadata
187
 *
188
 * @since 4.1
189
 *
190
 * @param array $book_information
191
 * @param bool  $network_excluded_directory
192
 *
193
 * @return array
194
 */
195
function book_information_to_schema( $book_information, $network_excluded_directory = false ) {
196 1
	$book_schema = [];
197

198 1
	$book_schema['@context'] = 'http://schema.org';
199 1
	$book_schema['@type'] = 'Book';
200

201
	$mapped_properties = [
202 1
		'pb_title' => 'name',
203
		'pb_short_title' => 'alternateName',
204
		'pb_ebook_isbn' => 'isbn',
205
		'pb_keywords_tags' => 'keywords',
206
		'pb_subtitle' => 'alternativeHeadline',
207
		'pb_subject' => 'genre',
208
		'pb_language' => 'inLanguage',
209
		'pb_copyright_year' => 'copyrightYear',
210
		'pb_about_50' => 'disambiguatingDescription',
211
		'pb_about_unlimited' => 'description',
212
		'pb_cover_image' => 'image',
213
		'pb_series_number' => 'position',
214
		'pb_is_based_on' => 'isBasedOn',
215
		'pb_word_count' => 'wordCount',
216
		'pb_storage_size' => 'storageSize',
217
		'pb_h5p_activities' => 'h5pActivities',
218
		'pb_in_catalog' => 'inCatalog',
219
		'pb_book_directory_excluded' => 'bookDirectoryExcluded',
220
	];
221

222 1
	foreach ( $mapped_properties as $old => $new ) {
223 1
		if ( isset( $book_information[ $old ] ) ) {
224 1
			$book_schema[ $new ] = $book_information[ $old ];
225
		}
226
	}
227

228 1
	if ( isset( $book_information['pb_primary_subject'] ) ) {
229 0
		$name = Metadata\get_subject_from_thema( $book_information['pb_primary_subject'] );
230 0
		$book_schema['about'][] = [
231 0
			'@type' => 'Thing',
232 0
			'identifier' => ( is_null( $name ) || ! $name ) ?
233 0
						$book_information['pb_primary_subject'] : $name,
234 0
			'name' => $name,
235
		];
236
	}
237

238 1
	if ( isset( $book_information['pb_additional_subjects'] ) ) {
239 0
		$additional_subjects = explode( ', ', $book_information['pb_additional_subjects'] );
240 0
		foreach ( $additional_subjects as $additional_subject ) {
241 0
			$name = Metadata\get_subject_from_thema( $additional_subject );
242 0
			$book_schema['about'][] = [
243 0
				'@type' => 'Thing',
244 0
				'identifier' => $additional_subject,
245 0
				'name' => ( is_null( $name ) || ! $name ) ? $additional_subject : $name,
246
			];
247
		}
248
	}
249

250 1
	if ( isset( $book_information['pb_bisac_subject'] ) ) {
251 0
		$bisac_subjects = explode( ', ', $book_information['pb_bisac_subject'] );
252 0
		foreach ( $bisac_subjects as $bisac_subject ) {
253 0
			$book_schema['about'][] = [
254 0
				'@type' => 'Thing',
255 0
				'identifier' => $bisac_subject,
256
			];
257
		}
258
	}
259

260 1
	if ( isset( $book_information['pb_authors'] ) ) {
261 1
		$authors = oxford_comma_explode( $book_information['pb_authors'] );
262 1
		foreach ( $authors as $author ) {
263 1
			$book_schema['author'][] = [
264 1
				'@type' => 'Person',
265 1
				'name' => $author,
266
			];
267
		}
268
	}
269

270 1
	if ( isset( $book_information['pb_editors'] ) ) {
271 1
		$editors = oxford_comma_explode( $book_information['pb_editors'] );
272 1
		foreach ( $editors as $editor ) {
273 0
			$book_schema['editor'][] = [
274 0
				'@type' => 'Person',
275 0
				'name' => $editor,
276
			];
277
		}
278
	}
279

280 1
	if ( isset( $book_information['pb_translators'] ) ) {
281 1
		$translators = oxford_comma_explode( $book_information['pb_translators'] );
282 1
		foreach ( $translators as $translator ) {
283 0
			$book_schema['translator'][] = [
284 0
				'@type' => 'Person',
285 0
				'name' => $translator,
286
			];
287
		}
288
	}
289

290 1
	if ( isset( $book_information['pb_reviewers'] ) ) {
291 1
		$reviewers = oxford_comma_explode( $book_information['pb_reviewers'] );
292 1
		foreach ( $reviewers as $reviewer ) {
293 0
			$book_schema['reviewedBy'][] = [
294 0
				'@type' => 'Person',
295 0
				'name' => $reviewer,
296
			];
297
		}
298
	}
299

300 1
	if ( isset( $book_information['pb_illustrators'] ) ) {
301 1
		$illustrators = oxford_comma_explode( $book_information['pb_illustrators'] );
302 1
		foreach ( $illustrators as $illustrator ) {
303 0
			$book_schema['illustrator'][] = [
304 0
				'@type' => 'Person',
305 0
				'name' => $illustrator,
306
			];
307
		}
308
	}
309

310 1
	if ( isset( $book_information['pb_contributors'] ) ) {
311 1
		$contributing_authors = oxford_comma_explode( $book_information['pb_contributors'] );
312 1
		foreach ( $contributing_authors as $contributor ) {
313 0
			$book_schema['contributor'][] = [
314 0
				'@type' => 'Person',
315 0
				'name' => $contributor,
316
			];
317
		}
318
	}
319

320 1
	if ( isset( $book_information['pb_publisher'] ) ) {
321 1
		$book_schema['publisher'] = [
322 1
			'@type' => 'Organization',
323 1
			'name' => $book_information['pb_publisher'],
324
		];
325

326 1
		if ( isset( $book_information['pb_publisher_city'] ) ) {
327 0
			$book_schema['publisher']['address'] = [
328 0
				'@type' => 'PostalAddress',
329 0
				'addressLocality' => $book_information['pb_publisher_city'],
330
			];
331
		}
332
	}
333

334 1
	if ( isset( $book_information['pb_audience'] ) ) {
335 0
		$book_schema['audience'] = [
336 0
			'@type' => 'Audience',
337 0
			'name' => $book_information['pb_audience'],
338
		];
339
	}
340

341 1
	if ( isset( $book_information['pb_publication_date'] ) && is_valid_timestamp( $book_information['pb_publication_date'] ) ) {
342 1
		$book_schema['datePublished'] = strftime( '%F', (int) $book_information['pb_publication_date'] );
343

344 1
		if ( ! isset( $book_information['pb_copyright_year'] ) ) {
345 1
			$book_schema['copyrightYear'] = strftime( '%Y', (int) $book_information['pb_publication_date'] );
346
		}
347
	}
348

349 1
	if ( isset( $book_information['pb_copyright_holder'] ) ) { // TODO: Person or Organization?
350 0
		$book_schema['copyrightHolder'] = [
351 0
			'@type' => 'Organization',
352 0
			'name' => $book_information['pb_copyright_holder'],
353
		];
354
	}
355

356 1
	if ( ! isset( $book_information['pb_book_license'] ) ) {
357 1
		$book_information['pb_book_license'] = 'all-rights-reserved';
358
	}
359

360 1
	$licensing = new Licensing;
361 1
	$supported_types = $licensing->getSupportedTypes();
362 1
	$book_schema['license'] = [
363 1
		'@type' => 'CreativeWork',
364 1
		'url' => $licensing->getUrlForLicense( $book_information['pb_book_license'] ),
365 1
		'name' => $supported_types[ $book_information['pb_book_license'] ]['desc'] ?? 'all-rights-reserved',
366 1
		'code' => $supported_types[ $book_information['pb_book_license'] ]['abbreviation'] ?? 'All Rights Reserved',
367
	];
368 1
	if ( isset( $book_information['pb_custom_copyright'] ) ) {
369 0
		$book_schema['license']['description'] = $book_information['pb_custom_copyright'];
370
	}
371

372 1
	if ( isset( $book_information['pb_book_doi'] ) ) {
373 1
		$book_schema['identifier'] = [
374 1
			'@type' => 'PropertyValue',
375 1
			'propertyID' => 'DOI',
376 1
			'value' => $book_information['pb_book_doi'],
377
		];
378
		/**
379
		 * Filter the DOI resolver service URL (default: https://dx.doi.org).
380
		 *
381
		 * @since 5.6.0
382
		 */
383 1
		$doi_resolver = apply_filters( 'pb_doi_resolver', 'https://dx.doi.org' );
384 1
		$book_schema['sameAs'] = trailingslashit( $doi_resolver ) . $book_information['pb_book_doi'];
385
	}
386

387 1
	if ( isset( $book_information['pb_word_count'] ) ) {
388 1
		$book_schema['wordCount'] = intval( $book_information['pb_word_count'] );
389
	}
390

391 1
	if ( isset( $book_information['pb_storage_size'] ) ) {
392 1
		$book_schema['storageSize'] = intval( $book_information['pb_storage_size'] );
393
	}
394

395 1
	if ( isset( $book_information['pb_h5p_activities'] ) ) {
396 1
		$book_schema['h5pActivities'] = intval( $book_information['pb_h5p_activities'] );
397
	}
398

399 1
	if ( isset( $book_information['pb_in_catalog'] ) ) {
400 1
		$book_schema['inCatalog'] = $book_information['pb_in_catalog'] === '1';
401
	}
402

403 1
	if ( true === $network_excluded_directory ) {
404 0
		$book_schema['bookDirectoryExcluded'] = $network_excluded_directory;
405 1
	} elseif ( isset( $book_schema['bookDirectoryExcluded'] ) ) {
406 1
		$book_schema['bookDirectoryExcluded'] = (bool) $book_information['pb_book_directory_excluded'];
407
	} else {
408 1
		$book_schema['bookDirectoryExcluded'] = false;
409
	}
410

411 1
	if ( isset( $book_information['last_updated'] ) ) {
412 1
		$book_schema['lastUpdated'] = $book_information['last_updated'];
413
	}
414

415 1
	if ( isset( $book_information['pb_language'] ) ) {
416 1
		$languages = \Pressbooks\L10n\supported_languages();
417 1
		$language = ( array_key_exists( $book_information['pb_language'], $languages ) ) ?
418 1
			$languages[ $book_information['pb_language'] ] : 'Unavailable code';
419 1
		$book_schema['language'] = [
420 1
			'@type' => 'Language',
421 1
			'code' => $book_information['pb_language'],
422 1
			'name' => $language,
423
		];
424
	}
425

426 1
	if ( isset( $book_information['site_name'] ) ) {
427 1
		$book_schema['network'] = [
428 1
			'@type' => 'Network',
429 1
			'host' => wp_parse_url( $book_information['pb_book_url'], PHP_URL_HOST ),
430 1
			'name' => $book_information['site_name'],
431
		];
432
	}
433

434
	// TODO: educationalAlignment, educationalUse, timeRequired, typicalAgeRange, interactivityType, learningResourceType, isBasedOnUrl
435

436 1
	return $book_schema;
437
}
438

439
/**
440
 * Convert book Schema.org metadata to Pressbooks Book Information
441
 *
442
 * @since 4.1
443
 *
444
 * @param array $book_schema
445
 *
446
 * @return array
447
 */
448
function schema_to_book_information( $book_schema ) {
449 1
	$book_information = [];
450

451 1
	if ( isset( $book_schema['description'] ) ) {
452 0
		$book_schema['description'] = html_entity_decode( $book_schema['description'] );
453
	}
454

455
	// Values expected to be text
456
	$mapped_properties = [
457 1
		'name' => 'pb_title',
458
		'alternateName' => 'pb_short_title',
459
		'isbn' => 'pb_ebook_isbn',
460
		'keywords' => 'pb_keywords_tags',
461
		'alternativeHeadline' => 'pb_subtitle',
462
		'genre' => 'pb_subject',
463
		'inLanguage' => 'pb_language',
464
		'copyrightYear' => 'pb_copyright_year',
465
		'disambiguatingDescription' => 'pb_about_50',
466
		'description' => 'pb_about_unlimited',
467
		'image' => 'pb_cover_image',
468
		'position' => 'pb_series_number',
469
		'isBasedOn' => 'pb_is_based_on',
470
	];
471

472 1
	foreach ( $mapped_properties as $old => $new ) {
473 1
		if ( isset( $book_schema[ $old ] ) ) {
474 1
			$book_information[ $new ] = $book_schema[ $old ];
475
		}
476
	}
477

478 1
	if ( isset( $book_schema['about'] ) ) {
479 0
		$subjects = [];
480 0
		$bisac_subjects = [];
481 0
		foreach ( $book_schema['about'] as $subject ) {
482 0
			if ( is_bisac( $subject['identifier'] ) ) {
483 0
				$bisac_subjects[] = $subject['identifier'];
484
			} else {
485 0
				$subjects[] = $subject['identifier'];
486
			}
487
		}
488 0
		$book_information['pb_primary_subject'] = array_shift( $subjects );
489 0
		$book_information['pb_additional_subjects'] = implode( ', ', $subjects );
490 0
		$book_information['pb_bisac_subject'] = implode( ', ', $bisac_subjects );
491
	}
492

493 1
	if ( isset( $book_schema['author'] ) ) {
494
		// Pressbooks 5
495 1
		$authors = [];
496 1
		foreach ( $book_schema['author'] as $author ) {
497 1
			if ( isset( $author['name'] ) ) {
498 1
				$authors[] = $author['name'];
499
			}
500
		}
501 1
		if ( empty( $authors ) && isset( $book_schema['author']['name'] ) ) {
502
			// Pressbooks 4
503 1
			$authors[] = $book_schema['author']['name']; // Backwards compatibility with Pressbooks 4
504 1
			if ( isset( $book_schema['author']['alternateName'] ) ) {
505 1
				$book_information['pb_author_file_as'] = $book_schema['author']['alternateName'];
506
			}
507
		} else {
508 1
			$book_information['pb_author'] = implode( ', ', $authors );
509
		}
510 1
		$book_information['pb_authors'] = oxford_comma( $authors );
511
	}
512

513 1
	if ( isset( $book_schema['editor'] ) ) {
514 1
		$editors = [];
515 1
		foreach ( $book_schema['editor'] as $editor ) {
516 1
			$editors[] = $editor['name'];
517
		}
518 1
		$book_information['pb_editors'] = oxford_comma( $editors );
519
	}
520

521 1
	if ( isset( $book_schema['translator'] ) ) {
522 1
		$translators = [];
523 1
		foreach ( $book_schema['translator'] as $translator ) {
524 1
			$translators[] = $translator['name'];
525
		}
526 1
		$book_information['pb_translators'] = oxford_comma( $translators );
527
	}
528

529 1
	if ( isset( $book_schema['reviewedBy'] ) ) {
530 1
		$reviewers = [];
531 1
		foreach ( $book_schema['reviewedBy'] as $reviewer ) {
532 1
			$reviewers[] = $reviewer['name'];
533
		}
534 1
		$book_information['pb_reviewers'] = oxford_comma( $reviewers );
535
	}
536

537 1
	if ( isset( $book_schema['illustrator'] ) ) {
538 1
		$illustrators = [];
539 1
		foreach ( $book_schema['illustrator'] as $illustrator ) {
540 1
			$illustrators[] = $illustrator['name'];
541
		}
542 1
		$book_information['pb_illustrators'] = oxford_comma( $illustrators );
543
	}
544

545 1
	if ( isset( $book_schema['contributor'] ) ) {
546 1
		$contributors = [];
547 1
		foreach ( $book_schema['contributor'] as $contributor ) {
548 1
			$contributors[] = $contributor['name'];
549
		}
550 1
		$book_information['pb_contributors'] = oxford_comma( $contributors );
551
	}
552

553 1
	if ( isset( $book_schema['publisher'] ) ) {
554 0
		$book_information['pb_publisher'] = $book_schema['publisher']['name'];
555 0
		if ( isset( $book_schema['publisher']['address'] ) ) {
556 0
			$book_information['pb_publisher_city'] = $book_schema['publisher']['address']['addressLocality'];
557
		}
558
	}
559

560 1
	if ( isset( $book_schema['audience'] ) ) {
561 1
		$book_information['pb_audience'] = $book_schema['audience']['name'];
562
	}
563

564 1
	if ( isset( $book_schema['datePublished'] ) ) {
565 1
		$book_information['pb_publication_date'] = strtotime( $book_schema['datePublished'] );
566
	}
567

568 1
	if ( isset( $book_schema['copyrightHolder'] ) ) {
569 1
		$book_information['pb_copyright_holder'] = $book_schema['copyrightHolder']['name'];
570
	}
571

572 1
	$licensing = new Licensing;
573 1
	if ( is_array( $book_schema['license'] ) ) {
574 1
		$book_information['pb_book_license'] = $licensing->getLicenseFromUrl( $book_schema['license']['url'] );
575 1
		if ( isset( $book_schema['license']['description'] ) ) {
576 1
			$book_information['pb_custom_copyright'] = $book_schema['license']['description'];
577
		}
578
	} else {
579 1
		$book_information['pb_book_license'] = $licensing->getLicenseFromUrl( $book_schema['license'] );
580
	}
581

582 1
	if ( isset( $book_schema['sameAs'] ) ) {
583
		/**
584
		 * Filter the DOI resolver service URL (default: https://dx.doi.org).
585
		 *
586
		 * @since 5.6.0
587
		 */
588 1
		$doi_resolver = apply_filters( 'pb_doi_resolver', 'https://dx.doi.org' );
589 1
		$book_information['pb_book_doi'] = str_replace( trailingslashit( $doi_resolver ), '', $book_schema['sameAs'] );
590
	}
591

592 1
	return $book_information;
593
}
594

595
/**
596
 * Convert Pressbooks Section Information to Schema.org-compatible metadata
597
 *
598
 * @since 4.1
599
 *
600
 * @param array $section_information
601
 * @param array $book_information
602
 *
603
 * @return array
604
 */
605
function section_information_to_schema( $section_information, $book_information ) {
606 1
	$section_schema = [];
607

608 1
	$section_schema['@context'] = 'http://schema.org';
609 1
	$section_schema['@type'] = 'Chapter';
610

611
	$mapped_section_properties = [
612 1
		'pb_title' => 'name',
613
		'pb_short_title' => 'alternateName',
614
		'pb_subtitle' => 'alternativeHeadline',
615
		'pb_is_based_on' => 'isBasedOn',
616
	];
617

618
	$mapped_book_properties = [
619 1
		'pb_language' => 'inLanguage',
620
		'pb_title' => 'isPartOf',
621
		'pb_copyright_year' => 'copyrightYear',
622
	];
623

624 1
	foreach ( $mapped_section_properties as $old => $new ) {
625 1
		if ( isset( $section_information[ $old ] ) ) {
626 1
			$section_schema[ $new ] = $section_information[ $old ];
627
		}
628
	}
629

630 1
	foreach ( $mapped_book_properties as $old => $new ) {
631 1
		if ( isset( $book_information[ $old ] ) ) {
632 1
			$section_schema[ $new ] = $book_information[ $old ];
633
		}
634
	}
635

636 1
	if ( ! empty( $section_information['pb_chapter_number'] ) ) {
637 1
		$section_schema['position'] = $section_information['pb_chapter_number'];
638
	}
639

640
	// Use section, if missing use book
641 1
	$authors = [];
642 1
	if ( ! empty( $section_information['pb_authors'] ) ) {
643 0
		$authors = oxford_comma_explode( $section_information['pb_authors'] );
644 1
	} elseif ( ! empty( $book_information['pb_authors'] ) ) {
645 1
		$authors = oxford_comma_explode( $book_information['pb_authors'] );
646
	}
647 1
	foreach ( $authors as $author ) {
648 1
		$section_schema['author'][] = [
649 1
			'@type' => 'Person',
650 1
			'name' => $author,
651
		];
652
	}
653

654 1
	if ( isset( $book_information['pb_editors'] ) ) {
655 1
		$editors = oxford_comma_explode( $book_information['pb_editors'] );
656 1
		foreach ( $editors as $editor ) {
657 0
			$section_schema['editor'][] = [
658 0
				'@type' => 'Person',
659 0
				'name' => $editor,
660
			];
661
		}
662
	}
663

664 1
	if ( isset( $book_information['pb_translators'] ) ) {
665 1
		$translators = oxford_comma_explode( $book_information['pb_translators'] );
666 1
		foreach ( $translators as $translator ) {
667 0
			$section_schema['translator'][] = [
668 0
				'@type' => 'Person',
669 0
				'name' => $translator,
670
			];
671
		}
672
	}
673

674 1
	if ( isset( $book_information['pb_reviewers'] ) ) {
675 1
		$reviewers = oxford_comma_explode( $book_information['pb_reviewers'] );
676 1
		foreach ( $reviewers as $reviewer ) {
677 0
			$section_schema['reviewedBy'][] = [
678 0
				'@type' => 'Person',
679 0
				'name' => $reviewer,
680
			];
681
		}
682
	}
683

684 1
	if ( isset( $book_information['pb_illustrators'] ) ) {
685 1
		$illustrators = oxford_comma_explode( $book_information['pb_illustrators'] );
686 1
		foreach ( $illustrators as $illustrator ) {
687 0
			$section_schema['illustrator'][] = [
688 0
				'@type' => 'Person',
689 0
				'name' => $illustrator,
690
			];
691
		}
692
	}
693

694 1
	if ( isset( $book_information['pb_contributors'] ) ) {
695 1
		$contributing_authors = oxford_comma_explode( $book_information['pb_contributors'] );
696 1
		foreach ( $contributing_authors as $contributor ) {
697 0
			$section_schema['contributor'][] = [
698 0
				'@type' => 'Person',
699 0
				'name' => $contributor,
700
			];
701
		}
702
	}
703

704 1
	if ( isset( $book_information['pb_audience'] ) ) {
705 0
		$section_schema['audience'] = [
706 0
			'@type' => 'Audience',
707 0
			'name' => $book_information['pb_audience'],
708
		];
709
	}
710

711 1
	if ( isset( $book_information['pb_publisher'] ) ) {
712 1
		$section_schema['publisher'] = [
713 1
			'@type' => 'Organization',
714 1
			'name' => $book_information['pb_publisher'],
715
		];
716

717 1
		if ( isset( $book_information['pb_publisher_city'] ) ) {
718 0
			$section_schema['publisher']['address'] = [
719 0
				'@type' => 'PostalAddress',
720 0
				'addressLocality' => $book_information['pb_publisher_city'],
721
			];
722
		}
723
	}
724

725 1
	if ( isset( $book_information['pb_publication_date'] ) && is_valid_timestamp( $book_information['pb_publication_date'] ) ) {
726 1
		$section_schema['datePublished'] = strftime( '%F', (int) $book_information['pb_publication_date'] );
727 1
		if ( ! isset( $book_information['pb_copyright_year'] ) ) {
728 1
			$section_schema['copyrightYear'] = strftime( '%Y', (int) $book_information['pb_publication_date'] );
729
		}
730
	}
731

732 1
	if ( isset( $book_information['pb_copyright_holder'] ) ) { // TODO: Person or Organization?
733 0
		$section_schema['copyrightHolder'] = [
734 0
			'@type' => 'Organization',
735 0
			'name' => $book_information['pb_copyright_holder'],
736
		];
737
	}
738

739 1
	if ( empty( $section_information['pb_section_license'] ) ) {
740 1
		if ( ! empty( $book_information['pb_book_license'] ) ) {
741 0
			$section_information['pb_section_license'] = $book_information['pb_book_license'];
742
		} else {
743 1
			$section_information['pb_section_license'] = 'all-rights-reserved';
744
		}
745
	}
746

747 1
	$licensing = new Licensing;
748

749 1
	if ( ! $licensing->isSupportedType( $section_information['pb_section_license'] ) ) {
750 0
		$section_information['pb_section_license'] = 'all-rights-reserved';
751
	}
752

753 1
	$section_schema['license'] = [
754 1
		'@type' => 'CreativeWork',
755 1
		'url' => $licensing->getUrlForLicense( $section_information['pb_section_license'] ),
756 1
		'name' => $licensing->getSupportedTypes()[ $section_information['pb_section_license'] ]['desc'] ?? 'all-rights-reserved',
757
	];
758

759 1
	if ( ! isset( $section_information['pb_is_based_on'] ) && isset( $book_information['pb_is_based_on'] ) ) {
760 0
		$section_schema['isBasedOn'] = $book_information['pb_is_based_on'];
761
	}
762

763 1
	if ( isset( $section_information['pb_section_doi'] ) ) {
764 1
		$section_schema['identifier'] = [
765 1
			'@type' => 'PropertyValue',
766 1
			'propertyID' => 'DOI',
767 1
			'value' => $section_information['pb_section_doi'],
768
		];
769
		/**
770
		 * Filter the DOI resolver service URL (default: https://dx.doi.org).
771
		 *
772
		 * @since 5.6.0
773
		 */
774 1
		$doi_resolver = apply_filters( 'pb_doi_resolver', 'https://dx.doi.org' );
775 1
		$section_schema['sameAs'] = trailingslashit( $doi_resolver ) . $section_information['pb_section_doi'];
776
	}
777

778
	// TODO: educationalAlignment, educationalUse, timeRequired, typicalAgeRange, interactivityType, learningResourceType, isBasedOnUrl
779

780 1
	return $section_schema;
781
}
782

783
/**
784
 * Convert section Schema.org metadata to Pressbooks Section Information
785
 *
786
 * @since 4.1
787
 *
788
 * @param array $section_schema
789
 * @param array $book_schema
790
 *
791
 * @return array
792
 */
793
function schema_to_section_information( $section_schema, $book_schema ) {
794 1
	$section_information = [];
795

796
	$mapped_section_properties = [
797 1
		'alternateName' => 'pb_short_title',
798
		'alternativeHeadline' => 'pb_subtitle',
799
	];
800

801 1
	foreach ( $mapped_section_properties as $old => $new ) {
802 1
		if ( isset( $section_schema[ $old ] ) ) {
803 0
			$section_information[ $new ] = $section_schema[ $old ];
804
		}
805
	}
806

807
	// Authors
808 1
	if ( isset( $section_schema['author'], $book_schema['author'] ) ) {
809 1
		$book_authors = [];
810 1
		if ( is_assoc( $book_schema['author'] ) ) {
811 1
			$book_schema['author'] = [ $book_schema['author'] ];
812
		}
813 1
		foreach ( $book_schema['author'] as $book_author ) {
814 1
			if ( isset( $book_author['name'] ) ) {
815 1
				$book_authors[] = $book_author['name'];
816
			}
817
		}
818 1
		$section_authors = [];
819 1
		if ( is_assoc( $section_schema['author'] ) ) {
820 1
			$section_schema['author'] = [ $section_schema['author'] ];
821
		}
822 1
		foreach ( $section_schema['author'] as $section_author ) {
823 1
			if ( isset( $section_author['name'] ) ) {
824 1
				$section_authors[] = $section_author['name'];
825
			}
826
		}
827 1
		if ( $section_authors !== $book_authors ) {
828 1
			$section_information['pb_authors'] = oxford_comma( $section_authors );
829
		}
830
	}
831

832
	// License
833 1
	$book_license = '';
834 1
	$section_license = '';
835 1
	if ( isset( $book_schema['license'] ) ) {
836 1
		if ( is_array( $book_schema['license'] ) ) {
837 1
			$book_license = $book_schema['license']['url'];
838
		} else {
839 1
			$book_license = $book_schema['license'];
840
		}
841
	}
842 1
	if ( isset( $section_schema['license'] ) ) {
843 1
		if ( is_array( $section_schema['license'] ) ) {
844 1
			$section_license = $section_schema['license']['url'];
845
		} else {
846 0
			$section_license = $section_schema['license'];
847
		}
848
	}
849 1
	if ( $section_license !== $book_license ) {
850 1
		$licensing = new Licensing;
851 1
		$section_information['pb_section_license'] = $licensing->getLicenseFromUrl( $section_license );
852
	}
853

854
	// Version Tracking
855 1
	if ( isset( $section_schema['isBasedOn'] ) ) {
856 0
		if ( empty( $book_schema['isBasedOn'] ) || $section_schema['isBasedOn'] !== $book_schema['isBasedOn'] ) {
857 0
			$section_information['pb_is_based_on'] = $section_schema['isBasedOn'];
858
		}
859
	}
860

861 1
	if ( isset( $section_schema['sameAs'] ) ) {
862
		/**
863
		 * Filter the DOI resolver service URL (default: https://dx.doi.org).
864
		 *
865
		 * @since 5.6.0
866
		 */
867 1
		$doi_resolver = apply_filters( 'pb_doi_resolver', 'https://dx.doi.org' );
868 1
		$section_information['pb_section_doi'] = str_replace( trailingslashit( $doi_resolver ), '', $section_schema['sameAs'] );
869
	}
870

871 1
	return $section_information;
872
}
873

874

875
/**
876
 * Return an array of Thema subject categories.
877
 *
878
 * @since 4.4.0
879
 *
880
 * @param bool $include_qualifiers Whether or not the Theme subject qualifiers should be included.
881
 *.
882
 * @return array
883
 */
884
function get_thema_subjects( $include_qualifiers = false ) {
885 1
	if ( \Pressbooks\Book::isBook() ) {
886 0
		$locale = substr( get_book_language(), 0, 2 );
887
	} else {
888 1
		$locale = substr( get_locale(), 0, 2 );
889
	}
890
	/**
891
	 * @since  5.9.1
892
	 * @param string $locale
893
	 */
894 1
	$locale = apply_filters( 'pb_thema_subjects_locale', $locale );
895

896 1
	$lang = ( in_array( $locale, [ 'de', 'en', 'es', 'fr', 'pt' ], true ) ) ? $locale : 'en';
897 1
	$json = get_contents( PB_PLUGIN_DIR . "symbionts/thema/thema-${lang}.json" );
898 1
	$values = json_decode( $json );
899 1
	$subjects = [];
900 1
	foreach ( $values->CodeList->ThemaCodes->Code as $code ) {
901 1
		if ( ctype_alpha( substr( $code->CodeValue, 0, 1 ) ) || $include_qualifiers && ctype_digit( substr( $code->CodeValue, 0, 1 ) ) ) {
902 1
			if ( strlen( $code->CodeValue ) === 1 ) {
903 1
				$subjects[ $code->CodeValue ] = [
904 1
					'label' => $code->CodeDescription,
905
				];
906 1
				if ( ctype_alpha( $code->CodeValue ) ) {
907 1
					$subjects[ $code->CodeValue ]['children'][ $code->CodeValue ] = $code->CodeDescription;
908
				}
909
			} else {
910 1
				$subjects[ substr( $code->CodeValue, 0, 1 ) ]['children'][ $code->CodeValue ] = $code->CodeDescription;
911
			}
912
		}
913
	}
914 1
	return $subjects;
915
}
916

917
/**
918
 * Retrieve the subject name from a Thema subject code.
919
 *
920
 * @since 4.4.0
921
 *
922
 * @param string $code The Thema code.
923
 *
924
 * @return string The subject name.
925
 */
926
function get_subject_from_thema( $code ) {
927 1
	$subjects = get_thema_subjects( true );
928 1
	foreach ( $subjects as $key => $group ) {
929 1
		if ( strpos( $code, strval( $key ) ) === 0 ) {
930 1
			return $group['children'][ $code ];
931
		}
932
	}
933

934 0
	return false;
935
}
936

937
/**
938
 * Determine if a subject code is a BISAC code.
939
 *
940
 * @since 4.4.0
941
 *
942
 * @param string $code The code.
943
 *
944
 * @return bool
945
 */
946

947
function is_bisac( $code ) {
948 1
	if ( strlen( $code ) === 9 ) {
949 1
		if ( ctype_alpha( substr( $code, 0, 3 ) ) && ctype_digit( substr( $code, 3, 6 ) ) ) {
950 1
			return true;
951
		}
952
	}
953

954 1
	return false;
955
}
956

957
/**
958
 * @since 5.0.0
959
 */
960
function register_contributor_meta() {
961
	$args = [
962 0
		'sanitize_callback' => 'sanitize_text_field',
963
	];
964 0
	register_term_meta( 'contributor', 'contributor_first_name', $args );
965 0
	register_term_meta( 'contributor', 'contributor_last_name', $args );
966
}
967

968
/**
969
 * Ensure book data models are registered.
970
 *
971
 * These should already have been initialized by hooks, but sometimes they are disabled because we don't want them in the root site.
972
 */
973
function init_book_data_models() {
974 1
	if ( ! post_type_exists( 'chapter' ) ) {
975 0
		\Pressbooks\PostType\register_post_types();
976
	}
977 1
	if ( get_post_status_object( 'web-only' ) === null ) {
978 0
		\Pressbooks\PostType\register_post_statii();
979
	}
980 1
	if ( ! taxonomy_exists( 'front-matter-type' ) ) {
981 0
		\Pressbooks\Taxonomy::init()->registerTaxonomies();
982
	}
983
}
984

985
/**
986
 * Get the section metadata for a given ID.
987
 *
988
 * @since 5.7.0
989
 *
990
 * @param int $post_id
991
 *
992
 * @return array
993
 */
994
function get_section_information( $post_id ) {
995 1
	$section_meta = get_post_meta( $post_id, '', true );
996 1
	$section_meta['pb_title'] = get_the_title( $post_id );
997 1
	if ( get_post_type( $post_id ) === 'chapter' ) {
998 1
		$section_meta['pb_chapter_number'] = pb_get_chapter_number( $post_id );
999
	}
1000 1
	foreach ( $section_meta as $key => $value ) {
1001 1
		if ( is_array( $value ) ) {
1002 1
			$section_meta[ $key ] = array_pop( $value );
1003
		}
1004
	}
1005
	// Override Contributors
1006 1
	$contributors = new \Pressbooks\Contributors();
1007 1
	foreach ( $contributors->getAll( $post_id ) as $key => $val ) {
1008 1
		$section_meta[ $key ] = $val;
1009
	};
1010

1011 1
	return $section_meta;
1012
}
1013

1014

1015
/**
1016
 * Echo the JSON-LD metadata tag for a book or section.
1017
 *
1018
 * @since 5.7.0
1019
 *
1020
 * @return null
1021
 */
1022
function add_json_ld_metadata() {
1023

1024 1
	$context = is_singular( [ 'front-matter', 'part', 'chapter', 'back-matter' ] ) ? 'section' : 'book';
1025 1
	if ( $context === 'section' ) {
1026
		global $post;
1027 0
		$section_information = get_section_information( $post->ID );
1028 0
		$book_information = Book::getBookInformation();
1029 0
		$metadata = section_information_to_schema( $section_information, $book_information );
1030
	} else {
1031 1
		$metadata = new Metadata();
1032
	}
1033 1
	printf( '<script type="application/ld+json">%s</script>', wp_json_encode( $metadata ) );
1034
}
1035

1036
/**
1037
 * Echo HighWire Press-compatible meta tags for Google Scholar and Zotero integration.
1038
 *
1039
 * @since 5.7.0
1040
 *
1041
 * @return null
1042
 */
1043
function add_citation_metadata() {
1044 1
	$context = is_singular( [ 'front-matter', 'part', 'chapter', 'back-matter' ] ) ? 'section' : 'book';
1045 1
	$book_information = Book::getBookInformation();
1046 1
	$tags = [];
1047

1048
	$map = [
1049 1
		'citation_book_title' => 'isPartOf',
1050
		'citation_title' => 'name',
1051
		'citation_year' => 'copyrightYear',
1052
		'citation_publication_date' => 'datePublished',
1053
		'citation_language' => 'inLanguage',
1054
		'citation_keywords' => 'keywords',
1055
		'citation_publisher' => 'publisher.name',
1056
		'citation_isbn' => 'isbn',
1057
		'citation_doi' => 'identifier.value',
1058
	];
1059

1060 1
	if ( $context === 'section' ) {
1061
		global $post;
1062 1
		$section_information = get_section_information( $post->ID );
1063 1
		$metadata = section_information_to_schema( $section_information, $book_information );
1064 1
		foreach ( $map as $to => $from ) {
1065 1
			if ( strpos( $from, '.' ) ) {
1066 1
				$pieces = explode( '.', $from );
1067 1
				if ( isset( $metadata[ $pieces[0] ][ $pieces[1] ] ) && ! empty( $metadata[ $pieces[0] ][ $pieces[1] ] ) ) {
1068 1
					$tags[] = sprintf( '<meta name="%1$s" content="%2$s">', $to, $metadata[ $pieces[0] ][ $pieces[1] ] );
1069
				}
1070
			} else {
1071 1
				if ( isset( $metadata[ $from ] ) && ! empty( $metadata[ $from ] ) ) {
1072 1
					$tags[] = sprintf( '<meta name="%1$s" content="%2$s">', $to, $metadata[ $from ] );
1073
				}
1074
			}
1075
		}
1076 1
		if ( isset( $metadata['author'] ) ) {
1077 1
			foreach ( $metadata['author'] as $author ) {
1078 1
				$tags[] = sprintf( '<meta name="%1$s" content="%2$s">', 'citation_author', $author['name'] );
1079
			}
1080
		}
1081
	} else {
1082 1
		$metadata = book_information_to_schema( $book_information );
1083 1
		$tags[] = sprintf( '<meta name="%1$s" content="%2$s">', 'og:type', 'book' );
1084 1
		foreach ( $map as $to => $from ) {
1085 1
			if ( strpos( $from, '.' ) ) {
1086 1
				$pieces = explode( '.', $from );
1087 1
				if ( isset( $metadata[ $pieces[0] ][ $pieces[1] ] ) && ! empty( $metadata[ $pieces[0] ][ $pieces[1] ] ) ) {
1088 1
					$tags[] = sprintf( '<meta name="%1$s" content="%2$s">', $to, $metadata[ $pieces[0] ][ $pieces[1] ] );
1089
				}
1090
			} else {
1091 1
				if ( isset( $metadata[ $from ] ) && ! empty( $metadata[ $from ] ) ) {
1092 1
					$tags[] = sprintf( '<meta name="%1$s" content="%2$s">', $to, $metadata[ $from ] );
1093
				}
1094
			}
1095
		}
1096 1
		if ( isset( $metadata['author'] ) ) {
1097 1
			foreach ( $metadata['author'] as $author ) {
1098 1
				$tags[] = sprintf( '<meta name="%1$s" content="%2$s">', 'citation_author', $author['name'] );
1099
			}
1100
		}
1101
	}
1102 1
	echo implode( "\n", $tags );
1103
}
1104

1105
/**
1106
 * @see https://github.com/lumenlearning/candela-citation
1107
 * @see https://github.com/lumenlearning/candela-bombadil
1108
 *
1109
 * @since 5.8.1
1110
 *
1111
 * @return string
1112
 */
1113
function add_candela_citations( $content ) {
1114 1
	if ( is_file( WP_PLUGIN_DIR . '/candela-citation/candela-citation.php' ) ) {
1115 0
		if ( is_plugin_active_for_network( 'candela-citation/candela-citation.php' ) || is_plugin_active( 'candela-citation/candela-citation.php' ) ) {
1116

1117
			// Candela Citations, out-of-the-box, already works with exports using pb_append_front_matter_content,
1118
			// pb_append_chapter_content, and pb_append_back_matter_content filters. They also handle appending webbook
1119
			// chapters with the Bombadil Theme.
1120
			//
1121
			// For backwards compatibility, this function should only print Candela Citations when we are in a webbook chapter
1122
			// (that isn't Bombadil).
1123

1124 0
			$is_book = Book::isBook();
1125 0
			$is_not_admin = ( ! is_admin() );
1126 0
			$is_not_bombadil = ( wp_get_theme()->get_stylesheet() !== 'candela-bombadil' );
1127

1128 0
			if ( $is_book && $is_not_admin && $is_not_bombadil ) {
1129 0
				$post = get_post();
1130 0
				if ( $post ) {
1131 0
					$citation = \Candela\Citation::renderCitation( $post->ID );
1132 0
					if ( $citation ) {
1133
						$new_html = '
1134
			 <section class="citations-section" role="contentinfo">
1135
			 <h3>Candela Citations</h3>
1136
					 <div>
1137 0
						 <div id="citation-list-' . $post->ID . '">
1138 0
							 ' . $citation . '
1139
						 </div>
1140
					 </div>
1141
			 </section>';
1142 0
						$content .= $new_html;
1143
					}
1144
				}
1145
			}
1146
		}
1147
	}
1148 1
	return $content;
1149
}
1150

1151

1152
/**
1153
 * Return $option to use in get_option() for "is this book in the network catalog?"
1154
 *
1155
 * @return string
1156
 */
1157
function get_in_catalog_option() {
1158
	// Try to find Aldine
1159 1
	if ( defined( '\Aldine\Admin\BLOG_OPTION' ) ) {
1160 0
		return \Aldine\Admin\BLOG_OPTION;
1161
	} else {
1162
		// Fallback to old pressbooks-publisher value
1163 1
		return 'pressbooks_publisher_in_catalog';
1164
	}
1165
}

Read our documentation on viewing source code .

Loading