| @@ -110,8 +110,13 @@ | ||
| 110 | 110 | 'required' => false, |
| 111 | 111 | 'type' => 'integer', |
| 112 | 112 | 'sanitize_callback' => 'absint', |
| 113 | 113 | ), |
| 114 | + 'post_type' => array( | |
| 115 | + 'required' => false, | |
| 116 | + 'type' => 'string', | |
| 117 | + 'sanitize_callback' => 'sanitize_key', | |
| 118 | + ), | |
| 114 | 119 | ), |
| 115 | 120 | ) ); |
| 116 | 121 | // Hash password endpoint. |
| 117 | 122 | register_rest_route( 'passster/v1', '/hash', array( |
| @@ -125,8 +130,13 @@ | ||
| 125 | 130 | 'sanitize_callback' => function ( $value ) { |
| 126 | 131 | return wp_unslash( $value ); |
| 127 | 132 | }, |
| 128 | 133 | ), |
| 134 | + 'post_id' => array( | |
| 135 | + 'required' => true, | |
| 136 | + 'type' => 'integer', | |
| 137 | + 'sanitize_callback' => 'absint', | |
| 138 | + ), | |
| 129 | 139 | ), |
| 130 | 140 | ) ); |
| 131 | 141 | // reCAPTCHA/hCaptcha validation endpoint. |
| 132 | 142 | register_rest_route( 'passster/v1', '/captcha', array( |
| @@ -203,8 +213,9 @@ | ||
| 203 | 213 | $redirect = $request->get_param( 'redirect' ); |
| 204 | 214 | $protection = $request->get_param( 'protection' ); |
| 205 | 215 | $acf = $request->get_param( 'acf' ); |
| 206 | 216 | $term_id = absint( $request->get_param( 'term_id' ) ); |
| 217 | + $post_type_param = sanitize_key( (string) $request->get_param( 'post_type' ) ); | |
| 207 | 218 | // Default error response. |
| 208 | 219 | $error_message = $options['error'] ?? __( 'Invalid password.', 'content-protector' ); |
| 209 | 220 | $remove_spaces = apply_filters( 'passster_remove_spaces_from_list', true ); |
| 210 | 221 | if ( empty( $protection ) ) { |
| @@ -223,8 +234,9 @@ | ||
| 223 | 234 | $remove_spaces |
| 224 | 235 | ); |
| 225 | 236 | if ( $result['valid'] ) { |
| 226 | 237 | do_action( 'passster_validation_success', $input ); |
| 238 | + PS_Helper::remember_unlock( hash_hmac( 'sha256', $input, get_option( 'passster_secure_key' ) ) ); | |
| 227 | 239 | $term_redirect = get_term_meta( $term_id, 'passster_redirect_url', true ); |
| 228 | 240 | if ( !empty( $term_redirect ) ) { |
| 229 | 241 | return new \WP_REST_Response(array( |
| 230 | 242 | 'success' => true, |
| @@ -240,8 +252,39 @@ | ||
| 240 | 252 | 'success' => false, |
| 241 | 253 | 'error' => $error_message, |
| 242 | 254 | ), 200); |
| 243 | 255 | } |
| 256 | + // Post type archive protection: post_type is passed directly from the form | |
| 257 | + // (no single post ID exists to key an unlock request off of on an archive page). | |
| 258 | + if ( !empty( $post_type_param ) && class_exists( 'passster\\PS_Post_Type_Lock' ) && PS_Post_Type_Lock::is_post_type_protected( $post_type_param ) ) { | |
| 259 | + $post_type_config = PS_Post_Type_Lock::get_post_type_config( $post_type_param ); | |
| 260 | + $result = $this->validate_post_type_unlock( | |
| 261 | + $input, | |
| 262 | + $type, | |
| 263 | + $post_type_config, | |
| 264 | + null, | |
| 265 | + '', | |
| 266 | + $remove_spaces | |
| 267 | + ); | |
| 268 | + if ( $result['valid'] ) { | |
| 269 | + do_action( 'passster_validation_success', $input ); | |
| 270 | + PS_Helper::remember_unlock( hash_hmac( 'sha256', $input, get_option( 'passster_secure_key' ) ) ); | |
| 271 | + if ( !empty( $post_type_config['passster_activate_misc_settings'] ) && !empty( $post_type_config['passster_redirect_url'] ) ) { | |
| 272 | + return new \WP_REST_Response(array( | |
| 273 | + 'success' => true, | |
| 274 | + 'redirect' => esc_url_raw( $post_type_config['passster_redirect_url'] ), | |
| 275 | + ), 200); | |
| 276 | + } | |
| 277 | + return new \WP_REST_Response(array( | |
| 278 | + 'success' => true, | |
| 279 | + 'requires_reload' => true, | |
| 280 | + ), 200); | |
| 281 | + } | |
| 282 | + return new \WP_REST_Response(array( | |
| 283 | + 'success' => false, | |
| 284 | + 'error' => $error_message, | |
| 285 | + ), 200); | |
| 286 | + } | |
| 244 | 287 | // Parent page protection inheritance. |
| 245 | 288 | $parent_id = wp_get_post_parent_id( $post_id ); |
| 246 | 289 | if ( $parent_id ) { |
| 247 | 290 | $activate_protection = get_post_meta( $parent_id, 'passster_activate_protection', true ); |
| @@ -262,10 +305,13 @@ | ||
| 262 | 305 | } |
| 263 | 306 | // Category/taxonomy protection: if the post itself has no protection, |
| 264 | 307 | // check if it belongs to a protected category and validate against term meta. |
| 265 | 308 | $post_protection = get_post_meta( $post_id, 'passster_activate_protection', true ); |
| 266 | - if ( !$post_protection && 'full' === $protection && class_exists( 'passster\\PS_Category_Lock' ) ) { | |
| 267 | - $term_data = PS_Category_Lock::get_instance()->get_protected_term_for_post( $post_id ); | |
| 309 | + $term_data = null; | |
| 310 | + if ( !$post_protection && 'full' === $protection ) { | |
| 311 | + if ( class_exists( 'passster\\PS_Category_Lock' ) ) { | |
| 312 | + $term_data = PS_Category_Lock::get_instance()->get_protected_term_for_post( $post_id ); | |
| 313 | + } | |
| 268 | 314 | if ( $term_data ) { |
| 269 | 315 | // Use term redirect if no redirect was sent from the frontend. |
| 270 | 316 | if ( empty( $redirect ) ) { |
| 271 | 317 | $redirect = get_term_meta( $term_data['term_id'], 'passster_redirect_url', true ); |
| @@ -299,8 +345,9 @@ | ||
| 299 | 345 | $input, |
| 300 | 346 | 'full' |
| 301 | 347 | ); |
| 302 | 348 | do_action( 'passster_validation_success', $input ); |
| 349 | + PS_Helper::remember_unlock( hash_hmac( 'sha256', $input, get_option( 'passster_secure_key' ) ) ); | |
| 303 | 350 | return new \WP_REST_Response($response_data, 200); |
| 304 | 351 | } |
| 305 | 352 | // Category protection exists but validation failed. |
| 306 | 353 | return new \WP_REST_Response(array( |
| @@ -307,8 +354,53 @@ | ||
| 307 | 354 | 'success' => false, |
| 308 | 355 | 'error' => $error_message, |
| 309 | 356 | ), 200); |
| 310 | 357 | } |
| 358 | + // Post type level protection: applies only when the post has neither its | |
| 359 | + // own protection nor a protected category/term (both take precedence). | |
| 360 | + if ( class_exists( 'passster\\PS_Post_Type_Lock' ) ) { | |
| 361 | + $post_type = get_post_type( $post_id ); | |
| 362 | + if ( $post_type && PS_Post_Type_Lock::is_post_type_protected( $post_type ) ) { | |
| 363 | + $config = PS_Post_Type_Lock::get_post_type_config( $post_type ); | |
| 364 | + if ( empty( $redirect ) && !empty( $config['passster_activate_misc_settings'] ) && !empty( $config['passster_redirect_url'] ) ) { | |
| 365 | + $redirect = $config['passster_redirect_url']; | |
| 366 | + } | |
| 367 | + $result = $this->validate_post_type_unlock( | |
| 368 | + $input, | |
| 369 | + $type, | |
| 370 | + $config, | |
| 371 | + $post, | |
| 372 | + $content, | |
| 373 | + $remove_spaces | |
| 374 | + ); | |
| 375 | + if ( $result['valid'] ) { | |
| 376 | + $response_data = array( | |
| 377 | + 'success' => true, | |
| 378 | + ); | |
| 379 | + if ( !empty( $redirect ) ) { | |
| 380 | + $response_data['redirect'] = $redirect; | |
| 381 | + } elseif ( $this->content_uses_page_builder( $result['content'], $post_id ) ) { | |
| 382 | + $response_data['requires_reload'] = true; | |
| 383 | + } else { | |
| 384 | + $response_data['content'] = apply_filters( 'the_content', str_replace( '{post-id}', $post_id, $result['content'] ) ); | |
| 385 | + } | |
| 386 | + do_action( | |
| 387 | + 'passsster_track_record', | |
| 388 | + $post_id, | |
| 389 | + $input, | |
| 390 | + 'full' | |
| 391 | + ); | |
| 392 | + do_action( 'passster_validation_success', $input ); | |
| 393 | + PS_Helper::remember_unlock( hash_hmac( 'sha256', $input, get_option( 'passster_secure_key' ) ) ); | |
| 394 | + return new \WP_REST_Response($response_data, 200); | |
| 395 | + } | |
| 396 | + // Post type protection exists but validation failed. | |
| 397 | + return new \WP_REST_Response(array( | |
| 398 | + 'success' => false, | |
| 399 | + 'error' => $error_message, | |
| 400 | + ), 200); | |
| 401 | + } | |
| 402 | + } | |
| 311 | 403 | } |
| 312 | 404 | // Validate based on type. |
| 313 | 405 | $valid = false; |
| 314 | 406 | $result_content = ''; |
| @@ -458,8 +550,9 @@ | ||
| 458 | 550 | $input, |
| 459 | 551 | $source |
| 460 | 552 | ); |
| 461 | 553 | do_action( 'passster_validation_success', $input ); |
| 554 | + PS_Helper::remember_unlock( hash_hmac( 'sha256', $input, get_option( 'passster_secure_key' ) ) ); | |
| 462 | 555 | return new \WP_REST_Response($response_data, 200); |
| 463 | 556 | } |
| 464 | 557 | |
| 465 | 558 | /** |
| @@ -933,8 +1026,109 @@ | ||
| 933 | 1026 | 'content' => '', |
| 934 | 1027 | ); |
| 935 | 1028 | } |
| 936 | 1029 | |
| 1030 | + /** | |
| 1031 | + * Validate unlock for posts protected wholesale via post type level protection | |
| 1032 | + * (password stored in the post type's configuration, not on the post itself). | |
| 1033 | + * | |
| 1034 | + * @param string $input User input. | |
| 1035 | + * @param string $type Protection type (password, passwords, password_list, password_lists). | |
| 1036 | + * @param array $config Post type configuration. | |
| 1037 | + * @param \WP_Post $post Post object. | |
| 1038 | + * @param string $content Post content. | |
| 1039 | + * @param bool $remove_spaces Whether to remove spaces from password lists. | |
| 1040 | + * @return array | |
| 1041 | + */ | |
| 1042 | + private function validate_post_type_unlock( | |
| 1043 | + $input, | |
| 1044 | + $type, | |
| 1045 | + array $config, | |
| 1046 | + $post, | |
| 1047 | + $content, | |
| 1048 | + $remove_spaces | |
| 1049 | + ) { | |
| 1050 | + // $post is null for archive-level unlocks, where there's no single post to check. | |
| 1051 | + if ( $post && 'publish' !== $post->post_status ) { | |
| 1052 | + return array( | |
| 1053 | + 'valid' => false, | |
| 1054 | + 'content' => '', | |
| 1055 | + ); | |
| 1056 | + } | |
| 1057 | + switch ( $type ) { | |
| 1058 | + case 'password': | |
| 1059 | + if ( !empty( $config['passster_password'] ) && $input === $config['passster_password'] ) { | |
| 1060 | + return array( | |
| 1061 | + 'valid' => true, | |
| 1062 | + 'content' => $content, | |
| 1063 | + ); | |
| 1064 | + } | |
| 1065 | + break; | |
| 1066 | + case 'passwords': | |
| 1067 | + $passwords_str = $config['passster_passwords']; | |
| 1068 | + if ( $remove_spaces ) { | |
| 1069 | + $passwords_str = str_replace( ' ', '', $passwords_str ); | |
| 1070 | + } | |
| 1071 | + $passwords = explode( ',', $passwords_str ); | |
| 1072 | + if ( !empty( $passwords ) && in_array( $input, $passwords, true ) ) { | |
| 1073 | + return array( | |
| 1074 | + 'valid' => true, | |
| 1075 | + 'content' => $content, | |
| 1076 | + ); | |
| 1077 | + } | |
| 1078 | + break; | |
| 1079 | + case 'password_list': | |
| 1080 | + $list_id = $config['passster_password_list']; | |
| 1081 | + if ( !empty( $list_id ) ) { | |
| 1082 | + $passwords_str = get_post_meta( $list_id, 'passster_passwords', true ); | |
| 1083 | + if ( $remove_spaces ) { | |
| 1084 | + $passwords_str = str_replace( ' ', '', $passwords_str ); | |
| 1085 | + } | |
| 1086 | + $passwords = explode( ',', $passwords_str ); | |
| 1087 | + if ( in_array( $input, $passwords, true ) ) { | |
| 1088 | + do_action( | |
| 1089 | + 'passster_validation_success_list', | |
| 1090 | + $input, | |
| 1091 | + $list_id, | |
| 1092 | + ( $post ? $post->ID : 0 ) | |
| 1093 | + ); | |
| 1094 | + PS_Conditional::maybe_expire_password_from_list__premium_only( $input, $passwords, $list_id ); | |
| 1095 | + return array( | |
| 1096 | + 'valid' => true, | |
| 1097 | + 'content' => $content, | |
| 1098 | + ); | |
| 1099 | + } | |
| 1100 | + } | |
| 1101 | + break; | |
| 1102 | + case 'password_lists': | |
| 1103 | + foreach ( (array) $config['passster_password_lists'] as $list_id ) { | |
| 1104 | + $passwords_str = get_post_meta( $list_id, 'passster_passwords', true ); | |
| 1105 | + if ( $remove_spaces ) { | |
| 1106 | + $passwords_str = str_replace( ' ', '', $passwords_str ); | |
| 1107 | + } | |
| 1108 | + $passwords = explode( ',', $passwords_str ); | |
| 1109 | + if ( in_array( $input, $passwords, true ) ) { | |
| 1110 | + do_action( | |
| 1111 | + 'passster_validation_success_list', | |
| 1112 | + $input, | |
| 1113 | + $list_id, | |
| 1114 | + ( $post ? $post->ID : 0 ) | |
| 1115 | + ); | |
| 1116 | + PS_Conditional::maybe_expire_password_from_list__premium_only( $input, $passwords, $list_id ); | |
| 1117 | + return array( | |
| 1118 | + 'valid' => true, | |
| 1119 | + 'content' => $content, | |
| 1120 | + ); | |
| 1121 | + } | |
| 1122 | + } | |
| 1123 | + break; | |
| 1124 | + } | |
| 1125 | + return array( | |
| 1126 | + 'valid' => false, | |
| 1127 | + 'content' => '', | |
| 1128 | + ); | |
| 1129 | + } | |
| 1130 | + | |
| 937 | 1131 | private function get_block_password( $post_id, $block_id ) { |
| 938 | 1132 | $post = get_post( $post_id ); |
| 939 | 1133 | if ( !$post ) { |
| 940 | 1134 | return array( |
| @@ -1065,9 +1259,18 @@ | ||
| 1065 | 1259 | * @return \WP_REST_Response |
| 1066 | 1260 | */ |
| 1067 | 1261 | public function hash_password( \WP_REST_Request $request ) { |
| 1068 | 1262 | $password = $request->get_param( 'password' ); |
| 1263 | + $post_id = $request->get_param( 'post_id' ); | |
| 1264 | + $real_password = get_post_meta( $post_id, 'passster_password', true ); | |
| 1265 | + if ( empty( $real_password ) || !hash_equals( $real_password, $password ) ) { | |
| 1266 | + return new \WP_REST_Response(array( | |
| 1267 | + 'success' => false, | |
| 1268 | + 'error' => __( 'Invalid password.', 'content-protector' ), | |
| 1269 | + ), 200); | |
| 1270 | + } | |
| 1069 | 1271 | $hashed = hash_hmac( 'sha256', $password, get_option( 'passster_secure_key' ) ); |
| 1272 | + PS_Helper::remember_unlock( $hashed ); | |
| 1070 | 1273 | return new \WP_REST_Response(array( |
| 1071 | 1274 | 'success' => true, |
| 1072 | 1275 | 'hash' => $hashed, |
| 1073 | 1276 | ), 200); |
| @@ -1118,13 +1321,15 @@ | ||
| 1118 | 1321 | } |
| 1119 | 1322 | // Get content based on protection type (mirrors AJAX logic). |
| 1120 | 1323 | $content = ''; |
| 1121 | 1324 | $requires_reload = false; |
| 1325 | + $captcha_protection_types = array('recaptcha', 'turnstile'); | |
| 1122 | 1326 | if ( 'full' !== $protection ) { |
| 1123 | 1327 | if ( 'area' === $protection ) { |
| 1124 | 1328 | if ( !empty( $area_id ) ) { |
| 1125 | 1329 | $area = get_post( $area_id ); |
| 1126 | - if ( $area && 'protected_areas' === $area->post_type && 'publish' === $area->post_status ) { | |
| 1330 | + $area_protection_type = get_post_meta( $area_id, 'passster_protection_type', true ); | |
| 1331 | + if ( $area && 'protected_areas' === $area->post_type && 'publish' === $area->post_status && in_array( $area_protection_type, $captcha_protection_types, true ) ) { | |
| 1127 | 1332 | $content = apply_filters( 'passster_compatibility_actions', $area->post_content, $area_id ); |
| 1128 | 1333 | } |
| 1129 | 1334 | } |
| 1130 | 1335 | } else { |
| @@ -1131,15 +1336,16 @@ | ||
| 1131 | 1336 | // Shortcode protection - extract content using captcha_id. |
| 1132 | 1337 | $post = get_post( $post_id ); |
| 1133 | 1338 | if ( $post ) { |
| 1134 | 1339 | $post_content = apply_filters( 'passster_compatibility_actions', $post->post_content, $post_id ); |
| 1135 | - $content = apply_filters( 'passster_compatibility_actions', PS_Helper::get_shortcode_content( $post_content, $captcha_id ) ); | |
| 1340 | + $content = apply_filters( 'passster_compatibility_actions', PS_Helper::get_shortcode_content( $post_content, $captcha_id, 'captcha' ) ); | |
| 1136 | 1341 | } |
| 1137 | 1342 | } |
| 1138 | 1343 | } else { |
| 1139 | 1344 | $post = get_post( $post_id ); |
| 1140 | 1345 | $post_protection = get_post_meta( $post_id, 'passster_activate_protection', true ); |
| 1141 | - if ( $post && 'publish' === $post->post_status && $post_protection ) { | |
| 1346 | + $post_protection_type = get_post_meta( $post_id, 'passster_protection_type', true ); | |
| 1347 | + if ( $post && 'publish' === $post->post_status && $post_protection && in_array( $post_protection_type, $captcha_protection_types, true ) ) { | |
| 1142 | 1348 | $content = apply_filters( 'passster_compatibility_actions', $post->post_content, $post_id ); |
| 1143 | 1349 | } elseif ( $post && 'publish' === $post->post_status && !$post_protection && class_exists( 'passster\\PS_Category_Lock' ) && PS_Category_Lock::get_instance()->get_protected_term_for_post( $post_id ) ) { |
| 1144 | 1350 | $requires_reload = true; |
| 1145 | 1351 | } |
| @@ -1160,8 +1366,9 @@ | ||
| 1160 | 1366 | // Success response. |
| 1161 | 1367 | $response_data = array( |
| 1162 | 1368 | 'success' => true, |
| 1163 | 1369 | ); |
| 1370 | + PS_Helper::remember_unlock( hash_hmac( 'sha256', 'captcha-verified', get_option( 'passster_secure_key' ) ) ); | |
| 1164 | 1371 | if ( !empty( $redirect ) ) { |
| 1165 | 1372 | $response_data['redirect'] = $redirect; |
| 1166 | 1373 | } elseif ( $requires_reload ) { |
| 1167 | 1374 | $response_data['requires_reload'] = true; |