Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion classes/PodsAPI.php
Original file line number Diff line number Diff line change
Expand Up @@ -6071,6 +6071,13 @@ public function prepare_tableless_data_for_save( $pod, $field, $values, $pieces
// Enforce integers / unique values for IDs
$value_ids = [];

// Item ID for custom upload directory tag context (e.g. {@ID}).
$item_id = 0;

if ( isset( $pieces['params'] ) && isset( $pieces['params']->id ) ) {
$item_id = (int) $pieces['params']->id;
}

// @todo Handle simple relationships eventually
foreach ( $values as $v ) {
if ( ! is_array( $v ) ) {
Expand All @@ -6084,7 +6091,7 @@ public function prepare_tableless_data_for_save( $pod, $field, $values, $pieces
// If file not found, add it
if ( empty( $v ) ) {
try {
$v = pods_attachment_import( $v );
$v = pods_attachment_import( $v, $item_id, false, false, $field, $pod );
} catch ( Throwable $throwable ) {
pods_debug_log( $throwable );

Expand Down
128 changes: 81 additions & 47 deletions classes/fields/file.php
Original file line number Diff line number Diff line change
Expand Up @@ -1289,61 +1289,37 @@ public function admin_ajax_upload() {

if ( null === $custom_handler ) {

// Start custom directory.
$upload_dir = pods_v( $field['type'] . '_upload_dir', $field, 'wp' );

if ( 'wp' !== $upload_dir ) {
$custom_dir = pods_v( $field['type'] . '_upload_dir_custom', $field, '' );
$context_pod = null;
$context_pod = null;

if ( $parent_post_id ) {
$context_pod = pods_get_instance( pods_v( 'name', $pod, false ), $parent_post_id );
if ( $parent_post_id ) {
$context_pod = pods_get_instance( pods_v( 'name', $pod, false ), $parent_post_id );

if ( ! $context_pod->exists() ) {
$context_pod = null;
}
if ( ! $context_pod->exists() ) {
$context_pod = null;
}

/**
* Filter the custom upload directory Pod context.
*
* @since 2.7.28
*
* @param Pods $context_pod The Pods object of the associated pod for the post type.
* @param object $params The POSTed parameters for the request.
* @param array $field The field configuration associated to the upload field.
* @param array $pod The pod configuration associated to the upload field.
*/
$context_pod = apply_filters( 'pods_upload_dir_custom_context_pod', $context_pod, $params, $field, $pod );

$custom_dir = pods_evaluate_tags( $custom_dir, [ 'pod' => $context_pod ] );

/**
* Filter the custom Pod upload directory.
*
* @since 2.7.28
*
* @param string $custom_dir The directory to use for the uploaded file.
* @param object $params The POSTed parameters for the request.
* @param Pods $context_pod The Pods object of the associated pod for the post type.
* @param array $field The field configuration associated to the upload field.
* @param array $pod The pod configuration associated to the upload field.
*/
$custom_dir = apply_filters( 'pods_upload_dir_custom', $custom_dir, $params, $context_pod, $field, $pod );

self::$tmp_upload_dir = $custom_dir;

add_filter( 'upload_dir', [ $this, 'filter_upload_dir' ] );
}

/**
* Filter the custom upload directory Pod context.
*
* @since 2.7.28
*
* @param Pods $context_pod The Pods object of the associated pod for the post type.
* @param object $params The POSTed parameters for the request.
* @param array $field The field configuration associated to the upload field.
* @param array $pod The pod configuration associated to the upload field.
*/
$context_pod = apply_filters( 'pods_upload_dir_custom_context_pod', $context_pod, $params, $field, $pod );

// Start custom directory.
$is_custom_dir = static::use_custom_upload_dir( $field, $context_pod, $params, $pod );

// Upload file.
$attachment_id = media_handle_upload( 'Filedata', $parent_post_id );

// End custom directory.
if ( 'wp' !== $upload_dir ) {
remove_filter( 'upload_dir', [ $this, 'filter_upload_dir' ] );

self::$tmp_upload_dir = null;
if ( $is_custom_dir ) {
static::use_custom_upload_dir_teardown();
}

if ( is_object( $attachment_id ) ) {
Expand Down Expand Up @@ -1382,6 +1358,64 @@ public function admin_ajax_upload() {
// KBAI!
}

/**
* Prepare the custom upload directory for an upload.
*
* Sets the temporary upload directory and registers the upload_dir filter
* when the field is configured to use a custom directory. Call
* use_custom_upload_dir_teardown() after the upload completes.
*
* @since 3.4.0
*
* @param array $field The field configuration associated to the upload field.
* @param Pods|null $context_pod The Pods object of the associated pod, or null if not available.
* @param object|array $params The upload parameters, for filter back-compat.
* @param array|null $pod The pod configuration, for filter back-compat.
*
* @return bool Whether a custom directory was set up.
*/
public static function use_custom_upload_dir( $field, $context_pod = null, $params = [], $pod = null ) {
$upload_dir = pods_v( $field['type'] . '_upload_dir', $field, 'wp' );

if ( 'wp' === $upload_dir ) {
return false;
}

$custom_dir = pods_v( $field['type'] . '_upload_dir_custom', $field, '' );

$custom_dir = pods_evaluate_tags( $custom_dir, [ 'pod' => $context_pod ] );

/**
* Filter the custom Pod upload directory.
*
* @since 2.7.28
*
* @param string $custom_dir The directory to use for the uploaded file.
* @param object $params The POSTed parameters for the request.
* @param Pods $context_pod The Pods object of the associated pod for the post type.
* @param array $field The field configuration associated to the upload field.
* @param array $pod The pod configuration associated to the upload field.
*/
$custom_dir = apply_filters( 'pods_upload_dir_custom', $custom_dir, $params, $context_pod, $field, $pod );

self::$tmp_upload_dir = $custom_dir;

add_filter( 'upload_dir', [ self::class, 'filter_upload_dir' ] );

return true;
}

/**
* Tear down the temporary custom upload directory setup.
*
* @since 3.4.0
*/
public static function use_custom_upload_dir_teardown() {
remove_filter( 'upload_dir', [ self::class, 'filter_upload_dir' ] );

self::$tmp_upload_dir = null;
}

/**
* Modify the upload directory.
*
Expand All @@ -1393,7 +1427,7 @@ public function admin_ajax_upload() {
*
* @return array The filtered uploads directory information.
*/
public function filter_upload_dir( $uploads ) {
public static function filter_upload_dir( $uploads ) {
if ( empty( self::$tmp_upload_dir ) ) {
return $uploads;
}
Expand Down
37 changes: 35 additions & 2 deletions includes/media.php
Original file line number Diff line number Diff line change
Expand Up @@ -231,29 +231,53 @@ function pods_image_url( $image, $size = 'thumbnail', $default = 0, $force = fal
* @param int $post_parent ID of post parent, default none.
* @param boolean $featured Whether to set it as the featured (post thumbnail) of the post parent.
* @param boolean $strict Whether to return errors upon failure.
* @param array $field Optional. The field configuration associated to the upload field.
* @param array $pod Optional. The pod configuration associated to the upload field.
*
* @return int Attachment ID.
*
* @since 2.3.0
*/
function pods_attachment_import( $url, $post_parent = null, $featured = false, $strict = false ) {// Only allow http(s).
function pods_attachment_import( $url, $post_parent = null, $featured = false, $strict = false, $field = null, $pod = null ) {
// Only allow http(s).
$scheme = wp_parse_url( $url, PHP_URL_SCHEME );

if ( ! in_array( $scheme, [ 'http', 'https' ], true ) ) {
return $strict ? (int) pods_error( __( 'Invalid file URL.', 'pods' ) ) : 0;
}

$is_custom_dir = false;

if ( $field ) {
$pod_name = pods_v( 'name', $pod, false );
$context_pod = null;

if ( $pod_name ) {
$context_pod = pods_get_instance( $pod_name, $post_parent );

if ( ! $context_pod->exists() ) {
$context_pod = null;
}
}

$is_custom_dir = PodsField_File::use_custom_upload_dir( $field, $context_pod, [], $pod );
}

require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/image.php';

$tmp = download_url( $url );

if ( is_wp_error( $tmp ) ) {
if ( $is_custom_dir ) {
PodsField_File::use_custom_upload_dir_teardown();
}

return $strict ? (int) pods_error( $tmp->get_error_message() ) : 0;
}

$file_array = [
$file_array = [
'name' => basename( wp_parse_url( $url, PHP_URL_PATH ) ),
'tmp_name' => $tmp,
];
Expand All @@ -263,9 +287,18 @@ function pods_attachment_import( $url, $post_parent = null, $featured = false, $
if ( is_wp_error( $attachment_id ) ) {
wp_delete_file( $tmp );

if ( $is_custom_dir ) {
PodsField_File::use_custom_upload_dir_teardown();
}

return $strict ? (int) pods_error( $attachment_id->get_error_message() ) : 0;
}

// End custom directory now that all attachment file sizes have been generated.
if ( $is_custom_dir ) {
PodsField_File::use_custom_upload_dir_teardown();
}

if ( 0 < $post_parent && $featured ) {
update_post_meta( $post_parent, '_thumbnail_id', $attachment_id );
}
Expand Down
124 changes: 124 additions & 0 deletions tests/codeception/wpunit/functions/MediaTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?php

namespace Pods_Unit_Tests\Functions;

use Pods_Unit_Tests\Pods_UnitTestCase;

/**
* Test attachment import via pods_attachment_import().
*
* @group pods
*/
class MediaTest extends Pods_UnitTestCase {

/**
* Path to the source image fixture.
*
* @var string
*/
protected $source_file;

public function setUp(): void {
parent::setUp();

$fixture = dirname( __DIR__, 2 ) . '/_data/images/zoltar.jpg';

$this->assertFileExists( $fixture, 'Test image fixture is missing.' );

$this->source_file = $fixture;
}

public function tearDown(): void {
$this->source_file = null;

parent::tearDown();
}

/**
* A file imported through a field configured with a custom upload directory
* should be stored under that directory and not the default yyyy/mm path.
*/
public function test_custom_upload_dir_used_for_import() {
$pod = [
'name' => 'test_media_pod',
'type' => 'pod',
];

$field = [
'type' => 'file',
'file_upload_dir' => 'custom',
'file_upload_dir_custom' => 'custom-test-dir',
];

$attachment_id = pods_attachment_import( $this->source_file, 0, false, false, $field, $pod );

$this->assertIsInt( $attachment_id );
$this->assertGreaterThan( 0, $attachment_id, 'Attachment import failed.' );

$file = get_attached_file( $attachment_id );

$this->assertIsString( $file );
$this->assertStringContainsString( 'custom-test-dir', $file, 'Attachment was not stored in the custom upload directory.' );

// The main file should live outside the default date-based uploads subdir.
$default_subdir = current_time( 'Y/m' );

$this->assertStringNotContainsString( $default_subdir, $file, 'Attachment was stored in the default uploads subdir instead of the custom directory.' );

// Image sub-sizes must also land in the custom directory.
$metadata = wp_get_attachment_metadata( $attachment_id );

if ( ! empty( $metadata['sizes'] ) ) {
$base_dir = dirname( $file );

foreach ( $metadata['sizes'] as $size ) {
$this->assertStringContainsString( 'custom-test-dir', $base_dir . '/' . $size['file'], 'Image sub-size was not stored in the custom upload directory.' );
}
}

wp_delete_attachment( $attachment_id, true );
}

/**
* Default behavior is unchanged: importing without a custom directory uses
* the standard WordPress uploads path.
*/
public function test_default_wp_upload_dir_used_without_custom_config() {
$attachment_id = pods_attachment_import( $this->source_file );

$this->assertIsInt( $attachment_id );
$this->assertGreaterThan( 0, $attachment_id, 'Attachment import failed.' );

$file = get_attached_file( $attachment_id );

$this->assertIsString( $file );
$this->assertStringNotContainsString( 'custom-test-dir', $file );

wp_delete_attachment( $attachment_id, true );
}

/**
* A field still using the default 'wp' upload directory should not apply a
* custom directory even when a custom path value is present.
*/
public function test_wp_upload_dir_option_ignores_custom_path() {
$field = [
'type' => 'file',
'file_upload_dir' => 'wp',
'file_upload_dir_custom' => 'should-be-ignored',
];

$attachment_id = pods_attachment_import( $this->source_file, 0, false, false, $field );

$this->assertIsInt( $attachment_id );
$this->assertGreaterThan( 0, $attachment_id, 'Attachment import failed.' );

$file = get_attached_file( $attachment_id );

$this->assertIsString( $file );
$this->assertStringNotContainsString( 'should-be-ignored', $file );

wp_delete_attachment( $attachment_id, true );
}

}