Source: utils.php

  1. <?php
  2. /**
  3. * Utility functions
  4. *
  5. * @package distributor
  6. */
  7. namespace Distributor\Utils;
  8. use Distributor\DistributorPost;
  9. /**
  10. * Determine if this is a development install of Distributor.
  11. *
  12. * @since 2.0.0
  13. *
  14. * @return bool True if this is a development install, false otherwise.
  15. */
  16. function is_development_version() {
  17. return file_exists( DT_PLUGIN_PATH . 'composer.lock' );
  18. }
  19. /**
  20. * Determine if we are on VIP
  21. *
  22. * @since 1.0
  23. * @return boolean
  24. */
  25. function is_vip_com() {
  26. return ( defined( 'WPCOM_IS_VIP_ENV' ) && WPCOM_IS_VIP_ENV );
  27. }
  28. /**
  29. * Determine if Gutenberg is being used.
  30. *
  31. * This duplicates the check from `use_block_editor_for_post()` in WordPress
  32. * but removes the check for the `meta-box-loader` querystring parameter as
  33. * it is not required for Distributor.
  34. *
  35. * @since 1.2
  36. * @since 1.7 Update Gutenberg plugin sniff to avoid deprecated function.
  37. * Update Classic Editor sniff to account for mu-plugins.
  38. * @since 2.0 Duplicate the check from WordPress Core's `use_block_editor_for_post()`.
  39. *
  40. * @param int|WP_Post $post The post ID or object.
  41. * @return boolean Whether post is using the block editor/Gutenberg.
  42. */
  43. function is_using_gutenberg( $post ) {
  44. $post = get_post( $post );
  45. if ( ! $post ) {
  46. return false;
  47. }
  48. // The posts page can't be edited in the block editor.
  49. if ( absint( get_option( 'page_for_posts' ) ) === $post->ID && empty( $post->post_content ) ) {
  50. return false;
  51. }
  52. // Make sure this post type supports Gutenberg
  53. $use_block_editor = dt_use_block_editor_for_post_type( $post->post_type );
  54. /** This filter is documented in wp-admin/includes/post.php */
  55. return apply_filters( 'use_block_editor_for_post', $use_block_editor, $post );
  56. }
  57. /**
  58. * Get Distributor settings with defaults
  59. *
  60. * @since 1.0
  61. * @return array
  62. */
  63. function get_settings() {
  64. $defaults = [
  65. 'override_author_byline' => true,
  66. 'media_handling' => 'featured',
  67. 'email' => '',
  68. 'license_key' => '',
  69. 'valid_license' => null,
  70. ];
  71. $settings = get_option( 'dt_settings', [] );
  72. $settings = wp_parse_args( $settings, $defaults );
  73. return $settings;
  74. }
  75. /**
  76. * Get Distributor network settings with defaults
  77. *
  78. * @since 1.2
  79. * @return array
  80. */
  81. function get_network_settings() {
  82. $defaults = [
  83. 'email' => '',
  84. 'license_key' => '',
  85. 'valid_license' => null,
  86. ];
  87. $settings = get_site_option( 'dt_settings', [] );
  88. $settings = wp_parse_args( $settings, $defaults );
  89. return $settings;
  90. }
  91. /**
  92. * Hit license API to see if key/email is valid
  93. *
  94. * @param string $email Email address.
  95. * @param string $license_key License key.
  96. * @since 1.2
  97. * @return bool
  98. */
  99. function check_license_key( $email, $license_key ) {
  100. $request = wp_remote_post(
  101. 'https://distributorplugin.com/wp-json/distributor-theme/v1/validate-license',
  102. [
  103. // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout
  104. 'timeout' => 10,
  105. 'headers' => [
  106. 'X-Distributor-Version' => DT_VERSION,
  107. ],
  108. 'body' => [
  109. 'license_key' => $license_key,
  110. 'email' => $email,
  111. ],
  112. ]
  113. );
  114. if ( is_wp_error( $request ) ) {
  115. return false;
  116. }
  117. if ( 200 === wp_remote_retrieve_response_code( $request ) ) {
  118. return true;
  119. }
  120. return false;
  121. }
  122. /**
  123. * Determine if plugin is in debug mode or not
  124. *
  125. * @since 1.0
  126. * @return boolean
  127. */
  128. function is_dt_debug() {
  129. return ( defined( 'DISTRIBUTOR_DEBUG' ) && DISTRIBUTOR_DEBUG );
  130. }
  131. /**
  132. * Given an array of meta, set meta to another post.
  133. *
  134. * Don't copy in excluded (Distributor) meta.
  135. *
  136. * @param int $post_id Post ID.
  137. * @param array $meta Array of meta as key => value
  138. */
  139. function set_meta( $post_id, $meta ) {
  140. /**
  141. * Fires before Distributor sets post meta.
  142. *
  143. * All sent meta is included in the `$meta` array, including excluded keys.
  144. * Any excluded keys returned in this filter will be subsequently removed
  145. * from the saved meta data.
  146. *
  147. * @since 2.0.0
  148. * @hook dt_before_set_meta
  149. *
  150. * @param {array} $meta All received meta for the post
  151. * @param {int} $post_id Post ID
  152. */
  153. $meta = apply_filters( 'dt_before_set_meta', $meta, $post_id );
  154. $existing_meta = get_post_meta( $post_id );
  155. $excluded_meta = excluded_meta();
  156. foreach ( $meta as $meta_key => $meta_values ) {
  157. if ( in_array( $meta_key, $excluded_meta, true ) ) {
  158. continue;
  159. }
  160. foreach ( (array) $meta_values as $meta_placement => $meta_value ) {
  161. $has_prev_value = isset( $existing_meta[ $meta_key ] )
  162. && is_array( $existing_meta[ $meta_key ] )
  163. && array_key_exists( $meta_placement, $existing_meta[ $meta_key ] )
  164. ? true : false;
  165. if ( $has_prev_value ) {
  166. $prev_value = maybe_unserialize( $existing_meta[ $meta_key ][ $meta_placement ] );
  167. }
  168. if ( ! is_array( $meta_value ) ) {
  169. $meta_value = maybe_unserialize( $meta_value );
  170. }
  171. if ( $has_prev_value ) {
  172. update_post_meta( $post_id, wp_slash( $meta_key ), wp_slash( $meta_value ), $prev_value );
  173. } else {
  174. add_post_meta( $post_id, wp_slash( $meta_key ), wp_slash( $meta_value ) );
  175. }
  176. }
  177. }
  178. /**
  179. * Fires after Distributor sets post meta.
  180. *
  181. * Note: All sent meta is included in the `$meta` array, including excluded keys.
  182. * Take care to continue to filter out excluded keys in any further meta setting.
  183. *
  184. * @since 1.3.8
  185. * @hook dt_after_set_meta
  186. * @tutorial snippets
  187. *
  188. * @param {array} $meta All received meta for the post
  189. * @param {array} $existing_meta Existing meta for the post
  190. * @param {int} $post_id Post ID
  191. */
  192. do_action( 'dt_after_set_meta', $meta, $existing_meta, $post_id );
  193. }
  194. /**
  195. * Get post types available for pulling.
  196. *
  197. * This will compare the public post types from a remote site
  198. * against the public post types from the origin site and return
  199. * an array of post types supported on both.
  200. *
  201. * @param \Distributor\Connection $connection Connection object
  202. * @param string $type Connection type
  203. * @since 1.3
  204. * @return array
  205. */
  206. function available_pull_post_types( $connection, $type ) {
  207. $post_types = array();
  208. $local_post_types = array();
  209. $remote_post_types = $connection->get_post_types();
  210. $distributable_post_types = distributable_post_types();
  211. // Return empty array, if the source site is not distributing any post type.
  212. if ( empty( $remote_post_types ) || is_wp_error( $remote_post_types ) ) {
  213. return [];
  214. }
  215. $local_post_types = array_diff_key( get_post_types( [ 'public' => true ], 'objects' ), array_flip( [ 'attachment', 'dt_ext_connection', 'dt_subscription' ] ) );
  216. $available_post_types = array_intersect_key( $remote_post_types, $local_post_types );
  217. if ( ! empty( $available_post_types ) ) {
  218. foreach ( $available_post_types as $post_type ) {
  219. $post_types[] = array(
  220. 'name' => 'external' === $type ? $post_type['name'] : $post_type->label,
  221. 'slug' => 'external' === $type ? $post_type['slug'] : $post_type->name,
  222. );
  223. }
  224. }
  225. /**
  226. * Filter the post types that should be available for pull.
  227. *
  228. * Helpful for sites that want to pull custom post type content from another site into a different existing post type on the receiving end.
  229. *
  230. * @since 1.3.5
  231. * @hook dt_available_pull_post_types
  232. *
  233. * @param {array} $post_types Post types available for pull with name and slug.
  234. * @param {array} $remote_post_types Post types available from the remote connection.
  235. * @param {array} $local_post_types Post types registered as public on the local site.
  236. * @param {Connection} $connection Distributor connection object.
  237. * @param {string} $type Distributor connection type.
  238. *
  239. * @return {array} Post types available for pull with name and slug.
  240. */
  241. $pull_post_types = apply_filters( 'dt_available_pull_post_types', $post_types, $remote_post_types, $local_post_types, $connection, $type );
  242. if ( ! empty( $pull_post_types ) ) {
  243. $post_types = array();
  244. foreach ( $pull_post_types as $post_type ) {
  245. if ( in_array( $post_type['slug'], $distributable_post_types, true ) ) {
  246. $post_types[] = $post_type;
  247. }
  248. }
  249. }
  250. return $post_types;
  251. }
  252. /**
  253. * Return post types that are allowed to be distributed
  254. *
  255. * @param string $output Optional. The type of output to return.
  256. * Accepts post type 'names' or 'objects'. Default 'names'.
  257. *
  258. * @since 1.0
  259. * @since 1.7.0 $output parameter introduced.
  260. * @return array
  261. */
  262. function distributable_post_types( $output = 'names' ) {
  263. $post_types = array_filter( get_post_types(), 'is_post_type_viewable' );
  264. $exclude_post_types = [
  265. 'attachment',
  266. 'dt_ext_connection',
  267. 'dt_subscription',
  268. ];
  269. foreach ( $exclude_post_types as $exclude_post_type ) {
  270. unset( $post_types[ $exclude_post_type ] );
  271. }
  272. /**
  273. * Filter post types that are distributable.
  274. *
  275. * @since 1.0.0
  276. * @hook distributable_post_types
  277. * @tutorial snippets
  278. *
  279. * @param {array} Post types that are distributable.
  280. *
  281. * @return {array} Post types that are distributable.
  282. */
  283. $post_types = apply_filters( 'distributable_post_types', $post_types );
  284. // Remove unregistered post types added via the filter.
  285. $post_types = array_filter( $post_types, 'post_type_exists' );
  286. if ( 'objects' === $output ) {
  287. // Convert to objects.
  288. $post_types = array_map( 'get_post_type_object', $post_types );
  289. }
  290. return $post_types;
  291. }
  292. /**
  293. * Return post statuses that are allowed to be distributed.
  294. *
  295. * @since 1.0
  296. * @return array
  297. */
  298. function distributable_post_statuses() {
  299. /**
  300. * Filter the post statuses that are allowed to be distributed.
  301. *
  302. * By default only published posts can be distributed.
  303. *
  304. * @hook dt_distributable_post_statuses
  305. *
  306. * @param {array} $statuses Post statuses that are distributable. Default `publish`.
  307. *
  308. * @return {array} Post statuses that are distributable.
  309. */
  310. return apply_filters( 'dt_distributable_post_statuses', array( 'publish' ) );
  311. }
  312. /**
  313. * Returns list of excluded meta keys
  314. *
  315. * @since 1.2
  316. * @deprecated 1.9.0 Use excluded_meta()
  317. * @return array
  318. */
  319. function blacklisted_meta() {
  320. _deprecated_function( __FUNCTION__, '1.9.0', '\Distributor\Utils\excluded_meta()' );
  321. return excluded_meta();
  322. }
  323. /**
  324. * Returns list of excluded meta keys
  325. *
  326. * @since 1.9.0
  327. * @return array
  328. */
  329. function excluded_meta() {
  330. /**
  331. * Filter meta keys that are excluded from distribution.
  332. *
  333. * @since 1.0.0
  334. * @deprecated 1.9.0 Use dt_excluded_meta
  335. *
  336. * @param array $meta_keys Excluded meta keys.
  337. *
  338. * @return array Excluded meta keys.
  339. */
  340. $excluded_meta = apply_filters_deprecated(
  341. 'dt_blacklisted_meta',
  342. [
  343. [
  344. 'classic-editor-remember',
  345. 'dt_unlinked',
  346. 'dt_syndicate_time',
  347. 'dt_subscriptions',
  348. 'dt_subscription_update',
  349. 'dt_subscription_signature',
  350. 'dt_original_post_url',
  351. 'dt_original_post_id',
  352. 'dt_original_blog_id',
  353. 'dt_connection_map',
  354. '_wp_old_slug',
  355. '_wp_old_date',
  356. '_wp_attachment_metadata',
  357. '_wp_attached_file',
  358. '_edit_lock',
  359. '_edit_last',
  360. ],
  361. ],
  362. '1.9.0',
  363. 'dt_excluded_meta',
  364. __( 'Please consider writing more inclusive code.', 'distributor' )
  365. );
  366. /**
  367. * Filter meta keys that are excluded from distribution.
  368. *
  369. * @since 1.9.0
  370. * @hook dt_excluded_meta
  371. * @tutorial snippets
  372. *
  373. * @param {array} $meta_keys Excluded meta keys. Default `dt_unlinked, dt_connection_map, dt_subscription_update, dt_subscriptions, dt_subscription_signature, dt_original_post_id, dt_original_post_url, dt_original_blog_id, dt_syndicate_time, _wp_attached_file, _wp_attachment_metadata, _edit_lock, _edit_last, _wp_old_slug, _wp_old_date`.
  374. *
  375. * @return {array} Excluded meta keys.
  376. */
  377. return apply_filters( 'dt_excluded_meta', $excluded_meta );
  378. }
  379. /**
  380. * Prepare meta for consumption
  381. *
  382. * @param int $post_id Post ID.
  383. * @since 1.0
  384. * @return array
  385. */
  386. function prepare_meta( $post_id ) {
  387. update_postmeta_cache( array( $post_id ) );
  388. $meta = get_post_meta( $post_id );
  389. if ( false === $meta ) {
  390. return array();
  391. }
  392. $meta = is_array( $meta ) ? $meta : array();
  393. $prepared_meta = array();
  394. $excluded_meta = excluded_meta();
  395. // Transfer all meta
  396. foreach ( $meta as $meta_key => $meta_array ) {
  397. foreach ( $meta_array as $meta_value ) {
  398. if ( ! in_array( $meta_key, $excluded_meta, true ) ) {
  399. $meta_value = maybe_unserialize( $meta_value );
  400. /**
  401. * Filter whether to sync meta.
  402. *
  403. * @hook dt_sync_meta
  404. *
  405. * @param {bool} $sync_meta Whether to sync meta. Default `true`.
  406. * @param {string} $meta_key The meta key.
  407. * @param {mixed} $meta_value The meta value.
  408. * @param {int} $post_id The post ID.
  409. *
  410. * @return {bool} Whether to sync meta.
  411. */
  412. if ( false === apply_filters( 'dt_sync_meta', true, $meta_key, $meta_value, $post_id ) ) {
  413. continue;
  414. }
  415. $prepared_meta[ $meta_key ][] = $meta_value;
  416. }
  417. }
  418. }
  419. /**
  420. * Filter prepared meta for consumption.
  421. *
  422. * Modify meta data before it is sent for consumption by a distributed
  423. * post. The prepared meta data should not include any excluded meta.
  424. * see `excluded_meta()`.
  425. *
  426. * @since 2.0.0
  427. * @hook dt_prepared_meta
  428. *
  429. * @param {array} $prepared_meta Prepared meta.
  430. * @param {int} $post_id Post ID.
  431. *
  432. * @return {array} Prepared meta.
  433. */
  434. $prepared_meta = apply_filters( 'dt_prepared_meta', $prepared_meta, $post_id );
  435. return $prepared_meta;
  436. }
  437. /**
  438. * Format media items for consumption
  439. *
  440. * @param int $post_id Post ID.
  441. * @since 1.0
  442. * @return array
  443. */
  444. function prepare_media( $post_id ) {
  445. $dt_post = new DistributorPost( $post_id );
  446. if ( ! $dt_post ) {
  447. return array();
  448. }
  449. return $dt_post->get_media();
  450. }
  451. /**
  452. * Format taxonomy terms for consumption
  453. *
  454. * @since 1.0
  455. *
  456. * @param int $post_id Post ID.
  457. * @param array $args Taxonomy query arguments. See get_taxonomies().
  458. * @return array[] Array of taxonomy terms.
  459. */
  460. function prepare_taxonomy_terms( $post_id, $args = array() ) {
  461. $post = get_post( $post_id );
  462. if ( ! $post ) {
  463. return array();
  464. }
  465. // Warm the term cache for the post.
  466. update_object_term_cache( array( $post->ID ), $post->post_type );
  467. if ( empty( $args ) ) {
  468. $args = array( 'publicly_queryable' => true );
  469. }
  470. $taxonomy_terms = [];
  471. $taxonomies = get_taxonomies( $args );
  472. /**
  473. * Filters the taxonomies that should be synced.
  474. *
  475. * @since 1.0
  476. * @hook dt_syncable_taxonomies
  477. *
  478. * @param {array} $taxonomies Associative array list of taxonomies supported by current post in the format of `$taxonomy => $terms`.
  479. * @param {WP_Post} $post The post object.
  480. *
  481. * @return {array} Associative array list of taxonomies supported by current post in the format of `$taxonomy => $terms`.
  482. */
  483. $taxonomies = apply_filters( 'dt_syncable_taxonomies', $taxonomies, $post );
  484. foreach ( $taxonomies as $taxonomy ) {
  485. $taxonomy_terms[ $taxonomy ] = wp_get_object_terms( $post_id, $taxonomy );
  486. }
  487. /**
  488. * Filters the taxonomy terms for consumption.
  489. *
  490. * Modify taxonomies and terms prior to distribution. The array should be
  491. * keyed by taxonomy. The returned data by filters should only return
  492. * taxonomies permitted for distribution. See the `dt_syncable_taxonomies` hook.
  493. *
  494. * @since 2.0.0
  495. * @hook dt_prepared_taxonomy_terms
  496. *
  497. * @param {array} $taxonomy_terms Associative array of terms keyed by taxonomy.
  498. * @param {int} $post_id Post ID.
  499. *
  500. * @param {array} $args Modified array of terms keyed by taxonomy.
  501. */
  502. $taxonomy_terms = apply_filters( 'dt_prepared_taxonomy_terms', $taxonomy_terms, $post_id );
  503. return $taxonomy_terms;
  504. }
  505. /**
  506. * Given an array of terms by taxonomy, set those terms to another post. This function will cleverly merge
  507. * terms into the post and create terms that don't exist.
  508. *
  509. * @param int $post_id Post ID.
  510. * @param array $taxonomy_terms Array with taxonomy as key and array of terms as values.
  511. * @since 1.0
  512. */
  513. function set_taxonomy_terms( $post_id, $taxonomy_terms ) {
  514. // Now let's add the taxonomy/terms to syndicated post
  515. foreach ( $taxonomy_terms as $taxonomy => $terms ) {
  516. // Continue if taxonomy doesn't exist
  517. if ( ! taxonomy_exists( $taxonomy ) ) {
  518. continue;
  519. }
  520. $term_ids = [];
  521. $term_id_mapping = [];
  522. foreach ( $terms as $term_array ) {
  523. if ( ! is_array( $term_array ) ) {
  524. $term_array = (array) $term_array;
  525. }
  526. $term = get_term_by( 'slug', $term_array['slug'], $taxonomy );
  527. // Create terms on remote site if they don't exist
  528. /**
  529. * Filter whether missing terms should be created.
  530. *
  531. * @since 1.0.0
  532. * @hook dt_create_missing_terms
  533. *
  534. * @param {bool} true Whether missing terms should be created. Default `true`.
  535. * @param {string} $taxonomy The taxonomy name.
  536. * @param {array} $term_array Term data.
  537. * @param {WP_Term|array|false} $term `WP_Term` object or `array` if found, `false` if not.
  538. *
  539. * @return {bool} Whether missing terms should be created.
  540. */
  541. $create_missing_terms = apply_filters( 'dt_create_missing_terms', true, $taxonomy, $term_array, $term );
  542. if ( empty( $term ) ) {
  543. // Bail if terms shouldn't be created
  544. if ( false === $create_missing_terms ) {
  545. continue;
  546. }
  547. $term = wp_insert_term(
  548. $term_array['name'],
  549. $taxonomy,
  550. [
  551. 'slug' => $term_array['slug'],
  552. 'description' => $term_array['description'],
  553. ]
  554. );
  555. if ( ! is_wp_error( $term ) ) {
  556. $term_id_mapping[ $term_array['term_id'] ] = $term['term_id'];
  557. $term_ids[] = $term['term_id'];
  558. }
  559. } else {
  560. $term_id_mapping[ $term_array['term_id'] ] = $term->term_id;
  561. $term_ids[] = $term->term_id;
  562. }
  563. }
  564. // Handle hierarchical terms if they exist
  565. /**
  566. * Filter whether term hierarchy should be updated.
  567. *
  568. * @since 1.0.0
  569. * @hook dt_update_term_hierarchy
  570. *
  571. * @param {bool} true Whether term hierarchy should be updated. Default `true`.
  572. * @param {string} $taxonomy The taxonomy slug for the current term.
  573. *
  574. * @return {bool} Whether term hierarchy should be updated.
  575. */
  576. $update_term_hierarchy = apply_filters( 'dt_update_term_hierarchy', true, $taxonomy );
  577. if ( ! empty( $update_term_hierarchy ) ) {
  578. foreach ( $terms as $term_array ) {
  579. if ( ! is_array( $term_array ) ) {
  580. $term_array = (array) $term_array;
  581. }
  582. if ( empty( $term_array['parent'] ) ) {
  583. $term = wp_update_term(
  584. $term_id_mapping[ $term_array['term_id'] ],
  585. $taxonomy,
  586. [
  587. 'parent' => '',
  588. ]
  589. );
  590. } elseif ( isset( $term_id_mapping[ $term_array['parent'] ] ) ) {
  591. $term = wp_update_term(
  592. $term_id_mapping[ $term_array['term_id'] ],
  593. $taxonomy,
  594. [
  595. 'parent' => $term_id_mapping[ $term_array['parent'] ],
  596. ]
  597. );
  598. }
  599. }
  600. }
  601. wp_set_object_terms( $post_id, $term_ids, $taxonomy );
  602. }
  603. }
  604. /**
  605. * Given an array of media, set the media to a new post. This function will cleverly merge media into the
  606. * new post deleting duplicates. Meta and featured image information for each image will be copied as well.
  607. *
  608. * @param int $post_id Post ID.
  609. * @param array $media Array of media posts.
  610. * @param array $args Additional args for set_media.
  611. * @since 1.0
  612. */
  613. function set_media( $post_id, $media, $args = [] ) {
  614. $settings = get_settings(); // phpcs:ignore
  615. $current_media_posts = get_attached_media( get_allowed_mime_types(), $post_id );
  616. $current_media = [];
  617. $args = wp_parse_args(
  618. $args,
  619. [
  620. 'use_filesystem' => false,
  621. ]
  622. );
  623. /**
  624. * Allow filtering of the set_media args.
  625. *
  626. * @since 1.6.0
  627. * @hook dt_set_media_args
  628. *
  629. * @param {array} $args List of args.
  630. * @param {int} $post_id Post ID.
  631. * @param {array} $media Array of media posts.
  632. *
  633. * @return {array} set_media args.
  634. */
  635. $args = apply_filters( 'dt_set_media_args', $args, $post_id, $media );
  636. // Create mapping so we don't create duplicates
  637. foreach ( $current_media_posts as $media_post ) {
  638. $original = get_post_meta( $media_post->ID, 'dt_original_media_url', true );
  639. $current_media[ $original ] = $media_post->ID;
  640. }
  641. $found_featured_image = false;
  642. // If we only want to process the featured image, remove all other media
  643. if ( 'featured' === $settings['media_handling'] ) {
  644. $featured_keys = wp_list_pluck( $media, 'featured' );
  645. // Note: this is not a strict search because of issues with typecasting in some setups
  646. $featured_key = array_search( true, $featured_keys ); // @codingStandardsIgnoreLine Ignore strict search requirement.
  647. $media = ( false !== $featured_key ) ? array( $media[ $featured_key ] ) : array();
  648. }
  649. foreach ( $media as $media_item ) {
  650. $args['source_file'] = $media_item['source_file'];
  651. // Delete duplicate if it exists (unless filter says otherwise)
  652. /**
  653. * Filter whether media should be deleted and replaced if it already exists.
  654. *
  655. * @since 1.0.0
  656. * @hook dt_sync_media_delete_and_replace
  657. *
  658. * @param {bool} true Whether pre-existing media should be deleted and replaced. Default `true`.
  659. * @param {int} $post_id The post ID.
  660. *
  661. * @return {bool} Whether pre-existing media should be deleted and replaced.
  662. */
  663. if ( apply_filters( 'dt_sync_media_delete_and_replace', true, $post_id ) ) {
  664. if ( ! empty( $current_media[ $media_item['source_url'] ] ) ) {
  665. wp_delete_attachment( $current_media[ $media_item['source_url'] ], true );
  666. }
  667. $image_id = process_media( $media_item['source_url'], $post_id, $args );
  668. } else {
  669. if ( ! empty( $current_media[ $media_item['source_url'] ] ) ) {
  670. $image_id = $current_media[ $media_item['source_url'] ];
  671. } else {
  672. $image_id = process_media( $media_item['source_url'], $post_id, $args );
  673. }
  674. }
  675. // Exit if the image ID is not valid.
  676. if ( ! $image_id ) {
  677. continue;
  678. }
  679. update_post_meta( $image_id, 'dt_original_media_url', $media_item['source_url'] );
  680. update_post_meta( $image_id, 'dt_original_media_id', $media_item['id'] );
  681. if ( $media_item['featured'] ) {
  682. $found_featured_image = true;
  683. set_post_thumbnail( $post_id, $image_id );
  684. }
  685. // Transfer all meta
  686. if ( isset( $media_item['meta'] ) ) {
  687. set_meta( $image_id, $media_item['meta'] );
  688. }
  689. // Transfer post properties
  690. wp_update_post(
  691. [
  692. 'ID' => $image_id,
  693. 'post_title' => $media_item['title'],
  694. 'post_content' => $media_item['description']['raw'],
  695. 'post_excerpt' => $media_item['caption']['raw'],
  696. ]
  697. );
  698. }
  699. if ( ! $found_featured_image ) {
  700. delete_post_meta( $post_id, '_thumbnail_id' );
  701. }
  702. }
  703. /**
  704. * This is a helper function for transporting/formatting data about a media post
  705. *
  706. * @param \WP_Post $media_post Media post.
  707. * @param int $post_id Post ID.
  708. * @since 1.0
  709. * @return array
  710. */
  711. function format_media_post( $media_post, $post_id = 0 ) {
  712. $media_item = array(
  713. 'id' => $media_post->ID,
  714. 'title' => $media_post->post_title,
  715. );
  716. $media_item['featured'] = false;
  717. if ( $post_id && (int) get_post_thumbnail_id( $post_id ) === $media_post->ID ) {
  718. $media_item['featured'] = true;
  719. }
  720. $media_item['description'] = array(
  721. 'raw' => $media_post->post_content,
  722. 'rendered' => get_processed_content( $media_post->post_content ),
  723. );
  724. $media_item['caption'] = array(
  725. 'raw' => $media_post->post_excerpt,
  726. );
  727. $media_item['alt_text'] = get_post_meta( $media_post->ID, '_wp_attachment_image_alt', true );
  728. $media_item['media_type'] = wp_attachment_is_image( $media_post->ID ) ? 'image' : 'file';
  729. $media_item['mime_type'] = $media_post->post_mime_type;
  730. /**
  731. * Filter media details retrieved by `wp_get_attachment_metadata()`.
  732. *
  733. * @hook dt_get_media_details
  734. *
  735. * @param {array|false} $metadata Array of media metadata. `false` on failure.
  736. * @param {int} $media_post->ID The media post ID.
  737. *
  738. * @return {array} Array of media metadata.
  739. */
  740. $media_item['media_details'] = apply_filters( 'dt_get_media_details', wp_get_attachment_metadata( $media_post->ID ), $media_post->ID );
  741. $media_item['post'] = $media_post->post_parent;
  742. $media_item['source_url'] = wp_get_attachment_url( $media_post->ID );
  743. $media_item['source_file'] = get_attached_file( $media_post->ID );
  744. $media_item['meta'] = \Distributor\Utils\prepare_meta( $media_post->ID );
  745. /**
  746. * Filter formatted media item.
  747. *
  748. * @hook dt_media_item_formatted
  749. *
  750. * @param {array} $media_item Array of media item details.
  751. * @param {int} $media_post->ID The media post ID.
  752. *
  753. * @return {array} Array of media item details.
  754. */
  755. return apply_filters( 'dt_media_item_formatted', $media_item, $media_post->ID );
  756. }
  757. /**
  758. * Simple function for sideloading media and returning the media id
  759. *
  760. * @param string $url URL of media.
  761. * @param int $post_id Post ID that the media will be assigned to.
  762. * @param array $args Additional args for process_media.
  763. * @since 1.0
  764. * @return int|bool
  765. */
  766. function process_media( $url, $post_id, $args = [] ) {
  767. global $wp_filesystem;
  768. $args = wp_parse_args(
  769. $args,
  770. [
  771. 'use_filesystem' => false,
  772. 'source_file' => '',
  773. ]
  774. );
  775. /**
  776. * Allow filtering of the process_media args.
  777. *
  778. * @since 1.6.0
  779. * @hook dt_process_media_args
  780. *
  781. * @param array $args List of args.
  782. * @param string $url URL of media.
  783. * @param int $post_id Post ID.
  784. *
  785. * @return array Process media arguments.
  786. */
  787. $args = apply_filters( 'dt_process_media_args', $args, $url, $post_id );
  788. /**
  789. * Filter allowed media extensions to be processed
  790. *
  791. * @since 1.3.7
  792. * @hook dt_allowed_media_extensions
  793. *
  794. * @param {array} $allowed_extensions Allowed extensions array.
  795. * @param {string} $url Media url.
  796. * @param {int} $post_id Post ID.
  797. *
  798. * @return {array} Media extensions to be processed.
  799. */
  800. $allowed_extensions = apply_filters( 'dt_allowed_media_extensions', array( 'jpg', 'jpeg', 'jpe', 'gif', 'png' ), $url, $post_id );
  801. preg_match( '/[^\?]+\.(' . implode( '|', $allowed_extensions ) . ')\b/i', $url, $matches );
  802. if ( ! $matches ) {
  803. $media_name = null;
  804. } else {
  805. $media_name = basename( $matches[0] );
  806. }
  807. /**
  808. * Filter name of the processing media.
  809. *
  810. * @since 1.3.7
  811. * @hook dt_media_processing_filename
  812. *
  813. * @param {string} $media_name Filename of the media being processed.
  814. * @param {string} $url Media url.
  815. * @param {int} $post_id Post ID.
  816. *
  817. * @return {string} Filename of the media being processed.
  818. */
  819. $media_name = apply_filters( 'dt_media_processing_filename', $media_name, $url, $post_id );
  820. if ( is_null( $media_name ) ) {
  821. return false;
  822. }
  823. $file_array = array();
  824. $file_array['name'] = $media_name;
  825. require_once ABSPATH . 'wp-admin/includes/image.php';
  826. require_once ABSPATH . 'wp-admin/includes/file.php';
  827. require_once ABSPATH . 'wp-admin/includes/media.php';
  828. $download_url = true;
  829. $source_file = false;
  830. $save_source_file_path = false;
  831. if ( $args['use_filesystem'] && isset( $args['source_file'] ) && ! empty( $args['source_file'] ) ) {
  832. $source_file = $args['source_file'];
  833. if ( ! is_a( $wp_filesystem, 'WP_Filesystem_Base' ) ) {
  834. $credentials = request_filesystem_credentials( site_url() );
  835. wp_filesystem( $credentials );
  836. }
  837. // Copy the source file so we don't mess with the original file.
  838. if ( $wp_filesystem->exists( $source_file ) ) {
  839. $temp_name = wp_tempnam( $source_file );
  840. $copied = $wp_filesystem->copy( $source_file, $temp_name, true );
  841. if ( $copied ) {
  842. /**
  843. * Allow filtering whether to save the source file path.
  844. *
  845. * @since 1.6.0
  846. * @hook dt_process_media_save_source_file_path
  847. *
  848. * @param {boolean} $save_file Whether to save the source file path. Default `false`.
  849. *
  850. * @return {boolean} Whether to save the source file path or not.
  851. */
  852. $save_source_file_path = apply_filters( 'dt_process_media_save_source_file_path', false );
  853. $file_array['tmp_name'] = $temp_name;
  854. $download_url = false;
  855. }
  856. }
  857. }
  858. // Default for external or if a local file copy failed.
  859. if ( $download_url ) {
  860. // Set the scheme to http: if a relative URL is specified.
  861. if ( str_starts_with( $url, '//' ) ) {
  862. $url = 'http:' . $url;
  863. }
  864. // Allows to pull media from local IP addresses
  865. // Uses a "magic number" for priority so we only unhook our call, just in case.
  866. add_filter( 'http_request_host_is_external', '__return_true', 88 );
  867. // Download file to temp location.
  868. $file_array['tmp_name'] = download_url( $url );
  869. remove_filter( 'http_request_host_is_external', '__return_true', 88 );
  870. }
  871. // If error storing temporarily, return the error.
  872. if ( is_wp_error( $file_array['tmp_name'] ) ) {
  873. // Distributor is in debug mode, display the issue, could be storage related.
  874. if ( is_dt_debug() ) {
  875. error_log( sprintf( 'Distributor: %s', $file_array['tmp_name']->get_error_message() ) ); // @codingStandardsIgnoreLine
  876. set_media_errors( $post_id, $file_array['tmp_name']->get_error_message() );
  877. }
  878. return false;
  879. }
  880. // Do the validation and storage stuff.
  881. $result = media_handle_sideload( $file_array, $post_id );
  882. if ( is_wp_error( $result ) ) {
  883. // Distributor is in debug mode, display the issue, could be storage related.
  884. if ( is_dt_debug() ) {
  885. error_log( sprintf( 'Distributor: %s', $result->get_error_message() ) ); // @codingStandardsIgnoreLine
  886. set_media_errors( $post_id, $result->get_error_message() );
  887. }
  888. return false;
  889. }
  890. // Make sure we clean up.
  891. //phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPressVIPMinimum.Functions.RestrictedFunctions.file_ops_unlink
  892. @unlink( $file_array['tmp_name'] );
  893. if ( $save_source_file_path ) {
  894. update_post_meta( $result, 'dt_original_file_path', sanitize_text_field( $source_file ) );
  895. }
  896. return (int) $result;
  897. }
  898. /**
  899. * Return whether a post type is compatible with the block editor.
  900. *
  901. * The block editor depends on the REST API, and if the post type is not shown in the
  902. * REST API, then it won't work with the block editor.
  903. *
  904. * This duplicates the function use_block_editor_for_post_type() in WordPress Core
  905. * to ensure the function is always available in Distributor. The function is not
  906. * available in some WordPress contexts.
  907. *
  908. * @source WordPress 5.0.0
  909. *
  910. * @param string $post_type The post type.
  911. * @return bool Whether the post type can be edited with the block editor.
  912. */
  913. function dt_use_block_editor_for_post_type( $post_type ) {
  914. // In some contexts this function doesn't exist so we can't reliably use it.
  915. if ( function_exists( 'use_block_editor_for_post_type' ) ) {
  916. return use_block_editor_for_post_type( $post_type );
  917. }
  918. if ( ! post_type_exists( $post_type ) ) {
  919. return false;
  920. }
  921. if ( ! post_type_supports( $post_type, 'editor' ) ) {
  922. return false;
  923. }
  924. $post_type_object = get_post_type_object( $post_type );
  925. if ( $post_type_object && ! $post_type_object->show_in_rest ) {
  926. return false;
  927. }
  928. /**
  929. * Filters whether an item is able to be edited in the block editor.
  930. *
  931. * @since 1.6.9
  932. * @hook dt_use_block_editor_for_post_type
  933. *
  934. * @param {bool} $use_block_editor Whether the post type uses the block editor. Default true.
  935. * @param {string} $post_type The post type being checked.
  936. *
  937. * @return {bool} Whether the post type uses the block editor.
  938. */
  939. return apply_filters( 'dt_use_block_editor_for_post_type', true, $post_type );
  940. }
  941. /**
  942. * Helper function to process post content.
  943. *
  944. * @param string $post_content The post content.
  945. *
  946. * @return string $post_content The processed post content.
  947. */
  948. function get_processed_content( $post_content ) {
  949. global $wp_embed;
  950. /**
  951. * Remove autoembed filter so that actual URL will be pushed and not the generated markup.
  952. */
  953. remove_filter( 'the_content', [ $wp_embed, 'autoembed' ], 8 );
  954. // Filter documented in WordPress core.
  955. $post_content = apply_filters( 'the_content', $post_content );
  956. add_filter( 'the_content', [ $wp_embed, 'autoembed' ], 8 );
  957. return $post_content;
  958. }
  959. /**
  960. * Gets the REST URL for a post.
  961. *
  962. * @param int $blog_id The blog ID.
  963. * @param int $post_id The post ID.
  964. * @return string
  965. */
  966. function get_rest_url( $blog_id, $post_id ) {
  967. if ( ! is_multisite() ) {
  968. // Filter documented below.
  969. return apply_filters( 'dt_get_rest_url', false, $blog_id, $post_id );
  970. }
  971. switch_to_blog( $blog_id );
  972. $post = get_post( $post_id );
  973. if ( ! is_a( $post, '\WP_Post' ) ) {
  974. restore_current_blog();
  975. // Filter documented below.
  976. return apply_filters( 'dt_get_rest_url', false, $blog_id, $post_id );
  977. }
  978. $obj = get_post_type_object( $post->post_type );
  979. $rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name;
  980. $base = sprintf( '%s/%s', 'wp/v2', $rest_base );
  981. $rest_url = rest_url( trailingslashit( $base ) . $post->ID );
  982. restore_current_blog();
  983. /**
  984. * Allow filtering of the REST API URL used for pulling post content.
  985. *
  986. * @hook dt_get_rest_url
  987. *
  988. * @param {string} $rest_url The default REST URL to the post.
  989. * @param {int} $blog_id The blog ID.
  990. * @param {int} $post_id The post ID being retrieved.
  991. *
  992. * @return {string} REST API URL for pulling post content.
  993. */
  994. return apply_filters( 'dt_get_rest_url', $rest_url, $blog_id, $post_id );
  995. }
  996. /**
  997. * Setup additional properties on a post object to enable them to be
  998. * fetched once and manipulated by filters.
  999. *
  1000. * @param WP_Post $post WP_Post object.
  1001. * @since 1.2.2
  1002. * @return WP_Post
  1003. */
  1004. function prepare_post( $post ) {
  1005. $post->link = get_permalink( $post->ID );
  1006. $post->meta = prepare_meta( $post->ID );
  1007. $post->terms = prepare_taxonomy_terms( $post->ID );
  1008. $post->media = prepare_media( $post->ID );
  1009. return $post;
  1010. }
  1011. /**
  1012. * Use transient to store media errors temporarily.
  1013. *
  1014. * @param int $post_id Post ID where the media attaches to.
  1015. * @param array|string $data Error message.
  1016. */
  1017. function set_media_errors( $post_id, $data ) {
  1018. $errors = get_transient( "dt_media_errors_$post_id" );
  1019. if ( ! $errors ) {
  1020. $errors = [];
  1021. }
  1022. if ( is_array( $data ) ) {
  1023. $errors += $data;
  1024. } else {
  1025. $errors[] = $data;
  1026. }
  1027. set_transient( "dt_media_errors_$post_id", $errors, HOUR_IN_SECONDS );
  1028. }
  1029. /**
  1030. * Reduce arguments passed to wp_insert_post to approved arguments only.
  1031. *
  1032. * @since 1.7.0
  1033. *
  1034. * @link http://developer.wordpress.org/reference/functions/wp_insert_post/ wp_insert_post() documentation.
  1035. *
  1036. * @param array $post_args Arguments used for wp_insert_post() or wp_update_post().
  1037. *
  1038. * @return array Arguments cleaned of any not expected by the core function.
  1039. */
  1040. function post_args_allow_list( $post_args ) {
  1041. $allowed_post_keys = array(
  1042. 'ID',
  1043. 'post_author',
  1044. 'post_date',
  1045. 'post_date_gmt',
  1046. 'post_content',
  1047. 'post_content_filtered',
  1048. 'post_title',
  1049. 'post_excerpt',
  1050. 'post_status',
  1051. 'post_type',
  1052. 'comment_status',
  1053. 'ping_status',
  1054. 'post_password',
  1055. 'post_name',
  1056. 'to_ping',
  1057. 'pinged',
  1058. 'post_modified',
  1059. 'post_modified_gmt',
  1060. 'post_parent',
  1061. 'menu_order',
  1062. 'post_mime_type',
  1063. 'guid',
  1064. 'import_id',
  1065. 'post_category',
  1066. 'tags_input',
  1067. 'tax_input',
  1068. 'meta_input',
  1069. );
  1070. return array_intersect_key( $post_args, array_flip( $allowed_post_keys ) );
  1071. }
  1072. /**
  1073. * Make a remote HTTP request.
  1074. *
  1075. * Wrapper function for wp_remote_request() and vip_safe_wp_remote_request(). The order
  1076. * of parameters differs from vip_safe_wp_remote_request() to promote the arguments array
  1077. * to the second parameter.
  1078. *
  1079. * The default request type is a GET request although the function can be used for other
  1080. * HTTP methods by setting the method in the $args array.
  1081. *
  1082. * See {@see http://developer.wordpress.org/reference/classes/WP_Http/request/ WP_Http::request} for $args defaults.
  1083. *
  1084. * @param string $url The URL to request.
  1085. * @param array $args Optional. An array of arguments to pass to wp_remote_get()/vip_safe_wp_remote_get().
  1086. * @param mixed $fallback Optional. Fallback value to return if the request fails. Default ''. VIP only.
  1087. * @param int $threshold Optional. The number of fails required before subsequent requests automatically
  1088. * return the fallback value. Defaults to 3, with a maximum of 10. VIP only.
  1089. * @param int $timeout Optional. The timeout for WP VIP requests. Use $args['timeout'] for others. VIP only.
  1090. * All requests have a maximum of 5 seconds except:
  1091. * - `POST` requests made via WP CLI have a maximum of 30 seconds.
  1092. * - `POST` requests within the WP Admin have a maximum of 15 seconds.
  1093. * @param int $retries Optional. The number of retries to attempt. Minimum and default is 10,
  1094. * lower values will be increased to 10. VIP only.
  1095. *
  1096. * @return mixed The response from the remote request. On VIP if the request fails, the fallback value is returned.
  1097. */
  1098. function remote_http_request( $url, $args = array(), $fallback = '', $threshold = 3, $timeout = 3, $retries = 10 ) {
  1099. if ( function_exists( 'vip_safe_wp_remote_request' ) && is_vip_com() ) {
  1100. return vip_safe_wp_remote_request( $url, $fallback, $threshold, $timeout, $retries, $args );
  1101. }
  1102. return wp_remote_request( $url, $args );
  1103. }
  1104. /**
  1105. * Determines if a post is distributed.
  1106. *
  1107. * @since 2.0.0
  1108. *
  1109. * @param int|\WP_Post $post The post object or ID been checked.
  1110. * @return bool True if the post is distributed, false otherwise.
  1111. */
  1112. function is_distributed_post( $post ) {
  1113. $post = get_post( $post );
  1114. if ( ! $post ) {
  1115. return false;
  1116. }
  1117. $post_id = $post->ID;
  1118. $original_post_id = get_post_meta( $post_id, 'dt_original_post_id', true );
  1119. return ! empty( $original_post_id );
  1120. }
  1121. /**
  1122. * Returns the admin icon in data URL base64 format.
  1123. *
  1124. * @since 2.0.1
  1125. *
  1126. * @param string $color The hex color if changing the color of the icon. Default `#a0a5aa`.
  1127. * @return string Data URL base64 encoded SVG icon string.
  1128. */
  1129. function get_admin_icon( $color = '#a0a5aa' ) {
  1130. $svg_icon = sprintf( '<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2" viewBox="13.4 8.8 573.2 573.2"><path fill="%1$s" d="M195.113 411.033c45.835 46.692 119.124 58.488 178.387 24.273 70.262-40.566 94.371-130.544 53.806-200.806-40.566-70.262-130.544-94.371-200.806-53.806-19.873 11.474-36.055 26.899-48.124 44.715l64.722 33.186c22.201-25.593 59.796-33.782 91.279-17.639 37.002 18.973 51.64 64.418 32.667 101.421-18.973 37.002-64.418 51.64-101.421 32.667-31.483-16.143-46.776-51.45-38.951-84.415l-81.702-41.892c-8.838-4.532-12.335-15.367-7.814-24.211 15.514-30.346 39.658-56.715 71.344-75.009 87.469-50.5 199.482-20.486 249.983 66.983 50.5 87.469 20.486 199.482-66.983 249.983-75.235 43.437-168.63 27.307-225.419-33.717-17.809 3.778-36.797-4.055-46.387-20.666-11.922-20.648-4.837-47.091 15.812-59.012 20.648-11.922 47.091-4.836 59.012 15.812 7.77 13.458 7.466 29.377.595 42.133Z"/><path fill="%1$s" d="M262.237 72.985C148.8 91.101 62 189.494 62 308c0 131.356 106.644 238 238 238s238-106.644 238-238c0-34.059-7.168-66.458-20.08-95.766-15.121.99-30.323-6.014-39.137-19.626-12.959-20.014-7.231-46.783 12.783-59.742 20.014-12.958 46.783-7.231 59.742 12.783 10.095 15.592 8.849 35.284-1.657 49.352C565.288 229.461 574 267.721 574 308c0 151.225-122.775 274-274 274S26 459.225 26 308C26 170.539 127.443 56.584 259.487 36.98 265.594 20.533 281.438 8.8 300 8.8c23.843 0 43.2 19.357 43.2 43.2 0 23.843-19.357 43.2-43.2 43.2-16.229 0-30.38-8.968-37.763-22.215Z"/></svg>', $color );
  1131. return sprintf( 'data:image/svg+xml;base64,%s', base64_encode( $svg_icon ) );
  1132. }