1
0
mirror of https://github.com/flarum/core.git synced 2025-07-31 13:40:20 +02:00

Initial commit

This commit is contained in:
Toby Zerner
2015-09-22 18:14:56 +09:30
commit 937b70bdf8
27 changed files with 740 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
# 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
[*.{diff,md}]
trim_trailing_whitespace = false
[*.php]
indent_size = 4

View File

@@ -0,0 +1,5 @@
**/bower_components/**/*
**/node_modules/**/*
vendor/**/*
**/Gulpfile.js
**/dist/**/*

View File

@@ -0,0 +1,175 @@
{
"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
},
"plugins": [
"react"
],
"rules": {
"react/jsx-uses-vars": 1,
/**
* 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
"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
}
}

4
extensions/approval/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/vendor
composer.phar
.DS_Store
Thumbs.db

View File

@@ -0,0 +1,5 @@
<?php
require __DIR__.'/vendor/autoload.php';
return 'Flarum\Approval\Extension';

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env bash
base=${PWD}
if [ ! -f flarum.json ]; then
echo "Could not find flarum.json file!"
exit 1
fi
extension=$(php <<CODE
<?php
\$flarum = json_decode(file_get_contents('flarum.json'), true);
echo array_key_exists('name', \$flarum) ? \$flarum['name'] : '';
CODE
)
release=/tmp/${extension}
rm -rf ${release}
mkdir ${release}
git archive --format zip --worktree-attributes HEAD > ${release}/release.zip
cd ${release}
unzip release.zip -d ./
rm release.zip
# Delete files
rm -rf ${release}/build.sh
# Install all Composer dependencies
composer install --prefer-dist --optimize-autoloader --ignore-platform-reqs --no-dev
cd "${release}/js"
if [ -f bower.json ]; then
bower install
fi
for app in forum admin; do
cd "${release}/js"
if [ -d $app ]; then
cd $app
if [ -f bower.json ]; then
bower install
fi
npm install
gulp --production
rm -rf node_modules bower_components
fi
done
rm -rf "${release}/extensions/${extension}/js/bower_components"
wait
# Finally, create the release archive
cd ${release}
find . -type d -exec chmod 0750 {} +
find . -type f -exec chmod 0644 {} +
chmod 0775 .
zip -r ${extension}.zip ./
mv ${extension}.zip ${base}/${extension}.zip

View File

@@ -0,0 +1,7 @@
{
"autoload": {
"psr-4": {
"Flarum\\Approval\\": "src/"
}
}
}

View File

@@ -0,0 +1,22 @@
{
"name": "approval",
"title": "Approval",
"description": "Make discussions and posts require moderator approval.",
"keywords": [],
"version": "0.1.0-beta.2",
"author": {
"name": "Toby Zerner",
"email": "toby@flarum.org",
"homepage": "http://tobyzerner.com"
},
"license": "MIT",
"require": {
"flarum": ">=0.1.0-beta.2",
"reports": ">=0.1.0-beta.2"
},
"icon": {
"name": "check",
"backgroundColor": "green",
"color": "#fff"
}
}

3
extensions/approval/js/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
bower_components
node_modules
dist

View File

@@ -0,0 +1,7 @@
var gulp = require('flarum-gulp');
gulp({
modules: {
'approval': 'src/**/*.js'
}
});

View File

@@ -0,0 +1,7 @@
{
"private": true,
"devDependencies": {
"gulp": "^3.8.11",
"flarum-gulp": "^0.1.0"
}
}

View File

@@ -0,0 +1,21 @@
import { extend } from 'flarum/extend';
import app from 'flarum/app';
import PermissionGrid from 'flarum/components/PermissionGrid';
app.initializers.add('approval', () => {
extend(PermissionGrid.prototype, 'replyItems', items => {
items.add('replyWithoutApproval', {
icon: 'check',
label: 'Reply without approval',
permission: 'discussion.replyWithoutApproval'
}, 95);
});
extend(PermissionGrid.prototype, 'moderateItems', items => {
items.add('approvePosts', {
icon: 'check',
label: 'Approve posts',
permission: 'discussion.approvePosts'
}, 65);
});
});

View File

@@ -0,0 +1,7 @@
var gulp = require('flarum-gulp');
gulp({
modules: {
'approval': 'src/**/*.js'
}
});

View File

@@ -0,0 +1,7 @@
{
"private": true,
"devDependencies": {
"gulp": "^3.8.11",
"flarum-gulp": "^0.1.0"
}
}

View File

@@ -0,0 +1,56 @@
import { extend, override } from 'flarum/extend';
import app from 'flarum/app';
import Discussion from 'flarum/models/Discussion';
import Post from 'flarum/models/Post';
import DiscussionListItem from 'flarum/components/DiscussionListItem';
import CommentPost from 'flarum/components/CommentPost';
import Button from 'flarum/components/Button';
import PostControls from 'flarum/utils/PostControls';
app.initializers.add('approval', () => {
Discussion.prototype.isApproved = Discussion.attribute('isApproved');
Post.prototype.isApproved = Post.attribute('isApproved');
Post.prototype.canApprove = Post.attribute('canApprove');
extend(DiscussionListItem.prototype, 'attrs', function(attrs) {
if (!this.props.discussion.isApproved()) {
attrs.className += ' DiscussionListItem--unapproved';
}
});
extend(CommentPost.prototype, 'attrs', function(attrs) {
if (!this.props.post.isApproved() && !this.props.post.isHidden()) {
attrs.className += ' CommentPost--unapproved';
}
});
extend(CommentPost.prototype, 'headerItems', function(items) {
if (!this.props.post.isApproved() && !this.props.post.isHidden()) {
items.add('unapproved', 'Awaiting Approval');
}
});
override(CommentPost.prototype, 'flagReason', function(original, flag) {
if (flag.type() === 'approval') {
return 'Awaiting approval';
}
return original(flag);
});
extend(PostControls, 'destructiveControls', function(items, post) {
if (!post.isApproved() && post.canApprove()) {
items.add('approve',
<Button icon="check" onclick={PostControls.approveAction.bind(post)}>
Approve
</Button>,
10
);
}
});
PostControls.approveAction = function() {
this.save({isApproved: true});
};
}, -10); // set initializer priority to run after reports

View File

@@ -0,0 +1,7 @@
.CommentPost--unapproved {
.Post-header,
.Post-body,
.Post-footer {
opacity: 0.5;
}
}

View File

@@ -0,0 +1,2 @@
approval:
# hello_world: "Hello, world!"

View File

@@ -0,0 +1,33 @@
<?php
namespace Flarum\Migrations\Approval;
use Illuminate\Database\Schema\Blueprint;
use Flarum\Migrations\Migration;
class AddIsApprovedToDiscussions extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$this->schema->table('discussions', function (Blueprint $table) {
$table->boolean('is_approved')->default(1);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
$this->schema->table('discussions', function (Blueprint $table) {
$table->dropColumn('is_approved');
});
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Flarum\Migrations\Approval;
use Illuminate\Database\Schema\Blueprint;
use Flarum\Migrations\Migration;
class AddIsApprovedToPosts extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$this->schema->table('posts', function (Blueprint $table) {
$table->boolean('is_approved')->default(1);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
$this->schema->table('posts', function (Blueprint $table) {
$table->dropColumn('is_approved');
});
}
}

View File

@@ -0,0 +1,31 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Approval\Events;
use Flarum\Core\Posts\Post;
class PostWasApproved
{
/**
* The post that was approved.
*
* @var Post
*/
public $post;
/**
* @param Post $post
*/
public function __construct(Post $post)
{
$this->post = $post;
}
}

View File

@@ -0,0 +1,16 @@
<?php namespace Flarum\Approval;
use Flarum\Support\Extension as BaseExtension;
use Illuminate\Events\Dispatcher;
class Extension extends BaseExtension
{
public function listen(Dispatcher $events)
{
$events->subscribe('Flarum\Approval\Listeners\AddClientAssets');
$events->subscribe('Flarum\Approval\Listeners\AddApiAttributes');
$events->subscribe('Flarum\Approval\Listeners\HideUnapprovedContent');
$events->subscribe('Flarum\Approval\Listeners\UnapproveNewContent');
$events->subscribe('Flarum\Approval\Listeners\ApproveContent');
}
}

View File

@@ -0,0 +1,26 @@
<?php namespace Flarum\Approval\Listeners;
use Flarum\Events\ApiAttributes;
use Flarum\Api\Serializers\DiscussionSerializer;
use Flarum\Api\Serializers\PostSerializer;
use Illuminate\Contracts\Events\Dispatcher;
class AddApiAttributes
{
public function subscribe(Dispatcher $events)
{
$events->listen(ApiAttributes::class, [$this, 'addApiAttributes']);
}
public function addApiAttributes(ApiAttributes $event)
{
if ($event->serializer instanceof DiscussionSerializer ||
$event->serializer instanceof PostSerializer) {
$event->attributes['isApproved'] = (bool) $event->model->is_approved;
}
if ($event->serializer instanceof PostSerializer) {
$event->attributes['canApprove'] = (bool) $event->model->discussion->can($event->actor, 'approvePosts');
}
}
}

View File

@@ -0,0 +1,44 @@
<?php namespace Flarum\Approval\Listeners;
use Flarum\Events\RegisterLocales;
use Flarum\Events\BuildClientView;
use Illuminate\Contracts\Events\Dispatcher;
class AddClientAssets
{
public function subscribe(Dispatcher $events)
{
$events->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('approval/main');
$event->forumTranslations([
// 'approval.hello_world'
]);
$event->adminAssets([
__DIR__.'/../../js/admin/dist/extension.js',
__DIR__.'/../../less/admin/extension.less'
]);
$event->adminBootstrapper('approval/main');
$event->adminTranslations([
// 'approval.hello_world'
]);
}
}

View File

@@ -0,0 +1,47 @@
<?php namespace Flarum\Approval\Listeners;
use Flarum\Events\PostWillBeSaved;
use Flarum\Approval\Events\PostWasApproved;
use Illuminate\Contracts\Events\Dispatcher;
class ApproveContent
{
public function subscribe(Dispatcher $events)
{
$events->listen(PostWillBeSaved::class, [$this, 'approvePost']);
$events->listen(PostWasApproved::class, [$this, 'approveDiscussion']);
}
public function approvePost(PostWillBeSaved $event)
{
$attributes = $event->data['attributes'];
$post = $event->post;
if (isset($attributes['isApproved'])) {
$post->assertCan($event->actor, 'approve');
$isApproved = (bool) $attributes['isApproved'];
} elseif (! empty($attributes['isHidden']) && $post->can($event->actor, 'approve')) {
$isApproved = true;
}
if (! empty($isApproved)) {
$post->is_approved = true;
$post->raise(new PostWasApproved($post));
}
}
public function approveDiscussion(PostWasApproved $event)
{
$post = $event->post;
$post->discussion->refreshCommentsCount();
$post->discussion->refreshLastPost();
if ($post->number == 1) {
$post->discussion->is_approved = true;
$post->discussion->save();
}
}
}

View File

@@ -0,0 +1,44 @@
<?php namespace Flarum\Approval\Listeners;
use Flarum\Events\ScopeModelVisibility;
use Flarum\Events\ScopePostVisibility;
use Flarum\Events\ScopeHiddenDiscussionVisibility;
use Flarum\Core\Discussions\Discussion;
use Illuminate\Contracts\Events\Dispatcher;
class HideUnapprovedContent
{
public function subscribe(Dispatcher $events)
{
$events->listen(ScopeModelVisibility::class, [$this, 'hideUnapprovedDiscussions']);
$events->listen(ScopePostVisibility::class, [$this, 'hideUnapprovedPosts']);
}
public function hideUnapprovedDiscussions(ScopeModelVisibility $event)
{
if ($event->model instanceof Discussion) {
$user = $event->actor;
if (! $user->hasPermission('discussion.editPosts')) {
$event->query->where(function ($query) use ($user) {
$query->where('discussions.is_approved', 1)
->orWhere('start_user_id', $user->id);
event(new ScopeHiddenDiscussionVisibility($query, $user, 'discussion.editPosts'));
});
}
}
}
public function hideUnapprovedPosts(ScopePostVisibility $event)
{
if ($event->discussion->can($event->actor, 'editPosts')) {
return;
}
$event->query->where(function ($query) use ($event) {
$query->where('posts.is_approved', 1)
->orWhere('user_id', $event->actor->id);
});
}
}

View File

@@ -0,0 +1,47 @@
<?php namespace Flarum\Approval\Listeners;
use Flarum\Events\PostWillBeSaved;
use Flarum\Flags\Flag;
use Illuminate\Contracts\Events\Dispatcher;
class UnapproveNewContent
{
private $savingPost;
public function subscribe(Dispatcher $events)
{
$events->listen(PostWillBeSaved::class, [$this, 'unapproveNewPosts']);
}
public function unapproveNewPosts(PostWillBeSaved $event)
{
$post = $event->post;
if (! $post->exists) {
if ($post->discussion->can($event->actor, 'replyWithoutApproval')) {
if ($post->is_approved === null) {
$post->is_approved = true;
}
return;
}
$post->is_approved = false;
$post->afterSave(function ($post) {
if ($post->number == 1) {
$post->discussion->is_approved = false;
$post->discussion->save();
}
$flag = new Flag;
$flag->post_id = $post->id;
$flag->type = 'approval';
$flag->time = time();
$flag->save();
});
}
}
}