PluginProbe
Media Cloud Sync / 1.4.2
Media Cloud Sync v1.4.2
1.4.2 1.4.1 1.4.0 1.3.12 1.3.11 1.3.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.2.0 1.2.10 1.2.11 1.2.12 1.2.13 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 All 36 releases
← All changes | includes/base/services/s3.php +79 -32 1.3.12 → 1.4.2 View file →
@@ -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 }