commit 8c897ceb724baba1ba82429bc94dff17b8f3fdc2 Author: Toby Zerner Date: Wed Aug 5 16:08:53 2015 +0930 Initial commit diff --git a/extensions/suspend/.editorconfig b/extensions/suspend/.editorconfig new file mode 100644 index 000000000..5612a5e74 --- /dev/null +++ b/extensions/suspend/.editorconfig @@ -0,0 +1,32 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 2 + +[*.js] +indent_style = space +indent_size = 2 + +[*.{css,less}] +indent_style = space +indent_size = 2 + +[*.html] +indent_style = space +indent_size = 2 + +[*.{diff,md}] +trim_trailing_whitespace = false + +[*.php] +indent_style = space +indent_size = 4 diff --git a/extensions/suspend/.eslintignore b/extensions/suspend/.eslintignore new file mode 100644 index 000000000..86b7c8854 --- /dev/null +++ b/extensions/suspend/.eslintignore @@ -0,0 +1,5 @@ +**/bower_components/**/* +**/node_modules/**/* +vendor/**/* +**/Gulpfile.js +**/dist/**/* diff --git a/extensions/suspend/.eslintrc b/extensions/suspend/.eslintrc new file mode 100644 index 000000000..9cebc759d --- /dev/null +++ b/extensions/suspend/.eslintrc @@ -0,0 +1,171 @@ +{ + "parser": "babel-eslint", // https://github.com/babel/babel-eslint + "env": { // http://eslint.org/docs/user-guide/configuring.html#specifying-environments + "browser": true // browser global variables + }, + "ecmaFeatures": { + "arrowFunctions": true, + "blockBindings": true, + "classes": true, + "defaultParams": true, + "destructuring": true, + "forOf": true, + "generators": false, + "modules": true, + "objectLiteralComputedProperties": true, + "objectLiteralDuplicateProperties": false, + "objectLiteralShorthandMethods": true, + "objectLiteralShorthandProperties": true, + "spread": true, + "superInFunctions": true, + "templateStrings": true, + "jsx": true + }, + "globals": { + "m": true, + "app": true, + "$": true, + "moment": true + }, + "rules": { +/** + * Strict mode + */ + // babel inserts "use strict"; for us + "strict": [2, "never"], // http://eslint.org/docs/rules/strict + +/** + * ES6 + */ + "no-var": 2, // http://eslint.org/docs/rules/no-var + "prefer-const": 2, // http://eslint.org/docs/rules/prefer-const + +/** + * Variables + */ + "no-shadow": 2, // http://eslint.org/docs/rules/no-shadow + "no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names + "no-unused-vars": [2, { // http://eslint.org/docs/rules/no-unused-vars + "vars": "local", + "args": "after-used" + }], + "no-use-before-define": 2, // http://eslint.org/docs/rules/no-use-before-define + +/** + * Possible errors + */ + "comma-dangle": [2, "never"], // http://eslint.org/docs/rules/comma-dangle + "no-cond-assign": [2, "always"], // http://eslint.org/docs/rules/no-cond-assign + "no-console": 1, // http://eslint.org/docs/rules/no-console + "no-debugger": 1, // http://eslint.org/docs/rules/no-debugger + "no-alert": 1, // http://eslint.org/docs/rules/no-alert + "no-constant-condition": 1, // http://eslint.org/docs/rules/no-constant-condition + "no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys + "no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case + "no-empty": 2, // http://eslint.org/docs/rules/no-empty + "no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign + "no-extra-boolean-cast": 0, // http://eslint.org/docs/rules/no-extra-boolean-cast + "no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi + "no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign + "no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations + "no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp + "no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace + "no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls + "no-reserved-keys": 2, // http://eslint.org/docs/rules/no-reserved-keys + "no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays + "no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable + "use-isnan": 2, // http://eslint.org/docs/rules/use-isnan + "block-scoped-var": 2, // http://eslint.org/docs/rules/block-scoped-var + +/** + * Best practices + */ + "consistent-return": 2, // http://eslint.org/docs/rules/consistent-return + "curly": [2, "multi-line"], // http://eslint.org/docs/rules/curly + "default-case": 2, // http://eslint.org/docs/rules/default-case + "dot-notation": [2, { // http://eslint.org/docs/rules/dot-notation + "allowKeywords": true + }], + "eqeqeq": 2, // http://eslint.org/docs/rules/eqeqeq + "no-caller": 2, // http://eslint.org/docs/rules/no-caller + "no-else-return": 2, // http://eslint.org/docs/rules/no-else-return + "no-eq-null": 2, // http://eslint.org/docs/rules/no-eq-null + "no-eval": 2, // http://eslint.org/docs/rules/no-eval + "no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native + "no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind + "no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough + "no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal + "no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval + "no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks + "no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func + "no-multi-str": 2, // http://eslint.org/docs/rules/no-multi-str + "no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign + "no-new": 2, // http://eslint.org/docs/rules/no-new + "no-new-func": 2, // http://eslint.org/docs/rules/no-new-func + "no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers + "no-octal": 2, // http://eslint.org/docs/rules/no-octal + "no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape + "no-param-reassign": 2, // http://eslint.org/docs/rules/no-param-reassign + "no-proto": 2, // http://eslint.org/docs/rules/no-proto + "no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare + "no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign + "no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare + "no-sequences": 2, // http://eslint.org/docs/rules/no-sequences + "no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal + "no-with": 2, // http://eslint.org/docs/rules/no-with + "radix": 2, // http://eslint.org/docs/rules/radix + "vars-on-top": 2, // http://eslint.org/docs/rules/vars-on-top + "wrap-iife": [2, "any"], // http://eslint.org/docs/rules/wrap-iife + "yoda": 2, // http://eslint.org/docs/rules/yoda + +/** + * Style + */ + "indent": [2, 2], // http://eslint.org/docs/rules/indent + "brace-style": [2, // http://eslint.org/docs/rules/brace-style + "1tbs", { + "allowSingleLine": true + }], + "quotes": [ + 2, "single", "avoid-escape" // http://eslint.org/docs/rules/quotes + ], + "camelcase": [2, { // http://eslint.org/docs/rules/camelcase + "properties": "never" + }], + "comma-spacing": [2, { // http://eslint.org/docs/rules/comma-spacing + "before": false, + "after": true + }], + "comma-style": [2, "last"], // http://eslint.org/docs/rules/comma-style + "eol-last": 2, // http://eslint.org/docs/rules/eol-last + "func-names": 1, // http://eslint.org/docs/rules/func-names + "key-spacing": [2, { // http://eslint.org/docs/rules/key-spacing + "beforeColon": false, + "afterColon": true + }], + "new-cap": [2, { // http://eslint.org/docs/rules/new-cap + "newIsCap": true + }], + "no-multiple-empty-lines": [2, { // http://eslint.org/docs/rules/no-multiple-empty-lines + "max": 2 + }], + "no-new-object": 2, // http://eslint.org/docs/rules/no-new-object + "no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func + "no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces + "no-wrap-func": 2, // http://eslint.org/docs/rules/no-wrap-func + "no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle + "one-var": [2, "never"], // http://eslint.org/docs/rules/one-var + "padded-blocks": [2, "never"], // http://eslint.org/docs/rules/padded-blocks + "semi": [2, "always"], // http://eslint.org/docs/rules/semi + "semi-spacing": [2, { // http://eslint.org/docs/rules/semi-spacing + "before": false, + "after": true + }], + "space-after-keywords": 2, // http://eslint.org/docs/rules/space-after-keywords + "space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks + "space-before-function-paren": [2, "never"], // http://eslint.org/docs/rules/space-before-function-paren + "space-infix-ops": 2, // http://eslint.org/docs/rules/space-infix-ops + "space-return-throw-case": 2, // http://eslint.org/docs/rules/space-return-throw-case + "spaced-line-comment": 2, // http://eslint.org/docs/rules/spaced-line-comment + } +} diff --git a/extensions/suspend/.gitignore b/extensions/suspend/.gitignore new file mode 100644 index 000000000..a4f3b125e --- /dev/null +++ b/extensions/suspend/.gitignore @@ -0,0 +1,4 @@ +/vendor +composer.phar +.DS_Store +Thumbs.db diff --git a/extensions/suspend/bootstrap.php b/extensions/suspend/bootstrap.php new file mode 100644 index 000000000..4844b660d --- /dev/null +++ b/extensions/suspend/bootstrap.php @@ -0,0 +1,10 @@ +=5.4.0", + "flarum": ">0.1.0" + } +} \ No newline at end of file diff --git a/extensions/suspend/js/.gitignore b/extensions/suspend/js/.gitignore new file mode 100644 index 000000000..372e20a51 --- /dev/null +++ b/extensions/suspend/js/.gitignore @@ -0,0 +1,3 @@ +bower_components +node_modules +dist diff --git a/extensions/suspend/js/admin/Gulpfile.js b/extensions/suspend/js/admin/Gulpfile.js new file mode 100644 index 000000000..c36cae0a2 --- /dev/null +++ b/extensions/suspend/js/admin/Gulpfile.js @@ -0,0 +1,7 @@ +var gulp = require('flarum-gulp'); + +gulp({ + modules: { + 'suspend': 'src/**/*.js' + } +}); diff --git a/extensions/suspend/js/admin/package.json b/extensions/suspend/js/admin/package.json new file mode 100644 index 000000000..3e0ef919d --- /dev/null +++ b/extensions/suspend/js/admin/package.json @@ -0,0 +1,7 @@ +{ + "private": true, + "devDependencies": { + "gulp": "^3.8.11", + "flarum-gulp": "git+https://github.com/flarum/gulp.git" + } +} diff --git a/extensions/suspend/js/admin/src/main.js b/extensions/suspend/js/admin/src/main.js new file mode 100644 index 000000000..4541e65e9 --- /dev/null +++ b/extensions/suspend/js/admin/src/main.js @@ -0,0 +1,6 @@ +import { extend } from 'flarum/extend'; +import app from 'flarum/app'; + +app.initializers.add('suspend', () => { + // TODO +}); diff --git a/extensions/suspend/js/forum/Gulpfile.js b/extensions/suspend/js/forum/Gulpfile.js new file mode 100644 index 000000000..c36cae0a2 --- /dev/null +++ b/extensions/suspend/js/forum/Gulpfile.js @@ -0,0 +1,7 @@ +var gulp = require('flarum-gulp'); + +gulp({ + modules: { + 'suspend': 'src/**/*.js' + } +}); diff --git a/extensions/suspend/js/forum/package.json b/extensions/suspend/js/forum/package.json new file mode 100644 index 000000000..3e0ef919d --- /dev/null +++ b/extensions/suspend/js/forum/package.json @@ -0,0 +1,7 @@ +{ + "private": true, + "devDependencies": { + "gulp": "^3.8.11", + "flarum-gulp": "git+https://github.com/flarum/gulp.git" + } +} diff --git a/extensions/suspend/js/forum/src/components/SuspendUserModal.js b/extensions/suspend/js/forum/src/components/SuspendUserModal.js new file mode 100644 index 000000000..da0b5e782 --- /dev/null +++ b/extensions/suspend/js/forum/src/components/SuspendUserModal.js @@ -0,0 +1,107 @@ +import Modal from 'flarum/components/Modal'; +import Button from 'flarum/components/Button'; + +export default class SuspendUserModal extends Modal { + constructor(...args) { + super(...args); + + let until = this.props.user.suspendUntil(); + let status = null; + + if (new Date() > until) until = null; + + if (until) { + if (until.getFullYear() === 9999) status = 'indefinitely'; + else status = 'limited'; + } + + this.status = m.prop(status); + this.daysRemaining = m.prop(status === 'limited' && -moment().diff(until, 'days') + 1); + } + + className() { + return 'SuspendUserModal Modal--small'; + } + + title() { + return 'Suspend ' + this.props.user.username(); + } + + content() { + return ( +
+
+
+ +
+ + + + + +
+
+ +
+ {Button.component({ + children: 'Save Changes', + className: 'Button Button--primary', + loading: this.loading + })} +
+
+
+ ); + } + + onsubmit(e) { + e.preventDefault(); + + this.loading = true; + + let suspendUntil = null; + switch (this.status()) { + case 'indefinitely': + suspendUntil = new Date('9999-12-31'); + break; + + case 'limited': + suspendUntil = moment().add(this.daysRemaining(), 'days').toDate(); + break; + + default: + // no default + } + + this.props.user.save({suspendUntil}).then( + () => this.hide(), + () => { + this.loading = false; + m.redraw(); + } + ); + } +} diff --git a/extensions/suspend/js/forum/src/main.js b/extensions/suspend/js/forum/src/main.js new file mode 100644 index 000000000..33473cbfb --- /dev/null +++ b/extensions/suspend/js/forum/src/main.js @@ -0,0 +1,36 @@ +import { extend } from 'flarum/extend'; +import app from 'flarum/app'; +import UserControls from 'flarum/utils/UserControls'; +import Button from 'flarum/components/Button'; +import Badge from 'flarum/components/Badge'; +import Model from 'flarum/Model'; +import User from 'flarum/models/User'; + +import SuspendUserModal from 'suspend/components/SuspendUserModal'; + +app.initializers.add('suspend', () => { + User.prototype.canSuspend = Model.attribute('canSuspend'); + User.prototype.suspendUntil = Model.attribute('suspendUntil', Model.transformDate); + + extend(UserControls, 'moderationControls', (items, user) => { + if (user.canSuspend()) { + items.add('suspend', Button.component({ + children: 'Suspend', + icon: 'shield', + onclick: () => app.modal.show(new SuspendUserModal({user})) + })); + } + }); + + extend(User.prototype, 'badges', function(items) { + const until = this.suspendUntil(); + + if (new Date() < until) { + items.add('suspended', Badge.component({ + icon: 'times', + type: 'suspended', + label: 'Suspended' + })); + } + }); +}); diff --git a/extensions/suspend/less/admin/extension.less b/extensions/suspend/less/admin/extension.less new file mode 100644 index 000000000..e69de29bb diff --git a/extensions/suspend/less/forum/extension.less b/extensions/suspend/less/forum/extension.less new file mode 100644 index 000000000..dc3ff814f --- /dev/null +++ b/extensions/suspend/less/forum/extension.less @@ -0,0 +1,12 @@ +.Badge--suspended { + background: #888; +} +.SuspendUserModal-days-input { + margin-top: 5px; + + input { + width: 75px; + display: inline-block; + margin-right: 5px; + } +} diff --git a/extensions/suspend/locale/en.yml b/extensions/suspend/locale/en.yml new file mode 100644 index 000000000..028189c95 --- /dev/null +++ b/extensions/suspend/locale/en.yml @@ -0,0 +1,2 @@ +suspend: + # hello_world: "Hello, world!" diff --git a/extensions/suspend/migrations/2015_05_11_000000_add_suspended_to_users_table.php b/extensions/suspend/migrations/2015_05_11_000000_add_suspended_to_users_table.php new file mode 100644 index 000000000..16a173ab8 --- /dev/null +++ b/extensions/suspend/migrations/2015_05_11_000000_add_suspended_to_users_table.php @@ -0,0 +1,31 @@ +getSchemaBuilder()->table('users', function (Blueprint $table) { + $table->dateTime('suspended_until')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + app('db')->getSchemaBuilder()->table('users', function (Blueprint $table) { + $table->dropColumn('suspended_until'); + }); + } +} diff --git a/extensions/suspend/src/Extension.php b/extensions/suspend/src/Extension.php new file mode 100644 index 000000000..75bccab3e --- /dev/null +++ b/extensions/suspend/src/Extension.php @@ -0,0 +1,29 @@ +subscribe('Flarum\Suspend\Listeners\AddClientAssets'); + $events->subscribe('Flarum\Suspend\Listeners\AddApiAttributes'); + $events->subscribe('Flarum\Suspend\Listeners\PersistData'); + } + + /** + * Register the service provider. + * + * @return void + */ + public function register() + { + // + } +} diff --git a/extensions/suspend/src/Listeners/AddApiAttributes.php b/extensions/suspend/src/Listeners/AddApiAttributes.php new file mode 100755 index 000000000..0c6a21cff --- /dev/null +++ b/extensions/suspend/src/Listeners/AddApiAttributes.php @@ -0,0 +1,38 @@ +listen(ModelDates::class, [$this, 'addDates']); + $events->listen(ApiAttributes::class, [$this, 'addAttributes']); + } + + public function addDates(ModelDates $event) + { + if ($event->model instanceof User) { + $event->dates[] = 'suspend_until'; + } + } + + public function addAttributes(ApiAttributes $event) + { + if ($event->serializer instanceof UserSerializer) { + $canSuspend = $event->model->can($event->actor, 'suspend'); + + if ($canSuspend) { + $suspendUntil = $event->model->suspend_until; + + $event->attributes['suspendUntil'] = $suspendUntil ? $suspendUntil->toRFC3339String() : null; + } + + $event->attributes['canSuspend'] = $canSuspend; + } + } +} diff --git a/extensions/suspend/src/Listeners/AddClientAssets.php b/extensions/suspend/src/Listeners/AddClientAssets.php new file mode 100644 index 000000000..99d9b4ead --- /dev/null +++ b/extensions/suspend/src/Listeners/AddClientAssets.php @@ -0,0 +1,44 @@ +listen(RegisterLocales::class, [$this, 'addLocale']); + $events->listen(BuildClientView::class, [$this, 'addAssets']); + } + + public function addLocale(RegisterLocales $event) + { + $event->addTranslations('en', __DIR__.'/../../locale/en.yml'); + } + + public function addAssets(BuildClientView $event) + { + $event->forumAssets([ + __DIR__.'/../../js/forum/dist/extension.js', + __DIR__.'/../../less/forum/extension.less' + ]); + + $event->forumBootstrapper('suspend/main'); + + $event->forumTranslations([ + // 'suspend.hello_world' + ]); + + $event->adminAssets([ + __DIR__.'/../../js/admin/dist/extension.js', + __DIR__.'/../../less/admin/extension.less' + ]); + + $event->adminBootstrapper('suspend/main'); + + $event->adminTranslations([ + // 'suspend.hello_world' + ]); + } +} diff --git a/extensions/suspend/src/Listeners/PersistData.php b/extensions/suspend/src/Listeners/PersistData.php new file mode 100755 index 000000000..2acb059ef --- /dev/null +++ b/extensions/suspend/src/Listeners/PersistData.php @@ -0,0 +1,39 @@ +listen(UserWillBeSaved::class, [$this, 'whenUserWillBeSaved']); + $events->listen(GetUserGroups::class, [$this, 'revokePermissions']); + } + + public function whenUserWillBeSaved(UserWillBeSaved $event) + { + $attributes = array_get($event->data, 'attributes', []); + + if (array_key_exists('suspendUntil', $attributes)) { + $suspendUntil = $attributes['suspendUntil']; + $user = $event->user; + $actor = $event->actor; + + $user->assertCan($actor, 'suspend'); + + $user->suspend_until = new Carbon($suspendUntil); + } + } + + public function revokePermissions(GetUserGroups $event) + { + $suspendUntil = $event->user->suspend_until; + + if ($suspendUntil && $suspendUntil->gt(Carbon::now())) { + $event->groupIds = [Group::GUEST_ID]; + } + } +}