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
11 changes: 11 additions & 0 deletions plugins/baser-core/src/Model/Behavior/BcContentsBehavior.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,17 @@ public function afterMarshal(EventInterface $event, EntityInterface $entity, Arr
$entity->content->type = $entity->content->type ?? Inflector::classify($type);
}
}

// バリデーションエラー時、アソシエーションエンティティ(Content)のファイルアップロードも復元
// BlogContentなど、BcUploadBehaviorがアタッチされていないテーブルでも、
// 関連するContentテーブルのファイルアップロード処理を実行
if ($entity->getErrors() && isset($entity->content) && $entity->content instanceof EntityInterface) {
$contentsTable = $this->Contents->getTarget();
if ($contentsTable->hasBehavior('BcUpload')) {
$uploadBehavior = $contentsTable->getBehavior('BcUpload');
$uploadBehavior->BcFileUploader[$contentsTable->getAlias()]->rollbackFile($entity->content, true);
}
}
}

/**
Expand Down
41 changes: 41 additions & 0 deletions plugins/baser-core/src/Model/Behavior/BcUploadBehavior.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,12 @@ public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObj
$this->oldEntity[$this->table()->getAlias()][$data['_bc_upload_id']] = (!empty($data['id']))? $this->getOldEntity($data['id']) : null;
// ファイルアップロード用のフィールドのエンティティ変換を許可する
$options['accessibleFields']['_bc_upload_id'] = true;

// バリデーションエラー時の_tmp値保持のため、_tmpフィールドも許可
$settings = $this->BcFileUploader[$this->table()->getAlias()]->getSettings();
foreach ($settings['fields'] as $setting) {
$options['accessibleFields'][$setting['name'] . '_tmp'] = true;
}
}

/**
Expand All @@ -150,9 +156,44 @@ public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObj
*/
public function afterMarshal(EventInterface $event, EntityInterface $entity, ArrayObject $options)
{
// 現在のエンティティにエラーがある場合
if ($entity->getErrors()) {
$this->BcFileUploader[$this->table()->getAlias()]->rollbackFile($entity);
}

// アソシエーションエンティティ(例: BlogContent->content、BlogPost->seo_meta)にもrollbackFile()を適用
// 複数モデルの場合、関連するContentなどのエンティティのバリデーションエラーにも対応
foreach ($this->table()->associations() as $association) {
$associationName = $association->getName();
// アソシエーション名をプロパティ名に変換(例: "Contents" → "content", "BlogCategories" → "blog_categories")
$associationProperty = \Cake\Utility\Inflector::variable($associationName);

// アソシエーションのプロパティ名も確認(CakePHPのORM定義による)
$associationPropertyName = $association->getProperty();

// 実際のプロパティ名を特定(両方試す)
$associatedEntity = null;
if (isset($entity->{$associationProperty}) && $entity->{$associationProperty} instanceof EntityInterface) {
$associatedEntity = $entity->{$associationProperty};
} elseif (isset($entity->{$associationPropertyName}) && $entity->{$associationPropertyName} instanceof EntityInterface) {
$associatedEntity = $entity->{$associationPropertyName};
}

if ($associatedEntity) {
$associatedTable = $association->getTarget();
if ($associatedTable->hasBehavior('BcUpload')) {
$associatedBehavior = $associatedTable->getBehavior('BcUpload');

// メインエンティティにエラーがある場合、アソシエーションエンティティのファイルも復元する
// $force=true でエンティティ自身のエラー有無チェックをスキップ
if ($entity->getErrors()) {
$associatedBehavior->BcFileUploader[$associatedTable->getAlias()]->rollbackFile($associatedEntity, true);
} else {
$associatedBehavior->BcFileUploader[$associatedTable->getAlias()]->rollbackFile($associatedEntity);
}
}
}
}
}

/**
Expand Down
113 changes: 105 additions & 8 deletions plugins/baser-core/src/Utility/BcFileUploader.php
Original file line number Diff line number Diff line change
Expand Up @@ -251,10 +251,22 @@ public function setupTmpData($data)
{
foreach($this->settings['fields'] as $setting) {
$name = $setting['name'];
if (isset($data[$name . '_tmp']) && $this->moveFileSessionToTmp($data, $name)) {

// _tmp値が存在し、かつ新しいファイルがアップロードされていない場合のみ、セッションから復元
// 新しいファイルがアップロードされている場合は、そちらを優先してセッション復元をスキップ
// UPLOAD_ERR_OK で判定することで、環境依存の /tmp/ パスに依存しない
$hasNewFile = !empty($data[$name])
&& is_array($data[$name])
&& isset($data[$name]['error'])
&& (int)$data[$name]['error'] === UPLOAD_ERR_OK
&& !empty($data[$name]['name'])
&& !empty($data[$name]['tmp_name']);

if (isset($data[$name . '_tmp']) && !$hasNewFile && $this->moveFileSessionToTmp($data, $name)) {
$data[$setting['name']] = $this->getUploadingFiles($data['_bc_upload_id'])[$setting['name']];
// セッションに一時ファイルが保存されている場合は復元する
unset($data[$setting['name'] . '_tmp']);
// 注: _tmp値は削除せず保持(バリデーションエラー時のrollbackFileで使用)
// unset($data[$setting['name'] . '_tmp']);
}
}
}
Expand Down Expand Up @@ -314,6 +326,12 @@ public function saveFiles($entity)
$entity->{$setting['name']} = $files[$setting['name']]['name'];
}
}
// 保存完了後は tmp セッションを削除(セッション肥大化防止)
$tmpValue = $entity->get($setting['name'] . '_tmp');
if ($tmpValue) {
$sessionKey = str_replace(['.', '/'], ['_', '_'], $tmpValue);
$this->Session->delete('Upload.' . $sessionKey);
}
}
$this->setUploadingFiles($files, $entity->_bc_upload_id);
}
Expand Down Expand Up @@ -478,9 +496,19 @@ public function moveFileSessionToTmp($data, $fieldName)
$fileName = $data[$fieldName . '_tmp'];
$sessionKey = str_replace(['.', '/'], ['_', '_'], $fileName);
$tmpName = $this->savePath . $sessionKey;
$fileData = base64_decode($this->Session->read('Upload.' . $sessionKey . '.data'));
$fileData = $this->Session->read('Upload.' . $sessionKey . '.data');

// セッションにデータが存在しない場合は処理をスキップ
if ($fileData === null) {
return false;
}

$fileData = base64_decode($fileData);
$fileType = $this->Session->read('Upload.' . $sessionKey . '.type');
$this->Session->delete('Upload.' . $sessionKey);

// 注: バリデーションエラーが繰り返される場合に備えて、セッションは削除しない
// 保存成功時に削除される(BcFileUploader::saveFiles() → BcFileUploader::deleteFiles())
// $this->Session->delete('Upload.' . $sessionKey);

// サイズを取得
if (ini_get('mbstring.func_overload') & 2 && function_exists('mb_strlen')) {
Expand Down Expand Up @@ -1091,7 +1119,6 @@ public function getUploadingFiles($bcUploadId): array
public function saveTmpFiles($data, $tmpId)
{
if (!$data) return false;
$this->Session->delete('Upload');
$this->tmpId = $tmpId;
$data = $this->setupRequestData($data);
$files = $this->getUploadingFiles($data['_bc_upload_id']);
Expand Down Expand Up @@ -1139,6 +1166,14 @@ public function saveTmpFile($setting, $file, $entity)
$fileName = $this->getSaveTmpFileName($setting, $file, $entity);
$this->rotateImage($file['tmp_name']);
$name = str_replace(['.', '/'], ['_', '_'], $fileName);

// 同フィールドの古い tmp セッションを削除(複数回アップロード時の旧エントリ蓄積を防ぐ)
$oldTmp = $entity->get($setting['name'] . '_tmp');
if ($oldTmp) {
$oldSessionKey = str_replace(['.', '/'], ['_', '_'], $oldTmp);
$this->Session->delete('Upload.' . $oldSessionKey);
}

$this->Session->write('Upload.' . $name, $setting);
$this->Session->write('Upload.' . $name . '.type', $file['type']);
$this->Session->write('Upload.' . $name . '.data', base64_encode(file_get_contents($file['tmp_name'])));
Expand All @@ -1162,8 +1197,31 @@ public function getSaveTmpFileName($setting, $file, $entity)
$entity[$setting['namefield']] = $this->tmpId;
}
$fileName = $this->getFieldBasename($setting, $file, $entity);

// getFieldBasename()がfalseを返した場合(entity_idが空など)、
// 代替のファイル名を生成(tmpId + フィールド名 + タイムスタンプ)
if ($fileName === false) {
// _bc_upload_idがある場合はそれを使用、なければ'tmp'を使用
$tmpId = !empty($entity['_bc_upload_id']) ? $entity['_bc_upload_id'] : 'tmp';
$fileName = $tmpId . '_' . $setting['name'] . '_' . time() . '.' . $file['ext'];
} else {
// 一時ファイルの場合、ファイル名にタイムスタンプを追加してユニーク化
// (バリデーションエラー時に新しいファイルをアップロードした際のブラウザキャッシュ問題を回避)
$pathInfo = pathinfo($fileName);
$dirname = $pathInfo['dirname'];
if ($dirname === '.') {
$fileName = $pathInfo['filename'] . '_' . time() . '.' . $pathInfo['extension'];
} else {
$fileName = $dirname . '/' .
$pathInfo['filename'] . '_' .
time() . '.' .
$pathInfo['extension'];
}
}
} else {
$fileName = $this->tmpId . '_' . $setting['name'] . '.' . $file['ext'];
// namefieldが指定されていない場合
$tmpId = !empty($entity['_bc_upload_id']) ? $entity['_bc_upload_id'] : 'tmp';
$fileName = $tmpId . '_' . $setting['name'] . '_' . time() . '.' . $file['ext'];
}
return $fileName;
}
Expand Down Expand Up @@ -1195,16 +1253,55 @@ public function resetUploaded()
* ファイルアップロード対象のデータを元に戻す
*
* @param EntityInterface $entity
* @param bool $force エラーがなくても強制実行する場合に true を指定
* @checked
* @noTodo
* @unitTest
*/
public function rollbackFile(EntityInterface $entity)
public function rollbackFile(EntityInterface $entity, bool $force = false)
{
if (!$entity->getErrors()) return;
if (!$force && !$entity->getErrors()) return;

// バリデーションエラー時、アップロードされたファイルをセッションに保存して復元可能にする
$uploadingFiles = $this->getUploadingFiles($entity->_bc_upload_id);

foreach($this->settings['fields'] as $setting) {
// 値を入れ直すとエラー状態がリセットされてしまうので改めてセットしなおす
$error = $entity->getError($setting['name']);

// 実際にユーザーが新規アップロードしたファイルかを判定
// UPLOAD_ERR_OK で判定することで、環境依存の /tmp/ パスに依存しない
// (セッションから復元されたファイルは error キーを持たないか UPLOAD_ERR_OK 以外になる)
$isNewUpload = !empty($uploadingFiles[$setting['name']])
&& isset($uploadingFiles[$setting['name']]['error'])
&& (int)$uploadingFiles[$setting['name']]['error'] === UPLOAD_ERR_OK
&& !empty($uploadingFiles[$setting['name']]['name'])
&& !empty($uploadingFiles[$setting['name']]['tmp_name']);

// 新しいファイルがアップロードされている場合:セッションを新規ファイルで更新
if ($isNewUpload) {
$tmpFileName = $this->saveTmpFile($setting, $uploadingFiles[$setting['name']], $entity);
if ($tmpFileName) {
// _tmpサフィックスでファイル名を保持(次回のリクエストで復元用)
$entity->{$setting['name'] . '_tmp'} = $tmpFileName;
}
} else {
// 新しいファイルがアップロードされていない場合:既存のセッション画像を保持
// リクエストデータ(hidden フィールド)から_tmp値を取得
// (getOriginal()ではなく、エンティティの現在の値を取得)
$tmpValue = $entity->get($setting['name'] . '_tmp');

if ($tmpValue) {
$entity->{$setting['name'] . '_tmp'} = $tmpValue;
} else {
// フォールバック: getOriginal()でも試す(念のため)
$originalTmpValue = $entity->getOriginal($setting['name'] . '_tmp');
if ($originalTmpValue) {
$entity->{$setting['name'] . '_tmp'} = $originalTmpValue;
}
}
}

$entity->{$setting['name']} = $entity->getOriginal($setting['name']);
if ($error) $entity->setError($setting['name'], $error);
}
Expand Down
Loading
Loading