'', 'additional' => [], ]; foreach ( (array) $revisionData['keyphrases'] as $k => $keyphrase ) { $keyphrase = (array) $keyphrase; if ( 'focus' === $k ) { $reduced['focus'] = $keyphrase['keyphrase'] ?? ''; } elseif ( 'additional' === $k ) { $additionalPhrases = array_map( 'trim', array_column( $keyphrase, 'keyphrase' ) ); sort( $additionalPhrases ); $reduced['additional'] = $additionalPhrases; } } $revisionData['keyphrases'] = $reduced; } } /** * Reduce a field to an array containing only the `$column` key values. * * @since 4.6.5 * * @param array $revisionData The Revision Data (passed by reference). * @return void */ public function reduceToColumn( &$revisionData, $field, $column ) { if ( array_key_exists( $field, $revisionData ) ) { $reduced = []; foreach ( (array) $revisionData[ $field ] as $value ) { if ( is_object( $value ) && isset( $value->$column ) ) { $reduced[] = $value->$column; } elseif ( is_array( $value ) && isset( $value[ $column ] ) ) { $reduced[] = $value[ $column ]; } elseif ( is_string( $value ) || is_numeric( $value ) ) { // If the value is already a scalar, use it directly. $reduced[] = $value; } } sort( $reduced ); $revisionData[ $field ] = array_filter( array_map( 'trim', $reduced ) ); } } /** * Reduce the 'schema' field to an array containing the 'customGraphs', 'default' and 'graphs' keys. * Get rid of other keys as e.g. 'blockGraphs'. * * @since 4.4.0 * * @param array $revisionData The Revision Data (passed by reference). * @return void */ public function reduceSchema( &$revisionData ) { if ( array_key_exists( 'schema', $revisionData ) ) { $reduced = [ 'default' => [], 'graphs' => [], 'customGraphs' => [] ]; foreach ( (array) $revisionData['schema'] as $k => $data ) { if ( 'default' === $k ) { // Prevent `$data['graphName']` from being watched. $reduced[ $k ] = array_diff_key( $data, [ 'graphName' => '' ] ); } if ( in_array( $k, [ 'graphs', 'customGraphs' ], true ) ) { $reduced[ $k ] = $data; } } aioseo()->helpers->arrayRecursiveKsort( $reduced ); $revisionData['schema'] = $reduced; } } /** * Retrieve the formatted Revision Data array for comparison. * This was first created, so we have the Focus/Additional Keyphrases fields separate on the UI. * * @since 4.4.0 * * @param array $data The Revision Data. * @param ObjectRevisions $objectRevisions An instance of the ObjectRevisions class. * @return array The formatted Revision Data. */ public function formatRevisionData( $data, $objectRevisions ) { $wpObject = $objectRevisions->getWpObject(); // 1. Before filling the fake columns 'focus' and 'additional' check if 'keyphrases' is present otherwise they'd show for Terms. if ( isset( $data['keyphrases'] ) ) { $data['focus'] = isset( $data['keyphrases']['focus'] ) ? $data['keyphrases']['focus'] : ''; $data['additional'] = isset( $data['keyphrases']['additional'] ) ? $data['keyphrases']['additional'] : ''; } // 2. Fill the fake column 'robots_all_settings'. $data['robots_all_settings'] = [ 'default' => ! empty( $data['robots_default'] ) ]; // 3. If 'robots_default' is not true, then show the chosen custom settings. if ( empty( $data['robots_default'] ) ) { $data['robots_all_settings'] = [ 'default' => ! empty( $data['robots_default'] ), 'noindex' => ! empty( $data['robots_noindex'] ), 'nofollow' => ! empty( $data['robots_nofollow'] ), 'noarchive' => ! empty( $data['robots_noarchive'] ), 'notranslate' => ! empty( $data['robots_notranslate'] ), 'noimageindex' => ! empty( $data['robots_noimageindex'] ), 'nosnippet' => ! empty( $data['robots_nosnippet'] ), 'noodp' => ! empty( $data['robots_noodp'] ), 'maxsnippet' => isset( $data['robots_max_snippet'] ) ? $data['robots_max_snippet'] : '', 'maxvideopreview' => isset( $data['robots_max_videopreview'] ) ? $data['robots_max_videopreview'] : '', 'maximagepreview' => isset( $data['robots_max_imagepreview'] ) ? $data['robots_max_imagepreview'] : '' ]; } // 4. Don't ever show these fields alone on the UI. unset( $data['keyphrases'] ); unset( $data['robots_default'] ); unset( $data['robots_noindex'] ); unset( $data['robots_noarchive'] ); unset( $data['robots_nosnippet'] ); unset( $data['robots_nofollow'] ); unset( $data['robots_noimageindex'] ); unset( $data['robots_noodp'] ); unset( $data['robots_notranslate'] ); unset( $data['robots_max_snippet'] ); unset( $data['robots_max_videopreview'] ); unset( $data['robots_max_imagepreview'] ); // 5. Don't show certain fields for attachments. if ( 'attachment' === ( $wpObject->post_type ?? '' ) ) { unset( $data['pillar_content'] ); } // 6. Prepare 'breadcrumb_settings' for the UI. if ( array_key_exists( 'breadcrumb_settings', $data ) ) { // If 'breadcrumb_settings' is set to the default value, its value is actually `null`, and the `default` key is not present, // so we need this workaround for making sure {@see $this->prepareRevisionDataFieldValue()} does its job. $data['breadcrumb_settings'] = is_null( $data['breadcrumb_settings'] ) ? [ 'default' => true ] : $data['breadcrumb_settings']; } return $data; } /** * Prepare the Revision Data field value for comparison rendering in Vue. * Useful so the user sees something similar to what is displayed while editing a post/term. * * @since 4.4.0 * * @param string $field The Revision Data field. E.g. 'title' or 'description'. * @param string|array $value The field value. * @param ObjectRevisions $objectRevisions An instance of the ObjectRevisions class. * @return string The formatted value. */ public function prepareRevisionDataFieldValue( $field, $value, $objectRevisions ) { $wpObject = $objectRevisions->getWpObject(); if ( aioseo()->helpers->isWooCommerceProductAttribute( $wpObject ) ) { $wpObject->taxonomy = 'product_attributes'; } switch ( $field ) { case 'title': case 'og_title': case 'twitter_title': if ( empty( $value ) ) { $value = 'term' === $objectRevisions->objectType ? aioseo()->meta->title->getTaxonomyTitle( $wpObject->taxonomy ) : aioseo()->meta->title->getPostTitle( $objectRevisions->objectId, true ); } $value = aioseo()->helpers->decodeHtmlEntities( $value ); break; case 'description': case 'og_description': case 'twitter_description': if ( empty( $value ) ) { $value = 'term' === $objectRevisions->objectType ? aioseo()->meta->description->getTaxonomyDescription( $wpObject->taxonomy ) : aioseo()->meta->description->getPostDescription( $objectRevisions->objectId, true ); } $value = aioseo()->helpers->decodeHtmlEntities( $value ); break; case 'og_object_type': case 'twitter_card': if ( ! empty( $value ) ) { $value = "{optionValue|$value}"; // Is replaced with option label in Vue. } break; case 'og_image_type': case 'twitter_image_type': if ( ! empty( $value ) ) { aioseo()->social->image->useCache = false; $type = strpos( $field, 'twitter' ) !== false ? 'twitter' : 'facebook'; $image = aioseo()->social->image->getImage( $type, $value, $wpObject ); $url = is_array( $image ) ? $image[0] : $image; $value = "{optionValue|$value}"; // Is replaced with the option label in Vue. $value .= "{imageUrl|$url}"; // Is replaced with the image preview in Vue. } break; case 'og_image_custom_url': case 'twitter_image_custom_url': if ( ! empty( $value ) ) { $value = "$value{imageUrl|$value}"; // Is replaced with the image preview in Vue. } break; case 'canonical_url': if ( empty( $value ) ) { // Translators: 1 - The string 'Post' or 'Term'. $value = sprintf( esc_html__( 'Default (the %s URL)', 'aioseo-pro' ), ucfirst( $objectRevisions->objectType ) ); } break; case 'focus': $value = empty( $value['keyphrase'] ) ? '' : $value['keyphrase']; break; case 'additional': case 'og_article_tags': case 'keywords': $searchKey = 'additional' === $field ? 'keyphrase' : 'label'; $items = ! empty( $value[0][ $searchKey ] ) ? array_map( 'trim', array_column( $value, $searchKey ) ) : []; sort( $items ); $value = implode( "\n", $items ); $value = esc_html( $value ); break; case 'schema': aioseo()->schema->reset(); $value = aioseo()->schema->getUserDefinedSchemaOutput( $objectRevisions->objectId, $value ); break; case 'twitter_use_og': case 'pillar_content': if ( is_bool( $value ) ) { $value = true === $value ? esc_html__( 'Yes', 'aioseo-pro' ) : esc_html__( 'No', 'aioseo-pro' ); } break; case 'robots_all_settings': $settings = []; foreach ( $value as $optionName => $optionValue ) { $settings[] = '{' . "$optionName|$optionValue" . '}'; } $value = implode( "\n", $settings ); break; case 'priority': $value = is_null( $value ) ? 'default' : strval( $value ); break; case 'primary_term': if ( is_array( $value ) ) { ksort( $value ); $items = []; foreach ( $value as $taxonomy => $termId ) { // Fallback to the raw term ID and taxonomy key if the term object or taxonomy object is not available anymore. $items[ $taxonomy ] = $taxonomy . ': ' . $termId; $termObject = get_term( $termId, $taxonomy ); $taxonomyObject = get_taxonomy( $taxonomy ); // If the term object and taxonomy object are available, use the term name and taxonomy singular name. if ( is_object( $termObject ) && is_object( $taxonomyObject ) ) { $items[ $taxonomy ] = ( $taxonomyObject->labels->singular_name ?? $termObject->taxonomy ) . ': ' . $termObject->name; } } $value = implode( "\n", $items ); $value = esc_html( $value ); } break; case 'breadcrumb_settings': if ( is_array( $value ) ) { $settings = []; foreach ( $value as $optionName => $optionValue ) { if ( 'taxonomy' === $optionName ) { $wpTaxonomy = get_taxonomy( $optionValue ); $optionValue = is_object( $wpTaxonomy ) ? ( $wpTaxonomy->labels->singular_name ?? $optionValue ) : $optionValue; } $settings[] = '{' . "$optionName|$optionValue" . '}'; } $value = implode( "\n", $settings ); $value = esc_html( $value ); } break; default: if ( is_null( $value ) ) { $value = ''; } break; } return is_string( $value ) ? $value : ''; } /** * Retrieve a human-readable HTML representation of the difference. * * @since 4.4.0 * * @param string $leftString "old" (left) version. * @param string $rightString "new" (right) version. * @param string $field The Revision Data field. * @return string Empty string if parameters are equivalent or HTML with differences. */ public function renderFieldDiff( $leftString, $rightString, $field ) { $args = [ 'show_split_view' => true, ]; switch ( $field ) { case 'additional': case 'og_article_tags': case 'keywords': case 'primary_term': $diff = $this->getTagsDiff( $leftString, $rightString ); break; case 'schema': $diff = $this->getSchemaDiff( $leftString, $rightString ); break; case 'robots_all_settings': case 'breadcrumb_settings': $diff = $this->getSettingsDiff( $leftString, $rightString ); break; default: $leftString = normalize_whitespace( $leftString ); $rightString = normalize_whitespace( $rightString ); $leftLines = explode( "\n", $leftString ); $rightLines = explode( "\n", $rightString ); $textDiff = new \Text_Diff( $leftLines, $rightLines ); $renderer = new TextDiffRendererTable( $args ); $diff = $renderer->render( $textDiff ); break; } if ( ! $diff ) { return ''; } $isSplitView = ! empty( $args['show_split_view'] ); $isSplitViewClass = $isSplitView ? ' is-split-view' : ''; $output = "
$leftSchema"; $html .= '
$rightSchema"; $html .= '