mirror of
https://github.com/flarum/framework.git
synced 2025-02-21 09:11:40 +08:00
Merge branch 'master' into psr-7
Conflicts: src/Api/Actions/Discussions/IndexAction.php src/Api/Actions/SerializeAction.php src/Core/Formatter/FormatterManager.php src/Extend/ForumAssets.php src/Forum/Actions/IndexAction.php src/Forum/ForumServiceProvider.php
This commit is contained in:
commit
0262f45f57
@ -1,51 +1,22 @@
|
||||
var gulp = require('gulp');
|
||||
var livereload = require('gulp-livereload');
|
||||
var concat = require('gulp-concat');
|
||||
var argv = require('yargs').argv;
|
||||
var uglify = require('gulp-uglify');
|
||||
var gulpif = require('gulp-if');
|
||||
var merge = require('merge-stream');
|
||||
var babel = require('gulp-babel');
|
||||
var cached = require('gulp-cached');
|
||||
var remember = require('gulp-remember');
|
||||
var gulp = require('flarum-gulp');
|
||||
|
||||
var vendorFiles = [
|
||||
'../bower_components/loader.js/loader.js',
|
||||
'../bower_components/mithril/mithril.js',
|
||||
'../bower_components/jquery/dist/jquery.js',
|
||||
'../bower_components/moment/moment.js',
|
||||
'../bower_components/bootstrap/dist/js/bootstrap.js',
|
||||
'../bower_components/spin.js/spin.js',
|
||||
'../bower_components/spin.js/jquery.spin.js'
|
||||
];
|
||||
|
||||
var moduleFiles = [
|
||||
'src/**/*.js',
|
||||
'../lib/**/*.js'
|
||||
];
|
||||
var modulePrefix = 'flarum';
|
||||
|
||||
gulp.task('default', function() {
|
||||
return merge(
|
||||
gulp.src(vendorFiles),
|
||||
gulp.src(moduleFiles)
|
||||
.pipe(cached('scripts'))
|
||||
.pipe(babel({ modules: 'amd', moduleIds: true, moduleRoot: modulePrefix }))
|
||||
.pipe(remember('scripts'))
|
||||
)
|
||||
.pipe(concat('app.js'))
|
||||
.pipe(gulpif(argv.production, uglify()))
|
||||
.pipe(gulp.dest('dist'))
|
||||
.pipe(livereload());
|
||||
});
|
||||
|
||||
gulp.task('watch', ['default'], function () {
|
||||
livereload.listen();
|
||||
var watcher = gulp.watch(moduleFiles.concat(vendorFiles), ['default']);
|
||||
watcher.on('change', function (event) {
|
||||
if (event.type === 'deleted') {
|
||||
delete cached.caches.scripts[event.path];
|
||||
remember.forget('scripts', event.path);
|
||||
}
|
||||
});
|
||||
gulp({
|
||||
files: [
|
||||
'node_modules/babel-core/external-helpers.js',
|
||||
'../bower_components/loader.js/loader.js',
|
||||
'../bower_components/mithril/mithril.js',
|
||||
'../bower_components/jquery/dist/jquery.js',
|
||||
'../bower_components/moment/moment.js',
|
||||
'../bower_components/bootstrap/dist/js/bootstrap.js',
|
||||
'../bower_components/spin.js/spin.js',
|
||||
'../bower_components/spin.js/jquery.spin.js'
|
||||
],
|
||||
moduleFiles: [
|
||||
'src/**/*.js',
|
||||
'../lib/**/*.js'
|
||||
],
|
||||
bootstrapFiles: [],
|
||||
modulePrefix: 'flarum',
|
||||
externalHelpers: true,
|
||||
outputFile: 'dist/app.js'
|
||||
});
|
||||
|
@ -1,14 +1,8 @@
|
||||
{
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"gulp": "^3.8.11",
|
||||
"gulp-babel": "^5.1.0",
|
||||
"gulp-cached": "^1.0.4",
|
||||
"gulp-concat": "^2.5.2",
|
||||
"gulp-if": "^1.2.5",
|
||||
"gulp-livereload": "^3.8.0",
|
||||
"gulp-remember": "^0.3.0",
|
||||
"gulp-uglify": "^1.2.0",
|
||||
"merge-stream": "^0.1.7",
|
||||
"yargs": "^3.7.2"
|
||||
"flarum-gulp": "git+https://github.com/flarum/gulp.git",
|
||||
"babel-core": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,53 +1,24 @@
|
||||
var gulp = require('gulp');
|
||||
var livereload = require('gulp-livereload');
|
||||
var concat = require('gulp-concat');
|
||||
var argv = require('yargs').argv;
|
||||
var uglify = require('gulp-uglify');
|
||||
var gulpif = require('gulp-if');
|
||||
var merge = require('merge-stream');
|
||||
var babel = require('gulp-babel');
|
||||
var cached = require('gulp-cached');
|
||||
var remember = require('gulp-remember');
|
||||
var gulp = require('flarum-gulp');
|
||||
|
||||
var vendorFiles = [
|
||||
'../bower_components/loader.js/loader.js',
|
||||
'../bower_components/mithril/mithril.js',
|
||||
'../bower_components/jquery/dist/jquery.js',
|
||||
'../bower_components/jquery.hotkeys/jquery.hotkeys.js',
|
||||
'../bower_components/color-thief/js/color-thief.js',
|
||||
'../bower_components/moment/moment.js',
|
||||
'../bower_components/bootstrap/dist/js/bootstrap.js',
|
||||
'../bower_components/spin.js/spin.js',
|
||||
'../bower_components/spin.js/jquery.spin.js'
|
||||
];
|
||||
|
||||
var moduleFiles = [
|
||||
'src/**/*.js',
|
||||
'../lib/**/*.js'
|
||||
];
|
||||
var modulePrefix = 'flarum';
|
||||
|
||||
gulp.task('default', function() {
|
||||
return merge(
|
||||
gulp.src(vendorFiles),
|
||||
gulp.src(moduleFiles)
|
||||
.pipe(cached('scripts'))
|
||||
.pipe(babel({ modules: 'amd', moduleIds: true, moduleRoot: modulePrefix }))
|
||||
.pipe(remember('scripts'))
|
||||
)
|
||||
.pipe(concat('app.js'))
|
||||
.pipe(gulpif(argv.production, uglify()))
|
||||
.pipe(gulp.dest('dist'))
|
||||
.pipe(livereload());
|
||||
});
|
||||
|
||||
gulp.task('watch', ['default'], function () {
|
||||
livereload.listen();
|
||||
var watcher = gulp.watch(moduleFiles.concat(vendorFiles), ['default']);
|
||||
watcher.on('change', function (event) {
|
||||
if (event.type === 'deleted') {
|
||||
delete cached.caches.scripts[event.path];
|
||||
remember.forget('scripts', event.path);
|
||||
}
|
||||
});
|
||||
gulp({
|
||||
files: [
|
||||
'node_modules/babel-core/external-helpers.js',
|
||||
'../bower_components/loader.js/loader.js',
|
||||
'../bower_components/mithril/mithril.js',
|
||||
'../bower_components/jquery/dist/jquery.js',
|
||||
'../bower_components/jquery.hotkeys/jquery.hotkeys.js',
|
||||
'../bower_components/color-thief/js/color-thief.js',
|
||||
'../bower_components/moment/moment.js',
|
||||
'../bower_components/bootstrap/dist/js/bootstrap.js',
|
||||
'../bower_components/spin.js/spin.js',
|
||||
'../bower_components/spin.js/jquery.spin.js'
|
||||
],
|
||||
moduleFiles: [
|
||||
'src/**/*.js',
|
||||
'../lib/**/*.js'
|
||||
],
|
||||
bootstrapFiles: [],
|
||||
modulePrefix: 'flarum',
|
||||
externalHelpers: true,
|
||||
outputFile: 'dist/app.js'
|
||||
});
|
||||
|
@ -1,14 +1,8 @@
|
||||
{
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"gulp": "^3.8.11",
|
||||
"gulp-babel": "^5.1.0",
|
||||
"gulp-cached": "^1.0.4",
|
||||
"gulp-concat": "^2.5.2",
|
||||
"gulp-if": "^1.2.5",
|
||||
"gulp-livereload": "^3.8.0",
|
||||
"gulp-remember": "^0.3.0",
|
||||
"gulp-uglify": "^1.2.0",
|
||||
"merge-stream": "^0.1.7",
|
||||
"yargs": "^3.7.2"
|
||||
"flarum-gulp": "git+https://github.com/flarum/gulp.git",
|
||||
"babel-core": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ export default class ChangeEmailModal extends FormModal {
|
||||
return super.view({
|
||||
className: 'modal-sm change-email-modal',
|
||||
title: 'Change Email',
|
||||
body: this.success()
|
||||
body: m('div.form-centered', this.success()
|
||||
? [
|
||||
m('p.help-text', 'We\'ve sent a confirmation email to ', m('strong', this.email()), '. If it doesn\'t arrive soon, check your spam folder.'),
|
||||
m('div.form-group', [
|
||||
@ -37,7 +37,7 @@ export default class ChangeEmailModal extends FormModal {
|
||||
m('div.form-group', [
|
||||
m('button.btn.btn-primary.btn-block[type=submit]', {disabled}, 'Save Changes')
|
||||
])
|
||||
]
|
||||
])
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -5,12 +5,12 @@ export default class ChangePasswordModal extends FormModal {
|
||||
return super.view({
|
||||
className: 'modal-sm change-password-modal',
|
||||
title: 'Change Password',
|
||||
body: [
|
||||
body: m('div.form-centered', [
|
||||
m('p.help-text', 'Click the button below and check your email for a link to change your password.'),
|
||||
m('div.form-group', [
|
||||
m('button.btn.btn-primary.btn-block[type=submit]', {disabled: this.loading()}, 'Send Password Reset Email')
|
||||
])
|
||||
]
|
||||
])
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -117,7 +117,7 @@ class Composer extends Component {
|
||||
this.updateHeight();
|
||||
|
||||
var scrollTop = $(window).scrollTop();
|
||||
this.updateBodyPadding(false, scrollTop > 0 && scrollTop + $(window).height() >= $(document).height());
|
||||
this.updateBodyPadding(scrollTop > 0 && scrollTop + $(window).height() >= $(document).height());
|
||||
|
||||
localStorage.setItem('composerHeight', height);
|
||||
}
|
||||
@ -136,9 +136,9 @@ class Composer extends Component {
|
||||
var $composer = this.$().stop(true);
|
||||
var oldHeight = $composer.is(':visible') ? $composer.outerHeight() : 0;
|
||||
|
||||
if (this.position() !== Composer.PositionEnum.HIDDEN) {
|
||||
m.redraw(true);
|
||||
}
|
||||
var scrollTop = $(window).scrollTop();
|
||||
|
||||
m.redraw(true);
|
||||
|
||||
this.$().height(this.computedHeight());
|
||||
var newHeight = $composer.outerHeight();
|
||||
@ -167,7 +167,8 @@ class Composer extends Component {
|
||||
}
|
||||
|
||||
if (this.position() !== Composer.PositionEnum.FULLSCREEN) {
|
||||
this.updateBodyPadding(true, anchorToBottom);
|
||||
this.updateBodyPadding();
|
||||
$('html, body').scrollTop(anchorToBottom ? $(document).height() : scrollTop);
|
||||
} else {
|
||||
this.component.focus();
|
||||
}
|
||||
@ -182,18 +183,11 @@ class Composer extends Component {
|
||||
// Update the amount of padding-bottom on the body so that the page's
|
||||
// content will still be visible above the composer when the page is
|
||||
// scrolled right to the bottom.
|
||||
updateBodyPadding(animate, anchorToBottom) {
|
||||
var func = animate ? 'animate' : 'css';
|
||||
var paddingBottom = this.position() !== Composer.PositionEnum.HIDDEN ? this.computedHeight() - parseInt($('#page').css('padding-bottom')) : 0;
|
||||
$('#content')[func]({paddingBottom}, 'fast');
|
||||
|
||||
if (anchorToBottom) {
|
||||
if (animate) {
|
||||
$('html, body').stop(true).animate({scrollTop: $(document).height()}, 'fast');
|
||||
} else {
|
||||
$('html, body').scrollTop($(document).height());
|
||||
}
|
||||
}
|
||||
updateBodyPadding() {
|
||||
var paddingBottom = this.position() !== Composer.PositionEnum.HIDDEN && this.position() !== Composer.PositionEnum.MINIMIZED
|
||||
? this.computedHeight() - parseInt($('#page').css('padding-bottom'))
|
||||
: 0;
|
||||
$('#content').css({paddingBottom});
|
||||
}
|
||||
|
||||
// Update the height of the stuff inside of the composer. There should be
|
||||
|
@ -11,7 +11,7 @@ export default class DeleteAccountModal extends FormModal {
|
||||
return super.view({
|
||||
className: 'modal-sm change-password-modal',
|
||||
title: 'Delete Account',
|
||||
body: [
|
||||
body: m('div.form-centered', [
|
||||
m('p.help-text', 'Hold up there skippy! If you delete your account, there\'s no going back. All of your posts will be kept, but no longer associated with your account.'),
|
||||
m('div.form-group', [
|
||||
m('input.form-control[name=confirm][placeholder=Type "DELETE" to proceed]', {oninput: m.withAttr('value', this.confirmation)})
|
||||
@ -19,7 +19,7 @@ export default class DeleteAccountModal extends FormModal {
|
||||
m('div.form-group', [
|
||||
m('button.btn.btn-primary.btn-block[type=submit]', {disabled: this.loading() || this.confirmation() != 'DELETE'}, 'Delete Account')
|
||||
])
|
||||
]
|
||||
])
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -41,12 +41,32 @@ export default class DiscussionComposer extends ComposerBody {
|
||||
if (empty) { $this.val(''); }
|
||||
});
|
||||
setTimeout(() => $(element).trigger('input'));
|
||||
},
|
||||
onkeydown: (e) => {
|
||||
if (e.which === 13) { // return
|
||||
e.preventDefault();
|
||||
this.editor.setSelectionRange(0, 0);
|
||||
}
|
||||
m.redraw.strategy('none');
|
||||
}
|
||||
})));
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
onload(element) {
|
||||
super.onload(element);
|
||||
|
||||
this.editor.$('textarea').keydown((e) => {
|
||||
if (e.which === 8 && e.target.selectionStart == 0 && e.target.selectionEnd == 0) { // Backspace
|
||||
e.preventDefault();
|
||||
var title = this.$(':input:enabled:visible:first')[0];
|
||||
title.focus();
|
||||
title.selectionStart = title.selectionEnd = title.value.length;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
preventExit() {
|
||||
return (this.title() || this.content()) && !confirm(this.props.confirmExit);
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ export default class ForgotPasswordModal extends FormModal {
|
||||
return super.view({
|
||||
className: 'modal-sm forgot-password',
|
||||
title: 'Forgot Password',
|
||||
body: this.success()
|
||||
body: m('div.form-centered', this.success()
|
||||
? [
|
||||
m('p.help-text', 'We\'ve sent you an email containing a link to reset your password. Check your spam folder if you don\'t receive it within the next minute or two.'),
|
||||
m('div.form-group', [
|
||||
@ -34,7 +34,7 @@ export default class ForgotPasswordModal extends FormModal {
|
||||
m('div.form-group', [
|
||||
m('button.btn.btn-primary.btn-block[type=submit]', {disabled: this.loading()}, 'Recover Password')
|
||||
])
|
||||
]
|
||||
])
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -23,9 +23,7 @@ export default class FormModal extends Component {
|
||||
m('form', {onsubmit: this.onsubmit.bind(this)}, [
|
||||
m('div.modal-header', m('h3.title-control', options.title)),
|
||||
alert ? m('div.modal-alert', alert) : '',
|
||||
m('div.modal-body', [
|
||||
m('div.form-centered', options.body)
|
||||
]),
|
||||
m('div.modal-body', options.body),
|
||||
options.footer ? m('div.modal-footer', options.footer) : ''
|
||||
])
|
||||
]),
|
||||
|
@ -160,7 +160,7 @@ export default class IndexPage extends Component {
|
||||
items.add('sort',
|
||||
SelectInput.component({
|
||||
options: sortOptions,
|
||||
value: this.params.sort,
|
||||
value: this.params().sort,
|
||||
onchange: this.reorder.bind(this)
|
||||
})
|
||||
);
|
||||
@ -271,6 +271,7 @@ export default class IndexPage extends Component {
|
||||
$('body').addClass('index-page');
|
||||
context.onunload = function() {
|
||||
$('body').removeClass('index-page');
|
||||
$('.global-page').css('min-height', '');
|
||||
};
|
||||
|
||||
app.setTitle('');
|
||||
@ -339,16 +340,30 @@ export default class IndexPage extends Component {
|
||||
/**
|
||||
* Initialize the composer for a new discussion.
|
||||
*
|
||||
* @todo return a promise
|
||||
* @return void
|
||||
* @return {Promise}
|
||||
*/
|
||||
newDiscussion() {
|
||||
var deferred = m.deferred();
|
||||
|
||||
if (app.session.user()) {
|
||||
app.composer.load(new DiscussionComposer({ user: app.session.user() }));
|
||||
app.composer.show();
|
||||
return true;
|
||||
this.composeNewDiscussion(deferred);
|
||||
} else {
|
||||
app.modal.show(
|
||||
new LoginModal({ onlogin: this.composeNewDiscussion.bind(this, deferred) })
|
||||
);
|
||||
}
|
||||
app.modal.show(new LoginModal({ onlogin: this.newDiscussion.bind(this) }));
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
composeNewDiscussion(deferred) {
|
||||
// @todo check global permissions
|
||||
var component = new DiscussionComposer({ user: app.session.user() });
|
||||
app.composer.load(component);
|
||||
app.composer.show();
|
||||
deferred.resolve(component);
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -18,7 +18,7 @@ export default class LoginModal extends FormModal {
|
||||
return super.view({
|
||||
className: 'modal-sm login-modal',
|
||||
title: 'Log In',
|
||||
body: [
|
||||
body: m('div.form-centered', [
|
||||
m('div.form-group', [
|
||||
m('input.form-control[name=email][placeholder=Username or Email]', {value: this.email(), onchange: m.withAttr('value', this.email), disabled: this.loading()})
|
||||
]),
|
||||
@ -28,7 +28,7 @@ export default class LoginModal extends FormModal {
|
||||
m('div.form-group', [
|
||||
m('button.btn.btn-primary.btn-block[type=submit]', {disabled: this.loading()}, 'Log In')
|
||||
])
|
||||
],
|
||||
]),
|
||||
footer: [
|
||||
m('p.forgot-password-link', m('a[href=javascript:;]', {onclick: () => {
|
||||
var email = this.email();
|
||||
|
@ -12,14 +12,14 @@ export default class PostPreview extends Component {
|
||||
var excerpt = post.contentPlain();
|
||||
var start = 0;
|
||||
|
||||
if (highlight) {
|
||||
if (this.props.highlight) {
|
||||
var regexp = new RegExp(this.props.highlight, 'gi');
|
||||
start = Math.max(0, excerpt.search(regexp) - 100);
|
||||
}
|
||||
|
||||
excerpt = (start > 0 ? '...' : '')+excerpt.substring(start, start + 200)+(excerpt.length > start + 200 ? '...' : '');
|
||||
|
||||
if (highlight) {
|
||||
if (this.props.highlight) {
|
||||
excerpt = highlight(excerpt, regexp);
|
||||
}
|
||||
|
||||
|
@ -134,7 +134,7 @@ export default class PostScrubber extends Component {
|
||||
// properties to a 'default' state. These values reflect what would be
|
||||
// seen if the browser were scrolled right up to the top of the page,
|
||||
// and the viewport had a height of 0.
|
||||
var $items = stream.$('> .item');
|
||||
var $items = stream.$('> .item[data-index]');
|
||||
var index = $items.first().data('index');
|
||||
var visible = 0;
|
||||
var period = '';
|
||||
|
@ -88,7 +88,7 @@ class PostStream extends mixin(Component, evented) {
|
||||
stream is not visible.
|
||||
*/
|
||||
pushPost(post) {
|
||||
if (this.visibleEnd >= this.count() - 1) {
|
||||
if (this.visibleEnd >= this.count() - 1 && this.posts.indexOf(post) === -1) {
|
||||
this.posts.push(post);
|
||||
this.visibleEnd++;
|
||||
}
|
||||
@ -295,7 +295,7 @@ class PostStream extends mixin(Component, evented) {
|
||||
var redraw = () => {
|
||||
if (start < this.visibleStart || end > this.visibleEnd) return;
|
||||
|
||||
var anchorIndex = backwards ? this.visibleEnd - 1 : this.visibleStart;
|
||||
var anchorIndex = backwards && $(window).scrollTop() > 0 ? this.visibleEnd - 1 : this.visibleStart;
|
||||
anchorScroll(this.$('.item[data-index='+anchorIndex+']'), () => m.redraw(true));
|
||||
|
||||
this.unpause();
|
||||
@ -422,7 +422,7 @@ class PostStream extends mixin(Component, evented) {
|
||||
scrollToIndex(index, noAnimation, bottom) {
|
||||
var $item = this.$('.item[data-index='+index+']');
|
||||
|
||||
return this.scrollToItem($item, noAnimation, true, true);
|
||||
return this.scrollToItem($item, noAnimation, true, bottom);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -23,7 +23,7 @@ export default class PostedActivity extends Component {
|
||||
near: post.number()
|
||||
}), config: m.route}, [
|
||||
m('ul.list-inline', listItems(this.headerItems().toArray())),
|
||||
m('div.body', m.trust(post.excerpt()))
|
||||
m('div.body', m.trust(post.contentPlain().substring(0, 200)))
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ export default class SignupModal extends FormModal {
|
||||
var vdom = super.view({
|
||||
className: 'modal-sm signup-modal'+(welcomeUser ? ' signup-modal-success' : ''),
|
||||
title: 'Sign Up',
|
||||
body: [
|
||||
body: m('div.form-centered', [
|
||||
m('div.form-group', [
|
||||
m('input.form-control[name=username][placeholder=Username]', {value: this.username(), onchange: m.withAttr('value', this.username), disabled: this.loading()})
|
||||
]),
|
||||
@ -35,7 +35,7 @@ export default class SignupModal extends FormModal {
|
||||
m('div.form-group', [
|
||||
m('button.btn.btn-primary.btn-block[type=submit]', {disabled: this.loading()}, 'Sign Up')
|
||||
])
|
||||
],
|
||||
]),
|
||||
footer: [
|
||||
m('p.log-in-link', [
|
||||
'Already have an account? ',
|
||||
|
@ -1,5 +1,5 @@
|
||||
import Component from 'flarum/component';
|
||||
import humanTime from 'flarum/utils/human-time';
|
||||
import humanTime from 'flarum/helpers/human-time';
|
||||
import username from 'flarum/helpers/username';
|
||||
|
||||
/**
|
||||
@ -16,10 +16,13 @@ export default class TerminalPost extends Component {
|
||||
var discussion = this.props.discussion;
|
||||
var lastPost = this.props.lastPost && discussion.repliesCount();
|
||||
|
||||
var user = discussion[lastPost ? 'lastUser' : 'startUser']();
|
||||
var time = discussion[lastPost ? 'lastTime' : 'startTime']();
|
||||
|
||||
return m('span', [
|
||||
username(discussion[lastPost ? 'lastUser' : 'startUser']()),
|
||||
username(user),
|
||||
lastPost ? ' replied ' : ' started ',
|
||||
m('time', humanTime(discussion[lastPost ? 'lastTime' : 'startTime']()))
|
||||
humanTime(time)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
@ -87,6 +87,8 @@ export default function(app) {
|
||||
}
|
||||
|
||||
if (this.canDelete()) {
|
||||
items.add('separator', Separator.component());
|
||||
|
||||
items.add('delete', ActionButton.component({ icon: 'times', label: 'Delete', onclick: this.deleteAction.bind(this) }));
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,12 @@
|
||||
import Composer from 'flarum/components/composer';
|
||||
import ReplyComposer from 'flarum/components/reply-composer';
|
||||
import DiscussionPage from 'flarum/components/discussion-page';
|
||||
|
||||
export default function(app) {
|
||||
app.composingReplyTo = function(discussion) {
|
||||
return this.composer.component instanceof ReplyComposer &&
|
||||
this.composer.component.props.discussion === discussion;
|
||||
this.composer.component.props.discussion === discussion &&
|
||||
this.composer.position() !== Composer.PositionEnum.HIDDEN;
|
||||
};
|
||||
|
||||
app.viewingDiscussion = function(discussion) {
|
||||
|
@ -30,7 +30,14 @@ export default class Model {
|
||||
if (data.links) {
|
||||
for (var i in data.links) {
|
||||
var model = data.links[i];
|
||||
data.links[i] = {linkage: {type: model.data().type, id: model.data().id}};
|
||||
var linkage = model => {
|
||||
return {type: model.data().type, id: model.data().id};
|
||||
};
|
||||
if (model instanceof Array) {
|
||||
data.links[i] = {linkage: model.map(linkage)};
|
||||
} else {
|
||||
data.links[i] = {linkage: linkage(model)};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,11 @@ class Discussion extends Model {
|
||||
}
|
||||
|
||||
if (newData.links && newData.links.addedPosts) {
|
||||
[].push.apply(posts.linkage, newData.links.addedPosts.linkage);
|
||||
newData.links.addedPosts.linkage.forEach(linkage => {
|
||||
if (posts.linkage[posts.linkage.length - 1].id != linkage.id) {
|
||||
posts.linkage.push(linkage);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import ItemList from 'flarum/utils/item-list';
|
||||
import Alert from 'flarum/components/alert';
|
||||
import ServerError from 'flarum/utils/server-error';
|
||||
import Translator from 'flarum/utils/translator';
|
||||
|
||||
class App {
|
||||
constructor() {
|
||||
this.initializers = new ItemList();
|
||||
this.translator = new Translator();
|
||||
this.cache = {};
|
||||
this.serverError = null;
|
||||
}
|
||||
@ -55,6 +57,10 @@ class App {
|
||||
var queryString = m.route.buildQueryString(params);
|
||||
return url+(queryString ? '?'+queryString : '');
|
||||
}
|
||||
|
||||
translate(key, input) {
|
||||
return this.translator.translate(key, input);
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
@ -41,21 +41,17 @@ export default class ItemList {
|
||||
addItems('push', false);
|
||||
addItems('push', 'last');
|
||||
|
||||
items = items.filter(function(item) {
|
||||
items.forEach(item => {
|
||||
var key = item.position.before || item.position.after;
|
||||
var type = item.position.before ? 'before' : 'after';
|
||||
if (key) {
|
||||
var index = array.indexOf(this[key]);
|
||||
if (index === -1) {
|
||||
console.log("Can't find item with key '"+key+"' to insert "+type+", inserting at end instead");
|
||||
return true;
|
||||
} else {
|
||||
array.splice(array.indexOf(this[key]) + (type === 'after' ? 1 : 0), 0, item);
|
||||
index = type === 'before' ? 0 : array.length;
|
||||
}
|
||||
array.splice(index + (type === 'after' ? 1 : 0), 0, item);
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
array = array.concat(items);
|
||||
});
|
||||
|
||||
return array.map((item) => item.content);
|
||||
}
|
||||
|
32
framework/core/js/lib/utils/translator.js
Normal file
32
framework/core/js/lib/utils/translator.js
Normal file
@ -0,0 +1,32 @@
|
||||
export default class Translator {
|
||||
constructor() {
|
||||
this.translations = {};
|
||||
}
|
||||
|
||||
plural(count) {
|
||||
return count == 1 ? 'one' : 'other';
|
||||
}
|
||||
|
||||
translate(key, input) {
|
||||
var parts = key.split('.');
|
||||
var translation = this.translations;
|
||||
|
||||
parts.forEach(function(part) {
|
||||
translation = translation && translation[part];
|
||||
});
|
||||
|
||||
if (typeof translation === 'object' && typeof input.count !== 'undefined') {
|
||||
translation = translation[this.plural(input.count)];
|
||||
}
|
||||
|
||||
if (typeof translation === 'string') {
|
||||
for (var i in input) {
|
||||
translation = translation.replace(new RegExp('{'+i+'}', 'gi'), input[i]);
|
||||
}
|
||||
|
||||
return translation;
|
||||
} else {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
@ -210,10 +210,9 @@
|
||||
& .title {
|
||||
margin: 0 0 6px;
|
||||
line-height: 1.3;
|
||||
color: @fl-secondary-color;
|
||||
color: @fl-body-heading-color;
|
||||
}
|
||||
&.unread .title {
|
||||
color: @fl-body-heading-color;
|
||||
font-weight: bold;
|
||||
}
|
||||
& .info {
|
||||
|
@ -21,8 +21,8 @@
|
||||
@fl-body-secondary-color: hsl(@fl-secondary-hue, min(50%, @fl-secondary-sat), 93%);
|
||||
|
||||
@fl-body-bg: #fff;
|
||||
@fl-body-color: #444;
|
||||
@fl-body-muted-color: hsl(@fl-secondary-hue, min(25%, @fl-secondary-sat), 66%);
|
||||
@fl-body-color: #333;
|
||||
@fl-body-muted-color: hsl(@fl-secondary-hue, min(25%, @fl-secondary-sat), 60%);
|
||||
@fl-body-muted-more-color: #bbb;
|
||||
@fl-shadow-color: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
3
framework/core/locale/en/config.js
Normal file
3
framework/core/locale/en/config.js
Normal file
@ -0,0 +1,3 @@
|
||||
app.translator.plural = function(count) {
|
||||
return count == 1 ? 'one' : 'other';
|
||||
};
|
7
framework/core/locale/en/config.php
Normal file
7
framework/core/locale/en/config.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'plural' => function ($count) {
|
||||
return $count == 1 ? 'one' : 'other';
|
||||
}
|
||||
];
|
2
framework/core/locale/en/translations.yml
Normal file
2
framework/core/locale/en/translations.yml
Normal file
@ -0,0 +1,2 @@
|
||||
core:
|
||||
|
@ -15,7 +15,6 @@ class CreateUsersDiscussionsTable extends Migration
|
||||
public function up()
|
||||
{
|
||||
Schema::create('users_discussions', function (Blueprint $table) {
|
||||
|
||||
$table->integer('user_id')->unsigned();
|
||||
$table->integer('discussion_id')->unsigned();
|
||||
$table->dateTime('read_time')->nullable();
|
||||
|
@ -20,6 +20,7 @@ class CreateUsersTable extends Migration
|
||||
$table->string('email', 150)->unique();
|
||||
$table->boolean('is_activated')->default(0);
|
||||
$table->string('password', 100);
|
||||
$table->string('locale', 10)->default('en');
|
||||
$table->text('bio')->nullable();
|
||||
$table->text('bio_html')->nullable();
|
||||
$table->string('avatar_path', 100)->nullable();
|
||||
|
@ -84,15 +84,11 @@ class IndexAction extends SerializeCollectionAction
|
||||
$load = array_merge($request->include, ['state']);
|
||||
$results = $this->searcher->search($criteria, $request->limit, $request->offset, $load);
|
||||
|
||||
if (($total = $results->getTotal()) !== null) {
|
||||
$document->addMeta('total', $total);
|
||||
}
|
||||
|
||||
static::addPaginationLinks(
|
||||
$document,
|
||||
$request,
|
||||
$this->url->toRoute('flarum.api.discussions.index'),
|
||||
$total ?: $results->areMoreResults()
|
||||
$results->areMoreResults()
|
||||
);
|
||||
|
||||
return $results->getDiscussions();
|
||||
|
@ -93,7 +93,7 @@ class ShowAction extends SerializeResourceAction
|
||||
|
||||
$discussion = $this->discussions->findOrFail($request->get('id'), $user);
|
||||
|
||||
$discussion->posts_ids = $discussion->posts()->whereCan($user, 'view')->get(['id'])->fetch('id')->all();
|
||||
$discussion->posts_ids = $discussion->visiblePosts($user)->lists('id');
|
||||
|
||||
if (in_array('posts', $request->include)) {
|
||||
$length = strlen($prefix = 'posts.');
|
||||
|
29
framework/core/src/Api/Actions/Forum/ShowAction.php
Normal file
29
framework/core/src/Api/Actions/Forum/ShowAction.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php namespace Flarum\Api\Actions\Forum;
|
||||
|
||||
use Flarum\Core\Models\Forum;
|
||||
use Flarum\Api\Actions\SerializeResourceAction;
|
||||
use Flarum\Api\JsonApiRequest;
|
||||
use Flarum\Api\JsonApiResponse;
|
||||
|
||||
class ShowAction extends SerializeResourceAction
|
||||
{
|
||||
/**
|
||||
* The name of the serializer class to output results with.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $serializer = 'Flarum\Api\Serializers\ForumSerializer';
|
||||
|
||||
/**
|
||||
* Get the forum, ready to be serialized and assigned to the JsonApi
|
||||
* response.
|
||||
*
|
||||
* @param \Flarum\Api\JsonApiRequest $request
|
||||
* @param \Flarum\Api\JsonApiResponse $response
|
||||
* @return \Flarum\Core\Models\Forum
|
||||
*/
|
||||
protected function data(JsonApiRequest $request, JsonApiResponse $response)
|
||||
{
|
||||
return app('flarum.forum');
|
||||
}
|
||||
}
|
@ -1,11 +1,12 @@
|
||||
<?php namespace Flarum\Api\Actions;
|
||||
|
||||
use Flarum\Api\Events\WillRespond;
|
||||
use Flarum\Api\Request;
|
||||
use Flarum\Api\JsonApiRequest;
|
||||
use Flarum\Api\JsonApiResponse;
|
||||
use Tobscure\JsonApi\Criteria;
|
||||
use Tobscure\JsonApi\Document;
|
||||
use Tobscure\JsonApi\SerializerInterface;
|
||||
use Tobscure\JsonApi\Criteria;
|
||||
|
||||
abstract class SerializeAction extends JsonApiAction
|
||||
{
|
||||
@ -68,15 +69,18 @@ abstract class SerializeAction extends JsonApiAction
|
||||
public function respond(Request $request)
|
||||
{
|
||||
$request = static::buildJsonApiRequest($request);
|
||||
|
||||
$document = new Document();
|
||||
|
||||
$data = $this->data($request, $document);
|
||||
|
||||
$serializer = new static::$serializer($request->actor, $request->include, $request->link);
|
||||
|
||||
$document->setData($this->serialize($serializer, $data));
|
||||
$response = new JsonApiResponse($document);
|
||||
|
||||
return new JsonApiResponse($document);
|
||||
event(new WillRespond($this, $data, $request, $response));
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -29,6 +29,8 @@ class ShowAction extends SerializeResourceAction
|
||||
'groups' => true
|
||||
];
|
||||
|
||||
public static $link = [];
|
||||
|
||||
/**
|
||||
* Instantiate the action.
|
||||
*
|
||||
|
20
framework/core/src/Api/Events/WillRespond.php
Normal file
20
framework/core/src/Api/Events/WillRespond.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php namespace Flarum\Api\Events;
|
||||
|
||||
class WillRespond
|
||||
{
|
||||
public $action;
|
||||
|
||||
public $data;
|
||||
|
||||
public $request;
|
||||
|
||||
public $response;
|
||||
|
||||
public function __construct($action, &$data, $request, $response)
|
||||
{
|
||||
$this->action = $action;
|
||||
$this->data = &$data;
|
||||
$this->request = $request;
|
||||
$this->response = $response;
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ use Tobscure\JsonApi\SerializerAbstract;
|
||||
use Flarum\Api\Events\SerializeAttributes;
|
||||
use Flarum\Api\Events\SerializeRelationship;
|
||||
use Flarum\Support\Actor;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
@ -54,7 +55,7 @@ abstract class BaseSerializer extends SerializerAbstract
|
||||
$data = $relation($model, $include);
|
||||
} else {
|
||||
if ($include) {
|
||||
$data = !is_null($model->$relation) ? $model->$relation : $model->$relation()->getResults();
|
||||
$data = $model->getRelation($relation);
|
||||
} elseif ($many) {
|
||||
$relationIds = $relation.'_ids';
|
||||
$data = $model->$relationIds ?: $model->$relation()->get(['id'])->fetch('id')->all();
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
class DiscussionBasicSerializer extends BaseSerializer
|
||||
{
|
||||
protected static $relationships = [];
|
||||
|
||||
/**
|
||||
* The resource type.
|
||||
*
|
||||
|
31
framework/core/src/Api/Serializers/ForumSerializer.php
Normal file
31
framework/core/src/Api/Serializers/ForumSerializer.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php namespace Flarum\Api\Serializers;
|
||||
|
||||
class ForumSerializer extends BaseSerializer
|
||||
{
|
||||
/**
|
||||
* The resource type.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'forums';
|
||||
|
||||
protected function id($forum)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize attributes of a Forum model for JSON output.
|
||||
*
|
||||
* @param Forum $forum The Forum model to serialize.
|
||||
* @return array
|
||||
*/
|
||||
protected function attributes($forum)
|
||||
{
|
||||
$attributes = [
|
||||
'title' => $forum->title
|
||||
];
|
||||
|
||||
return $this->extendAttributes($forum, $attributes);
|
||||
}
|
||||
}
|
@ -2,6 +2,8 @@
|
||||
|
||||
class PostBasicSerializer extends BaseSerializer
|
||||
{
|
||||
protected static $relationships = [];
|
||||
|
||||
/**
|
||||
* The resource type.
|
||||
*
|
||||
|
60
framework/core/src/Assets/AssetManager.php
Normal file
60
framework/core/src/Assets/AssetManager.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php namespace Flarum\Assets;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class AssetManager
|
||||
{
|
||||
protected $less;
|
||||
|
||||
protected $js;
|
||||
|
||||
public function __construct(CompilerInterface $js, CompilerInterface $less)
|
||||
{
|
||||
$this->js = $js;
|
||||
$this->less = $less;
|
||||
}
|
||||
|
||||
public function addFile($file)
|
||||
{
|
||||
$ext = pathinfo($file, PATHINFO_EXTENSION);
|
||||
|
||||
switch ($ext) {
|
||||
case 'js':
|
||||
$this->js->addFile($file);
|
||||
break;
|
||||
|
||||
case 'css':
|
||||
case 'less':
|
||||
$this->less->addFile($file);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new RuntimeException('Unsupported asset type: '.$ext);
|
||||
}
|
||||
}
|
||||
|
||||
public function addFiles(array $files)
|
||||
{
|
||||
array_walk($files, [$this, 'addFile']);
|
||||
}
|
||||
|
||||
public function addLess($string)
|
||||
{
|
||||
$this->less->addString($string);
|
||||
}
|
||||
|
||||
public function addJs($strings)
|
||||
{
|
||||
$this->js->addString($string);
|
||||
}
|
||||
|
||||
public function getCssFile()
|
||||
{
|
||||
return $this->less->getFile();
|
||||
}
|
||||
|
||||
public function getJsFile()
|
||||
{
|
||||
return $this->js->getFile();
|
||||
}
|
||||
}
|
10
framework/core/src/Assets/CompilerInterface.php
Normal file
10
framework/core/src/Assets/CompilerInterface.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php namespace Flarum\Assets;
|
||||
|
||||
interface CompilerInterface
|
||||
{
|
||||
public function addFile($file);
|
||||
|
||||
public function addString($string);
|
||||
|
||||
public function getFile();
|
||||
}
|
9
framework/core/src/Assets/JsCompiler.php
Normal file
9
framework/core/src/Assets/JsCompiler.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php namespace Flarum\Assets;
|
||||
|
||||
class JsCompiler extends RevisionCompiler
|
||||
{
|
||||
public function format($string)
|
||||
{
|
||||
return $string.";\n";
|
||||
}
|
||||
}
|
26
framework/core/src/Assets/LessCompiler.php
Normal file
26
framework/core/src/Assets/LessCompiler.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php namespace Flarum\Assets;
|
||||
|
||||
use Less_Parser;
|
||||
|
||||
class LessCompiler extends RevisionCompiler
|
||||
{
|
||||
public function compile()
|
||||
{
|
||||
ini_set('xdebug.max_nesting_level', 200);
|
||||
|
||||
$parser = new Less_Parser([
|
||||
'compress' => true,
|
||||
'cache_dir' => storage_path().'/less'
|
||||
]);
|
||||
|
||||
foreach ($this->files as $file) {
|
||||
$parser->parseFile($file);
|
||||
}
|
||||
|
||||
foreach ($this->strings as $string) {
|
||||
$parser->parse($string);
|
||||
}
|
||||
|
||||
return $parser->getCss();
|
||||
}
|
||||
}
|
95
framework/core/src/Assets/RevisionCompiler.php
Normal file
95
framework/core/src/Assets/RevisionCompiler.php
Normal file
@ -0,0 +1,95 @@
|
||||
<?php namespace Flarum\Assets;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class RevisionCompiler implements CompilerInterface
|
||||
{
|
||||
protected $files = [];
|
||||
|
||||
protected $strings = [];
|
||||
|
||||
public function __construct($path, $filename)
|
||||
{
|
||||
$this->path = $path;
|
||||
$this->filename = $filename;
|
||||
}
|
||||
|
||||
public function addFile($file)
|
||||
{
|
||||
$this->files[] = $file;
|
||||
}
|
||||
|
||||
public function addString($string)
|
||||
{
|
||||
$this->strings[] = $string;
|
||||
}
|
||||
|
||||
public function getFile()
|
||||
{
|
||||
if (! ($revision = $this->getRevision())) {
|
||||
$revision = Str::quickRandom();
|
||||
$this->putRevision($revision);
|
||||
}
|
||||
|
||||
$lastModTime = 0;
|
||||
foreach ($this->files as $file) {
|
||||
$lastModTime = max($lastModTime, filemtime($file));
|
||||
}
|
||||
|
||||
$ext = pathinfo($this->filename, PATHINFO_EXTENSION);
|
||||
$file = $this->path.'/'.substr_replace($this->filename, '-'.$revision, -strlen($ext) - 1, 0);
|
||||
|
||||
if (! file_exists($file)
|
||||
|| filemtime($file) < $lastModTime) {
|
||||
file_put_contents($file, $this->compile());
|
||||
}
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
protected function format($string)
|
||||
{
|
||||
return $string;
|
||||
}
|
||||
|
||||
protected function compile()
|
||||
{
|
||||
$output = '';
|
||||
|
||||
foreach ($this->files as $file) {
|
||||
$output .= $this->format(file_get_contents($file));
|
||||
}
|
||||
|
||||
foreach ($this->strings as $string) {
|
||||
$output .= $this->format($string);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
protected function getRevisionFile()
|
||||
{
|
||||
return $this->path.'/rev-manifest.json';
|
||||
}
|
||||
|
||||
protected function getRevision()
|
||||
{
|
||||
if (file_exists($file = $this->getRevisionFile())) {
|
||||
$manifest = json_decode(file_get_contents($file), true);
|
||||
return array_get($manifest, $this->filename);
|
||||
}
|
||||
}
|
||||
|
||||
protected function putRevision($revision)
|
||||
{
|
||||
if (file_exists($file = $this->getRevisionFile())) {
|
||||
$manifest = json_decode(file_get_contents($file), true);
|
||||
} else {
|
||||
$manifest = [];
|
||||
}
|
||||
|
||||
$manifest[$this->filename] = $revision;
|
||||
|
||||
return file_put_contents($this->getRevisionFile(), json_encode($manifest));
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ class ConsoleServiceProvider extends ServiceProvider
|
||||
{
|
||||
$this->commands('Flarum\Console\InstallCommand');
|
||||
$this->commands('Flarum\Console\SeedCommand');
|
||||
$this->commands('Flarum\Console\GenerateExtensionCommand');
|
||||
}
|
||||
|
||||
public function register()
|
||||
|
127
framework/core/src/Console/GenerateExtensionCommand.php
Normal file
127
framework/core/src/Console/GenerateExtensionCommand.php
Normal file
@ -0,0 +1,127 @@
|
||||
<?php namespace Flarum\Console;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
|
||||
class GenerateExtensionCommand extends Command
|
||||
{
|
||||
|
||||
/**
|
||||
* The console command name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name = 'flarum:extension';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Generate a Flarum extension skeleton.';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Application $app)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->app = $app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function fire()
|
||||
{
|
||||
do {
|
||||
$name = $this->ask('Extension name (<vendor>-<name>):');
|
||||
} while (! preg_match('/^([a-z0-9]+)-([a-z0-9-]+)$/i', $name, $match));
|
||||
|
||||
list(, $vendor, $package) = $match;
|
||||
|
||||
do {
|
||||
$title = $this->ask('Title:');
|
||||
} while (! $title);
|
||||
|
||||
$description = $this->ask('Description:');
|
||||
|
||||
$authorName = $this->ask('Author name:');
|
||||
|
||||
$authorEmail = $this->ask('Author email:');
|
||||
|
||||
$license = $this->ask('License:');
|
||||
|
||||
$this->info('Generating extension skeleton for "'.$name.'"...');
|
||||
|
||||
$dir = public_path().'/extensions/'.$name;
|
||||
|
||||
$replacements = [
|
||||
'{{namespace}}' => ucfirst($vendor).'\\'.ucfirst($package),
|
||||
'{{escapedNamespace}}' => ucfirst($vendor).'\\\\'.ucfirst($package),
|
||||
'{{classPrefix}}' => ucfirst($package),
|
||||
'{{name}}' => $name
|
||||
];
|
||||
|
||||
$this->copyStub($dir, $replacements);
|
||||
|
||||
rename($dir.'/src/ServiceProvider.php', $dir.'/src/'.ucfirst($package).'ServiceProvider.php');
|
||||
|
||||
$manifest = [
|
||||
'name' => $name,
|
||||
'title' => $title,
|
||||
'description' => $description,
|
||||
'tags' => [],
|
||||
'version' => '0.1.0',
|
||||
'author' => [
|
||||
'name' => $authorName,
|
||||
'email' => $authorEmail
|
||||
],
|
||||
'license' => $license,
|
||||
'require' => [
|
||||
'php' => '>=5.4.0',
|
||||
'flarum' => '>0.1.0'
|
||||
]
|
||||
];
|
||||
|
||||
file_put_contents($dir.'/flarum.json', json_encode($manifest, JSON_PRETTY_PRINT));
|
||||
|
||||
passthru("cd $dir; composer install; cd js; npm install; gulp");
|
||||
|
||||
$this->info('Extension "'.$name.'" generated!');
|
||||
}
|
||||
|
||||
protected function copyStub($destination, $replacements = [])
|
||||
{
|
||||
$this->recursiveCopy(__DIR__.'/../../stubs/extension', $destination, $replacements);
|
||||
}
|
||||
|
||||
protected function recursiveCopy($src, $dst, $replacements = [])
|
||||
{
|
||||
$dir = opendir($src);
|
||||
@mkdir($dst);
|
||||
|
||||
while (($file = readdir($dir)) !== false) {
|
||||
if ($file != '.' && $file != '..') {
|
||||
if (is_dir($src.'/'.$file)) {
|
||||
$this->recursiveCopy($src.'/'.$file, $dst.'/'.$file, $replacements);
|
||||
}
|
||||
else {
|
||||
$contents = file_get_contents($src.'/'.$file);
|
||||
$contents = str_replace(array_keys($replacements), array_values($replacements), $contents);
|
||||
|
||||
file_put_contents($dst.'/'.$file, $contents);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
closedir($dir);
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@ use Flarum\Core\Events\RegisterUserGambits;
|
||||
use Flarum\Extend\Permission;
|
||||
use Flarum\Extend\ActivityType;
|
||||
use Flarum\Extend\NotificationType;
|
||||
use Flarum\Extend\Locale;
|
||||
|
||||
class CoreServiceProvider extends ServiceProvider
|
||||
{
|
||||
@ -54,6 +55,17 @@ class CoreServiceProvider extends ServiceProvider
|
||||
(new ActivityType('Flarum\Core\Activity\StartedDiscussionActivity', 'Flarum\Api\Serializers\PostBasicSerializer')),
|
||||
(new ActivityType('Flarum\Core\Activity\JoinedActivity', 'Flarum\Api\Serializers\UserBasicSerializer'))
|
||||
);
|
||||
|
||||
foreach (['en'] as $locale) {
|
||||
$dir = __DIR__.'/../../locale/'.$locale;
|
||||
|
||||
$this->extend(
|
||||
(new Locale($locale))
|
||||
->translations($dir.'/translations.yml')
|
||||
->config($dir.'/config.php')
|
||||
->js($dir.'/config.js')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -70,6 +82,8 @@ class CoreServiceProvider extends ServiceProvider
|
||||
|
||||
$this->app->singleton('flarum.formatter', 'Flarum\Core\Formatter\FormatterManager');
|
||||
|
||||
$this->app->singleton('flarum.localeManager', 'Flarum\Locale\LocaleManager');
|
||||
|
||||
$this->app->bind(
|
||||
'Flarum\Core\Repositories\DiscussionRepositoryInterface',
|
||||
'Flarum\Core\Repositories\EloquentDiscussionRepository'
|
||||
@ -169,71 +183,78 @@ class CoreServiceProvider extends ServiceProvider
|
||||
$this->extend(
|
||||
new Permission('forum.view'),
|
||||
new Permission('forum.startDiscussion'),
|
||||
new Permission('discussion.rename'),
|
||||
new Permission('discussion.delete'),
|
||||
new Permission('discussion.reply'),
|
||||
new Permission('post.edit'),
|
||||
new Permission('post.delete')
|
||||
new Permission('discussion.editPosts'),
|
||||
new Permission('discussion.deletePosts'),
|
||||
new Permission('discussion.rename'),
|
||||
new Permission('discussion.delete')
|
||||
);
|
||||
|
||||
Forum::grantPermission(function ($grant, $user, $permission) {
|
||||
return $user->hasPermission('forum.'.$permission);
|
||||
Forum::allow('*', function ($forum, $user, $action) {
|
||||
if ($user->hasPermission('forum.'.$action)) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
Post::grantPermission(function ($grant, $user, $permission) {
|
||||
return $user->hasPermission('post'.$permission);
|
||||
Post::allow('*', function ($post, $user, $action) {
|
||||
if ($user->hasPermission('post.'.$action)) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Grant view access to a post only if the user can also view the
|
||||
// discussion which the post is in. Also, the if the post is hidden,
|
||||
// the user must have edit permissions too.
|
||||
Post::grantPermission('view', function ($grant) {
|
||||
$grant->whereCan('view', 'discussion');
|
||||
});
|
||||
|
||||
Post::demandPermission('view', function ($demand) {
|
||||
$demand->whereNull('hide_user_id')
|
||||
->orWhereCan('edit');
|
||||
});
|
||||
|
||||
// Allow a user to edit their own post, unless it has been hidden by
|
||||
// someone else.
|
||||
Post::grantPermission('edit', function ($grant, $user) {
|
||||
$grant->where('user_id', $user->id)
|
||||
->where(function ($query) use ($user) {
|
||||
// When fetching a discussion's posts: if the user doesn't have permission
|
||||
// to moderate the discussion, then they can't see posts that have been
|
||||
// hidden by someone other than themself.
|
||||
Discussion::scopeVisiblePosts(function ($query, User $user, Discussion $discussion) {
|
||||
if (! $discussion->can($user, 'editPosts')) {
|
||||
$query->where(function ($query) use ($user) {
|
||||
$query->whereNull('hide_user_id')
|
||||
->orWhere('hide_user_id', $user->id);
|
||||
});
|
||||
// @todo add limitations to time etc. according to a config setting
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
User::grantPermission(function ($grant, $user, $permission) {
|
||||
return $user->hasPermission('user.'.$permission);
|
||||
Post::allow('view', function ($post, $user) {
|
||||
if (! $post->hide_user_id || $post->can($user, 'edit')) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Grant view access to a user if the user can view the forum.
|
||||
User::grantPermission('view', function ($grant, $user) {
|
||||
$grant->whereCan('view', 'forum');
|
||||
// A post is allowed to be edited if the user has permission to moderate
|
||||
// the discussion which it's in, or if they are the author and the post
|
||||
// hasn't been deleted by someone else.
|
||||
Post::allow('edit', function ($post, $user) {
|
||||
if ($post->discussion->can($user, 'editPosts') ||
|
||||
($post->user_id == $user->id && (! $post->hide_user_id || $post->hide_user_id == $user->id))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Allow a user to edit their own account.
|
||||
User::grantPermission(['edit', 'delete'], function ($grant, $user) {
|
||||
$grant->where('id', $user->id);
|
||||
User::allow('*', function ($discussion, $user, $action) {
|
||||
if ($user->hasPermission('user.'.$action)) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
Discussion::grantPermission(function ($grant, $user, $permission) {
|
||||
return $user->hasPermission('discussion.'.$permission);
|
||||
User::allow(['edit', 'delete'], function ($user, $actor) {
|
||||
if ($user->id == $actor->id) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Grant view access to a discussion if the user can view the forum.
|
||||
Discussion::grantPermission('view', function ($grant, $user) {
|
||||
$grant->whereCan('view', 'forum');
|
||||
Discussion::allow('*', function ($discussion, $user, $action) {
|
||||
if ($user->hasPermission('discussion.'.$action)) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Allow a user to rename their own discussion.
|
||||
Discussion::grantPermission('rename', function ($grant, $user) {
|
||||
$grant->where('start_user_id', $user->id);
|
||||
// @todo add limitations to time etc. according to a config setting
|
||||
Discussion::allow('rename', function ($discussion, $user) {
|
||||
if ($discussion->start_user_id == $user->id) {
|
||||
return true;
|
||||
// @todo add limitations to time etc. according to a config setting
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,8 @@ class FormatterManager
|
||||
*/
|
||||
protected $container;
|
||||
|
||||
public $config;
|
||||
|
||||
/**
|
||||
* Create a new formatter manager instance.
|
||||
*
|
||||
@ -23,6 +25,17 @@ class FormatterManager
|
||||
public function __construct(Container $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
|
||||
// Studio does not yet merge autoload_files...
|
||||
// https://github.com/franzliedke/studio/commit/4f0f4314db4ed3e36c869a5f79b855c97bdd1be7
|
||||
require __DIR__.'/../../../vendor/ezyang/htmlpurifier/library/HTMLPurifier.composer.php';
|
||||
|
||||
$this->config = HTMLPurifier_Config::createDefault();
|
||||
$this->config->set('Core.Encoding', 'UTF-8');
|
||||
$this->config->set('Core.EscapeInvalidTags', true);
|
||||
$this->config->set('HTML.Doctype', 'HTML 4.01 Strict');
|
||||
$this->config->set('HTML.Allowed', 'p,em,strong,a[href|title],ul,ol,li,code,pre,blockquote,h1,h2,h3,h4,h5,h6,br,hr,img[src|alt]');
|
||||
$this->config->set('HTML.Nofollow', true);
|
||||
}
|
||||
|
||||
public function add($name, $formatter, $priority = 0)
|
||||
@ -66,18 +79,7 @@ class FormatterManager
|
||||
$text = $formatter->beforePurification($text, $post);
|
||||
}
|
||||
|
||||
// Studio does not yet merge autoload_files...
|
||||
// https://github.com/franzliedke/studio/commit/4f0f4314db4ed3e36c869a5f79b855c97bdd1be7
|
||||
require __DIR__.'/../../../vendor/ezyang/htmlpurifier/library/HTMLPurifier.composer.php';
|
||||
|
||||
$config = HTMLPurifier_Config::createDefault();
|
||||
$config->set('Core.Encoding', 'UTF-8');
|
||||
$config->set('Core.EscapeInvalidTags', true);
|
||||
$config->set('HTML.Doctype', 'HTML 4.01 Strict');
|
||||
$config->set('HTML.Allowed', 'p,em,strong,a[href|title],ul,ol,li,code,pre,blockquote,h1,h2,h3,h4,h5,h6,br,hr');
|
||||
$config->set('HTML.Nofollow', true);
|
||||
|
||||
$purifier = new HTMLPurifier($config);
|
||||
$purifier = new HTMLPurifier($this->config);
|
||||
|
||||
$text = $purifier->purify($text);
|
||||
|
||||
|
@ -20,9 +20,8 @@ class EditDiscussionCommandHandler
|
||||
$user = $command->user;
|
||||
$discussion = $this->discussions->findOrFail($command->discussionId, $user);
|
||||
|
||||
$discussion->assertCan($user, 'edit');
|
||||
|
||||
if (isset($command->data['title'])) {
|
||||
$discussion->assertCan($user, 'rename');
|
||||
$discussion->rename($command->data['title'], $user);
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
<?php namespace Flarum\Core\Models;
|
||||
|
||||
use Tobscure\Permissible\Permissible;
|
||||
use Flarum\Core\Support\EventGenerator;
|
||||
use Flarum\Core\Support\Locked;
|
||||
use Flarum\Core\Support\VisibleScope;
|
||||
use Flarum\Core\Events\DiscussionWasDeleted;
|
||||
use Flarum\Core\Events\DiscussionWasStarted;
|
||||
use Flarum\Core\Events\DiscussionWasRenamed;
|
||||
@ -9,7 +10,8 @@ use Flarum\Core\Models\User;
|
||||
|
||||
class Discussion extends Model
|
||||
{
|
||||
use Permissible;
|
||||
use Locked;
|
||||
use VisibleScope;
|
||||
|
||||
/**
|
||||
* The validation rules for this model.
|
||||
@ -28,6 +30,8 @@ class Discussion extends Model
|
||||
'last_post_number' => 'integer'
|
||||
];
|
||||
|
||||
protected static $relationships = [];
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
@ -213,6 +217,24 @@ class Discussion extends Model
|
||||
return $this->hasMany('Flarum\Core\Models\Post');
|
||||
}
|
||||
|
||||
protected static $visiblePostsScopes = [];
|
||||
|
||||
public static function scopeVisiblePosts($scope)
|
||||
{
|
||||
static::$visiblePostsScopes[] = $scope;
|
||||
}
|
||||
|
||||
public function visiblePosts(User $user)
|
||||
{
|
||||
$query = $this->posts();
|
||||
|
||||
foreach (static::$visiblePostsScopes as $scope) {
|
||||
$scope($query, $user, $this);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the relationship with the discussion's comments.
|
||||
*
|
||||
@ -295,9 +317,8 @@ class Discussion extends Model
|
||||
*/
|
||||
public function stateFor(User $user)
|
||||
{
|
||||
$loadedState = array_get($this->relations, 'state');
|
||||
if ($loadedState && $loadedState->user_id === $user->id) {
|
||||
return $loadedState;
|
||||
if ($this->isRelationLoaded('state')) {
|
||||
return $this->relations['state'];
|
||||
}
|
||||
|
||||
$state = $this->state($user)->first();
|
||||
|
@ -1,11 +1,13 @@
|
||||
<?php namespace Flarum\Core\Models;
|
||||
|
||||
use Tobscure\Permissible\Permissible;
|
||||
use Flarum\Core\Support\Locked;
|
||||
use Flarum\Core;
|
||||
|
||||
class Forum extends Model
|
||||
{
|
||||
use Permissible;
|
||||
use Locked;
|
||||
|
||||
protected static $relationships = [];
|
||||
|
||||
public function getTitleAttribute()
|
||||
{
|
||||
|
@ -169,37 +169,22 @@ class Model extends Eloquent
|
||||
return $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the user has permission to view this model, throwing an
|
||||
* exception if they don't.
|
||||
*
|
||||
* @param \Flarum\Core\Models\User $user
|
||||
* @return void
|
||||
*
|
||||
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
|
||||
*/
|
||||
public function assertVisibleTo(User $user)
|
||||
public function isRelationLoaded($relation)
|
||||
{
|
||||
if (! $this->can($user, 'view')) {
|
||||
throw new ModelNotFoundException;
|
||||
}
|
||||
return array_key_exists($relation, $this->relations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the user has a certain permission for this model, throwing
|
||||
* an exception if they don't.
|
||||
*
|
||||
* @param \Flarum\Core\Models\User $user
|
||||
* @param string $permission
|
||||
* @return void
|
||||
*
|
||||
* @throws \Flarum\Core\Exceptions\PermissionDeniedException
|
||||
*/
|
||||
public function assertCan(User $user, $permission)
|
||||
public function getRelation($relation)
|
||||
{
|
||||
if (! $this->can($user, $permission)) {
|
||||
throw new PermissionDeniedException;
|
||||
if (isset($this->$relation)) {
|
||||
return $this->$relation;
|
||||
}
|
||||
|
||||
if (! $this->isRelationLoaded($relation)) {
|
||||
$this->relations[$relation] = $this->$relation()->getResults();
|
||||
}
|
||||
|
||||
return $this->relations[$relation];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,11 +1,11 @@
|
||||
<?php namespace Flarum\Core\Models;
|
||||
|
||||
use Tobscure\Permissible\Permissible;
|
||||
use Flarum\Core\Events\PostWasDeleted;
|
||||
use Flarum\Core\Support\Locked;
|
||||
|
||||
class Post extends Model
|
||||
{
|
||||
use Permissible;
|
||||
use Locked;
|
||||
|
||||
/**
|
||||
* The validation rules for this model.
|
||||
|
@ -1,7 +1,6 @@
|
||||
<?php namespace Flarum\Core\Models;
|
||||
|
||||
use Illuminate\Contracts\Hashing\Hasher;
|
||||
use Tobscure\Permissible\Permissible;
|
||||
use Flarum\Core\Formatter\FormatterManager;
|
||||
use Flarum\Core\Events\UserWasDeleted;
|
||||
use Flarum\Core\Events\UserWasRegistered;
|
||||
@ -13,10 +12,13 @@ use Flarum\Core\Events\UserAvatarWasChanged;
|
||||
use Flarum\Core\Events\UserWasActivated;
|
||||
use Flarum\Core\Events\UserEmailWasConfirmed;
|
||||
use Flarum\Core\Events\UserEmailChangeWasRequested;
|
||||
use Flarum\Core\Support\Locked;
|
||||
use Flarum\Core\Support\VisibleScope;
|
||||
|
||||
class User extends Model
|
||||
{
|
||||
use Permissible;
|
||||
use Locked;
|
||||
use VisibleScope;
|
||||
|
||||
/**
|
||||
* The text formatter instance.
|
||||
@ -185,6 +187,7 @@ class User extends Model
|
||||
public function changeBio($bio)
|
||||
{
|
||||
$this->bio = $bio;
|
||||
$this->bio_html = null;
|
||||
|
||||
$this->raise(new UserBioWasChanged($this));
|
||||
|
||||
@ -199,7 +202,7 @@ class User extends Model
|
||||
*/
|
||||
public function getBioHtmlAttribute($value)
|
||||
{
|
||||
if (! $value) {
|
||||
if ($value === null) {
|
||||
$this->bio_html = $value = static::formatBio($this->bio);
|
||||
$this->save();
|
||||
}
|
||||
@ -309,9 +312,13 @@ class User extends Model
|
||||
return true;
|
||||
}
|
||||
|
||||
$count = $this->permissions()->where('permission', $permission)->count();
|
||||
static $permissions;
|
||||
|
||||
return (bool) $count;
|
||||
if (!$permissions) {
|
||||
$permissions = $this->permissions()->get();
|
||||
}
|
||||
|
||||
return (bool) $permissions->contains('permission', $permission);
|
||||
}
|
||||
|
||||
public function getUnreadNotificationsCount()
|
||||
|
@ -30,7 +30,7 @@ class EloquentDiscussionRepository implements DiscussionRepositoryInterface
|
||||
{
|
||||
$query = Discussion::where('id', $id);
|
||||
|
||||
return $this->scopeVisibleForUser($query, $user)->firstOrFail();
|
||||
return $this->scopeVisibleTo($query, $user)->firstOrFail();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -54,10 +54,10 @@ class EloquentDiscussionRepository implements DiscussionRepositoryInterface
|
||||
* @param \Flarum\Core\Models\User $user
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
protected function scopeVisibleForUser(Builder $query, User $user = null)
|
||||
protected function scopeVisibleTo(Builder $query, User $user = null)
|
||||
{
|
||||
if ($user !== null) {
|
||||
$query->whereCan($user, 'view');
|
||||
$query->whereVisibleTo($user);
|
||||
}
|
||||
|
||||
return $query;
|
||||
|
@ -1,10 +1,32 @@
|
||||
<?php namespace Flarum\Core\Repositories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Flarum\Core\Models\Post;
|
||||
use Flarum\Core\Models\User;
|
||||
use Flarum\Core\Models\Discussion;
|
||||
use Flarum\Core\Search\Discussions\Fulltext\DriverInterface;
|
||||
|
||||
// TODO: In some cases, the use of a post repository incurs extra query expense,
|
||||
// because for every post retrieved we need to check if the discussion it's in
|
||||
// is visible. Especially when retrieving a discussion's posts, we can end up
|
||||
// with an inefficient chain of queries like this:
|
||||
// 1. Api\Discussions\ShowAction: get discussion (will exit if not visible)
|
||||
// 2. Discussion@visiblePosts: get discussion tags (for post visibility purposes)
|
||||
// 3. Discussion@visiblePosts: get post IDs
|
||||
// 4. EloquentPostRepository@getIndexForNumber: get discussion
|
||||
// 5. EloquentPostRepository@getIndexForNumber: get discussion tags (for post visibility purposes)
|
||||
// 6. EloquentPostRepository@getIndexForNumber: get post index for number
|
||||
// 7. EloquentPostRepository@findWhere: get post IDs for discussion to check for discussion visibility
|
||||
// 8. EloquentPostRepository@findWhere: get post IDs in visible discussions
|
||||
// 9. EloquentPostRepository@findWhere: get posts
|
||||
// 10. EloquentPostRepository@findWhere: eager load discussion onto posts
|
||||
// 11. EloquentPostRepository@findWhere: get discussion tags to filter visible posts
|
||||
// 12. Api\Discussions\ShowAction: eager load users
|
||||
// 13. Api\Discussions\ShowAction: eager load groups
|
||||
// 14. Api\Discussions\ShowAction: eager load mentions
|
||||
// 14. Serializers\DiscussionSerializer: load discussion-user state
|
||||
|
||||
class EloquentPostRepository implements PostRepositoryInterface
|
||||
{
|
||||
protected $fulltext;
|
||||
@ -26,9 +48,13 @@ class EloquentPostRepository implements PostRepositoryInterface
|
||||
*/
|
||||
public function findOrFail($id, User $user = null)
|
||||
{
|
||||
$query = Post::where('id', $id);
|
||||
$posts = $this->findByIds([$id], $user);
|
||||
|
||||
return $this->scopeVisibleForUser($query, $user)->firstOrFail();
|
||||
if (! count($posts)) {
|
||||
throw new ModelNotFoundException;
|
||||
}
|
||||
|
||||
return $posts->first();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -52,7 +78,9 @@ class EloquentPostRepository implements PostRepositoryInterface
|
||||
$query->orderBy($field, $order);
|
||||
}
|
||||
|
||||
return $this->scopeVisibleForUser($query, $user)->get();
|
||||
$ids = $query->lists('id');
|
||||
|
||||
return $this->findByIds($ids, $user);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -65,9 +93,11 @@ class EloquentPostRepository implements PostRepositoryInterface
|
||||
*/
|
||||
public function findByIds(array $ids, User $user = null)
|
||||
{
|
||||
$query = Post::whereIn('id', (array) $ids);
|
||||
$ids = $this->filterDiscussionVisibleTo($ids, $user);
|
||||
|
||||
return $this->scopeVisibleForUser($query, $user)->get();
|
||||
$posts = Post::with('discussion')->whereIn('id', (array) $ids)->get();
|
||||
|
||||
return $this->filterVisibleTo($posts, $user);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -82,13 +112,17 @@ class EloquentPostRepository implements PostRepositoryInterface
|
||||
{
|
||||
$ids = $this->fulltext->match($string);
|
||||
|
||||
$ids = $this->filterDiscussionVisibleTo($ids, $user);
|
||||
|
||||
$query = Post::select('id', 'discussion_id')->whereIn('id', $ids);
|
||||
|
||||
foreach ($ids as $id) {
|
||||
$query->orderByRaw('id != ?', [$id]);
|
||||
}
|
||||
|
||||
return $this->scopeVisibleForUser($query, $user)->get();
|
||||
$posts = $query->get();
|
||||
|
||||
return $this->filterVisibleTo($posts, $user);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -103,7 +137,8 @@ class EloquentPostRepository implements PostRepositoryInterface
|
||||
*/
|
||||
public function getIndexForNumber($discussionId, $number, User $user = null)
|
||||
{
|
||||
$query = Post::where('discussion_id', $discussionId)
|
||||
$query = Discussion::find($discussionId)
|
||||
->visiblePosts($user)
|
||||
->where('time', '<', function ($query) use ($discussionId, $number) {
|
||||
$query->select('time')
|
||||
->from('posts')
|
||||
@ -116,22 +151,32 @@ class EloquentPostRepository implements PostRepositoryInterface
|
||||
->orderByRaw('ABS(CAST(number AS SIGNED) - '.(int) $number.')');
|
||||
});
|
||||
|
||||
return $this->scopeVisibleForUser($query, $user)->count();
|
||||
return $query->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to only include records that are visible to a user.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param \Flarum\Core\Models\User $user
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
protected function scopeVisibleForUser(Builder $query, User $user = null)
|
||||
protected function filterDiscussionVisibleTo($ids, $user)
|
||||
{
|
||||
if ($user !== null) {
|
||||
$query->whereCan($user, 'view');
|
||||
// For each post ID, we need to make sure that the discussion it's in
|
||||
// is visible to the user.
|
||||
if ($user) {
|
||||
$ids = Discussion::join('posts', 'discussions.id', '=', 'posts.discussion_id')
|
||||
->whereIn('posts.id', $ids)
|
||||
->whereVisibleTo($user)
|
||||
->get(['posts.id'])
|
||||
->lists('id');
|
||||
}
|
||||
|
||||
return $query;
|
||||
return $ids;
|
||||
}
|
||||
|
||||
protected function filterVisibleTo($posts, $user)
|
||||
{
|
||||
if ($user) {
|
||||
$posts = $posts->filter(function ($post) use ($user) {
|
||||
return $post->can($user, 'view');
|
||||
});
|
||||
}
|
||||
|
||||
return $posts;
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ class EloquentUserRepository implements UserRepositoryInterface
|
||||
{
|
||||
$query = User::where('id', $id);
|
||||
|
||||
return $this->scopeVisibleForUser($query, $user)->firstOrFail();
|
||||
return $this->scopeVisibleTo($query, $user)->firstOrFail();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -67,7 +67,7 @@ class EloquentUserRepository implements UserRepositoryInterface
|
||||
{
|
||||
$query = User::where('username', 'like', $username);
|
||||
|
||||
return $this->scopeVisibleForUser($query, $user)->pluck('id');
|
||||
return $this->scopeVisibleTo($query, $user)->pluck('id');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -85,7 +85,7 @@ class EloquentUserRepository implements UserRepositoryInterface
|
||||
->orderByRaw('username = ? desc', [$string])
|
||||
->orderByRaw('username like ? desc', [$string.'%']);
|
||||
|
||||
return $this->scopeVisibleForUser($query, $user)->lists('id');
|
||||
return $this->scopeVisibleTo($query, $user)->lists('id');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -95,10 +95,10 @@ class EloquentUserRepository implements UserRepositoryInterface
|
||||
* @param \Flarum\Core\Models\User $user
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
protected function scopeVisibleForUser(Builder $query, User $user = null)
|
||||
protected function scopeVisibleTo(Builder $query, User $user = null)
|
||||
{
|
||||
if ($user !== null) {
|
||||
$query->whereCan($user, 'view');
|
||||
$query->whereVisibleTo($user);
|
||||
}
|
||||
|
||||
return $query;
|
||||
|
@ -6,13 +6,10 @@ class DiscussionSearchResults
|
||||
|
||||
protected $areMoreResults;
|
||||
|
||||
protected $total;
|
||||
|
||||
public function __construct($discussions, $areMoreResults, $total)
|
||||
public function __construct($discussions, $areMoreResults)
|
||||
{
|
||||
$this->discussions = $discussions;
|
||||
$this->areMoreResults = $areMoreResults;
|
||||
$this->total = $total;
|
||||
}
|
||||
|
||||
public function getDiscussions()
|
||||
@ -20,11 +17,6 @@ class DiscussionSearchResults
|
||||
return $this->discussions;
|
||||
}
|
||||
|
||||
public function getTotal()
|
||||
{
|
||||
return $this->total;
|
||||
}
|
||||
|
||||
public function areMoreResults()
|
||||
{
|
||||
return $this->areMoreResults;
|
||||
|
@ -59,12 +59,10 @@ class DiscussionSearcher implements SearcherInterface
|
||||
public function search(DiscussionSearchCriteria $criteria, $limit = null, $offset = 0, $load = [])
|
||||
{
|
||||
$this->user = $criteria->user;
|
||||
$this->query = $this->discussions->query()->whereCan($criteria->user, 'view');
|
||||
$this->query = $this->discussions->query()->whereVisibleTo($criteria->user);
|
||||
|
||||
$this->gambits->apply($criteria->query, $this);
|
||||
|
||||
$total = $this->query->count();
|
||||
|
||||
$sort = $criteria->sort ?: $this->defaultSort;
|
||||
|
||||
foreach ($sort as $field => $order) {
|
||||
@ -112,6 +110,6 @@ class DiscussionSearcher implements SearcherInterface
|
||||
Discussion::setStateUser($this->user);
|
||||
$discussions->load($load);
|
||||
|
||||
return new DiscussionSearchResults($discussions, $areMoreResults, $total);
|
||||
return new DiscussionSearchResults($discussions, $areMoreResults);
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ class ConfigTableSeeder extends Seeder
|
||||
'welcome_message' => 'Flarum is now at a point where you can have basic conversations, so here is a little demo for you to break.',
|
||||
'welcome_title' => 'Welcome to Flarum Demo Forum',
|
||||
'extensions_enabled' => '[]',
|
||||
'locale' => 'en',
|
||||
'theme_primary_color' => '#536F90',
|
||||
'theme_secondary_color' => '#536F90',
|
||||
'theme_dark_mode' => false,
|
||||
|
@ -27,8 +27,8 @@ class PermissionsTableSeeder extends Seeder
|
||||
// Moderators can edit + delete stuff and suspend users
|
||||
[4, 'discussion.delete'],
|
||||
[4, 'discussion.rename'],
|
||||
[4, 'post.delete'],
|
||||
[4, 'post.edit'],
|
||||
[4, 'discussion.editPosts'],
|
||||
[4, 'discussion.deletePosts'],
|
||||
[4, 'user.suspend'],
|
||||
|
||||
];
|
||||
|
57
framework/core/src/Core/Support/Locked.php
Normal file
57
framework/core/src/Core/Support/Locked.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php namespace Flarum\Core\Support;
|
||||
|
||||
use Flarum\Core\Exceptions\PermissionDeniedException;
|
||||
use Flarum\Core\Models\User;
|
||||
use Closure;
|
||||
|
||||
trait Locked
|
||||
{
|
||||
protected static $conditions = [];
|
||||
|
||||
protected static function getConditions($action)
|
||||
{
|
||||
$conditions = isset(static::$conditions[$action]) ? static::$conditions[$action] : [];
|
||||
$all = isset(static::$conditions['*']) ? static::$conditions['*'] : [];
|
||||
|
||||
return array_merge($conditions, $all);
|
||||
}
|
||||
|
||||
public static function allow($action, Closure $condition)
|
||||
{
|
||||
foreach ((array) $action as $action) {
|
||||
if (! isset(static::$conditions[$action])) {
|
||||
static::$conditions[$action] = [];
|
||||
}
|
||||
|
||||
static::$conditions[$action][] = $condition;
|
||||
}
|
||||
}
|
||||
|
||||
public function can(User $user, $action)
|
||||
{
|
||||
foreach ($this->getConditions($action) as $condition) {
|
||||
$can = $condition($this, $user, $action);
|
||||
|
||||
if ($can !== null) {
|
||||
return $can;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the user has a certain permission for this model, throwing
|
||||
* an exception if they don't.
|
||||
*
|
||||
* @param \Flarum\Core\Models\User $user
|
||||
* @param string $permission
|
||||
* @return void
|
||||
*
|
||||
* @throws \Flarum\Core\Exceptions\PermissionDeniedException
|
||||
*/
|
||||
public function assertCan(User $user, $action)
|
||||
{
|
||||
if (! $this->can($user, $action)) {
|
||||
throw new PermissionDeniedException;
|
||||
}
|
||||
}
|
||||
}
|
20
framework/core/src/Core/Support/VisibleScope.php
Normal file
20
framework/core/src/Core/Support/VisibleScope.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php namespace Flarum\Core\Support;
|
||||
|
||||
use Flarum\Core\Models\User;
|
||||
|
||||
trait VisibleScope
|
||||
{
|
||||
protected static $visibleScopes = [];
|
||||
|
||||
public static function scopeVisible($scope)
|
||||
{
|
||||
static::$visibleScopes[] = $scope;
|
||||
}
|
||||
|
||||
public function scopeWhereVisibleTo($query, User $user)
|
||||
{
|
||||
foreach (static::$visibleScopes as $scope) {
|
||||
$scope($query, $user);
|
||||
}
|
||||
}
|
||||
}
|
18
framework/core/src/Extend/AdminTranslations.php
Normal file
18
framework/core/src/Extend/AdminTranslations.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php namespace Flarum\Extend;
|
||||
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
|
||||
class AdminTranslations implements ExtenderInterface
|
||||
{
|
||||
protected $keys;
|
||||
|
||||
public function __construct($keys)
|
||||
{
|
||||
$this->keys = $keys;
|
||||
}
|
||||
|
||||
public function extend(Container $container)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
28
framework/core/src/Extend/ApiLink.php
Normal file
28
framework/core/src/Extend/ApiLink.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php namespace Flarum\Extend;
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
|
||||
class ApiLink implements ExtenderInterface
|
||||
{
|
||||
protected $actions;
|
||||
|
||||
protected $relationships;
|
||||
|
||||
public function __construct($actions, $relationships)
|
||||
{
|
||||
$this->actions = $actions;
|
||||
$this->relationships = $relationships;
|
||||
}
|
||||
|
||||
public function extend(Application $app)
|
||||
{
|
||||
foreach ((array) $this->actions as $action) {
|
||||
$parts = explode('.', $action);
|
||||
$class = 'Flarum\Api\Actions\\'.ucfirst($parts[0]).'\\'.ucfirst($parts[1]).'Action';
|
||||
|
||||
foreach ((array) $this->relationships as $relationship) {
|
||||
$class::$link[] = $relationship;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@ class ForumAssets implements ExtenderInterface
|
||||
public function extend(Container $container)
|
||||
{
|
||||
$container->make('events')->listen('Flarum\Forum\Events\RenderView', function ($event) {
|
||||
$event->assets->addFile($this->files);
|
||||
$event->assets->addFiles($this->files);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
19
framework/core/src/Extend/ForumTranslations.php
Normal file
19
framework/core/src/Extend/ForumTranslations.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php namespace Flarum\Extend;
|
||||
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Flarum\Forum\Actions\IndexAction;
|
||||
|
||||
class ForumTranslations implements ExtenderInterface
|
||||
{
|
||||
protected $keys;
|
||||
|
||||
public function __construct($keys)
|
||||
{
|
||||
$this->keys = $keys;
|
||||
}
|
||||
|
||||
public function extend(Container $container)
|
||||
{
|
||||
IndexAction::$translations = array_merge(IndexAction::$translations, $this->keys);
|
||||
}
|
||||
}
|
57
framework/core/src/Extend/Locale.php
Normal file
57
framework/core/src/Extend/Locale.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php namespace Flarum\Extend;
|
||||
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
|
||||
class Locale implements ExtenderInterface
|
||||
{
|
||||
protected $locale;
|
||||
|
||||
protected $translations;
|
||||
|
||||
protected $config;
|
||||
|
||||
protected $js;
|
||||
|
||||
public function __construct($locale)
|
||||
{
|
||||
$this->locale = $locale;
|
||||
}
|
||||
|
||||
public function translations($translations)
|
||||
{
|
||||
$this->translations = $translations;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function config($config)
|
||||
{
|
||||
$this->config = $config;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function js($js)
|
||||
{
|
||||
$this->js = $js;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function extend(Container $container)
|
||||
{
|
||||
$manager = $container->make('flarum.localeManager');
|
||||
|
||||
if ($this->translations) {
|
||||
$manager->addTranslations($this->locale, $this->translations);
|
||||
}
|
||||
|
||||
if ($this->config) {
|
||||
$manager->addConfig($this->locale, $this->config);
|
||||
}
|
||||
|
||||
if ($this->js) {
|
||||
$manager->addJsFile($this->locale, $this->js);
|
||||
}
|
||||
}
|
||||
}
|
@ -7,17 +7,19 @@ class Relationship implements ExtenderInterface
|
||||
{
|
||||
protected $parent;
|
||||
|
||||
protected $type;
|
||||
|
||||
protected $name;
|
||||
|
||||
protected $type;
|
||||
|
||||
protected $child;
|
||||
|
||||
public function __construct($parent, $type, $name, $child = null)
|
||||
protected $table;
|
||||
|
||||
public function __construct($parent, $name, $type, $child = null)
|
||||
{
|
||||
$this->parent = $parent;
|
||||
$this->type = $type;
|
||||
$this->name = $name;
|
||||
$this->type = $type;
|
||||
$this->child = $child;
|
||||
}
|
||||
|
||||
@ -30,6 +32,8 @@ class Relationship implements ExtenderInterface
|
||||
return call_user_func($this->type, $model);
|
||||
} elseif ($this->type === 'belongsTo') {
|
||||
return $model->belongsTo($this->child, null, null, $this->name);
|
||||
} elseif ($this->type === 'belongsToMany') {
|
||||
return $model->belongsToMany($this->child, $this->table, null, null, $this->name);
|
||||
} else {
|
||||
// @todo
|
||||
}
|
||||
|
@ -1,10 +1,14 @@
|
||||
<?php namespace Flarum\Forum\Actions;
|
||||
|
||||
use Flarum\Api\Client;
|
||||
use Flarum\Assets\AssetManager;
|
||||
use Flarum\Assets\JsCompiler;
|
||||
use Flarum\Assets\LessCompiler;
|
||||
use Flarum\Core;
|
||||
use Flarum\Forum\Events\RenderView;
|
||||
use Flarum\Locale\JsCompiler as LocaleJsCompiler;
|
||||
use Flarum\Support\Actor;
|
||||
use Flarum\Support\HtmlAction;
|
||||
use Flarum\Forum\Events\RenderView;
|
||||
use Illuminate\Database\DatabaseManager;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
@ -19,6 +23,8 @@ class IndexAction extends HtmlAction
|
||||
|
||||
protected $database;
|
||||
|
||||
public static $translations = [];
|
||||
|
||||
// TODO: DatabaseManager should be ConnectionInterface
|
||||
public function __construct(Client $apiClient, Actor $actor, DatabaseManager $database, SessionInterface $session)
|
||||
{
|
||||
@ -35,12 +41,23 @@ class IndexAction extends HtmlAction
|
||||
$session = [];
|
||||
$alert = $this->session->get('alert');
|
||||
|
||||
$response = $this->apiClient->send('Flarum\Api\Actions\Forum\ShowAction');
|
||||
|
||||
$data = [$response->data];
|
||||
if (isset($response->included)) {
|
||||
$data = array_merge($data, $response->included);
|
||||
}
|
||||
|
||||
if (($user = $this->actor->getUser()) && $user->exists) {
|
||||
$session = [
|
||||
'userId' => $user->id,
|
||||
'token' => $request->getCookieParams()['flarum_remember'],
|
||||
];
|
||||
|
||||
// TODO: calling on the API here results in an extra query to get
|
||||
// the user + their groups, when we already have this information on
|
||||
// $this->actor. Can we simply run the CurrentUserSerializer
|
||||
// manually?
|
||||
$response = $this->apiClient->send('Flarum\Api\Actions\Users\ShowAction', ['id' => $user->id]);
|
||||
|
||||
$data = [$response->data];
|
||||
@ -57,23 +74,63 @@ class IndexAction extends HtmlAction
|
||||
->with('session', $session)
|
||||
->with('alert', $alert);
|
||||
|
||||
$assetManager = app('flarum.forum.assetManager');
|
||||
$root = __DIR__.'/../../..';
|
||||
$assetManager->addFile([
|
||||
$root.'/js/forum/dist/app.js',
|
||||
$root.'/less/forum/app.less'
|
||||
]);
|
||||
$assetManager->addLess('
|
||||
@fl-primary-color: '.Core::config('theme_primary_color').';
|
||||
@fl-secondary-color: '.Core::config('theme_secondary_color').';
|
||||
@fl-dark-mode: '.(Core::config('theme_dark_mode') ? 'true' : 'false').';
|
||||
@fl-colored_header: '.(Core::config('theme_colored_header') ? 'true' : 'false').';
|
||||
');
|
||||
$public = public_path().'/assets';
|
||||
|
||||
event(new RenderView($view, $assetManager, $this));
|
||||
$assets = new AssetManager(
|
||||
new JsCompiler($public, 'forum.js'),
|
||||
new LessCompiler($public, 'forum.css')
|
||||
);
|
||||
|
||||
$assets->addFile($root.'/js/forum/dist/app.js');
|
||||
$assets->addFile($root.'/less/forum/app.less');
|
||||
|
||||
$variables = [
|
||||
'fl-primary-color' => Core::config('theme_primary_color', '#000'),
|
||||
'fl-secondary-color' => Core::config('theme_secondary_color', '#000'),
|
||||
'fl-dark-mode' => Core::config('theme_dark_mode') ? 'true' : 'false',
|
||||
'fl-colored-header' => Core::config('theme_colored_header') ? 'true' : 'false'
|
||||
];
|
||||
foreach ($variables as $name => $value) {
|
||||
$assets->addLess("@$name: $value;");
|
||||
}
|
||||
|
||||
$locale = $user->locale ?: Core::config('locale', 'en');
|
||||
|
||||
$localeManager = app('flarum.localeManager');
|
||||
$translations = $localeManager->getTranslations($locale);
|
||||
$jsFiles = $localeManager->getJsFiles($locale);
|
||||
|
||||
$localeCompiler = new LocaleJsCompiler($public, 'locale-'.$locale.'.js');
|
||||
$localeCompiler->setTranslations(static::filterTranslations($translations));
|
||||
array_walk($jsFiles, [$localeCompiler, 'addFile']);
|
||||
|
||||
event(new RenderView($view, $assets, $this));
|
||||
|
||||
return $view
|
||||
->with('styles', $assetManager->getCSSFiles())
|
||||
->with('scripts', $assetManager->getJSFiles());
|
||||
->with('styles', [$assets->getCssFile()])
|
||||
->with('scripts', [$assets->getJsFile(), $localeCompiler->getFile()]);
|
||||
}
|
||||
|
||||
protected static function filterTranslations($translations)
|
||||
{
|
||||
$filtered = [];
|
||||
|
||||
foreach (static::$translations as $key) {
|
||||
$parts = explode('.', $key);
|
||||
$level = &$filtered;
|
||||
|
||||
foreach ($parts as $part) {
|
||||
if (! isset($level[$part])) {
|
||||
$level[$part] = [];
|
||||
}
|
||||
|
||||
$level = &$level[$part];
|
||||
}
|
||||
|
||||
$level = array_get($translations, $key);
|
||||
}
|
||||
|
||||
return $filtered;
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
<?php namespace Flarum\Forum;
|
||||
|
||||
use Flarum\Extend\ForumTranslations;
|
||||
use Flarum\Http\RouteCollection;
|
||||
use Flarum\Http\UrlGenerator;
|
||||
use Flarum\Support\AssetManager;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Flarum\Support\ServiceProvider;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class ForumServiceProvider extends ServiceProvider
|
||||
@ -43,6 +44,12 @@ class ForumServiceProvider extends ServiceProvider
|
||||
]);
|
||||
|
||||
$this->routes();
|
||||
|
||||
$this->extend(
|
||||
new ForumTranslations([
|
||||
//
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
protected function routes()
|
||||
|
24
framework/core/src/Locale/JsCompiler.php
Normal file
24
framework/core/src/Locale/JsCompiler.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php namespace Flarum\Locale;
|
||||
|
||||
use Flarum\Assets\RevisionCompiler;
|
||||
|
||||
class JsCompiler extends RevisionCompiler
|
||||
{
|
||||
protected $translations = [];
|
||||
|
||||
public function setTranslations(array $translations)
|
||||
{
|
||||
$this->translations = $translations;
|
||||
}
|
||||
|
||||
public function compile()
|
||||
{
|
||||
$output = "var app = require('flarum/app')['default']; app.translator.translations = ".json_encode($this->translations).";";
|
||||
|
||||
foreach ($this->files as $filename) {
|
||||
$output .= file_get_contents($filename);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
65
framework/core/src/Locale/LocaleManager.php
Normal file
65
framework/core/src/Locale/LocaleManager.php
Normal file
@ -0,0 +1,65 @@
|
||||
<?php namespace Flarum\Locale;
|
||||
|
||||
class LocaleManager
|
||||
{
|
||||
protected $translations = [];
|
||||
|
||||
protected $js = [];
|
||||
|
||||
protected $config = [];
|
||||
|
||||
public function addTranslations($locale, $translations)
|
||||
{
|
||||
if (! isset($this->translations[$locale])) {
|
||||
$this->translations[$locale] = [];
|
||||
}
|
||||
|
||||
$this->translations[$locale][] = $translations;
|
||||
}
|
||||
|
||||
public function addJsFile($locale, $js)
|
||||
{
|
||||
if (! isset($this->js[$locale])) {
|
||||
$this->js[$locale] = [];
|
||||
}
|
||||
|
||||
$this->js[$locale][] = $js;
|
||||
}
|
||||
|
||||
public function addConfig($locale, $config)
|
||||
{
|
||||
if (! isset($this->config[$locale])) {
|
||||
$this->config[$locale] = [];
|
||||
}
|
||||
|
||||
$this->config[$locale][] = $config;
|
||||
}
|
||||
|
||||
public function getTranslations($locale)
|
||||
{
|
||||
$files = array_get($this->translations, $locale, []);
|
||||
|
||||
$parts = explode('-', $locale);
|
||||
|
||||
if (count($parts) > 1) {
|
||||
$files = array_merge(array_get($this->translations, $parts[0], []), $files);
|
||||
}
|
||||
|
||||
$compiler = new TranslationCompiler($locale, $files);
|
||||
|
||||
return $compiler->getTranslations();
|
||||
}
|
||||
|
||||
public function getJsFiles($locale)
|
||||
{
|
||||
$files = array_get($this->js, $locale, []);
|
||||
|
||||
$parts = explode('-', $locale);
|
||||
|
||||
if (count($parts) > 1) {
|
||||
$files = array_merge(array_get($this->js, $parts[0], []), $files);
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
}
|
29
framework/core/src/Locale/TranslationCompiler.php
Normal file
29
framework/core/src/Locale/TranslationCompiler.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php namespace Flarum\Locale;
|
||||
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class TranslationCompiler
|
||||
{
|
||||
protected $locale;
|
||||
|
||||
protected $filenames;
|
||||
|
||||
public function __construct($locale, array $filenames)
|
||||
{
|
||||
$this->locale = $locale;
|
||||
$this->filenames = $filenames;
|
||||
}
|
||||
|
||||
public function getTranslations()
|
||||
{
|
||||
// @todo caching
|
||||
|
||||
$translations = [];
|
||||
|
||||
foreach ($this->filenames as $filename) {
|
||||
$translations = array_replace_recursive($translations, Yaml::parse(file_get_contents($filename)));
|
||||
}
|
||||
|
||||
return $translations;
|
||||
}
|
||||
}
|
42
framework/core/src/Locale/Translator.php
Normal file
42
framework/core/src/Locale/Translator.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php namespace Flarum\Locale;
|
||||
|
||||
use Closure;
|
||||
|
||||
class Translator
|
||||
{
|
||||
protected $translations;
|
||||
|
||||
protected $plural;
|
||||
|
||||
public function __construct(array $translations, Closure $plural)
|
||||
{
|
||||
$this->translations = $translations;
|
||||
$this->plural = $plural;
|
||||
}
|
||||
|
||||
public function plural($count)
|
||||
{
|
||||
$callback = $this->plural;
|
||||
|
||||
return $callback($count);
|
||||
}
|
||||
|
||||
public function translate($key, array $input = [])
|
||||
{
|
||||
$translation = array_get($this->translations, $key);
|
||||
|
||||
if (is_array($translation) && isset($input['count'])) {
|
||||
$translation = $translation[$this->plural($input['count'])];
|
||||
}
|
||||
|
||||
if (is_string($translation)) {
|
||||
foreach ($input as $k => $v) {
|
||||
$translation = str_replace('{'.$k.'}', $v, $translation);
|
||||
}
|
||||
|
||||
return $translation;
|
||||
} else {
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,166 +0,0 @@
|
||||
<?php namespace Flarum\Support;
|
||||
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\Support\Str;
|
||||
use Cache;
|
||||
use Less_Parser;
|
||||
use Closure;
|
||||
|
||||
class AssetManager
|
||||
{
|
||||
protected $files = [
|
||||
'css' => [],
|
||||
'js' => [],
|
||||
'less' => []
|
||||
];
|
||||
|
||||
protected $less = [];
|
||||
|
||||
protected $publicPath;
|
||||
|
||||
protected $name;
|
||||
|
||||
protected $storage;
|
||||
|
||||
public function __construct(Filesystem $storage, $publicPath, $name)
|
||||
{
|
||||
$this->storage = $storage;
|
||||
$this->publicPath = $publicPath;
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function addFile($files)
|
||||
{
|
||||
foreach ((array) $files as $file) {
|
||||
$ext = pathinfo($file, PATHINFO_EXTENSION);
|
||||
$this->files[$ext][] = $file;
|
||||
}
|
||||
}
|
||||
|
||||
public function addLess($strings)
|
||||
{
|
||||
foreach ((array) $strings as $string) {
|
||||
$this->less[] = $string;
|
||||
}
|
||||
}
|
||||
|
||||
protected function getAssetDirectory()
|
||||
{
|
||||
$dir = $this->publicPath;
|
||||
if (! $this->storage->isDirectory($dir)) {
|
||||
$this->storage->makeDirectory($dir);
|
||||
}
|
||||
return $dir;
|
||||
}
|
||||
|
||||
protected function getRevisionFile()
|
||||
{
|
||||
return $this->getAssetDirectory().'/'.$this->name;
|
||||
}
|
||||
|
||||
protected function getRevision()
|
||||
{
|
||||
if (file_exists($file = $this->getRevisionFile())) {
|
||||
return file_get_contents($file);
|
||||
}
|
||||
}
|
||||
|
||||
protected function putRevision($revision)
|
||||
{
|
||||
return file_put_contents($this->getRevisionFile(), $revision);
|
||||
}
|
||||
|
||||
protected function getFiles($type, Closure $callback)
|
||||
{
|
||||
$dir = $this->getAssetDirectory();
|
||||
|
||||
if (! ($revision = $this->getRevision())) {
|
||||
$revision = Str::quickRandom();
|
||||
$this->putRevision($revision);
|
||||
}
|
||||
|
||||
$lastModTime = 0;
|
||||
foreach ($this->files[$type] as $file) {
|
||||
$lastModTime = max($lastModTime, filemtime($file));
|
||||
}
|
||||
$debug = 0;
|
||||
// $debug = 1;
|
||||
|
||||
if (! file_exists($file = $dir.'/'.$this->name.'-'.$revision.'.'.$type)
|
||||
|| filemtime($file) < $lastModTime
|
||||
|| $debug) {
|
||||
$this->storage->put($file, $callback());
|
||||
}
|
||||
|
||||
return [$file];
|
||||
}
|
||||
|
||||
public function clearCache()
|
||||
{
|
||||
if ($revision = $this->getRevision()) {
|
||||
$dir = $this->getAssetDirectory();
|
||||
foreach (['css', 'js'] as $type) {
|
||||
@unlink($dir.'/'.$this->name.'-'.$revision.'.'.$type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getCSSFiles()
|
||||
{
|
||||
return $this->getFiles('css', function () {
|
||||
return $this->compileCSS();
|
||||
});
|
||||
}
|
||||
|
||||
public function getJSFiles()
|
||||
{
|
||||
return $this->getFiles('js', function () {
|
||||
return $this->compileJS();
|
||||
});
|
||||
}
|
||||
|
||||
public function compileLess()
|
||||
{
|
||||
ini_set('xdebug.max_nesting_level', 200);
|
||||
|
||||
$parser = new Less_Parser(['compress' => true, 'cache_dir' => storage_path().'/less']);
|
||||
|
||||
$css = [];
|
||||
$dir = $this->getAssetDirectory();
|
||||
foreach ($this->files['less'] as $file) {
|
||||
$parser->parseFile($file);
|
||||
}
|
||||
|
||||
foreach ($this->less as $less) {
|
||||
$parser->parse($less);
|
||||
}
|
||||
|
||||
return $parser->getCss();
|
||||
}
|
||||
|
||||
public function compileCSS()
|
||||
{
|
||||
$css = $this->compileLess();
|
||||
|
||||
foreach ($this->files['css'] as $file) {
|
||||
$css .= $this->storage->get($file);
|
||||
}
|
||||
|
||||
// minify
|
||||
|
||||
return $css;
|
||||
}
|
||||
|
||||
public function compileJS()
|
||||
{
|
||||
$js = '';
|
||||
|
||||
foreach ($this->files['js'] as $file) {
|
||||
$js .= $this->storage->get($file).';';
|
||||
}
|
||||
|
||||
// minify
|
||||
|
||||
return $js;
|
||||
}
|
||||
}
|
@ -21,11 +21,12 @@ class ExtensionsServiceProvider extends ServiceProvider
|
||||
$providers = [];
|
||||
|
||||
foreach ($extensions as $extension) {
|
||||
if (file_exists($file = base_path().'/extensions/'.$extension.'/bootstrap.php')) {
|
||||
if (file_exists($file = public_path().'/extensions/'.$extension.'/bootstrap.php') ||
|
||||
file_exists($file = base_path().'/extensions/'.$extension.'/bootstrap.php')) {
|
||||
$providers[$extension] = require $file;
|
||||
}
|
||||
}
|
||||
|
||||
// @todo store $providers somewhere so that extensions can talk to each other
|
||||
// @todo store $providers somewhere (in Core?) so that extensions can talk to each other
|
||||
}
|
||||
}
|
||||
|
@ -17,8 +17,14 @@ class ServiceProvider extends IlluminateServiceProvider
|
||||
|
||||
public function extend()
|
||||
{
|
||||
foreach (func_get_args() as $extender) {
|
||||
$extender->extend($this->app);
|
||||
// @todo don't support func_get_args
|
||||
foreach (func_get_args() as $extenders) {
|
||||
if (! is_array($extenders)) {
|
||||
$extenders = [$extenders];
|
||||
}
|
||||
foreach ($extenders as $extender) {
|
||||
$extender->extend($this->app);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
9
framework/core/stubs/extension/bootstrap.php
Normal file
9
framework/core/stubs/extension/bootstrap.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
// Require the extension's composer autoload file. This will enable all of our
|
||||
// classes in the src directory to be autoloaded.
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
// Register our service provider with the Flarum application. In here we can
|
||||
// register bindings and execute code when the application boots.
|
||||
return $this->app->register('{{namespace}}\{{classPrefix}}ServiceProvider');
|
7
framework/core/stubs/extension/composer.json
Normal file
7
framework/core/stubs/extension/composer.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"{{escapedNamespace}}\\": "src/"
|
||||
}
|
||||
}
|
||||
}
|
3
framework/core/stubs/extension/js/.gitignore
vendored
Normal file
3
framework/core/stubs/extension/js/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
bower_components
|
||||
node_modules
|
||||
dist
|
5
framework/core/stubs/extension/js/Gulpfile.js
Normal file
5
framework/core/stubs/extension/js/Gulpfile.js
Normal file
@ -0,0 +1,5 @@
|
||||
var gulp = require('flarum-gulp');
|
||||
|
||||
gulp({
|
||||
modulePrefix: '{{name}}'
|
||||
});
|
8
framework/core/stubs/extension/js/bootstrap.js
vendored
Normal file
8
framework/core/stubs/extension/js/bootstrap.js
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
import { extend, override } from 'flarum/extension-utils';
|
||||
import app from 'flarum/app';
|
||||
|
||||
app.initializers.add('{{name}}', function() {
|
||||
|
||||
// @todo
|
||||
|
||||
});
|
7
framework/core/stubs/extension/js/package.json
Normal file
7
framework/core/stubs/extension/js/package.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"gulp": "^3.8.11",
|
||||
"flarum-gulp": "git+https://github.com/flarum/gulp.git"
|
||||
}
|
||||
}
|
0
framework/core/stubs/extension/less/extension.less
Normal file
0
framework/core/stubs/extension/less/extension.less
Normal file
2
framework/core/stubs/extension/locale/en.yml
Normal file
2
framework/core/stubs/extension/locale/en.yml
Normal file
@ -0,0 +1,2 @@
|
||||
{{name}}:
|
||||
# hello_world: Hello, world!
|
41
framework/core/stubs/extension/src/ServiceProvider.php
Normal file
41
framework/core/stubs/extension/src/ServiceProvider.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php namespace {{namespace}};
|
||||
|
||||
use Flarum\Support\ServiceProvider;
|
||||
use Flarum\Extend\ForumAssets;
|
||||
use Flarum\Extend\Locale;
|
||||
use Flarum\Extend\ForumTranslations;
|
||||
|
||||
class {{classPrefix}}ServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap the application events.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
$this->extend(
|
||||
new ForumAssets([
|
||||
__DIR__.'/../js/dist/extension.js',
|
||||
__DIR__.'/../less/extension.less'
|
||||
]),
|
||||
|
||||
(new Locale('en'))->translations(__DIR__.'/../locale/en.yml'),
|
||||
|
||||
new ForumTranslations([
|
||||
// Add the keys of translations you would like to be available
|
||||
// for use by the JS client application.
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the service provider.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user