← All changes
|
includes/rest-api/controller/version1/class-mainwp-rest-api-v1.php
+112
-25
5.2
→
6.2.1
View file →
| @@ -24,8 +24,13 @@ | ||
| 24 | 24 | use MainWP\Dashboard\MainWP_Error_Helper; |
| 25 | 25 | use MainWP\Dashboard\MainWP_Exception; |
| 26 | 26 | use Exception; |
| 27 | 27 | |
| 28 | +// Exit if accessed directly. | |
| 29 | +if ( ! defined( 'ABSPATH' ) ) { | |
| 30 | + exit; | |
| 31 | +} | |
| 32 | + | |
| 28 | 33 | /** |
| 29 | 34 | * Class Rest_Api_V1 |
| 30 | 35 | * |
| 31 | 36 | * @package MainWP\Dashboard |
| @@ -76,8 +81,15 @@ | ||
| 76 | 81 | * Adds an action to generate API credentials. |
| 77 | 82 | * Adds an action to create the rest API endpoints if activated in the plugin settings. |
| 78 | 83 | */ |
| 79 | 84 | public function init() { |
| 85 | + add_action( 'init', array( &$this, 'init_hooks' ), 9 ); // Fix the issue where database tables are not created on a fresh install. | |
| 86 | + } | |
| 87 | + | |
| 88 | + /** | |
| 89 | + * Method init hooks. | |
| 90 | + */ | |
| 91 | + public function init_hooks() { | |
| 80 | 92 | if ( $this->is_rest_api_enabled() ) { |
| 81 | 93 | add_filter( 'mainwp_rest_api_validate', array( &$this, 'rest_api_validate' ), 10, 2 ); // to compatible version 1 rest api validate. |
| 82 | 94 | // run API. |
| 83 | 95 | if ( $this->enabled_rest_v1_api() ) { |
| @@ -111,22 +123,9 @@ | ||
| 111 | 123 | * |
| 112 | 124 | * Check if enable the REST API. |
| 113 | 125 | */ |
| 114 | 126 | private function enabled_rest_v1_api() { |
| 115 | - | |
| 116 | - $all_keys = get_option( 'mainwp_rest_api_keys', false ); | |
| 117 | - | |
| 118 | - if ( ! is_array( $all_keys ) ) { | |
| 119 | - return false; | |
| 120 | - } | |
| 121 | - | |
| 122 | - foreach ( $all_keys as $item ) { | |
| 123 | - if ( ! empty( $item['cs'] ) && ! empty( $item['enabled'] ) ) { | |
| 124 | - return true; // one key enabled, enabled the REST API. | |
| 125 | - } | |
| 126 | - } | |
| 127 | - | |
| 128 | - return false; // all keys disabled. | |
| 127 | + return get_option( 'mainwp_rest_api_keys', true ); // enabled the REST API if there is any key. | |
| 129 | 128 | } |
| 130 | 129 | |
| 131 | 130 | /** |
| 132 | 131 | * Method enabled_rest_v2_api() |
| @@ -133,9 +132,9 @@ | ||
| 133 | 132 | * |
| 134 | 133 | * Enabled the REST API. |
| 135 | 134 | */ |
| 136 | 135 | private function enabled_rest_v2_api() { |
| 137 | - return MainWP_DB::instance()->is_existed_enabled_rest_key(); | |
| 136 | + return true; // enabled the REST API if there is any key. | |
| 138 | 137 | } |
| 139 | 138 | |
| 140 | 139 | /** |
| 141 | 140 | * Method hook_rest_api_enabled() |
| @@ -518,8 +517,79 @@ | ||
| 518 | 517 | } |
| 519 | 518 | } |
| 520 | 519 | |
| 521 | 520 | /** |
| 521 | + * Sensitive site fields that must never appear in v1 REST responses. | |
| 522 | + * | |
| 523 | + * The data layer projection at MainWP_DB::get_websites_for_current_user | |
| 524 | + * returns these when full_data=true so that internal callers (cron jobs, | |
| 525 | + * MainWP_Connect) can sign requests to child sites. v1 REST callbacks | |
| 526 | + * pass through that projection directly, which is the leak path | |
| 527 | + * MWP-1542 addresses. Centralizing the strip list here keeps every | |
| 528 | + * callback in sync; copying the safe template at mainwp_rest_api_site_callback | |
| 529 | + * previously inherited only ['privkey', 'adminname'] which let | |
| 530 | + * http_pass / http_user / pubkey / securekey leak. | |
| 531 | + * | |
| 532 | + * @var string[] | |
| 533 | + */ | |
| 534 | + protected static $sensitive_site_fields = array( | |
| 535 | + 'privkey', | |
| 536 | + 'pubkey', | |
| 537 | + 'http_user', | |
| 538 | + 'http_pass', | |
| 539 | + 'adminname', | |
| 540 | + 'securekey', | |
| 541 | + // uniqueId is the per-site secure-mode signing identifier; the v2 schema | |
| 542 | + // narrows it to edit context only (MWP-1541) and the single-site v1 | |
| 543 | + // callback uses get_website_by_id (SELECT wp.*) which carries it | |
| 544 | + // through. Strip it here too so v1 stays consistent. | |
| 545 | + 'uniqueId', | |
| 546 | + ); | |
| 547 | + | |
| 548 | + /** | |
| 549 | + * Strip credential-shaped fields from a site object or array of sites | |
| 550 | + * before returning it from a v1 REST callback. Operates in place on | |
| 551 | + * each item; safe to call on a single site object, a list of objects, | |
| 552 | + * or a list of associative arrays. | |
| 553 | + * | |
| 554 | + * @param mixed $data Either a site stdClass / array, or a list of either. | |
| 555 | + * @return mixed The same $data with sensitive fields removed. | |
| 556 | + */ | |
| 557 | + public static function strip_sensitive_site_fields( $data ) { | |
| 558 | + if ( empty( $data ) ) { | |
| 559 | + return $data; | |
| 560 | + } | |
| 561 | + | |
| 562 | + // Detect single-item vs list. A list either has integer 0 as its | |
| 563 | + // first key (sequential array) or the first element is itself an | |
| 564 | + // array/object representing a site. | |
| 565 | + $is_list = false; | |
| 566 | + if ( is_array( $data ) ) { | |
| 567 | + $first_key = array_key_first( $data ); | |
| 568 | + if ( null !== $first_key && is_int( $first_key ) ) { | |
| 569 | + $is_list = true; | |
| 570 | + } | |
| 571 | + } | |
| 572 | + | |
| 573 | + if ( $is_list ) { | |
| 574 | + foreach ( $data as $idx => $item ) { | |
| 575 | + $data[ $idx ] = static::strip_sensitive_site_fields( $item ); | |
| 576 | + } | |
| 577 | + return $data; | |
| 578 | + } | |
| 579 | + | |
| 580 | + foreach ( static::$sensitive_site_fields as $field ) { | |
| 581 | + if ( is_object( $data ) && property_exists( $data, $field ) ) { | |
| 582 | + unset( $data->{$field} ); | |
| 583 | + } elseif ( is_array( $data ) && array_key_exists( $field, $data ) ) { | |
| 584 | + unset( $data[ $field ] ); | |
| 585 | + } | |
| 586 | + } | |
| 587 | + | |
| 588 | + return $data; | |
| 589 | + } | |
| 590 | + | |
| 591 | + /** | |
| 522 | 592 | * Method mainwp_rest_api_init() |
| 523 | 593 | * |
| 524 | 594 | * Makes sure the correct consumer key and secret are entered. |
| 525 | 595 | * |
| @@ -662,8 +732,9 @@ | ||
| 662 | 732 | |
| 663 | 733 | $method = $request->get_method(); |
| 664 | 734 | |
| 665 | 735 | if ( empty( $allow_methods ) || ! in_array( $method, $allow_methods ) ) { |
| 736 | + /* translators: %s: HTTP method name */ | |
| 666 | 737 | throw new MainWP_Exception( sprintf( esc_html__( 'Sorry, you are not allowed to do the %s method.', 'mainwp' ), ( isset( $methods_map[ $method ] ) ? $methods_map[ $method ] : '' ) ) ); //phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped |
| 667 | 738 | } |
| 668 | 739 | |
| 669 | 740 | return true; |
| @@ -792,8 +863,14 @@ | ||
| 792 | 863 | |
| 793 | 864 | // get data. |
| 794 | 865 | $data = MainWP_DB::instance()->get_websites_for_current_user( $params ); |
| 795 | 866 | |
| 867 | + // MWP-1542: strip credential fields before returning. The data | |
| 868 | + // layer's full_data projection includes privkey / http_pass / etc. | |
| 869 | + // for internal callers (cron, MainWP_Connect); REST consumers | |
| 870 | + // must never see them. | |
| 871 | + $data = static::strip_sensitive_site_fields( $data ); | |
| 872 | + | |
| 796 | 873 | $result = $data; |
| 797 | 874 | |
| 798 | 875 | if ( 'array' === $format && is_array( $data ) ) { |
| 799 | 876 | $result = array( |
| @@ -1187,11 +1264,12 @@ | ||
| 1187 | 1264 | return $valid; |
| 1188 | 1265 | } |
| 1189 | 1266 | |
| 1190 | 1267 | if ( true === $valid ) { |
| 1191 | - $data = array(); | |
| 1192 | - $params['extra_view'] = array( 'health_site_status' ); | |
| 1193 | - $websites = MainWP_DB::instance()->query( MainWP_DB::instance()->get_sql_websites_for_current_user() ); | |
| 1268 | + $data = array(); | |
| 1269 | + // Pass extra_view positionally (8th arg) so health_site_status is actually | |
| 1270 | + // fetched; a $params['extra_view'] local never reaches this query. | |
| 1271 | + $websites = MainWP_DB::instance()->query( MainWP_DB::instance()->get_sql_websites_for_current_user( false, null, 'wp.url', false, false, null, false, array( 'health_site_status' ) ) ); | |
| 1194 | 1272 | while ( $websites && ( $website = MainWP_DB::fetch_object( $websites ) ) ) { |
| 1195 | 1273 | $health_status = isset( $website->health_site_status ) ? json_decode( $website->health_site_status, true ) : array(); |
| 1196 | 1274 | $hstatus = MainWP_Utility::get_site_health( $health_status ); |
| 1197 | 1275 | $hval = $hstatus['val']; |
| @@ -1317,8 +1395,14 @@ | ||
| 1317 | 1395 | |
| 1318 | 1396 | // get data. |
| 1319 | 1397 | $data = MainWP_DB::instance()->get_websites_for_current_user( $params ); |
| 1320 | 1398 | |
| 1399 | + // MWP-1542: strip credential fields before returning. The data | |
| 1400 | + // layer's full_data projection includes privkey / http_pass / etc. | |
| 1401 | + // for internal callers (cron, MainWP_Connect); REST consumers | |
| 1402 | + // must never see them. | |
| 1403 | + $data = static::strip_sensitive_site_fields( $data ); | |
| 1404 | + | |
| 1321 | 1405 | $result = $data; |
| 1322 | 1406 | |
| 1323 | 1407 | if ( 'array' === $format && is_array( $data ) ) { |
| 1324 | 1408 | $result = array( |
| @@ -1374,8 +1458,14 @@ | ||
| 1374 | 1458 | |
| 1375 | 1459 | // get data. |
| 1376 | 1460 | $data = MainWP_DB::instance()->get_websites_for_current_user( $params ); |
| 1377 | 1461 | |
| 1462 | + // MWP-1542: strip credential fields before returning. The data | |
| 1463 | + // layer's full_data projection includes privkey / http_pass / etc. | |
| 1464 | + // for internal callers (cron, MainWP_Connect); REST consumers | |
| 1465 | + // must never see them. | |
| 1466 | + $data = static::strip_sensitive_site_fields( $data ); | |
| 1467 | + | |
| 1378 | 1468 | $result = $data; |
| 1379 | 1469 | |
| 1380 | 1470 | if ( 'array' === $format && is_array( $data ) ) { |
| 1381 | 1471 | $result = array( |
| @@ -1428,15 +1518,12 @@ | ||
| 1428 | 1518 | |
| 1429 | 1519 | // get data. |
| 1430 | 1520 | $data = MainWP_DB::instance()->get_website_by_id( $site_id, $selectGroups ); |
| 1431 | 1521 | |
| 1432 | - if ( ! empty( $data ) && property_exists( $data, 'privkey' ) ) { | |
| 1433 | - unset( $data->privkey ); | |
| 1434 | - } | |
| 1435 | - | |
| 1436 | - if ( ! empty( $data ) && property_exists( $data, 'adminname' ) ) { | |
| 1437 | - unset( $data->adminname ); | |
| 1438 | - } | |
| 1522 | + // MWP-1542: strip every credential field, not just privkey/adminname. | |
| 1523 | + // The pre-MWP-1542 partial strip let http_pass/http_user/pubkey/securekey | |
| 1524 | + // pass through whenever the underlying row had them populated. | |
| 1525 | + $data = static::strip_sensitive_site_fields( $data ); | |
| 1439 | 1526 | |
| 1440 | 1527 | if ( $with_tags && ! empty( $data ) && property_exists( $data, 'wpgroups' ) && property_exists( $data, 'wpgroupids' ) ) { |
| 1441 | 1528 | $wpgroupids = explode( ',', $data->wpgroupids ); |
| 1442 | 1529 | $wpgroups = explode( ',', $data->wpgroups ); |