class-opengraph-image.php 14 KB

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