From c1af2168723634b5291a75f0a2402915b8aea0c7 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Sun, 7 Jun 2015 10:12:48 +0930 Subject: [PATCH 01/44] Fix user profile activity regression --- framework/core/js/forum/src/components/posted-activity.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/core/js/forum/src/components/posted-activity.js b/framework/core/js/forum/src/components/posted-activity.js index 9b62d620a..e2e65ab68 100644 --- a/framework/core/js/forum/src/components/posted-activity.js +++ b/framework/core/js/forum/src/components/posted-activity.js @@ -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))) ]) ]); } From 4b92840fdeee2392679d0167d32c4b525492f214 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Mon, 8 Jun 2015 09:34:39 +0930 Subject: [PATCH 02/44] Prevent formatter from being invoked if bio is empty --- framework/core/src/Core/Models/User.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/core/src/Core/Models/User.php b/framework/core/src/Core/Models/User.php index 131530b83..126b938c5 100755 --- a/framework/core/src/Core/Models/User.php +++ b/framework/core/src/Core/Models/User.php @@ -200,7 +200,7 @@ class User extends Model */ public function getBioHtmlAttribute($value) { - if (! $value) { + if ($value === null) { $this->bio_html = $value = static::formatBio($this->bio); $this->save(); } From 4a2d0ad76e0a3e49833a58c1f9f9645b40df1116 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Mon, 8 Jun 2015 09:37:30 +0930 Subject: [PATCH 03/44] Make HTMLPurifier config extensible; allow images --- .../src/Core/Formatter/FormatterManager.php | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/framework/core/src/Core/Formatter/FormatterManager.php b/framework/core/src/Core/Formatter/FormatterManager.php index 236922de6..79093de8a 100644 --- a/framework/core/src/Core/Formatter/FormatterManager.php +++ b/framework/core/src/Core/Formatter/FormatterManager.php @@ -15,6 +15,8 @@ class FormatterManager */ protected $container; + public $config; + /** * Create a new formatter manager instance. * @@ -24,6 +26,17 @@ class FormatterManager public function __construct(Container $container = null) { $this->container = $container ?: new 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) @@ -67,18 +80,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); From 7ea3252776f7ebc251762880be01ce4575a9e096 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Mon, 8 Jun 2015 09:47:28 +0930 Subject: [PATCH 04/44] Make composer sliding animation less jumpy when replying --- .../core/js/forum/src/components/composer.js | 15 +++++---------- .../js/forum/src/initializers/state-helpers.js | 4 +++- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/framework/core/js/forum/src/components/composer.js b/framework/core/js/forum/src/components/composer.js index 5534ba263..fbd2aa8c9 100644 --- a/framework/core/js/forum/src/components/composer.js +++ b/framework/core/js/forum/src/components/composer.js @@ -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); } @@ -167,7 +167,7 @@ class Composer extends Component { } if (this.position() !== Composer.PositionEnum.FULLSCREEN) { - this.updateBodyPadding(true, anchorToBottom); + this.updateBodyPadding(anchorToBottom); } else { this.component.focus(); } @@ -182,17 +182,12 @@ 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'; + updateBodyPadding(anchorToBottom) { var paddingBottom = this.position() !== Composer.PositionEnum.HIDDEN ? this.computedHeight() - parseInt($('#page').css('padding-bottom')) : 0; - $('#content')[func]({paddingBottom}, 'fast'); + $('#content').css({paddingBottom}); if (anchorToBottom) { - if (animate) { - $('html, body').stop(true).animate({scrollTop: $(document).height()}, 'fast'); - } else { - $('html, body').scrollTop($(document).height()); - } + $('html, body').scrollTop($(document).height()); } } diff --git a/framework/core/js/forum/src/initializers/state-helpers.js b/framework/core/js/forum/src/initializers/state-helpers.js index 3363a6ff6..643b86a06 100644 --- a/framework/core/js/forum/src/initializers/state-helpers.js +++ b/framework/core/js/forum/src/initializers/state-helpers.js @@ -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) { From ef73b0cabbd0edcc1252437319ba4221048de4e2 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Mon, 8 Jun 2015 09:50:07 +0930 Subject: [PATCH 05/44] Clear bio HTML cache when saving bio --- framework/core/src/Core/Models/User.php | 1 + 1 file changed, 1 insertion(+) diff --git a/framework/core/src/Core/Models/User.php b/framework/core/src/Core/Models/User.php index 126b938c5..c9d9f8972 100755 --- a/framework/core/src/Core/Models/User.php +++ b/framework/core/src/Core/Models/User.php @@ -186,6 +186,7 @@ class User extends Model public function changeBio($bio) { $this->bio = $bio; + $this->bio_html = null; $this->raise(new UserBioWasChanged($this)); From a41d02f030e197375543c739a4702c708ca8e98c Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Mon, 8 Jun 2015 14:56:19 +0930 Subject: [PATCH 06/44] Add extension generator command. --- .../src/Console/ConsoleServiceProvider.php | 1 + .../src/Console/GenerateExtensionCommand.php | 127 ++++++++++++++++++ framework/core/stubs/extension/bootstrap.php | 9 ++ framework/core/stubs/extension/composer.json | 11 ++ framework/core/stubs/extension/js/.gitignore | 3 + framework/core/stubs/extension/js/Gulpfile.js | 5 + .../core/stubs/extension/js/bootstrap.js | 7 + .../core/stubs/extension/js/package.json | 7 + .../core/stubs/extension/less/extension.less | 0 .../stubs/extension/src/ServiceProvider.php | 32 +++++ 10 files changed, 202 insertions(+) create mode 100644 framework/core/src/Console/GenerateExtensionCommand.php create mode 100644 framework/core/stubs/extension/bootstrap.php create mode 100644 framework/core/stubs/extension/composer.json create mode 100644 framework/core/stubs/extension/js/.gitignore create mode 100644 framework/core/stubs/extension/js/Gulpfile.js create mode 100644 framework/core/stubs/extension/js/bootstrap.js create mode 100644 framework/core/stubs/extension/js/package.json create mode 100644 framework/core/stubs/extension/less/extension.less create mode 100644 framework/core/stubs/extension/src/ServiceProvider.php diff --git a/framework/core/src/Console/ConsoleServiceProvider.php b/framework/core/src/Console/ConsoleServiceProvider.php index 4d0eafb41..00111c96b 100644 --- a/framework/core/src/Console/ConsoleServiceProvider.php +++ b/framework/core/src/Console/ConsoleServiceProvider.php @@ -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() diff --git a/framework/core/src/Console/GenerateExtensionCommand.php b/framework/core/src/Console/GenerateExtensionCommand.php new file mode 100644 index 000000000..f07da4b4c --- /dev/null +++ b/framework/core/src/Console/GenerateExtensionCommand.php @@ -0,0 +1,127 @@ +app = $app; + } + + /** + * Execute the console command. + * + * @return mixed + */ + public function fire() + { + do { + $name = $this->ask('Extension 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); + } +} diff --git a/framework/core/stubs/extension/bootstrap.php b/framework/core/stubs/extension/bootstrap.php new file mode 100644 index 000000000..1d29892f9 --- /dev/null +++ b/framework/core/stubs/extension/bootstrap.php @@ -0,0 +1,9 @@ +app->register('{{namespace}}\{{classPrefix}}ServiceProvider'); diff --git a/framework/core/stubs/extension/composer.json b/framework/core/stubs/extension/composer.json new file mode 100644 index 000000000..78b078e14 --- /dev/null +++ b/framework/core/stubs/extension/composer.json @@ -0,0 +1,11 @@ +{ + "require": { + "php": ">=5.4.0" + }, + "autoload": { + "psr-4": { + "{{escapedNamespace}}\\": "src/" + } + }, + "minimum-stability": "dev" +} diff --git a/framework/core/stubs/extension/js/.gitignore b/framework/core/stubs/extension/js/.gitignore new file mode 100644 index 000000000..372e20a51 --- /dev/null +++ b/framework/core/stubs/extension/js/.gitignore @@ -0,0 +1,3 @@ +bower_components +node_modules +dist diff --git a/framework/core/stubs/extension/js/Gulpfile.js b/framework/core/stubs/extension/js/Gulpfile.js new file mode 100644 index 000000000..e53f28d21 --- /dev/null +++ b/framework/core/stubs/extension/js/Gulpfile.js @@ -0,0 +1,5 @@ +var gulp = require('flarum-gulp'); + +gulp({ + modulePrefix: '{{name}}' +}); diff --git a/framework/core/stubs/extension/js/bootstrap.js b/framework/core/stubs/extension/js/bootstrap.js new file mode 100644 index 000000000..5643a6449 --- /dev/null +++ b/framework/core/stubs/extension/js/bootstrap.js @@ -0,0 +1,7 @@ +import { extend, override } from 'flarum/extension-utils'; + +app.initializers.add('{{name}}', function() { + + // @todo + +}); diff --git a/framework/core/stubs/extension/js/package.json b/framework/core/stubs/extension/js/package.json new file mode 100644 index 000000000..3e0ef919d --- /dev/null +++ b/framework/core/stubs/extension/js/package.json @@ -0,0 +1,7 @@ +{ + "private": true, + "devDependencies": { + "gulp": "^3.8.11", + "flarum-gulp": "git+https://github.com/flarum/gulp.git" + } +} diff --git a/framework/core/stubs/extension/less/extension.less b/framework/core/stubs/extension/less/extension.less new file mode 100644 index 000000000..e69de29bb diff --git a/framework/core/stubs/extension/src/ServiceProvider.php b/framework/core/stubs/extension/src/ServiceProvider.php new file mode 100644 index 000000000..5b11546d8 --- /dev/null +++ b/framework/core/stubs/extension/src/ServiceProvider.php @@ -0,0 +1,32 @@ +extend( + new ForumAssets([ + __DIR__.'/../js/dist/extension.js', + __DIR__.'/../less/extension.less' + ]) + ); + } + + /** + * Register the service provider. + * + * @return void + */ + public function register() + { + // + } +} From e7f2e62cec4b51b85a0594202906296697e577d3 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Mon, 8 Jun 2015 14:56:49 +0930 Subject: [PATCH 07/44] Load extensions from the root directory, with precedence. --- .../src/Support/Extensions/ExtensionsServiceProvider.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/framework/core/src/Support/Extensions/ExtensionsServiceProvider.php b/framework/core/src/Support/Extensions/ExtensionsServiceProvider.php index 2a48d06b3..a46a7f332 100644 --- a/framework/core/src/Support/Extensions/ExtensionsServiceProvider.php +++ b/framework/core/src/Support/Extensions/ExtensionsServiceProvider.php @@ -22,11 +22,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 } } From b28bf04e5aa0b5328c923b73cc443e2c0d0b9005 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Mon, 8 Jun 2015 14:57:05 +0930 Subject: [PATCH 08/44] Remove old code. --- framework/core/src/Support/AssetManager.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/framework/core/src/Support/AssetManager.php b/framework/core/src/Support/AssetManager.php index be50e0d6f..dd511c366 100644 --- a/framework/core/src/Support/AssetManager.php +++ b/framework/core/src/Support/AssetManager.php @@ -83,12 +83,9 @@ class AssetManager 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) { + || filemtime($file) < $lastModTime) { $this->storage->put($file, $callback()); } From 1c80a509f18fc4537d9487ee0c424db6fdfd8f65 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Mon, 8 Jun 2015 14:57:23 +0930 Subject: [PATCH 09/44] Use new flarum-gulp package. --- framework/core/js/admin/Gulpfile.js | 67 +++++++------------------- framework/core/js/admin/package.json | 11 +---- framework/core/js/forum/Gulpfile.js | 71 ++++++++-------------------- framework/core/js/forum/package.json | 11 +---- 4 files changed, 42 insertions(+), 118 deletions(-) diff --git a/framework/core/js/admin/Gulpfile.js b/framework/core/js/admin/Gulpfile.js index 466135e1f..e921028ae 100644 --- a/framework/core/js/admin/Gulpfile.js +++ b/framework/core/js/admin/Gulpfile.js @@ -1,51 +1,20 @@ -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: [ + '../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', + outputFile: 'dist/app.js' }); diff --git a/framework/core/js/admin/package.json b/framework/core/js/admin/package.json index 51b539e61..3e0ef919d 100644 --- a/framework/core/js/admin/package.json +++ b/framework/core/js/admin/package.json @@ -1,14 +1,7 @@ { + "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" } } diff --git a/framework/core/js/forum/Gulpfile.js b/framework/core/js/forum/Gulpfile.js index 54b296bc8..ca3d343f9 100644 --- a/framework/core/js/forum/Gulpfile.js +++ b/framework/core/js/forum/Gulpfile.js @@ -1,53 +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/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: [ + '../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', + outputFile: 'dist/app.js' }); diff --git a/framework/core/js/forum/package.json b/framework/core/js/forum/package.json index 51b539e61..3e0ef919d 100644 --- a/framework/core/js/forum/package.json +++ b/framework/core/js/forum/package.json @@ -1,14 +1,7 @@ { + "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" } } From 1826d1597091ff3b5616282f7facdf677e96cd04 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Mon, 8 Jun 2015 15:05:35 +0930 Subject: [PATCH 10/44] Clean up extension stub. --- framework/core/stubs/extension/composer.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/framework/core/stubs/extension/composer.json b/framework/core/stubs/extension/composer.json index 78b078e14..74f608c32 100644 --- a/framework/core/stubs/extension/composer.json +++ b/framework/core/stubs/extension/composer.json @@ -1,11 +1,7 @@ { - "require": { - "php": ">=5.4.0" - }, "autoload": { "psr-4": { "{{escapedNamespace}}\\": "src/" } - }, - "minimum-stability": "dev" + } } From 0e15aafed7a453637d0b2560d4e4e02341d2d13e Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Mon, 8 Jun 2015 15:28:45 +0930 Subject: [PATCH 11/44] Externalize babel helpers. Saves 2kB after minify+gzip :D --- framework/core/js/admin/Gulpfile.js | 2 ++ framework/core/js/admin/package.json | 3 ++- framework/core/js/forum/Gulpfile.js | 2 ++ framework/core/js/forum/package.json | 3 ++- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/framework/core/js/admin/Gulpfile.js b/framework/core/js/admin/Gulpfile.js index e921028ae..c695c9a1e 100644 --- a/framework/core/js/admin/Gulpfile.js +++ b/framework/core/js/admin/Gulpfile.js @@ -2,6 +2,7 @@ var gulp = require('flarum-gulp'); 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', @@ -16,5 +17,6 @@ gulp({ ], bootstrapFiles: [], modulePrefix: 'flarum', + externalHelpers: true, outputFile: 'dist/app.js' }); diff --git a/framework/core/js/admin/package.json b/framework/core/js/admin/package.json index 3e0ef919d..81fb6266e 100644 --- a/framework/core/js/admin/package.json +++ b/framework/core/js/admin/package.json @@ -2,6 +2,7 @@ "private": true, "devDependencies": { "gulp": "^3.8.11", - "flarum-gulp": "git+https://github.com/flarum/gulp.git" + "flarum-gulp": "git+https://github.com/flarum/gulp.git", + "babel-core": "^5.0.0" } } diff --git a/framework/core/js/forum/Gulpfile.js b/framework/core/js/forum/Gulpfile.js index ca3d343f9..37f0b0332 100644 --- a/framework/core/js/forum/Gulpfile.js +++ b/framework/core/js/forum/Gulpfile.js @@ -2,6 +2,7 @@ var gulp = require('flarum-gulp'); 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', @@ -18,5 +19,6 @@ gulp({ ], bootstrapFiles: [], modulePrefix: 'flarum', + externalHelpers: true, outputFile: 'dist/app.js' }); diff --git a/framework/core/js/forum/package.json b/framework/core/js/forum/package.json index 3e0ef919d..81fb6266e 100644 --- a/framework/core/js/forum/package.json +++ b/framework/core/js/forum/package.json @@ -2,6 +2,7 @@ "private": true, "devDependencies": { "gulp": "^3.8.11", - "flarum-gulp": "git+https://github.com/flarum/gulp.git" + "flarum-gulp": "git+https://github.com/flarum/gulp.git", + "babel-core": "^5.0.0" } } From fbbeebbdee7b80f8c4aa7a54e4a0009e09b63dc3 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Wed, 10 Jun 2015 13:59:25 +0930 Subject: [PATCH 12/44] Clean up, use time helper rather than util --- framework/core/js/forum/src/components/terminal-post.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/framework/core/js/forum/src/components/terminal-post.js b/framework/core/js/forum/src/components/terminal-post.js index da9fa00de..fdabecd6d 100644 --- a/framework/core/js/forum/src/components/terminal-post.js +++ b/framework/core/js/forum/src/components/terminal-post.js @@ -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) ]) } } From f82aaa82a5cd1840df88eec1a9daa5b235a135e4 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Wed, 10 Jun 2015 14:23:56 +0930 Subject: [PATCH 13/44] Lay the groundwork for translation & refactor asset compilation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ditched the idea of having language packs as extensions. Reasoning: 1. Because we use machine keys for translations (rather than English keys), extensions need to be able to define default translations. If English translations are to be included in extensions and not in a language pack extension, then it doesn’t make sense to have other languages as language pack extensions. Inconsistency → complexity. 2. Translations should maintain version parity with their respective extensions. There’s no way to do this if extension translations are external to the extension. Instead, localisation will be a core effort, as well as a per-extension effort. Translators will be encouraged to send PRs to core + extensions. In core, each locale has a directory containing three files: - translations.yml - config.js: contains pluralisation logic for the JS app, as well as moment.js localisation if necessary - config.php: contains pluralisation logic for the PHP app Extensions can use the Flarum\Extend\Locale extender to add/override translations/config to a locale. Asset compilation has been completely refactored with a better architecture. Translations + config.js are compiled and cached for the currently active locale. --- framework/core/js/lib/utils/app.js | 6 + framework/core/js/lib/utils/translator.js | 32 ++++ framework/core/locale/en/config.js | 3 + framework/core/locale/en/config.php | 7 + framework/core/locale/en/translations.yml | 2 + .../2015_02_24_000000_create_users_table.php | 1 + framework/core/src/Assets/AssetManager.php | 60 +++++++ .../core/src/Assets/CompilerInterface.php | 10 ++ framework/core/src/Assets/JsCompiler.php | 9 + framework/core/src/Assets/LessCompiler.php | 26 +++ .../core/src/Assets/RevisionCompiler.php | 95 ++++++++++ .../core/src/Core/CoreServiceProvider.php | 14 ++ .../src/Core/Seeders/ConfigTableSeeder.php | 1 + .../core/src/Extend/AdminTranslations.php | 18 ++ framework/core/src/Extend/ForumAssets.php | 2 +- .../core/src/Extend/ForumTranslations.php | 19 ++ framework/core/src/Extend/Locale.php | 57 ++++++ .../core/src/Forum/Actions/IndexAction.php | 74 ++++++-- .../core/src/Forum/ForumServiceProvider.php | 9 +- framework/core/src/Locale/JsCompiler.php | 24 +++ framework/core/src/Locale/LocaleManager.php | 65 +++++++ .../core/src/Locale/TranslationCompiler.php | 29 ++++ framework/core/src/Locale/Translator.php | 42 +++++ framework/core/src/Support/AssetManager.php | 163 ------------------ 24 files changed, 589 insertions(+), 179 deletions(-) create mode 100644 framework/core/js/lib/utils/translator.js create mode 100644 framework/core/locale/en/config.js create mode 100644 framework/core/locale/en/config.php create mode 100644 framework/core/locale/en/translations.yml create mode 100644 framework/core/src/Assets/AssetManager.php create mode 100644 framework/core/src/Assets/CompilerInterface.php create mode 100644 framework/core/src/Assets/JsCompiler.php create mode 100644 framework/core/src/Assets/LessCompiler.php create mode 100644 framework/core/src/Assets/RevisionCompiler.php create mode 100644 framework/core/src/Extend/AdminTranslations.php create mode 100644 framework/core/src/Extend/ForumTranslations.php create mode 100644 framework/core/src/Extend/Locale.php create mode 100644 framework/core/src/Locale/JsCompiler.php create mode 100644 framework/core/src/Locale/LocaleManager.php create mode 100644 framework/core/src/Locale/TranslationCompiler.php create mode 100644 framework/core/src/Locale/Translator.php delete mode 100644 framework/core/src/Support/AssetManager.php diff --git a/framework/core/js/lib/utils/app.js b/framework/core/js/lib/utils/app.js index 8857a96d7..998b2d224 100644 --- a/framework/core/js/lib/utils/app.js +++ b/framework/core/js/lib/utils/app.js @@ -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; diff --git a/framework/core/js/lib/utils/translator.js b/framework/core/js/lib/utils/translator.js new file mode 100644 index 000000000..e8235e864 --- /dev/null +++ b/framework/core/js/lib/utils/translator.js @@ -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; + } + } +} diff --git a/framework/core/locale/en/config.js b/framework/core/locale/en/config.js new file mode 100644 index 000000000..42739e270 --- /dev/null +++ b/framework/core/locale/en/config.js @@ -0,0 +1,3 @@ +app.translator.plural = function(count) { + return count == 1 ? 'one' : 'other'; +}; diff --git a/framework/core/locale/en/config.php b/framework/core/locale/en/config.php new file mode 100644 index 000000000..3ca0f9cf2 --- /dev/null +++ b/framework/core/locale/en/config.php @@ -0,0 +1,7 @@ + function ($count) { + return $count == 1 ? 'one' : 'other'; + } +]; diff --git a/framework/core/locale/en/translations.yml b/framework/core/locale/en/translations.yml new file mode 100644 index 000000000..898cbf708 --- /dev/null +++ b/framework/core/locale/en/translations.yml @@ -0,0 +1,2 @@ +core: + diff --git a/framework/core/migrations/2015_02_24_000000_create_users_table.php b/framework/core/migrations/2015_02_24_000000_create_users_table.php index ec637bfc0..31656606d 100644 --- a/framework/core/migrations/2015_02_24_000000_create_users_table.php +++ b/framework/core/migrations/2015_02_24_000000_create_users_table.php @@ -19,6 +19,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(); diff --git a/framework/core/src/Assets/AssetManager.php b/framework/core/src/Assets/AssetManager.php new file mode 100644 index 000000000..1e6df4fc6 --- /dev/null +++ b/framework/core/src/Assets/AssetManager.php @@ -0,0 +1,60 @@ +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(); + } +} diff --git a/framework/core/src/Assets/CompilerInterface.php b/framework/core/src/Assets/CompilerInterface.php new file mode 100644 index 000000000..a5946dde0 --- /dev/null +++ b/framework/core/src/Assets/CompilerInterface.php @@ -0,0 +1,10 @@ + 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(); + } +} diff --git a/framework/core/src/Assets/RevisionCompiler.php b/framework/core/src/Assets/RevisionCompiler.php new file mode 100644 index 000000000..171732630 --- /dev/null +++ b/framework/core/src/Assets/RevisionCompiler.php @@ -0,0 +1,95 @@ +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)); + } +} diff --git a/framework/core/src/Core/CoreServiceProvider.php b/framework/core/src/Core/CoreServiceProvider.php index 5556a18f7..5ff1ec549 100644 --- a/framework/core/src/Core/CoreServiceProvider.php +++ b/framework/core/src/Core/CoreServiceProvider.php @@ -18,6 +18,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 { @@ -56,6 +57,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') + ); + } } /** @@ -72,6 +84,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' diff --git a/framework/core/src/Core/Seeders/ConfigTableSeeder.php b/framework/core/src/Core/Seeders/ConfigTableSeeder.php index 96d58846e..8efb0adc7 100644 --- a/framework/core/src/Core/Seeders/ConfigTableSeeder.php +++ b/framework/core/src/Core/Seeders/ConfigTableSeeder.php @@ -20,6 +20,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, diff --git a/framework/core/src/Extend/AdminTranslations.php b/framework/core/src/Extend/AdminTranslations.php new file mode 100644 index 000000000..bea1f8fbb --- /dev/null +++ b/framework/core/src/Extend/AdminTranslations.php @@ -0,0 +1,18 @@ +keys = $keys; + } + + public function extend(Container $container) + { + + } +} diff --git a/framework/core/src/Extend/ForumAssets.php b/framework/core/src/Extend/ForumAssets.php index 20fb0a260..7071f8eff 100644 --- a/framework/core/src/Extend/ForumAssets.php +++ b/framework/core/src/Extend/ForumAssets.php @@ -14,7 +14,7 @@ class ForumAssets implements ExtenderInterface public function extend(Application $app) { $app['events']->listen('Flarum\Forum\Events\RenderView', function ($event) { - $event->assets->addFile($this->files); + $event->assets->addFiles($this->files); }); } } diff --git a/framework/core/src/Extend/ForumTranslations.php b/framework/core/src/Extend/ForumTranslations.php new file mode 100644 index 000000000..3fae872d1 --- /dev/null +++ b/framework/core/src/Extend/ForumTranslations.php @@ -0,0 +1,19 @@ +keys = $keys; + } + + public function extend(Application $container) + { + IndexAction::$translations = array_merge(IndexAction::$translations, $this->keys); + } +} diff --git a/framework/core/src/Extend/Locale.php b/framework/core/src/Extend/Locale.php new file mode 100644 index 000000000..817a92d2c --- /dev/null +++ b/framework/core/src/Extend/Locale.php @@ -0,0 +1,57 @@ +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(Application $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); + } + } +} diff --git a/framework/core/src/Forum/Actions/IndexAction.php b/framework/core/src/Forum/Actions/IndexAction.php index 18f70afec..c1d36890b 100644 --- a/framework/core/src/Forum/Actions/IndexAction.php +++ b/framework/core/src/Forum/Actions/IndexAction.php @@ -10,9 +10,15 @@ use DB; use Flarum\Forum\Events\RenderView; use Flarum\Api\Request as ApiRequest; use Flarum\Core; +use Flarum\Assets\AssetManager; +use Flarum\Assets\JsCompiler; +use Flarum\Assets\LessCompiler; +use Flarum\Locale\JsCompiler as LocaleJsCompiler; class IndexAction extends BaseAction { + public static $translations = []; + public function handle(Request $request, $params = []) { $config = DB::table('config')->whereIn('key', ['base_url', 'api_url', 'forum_title', 'welcome_title', 'welcome_message'])->lists('value', 'key'); @@ -44,23 +50,63 @@ class IndexAction extends BaseAction ->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; } } diff --git a/framework/core/src/Forum/ForumServiceProvider.php b/framework/core/src/Forum/ForumServiceProvider.php index 9ce974e34..67a6f4374 100644 --- a/framework/core/src/Forum/ForumServiceProvider.php +++ b/framework/core/src/Forum/ForumServiceProvider.php @@ -1,7 +1,8 @@ extend( + new ForumTranslations([ + // + ]) + ); } /** diff --git a/framework/core/src/Locale/JsCompiler.php b/framework/core/src/Locale/JsCompiler.php new file mode 100644 index 000000000..de2d82b3c --- /dev/null +++ b/framework/core/src/Locale/JsCompiler.php @@ -0,0 +1,24 @@ +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; + } +} diff --git a/framework/core/src/Locale/LocaleManager.php b/framework/core/src/Locale/LocaleManager.php new file mode 100644 index 000000000..bc9e600e8 --- /dev/null +++ b/framework/core/src/Locale/LocaleManager.php @@ -0,0 +1,65 @@ +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; + } +} diff --git a/framework/core/src/Locale/TranslationCompiler.php b/framework/core/src/Locale/TranslationCompiler.php new file mode 100644 index 000000000..ce625007a --- /dev/null +++ b/framework/core/src/Locale/TranslationCompiler.php @@ -0,0 +1,29 @@ +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; + } +} diff --git a/framework/core/src/Locale/Translator.php b/framework/core/src/Locale/Translator.php new file mode 100644 index 000000000..239df50c9 --- /dev/null +++ b/framework/core/src/Locale/Translator.php @@ -0,0 +1,42 @@ +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; + } + } +} diff --git a/framework/core/src/Support/AssetManager.php b/framework/core/src/Support/AssetManager.php deleted file mode 100644 index dd511c366..000000000 --- a/framework/core/src/Support/AssetManager.php +++ /dev/null @@ -1,163 +0,0 @@ - [], - '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)); - } - - if (! file_exists($file = $dir.'/'.$this->name.'-'.$revision.'.'.$type) - || filemtime($file) < $lastModTime) { - $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; - } -} From 2180e15757b67dbc048836e1f15da8ab6ba64afd Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Wed, 10 Jun 2015 14:42:14 +0930 Subject: [PATCH 14/44] Fix incorrect highlighting of post excerpts --- framework/core/js/forum/src/components/post-preview.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/core/js/forum/src/components/post-preview.js b/framework/core/js/forum/src/components/post-preview.js index 592916cb9..30ace129a 100644 --- a/framework/core/js/forum/src/components/post-preview.js +++ b/framework/core/js/forum/src/components/post-preview.js @@ -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); } From 4ded9906bdecf9ce8726d894ec56c696f5d9cd10 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Thu, 11 Jun 2015 18:38:48 +0930 Subject: [PATCH 15/44] Fix sort menu not displaying the current option --- framework/core/js/forum/src/components/index-page.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/core/js/forum/src/components/index-page.js b/framework/core/js/forum/src/components/index-page.js index fbb82b025..6fc133f9f 100644 --- a/framework/core/js/forum/src/components/index-page.js +++ b/framework/core/js/forum/src/components/index-page.js @@ -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) }) ); From 548f2879de3d29253eda43a08124fc60f9c25714 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Thu, 11 Jun 2015 18:39:12 +0930 Subject: [PATCH 16/44] Fix incorrect visible range in post scrubber --- framework/core/js/forum/src/components/post-scrubber.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/core/js/forum/src/components/post-scrubber.js b/framework/core/js/forum/src/components/post-scrubber.js index d11cb116b..362988523 100644 --- a/framework/core/js/forum/src/components/post-scrubber.js +++ b/framework/core/js/forum/src/components/post-scrubber.js @@ -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 = ''; From 914528d6c4ce2ecdc0e342260fafbd19607a4e12 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Thu, 11 Jun 2015 18:39:36 +0930 Subject: [PATCH 17/44] Fix scrolling to first post not working --- framework/core/js/forum/src/components/post-stream.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/core/js/forum/src/components/post-stream.js b/framework/core/js/forum/src/components/post-stream.js index 6a3e6049d..35dd23f1d 100644 --- a/framework/core/js/forum/src/components/post-stream.js +++ b/framework/core/js/forum/src/components/post-stream.js @@ -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); } /** From cc6113243adfbed92a1e1c81072d390377039b20 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Thu, 11 Jun 2015 18:40:21 +0930 Subject: [PATCH 18/44] Don't anchor scroll when the top of the page has been reached --- framework/core/js/forum/src/components/post-stream.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/core/js/forum/src/components/post-stream.js b/framework/core/js/forum/src/components/post-stream.js index 35dd23f1d..d92229710 100644 --- a/framework/core/js/forum/src/components/post-stream.js +++ b/framework/core/js/forum/src/components/post-stream.js @@ -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(); From f667313cc228af07bab58ec191c37009151eb5d3 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Thu, 11 Jun 2015 18:41:13 +0930 Subject: [PATCH 19/44] Improve ordering of list items when specified key doesn't exist --- framework/core/js/lib/utils/item-list.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/framework/core/js/lib/utils/item-list.js b/framework/core/js/lib/utils/item-list.js index 8d7f8c511..0b5bfb9fb 100644 --- a/framework/core/js/lib/utils/item-list.js +++ b/framework/core/js/lib/utils/item-list.js @@ -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); } From 443a231aa53ed8cbcbed8fee043555e62c2482e4 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Thu, 11 Jun 2015 18:42:05 +0930 Subject: [PATCH 20/44] Increase text contrast I think you will like this change @franzliedke :) --- framework/core/less/forum/index.less | 3 +-- framework/core/less/lib/variables.less | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/framework/core/less/forum/index.less b/framework/core/less/forum/index.less index c1fc1ec2e..bb57786f4 100644 --- a/framework/core/less/forum/index.less +++ b/framework/core/less/forum/index.less @@ -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 { diff --git a/framework/core/less/lib/variables.less b/framework/core/less/lib/variables.less index d85161fd7..7dc335727 100644 --- a/framework/core/less/lib/variables.less +++ b/framework/core/less/lib/variables.less @@ -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); } From b6306efe01dcad763c9d9668692359a3d68a46aa Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Thu, 11 Jun 2015 18:42:26 +0930 Subject: [PATCH 21/44] Add newline in-between JS files, in case last line is a comment --- framework/core/src/Assets/JsCompiler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/core/src/Assets/JsCompiler.php b/framework/core/src/Assets/JsCompiler.php index 3fc5a0c07..56a126e4b 100644 --- a/framework/core/src/Assets/JsCompiler.php +++ b/framework/core/src/Assets/JsCompiler.php @@ -4,6 +4,6 @@ class JsCompiler extends RevisionCompiler { public function format($string) { - return $string.';'; + return $string.";\n"; } } From f67ad7ab982af00389fc7985c1a23044b31e53b7 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Thu, 11 Jun 2015 18:42:49 +0930 Subject: [PATCH 22/44] API: Reorder Extend\Relationship arguments --- framework/core/src/Extend/Relationship.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/framework/core/src/Extend/Relationship.php b/framework/core/src/Extend/Relationship.php index de16a6724..c002b1395 100644 --- a/framework/core/src/Extend/Relationship.php +++ b/framework/core/src/Extend/Relationship.php @@ -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 } From 0ca7003a3536066e4d50c6db6d95395e3f17dc5e Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Thu, 11 Jun 2015 18:45:17 +0930 Subject: [PATCH 23/44] Add missing import in extension stub JS --- framework/core/stubs/extension/js/bootstrap.js | 1 + 1 file changed, 1 insertion(+) diff --git a/framework/core/stubs/extension/js/bootstrap.js b/framework/core/stubs/extension/js/bootstrap.js index 5643a6449..421294ea7 100644 --- a/framework/core/stubs/extension/js/bootstrap.js +++ b/framework/core/stubs/extension/js/bootstrap.js @@ -1,4 +1,5 @@ import { extend, override } from 'flarum/extension-utils'; +import app from 'flarum/app'; app.initializers.add('{{name}}', function() { From 65df4c3a3383c8ee02aa0cd0940a705ddd08ca16 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Thu, 11 Jun 2015 18:45:31 +0930 Subject: [PATCH 24/44] Add English locale template to extension stub --- framework/core/stubs/extension/locale/en.yml | 2 ++ .../core/stubs/extension/src/ServiceProvider.php | 11 ++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 framework/core/stubs/extension/locale/en.yml diff --git a/framework/core/stubs/extension/locale/en.yml b/framework/core/stubs/extension/locale/en.yml new file mode 100644 index 000000000..7d28ca184 --- /dev/null +++ b/framework/core/stubs/extension/locale/en.yml @@ -0,0 +1,2 @@ +{{name}}: + # hello_world: Hello, world! diff --git a/framework/core/stubs/extension/src/ServiceProvider.php b/framework/core/stubs/extension/src/ServiceProvider.php index 5b11546d8..e62608409 100644 --- a/framework/core/stubs/extension/src/ServiceProvider.php +++ b/framework/core/stubs/extension/src/ServiceProvider.php @@ -2,6 +2,8 @@ use Flarum\Support\ServiceProvider; use Flarum\Extend\ForumAssets; +use Flarum\Extend\Locale; +use Flarum\Extend\ForumTranslations; class {{classPrefix}}ServiceProvider extends ServiceProvider { @@ -16,7 +18,14 @@ class {{classPrefix}}ServiceProvider extends ServiceProvider 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. + ]), ); } From 8542152c091bba3a42b1dfc4a8e0ac7c7acdbb81 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Fri, 12 Jun 2015 16:37:19 +0930 Subject: [PATCH 25/44] Don't pad the body when the composer is minimized --- framework/core/js/forum/src/components/composer.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/framework/core/js/forum/src/components/composer.js b/framework/core/js/forum/src/components/composer.js index fbd2aa8c9..720c24d6e 100644 --- a/framework/core/js/forum/src/components/composer.js +++ b/framework/core/js/forum/src/components/composer.js @@ -183,7 +183,9 @@ class Composer extends Component { // content will still be visible above the composer when the page is // scrolled right to the bottom. updateBodyPadding(anchorToBottom) { - var paddingBottom = this.position() !== Composer.PositionEnum.HIDDEN ? this.computedHeight() - parseInt($('#page').css('padding-bottom')) : 0; + var paddingBottom = this.position() !== Composer.PositionEnum.HIDDEN && this.position() !== Composer.PositionEnum.MINIMIZED + ? this.computedHeight() - parseInt($('#page').css('padding-bottom')) + : 0; $('#content').css({paddingBottom}); if (anchorToBottom) { From 333bb3529d2a9d5b9120ff3308fe979f86177a31 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Fri, 12 Jun 2015 16:37:43 +0930 Subject: [PATCH 26/44] Move between title/post inputs with return and backspace keys --- .../src/components/discussion-composer.js | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/framework/core/js/forum/src/components/discussion-composer.js b/framework/core/js/forum/src/components/discussion-composer.js index eb987b48f..3703b156e 100644 --- a/framework/core/js/forum/src/components/discussion-composer.js +++ b/framework/core/js/forum/src/components/discussion-composer.js @@ -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); } From fe94f2a123b7905bd470bc36cb375e6470cd916e Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Fri, 12 Jun 2015 16:40:20 +0930 Subject: [PATCH 27/44] Make the FormModal component more flexible --- framework/core/js/forum/src/components/change-email-modal.js | 4 ++-- .../core/js/forum/src/components/change-password-modal.js | 4 ++-- .../core/js/forum/src/components/delete-account-modal.js | 4 ++-- .../core/js/forum/src/components/forgot-password-modal.js | 4 ++-- framework/core/js/forum/src/components/form-modal.js | 4 +--- framework/core/js/forum/src/components/login-modal.js | 4 ++-- framework/core/js/forum/src/components/signup-modal.js | 4 ++-- 7 files changed, 13 insertions(+), 15 deletions(-) diff --git a/framework/core/js/forum/src/components/change-email-modal.js b/framework/core/js/forum/src/components/change-email-modal.js index 99da605d2..6dd356df2 100644 --- a/framework/core/js/forum/src/components/change-email-modal.js +++ b/framework/core/js/forum/src/components/change-email-modal.js @@ -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') ]) - ] + ]) }); } diff --git a/framework/core/js/forum/src/components/change-password-modal.js b/framework/core/js/forum/src/components/change-password-modal.js index d8103466b..76a995bf7 100644 --- a/framework/core/js/forum/src/components/change-password-modal.js +++ b/framework/core/js/forum/src/components/change-password-modal.js @@ -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') ]) - ] + ]) }); } diff --git a/framework/core/js/forum/src/components/delete-account-modal.js b/framework/core/js/forum/src/components/delete-account-modal.js index 14300a746..8c82fe252 100644 --- a/framework/core/js/forum/src/components/delete-account-modal.js +++ b/framework/core/js/forum/src/components/delete-account-modal.js @@ -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') ]) - ] + ]) }); } diff --git a/framework/core/js/forum/src/components/forgot-password-modal.js b/framework/core/js/forum/src/components/forgot-password-modal.js index 92a7949e0..befa231e5 100644 --- a/framework/core/js/forum/src/components/forgot-password-modal.js +++ b/framework/core/js/forum/src/components/forgot-password-modal.js @@ -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') ]) - ] + ]) }); } diff --git a/framework/core/js/forum/src/components/form-modal.js b/framework/core/js/forum/src/components/form-modal.js index bfae1bbf7..e2b11892b 100644 --- a/framework/core/js/forum/src/components/form-modal.js +++ b/framework/core/js/forum/src/components/form-modal.js @@ -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) : '' ]) ]), diff --git a/framework/core/js/forum/src/components/login-modal.js b/framework/core/js/forum/src/components/login-modal.js index 3e0897100..6a66cf60b 100644 --- a/framework/core/js/forum/src/components/login-modal.js +++ b/framework/core/js/forum/src/components/login-modal.js @@ -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(); diff --git a/framework/core/js/forum/src/components/signup-modal.js b/framework/core/js/forum/src/components/signup-modal.js index 6b8ddd545..4884462a4 100644 --- a/framework/core/js/forum/src/components/signup-modal.js +++ b/framework/core/js/forum/src/components/signup-modal.js @@ -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? ', From 4286243b7519cc113fe2b907323e80e5bf72e216 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Fri, 12 Jun 2015 16:40:47 +0930 Subject: [PATCH 28/44] Return a promise from the new discussion action --- .../js/forum/src/components/index-page.js | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/framework/core/js/forum/src/components/index-page.js b/framework/core/js/forum/src/components/index-page.js index 6fc133f9f..5ec57cc5d 100644 --- a/framework/core/js/forum/src/components/index-page.js +++ b/framework/core/js/forum/src/components/index-page.js @@ -339,16 +339,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; } /** From 42f7d61a491f7a18677b908d43429bc26d796a50 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Fri, 12 Jun 2015 16:40:57 +0930 Subject: [PATCH 29/44] Add a separator above the delete control --- framework/core/js/forum/src/initializers/discussion-controls.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/framework/core/js/forum/src/initializers/discussion-controls.js b/framework/core/js/forum/src/initializers/discussion-controls.js index 1b1a031fd..57d076836 100644 --- a/framework/core/js/forum/src/initializers/discussion-controls.js +++ b/framework/core/js/forum/src/initializers/discussion-controls.js @@ -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) })); } From 9ef431a542d5b26e00e236c670877b3987cee531 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Fri, 12 Jun 2015 16:41:13 +0930 Subject: [PATCH 30/44] Fix saving of to-many relationships --- framework/core/js/lib/model.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/framework/core/js/lib/model.js b/framework/core/js/lib/model.js index 156bafc9c..6e3ba9dd4 100644 --- a/framework/core/js/lib/model.js +++ b/framework/core/js/lib/model.js @@ -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)}; + } } } From 435880733d8f321e276713c6cb7fd521dbeb1b7b Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Fri, 12 Jun 2015 16:41:46 +0930 Subject: [PATCH 31/44] Update permissions --- .../Core/Handlers/Commands/EditDiscussionCommandHandler.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/framework/core/src/Core/Handlers/Commands/EditDiscussionCommandHandler.php b/framework/core/src/Core/Handlers/Commands/EditDiscussionCommandHandler.php index 49c1b8ec0..27f98f1ce 100644 --- a/framework/core/src/Core/Handlers/Commands/EditDiscussionCommandHandler.php +++ b/framework/core/src/Core/Handlers/Commands/EditDiscussionCommandHandler.php @@ -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); } From da461b1be7f5728431b5942c141a5a4b1cb85484 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Mon, 15 Jun 2015 08:59:33 +0930 Subject: [PATCH 32/44] Prefer passing an array to ServiceProvider::extend --- framework/core/src/Support/ServiceProvider.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/framework/core/src/Support/ServiceProvider.php b/framework/core/src/Support/ServiceProvider.php index 55e1071f4..d24261be0 100644 --- a/framework/core/src/Support/ServiceProvider.php +++ b/framework/core/src/Support/ServiceProvider.php @@ -22,8 +22,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); + } } } } From cf7b6974bcf5181719b284815ac6ef89b09a9d4b Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Mon, 15 Jun 2015 08:59:57 +0930 Subject: [PATCH 33/44] Don't add duplicate posts to a discussion --- framework/core/js/forum/src/components/post-stream.js | 2 +- framework/core/js/lib/models/discussion.js | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/framework/core/js/forum/src/components/post-stream.js b/framework/core/js/forum/src/components/post-stream.js index d92229710..61996411b 100644 --- a/framework/core/js/forum/src/components/post-stream.js +++ b/framework/core/js/forum/src/components/post-stream.js @@ -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++; } diff --git a/framework/core/js/lib/models/discussion.js b/framework/core/js/lib/models/discussion.js index ed2cdbeaf..20dee1926 100644 --- a/framework/core/js/lib/models/discussion.js +++ b/framework/core/js/lib/models/discussion.js @@ -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); + } + }); } } } From 7295cffd3234d5e481c3d21d3b575e381ad5cb44 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Mon, 15 Jun 2015 12:17:19 +0930 Subject: [PATCH 34/44] Maintain scroll position when hiding the composer --- .../core/js/forum/src/components/composer.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/framework/core/js/forum/src/components/composer.js b/framework/core/js/forum/src/components/composer.js index 720c24d6e..9398c6b59 100644 --- a/framework/core/js/forum/src/components/composer.js +++ b/framework/core/js/forum/src/components/composer.js @@ -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(anchorToBottom); + this.updateBodyPadding(); + $('html, body').scrollTop(anchorToBottom ? $(document).height() : scrollTop); } else { this.component.focus(); } @@ -182,15 +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(anchorToBottom) { + updateBodyPadding() { var paddingBottom = this.position() !== Composer.PositionEnum.HIDDEN && this.position() !== Composer.PositionEnum.MINIMIZED ? this.computedHeight() - parseInt($('#page').css('padding-bottom')) : 0; $('#content').css({paddingBottom}); - - if (anchorToBottom) { - $('html, body').scrollTop($(document).height()); - } } // Update the height of the stuff inside of the composer. There should be From 385ebf012ddc4cbaa48ec003f5711b5236be55c9 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Mon, 15 Jun 2015 12:18:20 +0930 Subject: [PATCH 35/44] Add a serializer and API action to get information about the forum --- .../core/src/Api/Actions/Forum/ShowAction.php | 29 +++++++++++++++++ .../src/Api/Serializers/BaseSerializer.php | 12 ++++++- .../src/Api/Serializers/ForumSerializer.php | 31 +++++++++++++++++++ framework/core/src/Core/Models/Discussion.php | 2 ++ framework/core/src/Core/Models/Forum.php | 2 ++ .../core/src/Forum/Actions/IndexAction.php | 12 +++++-- 6 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 framework/core/src/Api/Actions/Forum/ShowAction.php create mode 100644 framework/core/src/Api/Serializers/ForumSerializer.php diff --git a/framework/core/src/Api/Actions/Forum/ShowAction.php b/framework/core/src/Api/Actions/Forum/ShowAction.php new file mode 100644 index 000000000..44a80ebbd --- /dev/null +++ b/framework/core/src/Api/Actions/Forum/ShowAction.php @@ -0,0 +1,29 @@ +$relation) ? $model->$relation : $model->$relation()->getResults(); + if (! is_null($model->$relation)) { + $data = $model->$relation; + } else { + $relation = $model->$relation(); + if ($relation instanceof Relation) { + $data = $relation->getResults(); + } else { + $data = $relation->get(); + } + } } elseif ($many) { $relationIds = $relation.'_ids'; $data = $model->$relationIds ?: $model->$relation()->get(['id'])->fetch('id')->all(); diff --git a/framework/core/src/Api/Serializers/ForumSerializer.php b/framework/core/src/Api/Serializers/ForumSerializer.php new file mode 100644 index 000000000..56143f844 --- /dev/null +++ b/framework/core/src/Api/Serializers/ForumSerializer.php @@ -0,0 +1,31 @@ + $forum->title + ]; + + return $this->extendAttributes($forum, $attributes); + } +} diff --git a/framework/core/src/Core/Models/Discussion.php b/framework/core/src/Core/Models/Discussion.php index eb3c72116..d36ec93e4 100755 --- a/framework/core/src/Core/Models/Discussion.php +++ b/framework/core/src/Core/Models/Discussion.php @@ -28,6 +28,8 @@ class Discussion extends Model 'last_post_number' => 'integer' ]; + protected static $relationships = []; + /** * The table associated with the model. * diff --git a/framework/core/src/Core/Models/Forum.php b/framework/core/src/Core/Models/Forum.php index 7996ed5fa..078af04d7 100755 --- a/framework/core/src/Core/Models/Forum.php +++ b/framework/core/src/Core/Models/Forum.php @@ -7,6 +7,8 @@ class Forum extends Model { use Permissible; + protected static $relationships = []; + public function getTitleAttribute() { return Core::config('forum_title'); diff --git a/framework/core/src/Forum/Actions/IndexAction.php b/framework/core/src/Forum/Actions/IndexAction.php index c1d36890b..9e6351880 100644 --- a/framework/core/src/Forum/Actions/IndexAction.php +++ b/framework/core/src/Forum/Actions/IndexAction.php @@ -22,10 +22,18 @@ class IndexAction extends BaseAction public function handle(Request $request, $params = []) { $config = DB::table('config')->whereIn('key', ['base_url', 'api_url', 'forum_title', 'welcome_title', 'welcome_message'])->lists('value', 'key'); - $data = []; $session = []; $alert = Session::get('alert'); + $response = app('Flarum\Api\Actions\Forum\ShowAction') + ->handle(new ApiRequest([], $this->actor)) + ->content->toArray(); + + $data = [$response['data']]; + if (isset($response['included'])) { + $data = array_merge($data, $response['included']); + } + if (($user = $this->actor->getUser()) && $user->exists) { $session = [ 'userId' => $user->id, @@ -36,7 +44,7 @@ class IndexAction extends BaseAction ->handle(new ApiRequest(['id' => $user->id], $this->actor)) ->content->toArray(); - $data = [$response['data']]; + $data = array_merge($data, [$response['data']]); if (isset($response['included'])) { $data = array_merge($data, $response['included']); } From 320180efc469916343d4ba425fff173b839a19d0 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Tue, 16 Jun 2015 16:58:10 +0930 Subject: [PATCH 36/44] Remove total results from discussion searching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It’s too inefficient (requires a whole table scan) to do a query like: select count(*) from discussions where [conditions determining visibility] --- .../core/src/Api/Actions/Discussions/IndexAction.php | 6 +----- .../Search/Discussions/DiscussionSearchResults.php | 10 +--------- .../src/Core/Search/Discussions/DiscussionSearcher.php | 4 +--- 3 files changed, 3 insertions(+), 17 deletions(-) diff --git a/framework/core/src/Api/Actions/Discussions/IndexAction.php b/framework/core/src/Api/Actions/Discussions/IndexAction.php index c0cd6aaed..6d75bcece 100644 --- a/framework/core/src/Api/Actions/Discussions/IndexAction.php +++ b/framework/core/src/Api/Actions/Discussions/IndexAction.php @@ -74,11 +74,7 @@ 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) { - $response->content->addMeta('total', $total); - } - - static::addPaginationLinks($response, $request, route('flarum.api.discussions.index'), $total ?: $results->areMoreResults()); + static::addPaginationLinks($response, $request, route('flarum.api.discussions.index'), $results->areMoreResults()); return $results->getDiscussions(); } diff --git a/framework/core/src/Core/Search/Discussions/DiscussionSearchResults.php b/framework/core/src/Core/Search/Discussions/DiscussionSearchResults.php index eb974f773..58219cde2 100644 --- a/framework/core/src/Core/Search/Discussions/DiscussionSearchResults.php +++ b/framework/core/src/Core/Search/Discussions/DiscussionSearchResults.php @@ -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; diff --git a/framework/core/src/Core/Search/Discussions/DiscussionSearcher.php b/framework/core/src/Core/Search/Discussions/DiscussionSearcher.php index 742ba664c..82b571901 100644 --- a/framework/core/src/Core/Search/Discussions/DiscussionSearcher.php +++ b/framework/core/src/Core/Search/Discussions/DiscussionSearcher.php @@ -63,8 +63,6 @@ class DiscussionSearcher implements SearcherInterface $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); } } From f6e60901315e6a90134f851645a97374b2c66aaa Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Tue, 16 Jun 2015 16:59:48 +0930 Subject: [PATCH 37/44] Add todo about query optimization --- framework/core/src/Forum/Actions/IndexAction.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/framework/core/src/Forum/Actions/IndexAction.php b/framework/core/src/Forum/Actions/IndexAction.php index 9e6351880..b03955846 100644 --- a/framework/core/src/Forum/Actions/IndexAction.php +++ b/framework/core/src/Forum/Actions/IndexAction.php @@ -22,6 +22,7 @@ class IndexAction extends BaseAction public function handle(Request $request, $params = []) { $config = DB::table('config')->whereIn('key', ['base_url', 'api_url', 'forum_title', 'welcome_title', 'welcome_message'])->lists('value', 'key'); + $data = []; $session = []; $alert = Session::get('alert'); @@ -40,6 +41,10 @@ class IndexAction extends BaseAction 'token' => Cookie::get('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 = app('Flarum\Api\Actions\Users\ShowAction') ->handle(new ApiRequest(['id' => $user->id], $this->actor)) ->content->toArray(); From 822feb2497c20040d9cf5f9b6e18ee91a56f424a Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Tue, 16 Jun 2015 17:18:02 +0930 Subject: [PATCH 38/44] Cache user permissions between calls --- framework/core/src/Core/Models/User.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/framework/core/src/Core/Models/User.php b/framework/core/src/Core/Models/User.php index c9d9f8972..902c11013 100755 --- a/framework/core/src/Core/Models/User.php +++ b/framework/core/src/Core/Models/User.php @@ -311,9 +311,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() From a890dc211439bf2b12c6f1ba9b75818f1cba73b7 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Tue, 16 Jun 2015 17:21:04 +0930 Subject: [PATCH 39/44] Static relationship collections need to be initialised on subclasses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Will probably make this whole “custom relationships” thing a trait instead of being on the base class --- .../core/src/Api/Serializers/DiscussionBasicSerializer.php | 2 ++ framework/core/src/Api/Serializers/PostBasicSerializer.php | 2 ++ 2 files changed, 4 insertions(+) diff --git a/framework/core/src/Api/Serializers/DiscussionBasicSerializer.php b/framework/core/src/Api/Serializers/DiscussionBasicSerializer.php index b1c09ed4b..d9f69eedd 100644 --- a/framework/core/src/Api/Serializers/DiscussionBasicSerializer.php +++ b/framework/core/src/Api/Serializers/DiscussionBasicSerializer.php @@ -2,6 +2,8 @@ class DiscussionBasicSerializer extends BaseSerializer { + protected static $relationships = []; + /** * The resource type. * diff --git a/framework/core/src/Api/Serializers/PostBasicSerializer.php b/framework/core/src/Api/Serializers/PostBasicSerializer.php index 25aa96518..f8cfaae0b 100644 --- a/framework/core/src/Api/Serializers/PostBasicSerializer.php +++ b/framework/core/src/Api/Serializers/PostBasicSerializer.php @@ -2,6 +2,8 @@ class PostBasicSerializer extends BaseSerializer { + protected static $relationships = []; + /** * The resource type. * From 1ff2c2f90a3e10c48ac5a72e0bffd6a4d9e0ccd1 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Tue, 16 Jun 2015 17:22:15 +0930 Subject: [PATCH 40/44] Add WillRespond event So that custom data can be loaded onto a model before it is serialized. (Tags extension uses this to load tags onto the forum model.) --- .../core/src/Api/Actions/SerializeAction.php | 3 +++ framework/core/src/Api/Events/WillRespond.php | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 framework/core/src/Api/Events/WillRespond.php diff --git a/framework/core/src/Api/Actions/SerializeAction.php b/framework/core/src/Api/Actions/SerializeAction.php index fe26a45fd..a1f28562c 100644 --- a/framework/core/src/Api/Actions/SerializeAction.php +++ b/framework/core/src/Api/Actions/SerializeAction.php @@ -3,6 +3,7 @@ use Flarum\Api\Request; use Flarum\Api\JsonApiRequest; use Flarum\Api\JsonApiResponse; +use Flarum\Api\Events\WillRespond; use Tobscure\JsonApi\SerializerInterface; use Tobscure\JsonApi\Criteria; use Illuminate\Http\Response; @@ -71,6 +72,8 @@ abstract class SerializeAction extends JsonApiAction $data = $this->data($request, $response = new JsonApiResponse); + event(new WillRespond($this, $data, $request, $response)); + $serializer = new static::$serializer($request->actor, $request->include, $request->link); $response->content->setData($this->serialize($serializer, $data)); diff --git a/framework/core/src/Api/Events/WillRespond.php b/framework/core/src/Api/Events/WillRespond.php new file mode 100644 index 000000000..6519c8d7b --- /dev/null +++ b/framework/core/src/Api/Events/WillRespond.php @@ -0,0 +1,20 @@ +action = $action; + $this->data = &$data; + $this->request = $request; + $this->response = $response; + } +} From de9b6ff53064d5e2abfdd7d1f08a6a70a8d1b7d5 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Tue, 16 Jun 2015 17:23:14 +0930 Subject: [PATCH 41/44] Clear the page's min-height when navigating away --- framework/core/js/forum/src/components/index-page.js | 1 + 1 file changed, 1 insertion(+) diff --git a/framework/core/js/forum/src/components/index-page.js b/framework/core/js/forum/src/components/index-page.js index 5ec57cc5d..086998d85 100644 --- a/framework/core/js/forum/src/components/index-page.js +++ b/framework/core/js/forum/src/components/index-page.js @@ -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(''); From 31369bd806b7b82b949d57de6729eeddeb478870 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Tue, 16 Jun 2015 17:33:56 +0930 Subject: [PATCH 42/44] Overhaul permissions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Get rid of Permissible - too complex and inefficient. Replace with: - a “Locked” trait which works similarly but only evaluates logic on hydrated models. - a “VisibleScope” trait which also works similarly but only scopes queries This is all we need, Permissible is overkill. There is only one instance where we have to duplicate some logic (Discussion::scopeVisiblePosts and Post::allow(‘view’, …)) but it’s barely anything. Haven’t decoupled for now, we can definitely look at doing that later. Permissions table seeder slightly updated. Also did a bit of a query audit, there’s still a lot to be done but it’s much better than it was. Some relatively low-hanging fruit detailed in EloquentPostRepository. --- ..._000000_create_users_discussions_table.php | 1 - .../Api/Actions/Discussions/ShowAction.php | 2 +- .../src/Api/Serializers/BaseSerializer.php | 11 +-- .../core/src/Core/CoreServiceProvider.php | 93 ++++++++++--------- framework/core/src/Core/Models/Discussion.php | 29 +++++- framework/core/src/Core/Models/Forum.php | 4 +- framework/core/src/Core/Models/Model.php | 37 +++----- framework/core/src/Core/Models/Post.php | 4 +- framework/core/src/Core/Models/User.php | 6 +- .../EloquentDiscussionRepository.php | 6 +- .../Repositories/EloquentPostRepository.php | 83 +++++++++++++---- .../Repositories/EloquentUserRepository.php | 10 +- .../Search/Discussions/DiscussionSearcher.php | 2 +- .../Core/Seeders/PermissionsTableSeeder.php | 4 +- framework/core/src/Core/Support/Locked.php | 57 ++++++++++++ .../core/src/Core/Support/VisibleScope.php | 20 ++++ 16 files changed, 247 insertions(+), 122 deletions(-) create mode 100644 framework/core/src/Core/Support/Locked.php create mode 100644 framework/core/src/Core/Support/VisibleScope.php diff --git a/framework/core/migrations/2015_02_24_000000_create_users_discussions_table.php b/framework/core/migrations/2015_02_24_000000_create_users_discussions_table.php index bdd5bfea5..33bf57e5b 100644 --- a/framework/core/migrations/2015_02_24_000000_create_users_discussions_table.php +++ b/framework/core/migrations/2015_02_24_000000_create_users_discussions_table.php @@ -14,7 +14,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(); diff --git a/framework/core/src/Api/Actions/Discussions/ShowAction.php b/framework/core/src/Api/Actions/Discussions/ShowAction.php index 3c59872d7..1fa0f48a3 100644 --- a/framework/core/src/Api/Actions/Discussions/ShowAction.php +++ b/framework/core/src/Api/Actions/Discussions/ShowAction.php @@ -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.'); diff --git a/framework/core/src/Api/Serializers/BaseSerializer.php b/framework/core/src/Api/Serializers/BaseSerializer.php index d34e44b08..a5483d678 100644 --- a/framework/core/src/Api/Serializers/BaseSerializer.php +++ b/framework/core/src/Api/Serializers/BaseSerializer.php @@ -55,16 +55,7 @@ abstract class BaseSerializer extends SerializerAbstract $data = $relation($model, $include); } else { if ($include) { - if (! is_null($model->$relation)) { - $data = $model->$relation; - } else { - $relation = $model->$relation(); - if ($relation instanceof Relation) { - $data = $relation->getResults(); - } else { - $data = $relation->get(); - } - } + $data = $model->getRelation($relation); } elseif ($many) { $relationIds = $relation.'_ids'; $data = $model->$relationIds ?: $model->$relation()->get(['id'])->fetch('id')->all(); diff --git a/framework/core/src/Core/CoreServiceProvider.php b/framework/core/src/Core/CoreServiceProvider.php index 5ff1ec549..dcd4fe216 100644 --- a/framework/core/src/Core/CoreServiceProvider.php +++ b/framework/core/src/Core/CoreServiceProvider.php @@ -185,71 +185,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 + } }); } } diff --git a/framework/core/src/Core/Models/Discussion.php b/framework/core/src/Core/Models/Discussion.php index d36ec93e4..7718644ec 100755 --- a/framework/core/src/Core/Models/Discussion.php +++ b/framework/core/src/Core/Models/Discussion.php @@ -1,7 +1,8 @@ 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. * @@ -297,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(); diff --git a/framework/core/src/Core/Models/Forum.php b/framework/core/src/Core/Models/Forum.php index 078af04d7..5980317f1 100755 --- a/framework/core/src/Core/Models/Forum.php +++ b/framework/core/src/Core/Models/Forum.php @@ -1,11 +1,11 @@ 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]; } /** diff --git a/framework/core/src/Core/Models/Post.php b/framework/core/src/Core/Models/Post.php index 5fb1ade7f..baaef5045 100755 --- a/framework/core/src/Core/Models/Post.php +++ b/framework/core/src/Core/Models/Post.php @@ -1,11 +1,11 @@ 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; diff --git a/framework/core/src/Core/Repositories/EloquentPostRepository.php b/framework/core/src/Core/Repositories/EloquentPostRepository.php index b08d12ffb..393b668d7 100644 --- a/framework/core/src/Core/Repositories/EloquentPostRepository.php +++ b/framework/core/src/Core/Repositories/EloquentPostRepository.php @@ -1,10 +1,32 @@ 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; } } diff --git a/framework/core/src/Core/Repositories/EloquentUserRepository.php b/framework/core/src/Core/Repositories/EloquentUserRepository.php index ac584950f..89a143083 100644 --- a/framework/core/src/Core/Repositories/EloquentUserRepository.php +++ b/framework/core/src/Core/Repositories/EloquentUserRepository.php @@ -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; diff --git a/framework/core/src/Core/Search/Discussions/DiscussionSearcher.php b/framework/core/src/Core/Search/Discussions/DiscussionSearcher.php index 82b571901..81e1fbfd5 100644 --- a/framework/core/src/Core/Search/Discussions/DiscussionSearcher.php +++ b/framework/core/src/Core/Search/Discussions/DiscussionSearcher.php @@ -59,7 +59,7 @@ 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); diff --git a/framework/core/src/Core/Seeders/PermissionsTableSeeder.php b/framework/core/src/Core/Seeders/PermissionsTableSeeder.php index ae48890d2..85292636c 100644 --- a/framework/core/src/Core/Seeders/PermissionsTableSeeder.php +++ b/framework/core/src/Core/Seeders/PermissionsTableSeeder.php @@ -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'], ]; diff --git a/framework/core/src/Core/Support/Locked.php b/framework/core/src/Core/Support/Locked.php new file mode 100644 index 000000000..4d0cbd8a9 --- /dev/null +++ b/framework/core/src/Core/Support/Locked.php @@ -0,0 +1,57 @@ +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; + } + } +} diff --git a/framework/core/src/Core/Support/VisibleScope.php b/framework/core/src/Core/Support/VisibleScope.php new file mode 100644 index 000000000..cbe1a5b21 --- /dev/null +++ b/framework/core/src/Core/Support/VisibleScope.php @@ -0,0 +1,20 @@ + Date: Tue, 16 Jun 2015 17:39:47 +0930 Subject: [PATCH 43/44] Add API to add a link to an action --- framework/core/src/Extend/ApiLink.php | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 framework/core/src/Extend/ApiLink.php diff --git a/framework/core/src/Extend/ApiLink.php b/framework/core/src/Extend/ApiLink.php new file mode 100644 index 000000000..6450ad58e --- /dev/null +++ b/framework/core/src/Extend/ApiLink.php @@ -0,0 +1,28 @@ +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; + } + } + } +} From 8a0cf2dcba0703f9aebf2d82591f2d95f0dc7afa Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Tue, 16 Jun 2015 21:55:59 +0930 Subject: [PATCH 44/44] Override static property MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @franzliedke I didn’t realise that static properties are static to the class they are defined on, and not each individual subclass. All of the static members of the SerializeAction class (which are intended for extensions to alter per-action) are being inherited by all actions. Any ideas on how to work around this other than defining every static member on each individual subclass? --- framework/core/src/Api/Actions/Users/ShowAction.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/framework/core/src/Api/Actions/Users/ShowAction.php b/framework/core/src/Api/Actions/Users/ShowAction.php index 768016cd3..551a8638b 100644 --- a/framework/core/src/Api/Actions/Users/ShowAction.php +++ b/framework/core/src/Api/Actions/Users/ShowAction.php @@ -29,6 +29,8 @@ class ShowAction extends SerializeResourceAction 'groups' => true ]; + public static $link = []; + /** * Instantiate the action. *