Skip to content
Merged
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
95 changes: 69 additions & 26 deletions js/course-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,45 +69,85 @@ class CourseManager {
}

/**
* 各コースのチャレンジデータを読み込み
* 各コースのチャレンジデータを読み込み(遅延読み込み対応)
*/
async loadChallengeData() {
// 遅延読み込み: 現在選択されているコースのみ読み込み
const selectedCourseId = this.progressManager?.getSelectedCourse();

const challengeFiles = {
'sql-basics': 'slides/challenges.json',
'db-fundamentals': 'slides/db-fundamentals-challenges.json',
'big-data-basics': 'slides/big-data-basics-challenges.json'
};

for (const [courseId, filePath] of Object.entries(challengeFiles)) {
try {
const response = await fetch(filePath);
if (response.ok) {
this.challengeData[courseId] = await response.json();
} else {
console.warn(`チャレンジファイルが見つかりません: ${filePath}`);
this.challengeData[courseId] = [];
}
} catch (error) {
console.error(`チャレンジデータ読み込みエラー (${courseId}):`, error);
// 選択されたコースがある場合は、そのコースのみ読み込み
if (selectedCourseId && challengeFiles[selectedCourseId]) {
await this.loadCourseChallenge(selectedCourseId, challengeFiles[selectedCourseId]);
} else {
// 初回アクセス時は基本コースのみ読み込み
await this.loadCourseChallenge('sql-basics', challengeFiles['sql-basics']);
}
}

/**
* 特定コースのチャレンジデータを読み込み
*/
async loadCourseChallenge(courseId, filePath) {
// 既に読み込み済みの場合はスキップ
if (this.challengeData[courseId]) {
return this.challengeData[courseId];
}

try {
const response = await fetch(filePath);
if (response.ok) {
this.challengeData[courseId] = await response.json();
console.log(`チャレンジデータを読み込みました: ${courseId}`);
} else {
console.warn(`チャレンジファイルが見つかりません: ${filePath}`);
this.challengeData[courseId] = [];
}
} catch (error) {
console.error(`チャレンジデータ読み込みエラー (${courseId}):`, error);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Description: Log Injection occurs when untrusted user input is directly written to log files without proper sanitization. This can allow attackers to manipulate log entries, potentially leading to security issues like log forging or cross-site scripting. To prevent this, always sanitize user input using encodeURIComponent() or DOMPurify.sanitize() before logging. Learn more - https://cwe.mitre.org/data/definitions/117.html

Severity: High

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix uses DOMPurify.sanitize() to sanitize the courseId before logging it, preventing potential log injection attacks by removing any malicious content from the user input.

Suggested change
console.error(`チャレンジデータ読み込みエラー (${courseId}):`, error);
this.challengeData[courseId] = [];
}
} catch (error) {
// import DOMPurify from 'dompurify';
console.error(`チャレンジデータ読み込みエラー (${DOMPurify.sanitize(courseId)}):`, error);
// ErrorHandlerを使用してチャレンジデータエラーを処理
if (window.errorHandler) {


// ErrorHandlerを使用してチャレンジデータエラーを処理
if (window.errorHandler) {
const result = await window.errorHandler.handleError('CHALLENGE_LOAD_ERROR', error, {
courseId: courseId,
challengeFile: filePath,
operation: 'loadCourseChallenge'
});

// ErrorHandlerを使用してチャレンジデータエラーを処理
if (window.errorHandler) {
const result = await window.errorHandler.handleError('CHALLENGE_LOAD_ERROR', error, {
courseId: courseId,
challengeFile: filePath,
operation: 'loadChallengeData'
});

if (result.success) {
this.challengeData[courseId] = result.data;
} else {
this.challengeData[courseId] = [];
}
if (result.success) {
this.challengeData[courseId] = result.data;
} else {
this.challengeData[courseId] = [];
}
} else {
this.challengeData[courseId] = [];
}
}

return this.challengeData[courseId];
}

/**
* 必要に応じてコースのチャレンジデータを遅延読み込み
*/
async ensureCourseDataLoaded(courseId) {
if (!this.challengeData[courseId]) {
const challengeFiles = {
'sql-basics': 'slides/challenges.json',
'db-fundamentals': 'slides/db-fundamentals-challenges.json',
'big-data-basics': 'slides/big-data-basics-challenges.json'
};

if (challengeFiles[courseId]) {
await this.loadCourseChallenge(courseId, challengeFiles[courseId]);
}
}
return this.challengeData[courseId] || [];
}

/**
Expand Down Expand Up @@ -169,12 +209,15 @@ class CourseManager {
/**
* コースを選択
*/
selectCourse(courseId) {
async selectCourse(courseId) {
const course = this.getCourse(courseId);
if (!course) {
throw new Error(`コースが見つかりません: ${courseId}`);
}

// コースのチャレンジデータを遅延読み込み
await this.ensureCourseDataLoaded(courseId);

this.currentCourse = course;
this.progressManager.saveSelectedCourse(courseId);

Expand Down
89 changes: 78 additions & 11 deletions js/course-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,28 @@ export class CourseUI {
courseList: document.getElementById('course-list'),
appLayout: document.querySelector('.app-layout')
};

// UIOptimizerを初期化
this.uiOptimizer = null;
this.initializeUIOptimizer();

this.bindEvents();
}

/**
* UIOptimizerを初期化
*/
async initializeUIOptimizer() {
try {
const { UIOptimizer } = await import('./ui-optimizer.js');
this.uiOptimizer = new UIOptimizer();
this.uiOptimizer.initialize();
console.log('CourseUI: UIOptimizerが初期化されました');
} catch (error) {
console.warn('UIOptimizerの読み込みに失敗しました:', error);
}
}

/**
* イベントリスナーを設定
*/
Expand Down Expand Up @@ -47,19 +66,28 @@ export class CourseUI {
}

/**
* コース一覧を描画
* コース一覧を描画(最適化版)
*/
renderCourseList(courses) {
if (!courses || courses.length === 0) {
this.elements.courseList.innerHTML = '<p>利用可能なコースがありません</p>';
return;
}

const courseCards = courses.map(course => this.createCourseCard(course)).join('');
this.elements.courseList.innerHTML = courseCards;

// コース選択ボタンのイベントリスナーを設定
this.bindCourseSelectionEvents();
// UIOptimizerが利用可能な場合は最適化された更新を使用
if (this.uiOptimizer) {
this.uiOptimizer.updateCourseSelection(courses, 'high');

// イベントリスナーを遅延設定
setTimeout(() => {
this.bindCourseSelectionEvents();
}, 50);
} else {
// フォールバック: 従来の方法
const courseCards = courses.map(course => this.createCourseCard(course)).join('');
this.elements.courseList.innerHTML = courseCards;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Description: A potential code injection vulnerability has been detected, where untrusted input is passed to a method that may execute arbitrary code. This issue allows attackers to inject and execute arbitrary code within the application, which could lead to unauthorized access to sensitive data or other malicious actions. To mitigate this, ensure that all user-supplied input is properly sanitized and validated before being processed. Avoid passing untrusted input to methods like eval, render etc, that can execute arbitrary code. Where possible, use safer alternatives such as parameterized queries or more controlled methods for handling user input. Learn more

Severity: Critical

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix replaces the direct assignment to innerHTML with a safer approach using textContent and DOM manipulation to prevent potential XSS attacks. It creates a temporary div element to parse the HTML string and then appends it to the courseList element.

Suggested change
this.elements.courseList.innerHTML = courseCards;
renderCourseList(courses) {
if (!courses || courses.length === 0) {
this.elements.courseList.textContent = '利用可能なコースがありません';
return;
}
// UIOptimizerが利用可能な場合は最適化された更新を使用
if (this.uiOptimizer) {
this.uiOptimizer.updateCourseSelection(courses, 'high');
// イベントリスナーを遅延設定
setTimeout(() => {
this.bindCourseSelectionEvents();
}, 50);
} else {
// フォールバック: 従来の方法
const courseCards = courses.map(course => this.createCourseCard(course)).join('');
this.elements.courseList.textContent = '';
const tempDiv = document.createElement('div');
tempDiv.innerHTML = courseCards;
this.elements.courseList.appendChild(tempDiv);
this.bindCourseSelectionEvents();
}
}

this.bindCourseSelectionEvents();
}
}

/**
Expand Down Expand Up @@ -185,21 +213,25 @@ export class CourseUI {
}

/**
* コースを選択して学習を開始
* コースを選択して学習を開始(最適化版)
*/
async selectCourse(courseId) {
try {
// ローディング状態を表示
this.showCourseLoadingState(courseId);

// コースを取得
const course = this.courseManager.getCourse(courseId);

// 準備中のコースかチェック
if (course.status === 'coming_soon') {
alert('このコースは現在準備中です。近日公開予定です。');
this.hideCourseLoadingState();
return;
}

// コースを選択
const selectedCourse = this.courseManager.selectCourse(courseId);
// コースを選択(遅延読み込み対応)
const selectedCourse = await this.courseManager.selectCourse(courseId);
console.log(`コースを選択しました: ${selectedCourse.title}`);

// GameEngineにコースを設定
Expand All @@ -220,17 +252,52 @@ export class CourseUI {
window.progressUI.onCourseSelected(course);
}

// チャレンジを更新
// チャレンジを更新(非同期で実行)
if (window.uiController && typeof window.uiController.updateChallenge === 'function') {
window.uiController.updateChallenge();
// UI更新を次のフレームで実行
requestAnimationFrame(() => {
window.uiController.updateChallenge();
});
}

this.hideCourseLoadingState();

} catch (error) {
console.error('コース選択エラー:', error);
this.hideCourseLoadingState();
alert(`コース選択に失敗しました: ${error.message}`);
}
}

/**
* コース読み込み状態を表示
*/
showCourseLoadingState(courseId) {
const courseCard = document.querySelector(`[data-course-id="${courseId}"]`);
if (courseCard) {
const selectBtn = courseCard.querySelector('.course-select-btn');
if (selectBtn) {
selectBtn.disabled = true;
selectBtn.textContent = '読み込み中...';
}
}
}

/**
* コース読み込み状態を非表示
*/
hideCourseLoadingState() {
const selectBtns = document.querySelectorAll('.course-select-btn');
selectBtns.forEach(btn => {
btn.disabled = false;
const courseId = btn.dataset.courseId;
const course = this.courseManager.getCourse(courseId);
const progress = this.courseManager.getCourseProgress(courseId);
const isStarted = progress && progress.completedLessons.length > 0;
btn.textContent = isStarted ? '続きから学習' : 'コースを開始';
});
}

/**
* コース詳細情報を表示
*/
Expand Down
13 changes: 13 additions & 0 deletions js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { ErrorHandler } from './error-handler.js';
import { NotificationSystem } from './notification-system.js';
import { AdaptiveLearning } from './adaptive-learning.js';
import { AdaptiveLearningUI } from './adaptive-learning-ui.js';
import { UIOptimizer } from './ui-optimizer.js';
import { PerformanceMonitor } from './performance-monitor.js';


// グローバル変数
Expand All @@ -21,6 +23,8 @@ let courseUI;
let progressUI;
let adaptiveLearning;
let adaptiveLearningUI;
let uiOptimizer;
let performanceMonitor;

// アプリケーション初期化
async function initializeApp() {
Expand Down Expand Up @@ -66,11 +70,20 @@ async function initializeApp() {
// 既存のチャレンジ読み込み(フォールバック用)
await gameEngine.loadChallenges();

// UIOptimizer初期化
uiOptimizer = new UIOptimizer();
uiOptimizer.initialize();

// PerformanceMonitor初期化
performanceMonitor = new PerformanceMonitor();

// グローバルアクセス用
window.dbManager = dbManager;
window.gameEngine = gameEngine;
window.courseManager = courseManager;
window.adaptiveLearning = adaptiveLearning;
window.uiOptimizer = uiOptimizer;
window.performanceMonitor = performanceMonitor;

// UIコントローラー初期化
const autoComplete = new SQLAutoComplete();
Expand Down
Loading
Loading