| @@ -721,10 +721,18 @@ | ||
| 721 | 721 | } |
| 722 | 722 | |
| 723 | 723 | /** |
| 724 | 724 | * Add Bucket Policy |
| 725 | + * | |
| 726 | + * $private_prefix, when non-empty, carves that path out of the public | |
| 727 | + * grant entirely — every action in the list, not just reads, so an | |
| 728 | + * anonymous caller can't read, write, or delete anything under it. Kept | |
| 729 | + * as one statement with NotResource rather than split into "reads | |
| 730 | + * excluded, everything else still public" — that split would still let | |
| 731 | + * anonymous PutObject/DeleteObject reach a "private" file. | |
| 732 | + * @since 1.4.1 $private_prefix param added. | |
| 725 | 733 | */ |
| 726 | - private function putBucketPolicy($bucket, $s3Client = false) { | |
| 734 | + private function putBucketPolicy($bucket, $s3Client = false, $private_prefix = '') { | |
| 727 | 735 | if($s3Client == false) { |
| 728 | 736 | $s3Client = $this->s3Client; |
| 729 | 737 | } |
| 730 | 738 | |
| @@ -729,38 +737,45 @@ | ||
| 729 | 737 | } |
| 730 | 738 | |
| 731 | 739 | if(empty($bucket)) return false; |
| 732 | 740 | |
| 741 | + $actions = [ | |
| 742 | + "s3:DeleteObjectTagging", | |
| 743 | + "s3:ListBucketMultipartUploads", | |
| 744 | + "s3:DeleteObjectVersion", | |
| 745 | + "s3:ListBucket", | |
| 746 | + "s3:DeleteObjectVersionTagging", | |
| 747 | + "s3:GetBucketAcl", | |
| 748 | + "s3:ListMultipartUploadParts", | |
| 749 | + "s3:PutObject", | |
| 750 | + "s3:GetObjectAcl", | |
| 751 | + "s3:GetObject", | |
| 752 | + "s3:AbortMultipartUpload", | |
| 753 | + "s3:DeleteObject", | |
| 754 | + "s3:GetBucketLocation", | |
| 755 | + "s3:PutObjectAcl", | |
| 756 | + "s3:putBucketOwnershipControls", | |
| 757 | + "s3:putBucketPolicy" | |
| 758 | + ]; | |
| 759 | + | |
| 760 | + $statement = [ | |
| 761 | + "Effect" => "Allow", | |
| 762 | + "Principal" => "*", | |
| 763 | + "Action" => $actions, | |
| 764 | + ]; | |
| 765 | + | |
| 766 | + if (!empty($private_prefix)) { | |
| 767 | + $statement["NotResource"] = ["arn:aws:s3:::$bucket/$private_prefix/*"]; | |
| 768 | + } else { | |
| 769 | + $statement["Resource"] = [ | |
| 770 | + "arn:aws:s3:::$bucket/*", | |
| 771 | + "arn:aws:s3:::$bucket" | |
| 772 | + ]; | |
| 773 | + } | |
| 774 | + | |
| 733 | 775 | $policy = json_encode([ |
| 734 | - "Version" => "2012-10-17", | |
| 735 | - "Statement" => [ | |
| 736 | - [ | |
| 737 | - "Effect" => "Allow", | |
| 738 | - "Principal" => "*", | |
| 739 | - "Action" => [ | |
| 740 | - "s3:DeleteObjectTagging", | |
| 741 | - "s3:ListBucketMultipartUploads", | |
| 742 | - "s3:DeleteObjectVersion", | |
| 743 | - "s3:ListBucket", | |
| 744 | - "s3:DeleteObjectVersionTagging", | |
| 745 | - "s3:GetBucketAcl", | |
| 746 | - "s3:ListMultipartUploadParts", | |
| 747 | - "s3:PutObject", | |
| 748 | - "s3:GetObjectAcl", | |
| 749 | - "s3:GetObject", | |
| 750 | - "s3:AbortMultipartUpload", | |
| 751 | - "s3:DeleteObject", | |
| 752 | - "s3:GetBucketLocation", | |
| 753 | - "s3:PutObjectAcl", | |
| 754 | - "s3:putBucketOwnershipControls", | |
| 755 | - "s3:putBucketPolicy" | |
| 756 | - ], | |
| 757 | - "Resource" => [ | |
| 758 | - "arn:aws:s3:::$bucket/*", | |
| 759 | - "arn:aws:s3:::$bucket" | |
| 760 | - ] | |
| 761 | - ] | |
| 762 | - ] | |
| 776 | + "Version" => "2012-10-17", | |
| 777 | + "Statement" => [$statement] | |
| 763 | 778 | ]); |
| 764 | 779 | |
| 765 | 780 | try { |
| 766 | 781 | // Add bucket policy |
| @@ -776,8 +791,25 @@ | ||
| 776 | 791 | } |
| 777 | 792 | } |
| 778 | 793 | |
| 779 | 794 | /** |
| 795 | + * Apply (or, with an empty $private_prefix, un-apply) the private-path | |
| 796 | + * bucket policy carve-out. | |
| 797 | + * @since 1.4.1 | |
| 798 | + */ | |
| 799 | + public function applyPrivatePathPolicy($private_prefix) { | |
| 800 | + if (!$this->s3Client || empty($this->bucket_name)) { | |
| 801 | + return ['success' => false, 'code' => 200, 'message' => esc_html__('Client not configured', 'media-cloud-sync')]; | |
| 802 | + } | |
| 803 | + | |
| 804 | + $ok = $this->putBucketPolicy($this->bucket_name, $this->s3Client, $private_prefix); | |
| 805 | + | |
| 806 | + return $ok | |
| 807 | + ? ['success' => true, 'code' => 200, 'message' => esc_html__('Policy applied successfully', 'media-cloud-sync')] | |
| 808 | + : ['success' => false, 'code' => 200, 'message' => esc_html__('Failed to apply bucket policy', 'media-cloud-sync')]; | |
| 809 | + } | |
| 810 | + | |
| 811 | + /** | |
| 780 | 812 | * Add Bucket Ownership |
| 781 | 813 | */ |
| 782 | 814 | private function changeBucketOwnership($bucket, $s3Client = false, $ownership = 'BucketOwnerPreferred') { |
| 783 | 815 | if($s3Client == false) { |
| @@ -996,9 +1028,9 @@ | ||
| 996 | 1028 | * Upload Single |
| 997 | 1029 | * @since 1.0.0 |
| 998 | 1030 | * @return boolean |
| 999 | 1031 | */ |
| 1000 | - public function uploadSingle($absolute_source_path, $relative_source_path, $prefix='') { | |
| 1032 | + public function uploadSingle($absolute_source_path, $relative_source_path, $prefix='', $is_private = false) { | |
| 1001 | 1033 | if ( |
| 1002 | 1034 | isset($absolute_source_path) && !empty($absolute_source_path) && |
| 1003 | 1035 | isset($relative_source_path) && !empty($relative_source_path) |
| 1004 | 1036 | ) { |
| @@ -1003,9 +1035,19 @@ | ||
| 1003 | 1035 | isset($relative_source_path) && !empty($relative_source_path) |
| 1004 | 1036 | ) { |
| 1005 | 1037 | $file_name = wp_basename( $relative_source_path ); |
| 1006 | 1038 | if ($file_name) { |
| 1007 | - $upload_path = Utils::generate_object_key($relative_source_path, $prefix); | |
| 1039 | + $upload_path = Utils::generate_object_key($relative_source_path, $prefix, $is_private); | |
| 1040 | + if ($upload_path === false) { | |
| 1041 | + // Only happens for a private reupload with no private-path provider | |
| 1042 | + // available (Pro inactive/unlicensed) — refuse rather than upload | |
| 1043 | + // an already-private file to an unprotected path. | |
| 1044 | + return [ | |
| 1045 | + 'success' => false, | |
| 1046 | + 'code' => 200, | |
| 1047 | + 'message' => esc_html__('This file is marked private, but the private-media add-on is not currently active — reupload skipped to avoid exposing it.', 'media-cloud-sync') | |
| 1048 | + ]; | |
| 1049 | + } | |
| 1008 | 1050 | return $this->execute_upload($absolute_source_path, $upload_path); |
| 1009 | 1051 | } |
| 1010 | 1052 | return [ |
| 1011 | 1053 | 'success' => false, |
| @@ -1414,7 +1456,12 @@ | ||
| 1414 | 1456 | */ |
| 1415 | 1457 | public function get_domain() { |
| 1416 | 1458 | $region = isset($this->config['region']) ? $this->config['region'] : ''; |
| 1417 | 1459 | return "https://{$this->bucket_name}.s3.{$region}.amazonaws.com"; |
| 1460 | + } | |
| 1461 | + | |
| 1462 | + /** Exposes the already-constructed SDK client for StreamWrapper's registration — avoids reconstructing one from credentials. */ | |
| 1463 | + public function get_client() { | |
| 1464 | + return $this->s3Client; | |
| 1418 | 1465 | } |
| 1419 | 1466 | |
| 1420 | 1467 | } |