class-opengraph-image.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674
  1. <?php
  2. /**
  3. * WPSEO plugin file.
  4. *
  5. * @package WPSEO\Frontend
  6. */
  7. /**
  8. * Class WPSEO_OpenGraph_Image.
  9. */
  10. class WPSEO_OpenGraph_Image {
  11. /**
  12. * The image ID used when the image is external.
  13. *
  14. * @var string
  15. */
  16. const EXTERNAL_IMAGE_ID = '-1';
  17. /**
  18. * Holds the images that have been put out as OG image.
  19. *
  20. * @var array
  21. */
  22. protected $images = [];
  23. /**
  24. * Holds the WPSEO_OpenGraph instance, so we can call og_tag.
  25. *
  26. * @var WPSEO_OpenGraph
  27. */
  28. private $opengraph;
  29. /**
  30. * Image tags that we output for each image.
  31. *
  32. * @var array
  33. */
  34. private $image_tags = [
  35. 'width' => 'width',
  36. 'height' => 'height',
  37. 'mime-type' => 'type',
  38. ];
  39. /**
  40. * The parameters we have for Facebook images.
  41. *
  42. * @var array
  43. */
  44. private $image_params = [
  45. 'min_width' => 200,
  46. 'max_width' => 2000,
  47. 'min_height' => 200,
  48. 'max_height' => 2000,
  49. ];
  50. /**
  51. * Image types that are supported by OpenGraph.
  52. *
  53. * @var array
  54. */
  55. private $valid_image_types = [ 'image/jpeg', 'image/gif', 'image/png' ];
  56. /**
  57. * Image extensions that are supported by OpenGraph.
  58. *
  59. * @var array
  60. */
  61. private $valid_image_extensions = [ 'jpeg', 'jpg', 'gif', 'png' ];
  62. /**
  63. * Constructor.
  64. *
  65. * @param null|string $image Optional. The Image to use.
  66. * @param WPSEO_OpenGraph $opengraph Optional. The OpenGraph object.
  67. */
  68. public function __construct( $image = null, WPSEO_OpenGraph $opengraph = null ) {
  69. if ( $opengraph === null ) {
  70. global $wpseo_og;
  71. // Use the global if available.
  72. if ( empty( $wpseo_og ) ) {
  73. $wpseo_og = new WPSEO_OpenGraph();
  74. }
  75. $opengraph = $wpseo_og;
  76. }
  77. $this->opengraph = $opengraph;
  78. if ( ! empty( $image ) && is_string( $image ) ) {
  79. $this->add_image_by_url( $image );
  80. }
  81. if ( ! post_password_required() ) {
  82. $this->set_images();
  83. }
  84. }
  85. /**
  86. * Outputs the images.
  87. *
  88. * @return void
  89. */
  90. public function show() {
  91. foreach ( $this->get_images() as $image => $image_meta ) {
  92. $this->og_image_tag( $image );
  93. $this->show_image_meta( $image_meta );
  94. }
  95. }
  96. /**
  97. * Output the image metadata.
  98. *
  99. * @param array $image_meta Image meta data to output.
  100. *
  101. * @return void
  102. */
  103. private function show_image_meta( $image_meta ) {
  104. foreach ( $this->image_tags as $key => $value ) {
  105. if ( ! empty( $image_meta[ $key ] ) ) {
  106. $this->opengraph->og_tag( 'og:image:' . $key, $image_meta[ $key ] );
  107. }
  108. }
  109. }
  110. /**
  111. * Outputs an image tag based on whether it's https or not.
  112. *
  113. * @param string $image_url The image URL.
  114. *
  115. * @return void
  116. */
  117. private function og_image_tag( $image_url ) {
  118. $this->opengraph->og_tag( 'og:image', esc_url( $image_url ) );
  119. // Add secure URL if detected. Not all services implement this, so the regular one also needs to be rendered.
  120. if ( strpos( $image_url, 'https://' ) === 0 ) {
  121. $this->opengraph->og_tag( 'og:image:secure_url', esc_url( $image_url ) );
  122. }
  123. }
  124. /**
  125. * Return the images array.
  126. *
  127. * @return array The images.
  128. */
  129. public function get_images() {
  130. return $this->images;
  131. }
  132. /**
  133. * Check whether we have images or not.
  134. *
  135. * @return bool True if we have images, false if we don't.
  136. */
  137. public function has_images() {
  138. return ! empty( $this->images );
  139. }
  140. /**
  141. * Display an OpenGraph image tag.
  142. *
  143. * @param string|array $attachment Attachment array.
  144. *
  145. * @return void
  146. */
  147. public function add_image( $attachment ) {
  148. // In the past `add_image` accepted an image url, so leave this for backwards compatibility.
  149. if ( is_string( $attachment ) ) {
  150. $attachment = [ 'url' => $attachment ];
  151. }
  152. if ( ! is_array( $attachment ) || empty( $attachment['url'] ) ) {
  153. return;
  154. }
  155. // If the URL ends in `.svg`, we need to return.
  156. if ( ! $this->is_valid_image_url( $attachment['url'] ) ) {
  157. return;
  158. }
  159. /**
  160. * Filter: 'wpseo_opengraph_image' - Allow changing the OpenGraph image.
  161. *
  162. * @api string - The URL of the OpenGraph image.
  163. */
  164. $image_url = trim( apply_filters( 'wpseo_opengraph_image', $attachment['url'] ) );
  165. if ( empty( $image_url ) ) {
  166. return;
  167. }
  168. if ( WPSEO_Utils::is_url_relative( $image_url ) === true ) {
  169. $image_url = WPSEO_Image_Utils::get_relative_path( $image_url );
  170. }
  171. if ( array_key_exists( $image_url, $this->images ) ) {
  172. return;
  173. }
  174. $this->images[ $image_url ] = $attachment;
  175. }
  176. /**
  177. * Adds an image by ID if possible and by URL if the ID isn't present.
  178. *
  179. * @param string $image_id The image ID as set in the database.
  180. * @param string $image_url The saved URL for the image.
  181. * @param callable $on_save_id Function to call to save the ID if it needs to be saved.
  182. *
  183. * @return void
  184. */
  185. private function add_image_by_id_or_url( $image_id, $image_url, $on_save_id ) {
  186. switch ( $image_id ) {
  187. case self::EXTERNAL_IMAGE_ID:
  188. // Add image by URL, but skip attachment_to_id call. We already know this is an external image.
  189. $this->add_image( [ 'url' => $image_url ] );
  190. break;
  191. case '':
  192. // Add image by URL, try to save the ID afterwards. So we can use the ID the next time.
  193. $attachment_id = $this->add_image_by_url( $image_url );
  194. if ( $attachment_id !== null ) {
  195. call_user_func( $on_save_id, $attachment_id );
  196. }
  197. break;
  198. default:
  199. // Add the image by ID. This is our ideal scenario.
  200. $this->add_image_by_id( $image_id );
  201. break;
  202. }
  203. }
  204. /**
  205. * Saves the ID to the frontpage Open Graph image ID.
  206. *
  207. * @param string $attachment_id The ID to save.
  208. *
  209. * @return void
  210. */
  211. private function save_frontpage_image_id( $attachment_id ) {
  212. WPSEO_Options::set( 'og_frontpage_image_id', $attachment_id );
  213. }
  214. /**
  215. * If the frontpage image exists, call add_image.
  216. *
  217. * @return void
  218. */
  219. private function set_front_page_image() {
  220. if ( get_option( 'show_on_front' ) === 'page' ) {
  221. $this->set_user_defined_image();
  222. // Don't fall back to the frontpage image below, as that's not set for this situation, so we should fall back to the default image.
  223. return;
  224. }
  225. $frontpage_image_url = WPSEO_Options::get( 'og_frontpage_image' );
  226. $frontpage_image_id = WPSEO_Options::get( 'og_frontpage_image_id' );
  227. $this->add_image_by_id_or_url( $frontpage_image_id, $frontpage_image_url, [ $this, 'save_frontpage_image_id' ] );
  228. }
  229. /**
  230. * Get the images of the posts page.
  231. *
  232. * @return void
  233. */
  234. private function set_posts_page_image() {
  235. $post_id = get_option( 'page_for_posts' );
  236. $this->set_image_post_meta( $post_id );
  237. if ( $this->has_images() ) {
  238. return;
  239. }
  240. $this->set_featured_image( $post_id );
  241. }
  242. /**
  243. * Get the images of the singular post.
  244. *
  245. * @param null|int $post_id The post id to get the images for.
  246. *
  247. * @return void
  248. */
  249. private function set_singular_image( $post_id = null ) {
  250. if ( $post_id === null ) {
  251. $post_id = WPSEO_Frontend_Page_Type::get_simple_page_id();
  252. }
  253. $this->set_user_defined_image( $post_id );
  254. if ( $this->has_images() ) {
  255. return;
  256. }
  257. $this->add_first_usable_content_image( $post_id );
  258. }
  259. /**
  260. * Gets the user-defined image of the post.
  261. *
  262. * @param null|int $post_id The post id to get the images for.
  263. *
  264. * @return void
  265. */
  266. private function set_user_defined_image( $post_id = null ) {
  267. if ( $post_id === null ) {
  268. $post_id = WPSEO_Frontend_Page_Type::get_simple_page_id();
  269. }
  270. $this->set_image_post_meta( $post_id );
  271. if ( $this->has_images() ) {
  272. return;
  273. }
  274. $this->set_featured_image( $post_id );
  275. }
  276. /**
  277. * Saves the default image ID for Open Graph images to the database.
  278. *
  279. * @param string $attachment_id The ID to save.
  280. *
  281. * @return void
  282. */
  283. private function save_default_image_id( $attachment_id ) {
  284. WPSEO_Options::set( 'og_default_image_id', $attachment_id );
  285. }
  286. /**
  287. * Get default image and call add_image.
  288. *
  289. * @return void
  290. */
  291. private function maybe_set_default_image() {
  292. if ( $this->has_images() ) {
  293. return;
  294. }
  295. $default_image_url = WPSEO_Options::get( 'og_default_image', '' );
  296. $default_image_id = WPSEO_Options::get( 'og_default_image_id', '' );
  297. if ( $default_image_url === '' && $default_image_id === '' ) {
  298. return;
  299. }
  300. $this->add_image_by_id_or_url( $default_image_id, $default_image_url, [ $this, 'save_default_image_id' ] );
  301. }
  302. /**
  303. * Saves the Open Graph image meta to the database for the current post.
  304. *
  305. * @param string $attachment_id The ID to save.
  306. *
  307. * @return void
  308. */
  309. private function save_opengraph_image_id_meta( $attachment_id ) {
  310. $post_id = WPSEO_Frontend_Page_Type::get_simple_page_id();
  311. WPSEO_Meta::set_value( 'opengraph-image-id', (string) $attachment_id, $post_id );
  312. }
  313. /**
  314. * If opengraph-image is set, call add_image and return true.
  315. *
  316. * @param int $post_id Optional post ID to use.
  317. *
  318. * @return void
  319. */
  320. private function set_image_post_meta( $post_id = 0 ) {
  321. $image_id = WPSEO_Meta::get_value( 'opengraph-image-id', $post_id );
  322. $image_url = WPSEO_Meta::get_value( 'opengraph-image', $post_id );
  323. $this->add_image_by_id_or_url( $image_id, $image_url, [ $this, 'save_opengraph_image_id_meta' ] );
  324. }
  325. /**
  326. * Check if taxonomy has an image and add this image.
  327. *
  328. * @return void
  329. */
  330. private function set_taxonomy_image() {
  331. $image_url = WPSEO_Taxonomy_Meta::get_meta_without_term( 'opengraph-image' );
  332. $this->add_image_by_url( $image_url );
  333. }
  334. /**
  335. * If there is a featured image, check image size. If image size is correct, call add_image and return true.
  336. *
  337. * @param int $post_id The post ID.
  338. *
  339. * @return void
  340. */
  341. private function set_featured_image( $post_id ) {
  342. if ( has_post_thumbnail( $post_id ) ) {
  343. $attachment_id = get_post_thumbnail_id( $post_id );
  344. $this->add_image_by_id( $attachment_id );
  345. }
  346. }
  347. /**
  348. * If this is an attachment page, call add_image with the attachment.
  349. *
  350. * @return void
  351. */
  352. private function set_attachment_page_image() {
  353. $post_id = WPSEO_Frontend_Page_Type::get_simple_page_id();
  354. if ( wp_attachment_is_image( $post_id ) ) {
  355. $this->add_image_by_id( $post_id );
  356. }
  357. }
  358. /**
  359. * Adds an image based on a given URL, and attempts to be smart about it.
  360. *
  361. * @param string $url The given URL.
  362. *
  363. * @return null|number Returns the found attachment ID if it exists. Otherwise -1.
  364. * If the URL is empty we return null.
  365. */
  366. public function add_image_by_url( $url ) {
  367. if ( empty( $url ) ) {
  368. return null;
  369. }
  370. $attachment_id = WPSEO_Image_Utils::get_attachment_by_url( $url );
  371. if ( $attachment_id > 0 ) {
  372. $this->add_image_by_id( $attachment_id );
  373. return $attachment_id;
  374. }
  375. $this->add_image( [ 'url' => $url ] );
  376. return -1;
  377. }
  378. /**
  379. * Returns the overridden image size if it has been overridden.
  380. *
  381. * @return null|string The overridden image size or null.
  382. */
  383. protected function get_overridden_image_size() {
  384. /**
  385. * Filter: 'wpseo_opengraph_image_size' - Allow overriding the image size used
  386. * for OpenGraph sharing. If this filter is used, the defined size will always be
  387. * used for the og:image. The image will still be rejected if it is too small.
  388. *
  389. * Only use this filter if you manually want to determine the best image size
  390. * for the `og:image` tag.
  391. *
  392. * Use the `wpseo_image_sizes` filter if you want to use our logic. That filter
  393. * can be used to add an image size that needs to be taken into consideration
  394. * within our own logic.
  395. *
  396. * @api string $size Size string.
  397. */
  398. return apply_filters( 'wpseo_opengraph_image_size', null );
  399. }
  400. /**
  401. * Determines if the OpenGraph image size should overridden.
  402. *
  403. * @return bool Whether the size should be overridden.
  404. */
  405. protected function is_size_overridden() {
  406. return $this->get_overridden_image_size() !== null;
  407. }
  408. /**
  409. * Adds the possibility to short-circuit all the optimal variation logic with
  410. * your own size.
  411. *
  412. * @param int $attachment_id The attachment ID that is used.
  413. *
  414. * @return void
  415. */
  416. protected function get_overridden_image( $attachment_id ) {
  417. $attachment = WPSEO_Image_Utils::get_image( $attachment_id, $this->get_overridden_image_size() );
  418. if ( $attachment ) {
  419. $this->add_image( $attachment );
  420. }
  421. }
  422. /**
  423. * Adds an image to the list by attachment ID.
  424. *
  425. * @param int $attachment_id The attachment ID to add.
  426. *
  427. * @return void
  428. */
  429. public function add_image_by_id( $attachment_id ) {
  430. if ( ! $this->is_valid_attachment( $attachment_id ) ) {
  431. return;
  432. }
  433. if ( $this->is_size_overridden() ) {
  434. $this->get_overridden_image( $attachment_id );
  435. return;
  436. }
  437. $variations = WPSEO_Image_Utils::get_variations( $attachment_id );
  438. $variations = WPSEO_Image_Utils::filter_usable_dimensions( $this->image_params, $variations );
  439. $variations = WPSEO_Image_Utils::filter_usable_file_size( $variations );
  440. // If we are left without variations, there is no valid variation for this attachment.
  441. if ( empty( $variations ) ) {
  442. return;
  443. }
  444. // The variations are ordered so the first variations is by definition the best one.
  445. $attachment = $variations[0];
  446. if ( $attachment ) {
  447. $this->add_image( $attachment );
  448. }
  449. }
  450. /**
  451. * Sets the images based on the page type.
  452. *
  453. * @return void
  454. */
  455. protected function set_images() {
  456. /**
  457. * Filter: wpseo_add_opengraph_images - Allow developers to add images to the OpenGraph tags.
  458. *
  459. * @api WPSEO_OpenGraph_Image The current object.
  460. */
  461. do_action( 'wpseo_add_opengraph_images', $this );
  462. switch ( true ) {
  463. case is_front_page():
  464. $this->set_front_page_image();
  465. break;
  466. case is_home():
  467. $this->set_posts_page_image();
  468. break;
  469. case is_attachment():
  470. $this->set_attachment_page_image();
  471. break;
  472. case WPSEO_Frontend_Page_Type::is_simple_page():
  473. $this->set_singular_image();
  474. break;
  475. case is_category():
  476. case is_tag():
  477. case is_tax():
  478. $this->set_taxonomy_image();
  479. }
  480. /**
  481. * Filter: wpseo_add_opengraph_additional_images - Allows to add additional images to the OpenGraph tags.
  482. *
  483. * @api WPSEO_OpenGraph_Image The current object.
  484. */
  485. do_action( 'wpseo_add_opengraph_additional_images', $this );
  486. $this->maybe_set_default_image();
  487. }
  488. /**
  489. * Determines whether or not the wanted attachment is considered valid.
  490. *
  491. * @param int $attachment_id The attachment ID to get the attachment by.
  492. *
  493. * @return bool Whether or not the attachment is valid.
  494. */
  495. protected function is_valid_attachment( $attachment_id ) {
  496. $attachment = get_post_mime_type( $attachment_id );
  497. if ( $attachment === false ) {
  498. return false;
  499. }
  500. return $this->is_valid_image_type( $attachment );
  501. }
  502. /**
  503. * Determines whether the passed mime type is a valid image type.
  504. *
  505. * @param string $mime_type The detected mime type.
  506. *
  507. * @return bool Whether or not the attachment is a valid image type.
  508. */
  509. protected function is_valid_image_type( $mime_type ) {
  510. return in_array( $mime_type, $this->valid_image_types, true );
  511. }
  512. /**
  513. * Determines whether the passed URL is considered valid.
  514. *
  515. * @param string $url The URL to check.
  516. *
  517. * @return bool Whether or not the URL is a valid image.
  518. */
  519. protected function is_valid_image_url( $url ) {
  520. if ( ! is_string( $url ) ) {
  521. return false;
  522. }
  523. $image_extension = $this->get_extension_from_url( $url );
  524. $is_valid = in_array( $image_extension, $this->valid_image_extensions, true );
  525. /**
  526. * Filter: 'wpseo_opengraph_is_valid_image_url' - Allows extra validation for an image url.
  527. *
  528. * @api bool - Current validation result.
  529. *
  530. * @param string $url The image url to validate.
  531. */
  532. return apply_filters( 'wpseo_opengraph_is_valid_image_url', $is_valid, $url );
  533. }
  534. /**
  535. * Gets the image path from the passed URL.
  536. *
  537. * @param string $url The URL to get the path from.
  538. *
  539. * @return string The path of the image URL. Returns an empty string if URL parsing fails.
  540. */
  541. protected function get_image_url_path( $url ) {
  542. return (string) wp_parse_url( $url, PHP_URL_PATH );
  543. }
  544. /**
  545. * Determines the file extension of the passed URL.
  546. *
  547. * @param string $url The URL.
  548. *
  549. * @return string The extension.
  550. */
  551. protected function get_extension_from_url( $url ) {
  552. $extension = '';
  553. $path = $this->get_image_url_path( $url );
  554. if ( $path === '' ) {
  555. return $extension;
  556. }
  557. $parts = explode( '.', $path );
  558. if ( ! empty( $parts ) ) {
  559. $extension = end( $parts );
  560. }
  561. return $extension;
  562. }
  563. /**
  564. * Adds the first usable attachment image from the post content.
  565. *
  566. * @param int $post_id The post id.
  567. *
  568. * @return void
  569. */
  570. private function add_first_usable_content_image( $post_id ) {
  571. $image_url = WPSEO_Image_Utils::get_first_usable_content_image_for_post( $post_id );
  572. if ( $image_url === null || empty( $image_url ) ) {
  573. return;
  574. }
  575. $this->add_image( [ 'url' => $image_url ] );
  576. }
  577. }