← All changes
|
includes/abilities/content/class-update-post-seo.php
+72
-2
2.6.0
→
2.11.0
View file →
| @@ -10,8 +10,9 @@ | ||
| 10 | 10 | namespace ThinkRank\Abilities\Content; |
| 11 | 11 | |
| 12 | 12 | use ThinkRank\Abilities\Ability_Base; |
| 13 | 13 | use ThinkRank\Admin\Metabox_Manager; |
| 14 | +use ThinkRank\SEO\Object_Redirect; | |
| 14 | 15 | |
| 15 | 16 | if ( ! defined( 'ABSPATH' ) ) { |
| 16 | 17 | exit; // Exit if accessed directly. |
| 17 | 18 | } |
| @@ -50,8 +51,17 @@ | ||
| 50 | 51 | 'properties' => [ |
| 51 | 52 | 'title' => [ 'type' => 'string' ], |
| 52 | 53 | 'description' => [ 'type' => 'string' ], |
| 53 | 54 | 'canonical_url' => [ 'type' => 'string' ], |
| 55 | + 'redirect_url' => [ | |
| 56 | + 'type' => 'string', | |
| 57 | + 'description' => __( 'Send visitors from this post to this URL. Empty string removes the redirect. Requires ThinkRank Pro.', 'thinkrank' ), | |
| 58 | + ], | |
| 59 | + 'redirect_type' => [ | |
| 60 | + 'type' => 'integer', | |
| 61 | + 'enum' => Object_Redirect::TYPES, | |
| 62 | + 'description' => __( 'Redirect status code. Defaults to 301.', 'thinkrank' ), | |
| 63 | + ], | |
| 54 | 64 | 'focus_keyword' => [ 'type' => 'string' ], |
| 55 | 65 | 'focus_keywords' => [ |
| 56 | 66 | 'type' => 'array', |
| 57 | 67 | 'items' => [ 'type' => 'string' ], |
| @@ -58,8 +68,16 @@ | ||
| 58 | 68 | ], |
| 59 | 69 | 'robots_meta_enabled' => [ 'type' => 'boolean' ], |
| 60 | 70 | 'robots_meta' => [ 'type' => 'object' ], |
| 61 | 71 | 'advanced_robots_meta' => [ 'type' => 'object' ], |
| 72 | + 'exclude_from_search' => [ | |
| 73 | + 'type' => 'boolean', | |
| 74 | + 'description' => __( 'Keep this post out of this site\'s own search results. Not noindex: search engines are unaffected, and the post keeps its own URL. Use robots_meta for search engines.', 'thinkrank' ), | |
| 75 | + ], | |
| 76 | + 'exclude_from_archives' => [ | |
| 77 | + 'type' => 'boolean', | |
| 78 | + 'description' => __( 'Keep this post out of category, tag, author, date and blog listings on this site. Not noindex: search engines are unaffected, and the post keeps its own URL, its feed entry and its sitemap entry.', 'thinkrank' ), | |
| 79 | + ], | |
| 62 | 80 | 'og_title' => [ 'type' => 'string' ], |
| 63 | 81 | 'og_description' => [ 'type' => 'string' ], |
| 64 | 82 | 'og_image' => [ 'type' => 'string' ], |
| 65 | 83 | 'twitter_title' => [ 'type' => 'string' ], |
| @@ -131,10 +149,24 @@ | ||
| 131 | 149 | [ 'status' => 400 ] |
| 132 | 150 | ); |
| 133 | 151 | } |
| 134 | 152 | |
| 135 | - ( new Metabox_Manager() )->save_seo_fields( $post_id, $fields ); | |
| 153 | + $manager = new Metabox_Manager(); | |
| 154 | + $manager->save_seo_fields( $post_id, $fields ); | |
| 136 | 155 | |
| 156 | + // Everything else is stored unconditionally; the redirect can be | |
| 157 | + // refused (no Pro, plain permalinks, a destination that is this post's | |
| 158 | + // own URL). Reporting success for a redirect that was not written would | |
| 159 | + // leave the caller believing the site now redirects when it does not. | |
| 160 | + $redirect_error = $manager->get_last_redirect_error(); | |
| 161 | + if ( null !== $redirect_error ) { | |
| 162 | + return new \WP_Error( | |
| 163 | + $redirect_error->get_error_code(), | |
| 164 | + $redirect_error->get_error_message(), | |
| 165 | + [ 'status' => 400 ] | |
| 166 | + ); | |
| 167 | + } | |
| 168 | + | |
| 137 | 169 | return [ |
| 138 | 170 | 'success' => true, |
| 139 | 171 | 'message' => __( 'Post SEO metadata updated.', 'thinkrank' ), |
| 140 | 172 | 'post_id' => $post_id, |
| @@ -154,8 +186,9 @@ | ||
| 154 | 186 | $simple = [ |
| 155 | 187 | 'title' => 'thinkrank_seo_title', |
| 156 | 188 | 'description' => 'thinkrank_meta_description', |
| 157 | 189 | 'canonical_url' => 'thinkrank_canonical_url', |
| 190 | + 'redirect_url' => 'thinkrank_redirect_url', | |
| 158 | 191 | 'focus_keyword' => 'thinkrank_focus_keyword', |
| 159 | 192 | 'og_title' => 'thinkrank_og_title', |
| 160 | 193 | 'og_description' => 'thinkrank_og_description', |
| 161 | 194 | 'og_image' => 'thinkrank_og_image', |
| @@ -169,8 +202,15 @@ | ||
| 169 | 202 | $fields[ $field ] = (string) $settings[ $key ]; |
| 170 | 203 | } |
| 171 | 204 | } |
| 172 | 205 | |
| 206 | + // Only meaningful alongside a destination: sending a code on its own | |
| 207 | + // would be read by save_object_redirect() as "no redirect submitted" | |
| 208 | + // and dropped, so requiring the pair keeps the payload honest. | |
| 209 | + if ( array_key_exists( 'redirect_type', $settings ) && array_key_exists( 'redirect_url', $settings ) ) { | |
| 210 | + $fields['thinkrank_redirect_type'] = Object_Redirect::normalize_type( $settings['redirect_type'] ); | |
| 211 | + } | |
| 212 | + | |
| 173 | 213 | if ( array_key_exists( 'focus_keywords', $settings ) && is_array( $settings['focus_keywords'] ) ) { |
| 174 | 214 | $fields['thinkrank_focus_keywords'] = (string) wp_json_encode( array_values( $settings['focus_keywords'] ) ); |
| 175 | 215 | } |
| 176 | 216 | |
| @@ -176,9 +216,11 @@ | ||
| 176 | 216 | |
| 177 | 217 | $has_robots = array_key_exists( 'robots_meta', $settings ) || array_key_exists( 'advanced_robots_meta', $settings ); |
| 178 | 218 | |
| 179 | 219 | if ( array_key_exists( 'robots_meta_enabled', $settings ) ) { |
| 180 | - $fields['thinkrank_robots_meta_enabled'] = (int) (bool) $settings['robots_meta_enabled']; | |
| 220 | + // Sanitized for the same reason as the visibility flags below: the | |
| 221 | + // string "false" passes the schema and is truthy in PHP. | |
| 222 | + $fields['thinkrank_robots_meta_enabled'] = (int) rest_sanitize_boolean( $settings['robots_meta_enabled'] ); | |
| 181 | 223 | } elseif ( $has_robots ) { |
| 182 | 224 | // The metabox only persists robots blobs when the toggle key is |
| 183 | 225 | // present; default to the currently stored value (or 1). |
| 184 | 226 | $existing = get_post_meta( $post_id, '_thinkrank_robots_meta_enabled', true ); |
| @@ -190,8 +232,36 @@ | ||
| 190 | 232 | } |
| 191 | 233 | |
| 192 | 234 | if ( array_key_exists( 'advanced_robots_meta', $settings ) ) { |
| 193 | 235 | $fields['thinkrank_advanced_robots_meta'] = (string) wp_json_encode( (array) $settings['advanced_robots_meta'] ); |
| 236 | + } | |
| 237 | + | |
| 238 | + // On-site visibility (#633). get-post-seo already reports both, because | |
| 239 | + // it returns get_post_metadata() wholesale — so without these an agent | |
| 240 | + // could read a field it had no way to change, which is the worst shape | |
| 241 | + // a tool pair can have. | |
| 242 | + // | |
| 243 | + // The value goes through Metabox_Manager::save_visibility_meta(), which | |
| 244 | + // stores 1 on a truthy submission and DELETES the meta on a falsy one, | |
| 245 | + // so '' is how a flag is cleared. Passing the boolean straight through | |
| 246 | + // would work too; the string keeps this in the same shape as every | |
| 247 | + // other field in this map. | |
| 248 | + // | |
| 249 | + // rest_sanitize_boolean() rather than PHP truthiness: WP_Ability only | |
| 250 | + // VALIDATES the input against the schema, it never sanitizes it, and | |
| 251 | + // rest_is_boolean() accepts the strings 'true'/'false'/'1'/'0' for a | |
| 252 | + // boolean property. So a caller that sends "false" — which the schema | |
| 253 | + // accepts — reaches this line with a truthy string and would have the | |
| 254 | + // flag SET, the opposite of what it asked for, reported as a success. | |
| 255 | + foreach ( | |
| 256 | + [ | |
| 257 | + 'exclude_from_search' => 'thinkrank_exclude_from_search', | |
| 258 | + 'exclude_from_archives' => 'thinkrank_exclude_from_archives', | |
| 259 | + ] as $key => $field | |
| 260 | + ) { | |
| 261 | + if ( array_key_exists( $key, $settings ) ) { | |
| 262 | + $fields[ $field ] = rest_sanitize_boolean( $settings[ $key ] ) ? '1' : ''; | |
| 263 | + } | |
| 194 | 264 | } |
| 195 | 265 | |
| 196 | 266 | return $fields; |
| 197 | 267 | } |