diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..3bb9236ec --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: CI + +on: + push: + branches: [ master, v3-branch ] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies + run: docker run --rm -v $PWD:/code --entrypoint='' humanmade/plugin-tester composer install + + - name: Run tests + run: ./tests/run-tests.sh --coverage-clover=coverage.xml + + - name: Upload coverage to Codecov + run: bash <(curl -s https://codecov.io/bash) + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b3f59966a..000000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -# Travis CI Configuration File - -services: - - docker - -cache: - timeout: 1000 - directories: - - vendor - -notifications: - email: false - -before_script: - - docker run --rm -v $PWD:/code --entrypoint='' humanmade/plugin-tester composer install - -script: - - ./tests/run-tests.sh --coverage-clover=coverage.xml - - bash <(curl -s https://codecov.io/bash) diff --git a/README.md b/README.md index b1b67de94..f028ed00a 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,14 @@ Lightweight "drop-in" for storing WordPress uploads on Amazon S3 instead of the local filesystem. - + Psalm coverage - - Build status + + CI - - Coverage via codecov.io + + @@ -32,7 +32,7 @@ It's focused on providing a highly robust S3 interface with no "bells and whistl ## Requirements -- PHP >= 7.1 +- PHP >= 7.4 - WordPress >= 5.3 ## Getting Set Up @@ -133,7 +133,7 @@ Note: as either `` or `` can be S3 or local locations, you must specif ## Private Uploads -WordPress (and therefor S3 Uploads) default behaviour is that all uploaded media files are publicly accessible. In certain cases which may not be desireable. S3 Uploads supports setting S3 Objects to a `private` ACL and providing temporarily signed URLs for all files that are marked as private. +WordPress (and therefore S3 Uploads) default behaviour is that all uploaded media files are publicly accessible. In certain cases which may not be desireable. S3 Uploads supports setting S3 Objects to a `private` ACL and providing temporarily signed URLs for all files that are marked as private. S3 Uploads does not make assumptions or provide UI for marking attachments as private, instead you should integrate the `s3_uploads_is_attachment_private` WordPress filter to control the behaviour. For example, to mark _all_ attachments as private: @@ -155,6 +155,8 @@ add_filter( 's3_uploads_private_attachment_url_expiry', function ( $expiry ) { } ); ``` +If you're using [Stream](https://wordpress.org/plugins/stream/) for audit logs, [S3 Uploads Audit](https://github.com/humanmade/s3-uploads-audit) is an add-on plugin which supports logging some S3 Uploads actions e.g any setting of ACL for files of an attachment. So you can install it for such audit functionality. + ## Cache Control You can define the default HTTP `Cache-Control` header for uploaded media using the diff --git a/composer.json b/composer.json index ee085ff99..4595db00b 100644 --- a/composer.json +++ b/composer.json @@ -28,10 +28,16 @@ "require-dev": { "phpunit/phpunit": "7.5", "pcov/clobber": "^2.0", - "humanmade/psalm-plugin-wordpress": "^1.0" + "humanmade/psalm-plugin-wordpress": "^1.0", + "yoast/phpunit-polyfills": "^4.0" }, "scripts": { "test": "./tests/run-tests.sh", "check-types": "./vendor/bin/psalm" + }, + "config": { + "allow-plugins": { + "composer/installers": true + } } } diff --git a/inc/class-local-stream-wrapper.php b/inc/class-local-stream-wrapper.php index c134f5137..0a3fbf25d 100644 --- a/inc/class-local-stream-wrapper.php +++ b/inc/class-local-stream-wrapper.php @@ -143,7 +143,7 @@ protected function getLocalPath( $uri = null ) { $uri = $this->uri; } $path = $this->getDirectoryPath() . '/' . $this->getTarget( $uri ); - $realpath = $path; + $realpath = str_replace( '/', DIRECTORY_SEPARATOR, $path ); // ensure check against realpath passes on Windows machines $directory = realpath( $this->getDirectoryPath() ); diff --git a/inc/class-plugin.php b/inc/class-plugin.php index 30639e17b..50c2b0159 100644 --- a/inc/class-plugin.php +++ b/inc/class-plugin.php @@ -154,6 +154,13 @@ public function register_stream_wrapper() { stream_context_set_option( stream_context_get_default(), 's3', 'seekable', true ); } + /** + * Get the s3:// path for the bucket. + */ + public function get_s3_path() : string { + return 's3://' . $this->bucket; + } + /** * Overwrite the default wp_upload_dir. * @@ -163,19 +170,20 @@ public function register_stream_wrapper() { public function filter_upload_dir( array $dirs ) : array { $this->original_upload_dir = $dirs; + $s3_path = $this->get_s3_path(); - $dirs['path'] = str_replace( WP_CONTENT_DIR, 's3://' . $this->bucket, $dirs['path'] ); - $dirs['basedir'] = str_replace( WP_CONTENT_DIR, 's3://' . $this->bucket, $dirs['basedir'] ); + $dirs['path'] = str_replace( WP_CONTENT_DIR, $s3_path, $dirs['path'] ); + $dirs['basedir'] = str_replace( WP_CONTENT_DIR, $s3_path, $dirs['basedir'] ); if ( ! defined( 'S3_UPLOADS_DISABLE_REPLACE_UPLOAD_URL' ) || ! S3_UPLOADS_DISABLE_REPLACE_UPLOAD_URL ) { if ( defined( 'S3_UPLOADS_USE_LOCAL' ) && S3_UPLOADS_USE_LOCAL ) { - $dirs['url'] = str_replace( 's3://' . $this->bucket, $dirs['baseurl'] . '/s3/' . $this->bucket, $dirs['path'] ); - $dirs['baseurl'] = str_replace( 's3://' . $this->bucket, $dirs['baseurl'] . '/s3/' . $this->bucket, $dirs['basedir'] ); + $dirs['url'] = str_replace( $s3_path, $dirs['baseurl'] . '/s3/' . $this->bucket, $dirs['path'] ); + $dirs['baseurl'] = str_replace( $s3_path, $dirs['baseurl'] . '/s3/' . $this->bucket, $dirs['basedir'] ); } else { - $dirs['url'] = str_replace( 's3://' . $this->bucket, $this->get_s3_url(), $dirs['path'] ); - $dirs['baseurl'] = str_replace( 's3://' . $this->bucket, $this->get_s3_url(), $dirs['basedir'] ); + $dirs['url'] = str_replace( $s3_path, $this->get_s3_url(), $dirs['path'] ); + $dirs['baseurl'] = str_replace( $s3_path, $this->get_s3_url(), $dirs['basedir'] ); } } @@ -525,6 +533,15 @@ public function set_attachment_files_acl( int $attachment_id, string $acl ) : ?W return new WP_Error( $e->getCode(), $e->getMessage() ); } + /** + * Fires after ACL of files of an attachment is set. + * + * @param int $attachment_id Attachment whose ACL has been changed. + * @param string $acl The new ACL that's been set. + * @psalm-suppress TooManyArguments -- Currently do_action doesn't detect variable number of arguments. + */ + do_action( 's3_uploads_set_attachment_files_acl', $attachment_id, $acl ); + return null; } @@ -665,6 +682,10 @@ public function get_files_for_unique_filename_file_list( ?array $files, string $ $name = pathinfo( $filename, PATHINFO_FILENAME ); // The s3:// streamwrapper support listing by partial prefixes with wildcards. // For example, scandir( s3://bucket/2019/06/my-image* ) - return scandir( trailingslashit( $dir ) . $name . '*' ); + $scandir = scandir( trailingslashit( $dir ) . $name . '*' ); + if ( $scandir === false ) { + $scandir = []; // Set as empty array for return + } + return $scandir; } } diff --git a/inc/class-stream-wrapper.php b/inc/class-stream-wrapper.php index 134b63d72..0cb22676e 100644 --- a/inc/class-stream-wrapper.php +++ b/inc/class-stream-wrapper.php @@ -461,7 +461,7 @@ function () use ( $parts, $path ) { } catch ( S3Exception $e ) { // Maybe this isn't an actual key, but a prefix. Do a prefix // listing of objects to determine. - $result = $this->getClient()->listObjects( + $result = $this->getClient()->listObjectsV2( [ 'Bucket' => $parts['Bucket'], 'Prefix' => rtrim( $parts['Key'], '/' ) . '/', @@ -615,7 +615,7 @@ public function dir_opendir( $path, $options ) { // the preg_match() call. // // Essentially, wp_unique_filename( my-file.jpg ) doing a `scandir( s3://bucket/2019/04/ )` will actually result in an s3 - // listObject query for `s3://bucket/2019/04/my-file` which means even if there are millions of files in `2019/04/` we only + // listObjectsV2 query for `s3://bucket/2019/04/my-file` which means even if there are millions of files in `2019/04/` we only // return a much smaller subset. // // Anyone reading this far, brace yourselves for a mighty horrible hack. @@ -635,7 +635,7 @@ public function dir_opendir( $path, $options ) { // Filter our "/" keys added by the console as directories, and ensure // that if a filter function is provided that it passes the filter. $this->objectIterator = \Aws\flatmap( - $this->getClient()->getPaginator( 'ListObjects', $op ), + $this->getClient()->getPaginator( 'ListObjectsV2', $op ), function ( Result $result ) use ( $filterFn ) { /** @var list */ $contentsAndPrefixes = $result->search( '[Contents[], CommonPrefixes[]][]' ); @@ -1087,7 +1087,7 @@ private function deleteSubfolder( string $path, array $params ) : bool { // Use a key that adds a trailing slash if needed. $prefix = rtrim( $params['Key'], '/' ) . '/'; /** @var array{Contents: list, CommonPrefixes:array} */ - $result = $this->getClient()->listObjects( + $result = $this->getClient()->listObjectsV2( [ 'Bucket' => $params['Bucket'], 'Prefix' => $prefix, diff --git a/inc/class-wp-cli-command.php b/inc/class-wp-cli-command.php index 1134bfc12..a6ad8094c 100644 --- a/inc/class-wp-cli-command.php +++ b/inc/class-wp-cli-command.php @@ -143,7 +143,7 @@ public function ls( array $args ) { try { $objects = $s3->getIterator( - 'ListObjects', [ + 'ListObjectsV2', [ 'Bucket' => strtok( S3_UPLOADS_BUCKET, '/' ), 'Prefix' => $prefix, ] diff --git a/tests/run-tests.sh b/tests/run-tests.sh index 2a4ba3ade..46a6e32cf 100755 --- a/tests/run-tests.sh +++ b/tests/run-tests.sh @@ -10,7 +10,7 @@ mkdir /tmp/s3-uploads-tests/tests docker run --rm --name s3-uploads-tests-minio -d --rm -p 9000:9000 -e MINIO_ACCESS_KEY=AWSACCESSKEY -e MINIO_SECRET_KEY=AWSSECRETKEY -v /tmp/s3-uploads-tests:/data minio/minio server /data > /dev/null -docker run --rm -e S3_UPLOADS_BUCKET=tests -e S3_UPLOADS_KEY=AWSACCESSKEY -e S3_UPLOADS_SECRET=AWSSECRETKEY -e S3_UPLOADS_REGION=us-east-1 -v $PWD:/code humanmade/plugin-tester $@ +docker run --rm -e AWS_SUPPRESS_PHP_DEPRECATION_WARNING=1 -e S3_UPLOADS_BUCKET=tests -e S3_UPLOADS_KEY=AWSACCESSKEY -e S3_UPLOADS_SECRET=AWSSECRETKEY -e S3_UPLOADS_REGION=us-east-1 -v $PWD:/code humanmade/plugin-tester $@ docker kill s3-uploads-tests-minio > /dev/null echo "Running Psalm..." diff --git a/tests/test-s3-uploads.php b/tests/test-s3-uploads.php index 0c1756419..a0b7fd040 100644 --- a/tests/test-s3-uploads.php +++ b/tests/test-s3-uploads.php @@ -73,7 +73,7 @@ public function test_generate_attachment_metadata() { $upload_dir = wp_upload_dir(); copy( dirname( __FILE__ ) . '/data/sunflower.jpg', $upload_dir['path'] . '/sunflower.jpg' ); $test_file = $upload_dir['path'] . '/sunflower.jpg'; - $attachment_id = $this->factory->attachment->create_object( $test_file, 0, array( + $attachment_id = self::factory()->attachment->create_object( $test_file, 0, array( 'post_mime_type' => 'image/jpeg', 'post_excerpt' => 'A sample caption', ) ); @@ -99,7 +99,7 @@ public function test_generate_attachment_metadata_for_mp4() { $upload_dir = wp_upload_dir(); copy( dirname( __FILE__ ) . '/data/video.m4v', $upload_dir['path'] . '/video.m4v' ); $test_file = $upload_dir['path'] . '/video.m4v'; - $attachment_id = $this->factory->attachment->create_object( $test_file, 0, array( + $attachment_id = self::factory()->attachment->create_object( $test_file, 0, array( 'post_mime_type' => 'video/mp4', 'post_excerpt' => 'A sample caption', ) ); @@ -116,7 +116,7 @@ public function test_image_sizes_are_deleted_on_attachment_delete() { $upload_dir = wp_upload_dir(); copy( dirname( __FILE__ ) . '/data/sunflower.jpg', $upload_dir['path'] . '/sunflower.jpg' ); $test_file = $upload_dir['path'] . '/sunflower.jpg'; - $attachment_id = $this->factory->attachment->create_object( $test_file, 0, array( + $attachment_id = self::factory()->attachment->create_object( $test_file, 0, array( 'post_mime_type' => 'image/jpeg', 'post_excerpt' => 'A sample caption', ) ); @@ -138,7 +138,7 @@ public function test_generate_attachment_metadata_for_pdf() { $upload_dir = wp_upload_dir(); copy( dirname( __FILE__ ) . '/data/gdpr.pdf', $upload_dir['path'] . '/gdpr.pdf' ); $test_file = $upload_dir['path'] . '/gdpr.pdf'; - $attachment_id = $this->factory->attachment->create_object( $test_file, 0, array( + $attachment_id = self::factory()->attachment->create_object( $test_file, 0, array( 'post_mime_type' => 'application/pdf', 'post_excerpt' => 'A sample caption', ) );