diff --git a/.deployignore b/.deployignore deleted file mode 100644 index ca52ad887..000000000 --- a/.deployignore +++ /dev/null @@ -1,52 +0,0 @@ -# Directories and files that we do not want to be included with the built -# IDEs -# version and deployed to WordPress.org. -*.sql -*.tar.gz -*.zip -.DS_Store -.babelrc -.circleci/config.yml -.distignore -.editorconfig -.eslintignore -.eslintrc.json -.git -.gitignore -.gitlab-ci.yml -.idea -.phpcs -.phpcs-cache.json -.phpcs.xml -.phpcs.xml.dist -.phpstan -.phpunit.result.cache -.travis.yml -.vscode -.wordpress-org -.wp-env.json -Gruntfile.js -README.md -Thumbs.db -assets/js/pluginsidebar -assets/js/util -behat.yml -bin -bitbucket-pipelines.yml -multisite.xml -multisite.xml.dist -node_modules -npm-debug.log -phpcs.xml -phpcs.xml.dist -phpstan.neon.dist -phpunit.xml -phpunit.xml.dist -tags -tests -vendor -webpack.config.js -wp-cli.local.yml -yarn.lock -DOCKER_ENV -babel.config.json diff --git a/.distignore b/.distignore index 852095f18..b83a139e7 100644 --- a/.distignore +++ b/.distignore @@ -1,53 +1,33 @@ -# A set of files you probably don't want in your WordPress.org distribution -*.sql -*.tar.gz -*.zip -.DS_Store -.babelrc -.circleci/config.yml +# Exclusions when publishing to WordPress.org + +# Directories +.git +.github +assets/js/admin-settings +assets/js/components +assets/js/config +assets/js/pluginsidebar +assets/js/services +assets/js/util +node_modules +tests +vendor + +# Files .distignore .editorconfig .eslintignore .eslintrc.json -.git -.github .gitignore -.gitlab-ci.yml -.idea -.phpcs -.phpcs-cache.json +.nvmrc .phpcs.xml -.phpcs.xml.dist -.phpstan +.phpcs-cache.json .phpunit.result.cache -.travis.yml -.vscode -.wordpress-org -.wp-env.json -Gruntfile.js -README.md -Thumbs.db -assets/js/pluginsidebar -assets/js/util -behat.yml -bin -bitbucket-pipelines.yml +babel.config.json composer.json composer.lock -multisite.xml -multisite.xml.dist -node_modules -npm-debug.log -package-lock.json package.json -phpcs.xml -phpcs.xml.dist -phpstan.neon.dist -phpunit.xml +package-lock.json phpunit.xml.dist -tags -tests -vendor +README.md webpack.config.js -wp-cli.local.yml -yarn.lock diff --git a/README.md b/README.md index 2367642cc..0aeec17be 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ The Publish to Apple News plugin enables your WordPress content to be published - Control individual posts with options to publish, update, or delete. - Publish individual posts or in bulk. - Handles image galleries and popular embeds like YouTube and Vimeo that are supported by Apple News. -- Automatically adjust advertisement settings. Please visit our [wiki](https://github.com/alleyinteractive/apple-news/wiki) for detailed information on the follow items: diff --git a/admin/apple-actions/class-api-action.php b/admin/apple-actions/class-api-action.php index a4da4de46..9c90a2165 100644 --- a/admin/apple-actions/class-api-action.php +++ b/admin/apple-actions/class-api-action.php @@ -8,9 +8,9 @@ namespace Apple_Actions; -require_once plugin_dir_path( __FILE__ ) . 'class-action.php'; -require_once plugin_dir_path( __FILE__ ) . 'class-action-exception.php'; -require_once plugin_dir_path( __FILE__ ) . '../../includes/apple-push-api/autoload.php'; +require_once __DIR__ . '/class-action.php'; +require_once __DIR__ . '/class-action-exception.php'; +require_once dirname( __DIR__, 2 ) . '/includes/apple-push-api/autoload.php'; use Apple_Actions\Action; use Apple_Push_API\API; diff --git a/admin/apple-actions/index/class-channel.php b/admin/apple-actions/index/class-channel.php new file mode 100644 index 000000000..13f5819aa --- /dev/null +++ b/admin/apple-actions/index/class-channel.php @@ -0,0 +1,36 @@ +is_api_configuration_valid() ) { + $channel = $this->get_api()->get_channel( $this->get_setting( 'api_channel' ) ); + set_transient( 'apple_news_channel', $channel, 300 ); + } + } + + return ! empty( $channel ) ? $channel : null; + } +} diff --git a/admin/apple-actions/index/class-delete.php b/admin/apple-actions/index/class-delete.php index 3c21e9081..fcb7f44a0 100644 --- a/admin/apple-actions/index/class-delete.php +++ b/admin/apple-actions/index/class-delete.php @@ -8,7 +8,7 @@ namespace Apple_Actions\Index; -require_once plugin_dir_path( __FILE__ ) . '../class-api-action.php'; +require_once dirname( __DIR__ ) . '/class-api-action.php'; use Apple_Actions\API_Action; diff --git a/admin/apple-actions/index/class-export.php b/admin/apple-actions/index/class-export.php index 662d29641..7c602de4c 100644 --- a/admin/apple-actions/index/class-export.php +++ b/admin/apple-actions/index/class-export.php @@ -8,9 +8,9 @@ namespace Apple_Actions\Index; -require_once plugin_dir_path( __FILE__ ) . '../class-action.php'; -require_once plugin_dir_path( __FILE__ ) . '../class-action-exception.php'; -require_once plugin_dir_path( __FILE__ ) . '../../../includes/apple-exporter/autoload.php'; +require_once dirname( __DIR__ ) . '/class-action.php'; +require_once dirname( __DIR__ ) . '/class-action-exception.php'; +require_once dirname( __DIR__, 3 ) . '/includes/apple-exporter/autoload.php'; use Apple_Actions\Action; use Apple_Exporter\Exporter; diff --git a/admin/apple-actions/index/class-get.php b/admin/apple-actions/index/class-get.php index 66f221944..ffdc06b22 100644 --- a/admin/apple-actions/index/class-get.php +++ b/admin/apple-actions/index/class-get.php @@ -8,7 +8,7 @@ namespace Apple_Actions\Index; -require_once plugin_dir_path( __FILE__ ) . '../class-api-action.php'; +require_once dirname( __DIR__ ) . '/class-api-action.php'; use Apple_Actions\API_Action; diff --git a/admin/apple-actions/index/class-push.php b/admin/apple-actions/index/class-push.php index 8e3298f2f..9bd329647 100644 --- a/admin/apple-actions/index/class-push.php +++ b/admin/apple-actions/index/class-push.php @@ -8,14 +8,17 @@ namespace Apple_Actions\Index; -require_once plugin_dir_path( __FILE__ ) . '../class-api-action.php'; -require_once plugin_dir_path( __FILE__ ) . 'class-export.php'; +require_once dirname( __DIR__ ) . '/class-api-action.php'; +require_once __DIR__ . '/class-export.php'; +use Admin_Apple_Async; use Admin_Apple_Notice; use Admin_Apple_Sections; use Apple_Actions\Action_Exception; use Apple_Actions\API_Action; +use Apple_Exporter\Exporter; use Apple_Exporter\Settings; +use Apple_Push_API\Request\Request_Exception; /** * A class to handle a push request from the admin. @@ -61,7 +64,7 @@ class Push extends API_Action { * Constructor. * * @param Settings $settings A settings object containing settings at load time. - * @param int $id The ID for the content object to be pushed. + * @param int $id The ID for the content object to be pushed. */ public function __construct( $settings, $id ) { parent::__construct( $settings ); @@ -73,13 +76,14 @@ public function __construct( $settings, $id ) { * Perform the push action. * * @param boolean $doing_async Optional. Whether the action is being performed asynchronously. - * @param int $user_id Optional. The ID of the user performing the action. Defaults to the current user ID. + * @param int $user_id Optional. The ID of the user performing the action. Defaults to the current user ID. + * * @access public * @return boolean * @throws Action_Exception If the push fails. */ public function perform( $doing_async = false, $user_id = null ) { - if ( 'yes' === $this->settings->get( 'api_async' ) && false === $doing_async ) { + if ( 'yes' === $this->settings->__get( 'api_async' ) && false === $doing_async ) { // Do not proceed if this is already pending publish. $pending = get_post_meta( $this->id, 'apple_news_api_pending', true ); if ( ! empty( $pending ) ) { @@ -89,7 +93,7 @@ public function perform( $doing_async = false, $user_id = null ) { // Track this publish event as pending with the timestamp it was sent. update_post_meta( $this->id, 'apple_news_api_pending', time() ); - wp_schedule_single_event( time(), \Admin_Apple_Async::ASYNC_PUSH_HOOK, [ $this->id, get_current_user_id() ] ); + wp_schedule_single_event( time(), Admin_Apple_Async::ASYNC_PUSH_HOOK, [ $this->id, get_current_user_id() ] ); } else { return $this->push( $user_id ); } @@ -98,10 +102,10 @@ public function perform( $doing_async = false, $user_id = null ) { /** * Generate a checksum against the article JSON with certain fields ignored. * - * @param string $json The JSON to turn into a checksum. - * @param array $meta Optional. Metadata for the article. Defaults to empty array. + * @param string $json The JSON to turn into a checksum. + * @param array $meta Optional. Metadata for the article. Defaults to empty array. * @param array $bundles Optional. Any bundles that will be sent with the article. Defaults to empty array. - * @param bool $force Optional. Allows bypass of local cache for checksum. + * @param bool $force Optional. Allows bypass of local cache for checksum. * * @return string The checksum for the JSON. */ @@ -137,9 +141,11 @@ private function generate_checksum( $json, $meta = [], $bundles = [], $force = f * Check if the post is in sync before updating in Apple News. * * @access private - * @param string $json The JSON for this article to check if it is in sync. - * @param array $meta Optional. Metadata for the article. Defaults to empty array. + * + * @param string $json The JSON for this article to check if it is in sync. + * @param array $meta Optional. Metadata for the article. Defaults to empty array. * @param array $bundles Optional. Any bundles that will be sent with the article. Defaults to empty array. + * * @return boolean * @throws Action_Exception If the post could not be found. */ @@ -167,13 +173,13 @@ private function is_post_in_sync( $json, $meta = [], $bundles = [] ) { * logic, you can do that by modifying `$in_sync`. The most common use case * is to not update posts based on custom criteria. * - * @since 2.0.2 Added the $post_id, $json, $meta, and $bundles parameters. + * @param bool $in_sync Whether the current post is in sync or not. + * @param int $post_id The ID of the post being checked. + * @param string $json The JSON for the current article. + * @param array $meta Metadata for the current article. + * @param array $bundles Any bundles that will be sent with the current article. * - * @param bool $in_sync Whether the current post is in sync or not. - * @param int $post_id The ID of the post being checked. - * @param string $json The JSON for the current article. - * @param array $meta Metadata for the current article. - * @param array $bundles Any bundles that will be sent with the current article. + * @since 2.0.2 Added the $post_id, $json, $meta, and $bundles parameters. */ return apply_filters( 'apple_news_is_post_in_sync', $in_sync, $this->id, $json, $meta, $bundles ); } @@ -185,7 +191,7 @@ private function is_post_in_sync( $json, $meta = [], $bundles = [] ) { * @access private * @throws Action_Exception If there was an error getting the article from the API. */ - private function get() { + private function get(): void { // Ensure we have a valid ID. $apple_id = get_post_meta( $this->id, 'apple_news_api_id', true ); if ( empty( $apple_id ) ) { @@ -206,10 +212,11 @@ private function get() { * Push the post using the API data. * * @param int $user_id Optional. The ID of the user performing the push. Defaults to current user. + * * @access private * @throws Action_Exception If unable to push. */ - private function push( $user_id = null ) { + private function push( $user_id = null ): void { if ( ! $this->is_api_configuration_valid() ) { throw new Action_Exception( esc_html__( 'Your Apple News API settings seem to be empty. Please fill in the API key, API secret and API channel fields in the plugin configuration page.', 'apple-news' ) ); } @@ -222,8 +229,8 @@ private function push( $user_id = null ) { * category or tag. By default this is always `false` as all posts are * published once they reach this step. * - * @param bool $skip Whether the post should be skipped. Defaults to `false`. - * @param int $post_id The ID of the post. + * @param bool $skip Whether the post should be skipped. Defaults to `false`. + * @param int $post_id The ID of the post. */ if ( apply_filters( 'apple_news_skip_push', false, $this->id ) ) { throw new Action_Exception( @@ -254,10 +261,10 @@ private function push( $user_id = null ) { * for the plugin, but the list can be modified for individual posts via * this filter. * - * @since 2.3.0 - * * @param int[] $term_ids The list of term IDs that should trigger a skipped push. Defaults to the term IDs set in plugin options. - * @param int $post_id The ID of the post being exported. + * @param int $post_id The ID of the post being exported. + * + * @since 2.3.0 */ $skip_term_ids = apply_filters( 'apple_news_skip_push_term_ids', $skip_term_ids, $this->id ); @@ -333,21 +340,21 @@ private function push( $user_id = null ) { $meta['data']['links'] = [ 'sections' => $this->sections ]; } - // Get the isPreview setting. - $is_paid = (bool) get_post_meta( $this->id, 'apple_news_is_paid', true ); - $meta['data']['isPaid'] = $is_paid; - - // Get the isPreview setting. - $is_preview = (bool) get_post_meta( $this->id, 'apple_news_is_preview', true ); - $meta['data']['isPreview'] = $is_preview; - - // Get the isHidden setting. - $is_hidden = (bool) get_post_meta( $this->id, 'apple_news_is_hidden', true ); - $meta['data']['isHidden'] = $is_hidden; - - // Get the isSponsored setting. - $is_sponsored = (bool) get_post_meta( $this->id, 'apple_news_is_sponsored', true ); - $meta['data']['isSponsored'] = $is_sponsored; + // Set boolean metadata. Don't set values at all if they are not set in postmeta, or are set to empty string. + $metadata_keys = [ + 'isHidden' => 'apple_news_is_hidden', + 'isPaid' => 'apple_news_is_paid', + 'isPreview' => 'apple_news_is_preview', + 'isSponsored' => 'apple_news_is_sponsored', + ]; + foreach ( $metadata_keys as $metadata_property => $meta_key ) { + $meta_value = get_post_meta( $this->id, $meta_key, true ); + if ( 'true' === $meta_value || '1' === $meta_value ) { + $meta['data'][ $metadata_property ] = true; + } elseif ( 'false' === $meta_value || '0' === $meta_value ) { + $meta['data'][ $metadata_property ] = false; + } + } // Get the maturity rating setting. $maturity_rating = get_post_meta( $this->id, 'apple_news_maturity_rating', true ); @@ -357,36 +364,15 @@ private function push( $user_id = null ) { // Add custom metadata fields. $custom_meta = get_post_meta( $this->id, 'apple_news_metadata', true ); - if ( ! empty( $custom_meta ) && is_array( $custom_meta ) ) { - foreach ( $custom_meta as $metadata ) { - // Ensure required fields are set. - if ( empty( $metadata['key'] ) || empty( $metadata['type'] ) || ! isset( $metadata['value'] ) ) { - continue; - } - - // If the value is an array, we have to decode it from JSON. - $value = $metadata['value']; - if ( 'array' === $metadata['type'] ) { - $value = json_decode( $metadata['value'] ); - - // If the user entered a bad value for the array, bail out without adding it. - if ( empty( $value ) || ! is_array( $value ) ) { - continue; - } - } - - // Add the custom metadata field to the article metadata. - $meta['data'][ $metadata['key'] ] = $value; - } - } + $meta = $this->get_meta( $custom_meta, $meta ); /** * Allow article metadata to be filtered. * - * @since 2.4.0 - * * @param array $metadata The article metadata to be filtered. - * @param int $post_id The ID of the post being pushed to Apple News. + * @param int $post_id The ID of the post being pushed to Apple News. + * + * @since 2.4.0 */ $meta['data'] = apply_filters( 'apple_news_article_metadata', $meta['data'], $this->id ); @@ -438,11 +424,11 @@ private function push( $user_id = null ) { /** * Actions to be taken after an article was pushed to Apple News. * - * @param int $post_id The ID of the post. - * @param object $result The JSON returned by the Apple News API. + * @param int $post_id The ID of the post. + * @param object $result The JSON returned by the Apple News API. */ do_action( 'apple_news_after_push', $this->id, $result ); - } catch ( \Apple_Push_API\Request\Request_Exception $e ) { + } catch ( Request_Exception $e ) { // Remove the pending designation if it exists. delete_post_meta( $this->id, 'apple_news_api_pending' ); @@ -488,10 +474,11 @@ private function push( $user_id = null ) { * Processes errors, halts publishing if needed. * * @param array $errors Array of errors to be processed. + * * @access private * @throws Action_Exception If set to fail on component errors. */ - private function process_errors( $errors ) { + private function process_errors( $errors ): void { // Get the current alert settings. $component_alerts = $this->get_setting( 'component_alerts' ); @@ -503,7 +490,7 @@ private function process_errors( $errors ) { // Build the component alert error message, if required. if ( ! empty( $errors[0]['component_errors'] ) ) { - // Build an list of the components that caused errors. + // Build a list of the components that caused errors. $component_names = implode( ', ', $errors[0]['component_errors'] ); if ( 'warn' === $component_alerts ) { @@ -540,16 +527,89 @@ private function process_errors( $errors ) { // Throw an exception. throw new Action_Exception( esc_html( $alert_message ) ); } elseif ( 'warn' === $component_alerts && ! empty( $errors[0]['component_errors'] ) ) { - \Admin_Apple_Notice::error( $alert_message, $user_id ); + Admin_Apple_Notice::error( $alert_message, $user_id ); } } + /** + * Allows debugging of API requests by dumping the data + * that will be sent to the API. + */ + public function debug(): void { + $post_id = $this->id; + $post = get_post( $post_id ); + if ( ! $post ) { + return; + } + + $this->sections = Admin_Apple_Sections::get_sections_for_post( $post_id ); + [ $json, $bundles, $errors ] = $this->generate_article(); + + // Retrieve the metadata using the same method as in the push() function. + $meta = [ 'data' => [] ]; + + // Set sections. + if ( ! empty( $this->sections ) ) { + sort( $this->sections ); + $meta['data']['links'] = [ 'sections' => $this->sections ]; + } + + // Get the various settings. + $metadata_keys = [ + 'isHidden' => 'apple_news_is_hidden', + 'isPaid' => 'apple_news_is_paid', + 'isPreview' => 'apple_news_is_preview', + 'isSponsored' => 'apple_news_is_sponsored', + ]; + foreach ( $metadata_keys as $metadata_property => $meta_key ) { + $meta_value = get_post_meta( $post_id, $meta_key, true ); + if ( 'true' === $meta_value || '1' === $meta_value ) { + $meta['data'][ $metadata_property ] = true; + } elseif ( 'false' === $meta_value || '0' === $meta_value ) { + $meta['data'][ $metadata_property ] = false; + } + } + + $maturity_rating = get_post_meta( $post_id, 'apple_news_maturity_rating', true ); + if ( ! empty( $maturity_rating ) ) { + $meta['data']['maturityRating'] = $maturity_rating; + } + + // Add custom metadata fields. + $custom_meta = get_post_meta( $post_id, 'apple_news_metadata', true ); + $meta = $this->get_meta( $custom_meta, $meta ); + $meta['data'] = apply_filters( 'apple_news_article_metadata', $meta['data'], $post_id ); + + header( 'Content-Type: text/html' ); + + echo '

Apple News API Push

';
+		echo 'Post ID: ' . esc_html( $post_id ) . "\n";
+
+		echo '
Article:
'; + $formatted_json = json_decode( $json ); + echo wp_json_encode( $formatted_json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); + echo "\n"; + + echo '
Bundles:
'; + echo wp_json_encode( $bundles, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); + echo "\n"; + + echo '
Meta:
'; + echo wp_json_encode( $meta, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); + echo "\n"; + + echo '
'; + + // Clean the workspace after the debug. + $this->clean_workspace(); + } + /** * Clean up the workspace. * * @access private */ - private function clean_workspace() { + private function clean_workspace(): void { if ( is_null( $this->exporter ) ) { return; } @@ -578,12 +638,12 @@ private function generate_article() { /** * Sanitize the JSON output based on whether HTML or markdown is used. * - * @since 1.2.7 - * * @param string $json The JSON to be sanitized. + * * @access private * @return string * @throws Action_Exception If the JSON is invalid. + * @since 1.2.7 */ private function sanitize_json( $json ) { /** @@ -597,4 +657,41 @@ private function sanitize_json( $json ) { return wp_json_encode( $decoded ); } } + + /** + * Get the custom post meta data and add it to the article metadata array. + * + * @access private + * + * @param mixed $custom_meta The custom meta data. + * @param array $meta The article metadata. + * + * @return array The updated article metadata. + */ + private function get_meta( mixed $custom_meta, array $meta ): array { + if ( ! empty( $custom_meta ) && is_array( $custom_meta ) ) { + foreach ( $custom_meta as $metadata ) { + // Ensure required fields are set. + if ( empty( $metadata['key'] ) || empty( $metadata['type'] ) || ! isset( $metadata['value'] ) ) { + continue; + } + + // If the value is an array, we have to decode it from JSON. + $value = $metadata['value']; + if ( 'array' === $metadata['type'] ) { + $value = json_decode( $metadata['value'] ); + + // If the user entered a bad value for the array, bail out without adding it. + if ( empty( $value ) || ! is_array( $value ) ) { + continue; + } + } + + // Add the custom metadata field to the article metadata. + $meta['data'][ $metadata['key'] ] = $value; + } + } + + return $meta; + } } diff --git a/admin/apple-actions/index/class-section.php b/admin/apple-actions/index/class-section.php index e487a2ffb..80848e566 100644 --- a/admin/apple-actions/index/class-section.php +++ b/admin/apple-actions/index/class-section.php @@ -8,7 +8,7 @@ namespace Apple_Actions\Index; -require_once plugin_dir_path( __FILE__ ) . '../class-api-action.php'; +require_once dirname( __DIR__ ) . '/class-api-action.php'; use Apple_Actions\API_Action; use Apple_Exporter\Settings; diff --git a/admin/class-admin-apple-bulk-export-page.php b/admin/class-admin-apple-bulk-export-page.php index 276fd64d3..669ebf01a 100644 --- a/admin/class-admin-apple-bulk-export-page.php +++ b/admin/class-admin-apple-bulk-export-page.php @@ -6,7 +6,7 @@ */ // Include dependencies. -require_once plugin_dir_path( __FILE__ ) . 'apple-actions/index/class-push.php'; +require_once __DIR__ . '/apple-actions/index/class-push.php'; /** * Bulk export page. Display progress on multiple articles export process. @@ -105,7 +105,7 @@ public function build_page() { } } - require_once plugin_dir_path( __FILE__ ) . 'partials/page-bulk-export.php'; + require_once __DIR__ . '/partials/page-bulk-export.php'; } /** diff --git a/admin/class-admin-apple-index-page.php b/admin/class-admin-apple-index-page.php index 51adeb3ce..ccef3af76 100644 --- a/admin/class-admin-apple-index-page.php +++ b/admin/class-admin-apple-index-page.php @@ -8,12 +8,12 @@ */ // Include dependencies. -require_once plugin_dir_path( __FILE__ ) . 'apple-actions/index/class-get.php'; -require_once plugin_dir_path( __FILE__ ) . 'apple-actions/index/class-push.php'; -require_once plugin_dir_path( __FILE__ ) . 'apple-actions/index/class-delete.php'; -require_once plugin_dir_path( __FILE__ ) . 'apple-actions/index/class-export.php'; -require_once plugin_dir_path( __FILE__ ) . 'apple-actions/index/class-section.php'; -require_once plugin_dir_path( __FILE__ ) . 'class-admin-apple-news-list-table.php'; +require_once __DIR__ . '/apple-actions/index/class-get.php'; +require_once __DIR__ . '/apple-actions/index/class-push.php'; +require_once __DIR__ . '/apple-actions/index/class-delete.php'; +require_once __DIR__ . '/apple-actions/index/class-export.php'; +require_once __DIR__ . '/apple-actions/index/class-section.php'; +require_once __DIR__ . '/class-admin-apple-news-list-table.php'; use Apple_Exporter\Workspace; @@ -93,36 +93,12 @@ public function setup_admin_page() { } /** - * Decide which template to load for the Apple News admin page - * - * @access public + * Shows the list of articles available for publishing to Apple News. */ public function admin_page() { - $id = isset( $_GET['post_id'] ) ? absint( $_GET['post_id'] ) : null; // phpcs:ignore WordPress.VIP.SuperGlobalInputUsage.AccessDetected, WordPress.Security.NonceVerification.Recommended - $action = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : null; // phpcs:ignore WordPress.VIP.SuperGlobalInputUsage.AccessDetected, WordPress.Security.NonceVerification.Recommended - - switch ( $action ) { - case self::namespace_action( 'push' ): - /* phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable */ - - $section = new Apple_Actions\Index\Section( $this->settings ); - try { - $sections = $section->get_sections(); - } catch ( Apple_Actions\Action_Exception $e ) { - Admin_Apple_Notice::error( $e->getMessage() ); - } - - $post = get_post( $id ); - $post_meta = get_post_meta( $id ); - - /* phpcs:enable */ - - include plugin_dir_path( __FILE__ ) . 'partials/page-single-push.php'; - break; - default: - $this->show_post_list_action(); - break; - } + $table = new Admin_Apple_News_List_Table( $this->settings ); + $table->prepare_items(); + include __DIR__ . '/partials/page-index.php'; } /** @@ -148,6 +124,8 @@ public function page_router() { // Given an action and ID, map the attributes to corresponding actions. switch ( $action ) { + case self::namespace_action( 'debug' ): + return $this->debug_action( $id ); case self::namespace_action( 'export' ): return $this->export_action( $id ); case self::namespace_action( 'reset' ): @@ -335,17 +313,6 @@ public function setup_assets( $hook ) { ); } - /** - * Shows the list of articles available for publishing to Apple News. - * - * @access public - */ - public function show_post_list_action() { - $table = new Admin_Apple_News_List_Table( $this->settings ); - $table->prepare_items(); - include plugin_dir_path( __FILE__ ) . 'partials/page-index.php'; - } - /** * Handles an export action. * @@ -405,6 +372,25 @@ private function push_action( $id ) { } } + /** + * Handles a debug action. + * + * @param int|null $id The ID of the post being debugged. + * + * @access private + */ + private function debug_action( ?int $id ): void { + $post = get_post( $id ); + if ( ! $post ) { + return; + } + + $action = new Apple_Actions\Index\Push( $this->settings, $id ); + $action->debug(); + + exit( 'Debug output complete.' ); + } + /** * Handles a delete from Apple News action. * diff --git a/admin/class-admin-apple-json.php b/admin/class-admin-apple-json.php index 07f285e37..3d5e571c4 100644 --- a/admin/class-admin-apple-json.php +++ b/admin/class-admin-apple-json.php @@ -5,6 +5,8 @@ * @package Apple_News */ +use Apple_Exporter\Components\Component; + /** * This class is in charge of handling the management of custom JSON. */ @@ -35,6 +37,14 @@ class Admin_Apple_JSON extends Apple_News { */ private $selected_component = ''; + /** + * Holds the selected subcomponent from the request. + * + * @since 2.5.0 + * @var string + */ + private $selected_subcomponent = ''; + /** * Holds the selected theme from the request. * @@ -58,20 +68,20 @@ class Admin_Apple_JSON extends Apple_News { public function __construct() { $this->json_page_name = $this->plugin_domain . '-json'; - $this->valid_actions = array( - 'apple_news_get_json' => array(), - 'apple_news_reset_json' => array( - 'callback' => array( $this, 'reset_json' ), - ), - 'apple_news_save_json' => array( - 'callback' => array( $this, 'save_json' ), - ), - ); - - add_action( 'admin_menu', array( $this, 'setup_json_page' ), 99 ); - add_action( 'admin_init', array( $this, 'action_router' ) ); - add_action( 'admin_enqueue_scripts', array( $this, 'register_assets' ) ); - add_filter( 'admin_title', array( $this, 'set_title' ), 10, 2 ); + $this->valid_actions = [ + 'apple_news_get_json' => [], + 'apple_news_reset_json' => [ + 'callback' => [ $this, 'reset_json' ], + ], + 'apple_news_save_json' => [ + 'callback' => [ $this, 'save_json' ], + ], + ]; + + add_action( 'admin_menu', [ $this, 'setup_json_page' ], 99 ); + add_action( 'admin_init', [ $this, 'action_router' ] ); + add_action( 'admin_enqueue_scripts', [ $this, 'register_assets' ] ); + add_filter( 'admin_title', [ $this, 'set_title' ], 10, 2 ); } /** @@ -100,6 +110,14 @@ public function action_router() { $this->selected_component = ''; } + // Store the selected subcomponent value for use later. + $this->selected_subcomponent = isset( $_POST['apple_news_subcomponent'] ) + ? sanitize_text_field( wp_unslash( $_POST['apple_news_subcomponent'] ) ) + : ''; + if ( ! array_key_exists( $this->selected_subcomponent, $this->list_components() ) ) { + $this->selected_subcomponent = ''; + } + // Store the selected theme for use later. if ( ! empty( $_POST['apple_news_theme'] ) ) { $this->selected_theme = sanitize_text_field( wp_unslash( $_POST['apple_news_theme'] ) ); @@ -152,7 +170,7 @@ public function setup_json_page() { /** This filter is documented in admin/class-admin-apple-settings.php */ apply_filters( 'apple_news_settings_capability', 'manage_options' ), $this->json_page_name, - array( $this, 'page_json_render' ) + [ $this, 'page_json_render' ] ); } @@ -185,15 +203,19 @@ public function page_json_render() { ? $this->get_selected_component() : ''; - // If we have a class, get its specs. - $specs = ( ! empty( $selected_component ) ) - ? $this->get_specs( $selected_component ) - : array(); + // Handle subcomponents. + $component_can_be_parent = $this->get_component_class( $selected_component )?->can_be_parent(); + $selected_subcomponent = ( ! empty( $selected_component ) ) + ? $this->get_selected_subcomponent() + : ''; + + // If we have a component or subcomponent, get its specs. + $specs = $this->get_specs( $selected_component, $selected_subcomponent ); /* phpcs:enable */ // Load the template. - include plugin_dir_path( __FILE__ ) . 'partials/page-json.php'; + include __DIR__ . '/partials/page-json.php'; } /** @@ -210,14 +232,14 @@ public function register_assets( $hook ) { wp_enqueue_style( 'apple-news-json-css', plugin_dir_url( __FILE__ ) . '../assets/css/json.css', - array(), + [], self::$version ); wp_enqueue_script( 'apple-news-json-js', plugin_dir_url( __FILE__ ) . '../assets/js/json.js', - array( 'jquery' ), + [ 'jquery' ], self::$version, false ); @@ -225,7 +247,7 @@ public function register_assets( $hook ) { wp_enqueue_script( 'ace-js', 'https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ace.js', - array( 'jquery' ), + [ 'jquery' ], '1.2.6', false ); @@ -260,8 +282,11 @@ private function reset_json() { return; } + // Get the selected subcomponent. + $subcomponent = $this->get_selected_subcomponent(); + // Get the specs for the component. - $specs = $this->get_specs( $component ); + $specs = $this->get_specs( $component, $subcomponent ); if ( empty( $specs ) ) { \Admin_Apple_Notice::error( sprintf( @@ -317,9 +342,12 @@ private function save_json() { return; } - // Get the specs for the component and theme. + // Get the selected subcomponent. + $subcomponent = $this->get_selected_subcomponent(); + + // Get the specs for the component or subcomponent. $theme = sanitize_text_field( wp_unslash( $_POST['apple_news_theme'] ) ); - $specs = $this->get_specs( $component, $theme ); + $specs = $this->get_specs( $component, $subcomponent ); if ( empty( $specs ) ) { \Admin_Apple_Notice::error( sprintf( @@ -334,7 +362,7 @@ private function save_json() { // Iterate over the specs and save each one. // Keep track of which ones were updated. - $updates = array(); + $updates = []; foreach ( $specs as $spec ) { // Ensure the value exists. $key = 'apple_news_json_' . $spec->key_from_name( $spec->name ); @@ -368,18 +396,37 @@ private function save_json() { /** * Loads the JSON specs that can be customized for the component * - * @param string $component The component to get specs for. + * @param string $component The component to get specs for. + * @param ?string $subcomponent The subcomponent to get specs for. + * * @return array - * @access private */ - private function get_specs( $component ) { - if ( empty( $component ) ) { - return array(); + private function get_specs( $component, $subcomponent = null ) { + $component_class = $this->get_component_class( $component, $subcomponent ); + return $component_class ? $component_class->get_specs() : []; + } + + /** + * Given a component slug, returns the associated component class. + * + * @param string $component The component to get the class for. + * @param ?string $subcomponent The subcomponent to get the class for. + * + * @return ?Component + */ + private function get_component_class( $component, $subcomponent = null ) { + $classname = $this->namespace . $component; + if ( ! class_exists( $classname ) ) { + return null; } - $classname = $this->namespace . $component; - $component_class = new $classname(); - return $component_class->get_specs(); + // Handle subcomponents. + if ( ! empty( $subcomponent ) ) { + $subcomponent_classname = $this->namespace . $subcomponent; + return class_exists( $subcomponent_classname ) ? new $subcomponent_classname( null, null, null, null, null, null, null, new $classname() ) : null; + } + + return new $classname(); } /** @@ -394,7 +441,7 @@ private function list_components() { $components = $component_factory::get_components(); // Make this alphabetized and pretty. - $components_sanitized = array(); + $components_sanitized = []; foreach ( $components as $component ) { $component_key = str_replace( $this->namespace, '', $component ); $component_name = str_replace( '_', ' ', $component_key ); @@ -414,6 +461,15 @@ public function get_selected_component() { return $this->selected_component; } + /** + * Gets the currently selected subcomponent. + * + * @return string + */ + public function get_selected_subcomponent() { + return $this->selected_subcomponent; + } + /** * Checks for a valid selected theme. * diff --git a/admin/class-admin-apple-meta-boxes.php b/admin/class-admin-apple-meta-boxes.php index 90b321c64..69c90c785 100644 --- a/admin/class-admin-apple-meta-boxes.php +++ b/admin/class-admin-apple-meta-boxes.php @@ -175,10 +175,10 @@ public static function save_post_meta( $post_id ) { $fields = [ 'apple_news_coverimage' => 'integer', 'apple_news_coverimage_caption' => 'textarea', - 'apple_news_is_hidden' => 'boolean', - 'apple_news_is_paid' => 'boolean', - 'apple_news_is_preview' => 'boolean', - 'apple_news_is_sponsored' => 'boolean', + 'apple_news_is_hidden' => 'string', + 'apple_news_is_paid' => 'string', + 'apple_news_is_preview' => 'string', + 'apple_news_is_sponsored' => 'string', 'apple_news_pullquote' => 'string', 'apple_news_pullquote_position' => 'string', 'apple_news_slug' => 'string', @@ -357,12 +357,18 @@ public function publish_meta_box( $post ) { $pullquote_position = 'middle'; } + // Handle backwards compatibility for is* fields that were previously stored as booleans. + $is_hidden = '1' === $is_hidden ? 'true' : $is_hidden; + $is_paid = '1' === $is_paid ? 'true' : $is_paid; + $is_preview = '1' === $is_preview ? 'true' : $is_preview; + $is_sponsored = '1' === $is_sponsored ? 'true' : $is_sponsored; + // Create local copies of values to pass into the partial. $publish_action = self::PUBLISH_ACTION; /* phpcs:enable */ - include plugin_dir_path( __FILE__ ) . 'partials/metabox-publish.php'; + include __DIR__ . '/partials/metabox-publish.php'; } /** diff --git a/admin/class-admin-apple-news-list-table.php b/admin/class-admin-apple-news-list-table.php index 54801da32..676c878f7 100644 --- a/admin/class-admin-apple-news-list-table.php +++ b/admin/class-admin-apple-news-list-table.php @@ -188,6 +188,12 @@ public function column_title( $item ) { esc_url( Admin_Apple_Index_Page::action_query_params( 'export', $base_url ) ), esc_html__( 'Download', 'apple-news' ) ), + 'debug' => sprintf( + "%s", + esc_html__( 'Show API request for debugging', 'apple-news' ), + esc_url( Admin_Apple_Index_Page::action_query_params( 'debug', $base_url ) ), + esc_html__( 'Debug', 'apple-news' ) + ), ]; // Only add push if the article is not pending publish. diff --git a/admin/class-admin-apple-news.php b/admin/class-admin-apple-news.php index 852067f79..24cd3d419 100644 --- a/admin/class-admin-apple-news.php +++ b/admin/class-admin-apple-news.php @@ -8,28 +8,28 @@ */ // Include dependencies. -require_once plugin_dir_path( __FILE__ ) . 'class-admin-apple-settings.php'; -require_once plugin_dir_path( __FILE__ ) . 'class-admin-apple-post-sync.php'; -require_once plugin_dir_path( __FILE__ ) . 'class-admin-apple-index-page.php'; -require_once plugin_dir_path( __FILE__ ) . 'class-admin-apple-bulk-export-page.php'; -require_once plugin_dir_path( __FILE__ ) . 'class-admin-apple-notice.php'; -require_once plugin_dir_path( __FILE__ ) . 'class-admin-apple-meta-boxes.php'; -require_once plugin_dir_path( __FILE__ ) . 'class-admin-apple-async.php'; -require_once plugin_dir_path( __FILE__ ) . 'class-admin-apple-sections.php'; -require_once plugin_dir_path( __FILE__ ) . 'class-admin-apple-themes.php'; -require_once plugin_dir_path( __FILE__ ) . 'class-admin-apple-preview.php'; -require_once plugin_dir_path( __FILE__ ) . 'class-admin-apple-json.php'; -require_once plugin_dir_path( __FILE__ ) . 'class-automation.php'; +require_once __DIR__ . '/class-admin-apple-settings.php'; +require_once __DIR__ . '/class-admin-apple-post-sync.php'; +require_once __DIR__ . '/class-admin-apple-index-page.php'; +require_once __DIR__ . '/class-admin-apple-bulk-export-page.php'; +require_once __DIR__ . '/class-admin-apple-notice.php'; +require_once __DIR__ . '/class-admin-apple-meta-boxes.php'; +require_once __DIR__ . '/class-admin-apple-async.php'; +require_once __DIR__ . '/class-admin-apple-sections.php'; +require_once __DIR__ . '/class-admin-apple-themes.php'; +require_once __DIR__ . '/class-admin-apple-preview.php'; +require_once __DIR__ . '/class-admin-apple-json.php'; +require_once __DIR__ . '/class-automation.php'; // REST Includes. -require_once plugin_dir_path( __FILE__ ) . '../includes/REST/apple-news-delete.php'; -require_once plugin_dir_path( __FILE__ ) . '../includes/REST/apple-news-get-published-state.php'; -require_once plugin_dir_path( __FILE__ ) . '../includes/REST/apple-news-get-settings.php'; -require_once plugin_dir_path( __FILE__ ) . '../includes/REST/apple-news-modify-post.php'; -require_once plugin_dir_path( __FILE__ ) . '../includes/REST/apple-news-publish.php'; -require_once plugin_dir_path( __FILE__ ) . '../includes/REST/apple-news-sections.php'; -require_once plugin_dir_path( __FILE__ ) . '../includes/REST/apple-news-update.php'; -require_once plugin_dir_path( __FILE__ ) . '../includes/REST/apple-news-user-can-publish.php'; +require_once dirname( __DIR__ ) . '/includes/REST/apple-news-delete.php'; +require_once dirname( __DIR__ ) . '/includes/REST/apple-news-get-published-state.php'; +require_once dirname( __DIR__ ) . '/includes/REST/apple-news-get-settings.php'; +require_once dirname( __DIR__ ) . '/includes/REST/apple-news-modify-post.php'; +require_once dirname( __DIR__ ) . '/includes/REST/apple-news-publish.php'; +require_once dirname( __DIR__ ) . '/includes/REST/apple-news-sections.php'; +require_once dirname( __DIR__ ) . '/includes/REST/apple-news-update.php'; +require_once dirname( __DIR__ ) . '/includes/REST/apple-news-user-can-publish.php'; /** * Entry-point class for the plugin. @@ -122,20 +122,16 @@ public function __construct() { 'default' => '', ], 'apple_news_is_hidden' => [ - 'default' => false, - 'type' => 'boolean', + 'default' => '', ], 'apple_news_is_paid' => [ - 'default' => false, - 'type' => 'boolean', + 'default' => '', ], 'apple_news_is_preview' => [ - 'default' => false, - 'type' => 'boolean', + 'default' => '', ], 'apple_news_is_sponsored' => [ - 'default' => false, - 'type' => 'boolean', + 'default' => '', ], 'apple_news_maturity_rating' => [ 'default' => '', diff --git a/admin/class-admin-apple-notice.php b/admin/class-admin-apple-notice.php index a8976f63c..d5aeaa76a 100644 --- a/admin/class-admin-apple-notice.php +++ b/admin/class-admin-apple-notice.php @@ -404,7 +404,7 @@ private static function show_notice( $message, $type ) { $message = apply_filters( 'apple_news_notice_message', $message, $type ); // Load the partial for the notice. - include plugin_dir_path( __FILE__ ) . 'partials/notice.php'; + include __DIR__ . '/partials/notice.php'; } /** diff --git a/admin/class-admin-apple-post-sync.php b/admin/class-admin-apple-post-sync.php index 436ef2d26..9b1b38884 100644 --- a/admin/class-admin-apple-post-sync.php +++ b/admin/class-admin-apple-post-sync.php @@ -6,8 +6,8 @@ */ // Include dependencies. -require_once plugin_dir_path( __FILE__ ) . 'apple-actions/index/class-push.php'; -require_once plugin_dir_path( __FILE__ ) . 'apple-actions/index/class-delete.php'; +require_once __DIR__ . '/apple-actions/index/class-push.php'; +require_once __DIR__ . '/apple-actions/index/class-delete.php'; /** * This class is in charge of syncing posts creation, updates and deletions @@ -149,8 +149,8 @@ public function do_publish( $id, $post ) { // Proceed based on the current settings for auto publish and update. $updated = get_post_meta( $id, 'apple_news_api_id', true ); - if ( $updated && 'yes' !== $this->settings->api_autosync_update - || ! $updated && 'yes' !== $this->settings->api_autosync ) { + if ( ( $updated && 'yes' !== $this->settings->api_autosync_update ) + || ( ! $updated && 'yes' !== $this->settings->api_autosync ) ) { return; } diff --git a/admin/class-admin-apple-settings.php b/admin/class-admin-apple-settings.php index 8dbc5adb7..857736bb4 100644 --- a/admin/class-admin-apple-settings.php +++ b/admin/class-admin-apple-settings.php @@ -6,12 +6,12 @@ */ // Include dependencies. -require_once plugin_dir_path( __FILE__ ) . '../includes/apple-exporter/class-settings.php'; -require_once plugin_dir_path( __FILE__ ) . 'settings/class-admin-apple-settings-section.php'; -require_once plugin_dir_path( __FILE__ ) . 'settings/class-admin-apple-settings-section-api.php'; -require_once plugin_dir_path( __FILE__ ) . 'settings/class-admin-apple-settings-section-advanced.php'; -require_once plugin_dir_path( __FILE__ ) . 'settings/class-admin-apple-settings-section-post-types.php'; -require_once plugin_dir_path( __FILE__ ) . 'settings/class-admin-apple-settings-section-developer-tools.php'; +require_once dirname( __DIR__ ) . '/includes/apple-exporter/class-settings.php'; +require_once __DIR__ . '/settings/class-admin-apple-settings-section.php'; +require_once __DIR__ . '/settings/class-admin-apple-settings-section-api.php'; +require_once __DIR__ . '/settings/class-admin-apple-settings-section-advanced.php'; +require_once __DIR__ . '/settings/class-admin-apple-settings-section-post-types.php'; +require_once __DIR__ . '/settings/class-admin-apple-settings-section-developer-tools.php'; use Apple_Exporter\Settings; @@ -168,7 +168,7 @@ public function page_options_render() { /* phpcs:enable */ - include plugin_dir_path( __FILE__ ) . 'partials/page-options.php'; + include __DIR__ . '/partials/page-options.php'; } /** @@ -225,7 +225,7 @@ public function fetch_settings() { // Merge settings in the option with defaults. foreach ( $settings->all() as $key => $value ) { - $wp_value = ( empty( $wp_settings[ $key ] ) ) + $wp_value = ( ! isset( $wp_settings[ $key ] ) ) ? $value : $wp_settings[ $key ]; $settings->$key = $wp_value; diff --git a/admin/class-admin-apple-themes.php b/admin/class-admin-apple-themes.php index 7b8ca6a69..ea2b93f94 100644 --- a/admin/class-admin-apple-themes.php +++ b/admin/class-admin-apple-themes.php @@ -318,7 +318,7 @@ public function page_theme_edit_render() { /* phpcs:enable */ // Load the edit page. - include plugin_dir_path( __FILE__ ) . 'partials/page-theme-edit.php'; + include __DIR__ . '/partials/page-theme-edit.php'; } /** @@ -332,7 +332,7 @@ public function page_themes_render() { wp_die( esc_html__( 'You do not have permissions to access this page.', 'apple-news' ) ); } - include plugin_dir_path( __FILE__ ) . 'partials/page-themes.php'; + include __DIR__ . '/partials/page-themes.php'; } /** @@ -353,6 +353,15 @@ public function register_assets( $hook ) { return; } + // Get information about custom fonts from the channel. + require_once __DIR__ . '/apple-actions/index/class-channel.php'; + $admin_settings = new Admin_Apple_Settings(); + $channel_api = new \Apple_Actions\Index\Channel( $admin_settings->fetch_settings() ); + $channel = $channel_api->perform(); + $custom_fonts = ! empty( $channel->data->fonts ) && is_array( $channel->data->fonts ) + ? wp_list_pluck( $channel->data->fonts, 'name' ) + : []; + wp_enqueue_style( 'apple-news-themes-css', plugin_dir_url( __FILE__ ) . '../assets/css/themes.css', @@ -419,7 +428,9 @@ public function register_assets( $hook ) { 'apple-news-theme-edit-js', 'appleNewsThemeEdit', [ - 'fontNotice' => __( 'Font preview is only available on macOS', 'apple-news' ), + 'customFonts' => $custom_fonts, + 'customFontNotice' => __( 'Font preview may not be available', 'apple-news' ), + 'fontNotice' => __( 'Font preview is only available on macOS', 'apple-news' ), ] ); } diff --git a/admin/class-automation.php b/admin/class-automation.php index 77b7df585..09eba8c65 100644 --- a/admin/class-automation.php +++ b/admin/class-automation.php @@ -57,6 +57,13 @@ class Automation { ], ]; + /** + * Keeps track of the original title of posts by ID so we can refer to them when prepending. + * + * @var array + */ + private static array $original_titles = []; + /** * Initialize functionality of this class by registering hooks. */ @@ -66,6 +73,9 @@ public static function init(): void { add_filter( 'apple_news_active_theme', [ __CLASS__, 'filter__apple_news_active_theme' ], 0, 2 ); add_filter( 'apple_news_article_metadata', [ __CLASS__, 'filter__apple_news_article_metadata' ], 0, 2 ); add_filter( 'apple_news_exporter_slug', [ __CLASS__, 'filter__apple_news_exporter_slug' ], 0, 2 ); + add_filter( 'apple_news_exporter_title', [ __CLASS__, 'filter__apple_news_exporter_title' ], 0, 2 ); + add_filter( 'apple_news_generate_json', [ __CLASS__, 'filter__apple_news_generate_json' ], 0, 2 ); + add_filter( 'apple_news_metadata', [ __CLASS__, 'filter__apple_news_metadata' ], 0, 2 ); } /** @@ -168,6 +178,77 @@ public static function filter__apple_news_exporter_slug( $slug, $post_id ) { return $slug; } + /** + * A callback function for the apple_news_exporter_title filter. + * + * @param string $title The title to use. + * @param int $post_id The post ID associated with the title. + * + * @return string The filtered title value. + */ + public static function filter__apple_news_exporter_title( $title, $post_id ) { + // Make a backup of the original title so we can refer to it later. + self::$original_titles[ $post_id ] = $title; + + // Process rules. + $rules = self::get_automation_for_post( $post_id ); + foreach ( $rules as $rule ) { + if ( 'headline.prepend' === ( $rule['field'] ?? '' ) ) { + $title = sprintf( '%s %s', $rule['value'] ?? '', self::$original_titles[ $post_id ] ); + } + } + + return $title; + } + + /** + * Applies automation rules after the JSON has been generated. + * + * @param array $json Generated JSON for the article. + * @param int $post_id The post ID associated with the JSON. + * + * @return array Filtered JSON for the article. + */ + public static function filter__apple_news_generate_json( $json, $post_id ) { + $rules = self::get_automation_for_post( $post_id ); + $json['title'] = self::$original_titles[ $post_id ]; + foreach ( $rules as $rule ) { + if ( 'title.prepend' === ( $rule['field'] ?? '' ) ) { + $prepend = ! empty( $rule['value'] ) ? $rule['value'] . ' ' : ''; + $json['title'] = $prepend . self::$original_titles[ $post_id ]; + } + } + + return $json; + } + + /** + * Adds or sets metadata based on automation rules. + * + * @param array $meta Apple News metadata for a post. + * @param int $post_id The ID of the post. + * + * @return array The modified array of metadata. + */ + public static function filter__apple_news_metadata( $meta, $post_id ) { + // Trim down the list of matched rules to only those affecting metadata. + $metadata_rules = array_values( + array_filter( + self::get_automation_for_post( $post_id ), + function ( $rule ) { + return 'metadata' === self::get_fields()[ $rule['field'] ]['location'] ?? ''; + } + ) + ); + + // Loop through each matched rule and apply the value to metadata. + foreach ( $metadata_rules as $rule ) { + $meta[ $rule['field'] ] = $rule['value']; + } + + return $meta; + } + /** * Given a post ID, returns an array of matching automation rules. * @@ -210,37 +291,52 @@ public static function get_automation_rules(): array { */ public static function get_fields(): array { return [ - 'isHidden' => [ + 'contentGenerationType' => [ + 'location' => 'metadata', + 'type' => 'string', + 'label' => 'contentGenerationType', + ], + 'headline.prepend' => [ + 'location' => 'component', + 'type' => 'string', + 'label' => __( 'Headline: Prepend Text', 'apple-news' ), + ], + 'isHidden' => [ 'location' => 'article_metadata', - 'type' => 'boolean', - 'label' => __( 'isHidden', 'apple-news' ), + 'type' => 'string', + 'label' => 'isHidden', ], - 'isPaid' => [ + 'isPaid' => [ 'location' => 'article_metadata', - 'type' => 'boolean', - 'label' => __( 'isPaid', 'apple-news' ), + 'type' => 'string', + 'label' => 'isPaid', ], - 'isPreview' => [ + 'isPreview' => [ 'location' => 'article_metadata', - 'type' => 'boolean', - 'label' => __( 'isPreview', 'apple-news' ), + 'type' => 'string', + 'label' => 'isPreview', ], - 'isSponsored' => [ + 'isSponsored' => [ 'location' => 'article_metadata', - 'type' => 'boolean', - 'label' => __( 'isSponsored', 'apple-news' ), + 'type' => 'string', + 'label' => 'isSponsored', + ], + 'title.prepend' => [ + 'location' => 'component', + 'type' => 'string', + 'label' => __( 'Metadata Title: Prepend Text', 'apple-news' ), ], - 'links.sections' => [ + 'links.sections' => [ 'location' => 'article_metadata', 'type' => 'string', 'label' => __( 'Section', 'apple-news' ), ], - 'slug.#text#' => [ + 'slug.#text#' => [ 'location' => 'component', 'type' => 'string', 'label' => __( 'Slug', 'apple-news' ), ], - 'theme' => [ + 'theme' => [ 'location' => 'exporter', 'type' => 'string', 'label' => __( 'Theme', 'apple-news' ), diff --git a/admin/partials/metabox-publish.php b/admin/partials/metabox-publish.php index b08eb225e..6cd9a7809 100644 --- a/admin/partials/metabox-publish.php +++ b/admin/partials/metabox-publish.php @@ -48,25 +48,41 @@

-

+

-

+

-

+

-

+

- +
settings->get( 'api_autosync' ) diff --git a/admin/partials/page-json.php b/admin/partials/page-json.php index 90fb116f9..9604007ca 100644 --- a/admin/partials/page-json.php +++ b/admin/partials/page-json.php @@ -5,8 +5,10 @@ * phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable * * @global array $all_themes + * @global bool $component_can_be_parent * @global array $components * @global string $selected_component + * @global string $selected_subcomponent * @global string $selected_theme * @global array $specs * @global string $theme_admin_url @@ -34,11 +36,11 @@ '', '' ), - array( - 'a' => array( - 'href' => array(), - ), - ) + [ + 'a' => [ + 'href' => [], + ], + ] ) ?>

@@ -63,11 +65,11 @@ '', '' ), - array( - 'a' => array( - 'href' => array(), - ), - ) + [ + 'a' => [ + 'href' => [], + ], + ] ) ?>

@@ -87,7 +89,7 @@
-
+ + before_section(); ?> is_hidden() ) { - include plugin_dir_path( __FILE__ ) . 'page-options-section-hidden.php'; + include __DIR__ . '/page-options-section-hidden.php'; } else { - include plugin_dir_path( __FILE__ ) . 'page-options-section.php'; + include __DIR__ . '/page-options-section.php'; } $apple_section->after_section(); ?> diff --git a/admin/partials/page-single-push.php b/admin/partials/page-single-push.php deleted file mode 100644 index e6719ab06..000000000 --- a/admin/partials/page-single-push.php +++ /dev/null @@ -1,127 +0,0 @@ - -
-

post_title ); ?>”

- - -
-

- -
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- ID ); ?> -
- ID ); ?> -

-
-
- -
- -
- -
- -
- -

-
- -

-
- - -

-
- - -

- - -

-
-
diff --git a/admin/settings/class-admin-apple-settings-section-advanced.php b/admin/settings/class-admin-apple-settings-section-advanced.php index c2c44d96c..fda8cd0b9 100644 --- a/admin/settings/class-admin-apple-settings-section-advanced.php +++ b/admin/settings/class-admin-apple-settings-section-advanced.php @@ -32,22 +32,22 @@ public function __construct( $page ) { // Add the settings. $this->settings = [ - 'component_alerts' => [ + 'component_alerts' => [ 'label' => __( 'Component Alerts', 'apple-news' ), 'type' => [ 'none', 'warn', 'fail' ], 'description' => __( 'If a post has a component that is unsupported by Apple News, choose "none" to generate no alert, "warn" to provide an admin warning notice, or "fail" to generate a notice and stop publishing.', 'apple-news' ), ], - 'use_remote_images' => [ + 'use_remote_images' => [ 'label' => __( 'Use Remote Images?', 'apple-news' ), 'type' => [ 'yes', 'no' ], 'description' => __( 'Allow the Apple News API to retrieve images remotely rather than bundle them. This setting is recommended if you are having any issues with publishing images. If your images are not publicly accessible, such as on a development site, you cannot use this feature.', 'apple-news' ), ], - 'full_bleed_images' => [ + 'full_bleed_images' => [ 'label' => __( 'Use Full-Bleed Images?', 'apple-news' ), 'type' => [ 'yes', 'no' ], 'description' => __( 'If set to yes, images that are centered or have no alignment will span edge-to-edge rather than being constrained within the body margins.', 'apple-news' ), ], - 'html_support' => [ + 'html_support' => [ 'label' => __( 'Enable HTML support?', 'apple-news' ), 'type' => [ 'yes', 'no' ], 'description' => sprintf( @@ -57,6 +57,20 @@ public function __construct( $page ) { '' ), ], + 'in_article_position' => [ + 'label' => __( 'Position of In Article Module', 'apple-news' ), + 'type' => 'number', + 'min' => 3, + 'max' => 99, + 'step' => 1, + 'description' => __( 'If you have configured an In Article module via Customize JSON, the position that the module should be inserted into. Defaults to 3, which is after the third content block in the article body (e.g., the third paragraph).', 'apple-news' ), + ], + 'aside_component_class' => [ + 'label' => __( 'Aside Content CSS Class', 'apple-news' ), + 'type' => 'text', + 'description' => __( 'Enter a CSS class name that will be used to generate the Aside component. Do not prefix with a period.', 'apple-news' ), + 'required' => false, + ], ]; // Add the groups. @@ -71,7 +85,11 @@ public function __construct( $page ) { ], 'format' => [ 'label' => __( 'Format Settings', 'apple-news' ), - 'settings' => [ 'html_support' ], + 'settings' => [ 'html_support', 'in_article_position' ], + ], + 'aside' => [ + 'label' => __( 'Aside Component', 'apple-news' ), + 'settings' => [ 'aside_component_class' ], ], ]; diff --git a/admin/settings/class-admin-apple-settings-section.php b/admin/settings/class-admin-apple-settings-section.php index d32215834..4598d508c 100644 --- a/admin/settings/class-admin-apple-settings-section.php +++ b/admin/settings/class-admin-apple-settings-section.php @@ -122,6 +122,8 @@ class Admin_Apple_Settings_Section extends Apple_News { 'name' => [], 'value' => [], 'placeholder' => [], + 'min' => [], + 'max' => [], 'step' => [], 'type' => [], 'required' => [], @@ -260,7 +262,7 @@ public function render_field( $args ) { $type = $this->get_type_for( $name ); - // If the field has it's own render callback, use that here. + // If the field has its own render callback, use that here. // This is because the options page doesn't actually use do_settings_section. if ( ! empty( $callback ) ) { return call_user_func( $callback, $type ); @@ -320,6 +322,8 @@ public function render_field( $args ) { $field = ''; } elseif ( 'textarea' === $type ) { $field = ''; + } elseif ( 'number' === $type ) { + $field = ''; } else { // If nothing else matches, it's a string. $field = ''; @@ -365,6 +369,18 @@ public function render_field( $args ) { esc_attr( $name ), esc_attr( $value ) ); + } elseif ( 'number' === $type ) { + return sprintf( + $field, + esc_attr( $name ), + esc_attr( $name ), + esc_attr( $value ), + 5, + esc_attr( $this->get_min_for( $name ) ), + esc_attr( $this->get_max_for( $name ) ), + esc_attr( $this->get_step_for( $name ) ), + esc_attr( $this->is_required( $name ) ) + ); } else { return sprintf( $field, @@ -374,7 +390,6 @@ public function render_field( $args ) { intval( $size ), esc_attr( $this->is_required( $name ) ) ); - } } @@ -404,6 +419,45 @@ protected function get_description_for( $name ) { return empty( $this->settings[ $name ]['description'] ) ? '' : $this->settings[ $name ]['description']; } + /** + * Get the maximum value for a field. + * + * @param string $name The name of the field for which to fetch a maximum value. + * + * @return int|float|string The maximum value, or an empty string if not set. + */ + protected function get_max_for( $name ) { + return isset( $this->settings[ $name ]['max'] ) && ( is_int( $this->settings[ $name ]['max'] ) || is_float( $this->settings[ $name ]['max'] ) ) + ? $this->settings[ $name ]['max'] + : ''; + } + + /** + * Get the minimum value for a field. + * + * @param string $name The name of the field for which to fetch a minimum value. + * + * @return int|float|string The minimum value, or an empty string if not set. + */ + protected function get_min_for( $name ) { + return isset( $this->settings[ $name ]['min'] ) && ( is_int( $this->settings[ $name ]['min'] ) || is_float( $this->settings[ $name ]['min'] ) ) + ? $this->settings[ $name ]['min'] + : ''; + } + + /** + * Get the stepping value for a field. + * + * @param string $name The name of the field for which to fetch a stepping value. + * + * @return int|float The stepping value, or a default value of 1 if not set. + */ + protected function get_step_for( $name ) { + return isset( $this->settings[ $name ]['step'] ) && ( is_int( $this->settings[ $name ]['step'] ) || is_float( $this->settings[ $name ]['step'] ) ) + ? $this->settings[ $name ]['step'] + : 1; + } + /** * Get the size for a field. * diff --git a/apple-news.php b/apple-news.php index a8a50bd83..e33537137 100644 --- a/apple-news.php +++ b/apple-news.php @@ -14,7 +14,7 @@ * Plugin Name: Publish to Apple News * Plugin URI: http://github.com/alleyinteractive/apple-news * Description: Export and sync posts to Apple format. - * Version: 2.4.8 + * Version: 2.5.0 * Author: Alley * Author URI: https://alley.com * Text Domain: apple-news @@ -40,7 +40,7 @@ function apple_news_date( $format, $timestamp = null, $timezone = null ) { return date( $format, $timestamp ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date } -require_once plugin_dir_path( __FILE__ ) . './includes/meta.php'; +require_once __DIR__ . '/includes/meta.php'; if ( ! defined( 'WPINC' ) ) { die; @@ -57,7 +57,7 @@ function apple_news_activate_wp_plugin() { } } -require plugin_dir_path( __FILE__ ) . 'includes/apple-exporter/class-settings.php'; +require __DIR__ . '/includes/apple-exporter/class-settings.php'; /** * Deactivate the plugin. @@ -76,8 +76,8 @@ function apple_news_uninstall_wp_plugin() { } // Initialize plugin class. -require plugin_dir_path( __FILE__ ) . 'includes/class-apple-news.php'; -require plugin_dir_path( __FILE__ ) . 'admin/class-admin-apple-news.php'; +require __DIR__ . '/includes/class-apple-news.php'; +require __DIR__ . '/admin/class-admin-apple-news.php'; /** * Load plugin textdomain. @@ -85,7 +85,7 @@ function apple_news_uninstall_wp_plugin() { * @since 0.9.0 */ function apple_news_load_textdomain() { - load_plugin_textdomain( 'apple-news', false, plugin_dir_path( __FILE__ ) . '/lang' ); + load_plugin_textdomain( 'apple-news', false, __DIR__ . '/lang' ); } add_action( 'plugins_loaded', 'apple_news_load_textdomain' ); @@ -101,7 +101,7 @@ function apple_news_get_plugin_data() { if ( ! function_exists( 'get_plugin_data' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } - return get_plugin_data( plugin_dir_path( __FILE__ ) . '/apple-news.php' ); + return get_plugin_data( __DIR__ . '/apple-news.php' ); } new Admin_Apple_News(); @@ -200,14 +200,3 @@ function apple_news_is_classic_editor_plugin_active() { return false; } - -/** - * Given a user ID, a post ID, and an action, determines whether a user can - * perform the action or not. - * - * @param int $post_id The ID of the post to check. - * @param string $action The action to check. One of 'publish', 'update', 'delete'. - * @param int $user_id The user ID to check. - * - * @return bool True if the user can perform the action, false otherwise. - */ diff --git a/assets/js/admin-settings/rule.jsx b/assets/js/admin-settings/rule.jsx index c07a40aee..7ff51878f 100644 --- a/assets/js/admin-settings/rule.jsx +++ b/assets/js/admin-settings/rule.jsx @@ -28,6 +28,20 @@ function Rule({ taxonomies, themes, } = AppleNewsAutomationConfig; + let fieldType = ''; + if (field === 'contentGenerationType') { + fieldType = 'contentGenerationType'; + } else if (['isHidden', 'isPaid', 'isPreview', 'isSponsored'].includes(field)) { + fieldType = 'boolean-select'; + } else if (field === 'links.sections') { + fieldType = 'sections'; + } else if (field === 'theme') { + fieldType = 'themes'; + } else if (fields[field]?.type === 'boolean') { + fieldType = 'boolean'; + } else if (fields[field]?.type === 'string') { + fieldType = 'string'; + } return ( - {fields[field]?.label === 'Section' ? ( + {fieldType === 'contentGenerationType' ? ( + onUpdate('value', next)} + options={[ + { value: '', label: __('None', 'apple-news') }, + { value: 'AI', label: __('AI', 'apple-news') }, + ]} + value={value} + /> + ) : null} + {fieldType === 'sections' ? ( ) : null} - {fields[field]?.type === 'boolean' ? ( + {fieldType === 'boolean-select' ? ( + onUpdate('value', next)} + options={[ + { value: '', label: __('Channel Default', 'apple-news') }, + { value: 'true', label: __('True', 'apple-news') }, + { value: 'false', label: __('False', 'apple-news') }, + ]} + value={value} + /> + ) : null} + {fieldType === 'boolean' ? ( onUpdate('value', next.toString())} /> ) : null} - {fields[field]?.label === 'Slug' ? ( + {fieldType === 'string' ? ( ) : null} - {fields[field]?.label === 'Theme' ? ( + {fieldType === 'themes' ? ( - - -