# 機能設計書 30-テーマ管理

## 概要

本ドキュメントは、Ghostのテーマ管理機能の設計仕様を記述する。テーマ管理機能は、サイトの外観を定義するテーマのアップロード、有効化、削除、およびバリデーションを行う。

### 本機能の処理概要

**業務上の目的・背景**：Ghostはテーマベースのデザインシステムを採用しており、パブリッシャーは独自のテーマをアップロードしてサイトの外観をカスタマイズできる。テーマ管理機能は、テーマのライフサイクル全体を管理し、互換性チェックによりサイトの安定性を維持する。

**機能の利用シーン**：
- 新しいテーマのアップロード（ZIPファイル）
- GitHubからのテーマインストール
- アクティブテーマの切り替え
- 不要なテーマの削除
- テーマのエラー・警告の確認
- テーマのダウンロード（ZIPエクスポート）

**主要な処理内容**：
1. テーマファイルの読み込みとパース
2. gscan によるテーマバリデーション
3. テーマの有効化（アクティベーション）
4. テーマストレージへの保存・削除
5. テーマリストの管理

**関連システム・外部連携**：
- gscan（テーマバリデーションライブラリ）
- GitHub API（テーマインストール）
- ファイルシステム（テーマストレージ）

**権限による制御**：管理者のみがテーマ管理操作を実行可能。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| - | デザイン設定画面 | 主機能 | テーマ一覧・アップロード・有効化 |

## 機能種別

ファイル管理 / バリデーション / 設定管理

## 入力仕様

### 入力パラメータ（テーマアップロード）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| zip | File | Yes | テーマZIPファイル | 有効なZIP形式 |
| name | string | - | ZIPファイル名から抽出 | 'casper.zip', 'source.zip' は禁止 |

### 入力パラメータ（テーマ有効化）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| themeName | string | Yes | 有効化するテーマ名 | テーマリストに存在すること |

### 入力パラメータ（テーマ削除）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| themeName | string | Yes | 削除するテーマ名 | 'casper', 'source' 以外、アクティブでないこと |

### 入力データソース

- **ファイルシステム**: content/themes/ ディレクトリ
- **settings テーブル**: active_theme 設定

## 出力仕様

### 出力データ（テーマ情報）

| 項目名 | 型 | 説明 |
|--------|-----|------|
| name | string | テーマ名 |
| package | object | package.json の内容 |
| active | boolean | アクティブ状態 |
| errors | array | 致命的エラーのリスト |
| warnings | array | 警告のリスト |

### 出力先

- **ファイルシステム**: content/themes/{themeName}/
- **settings テーブル**: active_theme の更新
- **Admin API レスポンス**: テーマ情報

## 処理フロー

### 処理シーケンス（テーマアップロード）

```
1. ZIPファイル受信
   └─ ファイル名からテーマ名を抽出
   └─ デフォルトテーマ名（casper, source）の場合はエラー

2. gscan バリデーション
   └─ gscan.checkZip() でテーマを検証
   └─ 致命的エラーがある場合は ThemeValidationError

3. 既存テーマのバックアップ
   └─ 同名テーマが存在する場合、一時的にリネーム

4. テーマ保存
   └─ ThemeStorage.save() で展開・保存

5. テーマ読み込み
   └─ themeLoader.loadOneTheme() でテーマリストに追加

6. アクティブテーマの場合
   └─ activator.activateFromAPIOverride() で再有効化

7. バックアップ削除
   └─ 正常完了時にバックアップを削除
```

### フローチャート

```mermaid
flowchart TD
    A[ZIPアップロード] --> B{デフォルトテーマ?}
    B -->|Yes| C[ValidationError: 上書き禁止]
    B -->|No| D[gscan.checkZip]
    D --> E{致命的エラー?}
    E -->|Yes| F[ThemeValidationError]
    E -->|No| G{既存テーマ?}
    G -->|Yes| H[バックアップ作成]
    G -->|No| I[テーマ保存]
    H --> I
    I --> J[themeLoader.loadOneTheme]
    J --> K{アクティブテーマ?}
    K -->|Yes| L[activateFromAPIOverride]
    K -->|No| M[完了]
    L --> M
    M --> N[バックアップ削除]

    subgraph テーマ有効化
    O[有効化リクエスト] --> P{テーマ存在?}
    P -->|No| Q[ValidationError]
    P -->|Yes| R[gscan.checkSafe]
    R --> S{致命的エラー?}
    S -->|Yes| T[ThemeValidationError]
    S -->|No| U[activateFromAPI]
    U --> V[settings.active_theme 更新]
    V --> W[完了]
    end

    subgraph テーマ削除
    X[削除リクエスト] --> Y{デフォルトテーマ?}
    Y -->|Yes| Z[ValidationError: 削除禁止]
    Y -->|No| AA{アクティブテーマ?}
    AA -->|Yes| AB[ValidationError: 削除禁止]
    AA -->|No| AC{テーマ存在?}
    AC -->|No| AD[NotFoundError]
    AC -->|Yes| AE[ThemeStorage.delete]
    AE --> AF[テーマリストから削除]
    AF --> AG[完了]
    end
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-30-01 | デフォルトテーマ保護 | casper, source は上書き・削除禁止 | アップロード・削除時 |
| BR-30-02 | アクティブテーマ削除禁止 | 現在有効なテーマは削除不可 | 削除時 |
| BR-30-03 | 致命的エラー禁止 | 致命的エラーのあるテーマは有効化不可 | 有効化時 |
| BR-30-04 | バリデーションキャッシュ | バリデーション結果をキャッシュ | gscan 実行後 |
| BR-30-05 | 本番環境警告非表示 | 本番環境では警告は表示しない | バリデーション時 |
| BR-30-06 | 起動時チェックスキップ | optimization:themes:skipBootChecks で起動時チェックをスキップ可能 | 起動時 |

### 計算ロジック

**canActivate 判定**：
```javascript
canActivate(checkedTheme) {
    return !checkedTheme.results.hasFatalErrors;
}
```

## データベース操作仕様

### 操作別データベース影響一覧

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| テーマ有効化 | settings | UPDATE | active_theme の更新 |
| 設定取得 | settings | SELECT | 現在のアクティブテーマ取得 |

### テーブル別操作詳細

#### settings

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | value | key = 'active_theme' | 現在のアクティブテーマ名 |
| UPDATE | value | 新しいテーマ名 | settingsCache 経由で更新 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | ValidationError | デフォルトテーマの上書き試行 | 'Please rename your zip' |
| - | ValidationError | デフォルトテーマの削除試行 | 'Deleting the default theme is not allowed.' |
| - | ValidationError | アクティブテーマの削除試行 | 'Deleting the active theme is not allowed.' |
| - | ValidationError | テーマがリストに存在しない | 'Theme does not exist.' |
| - | ThemeValidationError | 致命的エラーのあるテーマ有効化 | エラー詳細を返却 |
| - | NotFoundError | 起動時にアクティブテーマが見つからない | ログ出力、管理画面は動作 |

### リトライ仕様

- アップロード失敗時はバックアップを復元

## トランザクション仕様

- ファイル操作のため、トランザクションは使用しない
- バックアップ・復元による擬似ロールバック

## パフォーマンス要件

- テーマ読み込み: 起動時に一括読み込み
- gscan バリデーション: キャッシュ使用で高速化
- skipBootChecks オプションで起動時チェックをスキップ可能

## セキュリティ考慮事項

- ZIPファイルの展開先はサニタイズ済みのパス
- 管理者権限によるアクセス制御
- テーマ内の実行可能ファイルは gscan でチェック

## 備考

- gscan は Ghost のテーマ仕様準拠をチェックするライブラリ
- checkedVersion は Ghost のメジャーバージョン（例: 'v5'）
- labs フラグによりテーマ機能の追加チェックが可能

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: エントリーポイントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | index.js | `ghost/core/core/server/services/themes/index.js` | テーマサービスのエントリーポイント |

**読解のコツ**: index.js は init(), loadInactiveThemes(), api オブジェクトをエクスポート。api には getJSON, activate, getZip, setFromZip, installFromGithub, destroy が含まれる。

#### Step 2: テーマ読み込みを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | loader.js | `ghost/core/core/server/services/themes/loader.js` | テーマ読み込み処理 |

**主要処理フロー**:
- **7-11行目**: loadAllThemes() - 全テーマの読み込み
- **13-17行目**: loadOneTheme() - 単一テーマの読み込み

#### Step 3: バリデーションを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | validate.js | `ghost/core/core/server/services/themes/validate.js` | gscan によるバリデーション |

**主要処理フロー**:
- **32-34行目**: init() - gscanCacheStore の初期化
- **36-38行目**: canActivate() - 有効化可能判定
- **47-93行目**: check() - 通常のバリデーション
- **124-142行目**: checkSafe() - 安全なバリデーション（致命的エラー時は例外）
- **100-122行目**: getThemeErrors() - キャッシュ済みエラーの取得

#### Step 4: 有効化処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | activate.js | `ghost/core/core/server/services/themes/activate.js` | テーマ有効化処理 |

**主要処理フロー**:
- **17-44行目**: loadAndActivate() - 起動時の読み込みと有効化
- **46-62行目**: activate() - API経由の有効化

#### Step 5: ストレージ操作を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | storage.js | `ghost/core/core/server/services/themes/storage.js` | テーマストレージ操作 |

**主要処理フロー**:
- **35-47行目**: getZip() - テーマのZIPエクスポート
- **48-128行目**: setFromZip() - ZIPからのテーマインストール
- **129-153行目**: destroy() - テーマ削除

### プログラム呼び出し階層図

```
themes.api
    │
    ├─ setFromZip(zip)
    │      │
    │      ├─ getStorage().getSanitizedFileName()
    │      │
    │      ├─ validate.checkSafe(themeName, zip, true)
    │      │      │
    │      │      ├─ gscan.checkZip()
    │      │      │
    │      │      ├─ gscan.format()
    │      │      │
    │      │      └─ canActivate() チェック
    │      │
    │      ├─ getStorage().exists(themeName)
    │      │
    │      ├─ getStorage().rename(themeName, backupName) // 既存時
    │      │
    │      ├─ getStorage().save({name, path})
    │      │
    │      ├─ themeLoader.loadOneTheme(themeName)
    │      │
    │      ├─ (アクティブ時) activator.activateFromAPIOverride()
    │      │
    │      └─ toJSON() → レスポンス生成
    │
    ├─ activate(themeName)
    │      │
    │      ├─ list.get(themeName)
    │      │
    │      ├─ validate.checkSafe(themeName, loadedTheme)
    │      │
    │      ├─ activator.activateFromAPI()
    │      │
    │      └─ validate.getErrorsFromCheckedTheme()
    │
    └─ destroy(themeName)
           │
           ├─ デフォルトテーマチェック ('casper', 'source')
           │
           ├─ アクティブテーマチェック
           │
           ├─ list.get(themeName)
           │
           ├─ getStorage().delete(themeName)
           │
           └─ list.del(themeName)

themes.init()
    │
    ├─ validate.init()
    │
    ├─ settingsCache.get('active_theme')
    │
    └─ activate.loadAndActivate(themeName, {skipChecks})
           │
           ├─ themeLoader.loadOneTheme(themeName)
           │
           ├─ validate.check(themeName, loadedTheme)
           │
           └─ activator.activateFromBoot()
```

### データフロー図

```
[入力]                     [処理]                          [出力]

ZIPファイル ─────────────▶ setFromZip()
                                 │
                                 ├─ getSanitizedFileName()
                                 │
                                 ├─ gscan.checkZip() ───────▶ checkedTheme
                                 │      │
                                 │      ├─ errors[]
                                 │      └─ warnings[]
                                 │
                                 ├─ canActivate() ──────────▶ boolean
                                 │
                                 ├─ ThemeStorage.save() ────▶ content/themes/{name}/
                                 │
                                 ├─ loadOneTheme() ─────────▶ themeList
                                 │
                                 └─ toJSON() ───────────────▶ API Response

themeName ───────────────▶ activate()
                                 │
                                 ├─ list.get()
                                 │
                                 ├─ checkSafe() ────────────▶ checkedTheme / エラー
                                 │
                                 └─ activateFromAPI()
                                        │
                                        └─ settings.active_theme 更新

themeName ───────────────▶ destroy()
                                 │
                                 ├─ デフォルト/アクティブチェック
                                 │
                                 ├─ ThemeStorage.delete() ──▶ ファイル削除
                                 │
                                 └─ list.del() ─────────────▶ themeList 更新
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| index.js | `ghost/core/core/server/services/themes/index.js` | サービス | モジュールエントリーポイント |
| activate.js | `ghost/core/core/server/services/themes/activate.js` | サービス | テーマ有効化 |
| loader.js | `ghost/core/core/server/services/themes/loader.js` | サービス | テーマ読み込み |
| storage.js | `ghost/core/core/server/services/themes/storage.js` | サービス | ストレージ操作（ZIP/ファイル） |
| validate.js | `ghost/core/core/server/services/themes/validate.js` | サービス | gscanバリデーション |
| list.js | `ghost/core/core/server/services/themes/list.js` | サービス | テーマリスト管理 |
| to-json.js | `ghost/core/core/server/services/themes/to-json.js` | サービス | JSONシリアライズ |
| activation-bridge.js | `ghost/core/core/server/services/themes/activation-bridge.js` | サービス | 有効化ブリッジ |
| theme-storage.js | `ghost/core/core/server/services/themes/theme-storage.js` | サービス | テーマストレージクラス |
| installer.js | `ghost/core/core/server/services/themes/installer.js` | サービス | GitHubインストーラー |
