diff --git a/framework/core/CHANGELOG.md b/framework/core/CHANGELOG.md new file mode 100644 index 000000000..cd46cf93d --- /dev/null +++ b/framework/core/CHANGELOG.md @@ -0,0 +1,44 @@ +# Change Log +All notable changes to Flarum and its bundled extensions will be documented in this file. +This project adheres to [Semantic Versioning](http://semver.org/). + +## [Unreleased][unreleased] +*nothing yet* + +## [0.1.0-beta.2] - 2015-09-15 +### Added +- Check prerequisites (PHP version, extensions, etc.) before installation (#364) +- Enforce maximum title and post length through validation (#53, #338) +- Ctrl+Enter submits posts (#276) +- Syntax highlighting for code blocks (#248) +- All links open in new window, receive rel=nofollow attribute (#247) +- Default build script for extensions (#438) +- Input validation in installer + +### Changed +- Ask for admin password confirmation in installer (#405) +- Increased some text contrasts for accessibility (#390) + +### Fixed +- Discussion list did not work with non-empty database prefix (#269, #380) +- Non-admins could not reset their password (#229) +- Requests ending with a slash resulted in a 404 (#334) +- In rare cases, posts did not load correctly (#295) +- Avatars did not show up when installed in a subfolder (#291) +- Installer crashed when views directory was not writable (#376) +- Table prefix could not be set in web installer (#269) +- Enabling an extension disabled all other extensions (#402) +- Invalid custom CSS could crash the application (#400) +- First posts could not be restored or deleted +- Several design bugs +- Set cookies to be HTTP-only +- Tags: Sometimes, tags could not be dragged for reordering in the admin panel (#341) +- Suspend: Use correct column name in when migrating database +- Lock: Check for correct permission when displaying lock control +- Likes: Allow liking permissions to be configured + +## 0.1.0-beta - 2015-08-27 +First Version + +[unreleased]: https://github.com/flarum/core/compare/v0.1.0-beta.2...HEAD +[0.1.0-beta.2]: https://github.com/flarum/core/compare/v0.1.0-beta...v0.1.0-beta.2 diff --git a/framework/core/composer.json b/framework/core/composer.json index 1f52e0b2f..f2e18d97d 100644 --- a/framework/core/composer.json +++ b/framework/core/composer.json @@ -26,13 +26,14 @@ "tobscure/json-api": "^0.1.1", "oyejorge/less.php": "~1.5", "intervention/image": "^2.3.0", - "s9e/text-formatter": "^0.1.0", + "s9e/text-formatter": "^0.3.2", "psr/http-message": "^1.0", "zendframework/zend-diactoros": "^1.1", "nikic/fast-route": "^0.6", "dflydev/fig-cookies": "^1.0", "symfony/console": "^2.7", - "symfony/yaml": "^2.7" + "symfony/yaml": "^2.7", + "doctrine/dbal": "^2.5" }, "require-dev": { "squizlabs/php_codesniffer": "2.*", diff --git a/framework/core/composer.lock b/framework/core/composer.lock index bb43b0637..9606be396 100644 --- a/framework/core/composer.lock +++ b/framework/core/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "9b1418962fa1c90ff76716ca0356562b", + "hash": "e60ca4174e0cfb22de050c0f15539735", "packages": [ { "name": "danielstjules/stringy", @@ -114,6 +114,354 @@ ], "time": "2015-06-03 18:02:30" }, + { + "name": "doctrine/annotations", + "version": "v1.2.7", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "f25c8aab83e0c3e976fd7d19875f198ccf2f7535" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/f25c8aab83e0c3e976fd7d19875f198ccf2f7535", + "reference": "f25c8aab83e0c3e976fd7d19875f198ccf2f7535", + "shasum": "" + }, + "require": { + "doctrine/lexer": "1.*", + "php": ">=5.3.2" + }, + "require-dev": { + "doctrine/cache": "1.*", + "phpunit/phpunit": "4.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-0": { + "Doctrine\\Common\\Annotations\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Docblock Annotations Parser", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "time": "2015-08-31 12:32:49" + }, + { + "name": "doctrine/cache", + "version": "v1.4.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/cache.git", + "reference": "8c434000f420ade76a07c64cbe08ca47e5c101ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/cache/zipball/8c434000f420ade76a07c64cbe08ca47e5c101ca", + "reference": "8c434000f420ade76a07c64cbe08ca47e5c101ca", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "conflict": { + "doctrine/common": ">2.2,<2.4" + }, + "require-dev": { + "phpunit/phpunit": ">=3.7", + "predis/predis": "~1.0", + "satooshi/php-coveralls": "~0.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5.x-dev" + } + }, + "autoload": { + "psr-0": { + "Doctrine\\Common\\Cache\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Caching library offering an object-oriented API for many cache backends", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "cache", + "caching" + ], + "time": "2015-08-31 12:36:41" + }, + { + "name": "doctrine/collections", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/collections.git", + "reference": "6c1e4eef75f310ea1b3e30945e9f06e652128b8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/collections/zipball/6c1e4eef75f310ea1b3e30945e9f06e652128b8a", + "reference": "6c1e4eef75f310ea1b3e30945e9f06e652128b8a", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-0": { + "Doctrine\\Common\\Collections\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Collections Abstraction library", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "array", + "collections", + "iterator" + ], + "time": "2015-04-14 22:21:58" + }, + { + "name": "doctrine/common", + "version": "v2.5.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/common.git", + "reference": "0009b8f0d4a917aabc971fb089eba80e872f83f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/common/zipball/0009b8f0d4a917aabc971fb089eba80e872f83f9", + "reference": "0009b8f0d4a917aabc971fb089eba80e872f83f9", + "shasum": "" + }, + "require": { + "doctrine/annotations": "1.*", + "doctrine/cache": "1.*", + "doctrine/collections": "1.*", + "doctrine/inflector": "1.*", + "doctrine/lexer": "1.*", + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~3.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6.x-dev" + } + }, + "autoload": { + "psr-0": { + "Doctrine\\Common\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Common Library for Doctrine projects", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "annotations", + "collections", + "eventmanager", + "persistence", + "spl" + ], + "time": "2015-08-31 13:00:22" + }, + { + "name": "doctrine/dbal", + "version": "v2.5.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "628c2256b646ae2417d44e063bce8aec5199d48d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/628c2256b646ae2417d44e063bce8aec5199d48d", + "reference": "628c2256b646ae2417d44e063bce8aec5199d48d", + "shasum": "" + }, + "require": { + "doctrine/common": ">=2.4,<2.6-dev", + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "4.*", + "symfony/console": "2.*" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "bin": [ + "bin/doctrine-dbal" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.5.x-dev" + } + }, + "autoload": { + "psr-0": { + "Doctrine\\DBAL\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Database Abstraction Layer", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "database", + "dbal", + "persistence", + "queryobject" + ], + "time": "2015-01-12 21:52:47" + }, { "name": "doctrine/inflector", "version": "v1.0.1", @@ -182,17 +530,71 @@ "time": "2014-12-20 21:24:13" }, { - "name": "guzzlehttp/psr7", - "version": "1.1.0", + "name": "doctrine/lexer", + "version": "v1.0.1", "source": { "type": "git", - "url": "https://github.com/guzzle/psr7.git", - "reference": "af0e1758de355eb113917ad79c3c0e3604bce4bd" + "url": "https://github.com/doctrine/lexer.git", + "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/af0e1758de355eb113917ad79c3c0e3604bce4bd", - "reference": "af0e1758de355eb113917ad79c3c0e3604bce4bd", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/83893c552fd2045dd78aef794c31e694c37c0b8c", + "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "Doctrine\\Common\\Lexer\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "lexer", + "parser" + ], + "time": "2014-09-09 13:34:57" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "4ef919b0cf3b1989523138b60163bbcb7ba1ff7e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/4ef919b0cf3b1989523138b60163bbcb7ba1ff7e", + "reference": "4ef919b0cf3b1989523138b60163bbcb7ba1ff7e", "shasum": "" }, "require": { @@ -216,7 +618,7 @@ "GuzzleHttp\\Psr7\\": "src/" }, "files": [ - "src/functions.php" + "src/functions_include.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -237,20 +639,20 @@ "stream", "uri" ], - "time": "2015-06-24 19:55:15" + "time": "2015-08-15 19:32:36" }, { "name": "illuminate/bus", - "version": "v5.1.8", + "version": "v5.1.16", "source": { "type": "git", "url": "https://github.com/illuminate/bus.git", - "reference": "e04397b4dcbe36997439ce1654dcb2a187a68a4e" + "reference": "f06735223936552d56733a6e7657040029dd7a7d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/bus/zipball/e04397b4dcbe36997439ce1654dcb2a187a68a4e", - "reference": "e04397b4dcbe36997439ce1654dcb2a187a68a4e", + "url": "https://api.github.com/repos/illuminate/bus/zipball/f06735223936552d56733a6e7657040029dd7a7d", + "reference": "f06735223936552d56733a6e7657040029dd7a7d", "shasum": "" }, "require": { @@ -282,20 +684,20 @@ ], "description": "The Illuminate Bus package.", "homepage": "http://laravel.com", - "time": "2015-07-03 11:51:37" + "time": "2015-08-25 00:21:39" }, { "name": "illuminate/cache", - "version": "v5.1.8", + "version": "v5.1.16", "source": { "type": "git", "url": "https://github.com/illuminate/cache.git", - "reference": "a15a873448c24b9657a91cfd97f65539eb040771" + "reference": "dcd7551151aa752324562ad220e4fe4ee0687116" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/cache/zipball/a15a873448c24b9657a91cfd97f65539eb040771", - "reference": "a15a873448c24b9657a91cfd97f65539eb040771", + "url": "https://api.github.com/repos/illuminate/cache/zipball/dcd7551151aa752324562ad220e4fe4ee0687116", + "reference": "dcd7551151aa752324562ad220e4fe4ee0687116", "shasum": "" }, "require": { @@ -332,11 +734,11 @@ ], "description": "The Illuminate Cache package.", "homepage": "http://laravel.com", - "time": "2015-06-30 21:20:09" + "time": "2015-09-03 11:44:03" }, { "name": "illuminate/config", - "version": "v5.1.8", + "version": "v5.1.16", "source": { "type": "git", "url": "https://github.com/illuminate/config.git", @@ -381,16 +783,16 @@ }, { "name": "illuminate/container", - "version": "v5.1.8", + "version": "v5.1.16", "source": { "type": "git", "url": "https://github.com/illuminate/container.git", - "reference": "248cd26c38d1e29eac74d32d9fff158b2232a3c6" + "reference": "66621248395705cc64bc1ce9262b99b4bfa606f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/container/zipball/248cd26c38d1e29eac74d32d9fff158b2232a3c6", - "reference": "248cd26c38d1e29eac74d32d9fff158b2232a3c6", + "url": "https://api.github.com/repos/illuminate/container/zipball/66621248395705cc64bc1ce9262b99b4bfa606f2", + "reference": "66621248395705cc64bc1ce9262b99b4bfa606f2", "shasum": "" }, "require": { @@ -420,20 +822,20 @@ ], "description": "The Illuminate Container package.", "homepage": "http://laravel.com", - "time": "2015-06-13 00:40:15" + "time": "2015-08-26 23:00:38" }, { "name": "illuminate/contracts", - "version": "v5.1.8", + "version": "v5.1.16", "source": { "type": "git", "url": "https://github.com/illuminate/contracts.git", - "reference": "140dd18acb6be297fbd9870f8dda9d9d0a17c460" + "reference": "610aea16e3d91dfd85db4cf43703403a83b7cba2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/contracts/zipball/140dd18acb6be297fbd9870f8dda9d9d0a17c460", - "reference": "140dd18acb6be297fbd9870f8dda9d9d0a17c460", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/610aea16e3d91dfd85db4cf43703403a83b7cba2", + "reference": "610aea16e3d91dfd85db4cf43703403a83b7cba2", "shasum": "" }, "require": { @@ -462,20 +864,20 @@ ], "description": "The Illuminate Contracts package.", "homepage": "http://laravel.com", - "time": "2015-07-14 01:36:14" + "time": "2015-08-31 12:59:22" }, { "name": "illuminate/database", - "version": "v5.1.8", + "version": "v5.1.16", "source": { "type": "git", "url": "https://github.com/illuminate/database.git", - "reference": "ad33fd28df334f248b97cd7ffd747b78635ec0c4" + "reference": "7655e39c507776c9b9226a8f5619ed9933d3396e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/database/zipball/ad33fd28df334f248b97cd7ffd747b78635ec0c4", - "reference": "ad33fd28df334f248b97cd7ffd747b78635ec0c4", + "url": "https://api.github.com/repos/illuminate/database/zipball/7655e39c507776c9b9226a8f5619ed9933d3396e", + "reference": "7655e39c507776c9b9226a8f5619ed9933d3396e", "shasum": "" }, "require": { @@ -521,20 +923,20 @@ "orm", "sql" ], - "time": "2015-07-20 16:50:34" + "time": "2015-09-03 14:53:32" }, { "name": "illuminate/events", - "version": "v5.1.8", + "version": "v5.1.16", "source": { "type": "git", "url": "https://github.com/illuminate/events.git", - "reference": "e10687896023649477a3f8617f3f4abfb39043ac" + "reference": "851c2351401145d81674991fe8491d560e620ec6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/events/zipball/e10687896023649477a3f8617f3f4abfb39043ac", - "reference": "e10687896023649477a3f8617f3f4abfb39043ac", + "url": "https://api.github.com/repos/illuminate/events/zipball/851c2351401145d81674991fe8491d560e620ec6", + "reference": "851c2351401145d81674991fe8491d560e620ec6", "shasum": "" }, "require": { @@ -566,20 +968,20 @@ ], "description": "The Illuminate Events package.", "homepage": "http://laravel.com", - "time": "2015-06-29 08:51:25" + "time": "2015-08-08 13:06:30" }, { "name": "illuminate/filesystem", - "version": "v5.1.8", + "version": "v5.1.16", "source": { "type": "git", "url": "https://github.com/illuminate/filesystem.git", - "reference": "74b0f13734a9dcd4a4e0be2a0324cfe76d825483" + "reference": "b93043ad8efe4a2ccd0bb202d8493255bb865720" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/filesystem/zipball/74b0f13734a9dcd4a4e0be2a0324cfe76d825483", - "reference": "74b0f13734a9dcd4a4e0be2a0324cfe76d825483", + "url": "https://api.github.com/repos/illuminate/filesystem/zipball/b93043ad8efe4a2ccd0bb202d8493255bb865720", + "reference": "b93043ad8efe4a2ccd0bb202d8493255bb865720", "shasum": "" }, "require": { @@ -616,11 +1018,11 @@ ], "description": "The Illuminate Filesystem package.", "homepage": "http://laravel.com", - "time": "2015-07-11 20:41:11" + "time": "2015-08-24 08:44:48" }, { "name": "illuminate/hashing", - "version": "v5.1.8", + "version": "v5.1.16", "source": { "type": "git", "url": "https://github.com/illuminate/hashing.git", @@ -664,16 +1066,16 @@ }, { "name": "illuminate/mail", - "version": "v5.1.8", + "version": "v5.1.16", "source": { "type": "git", "url": "https://github.com/illuminate/mail.git", - "reference": "de000dc1d2b74b7d8e746a9be2af470f90b4655b" + "reference": "467d6b7d4b8edbd8acb94248c4b294108773760d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/mail/zipball/de000dc1d2b74b7d8e746a9be2af470f90b4655b", - "reference": "de000dc1d2b74b7d8e746a9be2af470f90b4655b", + "url": "https://api.github.com/repos/illuminate/mail/zipball/467d6b7d4b8edbd8acb94248c4b294108773760d", + "reference": "467d6b7d4b8edbd8acb94248c4b294108773760d", "shasum": "" }, "require": { @@ -711,11 +1113,11 @@ ], "description": "The Illuminate Mail package.", "homepage": "http://laravel.com", - "time": "2015-07-11 20:41:11" + "time": "2015-08-08 22:00:02" }, { "name": "illuminate/pipeline", - "version": "v5.1.8", + "version": "v5.1.16", "source": { "type": "git", "url": "https://github.com/illuminate/pipeline.git", @@ -759,16 +1161,16 @@ }, { "name": "illuminate/support", - "version": "v5.1.8", + "version": "v5.1.16", "source": { "type": "git", "url": "https://github.com/illuminate/support.git", - "reference": "2ba8a22a554aee9a412baa2e16e4387262d6d57c" + "reference": "c5389968d48517b3b51cfd8cd4abd72f0cc1575b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/support/zipball/2ba8a22a554aee9a412baa2e16e4387262d6d57c", - "reference": "2ba8a22a554aee9a412baa2e16e4387262d6d57c", + "url": "https://api.github.com/repos/illuminate/support/zipball/c5389968d48517b3b51cfd8cd4abd72f0cc1575b", + "reference": "c5389968d48517b3b51cfd8cd4abd72f0cc1575b", "shasum": "" }, "require": { @@ -808,20 +1210,20 @@ ], "description": "The Illuminate Support package.", "homepage": "http://laravel.com", - "time": "2015-07-20 16:31:36" + "time": "2015-09-03 15:47:41" }, { "name": "illuminate/validation", - "version": "v5.1.8", + "version": "v5.1.16", "source": { "type": "git", "url": "https://github.com/illuminate/validation.git", - "reference": "23848aae24e6513aacd92c154befb26e00ebb858" + "reference": "6192667064fd75ff70efb83c6f5366a9edeac275" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/validation/zipball/23848aae24e6513aacd92c154befb26e00ebb858", - "reference": "23848aae24e6513aacd92c154befb26e00ebb858", + "url": "https://api.github.com/repos/illuminate/validation/zipball/6192667064fd75ff70efb83c6f5366a9edeac275", + "reference": "6192667064fd75ff70efb83c6f5366a9edeac275", "shasum": "" }, "require": { @@ -858,20 +1260,20 @@ ], "description": "The Illuminate Validation package.", "homepage": "http://laravel.com", - "time": "2015-07-18 02:49:20" + "time": "2015-08-17 22:03:13" }, { "name": "illuminate/view", - "version": "v5.1.8", + "version": "v5.1.16", "source": { "type": "git", "url": "https://github.com/illuminate/view.git", - "reference": "c5f6d9517ac956937adb44b081c44cb9ebca0ea6" + "reference": "82c5bc657554360a7325ace0761c24f26de9e771" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/view/zipball/c5f6d9517ac956937adb44b081c44cb9ebca0ea6", - "reference": "c5f6d9517ac956937adb44b081c44cb9ebca0ea6", + "url": "https://api.github.com/repos/illuminate/view/zipball/82c5bc657554360a7325ace0761c24f26de9e771", + "reference": "82c5bc657554360a7325ace0761c24f26de9e771", "shasum": "" }, "require": { @@ -905,20 +1307,20 @@ ], "description": "The Illuminate View package.", "homepage": "http://laravel.com", - "time": "2015-07-17 15:21:21" + "time": "2015-09-04 12:45:07" }, { "name": "intervention/image", - "version": "2.3.1", + "version": "2.3.2", "source": { "type": "git", "url": "https://github.com/Intervention/image.git", - "reference": "156f9d6f8a186c68b92f0c50084718f02dae1b5f" + "reference": "1124ff3c6298f0dcf9edf9156623904d7a5c3428" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Intervention/image/zipball/156f9d6f8a186c68b92f0c50084718f02dae1b5f", - "reference": "156f9d6f8a186c68b92f0c50084718f02dae1b5f", + "url": "https://api.github.com/repos/Intervention/image/zipball/1124ff3c6298f0dcf9edf9156623904d7a5c3428", + "reference": "1124ff3c6298f0dcf9edf9156623904d7a5c3428", "shasum": "" }, "require": { @@ -967,7 +1369,88 @@ "thumbnail", "watermark" ], - "time": "2015-07-10 15:03:58" + "time": "2015-08-16 15:31:59" + }, + { + "name": "league/flysystem", + "version": "1.0.12", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "7323424a9d39c24e597ed3f2144419dfbb52e086" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/7323424a9d39c24e597ed3f2144419dfbb52e086", + "reference": "7323424a9d39c24e597ed3f2144419dfbb52e086", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "ext-fileinfo": "*", + "mockery/mockery": "~0.9", + "phpspec/phpspec": "^2.2", + "phpspec/prophecy-phpunit": "~1.0", + "phpunit/phpunit": "~4.1" + }, + "suggest": { + "ext-fileinfo": "Required for MimeType", + "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2", + "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3", + "league/flysystem-azure": "Allows you to use Windows Azure Blob storage", + "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching", + "league/flysystem-copy": "Allows you to use Copy.com storage", + "league/flysystem-dropbox": "Allows you to use Dropbox storage", + "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem", + "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files", + "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib", + "league/flysystem-webdav": "Allows you to use WebDAV storage", + "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Filesystem abstraction: Many filesystems, one API.", + "keywords": [ + "Cloud Files", + "WebDAV", + "abstraction", + "aws", + "cloud", + "copy.com", + "dropbox", + "file systems", + "files", + "filesystem", + "filesystems", + "ftp", + "rackspace", + "remote", + "s3", + "sftp", + "storage" + ], + "time": "2015-09-05 12:06:41" }, { "name": "nesbot/carbon", @@ -1207,16 +1690,16 @@ }, { "name": "s9e/text-formatter", - "version": "0.1.0", + "version": "0.3.2", "source": { "type": "git", "url": "https://github.com/s9e/TextFormatter.git", - "reference": "ec60dea24268512c040016bbd54ff01f0d1c0443" + "reference": "4db4d250735125274c8cfd1ac41ef76eae7d15e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/s9e/TextFormatter/zipball/ec60dea24268512c040016bbd54ff01f0d1c0443", - "reference": "ec60dea24268512c040016bbd54ff01f0d1c0443", + "url": "https://api.github.com/repos/s9e/TextFormatter/zipball/4db4d250735125274c8cfd1ac41ef76eae7d15e8", + "reference": "4db4d250735125274c8cfd1ac41ef76eae7d15e8", "shasum": "" }, "require": { @@ -1262,7 +1745,7 @@ "parser", "shortcodes" ], - "time": "2015-08-12 12:44:18" + "time": "2015-09-06 18:52:07" }, { "name": "swiftmailer/swiftmailer", @@ -1318,17 +1801,74 @@ "time": "2015-06-06 14:19:39" }, { - "name": "symfony/finder", - "version": "v2.7.3", + "name": "symfony/console", + "version": "v2.7.4", "source": { "type": "git", - "url": "https://github.com/symfony/Finder.git", - "reference": "ae0f363277485094edc04c9f3cbe595b183b78e4" + "url": "git@github.com:symfony/Console.git", + "reference": "f1f7376766394f409b9b33a57a5675a52f8aad93" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Finder/zipball/ae0f363277485094edc04c9f3cbe595b183b78e4", - "reference": "ae0f363277485094edc04c9f3cbe595b183b78e4", + "url": "https://api.github.com/repos/symfony/Console/zipball/f1f7376766394f409b9b33a57a5675a52f8aad93", + "reference": "f1f7376766394f409b9b33a57a5675a52f8aad93", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/event-dispatcher": "~2.1", + "symfony/phpunit-bridge": "~2.7", + "symfony/process": "~2.1" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "time": "2015-09-03 11:40:38" + }, + { + "name": "symfony/finder", + "version": "v2.7.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/Finder.git", + "reference": "fff4b0c362640a0ab7355e2647b3d461608e9065" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/Finder/zipball/fff4b0c362640a0ab7355e2647b3d461608e9065", + "reference": "fff4b0c362640a0ab7355e2647b3d461608e9065", "shasum": "" }, "require": { @@ -1364,20 +1904,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2015-07-09 16:07:40" + "time": "2015-08-26 17:56:37" }, { "name": "symfony/http-foundation", - "version": "v2.7.3", + "version": "v2.7.4", "source": { "type": "git", "url": "https://github.com/symfony/HttpFoundation.git", - "reference": "863af6898081b34c65d42100c370b9f3c51b70ca" + "reference": "7253c2041652353e71560bbd300d6256d170ddaf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/HttpFoundation/zipball/863af6898081b34c65d42100c370b9f3c51b70ca", - "reference": "863af6898081b34c65d42100c370b9f3c51b70ca", + "url": "https://api.github.com/repos/symfony/HttpFoundation/zipball/7253c2041652353e71560bbd300d6256d170ddaf", + "reference": "7253c2041652353e71560bbd300d6256d170ddaf", "shasum": "" }, "require": { @@ -1417,20 +1957,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2015-07-22 10:11:00" + "time": "2015-08-27 06:45:45" }, { "name": "symfony/translation", - "version": "v2.7.3", + "version": "v2.7.4", "source": { "type": "git", "url": "https://github.com/symfony/Translation.git", - "reference": "c8dc34cc936152c609cdd722af317e4239d10dd6" + "reference": "485877661835e188cd78345c6d4eef1290d17571" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Translation/zipball/c8dc34cc936152c609cdd722af317e4239d10dd6", - "reference": "c8dc34cc936152c609cdd722af317e4239d10dd6", + "url": "https://api.github.com/repos/symfony/Translation/zipball/485877661835e188cd78345c6d4eef1290d17571", + "reference": "485877661835e188cd78345c6d4eef1290d17571", "shasum": "" }, "require": { @@ -1442,7 +1982,7 @@ "require-dev": { "psr/log": "~1.0", "symfony/config": "~2.7", - "symfony/intl": "~2.3", + "symfony/intl": "~2.4", "symfony/phpunit-bridge": "~2.7", "symfony/yaml": "~2.2" }, @@ -1478,20 +2018,20 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2015-07-09 16:07:40" + "time": "2015-09-06 08:36:38" }, { "name": "symfony/yaml", - "version": "v2.7.3", + "version": "v2.7.4", "source": { "type": "git", "url": "https://github.com/symfony/Yaml.git", - "reference": "71340e996171474a53f3d29111d046be4ad8a0ff" + "reference": "2dc7b06c065df96cc686c66da2705e5e18aef661" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Yaml/zipball/71340e996171474a53f3d29111d046be4ad8a0ff", - "reference": "71340e996171474a53f3d29111d046be4ad8a0ff", + "url": "https://api.github.com/repos/symfony/Yaml/zipball/2dc7b06c065df96cc686c66da2705e5e18aef661", + "reference": "2dc7b06c065df96cc686c66da2705e5e18aef661", "shasum": "" }, "require": { @@ -1527,7 +2067,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2015-07-28 14:07:07" + "time": "2015-08-24 07:13:45" }, { "name": "tobscure/json-api", @@ -1770,16 +2310,16 @@ }, { "name": "phpspec/phpspec", - "version": "2.2.1", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/phpspec/phpspec.git", - "reference": "e9a40577323e67f1de2e214abf32976a0352d8f8" + "reference": "36635a903bdeb54899d7407bc95610501fd98559" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/phpspec/zipball/e9a40577323e67f1de2e214abf32976a0352d8f8", - "reference": "e9a40577323e67f1de2e214abf32976a0352d8f8", + "url": "https://api.github.com/repos/phpspec/phpspec/zipball/36635a903bdeb54899d7407bc95610501fd98559", + "reference": "36635a903bdeb54899d7407bc95610501fd98559", "shasum": "" }, "require": { @@ -1791,15 +2331,14 @@ "symfony/console": "~2.3", "symfony/event-dispatcher": "~2.1", "symfony/finder": "~2.1", - "symfony/process": "~2.1", + "symfony/process": "^2.6", "symfony/yaml": "~2.1" }, "require-dev": { "behat/behat": "^3.0.11", "bossa/phpspec2-expect": "~1.0", "phpunit/phpunit": "~4.4", - "symfony/filesystem": "~2.1", - "symfony/process": "~2.1" + "symfony/filesystem": "~2.1" }, "suggest": { "phpspec/nyan-formatters": "~1.0 – Adds Nyan formatters" @@ -1844,20 +2383,20 @@ "testing", "tests" ], - "time": "2015-05-30 15:21:40" + "time": "2015-09-07 07:07:37" }, { "name": "phpspec/prophecy", - "version": "v1.4.1", + "version": "v1.5.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "3132b1f44c7bf2ec4c7eb2d3cb78fdeca760d373" + "reference": "4745ded9307786b730d7a60df5cb5a6c43cf95f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/3132b1f44c7bf2ec4c7eb2d3cb78fdeca760d373", - "reference": "3132b1f44c7bf2ec4c7eb2d3cb78fdeca760d373", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4745ded9307786b730d7a60df5cb5a6c43cf95f7", + "reference": "4745ded9307786b730d7a60df5cb5a6c43cf95f7", "shasum": "" }, "require": { @@ -1904,7 +2443,7 @@ "spy", "stub" ], - "time": "2015-04-27 22:15:08" + "time": "2015-08-13 10:07:40" }, { "name": "sebastian/comparator", @@ -2143,16 +2682,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "2.3.3", + "version": "2.3.4", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "c1a26c729508f73560c1a4f767f60b8ab6b4a666" + "reference": "11a2545c44a5915f883e2e5ec12e14ed345e3ab2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/c1a26c729508f73560c1a4f767f60b8ab6b4a666", - "reference": "c1a26c729508f73560c1a4f767f60b8ab6b4a666", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/11a2545c44a5915f883e2e5ec12e14ed345e3ab2", + "reference": "11a2545c44a5915f883e2e5ec12e14ed345e3ab2", "shasum": "" }, "require": { @@ -2213,77 +2752,20 @@ "phpcs", "standards" ], - "time": "2015-06-24 03:16:23" - }, - { - "name": "symfony/console", - "version": "v2.7.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/Console.git", - "reference": "d6cf02fe73634c96677e428f840704bfbcaec29e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/Console/zipball/d6cf02fe73634c96677e428f840704bfbcaec29e", - "reference": "d6cf02fe73634c96677e428f840704bfbcaec29e", - "shasum": "" - }, - "require": { - "php": ">=5.3.9" - }, - "require-dev": { - "psr/log": "~1.0", - "symfony/event-dispatcher": "~2.1", - "symfony/phpunit-bridge": "~2.7", - "symfony/process": "~2.1" - }, - "suggest": { - "psr/log": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/process": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.7-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Console\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Console Component", - "homepage": "https://symfony.com", - "time": "2015-07-28 15:18:12" + "time": "2015-09-09 00:18:50" }, { "name": "symfony/event-dispatcher", - "version": "v2.7.3", + "version": "v2.7.4", "source": { "type": "git", "url": "https://github.com/symfony/EventDispatcher.git", - "reference": "9310b5f9a87ec2ea75d20fec0b0017c77c66dac3" + "reference": "b58c916f1db03a611b72dd702564f30ad8fe83fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/9310b5f9a87ec2ea75d20fec0b0017c77c66dac3", - "reference": "9310b5f9a87ec2ea75d20fec0b0017c77c66dac3", + "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/b58c916f1db03a611b72dd702564f30ad8fe83fa", + "reference": "b58c916f1db03a611b72dd702564f30ad8fe83fa", "shasum": "" }, "require": { @@ -2328,20 +2810,20 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2015-06-18 19:21:56" + "time": "2015-08-24 07:13:45" }, { "name": "symfony/process", - "version": "v2.7.3", + "version": "v2.7.4", "source": { "type": "git", "url": "https://github.com/symfony/Process.git", - "reference": "48aeb0e48600321c272955132d7606ab0a49adb3" + "reference": "f7b3f73f70a7f8f49a1c838dc3debbf054732d8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Process/zipball/48aeb0e48600321c272955132d7606ab0a49adb3", - "reference": "48aeb0e48600321c272955132d7606ab0a49adb3", + "url": "https://api.github.com/repos/symfony/Process/zipball/f7b3f73f70a7f8f49a1c838dc3debbf054732d8e", + "reference": "f7b3f73f70a7f8f49a1c838dc3debbf054732d8e", "shasum": "" }, "require": { @@ -2377,7 +2859,7 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2015-07-01 11:25:50" + "time": "2015-08-27 06:45:45" } ], "aliases": [], diff --git a/framework/core/js/admin/src/components/AppearancePage.js b/framework/core/js/admin/src/components/AppearancePage.js index 67b1884e0..2b6fd436c 100644 --- a/framework/core/js/admin/src/components/AppearancePage.js +++ b/framework/core/js/admin/src/components/AppearancePage.js @@ -44,6 +44,7 @@ export default class AppearancePage extends Component { {Button.component({ className: 'Button Button--primary', + type: 'submit', children: 'Save Changes', loading: this.loading })} diff --git a/framework/core/js/admin/src/components/EditCustomCssModal.js b/framework/core/js/admin/src/components/EditCustomCssModal.js index 1619a66ec..651a4f183 100644 --- a/framework/core/js/admin/src/components/EditCustomCssModal.js +++ b/framework/core/js/admin/src/components/EditCustomCssModal.js @@ -30,6 +30,7 @@ export default class EditCustomCssModal extends Modal {
{Button.component({ className: 'Button Button--primary', + type: 'submit', children: 'Save Changes', loading: this.loading })} diff --git a/framework/core/js/forum/src/ForumApp.js b/framework/core/js/forum/src/ForumApp.js index 9e7edd397..2abeea524 100644 --- a/framework/core/js/forum/src/ForumApp.js +++ b/framework/core/js/forum/src/ForumApp.js @@ -4,6 +4,7 @@ import Search from 'flarum/components/Search'; import Composer from 'flarum/components/Composer'; import ReplyComposer from 'flarum/components/ReplyComposer'; import DiscussionPage from 'flarum/components/DiscussionPage'; +import SignUpModal from 'flarum/components/SignUpModal'; export default class ForumApp extends App { constructor(...args) { @@ -76,4 +77,27 @@ export default class ForumApp extends App { return this.current instanceof DiscussionPage && this.current.discussion === discussion; } + + /** + * Callback for when an external authenticator (social login) action has + * completed. + * + * If the payload indicates that the user has been logged in, then the page + * will be reloaded. Otherwise, a SignUpModal will be opened, prefilled + * with the provided details. + * + * @param {Object} payload A dictionary of props to pass into the sign up + * modal. A truthy `authenticated` prop indicates that the user has logged + * in, and thus the page is reloaded. + * @public + */ + authenticationComplete(payload) { + if (payload.authenticated) { + window.location.reload(); + } else { + const modal = new SignUpModal(payload); + this.modal.show(modal); + modal.$('[name=password]').focus(); + } + } } diff --git a/framework/core/js/forum/src/components/Activity.js b/framework/core/js/forum/src/components/Activity.js deleted file mode 100644 index ea5821bdb..000000000 --- a/framework/core/js/forum/src/components/Activity.js +++ /dev/null @@ -1,66 +0,0 @@ -import Component from 'flarum/Component'; -import humanTime from 'flarum/helpers/humanTime'; -import avatar from 'flarum/helpers/avatar'; - -/** - * The `Activity` component represents a piece of activity of a user's activity - * feed. Subclasses should implement the `description` and `content` methods. - * - * ### Props - * - * - `activity` - * - * @abstract - */ -export default class Activity extends Component { - view() { - const activity = this.props.activity; - - return ( -
- {avatar(this.user(), {className: 'Activity-avatar'})} - -
- {this.description()} - {humanTime(this.time())} -
- - {this.content()} -
- ); - } - - /** - * Get the user whose avatar should be displayed. - * - * @return {User} - * @abstract - */ - user() { - } - - /** - * Get the time of the activity. - * - * @return {Date} - * @abstract - */ - time() { - } - - /** - * Get the description of the activity. - * - * @return {VirtualElement} - */ - description() { - } - - /** - * Get the content to show below the activity description. - * - * @return {VirtualElement} - */ - content() { - } -} diff --git a/framework/core/js/forum/src/components/LogInButton.js b/framework/core/js/forum/src/components/LogInButton.js new file mode 100644 index 000000000..3de450589 --- /dev/null +++ b/framework/core/js/forum/src/components/LogInButton.js @@ -0,0 +1,30 @@ +import Button from 'flarum/components/Button'; + +/** + * The `LogInButton` component displays a social login button which will open + * a popup window containing the specified path. + * + * ### Props + * +* - `path` + */ +export default class LogInButton extends Button { + static initProps(props) { + props.className = (props.className || '') + ' LogInButton'; + + props.onclick = function() { + const width = 620; + const height = 400; + const $window = $(window); + + window.open(app.forum.attribute('baseUrl') + props.path, 'logInPopup', + `width=${width},` + + `height=${height},` + + `top=${$window.height() / 2 - height / 2},` + + `left=${$window.width() / 2 - width / 2},` + + 'status=no,scrollbars=no,resizable=no'); + }; + + super.initProps(props); + } +} diff --git a/framework/core/js/forum/src/components/LogInButtons.js b/framework/core/js/forum/src/components/LogInButtons.js new file mode 100644 index 000000000..ef2c853e9 --- /dev/null +++ b/framework/core/js/forum/src/components/LogInButtons.js @@ -0,0 +1,25 @@ +import Component from 'flarum/Component'; +import ItemList from 'flarum/utils/ItemList'; + +/** + * The `LogInButtons` component displays a collection of social login buttons. + */ +export default class LogInButtons extends Component { + view() { + return ( +
+ {this.items().toArray()} +
+ ); + } + + /** + * Build a list of LogInButton components. + * + * @return {ItemList} + * @public + */ + items() { + return new ItemList(); + } +} diff --git a/framework/core/js/forum/src/components/LogInModal.js b/framework/core/js/forum/src/components/LogInModal.js index ad839a863..ad2ac1769 100644 --- a/framework/core/js/forum/src/components/LogInModal.js +++ b/framework/core/js/forum/src/components/LogInModal.js @@ -3,6 +3,7 @@ import ForgotPasswordModal from 'flarum/components/ForgotPasswordModal'; import SignUpModal from 'flarum/components/SignUpModal'; import Alert from 'flarum/components/Alert'; import Button from 'flarum/components/Button'; +import LogInButtons from 'flarum/components/LogInButtons'; /** * The `LogInModal` component displays a modal dialog with a login form. @@ -42,6 +43,8 @@ export default class LogInModal extends Modal { content() { return [
+ +
{app.trans('core.forgot_password_link')}

+ {app.forum.attribute('allowSignUp') ? (

{app.trans('core.before_sign_up_link')}{' '} @@ -84,6 +88,8 @@ export default class LogInModal extends Modal { /** * Open the forgot password modal, prefilling it with an email if the user has * entered one. + * + * @public */ forgotPassword() { const email = this.email(); @@ -95,6 +101,8 @@ export default class LogInModal extends Modal { /** * Open the sign up modal, prefilling it with an email/username/password if * the user has entered one. + * + * @public */ signUp() { const props = {password: this.password()}; diff --git a/framework/core/js/forum/src/components/SignUpModal.js b/framework/core/js/forum/src/components/SignUpModal.js index 0cbdb231e..3d6ffb1df 100644 --- a/framework/core/js/forum/src/components/SignUpModal.js +++ b/framework/core/js/forum/src/components/SignUpModal.js @@ -2,6 +2,7 @@ import Modal from 'flarum/components/Modal'; import LogInModal from 'flarum/components/LogInModal'; import avatar from 'flarum/helpers/avatar'; import Button from 'flarum/components/Button'; +import LogInButtons from 'flarum/components/LogInButtons'; /** * The `SignUpModal` component displays a modal dialog with a singup form. @@ -11,6 +12,7 @@ import Button from 'flarum/components/Button'; * - `username` * - `email` * - `password` + * - `token` An email token to sign up with. */ export default class SignUpModal extends Modal { constructor(...args) { @@ -65,7 +67,9 @@ export default class SignUpModal extends Modal { } body() { - const body = [( + const body = [ + this.props.token ? '' : , +

+ disabled={this.loading || this.props.token} />
-
- -
+ {this.props.token ? '' : ( +
+ +
+ )}
- {Button.component({ - className: 'Button Button--primary Button--block', - type: 'submit', - loading: this.loading, - children: app.trans('core.sign_up') - })} +
- )]; + ]; if (this.welcomeUser) { const user = this.welcomeUser; @@ -115,20 +121,12 @@ export default class SignUpModal extends Modal { {avatar(user)}

{app.trans('core.welcome_user', {user})}

- {!user.isActivated() ? [ -

{app.trans('core.confirmation_email_sent', {email: {user.email()}})}

, -

- - {app.trans('core.go_to', {location: emailProviderName})} - -

- ] : ( -

- -

- )} +

{app.trans('core.confirmation_email_sent', {email: {user.email()}})}

, +

+ + {app.trans('core.go_to', {location: emailProviderName})} + +

@@ -150,6 +148,8 @@ export default class SignUpModal extends Modal { /** * Open the log in modal, prefilling it with an email/username/password if * the user has entered one. + * + * @public */ logIn() { const props = { @@ -161,7 +161,7 @@ export default class SignUpModal extends Modal { } onready() { - if (this.props.username) { + if (this.props.username && !this.props.token) { this.$('[name=email]').select(); } else { super.onready(); @@ -175,24 +175,50 @@ export default class SignUpModal extends Modal { const data = this.submitData(); - app.store.createRecord('users').save(data).then( - user => { - this.welcomeUser = user; - this.loading = false; - m.redraw(); + app.request({ + url: app.forum.attribute('baseUrl') + '/register', + method: 'POST', + data + }).then( + payload => { + const user = app.store.pushPayload(payload); + + // If the user's new account has been activated, then we can assume + // that they have been logged in too. Thus, we will reload the page. + // Otherwise, we will show a message asking them to check their email. + if (user.isActivated()) { + window.location.reload(); + } else { + this.welcomeUser = user; + this.loading = false; + m.redraw(); + } }, response => { this.loading = false; - this.handleErrors(response.errors); + this.handleErrors(response); } ); } + /** + * Get the data that should be submitted in the sign-up request. + * + * @return {Object} + * @public + */ submitData() { - return { + const data = { username: this.username(), - email: this.email(), - password: this.password() + email: this.email() }; + + if (this.props.token) { + data.token = this.props.token; + } else { + data.password = this.password(); + } + + return data; } } diff --git a/framework/core/js/lib/Session.js b/framework/core/js/lib/Session.js index 28b7ec40b..a6c121b49 100644 --- a/framework/core/js/lib/Session.js +++ b/framework/core/js/lib/Session.js @@ -16,6 +16,7 @@ export default class Session { * The token that was used for authentication. * * @type {String|null} + * @public */ this.token = token; } @@ -26,6 +27,7 @@ export default class Session { * @param {String} identification The username/email. * @param {String} password * @return {Promise} + * @public */ login(identification, password) { return app.request({ @@ -38,6 +40,8 @@ export default class Session { /** * Log the user out. + * + * @public */ logout() { window.location = app.forum.attribute('baseUrl') + '/logout?token=' + this.token; @@ -48,8 +52,11 @@ export default class Session { * XMLHttpRequest object. * * @param {XMLHttpRequest} xhr + * @public */ authorize(xhr) { - xhr.setRequestHeader('Authorization', 'Token ' + this.token); + if (this.token) { + xhr.setRequestHeader('Authorization', 'Token ' + this.token); + } } } diff --git a/framework/core/js/lib/components/Button.js b/framework/core/js/lib/components/Button.js index a2ae2d7de..eed25c372 100644 --- a/framework/core/js/lib/components/Button.js +++ b/framework/core/js/lib/components/Button.js @@ -25,7 +25,8 @@ export default class Button extends Component { delete attrs.children; - attrs.className = (attrs.className || ''); + attrs.className = attrs.className || ''; + attrs.type = attrs.type || 'button'; const iconName = extract(attrs, 'icon'); if (iconName) attrs.className += ' hasIcon'; diff --git a/framework/core/js/lib/components/Modal.js b/framework/core/js/lib/components/Modal.js index 38a42c280..1d5b81493 100644 --- a/framework/core/js/lib/components/Modal.js +++ b/framework/core/js/lib/components/Modal.js @@ -129,7 +129,7 @@ export default class Modal extends Component { m.redraw(); if (errors) { - this.$('form [name=' + errors[0].path + ']').select(); + this.$('form [name=' + errors[0].source.pointer.replace('/data/attributes/', '') + ']').select(); } else { this.$('form :input:first').select(); } diff --git a/framework/core/less/forum/DiscussionListItem.less b/framework/core/less/forum/DiscussionListItem.less index da5228a29..22b775d66 100644 --- a/framework/core/less/forum/DiscussionListItem.less +++ b/framework/core/less/forum/DiscussionListItem.less @@ -45,7 +45,7 @@ text-overflow: ellipsis; .read & { - color: mix(@heading-color, @body-bg, 60%); + color: mix(@heading-color, @body-bg, 55%); } .unread & { font-weight: 600; @@ -59,6 +59,12 @@ > li { display: inline; + opacity: 0.7; + .transition(opacity 0.2s); + + .DiscussionListItem:hover &, .DiscussionListItem.active & { + opacity: 1; + } } .username { font-weight: bold; diff --git a/framework/core/less/forum/LogInButton.less b/framework/core/less/forum/LogInButton.less new file mode 100644 index 000000000..f0fd6c1e9 --- /dev/null +++ b/framework/core/less/forum/LogInButton.less @@ -0,0 +1,11 @@ +.LogInButton { + &:extend(.Button--block); +} +.LogInButtons { + width: 200px; + margin: 0 auto 20px; + + .LogInButton { + margin-bottom: 5px; + } +} diff --git a/framework/core/less/forum/Post.less b/framework/core/less/forum/Post.less index 60c4f7328..fa84e43fe 100644 --- a/framework/core/less/forum/Post.less +++ b/framework/core/less/forum/Post.less @@ -139,7 +139,7 @@ font-size: 100%; font-weight: bold; } - img { + img, iframe { max-width: 100%; } } diff --git a/framework/core/less/forum/UserCard.less b/framework/core/less/forum/UserCard.less index fe10abb26..731ea28c2 100644 --- a/framework/core/less/forum/UserCard.less +++ b/framework/core/less/forum/UserCard.less @@ -126,6 +126,9 @@ & .icon { font-size: 14px; } + &.online .fa-circle { + color: @online-user-circle-color; + } &.online .icon { font-size: 12px; } diff --git a/framework/core/less/forum/app.less b/framework/core/less/forum/app.less index 40bac2b0a..d9b4a3122 100644 --- a/framework/core/less/forum/app.less +++ b/framework/core/less/forum/app.less @@ -10,6 +10,7 @@ @import "EditUserModal.less"; @import "Hero.less"; @import "IndexPage.less"; +@import "LogInButton.less"; @import "LogInModal.less"; @import "NotificationGrid.less"; @import "NotificationList.less"; diff --git a/framework/core/less/lib/FormControl.less b/framework/core/less/lib/FormControl.less index 66c09f2ac..73c562c2e 100755 --- a/framework/core/less/lib/FormControl.less +++ b/framework/core/less/lib/FormControl.less @@ -27,8 +27,7 @@ &[disabled], &[readonly], fieldset[disabled] & { - // background-color: @input-bg-disabled; - opacity: 1; // iOS fix for unreadable disabled content; see https://github.com/twbs/bootstrap/issues/11655 + opacity: 0.5; } &[disabled], diff --git a/framework/core/less/lib/variables.less b/framework/core/less/lib/variables.less index bbca8b077..3b39e1357 100755 --- a/framework/core/less/lib/variables.less +++ b/framework/core/less/lib/variables.less @@ -27,7 +27,7 @@ @text-color: #111; @link-color: saturate(@primary-color, 10%); @heading-color: @text-color; - @muted-color: hsl(@secondary-hue, min(25%, @secondary-sat), 60%); + @muted-color: hsl(@secondary-hue, min(25%, @secondary-sat), 55%); @muted-more-color: #aaa; @shadow-color: rgba(0, 0, 0, 0.35); @@ -130,3 +130,5 @@ @tooltip-bg: rgba(0, 0, 0, 0.9); @tooltip-color: #fff; + +@online-user-circle-color: #7FBA00; diff --git a/framework/core/locale/en.yml b/framework/core/locale/en.yml index e68aac89a..c40cdc9e7 100644 --- a/framework/core/locale/en.yml +++ b/framework/core/locale/en.yml @@ -28,7 +28,6 @@ core: discussion_started: "Started {ago} by {username}" discussion_title: Discussion Title discussions: Discussions - dismiss: Dismiss edit: Edit editing_post: "Post #{number} in {discussion}" email: Email diff --git a/framework/core/migrations/2015_02_24_000000_create_email_tokens_table.php b/framework/core/migrations/2015_02_24_000000_create_email_tokens_table.php index e14bff848..a199c3e03 100644 --- a/framework/core/migrations/2015_02_24_000000_create_email_tokens_table.php +++ b/framework/core/migrations/2015_02_24_000000_create_email_tokens_table.php @@ -23,8 +23,8 @@ class CreateEmailTokensTable extends Migration { $this->schema->create('email_tokens', function (Blueprint $table) { $table->string('id', 100)->primary(); - $table->integer('user_id')->unsigned(); $table->string('email', 150); + $table->integer('user_id')->unsigned(); $table->timestamp('created_at'); }); } diff --git a/framework/core/migrations/2015_09_15_000000_make_email_tokens_user_id_column_nullable.php b/framework/core/migrations/2015_09_15_000000_make_email_tokens_user_id_column_nullable.php new file mode 100644 index 000000000..5e8ced5e1 --- /dev/null +++ b/framework/core/migrations/2015_09_15_000000_make_email_tokens_user_id_column_nullable.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Flarum\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; + +class MakeEmailTokensUserIdColumnNullable extends Migration +{ + /** + * Run the migrations. + * + * @return void + */ + public function up() + { + $this->schema->table('email_tokens', function (Blueprint $table) { + $table->integer('user_id')->unsigned()->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + $this->schema->table('email_tokens', function (Blueprint $table) { + $table->integer('user_id')->unsigned()->change(); + }); + } +} diff --git a/framework/core/src/Admin/Middleware/LoginWithCookieAndCheckAdmin.php b/framework/core/src/Admin/Middleware/LoginWithCookieAndCheckAdmin.php index b1b0d6d72..04169ca3f 100644 --- a/framework/core/src/Admin/Middleware/LoginWithCookieAndCheckAdmin.php +++ b/framework/core/src/Admin/Middleware/LoginWithCookieAndCheckAdmin.php @@ -15,36 +15,32 @@ use Illuminate\Contracts\Container\Container; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Zend\Stratigility\MiddlewareInterface; +use Flarum\Forum\Middleware\LoginWithCookie; +use Flarum\Core\Exceptions\PermissionDeniedException; -class LoginWithCookieAndCheckAdmin implements MiddlewareInterface +class LoginWithCookieAndCheckAdmin extends LoginWithCookie { - /** - * @var Container - */ - protected $app; - - /** - * @param Container $app - */ - public function __construct(Container $app) - { - $this->app = $app; - } - /** * {@inheritdoc} */ public function __invoke(Request $request, Response $response, callable $out = null) { - if (($token = array_get($request->getCookieParams(), 'flarum_remember')) && - ($accessToken = AccessToken::valid($token)) && - $accessToken->user->isAdmin() - ) { - $this->app->instance('flarum.actor', $accessToken->user); - } else { - die('Access Denied'); + if (! $this->logIn($request)) { + throw new PermissionDeniedException; } return $out ? $out($request, $response) : $response; } + + /** + * {@inheritdoc} + */ + protected function getToken(Request $request) + { + $token = parent::getToken($request); + + if ($token && $token->user && $token->user->isAdmin()) { + return $token; + } + } } diff --git a/framework/core/src/Api/AccessToken.php b/framework/core/src/Api/AccessToken.php index fdad6f4ec..9a6efa56e 100644 --- a/framework/core/src/Api/AccessToken.php +++ b/framework/core/src/Api/AccessToken.php @@ -12,6 +12,7 @@ namespace Flarum\Api; use Flarum\Core\Model; use DateTime; +use Exception; /** * @todo document database columns with @property @@ -55,14 +56,13 @@ class AccessToken extends Model } /** - * Get the given token only if it is valid. + * Check that the token has not expired. * - * @param string $token - * @return static|null + * @return bool */ - public static function valid($token) + public function isValid() { - return static::where('id', $token)->where('expires_at', '>', new DateTime)->first(); + return $this->expires_at > new DateTime; } /** diff --git a/framework/core/src/Api/Actions/DeleteAction.php b/framework/core/src/Api/Actions/DeleteAction.php index f460063c7..2d1863695 100644 --- a/framework/core/src/Api/Actions/DeleteAction.php +++ b/framework/core/src/Api/Actions/DeleteAction.php @@ -13,7 +13,7 @@ namespace Flarum\Api\Actions; use Flarum\Api\Request; use Zend\Diactoros\Response\EmptyResponse; -abstract class DeleteAction extends JsonApiAction +abstract class DeleteAction implements Action { /** * Delegate deletion of the resource, and return a 204 No Content @@ -22,7 +22,7 @@ abstract class DeleteAction extends JsonApiAction * @param \Flarum\Api\Request $request * @return \Psr\Http\Message\ResponseInterface */ - public function respond(Request $request) + public function handle(Request $request) { $this->delete($request); diff --git a/framework/core/src/Api/Actions/Extensions/UpdateAction.php b/framework/core/src/Api/Actions/Extensions/UpdateAction.php index 3a44dea15..4f2e633b5 100644 --- a/framework/core/src/Api/Actions/Extensions/UpdateAction.php +++ b/framework/core/src/Api/Actions/Extensions/UpdateAction.php @@ -10,13 +10,13 @@ namespace Flarum\Api\Actions\Extensions; -use Flarum\Api\Actions\JsonApiAction; +use Flarum\Api\Actions\Action; use Flarum\Api\Request; use Illuminate\Contracts\Bus\Dispatcher; use Flarum\Core\Exceptions\PermissionDeniedException; use Flarum\Support\ExtensionManager; -class UpdateAction extends JsonApiAction +class UpdateAction implements Action { protected $extensions; @@ -25,7 +25,7 @@ class UpdateAction extends JsonApiAction $this->extensions = $extensions; } - protected function respond(Request $request) + public function handle(Request $request) { if (! $request->actor->isAdmin()) { throw new PermissionDeniedException; diff --git a/framework/core/src/Api/Actions/ForgotAction.php b/framework/core/src/Api/Actions/ForgotAction.php index 95e56f49b..fa4918248 100644 --- a/framework/core/src/Api/Actions/ForgotAction.php +++ b/framework/core/src/Api/Actions/ForgotAction.php @@ -16,7 +16,7 @@ use Flarum\Core\Users\Commands\RequestPasswordReset; use Illuminate\Contracts\Bus\Dispatcher; use Zend\Diactoros\Response\EmptyResponse; -class ForgotAction extends JsonApiAction +class ForgotAction implements Action { protected $users; @@ -34,7 +34,7 @@ class ForgotAction extends JsonApiAction * @param \Flarum\Api\Request $request * @return \Psr\Http\Message\ResponseInterface */ - public function respond(Request $request) + public function handle(Request $request) { $email = $request->get('email'); diff --git a/framework/core/src/Api/Actions/JsonApiAction.php b/framework/core/src/Api/Actions/JsonApiAction.php deleted file mode 100644 index 09e54dcf4..000000000 --- a/framework/core/src/Api/Actions/JsonApiAction.php +++ /dev/null @@ -1,63 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Api\Actions; - -use Flarum\Api\Request; -use Illuminate\Contracts\Validation\ValidationException; -use Illuminate\Database\Eloquent\ModelNotFoundException; -use Flarum\Core\Exceptions\ValidationFailureException; -use Flarum\Core\Exceptions\PermissionDeniedException; -use Zend\Diactoros\Response\JsonResponse; - -abstract class JsonApiAction implements Action -{ - /** - * Handle an API request and return an API response, handling any relevant - * (API-related) exceptions that are thrown. - * - * @param Request $request - * @return \Psr\Http\Message\ResponseInterface - */ - public function handle(Request $request) - { - // TODO: This is gross. Move this error handling code to middleware? - try { - return $this->respond($request); - } catch (ValidationException $e) { - $errors = []; - foreach ($e->errors()->toArray() as $field => $messages) { - $errors[] = [ - 'detail' => implode("\n", $messages), - 'path' => $field - ]; - } - return new JsonResponse(['errors' => $errors], 422); - } catch (\Flarum\Core\Exceptions\ValidationException $e) { - $errors = []; - foreach ($e->getMessages() as $path => $detail) { - $errors[] = compact('path', 'detail'); - } - return new JsonResponse(['errors' => $errors], 422); - } catch (PermissionDeniedException $e) { - return new JsonResponse(null, 401); - } catch (ModelNotFoundException $e) { - return new JsonResponse(null, 404); - } - } - - /** - * Handle an API request and return an API response. - * - * @param Request $request - * @return \Psr\Http\Message\ResponseInterface - */ - abstract protected function respond(Request $request); -} diff --git a/framework/core/src/Api/Actions/SerializeAction.php b/framework/core/src/Api/Actions/SerializeAction.php index b60b98c45..16398282b 100644 --- a/framework/core/src/Api/Actions/SerializeAction.php +++ b/framework/core/src/Api/Actions/SerializeAction.php @@ -19,7 +19,7 @@ use Tobscure\JsonApi\Document; use Tobscure\JsonApi\SerializerInterface; use Zend\Diactoros\Response\JsonResponse; -abstract class SerializeAction extends JsonApiAction +abstract class SerializeAction implements Action { /** * The name of the serializer class to output results with. @@ -77,7 +77,7 @@ abstract class SerializeAction extends JsonApiAction * @param Request $request * @return JsonResponse */ - public function respond(Request $request) + public function handle(Request $request) { $request = $this->buildJsonApiRequest($request); $document = new Document(); diff --git a/framework/core/src/Api/Actions/TokenAction.php b/framework/core/src/Api/Actions/TokenAction.php index 4dfeff869..5d4da1ec6 100644 --- a/framework/core/src/Api/Actions/TokenAction.php +++ b/framework/core/src/Api/Actions/TokenAction.php @@ -18,7 +18,7 @@ use Flarum\Events\UserEmailChangeWasRequested; use Illuminate\Contracts\Bus\Dispatcher; use Zend\Diactoros\Response\JsonResponse; -class TokenAction extends JsonApiAction +class TokenAction implements Action { protected $users; @@ -37,7 +37,7 @@ class TokenAction extends JsonApiAction * @return \Psr\Http\Message\ResponseInterface * @throws PermissionDeniedException */ - public function respond(Request $request) + public function handle(Request $request) { $identification = $request->get('identification'); $password = $request->get('password'); diff --git a/framework/core/src/Api/Actions/Users/CreateAction.php b/framework/core/src/Api/Actions/Users/CreateAction.php index d88117f2f..92b85730e 100644 --- a/framework/core/src/Api/Actions/Users/CreateAction.php +++ b/framework/core/src/Api/Actions/Users/CreateAction.php @@ -73,8 +73,12 @@ class CreateAction extends BaseCreateAction */ protected function create(JsonApiRequest $request) { - return $this->bus->dispatch( + $user = $this->bus->dispatch( new RegisterUser($request->actor, $request->get('data')) ); + + $request->actor = $user; + + return $user; } } diff --git a/framework/core/src/Api/Client.php b/framework/core/src/Api/Client.php index 197e5e46a..6f05dc2ff 100644 --- a/framework/core/src/Api/Client.php +++ b/framework/core/src/Api/Client.php @@ -12,6 +12,8 @@ namespace Flarum\Api; use Flarum\Core\Users\User; use Illuminate\Contracts\Container\Container; +use Exception; +use Flarum\Api\Middleware\JsonApiErrors; class Client { @@ -38,10 +40,16 @@ class Client */ public function send(User $actor, $actionClass, array $input = []) { - /** @var \Flarum\Api\Actions\JsonApiAction $action */ + /** @var \Flarum\Api\Actions\Action $action */ $action = $this->container->make($actionClass); - $response = $action->handle(new Request($input, $actor)); + try { + $response = $action->handle(new Request($input, $actor)); + } catch (Exception $e) { + $middleware = new JsonApiErrors(); + + $response = $middleware->handle($e); + } return new Response($response); } diff --git a/framework/core/src/Api/Middleware/JsonApiErrors.php b/framework/core/src/Api/Middleware/JsonApiErrors.php index 0b76a70a1..bf55f2b99 100644 --- a/framework/core/src/Api/Middleware/JsonApiErrors.php +++ b/framework/core/src/Api/Middleware/JsonApiErrors.php @@ -10,35 +10,65 @@ namespace Flarum\Api\Middleware; +use Flarum\Core\Exceptions\JsonApiSerializable; +use Illuminate\Contracts\Validation\ValidationException; +use Illuminate\Database\Eloquent\ModelNotFoundException; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Zend\Diactoros\Response\JsonResponse; use Zend\Stratigility\ErrorMiddlewareInterface; +use Flarum\Core; +use Exception; class JsonApiErrors implements ErrorMiddlewareInterface { /** * {@inheritdoc} */ - public function __invoke($error, Request $request, Response $response, callable $out = null) + public function __invoke($e, Request $request, Response $response, callable $out = null) { - $errorObject = [ - 'title' => $error->getMessage(), - ]; + return $this->handle($e); + } - $status = 500; + public function handle(Exception $e) + { + if ($e instanceof JsonApiSerializable) { + $status = $e->getStatusCode(); - // If it seems to be a valid HTTP status code, we pass on the - // exception's status. - $errorCode = $error->getCode(); - if (is_int($errorCode) && $errorCode >= 400 && $errorCode < 600) { - $status = $errorCode; + $errors = $e->getErrors(); + } else if ($e instanceof ValidationException) { + $status = 422; + + $errors = $e->errors()->toArray(); + $errors = array_map(function ($field, $messages) { + return [ + 'detail' => implode("\n", $messages), + 'source' => ['pointer' => '/data/attributes/' . $field], + ]; + }, array_keys($errors), $errors); + } else if ($e instanceof ModelNotFoundException) { + $status = 404; + + $errors = []; + } else { + $status = 500; + + $error = [ + 'code' => $status, + 'title' => 'Internal Server Error' + ]; + + if (Core::inDebugMode()) { + $error['detail'] = (string) $e; + } + + $errors = [$error]; } // JSON API errors must be collected in an array under the // "errors" key in the top level of the document $data = [ - 'errors' => [$errorObject] + 'errors' => $errors, ]; return new JsonResponse($data, $status); diff --git a/framework/core/src/Api/Middleware/LoginWithHeader.php b/framework/core/src/Api/Middleware/LoginWithHeader.php index 1a66456af..c5873bb40 100644 --- a/framework/core/src/Api/Middleware/LoginWithHeader.php +++ b/framework/core/src/Api/Middleware/LoginWithHeader.php @@ -50,7 +50,7 @@ class LoginWithHeader implements MiddlewareInterface if (isset($parts[0]) && starts_with($parts[0], $this->prefix)) { $token = substr($parts[0], strlen($this->prefix)); - if ($accessToken = AccessToken::valid($token)) { + if (($accessToken = AccessToken::find($token)) && $accessToken->isValid()) { $this->app->instance('flarum.actor', $user = $accessToken->user); $user->updateLastSeen()->save(); diff --git a/framework/core/src/Core/Application.php b/framework/core/src/Core/Application.php index b3de34329..aa4972478 100644 --- a/framework/core/src/Core/Application.php +++ b/framework/core/src/Core/Application.php @@ -27,7 +27,7 @@ class Application extends Container implements ApplicationContract * * @var string */ - const VERSION = '0.1.0-beta'; + const VERSION = '0.1.0-beta.2'; /** * The base path for the Laravel installation. diff --git a/framework/core/src/Core/Exceptions/InvalidConfirmationTokenException.php b/framework/core/src/Core/Exceptions/InvalidConfirmationTokenException.php index 22ebed2b8..14c22bd56 100644 --- a/framework/core/src/Core/Exceptions/InvalidConfirmationTokenException.php +++ b/framework/core/src/Core/Exceptions/InvalidConfirmationTokenException.php @@ -12,6 +12,21 @@ namespace Flarum\Core\Exceptions; use Exception; -class InvalidConfirmationTokenException extends Exception +class InvalidConfirmationTokenException extends Exception implements JsonApiSerializable { + /** + * {@inheritdoc} + */ + public function getStatusCode() + { + return 403; + } + + /** + * {@inheritdoc} + */ + public function getErrors() + { + return ['code' => 'invalid_confirmation_token']; + } } diff --git a/framework/core/src/Core/Exceptions/JsonApiSerializable.php b/framework/core/src/Core/Exceptions/JsonApiSerializable.php new file mode 100644 index 000000000..4ff671d22 --- /dev/null +++ b/framework/core/src/Core/Exceptions/JsonApiSerializable.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Core\Exceptions; + +interface JsonApiSerializable +{ + /** + * Return the HTTP status code to be used for this exception. + * + * @return int + */ + public function getStatusCode(); + + /** + * Return an array of errors, formatted as JSON-API error objects. + * + * @see http://jsonapi.org/format/#error-objects + * @return array + */ + public function getErrors(); +} diff --git a/framework/core/src/Core/Exceptions/PermissionDeniedException.php b/framework/core/src/Core/Exceptions/PermissionDeniedException.php index 14b7c1d66..9cffa8eb3 100644 --- a/framework/core/src/Core/Exceptions/PermissionDeniedException.php +++ b/framework/core/src/Core/Exceptions/PermissionDeniedException.php @@ -12,6 +12,26 @@ namespace Flarum\Core\Exceptions; use Exception; -class PermissionDeniedException extends Exception +class PermissionDeniedException extends Exception implements JsonApiSerializable { + /** + * Return the HTTP status code to be used for this exception. + * + * @return int + */ + public function getStatusCode() + { + return 401; + } + + /** + * Return an array of errors, formatted as JSON-API error objects. + * + * @see http://jsonapi.org/format/#error-objects + * @return array + */ + public function getErrors() + { + return []; + } } diff --git a/framework/core/src/Core/Exceptions/ValidationException.php b/framework/core/src/Core/Exceptions/ValidationException.php index 56ff781a9..445ee0599 100644 --- a/framework/core/src/Core/Exceptions/ValidationException.php +++ b/framework/core/src/Core/Exceptions/ValidationException.php @@ -12,17 +12,39 @@ namespace Flarum\Core\Exceptions; use Exception; -class ValidationException extends Exception +class ValidationException extends Exception implements JsonApiSerializable { protected $messages; public function __construct(array $messages) { $this->messages = $messages; + + parent::__construct(implode("\n", $messages)); } public function getMessages() { return $this->messages; } + + /** + * {@inheritdoc} + */ + public function getStatusCode() + { + return 422; + } + + /** + * {@inheritdoc} + */ + public function getErrors() + { + return array_map(function ($path, $detail) { + $source = ['pointer' => '/data/attributes/' . $path]; + + return compact('source', 'detail'); + }, array_keys($this->messages), $this->messages); + } } diff --git a/framework/core/src/Core/Model.php b/framework/core/src/Core/Model.php index d1bdf255f..aa1f3ebb4 100755 --- a/framework/core/src/Core/Model.php +++ b/framework/core/src/Core/Model.php @@ -10,14 +10,8 @@ namespace Flarum\Core; -use Flarum\Core\Exceptions\PermissionDeniedException; -use Flarum\Core\Users\User; -use Flarum\Events\ModelAllow; use Flarum\Events\ModelDates; use Flarum\Events\ModelRelationship; -use Flarum\Events\ScopeModelVisibility; -use Illuminate\Contracts\Validation\Factory; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model as Eloquent; use Illuminate\Database\Eloquent\Relations\Relation; use LogicException; diff --git a/framework/core/src/Core/Users/Commands/ConfirmEmailHandler.php b/framework/core/src/Core/Users/Commands/ConfirmEmailHandler.php index 71f766915..9825f8926 100644 --- a/framework/core/src/Core/Users/Commands/ConfirmEmailHandler.php +++ b/framework/core/src/Core/Users/Commands/ConfirmEmailHandler.php @@ -13,7 +13,6 @@ namespace Flarum\Core\Users\Commands; use Flarum\Core\Users\UserRepository; use Flarum\Events\UserWillBeSaved; use Flarum\Core\Support\DispatchesEvents; -use Flarum\Core\Exceptions\InvalidConfirmationTokenException; use Flarum\Core\Users\EmailToken; use DateTime; @@ -36,16 +35,14 @@ class ConfirmEmailHandler /** * @param ConfirmEmail $command - * @return \Flarum\Core\Users\User + * * @throws InvalidConfirmationTokenException + * + * @return \Flarum\Core\Users\User */ public function handle(ConfirmEmail $command) { - $token = EmailToken::find($command->token); - - if (! $token || $token->created_at < new DateTime('-1 day')) { - throw new InvalidConfirmationTokenException; - } + $token = EmailToken::validOrFail($command->token); $user = $token->user; $user->changeEmail($token->email); diff --git a/framework/core/src/Core/Users/Commands/RegisterUserHandler.php b/framework/core/src/Core/Users/Commands/RegisterUserHandler.php index af8dbeeb2..132a400e0 100644 --- a/framework/core/src/Core/Users/Commands/RegisterUserHandler.php +++ b/framework/core/src/Core/Users/Commands/RegisterUserHandler.php @@ -11,17 +11,25 @@ namespace Flarum\Core\Users\Commands; use Flarum\Core\Users\User; +use Flarum\Core\Users\EmailToken; use Flarum\Events\UserWillBeSaved; use Flarum\Core\Support\DispatchesEvents; use Flarum\Core\Settings\SettingsRepository; use Flarum\Core\Exceptions\PermissionDeniedException; +use DateTime; class RegisterUserHandler { use DispatchesEvents; + /** + * @var SettingsRepository + */ protected $settings; + /** + * @param SettingsRepository $settings + */ public function __construct(SettingsRepository $settings) { $this->settings = $settings; @@ -29,26 +37,57 @@ class RegisterUserHandler /** * @param RegisterUser $command + * + * @throws PermissionDeniedException if signup is closed and the actor is + * not an administrator. + * @throws \Flarum\Core\Exceptions\InvalidConfirmationTokenException if an + * email confirmation token is provided but is invalid. + * * @return User */ public function handle(RegisterUser $command) { - if (! $this->settings->get('allow_sign_up')) { - throw new PermissionDeniedException; - } - $actor = $command->actor; $data = $command->data; + if (! $this->settings->get('allow_sign_up') && ! $actor->isAdmin()) { + throw new PermissionDeniedException; + } + + // If a valid email confirmation token was provided as an attribute, + // then we can create a random password for this user and consider their + // email address confirmed. + if (isset($data['attributes']['token'])) { + $token = EmailToken::whereNull('user_id')->validOrFail($data['attributes']['token']); + + $email = $token->email; + $password = array_get($data, 'attributes.password', str_random(20)); + } else { + $email = array_get($data, 'attributes.email'); + $password = array_get($data, 'attributes.password'); + } + + // Create the user's new account. If their email was set via token, then + // we can activate their account from the get-go, and they won't need + // to confirm their email address. $user = User::register( array_get($data, 'attributes.username'), - array_get($data, 'attributes.email'), - array_get($data, 'attributes.password') + $email, + $password ); + if (isset($token)) { + $user->activate(); + } + event(new UserWillBeSaved($user, $actor, $data)); $user->save(); + + if (isset($token)) { + $token->delete(); + } + $this->dispatchEventsFor($user); return $user; diff --git a/framework/core/src/Core/Users/EmailToken.php b/framework/core/src/Core/Users/EmailToken.php index 92acd1bc9..eb61b0cd0 100644 --- a/framework/core/src/Core/Users/EmailToken.php +++ b/framework/core/src/Core/Users/EmailToken.php @@ -11,6 +11,8 @@ namespace Flarum\Core\Users; use Flarum\Core\Model; +use Flarum\Core\Exceptions\InvalidConfirmationTokenException; +use DateTime; /** * @todo document database columns with @property @@ -37,11 +39,12 @@ class EmailToken extends Model /** * Generate an email token for the specified user. * - * @param int $userId * @param string $email + * @param int $userId + * * @return static */ - public static function generate($userId, $email) + public static function generate($email, $userId = null) { $token = new static; @@ -62,4 +65,25 @@ class EmailToken extends Model { return $this->belongsTo('Flarum\Core\Users\User'); } + + /** + * Find the token with the given ID, and assert that it has not expired. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param string $id + * + * @throws InvalidConfirmationTokenException + * + * @return static + */ + public function scopeValidOrFail($query, $id) + { + $token = $query->find($id); + + if (! $token || $token->created_at < new DateTime('-1 day')) { + throw new InvalidConfirmationTokenException; + } + + return $token; + } } diff --git a/framework/core/src/Core/Users/Listeners/EmailConfirmationMailer.php b/framework/core/src/Core/Users/Listeners/EmailConfirmationMailer.php index 497285e3a..7ca9eb384 100755 --- a/framework/core/src/Core/Users/Listeners/EmailConfirmationMailer.php +++ b/framework/core/src/Core/Users/Listeners/EmailConfirmationMailer.php @@ -57,6 +57,11 @@ class EmailConfirmationMailer public function whenUserWasRegistered(UserWasRegistered $event) { $user = $event->user; + + if ($user->is_activated) { + return; + } + $data = $this->getEmailData($user, $user->email); $this->mailer->send(['text' => 'flarum::emails.activateAccount'], $data, function (Message $message) use ($user) { @@ -82,11 +87,12 @@ class EmailConfirmationMailer /** * @param User $user * @param string $email + * * @return EmailToken */ protected function generateToken(User $user, $email) { - $token = EmailToken::generate($user->id, $email); + $token = EmailToken::generate($email, $user->id); $token->save(); return $token; @@ -97,6 +103,7 @@ class EmailConfirmationMailer * * @param User $user * @param string $email + * * @return array */ protected function getEmailData(User $user, $email) diff --git a/framework/core/src/Forum/Actions/ClientAction.php b/framework/core/src/Forum/Actions/ClientAction.php index bf358d1a3..49224baf6 100644 --- a/framework/core/src/Forum/Actions/ClientAction.php +++ b/framework/core/src/Forum/Actions/ClientAction.php @@ -56,7 +56,6 @@ class ClientAction extends BaseClientAction 'core.discussion_started', 'core.discussion_title', 'core.discussions', - 'core.dismiss', 'core.edit', 'core.editing_post', 'core.email', diff --git a/framework/core/src/Forum/Actions/ExternalAuthenticatorTrait.php b/framework/core/src/Forum/Actions/ExternalAuthenticatorTrait.php new file mode 100644 index 000000000..0ed0337ef --- /dev/null +++ b/framework/core/src/Forum/Actions/ExternalAuthenticatorTrait.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Forum\Actions; + +use Flarum\Core\Users\User; +use Zend\Diactoros\Response\HtmlResponse; +use Flarum\Api\Commands\GenerateAccessToken; +use Flarum\Core\Users\EmailToken; + +trait ExternalAuthenticatorTrait +{ + use WritesRememberCookie; + + /** + * @var \Illuminate\Contracts\Bus\Dispatcher + */ + protected $bus; + + /** + * Respond with JavaScript to tell the Flarum app that the user has been + * authenticated, or with information about their sign up status. + * + * @param string $email The email of the user's account. + * @param string $username A suggested username for the user's account. + * @return HtmlResponse + */ + protected function authenticated($email, $username) + { + $user = User::where('email', $email)->first(); + + // If a user with this email address doesn't already exist, then we will + // generate a unique confirmation token for this email address and add + // it to the response, along with the email address and a suggested + // username. Otherwise, we will log in the existing user by generating + // an access token. + if (! $user) { + $token = EmailToken::generate($email); + $token->save(); + + $payload = compact('email', 'username'); + + $payload['token'] = $token->id; + } else { + $accessToken = $this->bus->dispatch(new GenerateAccessToken($user->id)); + + $payload = ['authenticated' => true]; + } + + $content = sprintf('', json_encode($payload)); + + $response = new HtmlResponse($content); + + if (isset($accessToken)) { + $response = $this->withRememberCookie($response, $accessToken->id); + } + + return $response; + } +} diff --git a/framework/core/src/Forum/Actions/LoginAction.php b/framework/core/src/Forum/Actions/LoginAction.php index 847c0c5a0..d089df799 100644 --- a/framework/core/src/Forum/Actions/LoginAction.php +++ b/framework/core/src/Forum/Actions/LoginAction.php @@ -47,7 +47,7 @@ class LoginAction extends Action /** * @param Request $request * @param array $routeParams - * @return \Psr\Http\Message\ResponseInterface|EmptyResponse + * @return JsonResponse|EmptyResponse */ public function handle(Request $request, array $routeParams = []) { diff --git a/framework/core/src/Forum/Actions/RegisterAction.php b/framework/core/src/Forum/Actions/RegisterAction.php new file mode 100644 index 000000000..db9d74e9e --- /dev/null +++ b/framework/core/src/Forum/Actions/RegisterAction.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Forum\Actions; + +use Flarum\Api\Client; +use Flarum\Api\AccessToken; +use Flarum\Events\UserLoggedIn; +use Flarum\Support\Action; +use Flarum\Api\Commands\GenerateAccessToken; +use Psr\Http\Message\ServerRequestInterface as Request; +use Zend\Diactoros\Response\EmptyResponse; +use Zend\Diactoros\Response\JsonResponse; +use Illuminate\Contracts\Bus\Dispatcher; +use DateTime; + +class RegisterAction extends Action +{ + use WritesRememberCookie; + + /** + * @var Dispatcher + */ + protected $bus; + + /** + * @var Client + */ + protected $apiClient; + + /** + * @param Dispatcher $bus + * @param Client $apiClient + */ + public function __construct(Dispatcher $bus, Client $apiClient) + { + $this->bus = $bus; + $this->apiClient = $apiClient; + } + + /** + * @param Request $request + * @param array $routeParams + * + * @return JsonResponse + */ + public function handle(Request $request, array $routeParams = []) + { + $params = ['data' => ['attributes' => $request->getAttributes()]]; + + $apiResponse = $this->apiClient->send(app('flarum.actor'), 'Flarum\Api\Actions\Users\CreateAction', $params); + + $body = $apiResponse->getBody(); + $statusCode = $apiResponse->getStatusCode(); + + $response = new JsonResponse($body, $statusCode); + + if (! empty($body->data->attributes->isActivated)) { + $token = $this->bus->dispatch(new GenerateAccessToken($body->data->id)); + + // Extend the token's expiry to 2 weeks so that we can set a + // remember cookie + AccessToken::where('id', $token->id)->update(['expires_at' => new DateTime('+2 weeks')]); + + return $this->withRememberCookie( + $response, + $token->id + ); + } + + return $response; + } +} diff --git a/framework/core/src/Forum/Actions/WritesRememberCookie.php b/framework/core/src/Forum/Actions/WritesRememberCookie.php index a52872ed4..24e61d80f 100644 --- a/framework/core/src/Forum/Actions/WritesRememberCookie.php +++ b/framework/core/src/Forum/Actions/WritesRememberCookie.php @@ -24,6 +24,7 @@ trait WritesRememberCookie SetCookie::create('flarum_remember', $token) ->withMaxAge(14 * 24 * 60 * 60) ->withPath('/') + ->withHttpOnly(true) ); } @@ -35,6 +36,7 @@ trait WritesRememberCookie SetCookie::create('flarum_remember') ->withMaxAge(-2628000) ->withPath('/') + ->withHttpOnly(true) ); } } diff --git a/framework/core/src/Forum/ForumServiceProvider.php b/framework/core/src/Forum/ForumServiceProvider.php index af6d98acf..0edd323e6 100644 --- a/framework/core/src/Forum/ForumServiceProvider.php +++ b/framework/core/src/Forum/ForumServiceProvider.php @@ -102,6 +102,12 @@ class ForumServiceProvider extends ServiceProvider $this->action('Flarum\Forum\Actions\LoginAction') ); + $routes->post( + '/register', + 'flarum.forum.register', + $this->action('Flarum\Forum\Actions\RegisterAction') + ); + $routes->get( '/confirm/{token}', 'flarum.forum.confirmEmail', diff --git a/framework/core/src/Forum/Middleware/LoginWithCookie.php b/framework/core/src/Forum/Middleware/LoginWithCookie.php index 95e5e9605..78ce75879 100644 --- a/framework/core/src/Forum/Middleware/LoginWithCookie.php +++ b/framework/core/src/Forum/Middleware/LoginWithCookie.php @@ -36,14 +36,46 @@ class LoginWithCookie implements MiddlewareInterface */ public function __invoke(Request $request, Response $response, callable $out = null) { - if (($token = array_get($request->getCookieParams(), 'flarum_remember')) && - ($accessToken = AccessToken::valid($token)) - ) { - $this->app->instance('flarum.actor', $user = $accessToken->user); - - $user->updateLastSeen()->save(); - } + $this->logIn($request); return $out ? $out($request, $response) : $response; } + + /** + * Set the application's actor instance according to the request token. + * + * @param Request $request + * @return bool + */ + protected function logIn(Request $request) + { + if ($token = $this->getToken($request)) { + if (! $token->isValid()) { + // TODO: https://github.com/flarum/core/issues/253 + } elseif ($token->user) { + $this->app->instance('flarum.actor', $user = $token->user); + + $user->updateLastSeen()->save(); + + return true; + } + } + + return false; + } + + /** + * Get the access token referred to by the request cookie. + * + * @param Request $request + * @return AccessToken|null + */ + protected function getToken(Request $request) + { + $token = array_get($request->getCookieParams(), 'flarum_remember'); + + if ($token) { + return AccessToken::find($token); + } + } } diff --git a/framework/core/src/Install/Console/InstallCommand.php b/framework/core/src/Install/Console/InstallCommand.php index 626b41bf6..b4c15cdd0 100644 --- a/framework/core/src/Install/Console/InstallCommand.php +++ b/framework/core/src/Install/Console/InstallCommand.php @@ -11,12 +11,14 @@ namespace Flarum\Install\Console; use Flarum\Console\Command; +use Flarum\Core\Exceptions\ValidationException; use Flarum\Core\Model; use Flarum\Core\Users\User; use Flarum\Core\Groups\Group; use Flarum\Core\Groups\Permission; use Illuminate\Contracts\Foundation\Application; use Illuminate\Support\Arr; +use Illuminate\Validation\Factory; use PDO; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -100,6 +102,43 @@ class InstallCommand extends Command protected function install() { try { + $this->dbConfig = $this->dataSource->getDatabaseConfiguration(); + + $validation = $this->getValidator()->make( + $this->dbConfig, + [ + 'driver' => 'required|in:mysql', + 'host' => 'required', + 'database' => 'required|alpha_dash', + 'username' => 'required|alpha_dash', + 'prefix' => 'alpha_dash|max:10' + ] + ); + + if ($validation->fails()) { + throw new Exception(implode("\n", call_user_func_array('array_merge', $validation->getMessageBag()->toArray()))); + } + + $this->baseUrl = $this->dataSource->getBaseUrl(); + $this->settings = $this->dataSource->getSettings(); + $this->adminUser = $admin = $this->dataSource->getAdminUser(); + + if (strlen($admin['password']) < 8) { + throw new Exception('Password must be at least 8 characters.'); + } + + if ($admin['password'] !== $admin['password_confirmation']) { + throw new Exception('The password did not match its confirmation.'); + } + + if (! filter_var($admin['email'], FILTER_VALIDATE_EMAIL)) { + throw new Exception('You must enter a valid email.'); + } + + if (! $admin['username'] || preg_match('/[^a-z0-9_-]/i', $admin['username'])) { + throw new Exception('Username can only contain letters, numbers, underscores, and dashes.'); + } + $this->storeConfiguration(); $this->runMigrations(); @@ -127,7 +166,7 @@ class InstallCommand extends Command protected function storeConfiguration() { - $dbConfig = $this->dataSource->getDatabaseConfiguration(); + $dbConfig = $this->dbConfig; $config = [ 'debug' => true, @@ -142,7 +181,7 @@ class InstallCommand extends Command 'prefix' => $dbConfig['prefix'], 'strict' => false ], - 'url' => $this->dataSource->getBaseUrl(), + 'url' => $this->baseUrl, 'paths' => [ 'api' => 'api', 'admin' => 'admin', @@ -186,12 +225,11 @@ class InstallCommand extends Command protected function writeSettings() { - $data = $this->dataSource->getSettings(); $settings = $this->application->make('Flarum\Core\Settings\SettingsRepository'); $this->info('Writing default settings'); - foreach ($data as $k => $v) { + foreach ($this->settings as $k => $v) { $settings->set($k, $v); } } @@ -246,7 +284,7 @@ class InstallCommand extends Command protected function createAdminUser() { - $admin = $this->dataSource->getAdminUser(); + $admin = $this->adminUser; if ($admin['password'] !== $admin['password_confirmation']) { throw new Exception('The password did not match its confirmation.'); @@ -254,11 +292,13 @@ class InstallCommand extends Command $this->info('Creating admin user '.$admin['username']); - User::unguard(); + $user = User::register( + $admin['username'], + $admin['email'], + $admin['password'] + ); - $user = new User(Arr::except($admin, 'password_confirmation')); $user->is_activated = 1; - $user->join_time = time(); $user->save(); $user->groups()->sync([1]); @@ -300,6 +340,14 @@ class InstallCommand extends Command return $this->application->make('Flarum\Install\Prerequisites\Prerequisite'); } + /** + * @return \Illuminate\Contracts\Validation\Factory + */ + protected function getValidator() + { + return new Factory($this->application->make('Symfony\Component\Translation\TranslatorInterface')); + } + protected function showErrors($errors) { foreach ($errors as $error) { diff --git a/framework/core/views/install/app.php b/framework/core/views/install/app.php index 0f57b6146..e6facc153 100644 --- a/framework/core/views/install/app.php +++ b/framework/core/views/install/app.php @@ -162,7 +162,7 @@

- +