Changeset 3737477 for wplr-sync/trunk/classes/api.php
- Timestamp:
- 10/10/2026 01:03:33 AM (7 days ago)
- File:
-
- 1 edited
-
wplr-sync/trunk/classes/api.php (modified) (11 diffs)
Legend:
- Unmodified
- Added
- Removed
-
wplr-sync/trunk/classes/api.php
r3714439 r3737477 40 40 // Upload 41 41 else if ( isset( $_POST['action'] ) && $_POST['action'] === 'sync' ) { 42 if ( !$user = $this->auth( $_POST['token']) ) {42 if ( !$user = $this->auth( isset( $_POST['token'] ) ? wp_unslash( $_POST['token'] ) : null ) ) { 43 43 $this->response( null, false, $this->error ); 44 44 $this->finalize(); … … 51 51 $body = file_get_contents('php://input'); 52 52 $args = json_decode( $body ); 53 if ( !$user = $this->auth( $args->token ) ) { 53 if ( !is_object( $args ) || !isset( $args->action ) ) { 54 $this->response( null, false, "Invalid request." ); 55 $this->finalize(); 56 } 57 if ( !$user = $this->auth( isset( $args->token ) ? $args->token : null ) ) { 54 58 $this->response( null, false, $this->error ); 55 59 $this->finalize(); 56 60 } 57 61 $wplr->log( "Action: " . $args->action ); 58 if ( !$user = $this->auth( $args->token ) ) 59 $this->response( null, false, $this->error ); 60 else if ( $args->action === 'presync' ) 62 if ( $args->action === 'presync' ) 61 63 $this->presync( $args ); 62 64 else if ( $args->action === 'sync_collection' ) … … 117 119 global $wplr, $wpdb; 118 120 119 120 if ( empty( $token ) ) { 121 // Failed attempts are counted per IP: a token can't be guessed by hammering the endpoint. 122 $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; 123 $fail_key = 'wplr_api_fail_' . md5( $ip ); 124 $fails = (int) get_transient( $fail_key ); 125 if ( $fails >= 20 ) { 126 $this->error = "Too many failed attempts. Please try again in 15 minutes."; 127 $wplr->log( $this->error ); 128 return false; 129 } 130 131 if ( !is_string( $token ) || strlen( $token ) < 12 || strlen( $token ) > 128 ) { 121 132 $this->error = "Authentification is missing."; 122 133 $wplr->log( $this->error ); 134 set_transient( $fail_key, $fails + 1, 15 * MINUTE_IN_SECONDS ); 123 135 return false; 124 136 } 125 126 //* DEBUG: allow to authenticate as admin with a hardcoded token (for testing purposes only)127 // if ( $token == "MEOW_TOKEN" ) {128 129 // $this->user = get_userdata( 1 );130 // if ( empty( $this->user ) ) {131 // $this->error = "User admin does not exist.";132 // $wplr->log( $this->error );133 // return false;134 // }135 136 // $wplr->log( "Authenticated as admin" );137 // return $this->user;138 // }139 137 140 138 $prefix = defined( 'BLOG_ID_CURRENT_SITE' ) ? $wpdb->get_blog_prefix( BLOG_ID_CURRENT_SITE ) : $wpdb->prefix; 141 139 $tbl_meta = defined('CUSTOM_USER_META_TABLE' ) ? CUSTOM_USER_META_TABLE : $prefix . "usermeta"; 142 $ userId = $wpdb->get_var( $wpdb->prepare( "SELECT user_idFROM $tbl_meta140 $row = $wpdb->get_row( $wpdb->prepare( "SELECT user_id, meta_value FROM $tbl_meta 143 141 WHERE meta_value = %s AND meta_key = %s", $token, 'wplr_auth_token' ) ); 144 if ( empty( $userId ) ) { 145 $this->error = "Token '$token' seems to be invalid."; 142 // The SQL match can be case-insensitive (collation): the token must match exactly. 143 if ( empty( $row ) || !is_string( $row->meta_value ) || !hash_equals( $row->meta_value, $token ) ) { 144 $this->error = "This token seems to be invalid."; 146 145 $wplr->log( $this->error ); 146 set_transient( $fail_key, $fails + 1, 15 * MINUTE_IN_SECONDS ); 147 147 return false; 148 148 } 149 $this->user = get_userdata( $ userId );149 $this->user = get_userdata( $row->user_id ); 150 150 if ( empty( $this->user ) ) { 151 151 $this->error = 'User does not exist.'; … … 153 153 return false; 154 154 } 155 156 // Act as this user, so every capability check below applies to them. 157 wp_set_current_user( $this->user->ID ); 158 if ( !current_user_can( 'upload_files' ) ) { 159 $this->error = 'This user is not allowed to upload files.'; 160 $wplr->log( $this->error ); 161 $this->user = null; 162 return false; 163 } 155 164 $wplr->log( "Authenticated as " . $this->user->user_login ); 156 165 return $this->user; 166 } 167 168 // The attachment(s) behind a Lightroom ID must be editable (or deletable) by the authenticated user. 169 function can_on_lr_id( $lr_id, $cap ) { 170 global $wplr; 171 foreach ( (array) $wplr->list_wpids( (int) $lr_id ) as $row ) { 172 if ( !empty( $row->wp_id ) && !current_user_can( $cap, (int) $row->wp_id ) ) { 173 return false; 174 } 175 } 176 return true; 177 } 178 179 function can_on_wp_id( $wp_id, $cap ) { 180 $wp_id = (int) $wp_id; 181 return $wp_id > 0 && get_post_type( $wp_id ) === 'attachment' && current_user_can( $cap, $wp_id ); 157 182 } 158 183 … … 237 262 $file = $_FILES['file']['tmp_name']; 238 263 264 // Syncing over an existing photo replaces its file: the user must be allowed to edit it. 265 if ( !$this->can_on_lr_id( $args["id"], 'edit_post' ) ) { 266 return $this->response( null, false, 'You are not allowed to edit this media.' ); 267 } 268 239 269 if ( !isset( $args["wp_col_id"] ) || $args["wp_col_id"] == null ) 240 270 $args["wp_col_id"] = -1; … … 301 331 global $wplr; 302 332 $res = false; 333 // Deleting a collection also deletes its photos, which may belong to anyone. 334 if ( !current_user_can( 'delete_others_posts' ) ) { 335 return $this->response( null, false, 'You are not allowed to delete collections.' ); 336 } 303 337 if ( property_exists( $args, 'remoteId' ) ) 304 338 $res = $wplr->delete_collection( $args->remoteId ); … … 312 346 if ( property_exists( $args, 'collection_id' ) ) 313 347 $collectionId = $args->collection_id; 314 $list = $wplr->delete_media( $args->lr_id, $collectionId ); 348 if ( !property_exists( $args, 'lr_id' ) || !$this->can_on_lr_id( $args->lr_id, 'delete_post' ) ) { 349 return $this->response( null, false, 'You are not allowed to delete this media.' ); 350 } 351 $list = $wplr->delete_media( (int) $args->lr_id, $collectionId ); 315 352 return $this->response( $list ); 316 353 } … … 335 372 function unlink( $args ) { 336 373 global $wplr; 337 $res = $wplr->unlink_media( $args->lr_id, $args->wp_id ); 374 $wp_id = property_exists( $args, 'wp_id' ) ? (int) $args->wp_id : 0; 375 $lr_id = property_exists( $args, 'lr_id' ) ? (int) $args->lr_id : 0; 376 if ( $wp_id ? !$this->can_on_wp_id( $wp_id, 'edit_post' ) : !$this->can_on_lr_id( $lr_id, 'edit_post' ) ) { 377 return $this->response( null, false, 'You are not allowed to edit this media.' ); 378 } 379 $res = $wplr->unlink_media( $lr_id, $wp_id ); 338 380 return $this->response( $res ); 339 381 } … … 342 384 function link( $args ) { 343 385 global $wplr; 344 $res = $wplr->link_media( $args->lr_id, $args->wp_id ); 386 // Linking decides which attachment a later sync or delete will touch. 387 $wp_id = property_exists( $args, 'wp_id' ) ? (int) $args->wp_id : 0; 388 if ( !$this->can_on_wp_id( $wp_id, 'edit_post' ) ) { 389 return $this->response( null, false, 'You are not allowed to link this media.' ); 390 } 391 $res = $wplr->link_media( (int) $args->lr_id, $wp_id ); 345 392 return $this->response( $res ); 346 393 } … … 357 404 function linkinfo( $args ) { 358 405 global $wplr; 359 $linkinfo = $wplr->linkinfo_media( $args->wp_id ); 406 if ( !$this->can_on_wp_id( property_exists( $args, 'wp_id' ) ? $args->wp_id : 0, 'edit_post' ) ) { 407 return $this->response( null, false, 'You are not allowed to access this media.' ); 408 } 409 $linkinfo = $wplr->linkinfo_media( (int) $args->wp_id ); 360 410 return $this->response( $linkinfo ); 361 411 } … … 365 415 global $wplr; 366 416 $wp_ids = isset( $args->wp_ids ) ? (array)$args->wp_ids : array(); 417 $wp_ids = array_values( array_filter( array_map( 'intval', $wp_ids ), function ( $id ) { 418 return $this->can_on_wp_id( $id, 'edit_post' ); 419 } ) ); 367 420 $list = $wplr->linkinfo_bulk( $wp_ids ); 368 421 return $this->response( $list );
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)