diff --git a/plugins/baser-core/src/Model/Behavior/BcContentsBehavior.php b/plugins/baser-core/src/Model/Behavior/BcContentsBehavior.php
index 8c288195f7..e9e5686a8f 100644
--- a/plugins/baser-core/src/Model/Behavior/BcContentsBehavior.php
+++ b/plugins/baser-core/src/Model/Behavior/BcContentsBehavior.php
@@ -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);
+ }
+ }
}
/**
diff --git a/plugins/baser-core/src/Model/Behavior/BcUploadBehavior.php b/plugins/baser-core/src/Model/Behavior/BcUploadBehavior.php
index 67affdf275..6cc77e9d0f 100755
--- a/plugins/baser-core/src/Model/Behavior/BcUploadBehavior.php
+++ b/plugins/baser-core/src/Model/Behavior/BcUploadBehavior.php
@@ -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;
+ }
}
/**
@@ -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);
+ }
+ }
+ }
+ }
}
/**
diff --git a/plugins/baser-core/src/Utility/BcFileUploader.php b/plugins/baser-core/src/Utility/BcFileUploader.php
index 4b3fecdc2f..3fec31c680 100644
--- a/plugins/baser-core/src/Utility/BcFileUploader.php
+++ b/plugins/baser-core/src/Utility/BcFileUploader.php
@@ -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']);
}
}
}
@@ -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);
}
@@ -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')) {
@@ -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']);
@@ -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'])));
@@ -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;
}
@@ -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);
}
diff --git a/plugins/baser-core/src/View/Helper/BcFormHelper.php b/plugins/baser-core/src/View/Helper/BcFormHelper.php
index d075b9a878..530920fe00 100644
--- a/plugins/baser-core/src/View/Helper/BcFormHelper.php
+++ b/plugins/baser-core/src/View/Helper/BcFormHelper.php
@@ -13,6 +13,7 @@
use BaserCore\Utility\BcContainerTrait;
use Cake\Core\Plugin;
+use Cake\ORM\Entity;
use Cake\ORM\TableRegistry;
use Cake\Utility\Inflector;
use Cake\View\Form\EntityContext;
@@ -796,7 +797,17 @@ public function file($fieldName, $options = []): string
{
$options = $this->_initInputField($fieldName, $options);
- $table = $this->getTable($fieldName);
+ // optionsで明示的にtableが指定されている場合はそれを使う(動的アソシエーション対応)
+ if (!empty($options['table'])) {
+ try {
+ $table = TableRegistry::getTableLocator()->get($options['table']);
+ } catch (\Exception $e) {
+ $table = $this->getTable($fieldName);
+ }
+ } else {
+ $table = $this->getTable($fieldName);
+ }
+
if (!$table || !$table->hasBehavior('BcUpload')) {
return parent::file($fieldName, $options);
}
@@ -857,7 +868,55 @@ public function file($fieldName, $options = []): string
unset($options['deleteSpan'], $options['deleteCheckbox'], $options['deleteLabel']);
unset($options['figure'], $options['img'], $options['figcaption'], $options['div'], $options['table']);
- $fileLinkTag = $this->BcUpload->fileLink($fieldName, $this->_getContext()->entity(), $linkOptions);
+ // コンテキストからエンティティデータを取得
+ // fieldNameに'.'が含まれる場合(例:content.eyecatch)、モデル名を抽出
+ $modelName = null;
+ if (strpos($fieldName, '.') !== false) {
+ [$modelName, $shortFieldName] = explode('.', $fieldName);
+ } else {
+ $shortFieldName = $fieldName;
+ }
+
+ // コンテキストから既存のエンティティを取得(バリデーションエラー時の_tmp値を保持)
+ $context = $this->context();
+ $entity = null;
+ if ($context instanceof \Cake\View\Form\EntityContext) {
+ try {
+ if ($modelName) {
+ // ネストされたフィールドの場合(例:content.eyecatch)
+ $entity = $context->entity([$modelName]);
+ } else {
+ // トップレベルのフィールドの場合はfieldName全体で取得を試みる
+ $entity = $context->entity(explode('.', $fieldName));
+ }
+ } catch (\Exception $e) {
+ // エンティティ取得失敗時は後続の処理で新規作成
+ $entity = null;
+ }
+ }
+
+ // エンティティが取得できない場合は新規作成
+ if (!$entity) {
+ if ($modelName && method_exists($table, 'newEmptyEntity')) {
+ // まずコンテキストから配列を取得(バリデーションエラー時の_tmp値が含まれている)
+ $modelData = $context ? $context->val($modelName) : null;
+ // コンテキストから取得できない場合は、リクエストデータから取得
+ if (!$modelData || !is_array($modelData)) {
+ $modelData = $this->getSourceValue($modelName);
+ }
+ if (is_array($modelData)) {
+ // Viewレンダリング時はモデルイベントの副作用(BcUploadBehaviorのセッション書き込み等)を避けるため
+ // $table->newEntity() ではなく new Entity() で展示用エンティティを直接生成
+ $entity = new Entity($modelData);
+ } else {
+ $entity = $table->newEmptyEntity();
+ }
+ } else {
+ $entity = $table->newEmptyEntity();
+ }
+ }
+
+ $fileLinkTag = $this->BcUpload->fileLink($fieldName, $entity, $linkOptions);
$fileTag = parent::file($fieldName, $options);
if (empty($options['value'])) {
@@ -875,7 +934,35 @@ public function file($fieldName, $options = []): string
}
$hiddenValue = $this->getSourceValue($fieldName . '_');
$fileValue = $this->getSourceValue($fieldName);
+ // _tmp値の取得: rollbackFile()で設定された値を優先するため、コンテキストから取得
+ $tmpValue = null;
+ $context = $this->context();
+ if ($context) {
+ // ネストされたフィールド(content.eyecatch)の場合、まず完全なパスで試す
+ $tmpValue = $context->val($fieldName . '_tmp');
+ // 取得できない場合は、shortFieldNameで試す
+ if (!$tmpValue && $shortFieldName !== $fieldName) {
+ $tmpValue = $context->val($shortFieldName . '_tmp');
+ }
+ // 動的アソシエーション(ContentFiveTitle)の配列から取得を試みる
+ if (!$tmpValue && $modelName) {
+ // modelNameが存在する場合(例:content_five_title.title_5)
+ // まず配列を取得してから、_tmpキーを取得
+ $arrayData = $context->val($modelName);
+ if (is_array($arrayData) && isset($arrayData[$shortFieldName . '_tmp'])) {
+ $tmpValue = $arrayData[$shortFieldName . '_tmp'];
+ }
+ }
+ }
+ // コンテキストから取得できない場合は、リクエストデータから取得(後方互換性のため)
+ if (!$tmpValue) {
+ $tmpValue = $this->getSourceValue($fieldName . '_tmp');
+ if (!$tmpValue && strpos($fieldName, '.') !== false) {
+ $tmpValue = $this->getSourceValue($shortFieldName . '_tmp');
+ }
+ }
+ // hidden値管理(複数ファイルフィールド対応、セッションキー命名・変換は現状維持)
$hiddenTag = '';
if ($fileLinkTag) {
if (is_object($fileValue) && empty($fileValue->getClientFileName()) && $hiddenValue) {
@@ -888,10 +975,18 @@ public function file($fieldName, $options = []): string
}
}
+ // バリデーションエラー時のセッション画像参照用にtmpサフィックスのhidden値を追加(fileLinkTagの有無に関わらず)
+ if ($tmpValue) {
+ $hiddenTag .= $this->hidden($fieldName . '_tmp', ['value' => $tmpValue]);
+ }
+
$out = $fileTag;
if ($fileLinkTag) {
$out .= ' ' . $delCheckTag . $hiddenTag . '
' . $fileLinkTag;
+ } elseif ($hiddenTag) {
+ // fileLinkTagがなくてもhidden値があれば出力
+ $out .= $hiddenTag;
}
if (isset($divOptions)) {
@@ -942,6 +1037,10 @@ public function getTable($fieldName)
return false;
}
+ // $entityが配列の場合はgetSource()を呼ばずにfalseを返す
+ if (is_array($entity)) {
+ return false;
+ }
$alias = $entity->getSource();
$plugin = '';
if (strpos($alias, '.')) {
@@ -997,7 +1096,12 @@ public function control(string $fieldName, array $options = []): string
$options = ($event->getResult() === null || $event->getResult() === true)? $event->getData('options') : $event->getResult();
}
- $output = parent::control($fieldName, $options);
+ // type='file'の場合は直接file()メソッドを呼び出す(バリデーションエラー時のファイル保持対応)
+ if (!empty($options['type']) && $options['type'] === 'file') {
+ $output = $this->file($fieldName, $options);
+ } else {
+ $output = parent::control($fieldName, $options);
+ }
// EVENT Form.afterControl
$event = $this->dispatchLayerEvent('afterControl', [
diff --git a/plugins/baser-core/src/View/Helper/BcUploadHelper.php b/plugins/baser-core/src/View/Helper/BcUploadHelper.php
index 20753d44ec..5d9ab4ed78 100755
--- a/plugins/baser-core/src/View/Helper/BcUploadHelper.php
+++ b/plugins/baser-core/src/View/Helper/BcUploadHelper.php
@@ -101,7 +101,9 @@ public function beforeRender(Event $event, $viewFile)
*/
public function fileLink($fieldName, $entity, $options = [])
{
- if(!($entity instanceof EntityInterface)) throw new BcException(__d('baser_core', '第2引数に EntityInterface を指定してください。'));
+ if(!($entity instanceof EntityInterface)) {
+ throw new BcException(__d('baser_core', '第2引数に EntityInterface を指定してください。'));
+ }
$options = array_merge([
'imgsize' => 'medium', // 画像サイズ
'rel' => '', // rel属性
@@ -143,17 +145,34 @@ public function fileLink($fieldName, $entity, $options = [])
$basePath = '/files/' . str_replace(DS, '/', $settings['saveDir']) . '/';
+ // tmp画像優先表示ロジック: fieldName変更前に_tmp値をチェック(セッションキー命名・変換は現状維持)
+ $originalFieldName = $fieldName;
+ $sessionKey = Hash::get($entity, $fieldName . '_tmp');
+
+ // ネストフィールドの場合(例:seo_meta.og_image)、短いフィールド名でも試す
+ $shortFieldName = $fieldName;
+ if (strpos($fieldName, '.') !== false) {
+ [, $shortFieldName] = explode('.', $fieldName);
+ if (!$sessionKey) {
+ $sessionKey = Hash::get($entity, $shortFieldName . '_tmp');
+ }
+ }
+
if (empty($options['value'])) {
$value = Hash::get($entity, $fieldName);
if (!$value && strpos($fieldName, '.') !== false) {
[, $fieldName] = explode('.', $fieldName);
$value = Hash::get($entity, $fieldName);
+ // fieldNameが変更された場合も_tmp値を再チェック
+ if (!$sessionKey) {
+ $sessionKey = Hash::get($entity, $fieldName . '_tmp');
+ }
}
} else {
$value = $options['value'];
}
- $sessionKey = Hash::get($entity, $fieldName . '_tmp');
+ // セッションに一時ファイルがある場合は優先表示
if ($sessionKey) {
$tmp = true;
$value = str_replace('/', '_', $sessionKey);
@@ -187,7 +206,24 @@ public function fileLink($fieldName, $entity, $options = [])
} else {
$figcaptionOptions['class'] = 'file-name';
}
- if ($uploadSettings['type'] == 'image' || in_array($ext, $this->table->getBehavior('BcUpload')->BcFileUploader[$this->table->getAlias()]->imgExts)) {
+ $imgExts = [];
+ if ($this->table->hasBehavior('BcUpload')) {
+ $behavior = $this->table->getBehavior('BcUpload');
+ $alias = $this->table->getAlias();
+ $imgExts = [];
+ try {
+ $ref = new \ReflectionClass($behavior);
+ if ($ref->hasProperty('BcFileUploader')) {
+ $prop = $ref->getProperty('BcFileUploader');
+ $prop->setAccessible(true);
+ $bcFileUploader = $prop->getValue($behavior);
+ if ($bcFileUploader && isset($bcFileUploader[$alias]) && property_exists($bcFileUploader[$alias], 'imgExts')) {
+ $imgExts = $bcFileUploader[$alias]->imgExts;
+ }
+ }
+ } catch (\ReflectionException $e) {}
+ }
+ if ($uploadSettings['type'] == 'image' || in_array($ext, $imgExts)) {
$imgOptions = array_merge([
'imgsize' => $options['imgsize'],
'rel' => $options['rel'],
@@ -200,7 +236,7 @@ public function fileLink($fieldName, $entity, $options = [])
if ($tmp) {
$imgOptions['tmp'] = true;
}
- $out = $this->Html->tag('figure', $this->uploadImage($fieldName, $entity, $imgOptions) . '
' . $this->Html->tag('figcaption', BcUtil::mbBasename($value), $figcaptionOptions), $figureOptions);
+ $out = $this->Html->tag('figure', $this->uploadImage($originalFieldName, $entity, $imgOptions) . '
' . $this->Html->tag('figcaption', BcUtil::mbBasename($value), $figcaptionOptions), $figureOptions);
} else {
$filePath = $basePath . $value;
$linkOptions = ['target' => '_blank'];
@@ -274,6 +310,12 @@ public function uploadImage($fieldName, $entity, $options = [])
}
$fileName = Hash::get($entity, $fieldName);
+ // ドット記法(例: seo_meta.og_image)の場合、エンティティがネスト先そのものであれば
+ // 短いフィールド名(最後のセグメント)でフォールバック取得
+ if (!$fileName && strpos($fieldName, '.') !== false) {
+ $parts = explode('.', $fieldName);
+ $fileName = Hash::get($entity, end($parts));
+ }
// EVENT BcUpload.beforeUploadImage
$event = $this->dispatchLayerEvent('beforeUploadImage', [
@@ -315,8 +357,14 @@ public function uploadImage($fieldName, $entity, $options = [])
unset($linkOptions['class']);
}
+ // tmp画像優先表示ロジック(セッションキー命名・変換は現状維持)
if($entity) {
$sessionKey = Hash::get($entity, $fieldName . '_tmp');
+ // ドット記法の場合は短いフィールド名でも試みる
+ if (!$sessionKey && strpos($fieldName, '.') !== false) {
+ $parts = explode('.', $fieldName);
+ $sessionKey = Hash::get($entity, end($parts) . '_tmp');
+ }
if ($sessionKey) {
$fileName = $sessionKey;
$options['tmp'] = true;
@@ -353,9 +401,9 @@ public function uploadImage($fieldName, $entity, $options = [])
$options['imgsize'] = 'default';
}
if ($options['tmp']) {
- $options['link'] = false;
$fileUrl = '/baser-core/uploads/tmp/';
- if ($options['imgsize']) {
+ $maxSizeUrl = $fileUrl . str_replace('/', '_', $fileName);
+ if ($options['imgsize'] && $options['imgsize'] !== 'default') {
$fileUrl .= $options['imgsize'] . '/';
}
}
@@ -363,7 +411,7 @@ public function uploadImage($fieldName, $entity, $options = [])
if ($fileName == $options['noimage']) {
$mostSizeUrl = $fileName;
} elseif ($options['tmp']) {
- $mostSizeUrl = $fileUrl . str_replace(['.', '/'], ['_', '_'], $fileName);
+ $mostSizeUrl = $fileUrl . str_replace('/', '_', $fileName);
} else {
$check = false;
$maxSizeExists = false;
diff --git a/plugins/baser-core/tests/TestCase/Controller/Api/Admin/ThemesControllerTest.php b/plugins/baser-core/tests/TestCase/Controller/Api/Admin/ThemesControllerTest.php
index 722efaae2e..44f2242908 100644
--- a/plugins/baser-core/tests/TestCase/Controller/Api/Admin/ThemesControllerTest.php
+++ b/plugins/baser-core/tests/TestCase/Controller/Api/Admin/ThemesControllerTest.php
@@ -85,9 +85,10 @@ public function testIndex(): void
$this->assertResponseOk();
$result = json_decode((string)$this->_response->getBody());
- $this->assertCount(3, $result->themes);
- $this->assertEquals('BcColumn', $result->themes[0]->name);
- $this->assertEquals('BcThemeSample', $result->themes[1]->name);
+ $themeNames = array_map(fn($theme) => $theme->name, (array) $result->themes);
+ $this->assertContains('BcColumn', $themeNames);
+ $this->assertContains('BcThemeSample', $themeNames);
+ $this->assertContains('BcFront', $themeNames);
}
/**
diff --git a/plugins/baser-core/tests/TestCase/Controller/UploadsControllerTest.php b/plugins/baser-core/tests/TestCase/Controller/UploadsControllerTest.php
index 5e5782f645..096351fced 100644
--- a/plugins/baser-core/tests/TestCase/Controller/UploadsControllerTest.php
+++ b/plugins/baser-core/tests/TestCase/Controller/UploadsControllerTest.php
@@ -48,7 +48,9 @@ public function tearDown(): void
*/
public function testTmp()
{
- mkdir(TMP . 'uploads');
+ if (!is_dir(TMP . 'uploads')) {
+ mkdir(TMP . 'uploads');
+ }
touch(TMP . 'uploads/test.gif');
copy(ROOT . '/plugins/bc-admin-third/webroot/img/baser.power.gif', TMP . 'uploads/test.gif');
diff --git a/plugins/baser-core/tests/TestCase/Model/Behavior/BcContentsBehaviorTest.php b/plugins/baser-core/tests/TestCase/Model/Behavior/BcContentsBehaviorTest.php
index 6d719741c5..237e277176 100644
--- a/plugins/baser-core/tests/TestCase/Model/Behavior/BcContentsBehaviorTest.php
+++ b/plugins/baser-core/tests/TestCase/Model/Behavior/BcContentsBehaviorTest.php
@@ -256,4 +256,74 @@ public function testGetType()
$this->assertEquals('BaserContent', $rs);
}
+ /**
+ * test afterMarshal rollbacks content file on error
+ * メインエンティティにエラーがある場合、Contentのeyecatch_tmpが保持される
+ */
+ public function testAfterMarshalRollbacksContentFileOnError()
+ {
+ $this->loadFixtureScenario(ContentFoldersScenario::class);
+ $this->loadFixtureScenario(ContentsScenario::class);
+
+ // ContentアソシエーションつきでContentFolderを取得
+ $contentFolder = $this->table->find()->contain('Contents')->first();
+ $this->assertNotEmpty($contentFolder->content, 'Contentが取得できていません');
+
+ // content の eyecatch_tmp を設定(セッション画像の参照)
+ $contentFolder->content->set('eyecatch_tmp', 'session_content_eyecatch.jpg');
+
+ // メインエンティティに直接エラーを設定(validate=falseでバリデーション処理をスキップ)
+ $contentFolder->setError('folder_template', ['バリデーションエラー']);
+
+ $result = $this->table->dispatchEvent('Model.afterMarshal', [
+ 'entity' => $contentFolder,
+ 'data' => new ArrayObject($contentFolder->toArray()),
+ 'options' => new ArrayObject(['validate' => false]),
+ ]);
+
+ $updatedEntity = $result->getData('entity');
+
+ // バリデーションエラー時、content.eyecatch_tmpが保持されていることを確認
+ $this->assertEquals(
+ 'session_content_eyecatch.jpg',
+ $updatedEntity->content->get('eyecatch_tmp'),
+ 'バリデーションエラー時にcontent.eyecatch_tmpが保持されるべきです'
+ );
+ }
+
+ /**
+ * test afterMarshal does not rollback content file on success
+ * メインエンティティにエラーがない場合、rollbackFileが呼ばれずeyecatchは変更されたまま
+ */
+ public function testAfterMarshalNoRollbackContentFileOnSuccess()
+ {
+ $this->loadFixtureScenario(ContentFoldersScenario::class);
+ $this->loadFixtureScenario(ContentsScenario::class);
+
+ // ContentアソシエーションつきでContentFolderを取得
+ $contentFolder = $this->table->find()->contain('Contents')->first();
+ $this->assertNotEmpty($contentFolder->content, 'Contentが取得できていません');
+
+ // content の eyecatch を変更(dirty状態)
+ $contentFolder->content->set('eyecatch', 'new_eyecatch.jpg');
+
+ // メインエンティティにエラーなし
+ $this->assertFalse($contentFolder->hasErrors());
+
+ $result = $this->table->dispatchEvent('Model.afterMarshal', [
+ 'entity' => $contentFolder,
+ 'data' => new ArrayObject($contentFolder->toArray()),
+ 'options' => new ArrayObject(['validate' => false]),
+ ]);
+
+ $updatedEntity = $result->getData('entity');
+
+ // エラーなしの場合はrollbackFileが呼ばれないため、eyecatchが変更されたままであることを確認
+ $this->assertEquals(
+ 'new_eyecatch.jpg',
+ $updatedEntity->content->get('eyecatch'),
+ 'エラーなしの場合はrollbackFileが呼ばれず、eyecatchが変更されたままであるべきです'
+ );
+ }
+
}
diff --git a/plugins/baser-core/tests/TestCase/Model/Behavior/BcUploadBehaviorTest.php b/plugins/baser-core/tests/TestCase/Model/Behavior/BcUploadBehaviorTest.php
index 91e7dc64c5..79163a9cf4 100644
--- a/plugins/baser-core/tests/TestCase/Model/Behavior/BcUploadBehaviorTest.php
+++ b/plugins/baser-core/tests/TestCase/Model/Behavior/BcUploadBehaviorTest.php
@@ -169,7 +169,14 @@ public function testAfterSave()
$this->table->dispatchEvent('Model.afterSave', ['entity' => $entity, 'options' => new ArrayObject()]);
$this->assertFileExists($this->savePath . 'baser.power.gif');
// 削除の場合
- $this->table->dispatchEvent('Model.beforeMarshal', ['data' => new ArrayObject(['id' => 6, 'eyecatch_delete' => true]), 'options' => new ArrayObject()]);
+ $deleteData = new ArrayObject(['id' => 6, 'eyecatch_delete' => true]);
+ $this->table->dispatchEvent('Model.beforeMarshal', ['data' => $deleteData, 'options' => new ArrayObject()]);
+ $entity->set('_bc_upload_id', $deleteData['_bc_upload_id']);
+ $this->BcUploadBehavior->oldEntity[$this->table->getAlias()][$deleteData['_bc_upload_id']] = new Entity([
+ 'id' => 6,
+ 'eyecatch' => 'baser.power.gif',
+ '_bc_upload_id' => $deleteData['_bc_upload_id']
+ ]);
$return = $this->table->dispatchEvent('Model.afterSave', ['entity' => $entity, 'options' => new ArrayObject()]);
$this->assertEquals('', $return->getData('entity')->eyecatch);
$this->assertFileDoesNotExist($this->savePath . 'baser.power.gif');
@@ -298,4 +305,91 @@ public static function renameToBasenameFieldsDataProvider(): array
];
}
+ /**
+ * test afterMarshal calls rollbackFile
+ * afterMarshalイベントでrollbackFileが呼ばれる
+ */
+ public function testAfterMarshalCallsRollbackFile()
+ {
+ $this->loadFixtureScenario(ContentsScenario::class);
+ $table = $this->table;
+
+ // アップロードファイルを準備
+ $tmpFile = '/tmp/test_upload_' . time() . '.jpg';
+ file_put_contents($tmpFile, 'test image data');
+
+ $uploadedFiles = normalizeUploadedFiles([
+ 'eyecatch' => [
+ 'name' => 'test.jpg',
+ 'tmp_name' => $tmpFile,
+ 'type' => 'image/jpeg',
+ 'size' => 100,
+ 'error' => 0
+ ]
+ ]);
+
+ $data = [
+ 'id' => 1,
+ 'name' => 'test',
+ 'eyecatch' => $uploadedFiles['eyecatch']
+ ];
+
+ $entity = $table->newEntity($data);
+ $entity->setError('name', ['name field error']); // 他フィールドでバリデーションエラー
+
+ // eyecatch_tmpが設定されていることを確認(rollbackFileが実行された証拠)
+ $this->assertNotEmpty($entity->get('eyecatch_tmp'), 'rollbackFileが実行されていません');
+
+ @unlink($tmpFile);
+ }
+
+ /**
+ * test beforeMarshal adds accessible fields
+ * beforeMarshalで_tmpと_deleteがaccessibleFieldsに追加される
+ */
+ public function testBeforeMarshalAddsAccessibleFields()
+ {
+ $this->loadFixtureScenario(ContentsScenario::class);
+ $table = $this->table;
+
+ $data = [
+ 'id' => 1,
+ 'name' => 'test',
+ 'eyecatch_tmp' => 'tmp_file.jpg',
+ 'eyecatch_delete' => '1'
+ ];
+
+ $entity = $table->newEntity($data);
+
+ // _tmpと_deleteフィールドがアクセス可能になっていることを確認
+ $this->assertTrue($entity->isAccessible('eyecatch_tmp'), 'eyecatch_tmpがaccessibleではありません');
+ $this->assertTrue($entity->isAccessible('eyecatch_delete'), 'eyecatch_deleteがaccessibleではありません');
+
+ // 値が設定されていることを確認
+ $this->assertEquals('tmp_file.jpg', $entity->get('eyecatch_tmp'));
+ $this->assertEquals('1', $entity->get('eyecatch_delete'));
+ }
+
+ /**
+ * test afterMarshal does not rollback on success
+ * バリデーションエラーがない場合はrollbackFileを呼ばない(eyecatch_tmpが設定されない)
+ */
+ public function testAfterMarshalNoRollbackOnSuccess()
+ {
+ $this->loadFixtureScenario(ContentsScenario::class);
+
+ // エラーなしのエンティティを直接作成(バリデーションをスキップ)
+ $entity = new Entity(['id' => 1, 'name' => 'valid_name', '_bc_upload_id' => 'test_no_err_id']);
+ $this->assertFalse($entity->hasErrors(), 'エラーなしであるべきです');
+
+ // afterMarshal を手動でディスパッチ
+ $result = $this->table->dispatchEvent('Model.afterMarshal', [
+ 'entity' => $entity,
+ 'options' => new ArrayObject(),
+ ]);
+ $updatedEntity = $result->getData('entity');
+
+ // エラーなしの場合はrollbackFileが呼ばれないため、eyecatch_tmpが設定されていないことを確認
+ $this->assertEmpty($updatedEntity->get('eyecatch_tmp'), 'エラーなしの場合はeyecatch_tmpが設定されるべきではありません');
+ }
}
diff --git a/plugins/baser-core/tests/TestCase/Utility/BcFileUploaderTest.php b/plugins/baser-core/tests/TestCase/Utility/BcFileUploaderTest.php
index fc9d8c2a11..0ce8bff16a 100644
--- a/plugins/baser-core/tests/TestCase/Utility/BcFileUploaderTest.php
+++ b/plugins/baser-core/tests/TestCase/Utility/BcFileUploaderTest.php
@@ -267,7 +267,8 @@ public function testSaveTmpFiles()
];
$entity = $this->BcFileUploader->saveTmpFiles($data, 1);
$tmpId = $this->BcFileUploader->tmpId;
- $this->assertEquals("00000001_eyecatch.png", $entity->eyecatch_tmp, 'saveTmpFiles()の返り値が正しくありません');
+ // タイムスタンプ付きファイル名を許容
+ $this->assertMatchesRegularExpression('/^00000001_eyecatch(_[0-9]+)?\\.png$/', $entity->eyecatch_tmp, 'saveTmpFiles()の返り値が正しくありません');
$this->assertEquals(1, $tmpId, 'tmpIdが正しく設定されていません');
//不要なフォルダを削除
(new BcFolder($filePath))->delete();
@@ -294,8 +295,11 @@ public function testSaveTmpFile()
$entity = $this->table->patchEntity($this->table->newEmptyEntity(), $data);
$this->BcFileUploader->tmpId = 1;
- $this->BcFileUploader->saveTmpFile($this->BcFileUploader->settings['fields']['eyecatch'], $data, $entity);
- $this->assertNotEmpty($_SESSION['Upload']['00000001_eyecatch_png']);
+ $fileName = $this->BcFileUploader->saveTmpFile($this->BcFileUploader->settings['fields']['eyecatch'], $data, $entity);
+ // タイムスタンプ付きファイル名を許容
+ $this->assertNotEmpty($fileName);
+ $sessionKey = str_replace(['.', '/'], ['_', '_'], $fileName);
+ $this->assertNotEmpty($_SESSION['Upload'][$sessionKey]);
//不要なフォルダを削除
(new BcFolder($filePath))->delete();
}
@@ -320,9 +324,10 @@ public function testGetSaveTmpFileName()
$entity = $this->table->patchEntity($this->table->newEmptyEntity(), $data);
$this->BcFileUploader->tmpId = 1;
$file = $this->BcFileUploader->getSaveTmpFileName($this->BcFileUploader->settings['fields']['eyecatch'], $data, $entity);
- $this->assertEquals('00000001_eyecatch.png', $file);
- $file = $this->BcFileUploader->getSaveTmpFileName(['name' => 'eyecatch'], $data, $entity);
- $this->assertEquals('1_eyecatch.png', $file);
+ // タイムスタンプ付きファイル名を許容
+ $this->assertMatchesRegularExpression('/^00000001_eyecatch(_[0-9]+)?\\.png$/', $file);
+ $file2 = $this->BcFileUploader->getSaveTmpFileName(['name' => 'eyecatch'], $data, $entity);
+ $this->assertMatchesRegularExpression('/^1_eyecatch(_[0-9]+)?\\.png$/', $file2);
//不要なフォルダを削除
(new BcFolder($filePath))->delete();
}
@@ -1373,4 +1378,285 @@ public static function rollbackFileNoErrorsDataProvider()
],
];
}
+
+ /**
+ * test rollbackFile with session save
+ * バリデーションエラー時に新規アップロードファイルがセッションに保存される
+ */
+ public function testRollbackFileWithSessionSave()
+ {
+ $session = $this->BcFileUploader->Session;
+
+ // アップロードファイルを準備(/tmp/で始まるパス)
+ $tmpFile = '/tmp/test_upload_' . time() . '.jpg';
+ file_put_contents($tmpFile, 'test image data');
+
+ $uploadingFiles = [
+ 'eyecatch' => [
+ 'name' => 'test.jpg',
+ 'tmp_name' => $tmpFile,
+ 'type' => 'image/jpeg',
+ 'size' => 100,
+ 'error' => 0,
+ 'ext' => 'jpg'
+ ]
+ ];
+
+ $this->BcFileUploader->settings['fields'] = [
+ ['name' => 'eyecatch', 'type' => 'image', 'namefield' => 'id']
+ ];
+
+ $entity = new Entity(['id' => 1, 'eyecatch' => 'old_image.jpg', '_bc_upload_id' => 'test123']);
+ $entity->clean();
+ $entity->set('eyecatch', 'new_image.jpg');
+ $entity->setError('eyecatch', ['validation error']);
+
+ $this->BcFileUploader->setUploadingFiles($uploadingFiles, 'test123');
+ $this->BcFileUploader->rollbackFile($entity);
+
+ // _tmp値が設定されていることを確認
+ $this->assertNotEmpty($entity->get('eyecatch_tmp'), '_tmp値が設定されていません');
+
+ // セッションに保存されていることを確認
+ $sessionKey = str_replace(['.', '/'], ['_', '_'], $entity->get('eyecatch_tmp'));
+ $sessionData = $session->read('Upload.' . $sessionKey . '.data');
+ $this->assertNotEmpty($sessionData, 'セッションにファイルデータが保存されていません');
+
+ // 元の値にロールバックされていることを確認
+ $this->assertEquals('old_image.jpg', $entity->get('eyecatch'), '元の値にロールバックされていません');
+
+ // エラーが保持されていることを確認
+ $this->assertNotEmpty($entity->getError('eyecatch'), 'エラーが保持されていません');
+
+ @unlink($tmpFile);
+ }
+
+ /**
+ * test rollbackFile preserves _tmp value
+ * ファイルなし時に既存の_tmp値を保持する
+ */
+ public function testRollbackFilePreservesTmpValue()
+ {
+ $this->BcFileUploader->settings['fields'] = [
+ ['name' => 'eyecatch']
+ ];
+
+ $entity = new Entity([
+ 'id' => 1,
+ 'eyecatch' => 'old_image.jpg',
+ 'eyecatch_tmp' => 'existing_tmp_file.jpg',
+ '_bc_upload_id' => 'test123'
+ ]);
+ $entity->clean();
+ $entity->set('eyecatch', 'old_image.jpg'); // ファイルアップロードなし
+ $entity->setError('eyecatch', ['validation error']);
+
+ // アップロードファイルなし
+ $this->BcFileUploader->setUploadingFiles([], 'test123');
+ $this->BcFileUploader->rollbackFile($entity);
+
+ // 既存の_tmp値が保持されていることを確認
+ $this->assertEquals('existing_tmp_file.jpg', $entity->get('eyecatch_tmp'), '既存の_tmp値が保持されていません');
+
+ // 元の値が保持されていることを確認
+ $this->assertEquals('old_image.jpg', $entity->get('eyecatch'), '元の値が保持されていません');
+ }
+
+ /**
+ * test getSaveTmpFileName with timestamp
+ * タイムスタンプ付きファイル名でユニーク化される
+ */
+ public function testGetSaveTmpFileNameWithTimestamp()
+ {
+ $setting = [
+ 'name' => 'eyecatch',
+ 'namefield' => 'id',
+ 'nameformat' => '%08d'
+ ];
+
+ $file = [
+ 'name' => 'test.jpg',
+ 'ext' => 'jpg'
+ ];
+
+ $entity = new Entity(['id' => 1]);
+
+ $fileName1 = $this->BcFileUploader->getSaveTmpFileName($setting, $file, $entity);
+
+ // タイムスタンプが含まれていることを確認
+ $this->assertMatchesRegularExpression('/_\d+\.jpg$/', $fileName1, 'タイムスタンプが含まれていません');
+
+ // ファイル名の形式を確認 (00000001_eyecatch_timestamp.jpg)
+ $this->assertStringContainsString('00000001_eyecatch', $fileName1, 'nameフィールドが正しく生成されていません');
+
+ // 2回生成すると異なるタイムスタンプになることを確認(ユニーク性)
+ sleep(1);
+ $fileName2 = $this->BcFileUploader->getSaveTmpFileName($setting, $file, $entity);
+ $this->assertNotEquals($fileName1, $fileName2, 'タイムスタンプによるユニーク化が機能していません');
+ }
+
+ /**
+ * test setupTmpData restores from session
+ * _tmp値があり新規ファイルなしの場合、セッションからファイルを復元する
+ */
+ public function testSetupTmpDataRestoresFromSession()
+ {
+ // セッションにファイルデータを設定
+ $tmpFileName = 'eyecatch_restore_test.jpg';
+ $sessionKey = str_replace(['.', '/'], ['_', '_'], $tmpFileName);
+ $testContent = 'fake image content for restore test';
+ $this->BcFileUploader->Session->write('Upload.' . $sessionKey . '.data', base64_encode($testContent));
+ $this->BcFileUploader->Session->write('Upload.' . $sessionKey . '.type', 'image/jpeg');
+
+ // ArrayObjectとして渡す(実際のbeforeMarshalと同じ形式)
+ $data = new \ArrayObject([
+ '_bc_upload_id' => 'test_restore_id',
+ 'eyecatch_tmp' => $tmpFileName,
+ // eyecatch フィールドなし(新規ファイルなし)
+ ]);
+
+ $this->BcFileUploader->setupTmpData($data);
+
+ // セッションから復元されてuploadingFilesに設定されていることを確認
+ $uploadingFiles = $this->BcFileUploader->getUploadingFiles('test_restore_id');
+ $this->assertNotEmpty($uploadingFiles['eyecatch'], 'セッションからファイルが復元されていません');
+ $this->assertEquals($tmpFileName, $uploadingFiles['eyecatch']['name'], '復元されたファイル名が正しくありません');
+
+ // 後片付け
+ @unlink($this->savePath . $sessionKey);
+ }
+
+ /**
+ * test setupTmpData skips session when new file exists
+ * _tmp値があっても /tmp/ から始まる新規ファイルがある場合はセッション復元をスキップする
+ */
+ public function testSetupTmpDataSkipsSessionWhenNewFileExists()
+ {
+ // セッションにファイルデータを設定
+ $tmpFileName = 'eyecatch_skip_test.jpg';
+ $sessionKey = str_replace(['.', '/'], ['_', '_'], $tmpFileName);
+ $this->BcFileUploader->Session->write('Upload.' . $sessionKey . '.data', base64_encode('session image data'));
+ $this->BcFileUploader->Session->write('Upload.' . $sessionKey . '.type', 'image/jpeg');
+
+ // 新規ファイルが /tmp/ から始まる(新規アップロード)
+ $data = new \ArrayObject([
+ '_bc_upload_id' => 'test_skip_id',
+ 'eyecatch_tmp' => $tmpFileName,
+ 'eyecatch' => [
+ 'name' => 'new_file.jpg',
+ 'tmp_name' => '/tmp/php_upload_new_file_test',
+ 'type' => 'image/jpeg',
+ 'size' => 100,
+ 'error' => 0,
+ ],
+ ]);
+
+ $this->BcFileUploader->setupTmpData($data);
+
+ // 新規ファイルがあるのでセッションから復元されていないことを確認
+ $uploadingFiles = $this->BcFileUploader->getUploadingFiles('test_skip_id');
+ $this->assertEmpty($uploadingFiles['eyecatch'] ?? null, '新規ファイルがある場合はセッションから復元すべきではありません');
+
+ @unlink($this->savePath . $sessionKey);
+ }
+
+ /**
+ * test saveTmpFiles preserves existing tmp session without new upload
+ * file_tmp だけの再送時に既存の Upload セッションを消さない
+ */
+ public function testSaveTmpFilesPreservesExistingTmpSessionWithoutNewUpload()
+ {
+ $tmpFileName = 'eyecatch_keep_session.jpg';
+ $sessionKey = str_replace(['.', '/'], ['_', '_'], $tmpFileName);
+ $testContent = 'fake image content for keep session test';
+ $this->BcFileUploader->Session->write('Upload.' . $sessionKey . '.data', base64_encode($testContent));
+ $this->BcFileUploader->Session->write('Upload.' . $sessionKey . '.type', 'image/jpeg');
+
+ $entity = $this->BcFileUploader->saveTmpFiles([
+ 'eyecatch_tmp' => $tmpFileName
+ ], 1);
+
+ $this->assertEquals($tmpFileName, $entity->get('eyecatch_tmp'));
+ $this->assertEquals(base64_encode($testContent), $this->BcFileUploader->Session->read('Upload.' . $sessionKey . '.data'));
+ $this->assertEquals('image/jpeg', $this->BcFileUploader->Session->read('Upload.' . $sessionKey . '.type'));
+ }
+
+ /**
+ * test moveFileSessionToTmp returns false when session data is null
+ * セッションデータがnullの場合はfalseを返す(追加されたnullチェック)
+ */
+ public function testMoveFileSessionToTmpWithNullSession()
+ {
+ // セッションに何も書き込まない → null
+ $tmpFileName = 'nonexistent_session_file.jpg';
+ $data = [
+ '_bc_upload_id' => 'test_null_id',
+ 'eyecatch_tmp' => $tmpFileName,
+ ];
+
+ $result = $this->BcFileUploader->moveFileSessionToTmp($data, 'eyecatch');
+
+ $this->assertFalse($result, 'セッションデータがnullの場合はfalseを返すべきです');
+ }
+
+ /**
+ * test rollbackFile with force=true
+ * $force=trueの場合、バリデーションエラーがなくても強制的にロールバックを実行する
+ */
+ public function testRollbackFileWithForce()
+ {
+ $BcFileUploader = new BcFileUploader();
+ $BcFileUploader->settings['fields'] = [
+ ['name' => 'image'],
+ ];
+ $BcFileUploader->setUploadingFiles([], 'test_force_id');
+
+ // エラーなしのエンティティ
+ $entity = new Entity(['image' => 'original_image.jpg', '_bc_upload_id' => 'test_force_id']);
+ $entity->clean();
+ $entity->set('image', 'new_image.jpg');
+
+ $this->assertFalse($entity->hasErrors());
+
+ // $force=false の場合: エラーなしなのでスキップ
+ $BcFileUploader->rollbackFile($entity, false);
+ $this->assertEquals('new_image.jpg', $entity->get('image'), '$force=falseでエラーなしの場合はロールバックしないべきです');
+
+ // $force=true の場合: エラーなしでも強制実行
+ $BcFileUploader->rollbackFile($entity, true);
+ $this->assertEquals('original_image.jpg', $entity->get('image'), '$force=trueでエラーなしでも元の値に戻るべきです');
+ }
+
+ /**
+ * test rollbackFile original tmp fallback
+ * entity->get('field_tmp')がnullのとき、getOriginal()のフォールバックが機能する
+ */
+ public function testRollbackFileOriginalTmpFallback()
+ {
+ $BcFileUploader = new BcFileUploader();
+ $BcFileUploader->settings['fields'] = [
+ ['name' => 'eyecatch'],
+ ];
+ $BcFileUploader->setUploadingFiles([], 'test_fallback_id');
+
+ // getOriginal()のみに_tmp値がある状態を再現:
+ // 1. eyecatch_tmpをoriginalに設定 (cleanにより元の値になる)
+ // 2. eyecatch_tmpをnullにdirtyにする (get()はnullを返す)
+ $entity = new Entity([
+ 'eyecatch' => 'old.jpg',
+ 'eyecatch_tmp' => 'original_tmp.jpg',
+ '_bc_upload_id' => 'test_fallback_id',
+ ]);
+ $entity->clean();
+ $entity->set('eyecatch_tmp', null); // get()はnull、getOriginal()は'original_tmp.jpg'
+ $entity->setError('eyecatch', ['validation error']);
+
+ $this->assertNull($entity->get('eyecatch_tmp'));
+ $this->assertEquals('original_tmp.jpg', $entity->getOriginal('eyecatch_tmp'));
+
+ $BcFileUploader->rollbackFile($entity);
+
+ // getOriginal()フォールバックで_tmp値が設定されていることを確認
+ $this->assertEquals('original_tmp.jpg', $entity->get('eyecatch_tmp'), 'getOriginal()フォールバックで_tmp値が設定されるべきです');
+ }
}
diff --git a/plugins/baser-core/tests/TestCase/View/Helper/BcBaserHelperTest.php b/plugins/baser-core/tests/TestCase/View/Helper/BcBaserHelperTest.php
index cb3b05d805..1eaaa374a9 100644
--- a/plugins/baser-core/tests/TestCase/View/Helper/BcBaserHelperTest.php
+++ b/plugins/baser-core/tests/TestCase/View/Helper/BcBaserHelperTest.php
@@ -1113,7 +1113,10 @@ public function testContent()
*/
public function testScripts()
{
- $themeConfigTag = '';
+ $themeConfigTags = [
+ '',
+ ''
+ ];
// CSS
$expected = '
@@ -1124,6 +1127,7 @@ public function testScripts()
$this->BcBaser->css('admin/layout', false);
$this->BcBaser->scripts();
$result = ob_get_clean();
+ $result = str_replace($themeConfigTags, '', $result);
$this->assertEquals($expected, $result);
$view = $this->BcBaser->getView();
@@ -1140,7 +1144,7 @@ public function testScripts()
ob_start();
$this->BcBaser->scripts();
$result = ob_get_clean();
- $result = str_replace($themeConfigTag, '', $result);
+ $result = str_replace($themeConfigTags, '', $result);
$this->assertEquals($expected, $result);
$view->assign('script', '');
diff --git a/plugins/baser-core/tests/TestCase/View/Helper/BcFormHelperTest.php b/plugins/baser-core/tests/TestCase/View/Helper/BcFormHelperTest.php
index a9569ce4a2..1fd40a141c 100644
--- a/plugins/baser-core/tests/TestCase/View/Helper/BcFormHelperTest.php
+++ b/plugins/baser-core/tests/TestCase/View/Helper/BcFormHelperTest.php
@@ -808,4 +808,82 @@ public function test_control()
$this->assertStringNotContainsString('