Files
CTFd/webpack.config.js
Kevin Chung adc70fb320 3.0.0a1 (#1523)
Alpha release of CTFd v3. 

# 3.0.0a1 / 2020-07-01

**General**

- CTFd is now Python 3 only
- Render markdown with the CommonMark spec provided by `cmarkgfm`
- Render markdown stripped of any malicious JavaScript or HTML.
  - This is a significant change from previous versions of CTFd where any HTML content from an admin was considered safe.
- Inject `Config`, `User`, `Team`, `Session`, and `Plugin` globals into Jinja
- User sessions no longer store any user-specific attributes.
  - Sessions only store the user's ID, CSRF nonce, and an hmac of the user's password
  - This allows for session invalidation on password changes
- The user facing side of CTFd now has user and team searching
- GeoIP support now available for converting IP addresses to guessed countries

**Admin Panel**

- Use EasyMDE as an improved description/text editor for Markdown enabled fields.
- Media Library button now integrated into EasyMDE enabled fields
- VueJS now used as the underlying implementation for the Media Library
- Fix setting theme color in Admin Panel
- Green outline border has been removed from the Admin Panel

**API**

- Significant overhauls in API documentation provided by Swagger UI and Swagger json
- Make almost all API endpoints provide filtering and searching capabilities
- Change `GET /api/v1/config/<config_key>` to return structured data according to ConfigSchema

**Themes**

- Themes now have access to the `Configs` global which provides wrapped access to `get_config`.
  - For example, `{{ Configs.ctf_name }}` instead of `get_ctf_name()` or `get_config('ctf_name')`
- Themes must now specify a `challenge.html` which control how a challenge should look.
- The main library for charts has been changed from Plotly to Apache ECharts.
- Forms have been moved into wtforms for easier form rendering inside of Jinja.
  - From Jinja you can access forms via the Forms global i.e. `{{ Forms }}`
  - This allows theme developers to more easily re-use a form without having to copy-paste HTML.
- Themes can now provide a theme settings JSON blob which can be injected into the theme with `{{ Configs.theme_settings }}`
- Core theme now includes the challenge ID in location hash identifiers to always refer the right challenge despite duplicate names

**Plugins**

- Challenge plugins have changed in structure to better allow integration with themes and prevent obtrusive Javascript/XSS.
  - Challenge rendering now uses `challenge.html` from the provided theme.
  - Accessing the challenge view content is now provided by `/api/v1/challenges/<challenge_id>` in the `view` section. This allows for HTML to be properly sanitized and rendered by the server allowing CTFd to remove client side Jinja rendering.
  - `challenge.html` now specifies what's required and what's rendered by the theme. This allows the challenge plugin to avoid having to deal with aspects of the challenge besides the description and input.
  - A more complete migration guide will be provided when CTFd v3 leaves beta
- Display current attempt count in challenge view when max attempts is enabled
- `get_standings()`, `get_team_stanadings()`, `get_user_standings()` now has a fields keyword argument that allows for specificying additional fields that SQLAlchemy should return when building the response set.
  - Useful for gathering additional data when building scoreboard pages
- Flags can now control the message that is shown to the user by raising `FlagException`
- Fix `override_template()` functionality

**Deployment**

- Enable SQLAlchemy's `pool_pre_ping` by default to reduce the likelihood of database connection issues
- Mailgun email settings are now deprecated. Admins should move to SMTP email settings instead.
- Postgres is now considered a second class citizen in CTFd. It is tested against but not a main database backend. If you use Postgres, you are entirely on your own with regards to supporting CTFd.
- Docker image now uses Debian instead of Alpine. See https://github.com/CTFd/CTFd/issues/1215 for rationale.
- `docker-compose.yml` now uses a non-root user to connect to MySQL/MariaDB
- `config.py` should no longer be editting for configuration, instead edit `config.ini` or the environment variables in `docker-compose.yml`
2020-07-01 12:06:05 -04:00

285 lines
9.2 KiB
JavaScript

const path = require('path')
const webpack = require('webpack')
const FixStyleOnlyEntriesPlugin = require("webpack-fix-style-only-entries")
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const RemoveStrictPlugin = require('remove-strict-webpack-plugin')
const WebpackShellPlugin = require('webpack-shell-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const roots = {
'themes/core': {
'css': {
'challenge-board': 'assets/css/challenge-board.scss',
'fonts': 'assets/css/fonts.scss',
'main': 'assets/css/main.scss',
'core': 'assets/css/core.scss',
'codemirror': 'assets/css/codemirror.scss',
},
'js': {
'pages/main': 'assets/js/pages/main.js',
'pages/setup': 'assets/js/pages/setup.js',
'pages/challenges': 'assets/js/pages/challenges.js',
'pages/scoreboard': 'assets/js/pages/scoreboard.js',
'pages/settings': 'assets/js/pages/settings.js',
'pages/stats': 'assets/js/pages/stats.js',
'pages/notifications': 'assets/js/pages/notifications.js',
'pages/teams/private': 'assets/js/pages/teams/private.js',
}
},
'themes/admin': {
'css': {
'admin': 'assets/css/admin.scss',
'challenge-board': 'assets/css/challenge-board.scss',
'codemirror': 'assets/css/codemirror.scss',
},
'js': {
'pages/main': 'assets/js/pages/main.js',
'pages/challenge': 'assets/js/pages/challenge.js',
'pages/challenges': 'assets/js/pages/challenges.js',
'pages/configs': 'assets/js/pages/configs.js',
'pages/notifications': 'assets/js/pages/notifications.js',
'pages/editor': 'assets/js/pages/editor.js',
'pages/pages': 'assets/js/pages/pages.js',
'pages/reset': 'assets/js/pages/reset.js',
'pages/scoreboard': 'assets/js/pages/scoreboard.js',
'pages/statistics': 'assets/js/pages/statistics.js',
'pages/submissions': 'assets/js/pages/submissions.js',
'pages/team': 'assets/js/pages/team.js',
'pages/teams': 'assets/js/pages/teams.js',
'pages/user': 'assets/js/pages/user.js',
'pages/users': 'assets/js/pages/users.js',
}
},
}
function getJSConfig(root, type, entries, mode) {
const out = {}
const ext = mode == 'development' ? 'dev' : 'min'
const chunk_file = `[name].${ext}.chunk.js`
for (let key in entries) {
out[key] = path.resolve(__dirname, 'CTFd', root, entries[key])
}
return {
entry: out,
output: {
path: path.resolve(__dirname, 'CTFd', root, 'static', type),
publicPath: '/' + root + '/static/' + type,
filename: `[name].${ext}.js`,
chunkFilename: chunk_file,
},
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
echarts: {
name: 'echarts',
filename: `echarts.bundle.${ext}.js`,
test: /echarts/,
priority: 1,
enforce: true,
},
vendor: {
name: 'vendor',
filename: `vendor.bundle.${ext}.js`,
test: /node_modules/,
// maxSize: 1024 * 256,
enforce: true,
},
graphs: {
name: 'graphs',
filename: `graphs.${ext}.js`,
test: /graphs/,
priority: 1,
reuseExistingChunk: true,
},
helpers: {
name: 'helpers',
filename: `helpers.${ext}.js`,
test: /helpers/,
priority: 1,
reuseExistingChunk: true,
},
default: {
filename: `core.${ext}.js`,
minChunks: 2,
priority: -1,
reuseExistingChunk: true,
},
},
},
minimizer: [
new UglifyJsPlugin({
cache: true,
parallel: true,
sourceMap: true
}),
],
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
presets: [
['@babel/preset-env', { useBuiltIns: 'entry', modules: 'commonjs' }],
],
}
}
},
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
loaders: {
css: ['vue-style-loader', {
loader: 'css-loader',
}],
js: [
'babel-loader',
],
},
cacheBusting: true,
},
}
],
},
plugins: [
new VueLoaderPlugin(),
new webpack.NamedModulesPlugin(),
new RemoveStrictPlugin(),
// Identify files that are generated in development but not in production and create stubs to avoid a 404
// Pretty nasty hack, would be a little better if this was purely JS
new WebpackShellPlugin({
onBuildEnd:[
mode == 'development' ? 'echo Skipping JS stub generation' : 'python3 -c \'exec(\"\"\"\nimport glob\nimport os\n\nstatic_js_dirs = [\n "CTFd/themes/core/static/js/**/*.dev.js",\n "CTFd/themes/admin/static/js/**/*.dev.js",\n]\n\nfor js_dir in static_js_dirs:\n for path in glob.glob(js_dir, recursive=True):\n if path.endswith(".dev.js"):\n path = path.replace(".dev.js", ".min.js")\n if os.path.isfile(path) is False:\n open(path, "a").close()\n\"\"\")\''
],
safe: true,
}),
],
resolve: {
extensions: ['.js'],
alias: {
core: path.resolve(__dirname, 'CTFd/themes/core/assets/js/'),
},
},
}
}
function getCSSConfig(root, type, entries, mode) {
const out = {}
const ext = mode == 'development' ? 'dev' : 'min'
const ouptut_file = `[name].${ext}.css`
const chunk_file = `[id].${ext}.css`
for (let key in entries) {
out[key] = path.resolve(__dirname, 'CTFd', root, entries[key])
}
return {
entry: out,
output: {
path: path.resolve(__dirname, 'CTFd', root, 'static', type),
publicPath: '/' + root + '/static/' + type,
},
optimization: {
minimizer: [
new OptimizeCssAssetsPlugin({})
]
},
module: {
rules: [
{
test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?(#\w+)?$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[ext]',
publicPath: '../fonts',
outputPath: '../fonts',
}
}
]
},
// {
// test: /\.css$/,
// use: [
// 'vue-style-loader',
// 'css-loader'
// ]
// },
{
test: /\.(s?)css$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
importLoaders: 2,
}
},
{
loader: 'string-replace-loader',
options: {
multiple: [
// Replace core font-faces
{ search: "font-face\s*{\s*font-family:\s*[\"']Lato[\"']", replace: "font-face{font-family:'LatoOffline'", flags: 'gm' },
{ search: "font-face\s*{\s*font-family:\s*[\"']Raleway[\"']", replace: "font-face{font-family:'RalewayOffline'", flags: 'gm' },
// Replace Font-Awesome font-faces
{ search: "font-face\s*{\s*font-family:\s*[\"']Font Awesome 5 Free[\"']", replace: "font-face{font-family:'Font Awesome 5 Free Offline'", flags: 'gm' },
{ search: "font-face\s*{\s*font-family:\s*[\"']Font Awesome 5 Brands[\"']", replace: "font-face{font-family:'Font Awesome 5 Brands Offline'", flags: 'gm' },
// Replace Font-Awesome class rules
{ search: "far\s*{\s*font-family:\s*[\"']Font Awesome 5 Free[\"']", replace: "far{font-family:'Font Awesome 5 Free','Font Awesome 5 Free Offline'", flags: 'gm' },
{ search: "fas\s*{\s*font-family:\s*[\"']Font Awesome 5 Free[\"']", replace: "fas{font-family:'Font Awesome 5 Free','Font Awesome 5 Free Offline'", flags: 'gm' },
{ search: "fab\s*{\s*font-family:\s*[\"']Font Awesome 5 Brands[\"']", replace: "fab{font-family:'Font Awesome 5 Brands','Font Awesome 5 Brands Offline'", flags: 'gm' },
],
strict: true,
}
},
{
loader: 'sass-loader',
},
],
},
]
},
resolve: {
extensions: ['.css'],
alias: {
core: path.resolve(__dirname, 'CTFd/themes/core/assets/css/'),
},
},
plugins: [
new FixStyleOnlyEntriesPlugin(),
new MiniCssExtractPlugin({
filename: ouptut_file,
chunkFilename: chunk_file
}),
],
}
}
const mapping = {
'js': getJSConfig,
'css': getCSSConfig,
}
module.exports = (env, options) => {
let output = []
let mode = options.mode
for (let root in roots) {
for (let type in roots[root]) {
let entry = mapping[type](root, type, roots[root][type], mode);
output.push(entry)
}
}
return output
}