chrome-extension-test-framework
Version:
Universal testing framework for Chrome extensions - Fast static analysis without browser dependencies
405 lines (360 loc) • 14.4 kB
JavaScript
/**
* ImprovedErrorMessages - 具体的で実用的なエラーメッセージ
*
* エラーメッセージをより分かりやすく、
* 具体的な解決方法を提示するモジュール
*/
class ImprovedErrorMessages {
constructor() {
// エラータイプごとのメッセージテンプレート
this.messageTemplates = {
// セキュリティ関連
'unsafe-innerHTML': {
message: 'Unsafe innerHTML assignment detected',
detailed: 'Direct assignment to innerHTML with user-controlled data can lead to XSS vulnerabilities',
solutions: [
'Use textContent for plain text content',
'Use DOMPurify.sanitize() for HTML content',
'Use chrome.i18n.getMessage() for localized strings',
'Create DOM elements programmatically with createElement()'
],
example: `// Bad:
element.innerHTML = userInput;
// Good:
element.textContent = userInput; // For plain text
element.innerHTML = DOMPurify.sanitize(userInput); // For HTML
element.innerHTML = chrome.i18n.getMessage('key'); // For i18n`
},
'eval': {
message: 'Use of eval() detected',
detailed: 'eval() executes arbitrary code and is a major security risk in Chrome extensions',
solutions: [
'Use JSON.parse() for parsing JSON data',
'Use Function constructor only if absolutely necessary',
'Refactor code to avoid dynamic code execution',
'Use chrome.scripting.executeScript() for content script injection'
],
example: `// Bad:
const result = eval(codeString);
// Good:
const data = JSON.parse(jsonString);
// Or use chrome.scripting for dynamic scripts:
chrome.scripting.executeScript({
target: {tabId: tab.id},
func: myFunction
});`
},
'localStorage': {
message: 'localStorage usage detected',
detailed: 'localStorage is not recommended for Chrome extensions. Use chrome.storage API instead',
solutions: [
'Use chrome.storage.local for local storage',
'Use chrome.storage.sync for synced storage',
'Use chrome.storage.session for temporary storage',
'Encrypt sensitive data before storing'
],
example: `// Bad:
localStorage.setItem('key', value);
const value = localStorage.getItem('key');
// Good:
chrome.storage.local.set({key: value});
chrome.storage.local.get(['key'], (result) => {
console.log(result.key);
});`
},
'hardcoded-secret': {
message: 'Hardcoded secret detected',
detailed: 'Secrets should never be hardcoded in source code',
solutions: [
'Use environment variables during build',
'Store secrets in a secure backend service',
'Use OAuth2 flow for API authentication',
'Implement proper key management'
],
example: `// Bad:
const API_KEY = 'sk_live_abcd1234';
// Good:
// 1. Use OAuth2:
chrome.identity.getAuthToken({interactive: true}, (token) => {
// Use token for API calls
});
// 2. Or fetch from secure backend:
const response = await fetch('https://your-backend.com/api/get-key', {
headers: {'Authorization': 'Bearer ' + userToken}
});`
},
'missing-permission': {
message: 'Required permission missing',
detailed: 'The extension uses an API that requires a permission not declared in manifest.json',
solutions: [
'Add the required permission to manifest.json',
'Use optional_permissions for runtime permissions',
'Check if the API is available before using it',
'Use alternative APIs that don\'t require permissions'
],
example: `// manifest.json:
{
"permissions": [
"storage",
"tabs"
],
"optional_permissions": [
"cookies",
"history"
]
}
// Check permission before use:
chrome.permissions.contains({
permissions: ['cookies']
}, (result) => {
if (result) {
// Use the API
} else {
// Request permission or use alternative
}
});`
},
'console': {
message: 'Console statement detected',
detailed: 'Console statements should be removed from production code',
solutions: [
'Remove console statements before release',
'Use a build tool to strip console statements',
'Implement a proper logging system',
'Use conditional logging for development'
],
example: `// Bad:
console.log('Debug info:', data);
// Good:
// 1. Conditional logging:
if (process.env.NODE_ENV === 'development') {
console.log('Debug info:', data);
}
// 2. Custom logger:
class Logger {
static log(...args) {
if (isDevelopment) console.log(...args);
}
}
// 3. Build-time removal with Terser/webpack`
},
'insecure-message-passing': {
message: 'Insecure message passing detected',
detailed: 'Message sender is not properly verified, allowing potential spoofing',
solutions: [
'Always verify the sender in message listeners',
'Check sender.id against chrome.runtime.id',
'Validate sender.origin for web pages',
'Use specific message types/actions'
],
example: `// Bad:
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
// No sender verification!
processMessage(request);
});
// Good:
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
// Verify sender
if (sender.id !== chrome.runtime.id) return;
// Validate message structure
if (!request.type || !request.data) return;
// Process based on type
switch (request.type) {
case 'GET_DATA':
// Handle specific action
break;
}
});`
},
'csp-violation': {
message: 'Content Security Policy violation',
detailed: 'The code violates Chrome extension CSP restrictions',
solutions: [
'Remove inline scripts and styles',
'Move JavaScript to external files',
'Use addEventListener instead of onclick',
'Avoid unsafe-eval and unsafe-inline'
],
example: `<!-- Bad: -->
<button onclick="handleClick()">Click</button>
<script>function handleClick() {}</script>
<!-- Good: -->
<button id="myButton">Click</button>
<script src="popup.js"></script>
// popup.js:
document.getElementById('myButton').addEventListener('click', handleClick);`
}
};
// コンテキスト別の追加情報
this.contextualInfo = {
'background-script': {
tips: [
'Background scripts run in a privileged context',
'Use service workers in Manifest V3',
'Avoid DOM manipulation in background scripts',
'Use chrome.action API instead of browserAction/pageAction'
]
},
'content-script': {
tips: [
'Content scripts run in web page context',
'Use chrome.runtime.sendMessage for communication',
'Be careful with page DOM manipulation',
'Isolate your code from the page\'s JavaScript'
]
},
'popup': {
tips: [
'Popup scripts have limited lifetime',
'Save state before popup closes',
'Use chrome.storage for persistence',
'Keep initialization fast'
]
}
};
}
/**
* エラーメッセージを改善
*/
improve(issue, context = {}) {
const template = this.messageTemplates[issue.type];
if (!template) {
return this.createGenericMessage(issue);
}
const improved = {
...issue,
message: template.message,
detailed: template.detailed,
solutions: template.solutions,
example: template.example
};
// コンテキストに基づく追加情報
if (context.fileType && this.contextualInfo[context.fileType]) {
improved.contextTips = this.contextualInfo[context.fileType].tips;
}
// 具体的なコード位置の情報
if (issue.context) {
improved.codeContext = this.formatCodeContext(issue.context, issue.line, issue.column);
}
// 関連リンク
improved.references = this.getReferences(issue.type);
return improved;
}
/**
* 汎用的なメッセージを生成
*/
createGenericMessage(issue) {
return {
...issue,
message: issue.message || `Issue detected: ${issue.type}`,
detailed: 'This issue may affect your extension\'s functionality or security',
solutions: [
'Review the code at the specified location',
'Consult Chrome extension documentation',
'Follow best practices for Chrome extensions'
],
references: [
'https://developer.chrome.com/docs/extensions/mv3/'
]
};
}
/**
* コードコンテキストをフォーマット
*/
formatCodeContext(context, line, column) {
const lines = context.split('\n');
const formatted = lines.map((lineContent, index) => {
const lineNum = line - Math.floor(lines.length / 2) + index;
const isErrorLine = index === Math.floor(lines.length / 2);
const prefix = isErrorLine ? '>' : ' ';
let formattedLine = `${prefix} ${lineNum}: ${lineContent}`;
// エラー位置を示す
if (isErrorLine && column) {
const spaces = ' '.repeat(prefix.length + lineNum.toString().length + 2 + column - 1);
formattedLine += '\n' + spaces + '^';
}
return formattedLine;
}).join('\n');
return formatted;
}
/**
* 参考リンクを取得
*/
getReferences(issueType) {
const references = {
'unsafe-innerHTML': [
'https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML#security_considerations',
'https://github.com/cure53/DOMPurify'
],
'localStorage': [
'https://developer.chrome.com/docs/extensions/reference/storage/',
'https://developer.chrome.com/docs/extensions/mv3/intro/mv3-migration/#storage'
],
'eval': [
'https://developer.chrome.com/docs/extensions/mv3/intro/mv3-migration/#remotely-hosted-code',
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval#never_use_eval!'
],
'console': [
'https://developer.chrome.com/docs/extensions/mv3/service_workers/#debugging'
],
'missing-permission': [
'https://developer.chrome.com/docs/extensions/mv3/declare_permissions/',
'https://developer.chrome.com/docs/extensions/reference/permissions/'
],
'csp-violation': [
'https://developer.chrome.com/docs/extensions/mv3/intro/mv3-migration/#content-security-policy'
]
};
return references[issueType] || [
'https://developer.chrome.com/docs/extensions/mv3/'
];
}
/**
* 修正の難易度を評価
*/
assessFixDifficulty(issue) {
const difficulties = {
'console': 'easy', // 単純な削除
'localStorage': 'medium', // APIの置き換え
'eval': 'hard', // ロジックの再設計が必要
'unsafe-innerHTML': 'medium',
'hardcoded-secret': 'hard',
'missing-permission': 'easy'
};
return difficulties[issue.type] || 'medium';
}
/**
* 自動修正可能かどうか
*/
isAutoFixable(issue) {
const autoFixable = [
'console', // 削除可能
'missing-permission', // manifest.jsonに追加可能
'trailing-spaces', // 自動フォーマット可能
'quotes' // 引用符の統一
];
return autoFixable.includes(issue.type);
}
/**
* バッチメッセージを生成(複数の同じタイプのエラー)
*/
createBatchMessage(issues) {
if (issues.length === 0) return null;
const type = issues[0].type;
const template = this.messageTemplates[type];
return {
type: type,
count: issues.length,
message: `${issues.length} instances of ${template?.message || type}`,
detailed: template?.detailed,
locations: issues.map(i => ({
file: i.file,
line: i.line,
column: i.column
})),
solutions: template?.solutions || [],
example: template?.example
};
}
}
module.exports = ImprovedErrorMessages;