updated-packages

This commit is contained in:
RafficMohammed
2023-01-08 00:13:22 +05:30
parent 3ff7df7487
commit da241bacb6
12659 changed files with 563377 additions and 510538 deletions

View File

@@ -1,4 +1,203 @@
# Release Notes for 1.0.x
# Release Notes
## [Unreleased](https://github.com/laravel/dusk/compare/v5.11.0...5.0)
## [v5.11.0 (2020-03-24)](https://github.com/laravel/dusk/compare/v5.10.0...v5.11.0)
### Added
- Add assert attribute methods ([#751](https://github.com/laravel/dusk/pull/751))
### Fixed
- Fix Call to undefined method ([#750](https://github.com/laravel/dusk/pull/750))
- Avoid throwing exception on production ([#755](https://github.com/laravel/dusk/pull/755))
## [v5.10.0 (2020-03-17)](https://github.com/laravel/dusk/compare/v5.9.2...v5.10.0)
### Added
- Adds `typeSlowly` & `appendSlowly` ([#748](https://github.com/laravel/dusk/pull/748))
## [v5.9.2 (2020-02-18)](https://github.com/laravel/dusk/compare/v5.9.1...v5.9.2)
### Fixed
- Bugfix quoting for `InteractsWithElements::value` ([#735](https://github.com/laravel/dusk/pull/735))
- Remove php-webdriver constraints ([#737](https://github.com/laravel/dusk/pull/737))
## [v5.9.1 (2020-02-12)](https://github.com/laravel/dusk/compare/v5.9.0...v5.9.1)
### Fixed
- Adds the missing import for `InteractsWithMouse@clickAtXPath` ([#728](https://github.com/laravel/dusk/pull/728))
- Size sanity check at fitContent ([#730](https://github.com/laravel/dusk/pull/730))
- Lock php-webdriver constraints for now ([#733](https://github.com/laravel/dusk/pull/733))
## [v5.9.0 (2020-01-28)](https://github.com/laravel/dusk/compare/v5.8.2...v5.9.0)
### Added
- Add `clickAtXPath` ([#723](https://github.com/laravel/dusk/pull/723), [effe73d](https://github.com/laravel/dusk/commit/effe73d6eb61b4bd77f88814bcd679e4fceb6f25))
- Add `ProvidesBrowser::getCallerName()` ([#725](https://github.com/laravel/dusk/pull/725))
### Fixed
- Fit content to `<html>` instead of `<body>` ([#726](https://github.com/laravel/dusk/pull/726))
## [v5.8.2 (2020-01-21)](https://github.com/laravel/dusk/compare/v5.8.1...v5.8.2)
### Changed
- Rename php-webdriver package ([#720](https://github.com/laravel/dusk/pull/720))
- Update jQuery file ([#721](https://github.com/laravel/dusk/pull/721))
### Fixed
- Update `fitContent()` ([#717](https://github.com/laravel/dusk/pull/717))
## [v5.8.1 (2020-01-07)](https://github.com/laravel/dusk/compare/v5.8.0...v5.8.1)
### Fixed
- Cast boolean values to appropriate string ([#713](https://github.com/laravel/dusk/pull/713))
## [v5.8.0 (2019-12-30)](https://github.com/laravel/dusk/compare/v5.7.0...v5.8.0)
### Added
- Add "waitUntilMissingText" ([#706](https://github.com/laravel/dusk/pull/706))
- Add ability to store source from browser ([#707](https://github.com/laravel/dusk/pull/707), [9c90e2a](https://github.com/laravel/dusk/commit/9c90e2a716030c9a36e6306c3f67d606a254bbb7), [1d5bc20](https://github.com/laravel/dusk/commit/1d5bc203b67ffc5a17eb1b89f3e22547e3ea174b))
## [v5.7.0 (2019-12-17)](https://github.com/laravel/dusk/compare/v5.6.3...v5.7.0)
### Added
- Automatically fit content on failures ([#704](https://github.com/laravel/dusk/pull/704))
## [v5.6.3 (2019-12-03)](https://github.com/laravel/dusk/compare/v5.6.2...v5.6.3)
### Added
- Support phpdotenv v4 ([#699](https://github.com/laravel/dusk/pull/699))
### Fixed
- scrollTo: add support for selectors with quotes ([#697](https://github.com/laravel/dusk/pull/697))
## [v5.6.2 (2019-11-26)](https://github.com/laravel/dusk/compare/v5.6.1...v5.6.2)
### Changed
- Allow for Symfony 5 ([#696](https://github.com/laravel/dusk/pull/696))
## [v5.6.1 (2019-11-12)](https://github.com/laravel/dusk/compare/v5.6.0...v5.6.1)
### Fixed
- Ensure jQuery for scrollTo ([#686](https://github.com/laravel/dusk/pull/686))
- Added missing return statement in withDuskEnvironment ([#691](https://github.com/laravel/dusk/pull/691))
- Prevent using pcntl when not installed ([#692](https://github.com/laravel/dusk/pull/692))
## [v5.6.0 (2019-10-29)](https://github.com/laravel/dusk/compare/v5.5.2...v5.6.0)
### Added
- Add scrollTo method ([#684](https://github.com/laravel/dusk/pull/684))
### Fixed
- Add graceful handler for `SIGINT` for .env restoration ([#682](https://github.com/laravel/dusk/pull/682), [f843b8a](https://github.com/laravel/dusk/commit/f843b8a51ae96933cefcc74dec515377d3135611))
## [v5.5.2 (2019-09-24)](https://github.com/laravel/dusk/compare/v5.5.1...v5.5.2)
### Fixed
- Improve detection of latest stable ChromeDriver release ([#677](https://github.com/laravel/dusk/pull/677))
## [v5.5.1 (2019-09-12)](https://github.com/laravel/dusk/compare/v5.5.0...v5.5.1)
### Fixed
- Update regular expression base on website changes ([#674](https://github.com/laravel/dusk/pull/674))
## [v5.5.0 (2019-08-06)](https://github.com/laravel/dusk/compare/v5.4.0...v5.5.0)
### Added
- Allow saving screenshots in a subdirectory ([#662](https://github.com/laravel/dusk/pull/662))
## [v5.4.0 (2019-07-30)](https://github.com/laravel/dusk/compare/v5.3.0...v5.4.0)
### Added
- Add assertion checks if a button is disabled or enabled ([#661](https://github.com/laravel/dusk/pull/661))
### Fixed
- Update constraints for Laravel 6.0 ([e4b4d63](https://github.com/laravel/dusk/commit/e4b4d63c179bb1f228db22852bd776db750d1ec6))
## [v5.3.0 (2019-07-11)](https://github.com/laravel/dusk/compare/v5.2.0...v5.3.0)
### Added
- Add proxy support to the `dusk:install` command ([#659](https://github.com/laravel/dusk/pull/659))
## [v5.2.0 (2019-06-25)](https://github.com/laravel/dusk/compare/v5.1.1...v5.2.0)
### Added
- Add option to fullsize the browser ([#655](https://github.com/laravel/dusk/pull/655))
## [v5.1.1 (2019-06-14)](https://github.com/laravel/dusk/compare/v5.1.0...v5.1.1)
### Fixed
- Add latest version of Facebook Webdriver ([#654](https://github.com/laravel/dusk/pull/654))
## [v5.1.0 (2019-05-02)](https://github.com/laravel/dusk/compare/v5.0.3...v5.1.0)
### Added
- Implement ChromeDriverCommand ([#643](https://github.com/laravel/dusk/pull/643), [60339a5](https://github.com/laravel/dusk/commit/60339a521a1b05e55af7c90b3151557100a0db4d), [#644](https://github.com/laravel/dusk/pull/644))
## [v5.0.3 (2019-04-02)](https://github.com/laravel/dusk/compare/v5.0.2...v5.0.3)
### Fixed
- Fix `assertVueContains` and `assertVueDoesNotContain` ([#638](https://github.com/laravel/dusk/pull/638))
## [v5.0.2 (2019-03-12)](https://github.com/laravel/dusk/compare/v5.0.1...v5.0.2)
### Fixed
- Fix cookies with falsey values ([#617](https://github.com/laravel/dusk/pull/617))
- Fix `with()` and page assertions ([#625](https://github.com/laravel/dusk/pull/625))
- Avoid deprecation messages on PHPUnit 8 ([#620](https://github.com/laravel/dusk/pull/620))
## [v5.0.1 (2019-02-27)](https://github.com/laravel/dusk/compare/v5.0.0...v5.0.1)
### Added
- Added support for `phpunit.dusk.xml.dist` ([#610](https://github.com/laravel/dusk/pull/610))
- Use custom DISPLAY variable when set on Linux ([#613](https://github.com/laravel/dusk/pull/613), [2480495](https://github.com/laravel/dusk/commit/24804953c5b521415a717afbf82ff4b938c10535))
### Fixed
- Added missing dependencies ([98eccfd](https://github.com/laravel/dusk/commit/98eccfd56e9b2b23b093b801f62c766aaf67589f))
- Fix installation of Dotenv on Laravel 5.8 ([1f67bf2](https://github.com/laravel/dusk/commit/1f67bf204fab65a212975683b5391c2f55dd3bcf))
## [v5.0.0 (2019-02-12)](https://github.com/laravel/dusk/compare/v4.0.5...v5.0.0)
### Added
- Support PHPUnit 8 ([788e79c](https://github.com/laravel/dusk/commit/788e79c4713a5706eeafaf7270986a71a4ed43be))
- Support Carbon 2 ([85cfc50](https://github.com/laravel/dusk/commit/85cfc50e126a3835428577052cc0660d1c3b639e))
- Support Laravel 5.8 ([5b2e9aa](https://github.com/laravel/dusk/commit/5b2e9aa9e4a124f4c4878f65fb644101de1644e7))
### Changed
- Update minimum Laravel version ([fcb2226](https://github.com/laravel/dusk/commit/fcb2226a524f47b51b9b49316d87bc51738733d7))
- Update Symfony dependencies to latest version ([788e79c](https://github.com/laravel/dusk/commit/788e79c4713a5706eeafaf7270986a71a4ed43be))
- Prefer stable dependencies ([fdb2fd4](https://github.com/laravel/dusk/commit/fdb2fd4b2a2e787b08cf44649c4eef84837324ca))
## [v4.0.0 (2018-08-11)](https://github.com/laravel/dusk/compare/v3.0.10...v4.0.0)
Dusk 4.0.0 disables cookie serialization and is intended for use with Laravel 5.6.30 or later. If you are using a previous version of Laravel, please continue using Dusk 3.0.0.
## v1.0.13 (2017-04-20)
@@ -16,6 +215,7 @@
- Use `getAttribute('value')` instead of `getText()` for `textarea` elements ([#237](https://github.com/laravel/dusk/pull/237))
- Fixed bug when giving strings with apostrophe to `clickLink()` ([#228](https://github.com/laravel/dusk/pull/228))
## v1.0.12 (2017-04-07)
### Added
@@ -38,6 +238,3 @@
- Updated ChromeDriver to v2.28 so that it works with Chrome 57 ([#199](https://github.com/laravel/dusk/pull/199))
- Comparison to `option` inside `select` will no longer be strict ([#178](https://github.com/laravel/dusk/pull/178))
- Type-hint Browser for auto-complete support ([#174](https://github.com/laravel/dusk/pull/174))
## v1.0.10 (2017-03-03)

View File

@@ -1,7 +1,7 @@
<p align="center"><img src="https://laravel.com/assets/img/components/logo-dusk.svg"></p>
<p align="center">
<a href="https://travis-ci.org/laravel/dusk"><img src="https://travis-ci.org/laravel/dusk.svg" alt="Build Status"></a>
<a href="https://github.com/laravel/dusk/actions"><img src="https://github.com/laravel/dusk/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/dusk"><img src="https://poser.pugx.org/laravel/dusk/d/total.svg" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/dusk"><img src="https://poser.pugx.org/laravel/dusk/v/stable.svg" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/dusk"><img src="https://poser.pugx.org/laravel/dusk/license.svg" alt="License"></a>
@@ -13,8 +13,20 @@ Laravel Dusk provides an expressive, easy-to-use browser automation and testing
## Official Documentation
Documentation for Dusk can be found on the [Laravel website](https://laravel.com/docs/master/dusk).
Documentation for Dusk can be found on the [Laravel website](https://laravel.com/docs/dusk).
## Contributing
Thank you for considering contributing to Dusk! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
## Code of Conduct
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## Security Vulnerabilities
Please review [our security policy](https://github.com/laravel/dusk/security/policy) on how to report security vulnerabilities.
## License
Laravel Dusk is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT)
Laravel Dusk is open-sourced software licensed under the [MIT license](LICENSE.md).

17
vendor/laravel/dusk/UPGRADE.md vendored Normal file
View File

@@ -0,0 +1,17 @@
# Upgrade Guide
## Upgrading To 5.0 From 4.0
### PHPUnit 8
Dusk now provides optional support for PHPUnit 8, which requires PHP >= 7.2 Please read through the entire list of changes in [the PHPUnit 8 release announcement](https://phpunit.de/announcements/phpunit-8.html). Using PHPUnit 8 will require Laravel 5.8, which will be released at the end of February 2019.
You may also continue using PHPUnit 7, which requires a minimum of PHP 7.1.
### Minimum Laravel version
Laravel 5.7 is now the minimum supported version of the framework and you should upgrade to continue using Dusk.
### `setUp` and `tearDown` changes
The `setUp` and `tearDown` methods now require the `void` return type. If you were overwriting these methods you should add it to the method signatures.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -11,25 +11,37 @@
],
"require": {
"php": ">=7.1.0",
"facebook/webdriver": "~1.3",
"nesbot/carbon": "~1.20",
"illuminate/console": "~5.6",
"illuminate/support": "~5.6",
"symfony/console": "~4.0",
"symfony/process": "~4.0"
"ext-json": "*",
"ext-zip": "*",
"php-webdriver/webdriver": "^1.8.1",
"nesbot/carbon": "^1.20|^2.0",
"illuminate/console": "~5.7.0|~5.8.0|^6.0|^7.0",
"illuminate/support": "~5.7.0|~5.8.0|^6.0|^7.0",
"symfony/console": "^4.0|^5.0",
"symfony/finder": "^4.0|^5.0",
"symfony/process": "^4.0|^5.0",
"vlucas/phpdotenv": "^2.2|^3.0|^4.0"
},
"require-dev": {
"mockery/mockery": "~1.0",
"phpunit/phpunit": "~7.0"
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^7.5|^8.0"
},
"suggest": {
"ext-pcntl": "Used to gracefully terminate Dusk when tests are running."
},
"autoload": {
"psr-4": {
"Laravel\\Dusk\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Laravel\\Dusk\\Tests\\": "tests/"
}
},
"extra": {
"branch-alias": {
"dev-master": "4.0-dev"
"dev-master": "5.0-dev"
},
"laravel": {
"providers": [
@@ -37,5 +49,9 @@
]
}
},
"minimum-stability": "dev"
"config": {
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true
}

View File

@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
beStrictAboutTestsThatDoNotTestAnything="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
failOnRisky="true"
failOnWarning="true"
processIsolation="false"
stopOnError="false"
stopOnFailure="false"
verbose="true"
>
<testsuites>
<testsuite name="Laravel Dusk Test Suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./src</directory>
</whitelist>
</filter>
</phpunit>

View File

@@ -2,12 +2,14 @@
namespace Laravel\Dusk;
use Closure;
use BadMethodCallException;
use Closure;
use Facebook\WebDriver\Remote\WebDriverBrowserType;
use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\WebDriverDimension;
use Facebook\WebDriver\WebDriverPoint;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Macroable;
use Facebook\WebDriver\WebDriverDimension;
use Facebook\WebDriver\Remote\WebDriverBrowserType;
class Browser
{
@@ -17,6 +19,7 @@ class Browser
Concerns\InteractsWithJavascript,
Concerns\InteractsWithMouse,
Concerns\MakesAssertions,
Concerns\MakesUrlAssertions,
Concerns\WaitsForElements,
Macroable {
__call as macroCall;
@@ -43,6 +46,13 @@ class Browser
*/
public static $storeConsoleLogAt;
/**
* The directory where source code snapshots will be stored.
*
* @var string
*/
public static $storeSourceAt;
/**
* The browsers that support retrieving logs.
*
@@ -77,7 +87,7 @@ class Browser
/**
* The element resolver instance.
*
* @var ElementResolver
* @var \Laravel\Dusk\ElementResolver
*/
public $resolver;
@@ -95,11 +105,18 @@ class Browser
*/
public $component;
/**
* Indicates that the browser should be resized to fit the entire "body" before screenshotting failures.
*
* @var bool
*/
public $fitOnFailure = true;
/**
* Create a browser instance.
*
* @param \Facebook\WebDriver\Remote\RemoteWebDriver $driver
* @param ElementResolver $resolver
* @param \Laravel\Dusk\ElementResolver $resolver
* @return void
*/
public function __construct($driver, $resolver = null)
@@ -164,6 +181,21 @@ class Browser
* @return $this
*/
public function on($page)
{
$this->onWithoutAssert($page);
$page->assert($this);
return $this;
}
/**
* Set the current page object without executing the assertions.
*
* @param mixed $page
* @return $this
*/
public function onWithoutAssert($page)
{
$this->page = $page;
@@ -174,8 +206,6 @@ class Browser
$page::siteElements(), $page->elements()
));
$page->assert($this);
return $this;
}
@@ -231,6 +261,81 @@ class Browser
return $this;
}
/**
* Make the browser window as large as the content.
*
* @return $this
*/
public function fitContent()
{
$this->driver->switchTo()->defaultContent();
$html = $this->driver->findElement(WebDriverBy::tagName('html'));
if (! empty($html) && ($html->getSize()->getWidth() <= 0 || $html->getSize()->getHeight() <= 0)) {
$this->resize($html->getSize()->getWidth(), $html->getSize()->getHeight());
}
return $this;
}
/**
* Disable fit on failures.
*
* @return $this
*/
public function disableFitOnFailure()
{
$this->fitOnFailure = false;
return $this;
}
/**
* Enable fit on failures.
*
* @return $this
*/
public function enableFitOnFailure()
{
$this->fitOnFailure = true;
return $this;
}
/**
* Move the browser window.
*
* @param int $x
* @param int $y
* @return $this
*/
public function move($x, $y)
{
$this->driver->manage()->window()->setPosition(
new WebDriverPoint($x, $y)
);
return $this;
}
/**
* Scroll screen to element at the given selector.
*
* @param string $selector
* @return $this
*/
public function scrollTo($selector)
{
$this->ensurejQueryIsAvailable();
$selector = addslashes($this->resolver->format($selector));
$this->driver->executeScript("jQuery(\"html, body\").animate({scrollTop: jQuery(\"$selector\").offset().top}, 0);");
return $this;
}
/**
* Take a screenshot and store it with the given name.
*
@@ -239,9 +344,15 @@ class Browser
*/
public function screenshot($name)
{
$this->driver->takeScreenshot(
sprintf('%s/%s.png', rtrim(static::$storeScreenshotsAt, '/'), $name)
);
$filePath = sprintf('%s/%s.png', rtrim(static::$storeScreenshotsAt, '/'), $name);
$directoryPath = dirname($filePath);
if (! is_dir($directoryPath)) {
mkdir($directoryPath, 0777, true);
}
$this->driver->takeScreenshot($filePath);
return $this;
}
@@ -257,10 +368,9 @@ class Browser
if (in_array($this->driver->getCapabilities()->getBrowserName(), static::$supportsRemoteLogs)) {
$console = $this->driver->manage()->getLog('browser');
if (!empty($console)) {
if (! empty($console)) {
file_put_contents(
sprintf('%s/%s.log', rtrim(static::$storeConsoleLogAt, '/'), $name)
, json_encode($console, JSON_PRETTY_PRINT)
sprintf('%s/%s.log', rtrim(static::$storeConsoleLogAt, '/'), $name), json_encode($console, JSON_PRETTY_PRINT)
);
}
}
@@ -269,9 +379,29 @@ class Browser
}
/**
* Switch to a specified frame in the browser.
* Store a snapshot of the page's current source code with the given name.
*
* @param string $name
* @return $this
*/
public function storeSource($name)
{
$source = $this->driver->getPageSource();
if (! empty($source)) {
file_put_contents(
sprintf('%s/%s.txt', rtrim(static::$storeSourceAt, '/'), $name), $source
);
}
return $this;
}
/**
* Switch to a specified frame in the browser and execute the given callback.
*
* @param string $selector
* @param \Closure $callback
* @return $this
*/
public function withinFrame($selector, Closure $callback)
@@ -311,7 +441,7 @@ class Browser
);
if ($this->page) {
$browser->on($this->page);
$browser->onWithoutAssert($this->page);
}
if ($selector instanceof Component) {
@@ -355,7 +485,7 @@ class Browser
*/
public function ensurejQueryIsAvailable()
{
if ($this->driver->executeScript("return window.jQuery == null")) {
if ($this->driver->executeScript('return window.jQuery == null')) {
$this->driver->executeScript(file_get_contents(__DIR__.'/../bin/jquery.js'));
}
}
@@ -439,6 +569,8 @@ class Browser
* @param string $method
* @param array $parameters
* @return mixed
*
* @throws \BadMethodCallException
*/
public function __call($method, $parameters)
{

View File

@@ -2,8 +2,8 @@
namespace Laravel\Dusk\Chrome;
use Laravel\Dusk\OperatingSystem;
use RuntimeException;
use Illuminate\Support\Str;
use Symfony\Component\Process\Process;
class ChromeProcess
@@ -11,7 +11,7 @@ class ChromeProcess
/**
* The path to the Chromedriver.
*
* @var String
* @var string
*/
protected $driver;
@@ -20,6 +20,8 @@ class ChromeProcess
*
* @param string $driver
* @return void
*
* @throws \RuntimeException
*/
public function __construct($driver = null)
{
@@ -33,37 +35,37 @@ class ChromeProcess
/**
* Build the process to run Chromedriver.
*
* @param array $arguments
* @return \Symfony\Component\Process\Process
*/
public function toProcess()
public function toProcess(array $arguments = [])
{
if ($this->driver) {
return $this->process();
return $this->process($arguments);
}
if ($this->onWindows()) {
$this->driver = realpath(__DIR__.'/../../bin/chromedriver-win.exe');
return $this->process();
} elseif ($this->onMac()) {
$this->driver = realpath(__DIR__.'/../../bin/chromedriver-mac');
} else {
$this->driver = realpath(__DIR__.'/../../bin/chromedriver-linux');
}
$this->driver = $this->onMac()
? realpath(__DIR__.'/../../bin/chromedriver-mac')
: realpath(__DIR__.'/../../bin/chromedriver-linux');
return $this->process();
return $this->process($arguments);
}
/**
* Build the Chromedriver with Symfony Process.
*
* @param array $arguments
* @return \Symfony\Component\Process\Process
*/
protected function process()
protected function process(array $arguments = [])
{
return (new Process(
[realpath($this->driver)], null, $this->chromeEnvironment()
));
return new Process(
array_merge([realpath($this->driver)], $arguments), null, $this->chromeEnvironment()
);
}
/**
@@ -77,7 +79,7 @@ class ChromeProcess
return [];
}
return ['DISPLAY' => ':0'];
return ['DISPLAY' => $_ENV['DISPLAY'] ?? ':0'];
}
/**
@@ -87,7 +89,7 @@ class ChromeProcess
*/
protected function onWindows()
{
return PHP_OS === 'WINNT' || Str::contains(php_uname(), 'Microsoft');
return OperatingSystem::onWindows();
}
/**
@@ -97,6 +99,6 @@ class ChromeProcess
*/
protected function onMac()
{
return PHP_OS === 'Darwin';
return OperatingSystem::onMac();
}
}

View File

@@ -21,13 +21,14 @@ trait SupportsChrome
/**
* Start the Chromedriver process.
*
* @throws \RuntimeException if the driver file path doesn't exist.
*
* @param array $arguments
* @return void
*
* @throws \RuntimeException
*/
public static function startChromeDriver()
public static function startChromeDriver(array $arguments = [])
{
static::$chromeProcess = static::buildChromeProcess();
static::$chromeProcess = static::buildChromeProcess($arguments);
static::$chromeProcess->start();
@@ -51,13 +52,14 @@ trait SupportsChrome
/**
* Build the process to run the Chromedriver.
*
*
* @param array $arguments
* @return \Symfony\Component\Process\Process
* @throws \RuntimeException if the driver file path doesn't exist.
*
* @throws \RuntimeException
*/
protected static function buildChromeProcess()
protected static function buildChromeProcess(array $arguments = [])
{
return (new ChromeProcess(static::$chromeDriver))->toProcess();
return (new ChromeProcess(static::$chromeDriver))->toProcess($arguments);
}
/**

View File

@@ -21,7 +21,7 @@ trait InteractsWithAuthentication
* Log into the application using a given user ID or email.
*
* @param object|string $userId
* @param string $guard
* @param string $guard
* @return $this
*/
public function loginAs($userId, $guard = null)

View File

@@ -17,12 +17,12 @@ trait InteractsWithCookies
*/
public function cookie($name, $value = null, $expiry = null, array $options = [])
{
if ($value) {
if (! is_null($value)) {
return $this->addCookie($name, $value, $expiry, $options);
}
if ($cookie = $this->driver->manage()->getCookieNamed($name)) {
return decrypt(rawurldecode($cookie['value']));
return decrypt(rawurldecode($cookie['value']), $unserialize = false);
}
}
@@ -37,7 +37,7 @@ trait InteractsWithCookies
*/
public function plainCookie($name, $value = null, $expiry = null, array $options = [])
{
if ($value) {
if (! is_null($value)) {
return $this->addCookie($name, $value, $expiry, $options, false);
}
@@ -59,7 +59,7 @@ trait InteractsWithCookies
public function addCookie($name, $value, $expiry = null, array $options = [], $encrypt = true)
{
if ($encrypt) {
$value = encrypt($value);
$value = encrypt($value, $serialize = false);
}
if ($expiry instanceof DateTimeInterface) {

View File

@@ -2,11 +2,11 @@
namespace Laravel\Dusk\Concerns;
use Illuminate\Support\Str;
use Facebook\WebDriver\Interactions\WebDriverActions;
use Facebook\WebDriver\Remote\LocalFileDetector;
use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\WebDriverKeys;
use Facebook\WebDriver\Remote\LocalFileDetector;
use Facebook\WebDriver\Interactions\WebDriverActions;
use Illuminate\Support\Str;
trait InteractsWithElements
{
@@ -39,11 +39,11 @@ trait InteractsWithElements
* @param string $element
* @return $this
*/
public function clickLink($link, $element = "a")
public function clickLink($link, $element = 'a')
{
$this->ensurejQueryIsAvailable();
$selector = addslashes(trim($this->resolver->format("{$element}:contains({$link})")));
$selector = addslashes(trim($this->resolver->format("{$element}:contains({$link}):visible")));
$this->driver->executeScript("jQuery.find(\"{$selector}\")[0].click();");
@@ -66,7 +66,7 @@ trait InteractsWithElements
$selector = $this->resolver->format($selector);
$this->driver->executeScript(
"document.querySelector('{$selector}').value = '{$value}';"
'document.querySelector('.json_encode($selector).').value = '.json_encode($value).';'
);
return $this;
@@ -144,6 +144,21 @@ trait InteractsWithElements
return $this;
}
/**
* Type the given value in the given field slowly.
*
* @param string $field
* @param string $value
* @param int $pause
* @return $this
*/
public function typeSlowly($field, $value, $pause = 100)
{
$this->clear($field)->appendSlowly($field, $value, $pause);
return $this;
}
/**
* Type the given value in the given field without clearing it.
*
@@ -158,6 +173,23 @@ trait InteractsWithElements
return $this;
}
/**
* Type the given value in the given field slowly without clearing it.
*
* @param string $field
* @param string $value
* @param int $pause
* @return $this
*/
public function appendSlowly($field, $value, $pause = 100)
{
foreach (str_split($value) as $char) {
$this->append($field, $char)->pause($pause);
}
return $this;
}
/**
* Clear the given field.
*
@@ -182,11 +214,15 @@ trait InteractsWithElements
{
$element = $this->resolver->resolveForSelection($field);
$options = $element->findElements(WebDriverBy::tagName('option'));
$options = $element->findElements(WebDriverBy::cssSelector('option:not([disabled])'));
if (is_null($value)) {
$options[array_rand($options)]->click();
} else {
if (is_bool($value)) {
$value = $value ? '1' : '0';
}
foreach ($options as $option) {
if ((string) $option->getAttribute('value') === (string) $value) {
$option->click();

View File

@@ -3,6 +3,7 @@
namespace Laravel\Dusk\Concerns;
use Facebook\WebDriver\Interactions\WebDriverActions;
use Facebook\WebDriver\WebDriverBy;
trait InteractsWithMouse
{
@@ -54,6 +55,21 @@ trait InteractsWithMouse
return $this;
}
/**
* Click the element at the given XPath expression.
*
* @param string $selector
* @return $this
*/
public function clickAtXPath($expression)
{
$this->driver
->findElement(WebDriverBy::xpath($expression))
->click();
return $this;
}
/**
* Perform a mouse click and hold the mouse button down.
*

View File

@@ -2,10 +2,10 @@
namespace Laravel\Dusk\Concerns;
use Facebook\WebDriver\Exception\NoSuchElementException;
use Facebook\WebDriver\Remote\RemoteWebElement;
use Illuminate\Support\Str;
use PHPUnit\Framework\Assert as PHPUnit;
use Facebook\WebDriver\Remote\RemoteWebElement;
use Facebook\WebDriver\Exception\NoSuchElementException;
trait MakesAssertions
{
@@ -41,233 +41,6 @@ trait MakesAssertions
return $this;
}
/**
* Assert that the current URL matches the given URL.
*
* @param string $url
* @return $this
*/
public function assertUrlIs($url)
{
$pattern = str_replace('\*', '.*', preg_quote($url, '/'));
$segments = parse_url($this->driver->getCurrentURL());
$currentUrl = sprintf(
'%s://%s%s%s',
$segments['scheme'],
$segments['host'],
array_get($segments, 'port', '') ? ':'.$segments['port'] : '',
array_get($segments, 'path', '')
);
PHPUnit::assertRegExp(
'/^'.$pattern.'$/u', $currentUrl,
"Actual URL [{$this->driver->getCurrentURL()}] does not equal expected URL [{$url}]."
);
return $this;
}
/**
* Assert that the current URL path matches the given pattern.
*
* @param string $path
* @return $this
*/
public function assertPathIs($path)
{
$pattern = preg_quote($path, '/');
$pattern = str_replace('\*', '.*', $pattern);
$actualPath = parse_url($this->driver->getCurrentURL())['path'];
PHPUnit::assertRegExp(
'/^'.$pattern.'$/u', $actualPath,
"Actual path [{$actualPath}] does not equal expected path [{$path}]."
);
return $this;
}
/**
* Assert that the current URL path begins with given path.
*
* @param string $path
* @return $this
*/
public function assertPathBeginsWith($path)
{
$actualPath = parse_url($this->driver->getCurrentURL())['path'];
PHPUnit::assertStringStartsWith(
$path, $actualPath,
"Actual path [{$actualPath}] does not begin with expected path [{$path}]."
);
return $this;
}
/**
* Assert that the current URL path does not match the given path.
*
* @param string $path
* @return $this
*/
public function assertPathIsNot($path)
{
$actualPath = parse_url($this->driver->getCurrentURL())['path'];
PHPUnit::assertNotEquals(
$path, $actualPath,
"Path [{$path}] should not equal the actual value."
);
return $this;
}
/**
* Assert that the current URL fragment matches the given pattern.
*
* @param string $fragment
* @return $this
*/
public function assertFragmentIs($fragment)
{
$pattern = preg_quote($fragment, '/');
$actualFragment = (string) parse_url($this->driver->executeScript('return window.location.href;'), PHP_URL_FRAGMENT);
PHPUnit::assertRegExp(
'/^'.str_replace('\*', '.*', $pattern).'$/u', $actualFragment,
"Actual fragment [{$actualFragment}] does not equal expected fragment [{$fragment}]."
);
return $this;
}
/**
* Assert that the current URL fragment begins with given fragment.
*
* @param string $fragment
* @return $this
*/
public function assertFragmentBeginsWith($fragment)
{
$actualFragment = (string) parse_url($this->driver->executeScript('return window.location.href;'), PHP_URL_FRAGMENT);
PHPUnit::assertStringStartsWith(
$fragment, $actualFragment,
"Actual fragment [$actualFragment] does not begin with expected fragment [$fragment]."
);
return $this;
}
/**
* Assert that the current URL fragment does not match the given fragment.
*
* @param string $fragment
* @return $this
*/
public function assertFragmentIsNot($fragment)
{
$actualFragment = (string) parse_url($this->driver->executeScript('return window.location.href;'), PHP_URL_FRAGMENT);
PHPUnit::assertNotEquals(
$fragment, $actualFragment,
"Fragment [{$fragment}] should not equal the actual value."
);
return $this;
}
/**
* Assert that the current URL path matches the given route.
*
* @param string $route
* @param array $parameters
* @return $this
*/
public function assertRouteIs($route, $parameters = [])
{
return $this->assertPathIs(route($route, $parameters, false));
}
/**
* Assert that a query string parameter is present and has a given value.
*
* @param string $name
* @param string $value
* @return $this
*/
public function assertQueryStringHas($name, $value = null)
{
$output = $this->assertHasQueryStringParameter($name);
if (is_null($value)) {
return $this;
}
PHPUnit::assertEquals(
$value, $output[$name],
"Query string parameter [{$name}] had value [{$output[$name]}], but expected [{$value}]."
);
return $this;
}
/**
* Assert that the given query string parameter is missing.
*
* @param string $name
* @return $this
*/
public function assertQueryStringMissing($name)
{
$parsedUrl = parse_url($this->driver->getCurrentURL());
if (! array_key_exists('query', $parsedUrl)) {
PHPUnit::assertTrue(true);
return $this;
}
parse_str($parsedUrl['query'], $output);
PHPUnit::assertArrayNotHasKey(
$name, $output,
"Found unexpected query string parameter [{$name}] in [".$this->driver->getCurrentURL()."]."
);
return $this;
}
/**
* Assert that the given query string parameter is present.
*
* @param string $name
* @return array
*/
protected function assertHasQueryStringParameter($name)
{
$parsedUrl = parse_url($this->driver->getCurrentURL());
PHPUnit::assertArrayHasKey(
'query', $parsedUrl,
"Did not see expected query string in [".$this->driver->getCurrentURL()."]."
);
parse_str($parsedUrl['query'], $output);
PHPUnit::assertArrayHasKey(
$name, $output,
"Did not see expected query string parameter [{$name}] in [".$this->driver->getCurrentURL()."]."
);
return $output;
}
/**
* Assert that the given cookie is present.
*
@@ -432,8 +205,8 @@ trait MakesAssertions
*/
public function assertSourceHas($code)
{
PHPUnit::assertContains(
$code, $this->driver->getPageSource(),
PHPUnit::assertTrue(
Str::contains($this->driver->getPageSource(), $code),
"Did not find expected source code [{$code}]"
);
@@ -448,8 +221,8 @@ trait MakesAssertions
*/
public function assertSourceMissing($code)
{
PHPUnit::assertNotContains(
$code, $this->driver->getPageSource(),
PHPUnit::assertFalse(
Str::contains($this->driver->getPageSource(), $code),
"Found unexpected source code [{$code}]"
);
@@ -682,8 +455,8 @@ JS;
/**
* Assert that the given array of values are available to be selected.
*
* @param string $field
* @param array $values
* @param string $field
* @param array $values
* @return $this
*/
public function assertSelectHasOptions($field, array $values)
@@ -696,7 +469,7 @@ JS;
PHPUnit::assertCount(
count($values), $options,
"Expected options [".implode(',', $values)."] for selection field [{$field}] to be available."
'Expected options ['.implode(',', $values)."] for selection field [{$field}] to be available."
);
return $this;
@@ -705,15 +478,15 @@ JS;
/**
* Assert that the given array of values are not available to be selected.
*
* @param string $field
* @param array $values
* @param string $field
* @param array $values
* @return $this
*/
public function assertSelectMissingOptions($field, array $values)
{
PHPUnit::assertCount(
0, $this->resolver->resolveSelectOptions($field, $values),
"Unexpected options [".implode(',', $values)."] for selection field [{$field}]."
'Unexpected options ['.implode(',', $values)."] for selection field [{$field}]."
);
return $this;
@@ -722,8 +495,8 @@ JS;
/**
* Assert that the given value is available to be selected on the given field.
*
* @param string $field
* @param string $value
* @param string $field
* @param string $value
* @return $this
*/
public function assertSelectHasOption($field, $value)
@@ -734,8 +507,8 @@ JS;
/**
* Assert that the given value is not available to be selected on the given field.
*
* @param string $field
* @param string $value
* @param string $field
* @param string $value
* @return $this
*/
public function assertSelectMissingOption($field, $value)
@@ -752,9 +525,11 @@ JS;
*/
public function selected($field, $value)
{
$element = $this->resolver->resolveForSelection($field);
$options = $this->resolver->resolveSelectOptions($field, (array) $value);
return (string) $element->getAttribute('value') === (string) $value;
return collect($options)->contains(function (RemoteWebElement $option) {
return $option->isSelected();
});
}
/**
@@ -773,6 +548,59 @@ JS;
return $this;
}
/**
* Assert that the element at the given selector has the given attribute value.
*
* @param string $selector
* @param string $attribute
* @param string $value
* @return $this
*/
public function assertAttribute($selector, $attribute, $value)
{
$fullSelector = $this->resolver->format($selector);
$actual = $this->resolver->findOrFail($selector)->getAttribute($attribute);
PHPUnit::assertNotNull(
$actual,
"Did not see expected attribute [{$attribute}] within element [{$fullSelector}]."
);
PHPUnit::assertEquals(
$value, $actual,
"Expected '$attribute' attribute [{$value}] does not equal actual value [$actual]."
);
return $this;
}
/**
* Assert that the element at the given selector has the given data attribute value.
*
* @param string $selector
* @param string $attribute
* @param string $value
* @return $this
*/
public function assertDataAttribute($selector, $attribute, $value)
{
return $this->assertAttribute($selector, 'data-'.$attribute, $value);
}
/**
* Assert that the element at the given selector has the given aria attribute value.
*
* @param string $selector
* @param string $attribute
* @param string $value
* @return $this
*/
public function assertAriaAttribute($selector, $attribute, $value)
{
return $this->assertAttribute($selector, 'aria-'.$attribute, $value);
}
/**
* Assert that the element with the given selector is visible.
*
@@ -854,7 +682,8 @@ JS;
* @param string $field
* @return $this
*/
public function assertEnabled($field) {
public function assertEnabled($field)
{
$element = $this->resolver->resolveForField($field);
PHPUnit::assertTrue(
@@ -871,7 +700,8 @@ JS;
* @param string $field
* @return $this
*/
public function assertDisabled($field) {
public function assertDisabled($field)
{
$element = $this->resolver->resolveForField($field);
PHPUnit::assertFalse(
@@ -882,13 +712,50 @@ JS;
return $this;
}
/**
* Assert that the given button is enabled.
*
* @param string $button
* @return $this
*/
public function assertButtonEnabled($button)
{
$element = $this->resolver->resolveForButtonPress($button);
PHPUnit::assertTrue(
$element->isEnabled(),
"Expected button [{$button}] to be enabled, but it wasn't."
);
return $this;
}
/**
* Assert that the given button is disabled.
*
* @param string $button
* @return $this
*/
public function assertButtonDisabled($button)
{
$element = $this->resolver->resolveForButtonPress($button);
PHPUnit::assertFalse(
$element->isEnabled(),
"Expected button [{$button}] to be disabled, but it wasn't."
);
return $this;
}
/**
* Assert that the given field is focused.
*
* @param string $field
* @return $this
*/
public function assertFocused($field) {
public function assertFocused($field)
{
$element = $this->resolver->resolveForField($field);
PHPUnit::assertTrue(
@@ -905,7 +772,8 @@ JS;
* @param string $field
* @return $this
*/
public function assertNotFocused($field) {
public function assertNotFocused($field)
{
$element = $this->resolver->resolveForField($field);
PHPUnit::assertFalse(
@@ -958,7 +826,10 @@ JS;
*/
public function assertVueContains($key, $value, $componentSelector = null)
{
PHPUnit::assertContains($value, $this->vueAttribute($componentSelector, $key));
$attribute = $this->vueAttribute($componentSelector, $key);
PHPUnit::assertIsArray($attribute, "The attribute for key [$key] is not an array.");
PHPUnit::assertContains($value, $attribute);
return $this;
}
@@ -974,7 +845,10 @@ JS;
*/
public function assertVueDoesNotContain($key, $value, $componentSelector = null)
{
PHPUnit::assertNotContains($value, $this->vueAttribute($componentSelector, $key));
$attribute = $this->vueAttribute($componentSelector, $key);
PHPUnit::assertIsArray($attribute, "The attribute for key [$key] is not an array.");
PHPUnit::assertNotContains($value, $attribute);
return $this;
}
@@ -991,7 +865,7 @@ JS;
$fullSelector = $this->resolver->format($componentSelector);
return $this->driver->executeScript(
"return document.querySelector('" . $fullSelector . "').__vue__." . $key
"return document.querySelector('".$fullSelector."').__vue__.".$key
);
}
}

View File

@@ -0,0 +1,353 @@
<?php
namespace Laravel\Dusk\Concerns;
use Illuminate\Support\Arr;
use PHPUnit\Framework\Assert as PHPUnit;
trait MakesUrlAssertions
{
/**
* Assert that the current URL matches the given URL.
*
* @param string $url
* @return $this
*/
public function assertUrlIs($url)
{
$pattern = str_replace('\*', '.*', preg_quote($url, '/'));
$segments = parse_url($this->driver->getCurrentURL());
$currentUrl = sprintf(
'%s://%s%s%s',
$segments['scheme'],
$segments['host'],
Arr::get($segments, 'port', '') ? ':'.$segments['port'] : '',
Arr::get($segments, 'path', '')
);
PHPUnit::assertRegExp(
'/^'.$pattern.'$/u', $currentUrl,
"Actual URL [{$this->driver->getCurrentURL()}] does not equal expected URL [{$url}]."
);
return $this;
}
/**
* Assert that the current scheme matches the given scheme.
*
* @param string $scheme
* @return $this
*/
public function assertSchemeIs($scheme)
{
$pattern = str_replace('\*', '.*', preg_quote($scheme, '/'));
$actual = parse_url($this->driver->getCurrentURL(), PHP_URL_SCHEME) ?? '';
PHPUnit::assertRegExp(
'/^'.$pattern.'$/u', $actual,
"Actual scheme [{$actual}] does not equal expected scheme [{$pattern}]."
);
return $this;
}
/**
* Assert that the current scheme does not match the given scheme.
*
* @param string $scheme
* @return $this
*/
public function assertSchemeIsNot($scheme)
{
$actual = parse_url($this->driver->getCurrentURL(), PHP_URL_SCHEME) ?? '';
PHPUnit::assertNotEquals(
$scheme, $actual,
"Scheme [{$scheme}] should not equal the actual value."
);
return $this;
}
/**
* Assert that the current host matches the given host.
*
* @param string $host
* @return $this
*/
public function assertHostIs($host)
{
$pattern = str_replace('\*', '.*', preg_quote($host, '/'));
$actual = parse_url($this->driver->getCurrentURL(), PHP_URL_HOST) ?? '';
PHPUnit::assertRegExp(
'/^'.$pattern.'$/u', $actual,
"Actual host [{$actual}] does not equal expected host [{$pattern}]."
);
return $this;
}
/**
* Assert that the current host does not match the given host.
*
* @param string $host
* @return $this
*/
public function assertHostIsNot($host)
{
$actual = parse_url($this->driver->getCurrentURL(), PHP_URL_HOST) ?? '';
PHPUnit::assertNotEquals(
$host, $actual,
"Host [{$host}] should not equal the actual value."
);
return $this;
}
/**
* Assert that the current port matches the given port.
*
* @param string $port
* @return $this
*/
public function assertPortIs($port)
{
$pattern = str_replace('\*', '.*', preg_quote($port, '/'));
$actual = parse_url($this->driver->getCurrentURL(), PHP_URL_PORT) ?? '';
PHPUnit::assertRegExp(
'/^'.$pattern.'$/u', $actual,
"Actual port [{$actual}] does not equal expected port [{$pattern}]."
);
return $this;
}
/**
* Assert that the current host does not match the given host.
*
* @param string $port
* @return $this
*/
public function assertPortIsNot($port)
{
$actual = parse_url($this->driver->getCurrentURL(), PHP_URL_PORT) ?? '';
PHPUnit::assertNotEquals(
$port, $actual,
"Port [{$port}] should not equal the actual value."
);
return $this;
}
/**
* Assert that the current URL path matches the given pattern.
*
* @param string $path
* @return $this
*/
public function assertPathIs($path)
{
$pattern = str_replace('\*', '.*', preg_quote($path, '/'));
$actualPath = parse_url($this->driver->getCurrentURL(), PHP_URL_PATH) ?? '';
PHPUnit::assertRegExp(
'/^'.$pattern.'$/u', $actualPath,
"Actual path [{$actualPath}] does not equal expected path [{$path}]."
);
return $this;
}
/**
* Assert that the current URL path begins with given path.
*
* @param string $path
* @return $this
*/
public function assertPathBeginsWith($path)
{
$actualPath = parse_url($this->driver->getCurrentURL(), PHP_URL_PATH) ?? '';
PHPUnit::assertStringStartsWith(
$path, $actualPath,
"Actual path [{$actualPath}] does not begin with expected path [{$path}]."
);
return $this;
}
/**
* Assert that the current URL path does not match the given path.
*
* @param string $path
* @return $this
*/
public function assertPathIsNot($path)
{
$actualPath = parse_url($this->driver->getCurrentURL(), PHP_URL_PATH) ?? '';
PHPUnit::assertNotEquals(
$path, $actualPath,
"Path [{$path}] should not equal the actual value."
);
return $this;
}
/**
* Assert that the current URL fragment matches the given pattern.
*
* @param string $fragment
* @return $this
*/
public function assertFragmentIs($fragment)
{
$pattern = preg_quote($fragment, '/');
$actualFragment = (string) parse_url($this->driver->executeScript('return window.location.href;'), PHP_URL_FRAGMENT);
PHPUnit::assertRegExp(
'/^'.str_replace('\*', '.*', $pattern).'$/u', $actualFragment,
"Actual fragment [{$actualFragment}] does not equal expected fragment [{$fragment}]."
);
return $this;
}
/**
* Assert that the current URL fragment begins with given fragment.
*
* @param string $fragment
* @return $this
*/
public function assertFragmentBeginsWith($fragment)
{
$actualFragment = (string) parse_url($this->driver->executeScript('return window.location.href;'), PHP_URL_FRAGMENT);
PHPUnit::assertStringStartsWith(
$fragment, $actualFragment,
"Actual fragment [$actualFragment] does not begin with expected fragment [$fragment]."
);
return $this;
}
/**
* Assert that the current URL fragment does not match the given fragment.
*
* @param string $fragment
* @return $this
*/
public function assertFragmentIsNot($fragment)
{
$actualFragment = (string) parse_url($this->driver->executeScript('return window.location.href;'), PHP_URL_FRAGMENT);
PHPUnit::assertNotEquals(
$fragment, $actualFragment,
"Fragment [{$fragment}] should not equal the actual value."
);
return $this;
}
/**
* Assert that the current URL path matches the given route.
*
* @param string $route
* @param array $parameters
* @return $this
*/
public function assertRouteIs($route, $parameters = [])
{
return $this->assertPathIs(route($route, $parameters, false));
}
/**
* Assert that a query string parameter is present and has a given value.
*
* @param string $name
* @param string $value
* @return $this
*/
public function assertQueryStringHas($name, $value = null)
{
$output = $this->assertHasQueryStringParameter($name);
if (is_null($value)) {
return $this;
}
$parsedOutputName = is_array($output[$name]) ? implode(',', $output[$name]) : $output[$name];
$parsedValue = is_array($value) ? implode(',', $value) : $value;
PHPUnit::assertEquals(
$value, $output[$name],
"Query string parameter [{$name}] had value [{$parsedOutputName}], but expected [{$parsedValue}]."
);
return $this;
}
/**
* Assert that the given query string parameter is missing.
*
* @param string $name
* @return $this
*/
public function assertQueryStringMissing($name)
{
$parsedUrl = parse_url($this->driver->getCurrentURL());
if (! array_key_exists('query', $parsedUrl)) {
PHPUnit::assertTrue(true);
return $this;
}
parse_str($parsedUrl['query'], $output);
PHPUnit::assertArrayNotHasKey(
$name, $output,
"Found unexpected query string parameter [{$name}] in [".$this->driver->getCurrentURL().'].'
);
return $this;
}
/**
* Assert that the given query string parameter is present.
*
* @param string $name
* @return array
*/
protected function assertHasQueryStringParameter($name)
{
$parsedUrl = parse_url($this->driver->getCurrentURL());
PHPUnit::assertArrayHasKey(
'query', $parsedUrl,
'Did not see expected query string in ['.$this->driver->getCurrentURL().'].'
);
parse_str($parsedUrl['query'], $output);
PHPUnit::assertArrayHasKey(
$name, $output,
"Did not see expected query string parameter [{$name}] in [".$this->driver->getCurrentURL().'].'
);
return $output;
}
}

View File

@@ -4,10 +4,10 @@ namespace Laravel\Dusk\Concerns;
use Closure;
use Exception;
use Throwable;
use ReflectionFunction;
use Laravel\Dusk\Browser;
use Illuminate\Support\Collection;
use Laravel\Dusk\Browser;
use ReflectionFunction;
use Throwable;
trait ProvidesBrowser
{
@@ -56,6 +56,7 @@ trait ProvidesBrowser
*
* @param \Closure $callback
* @return \Laravel\Dusk\Browser|void
*
* @throws \Exception
* @throws \Throwable
*/
@@ -85,6 +86,7 @@ trait ProvidesBrowser
*
* @param \Closure $callback
* @return array
*
* @throws \ReflectionException
*/
protected function createBrowsersFor(Closure $callback)
@@ -118,6 +120,7 @@ trait ProvidesBrowser
*
* @param \Closure $callback
* @return int
*
* @throws \ReflectionException
*/
protected function browsersNeededFor(Closure $callback)
@@ -134,7 +137,11 @@ trait ProvidesBrowser
protected function captureFailuresFor($browsers)
{
$browsers->each(function ($browser, $key) {
$name = str_replace('\\', '_', get_class($this)).'_'.$this->getName(false);
if (property_exists($browser, 'fitOnFailure') && $browser->fitOnFailure) {
$browser->fitContent();
}
$name = $this->getCallerName();
$browser->screenshot('failure-'.$name.'-'.$key);
});
@@ -149,7 +156,7 @@ trait ProvidesBrowser
protected function storeConsoleLogsFor($browsers)
{
$browsers->each(function ($browser, $key) {
$name = str_replace('\\', '_', get_class($this)).'_'.$this->getName(false);
$name = $this->getCallerName();
$browser->storeConsoleLog($name.'-'.$key);
});
@@ -184,6 +191,7 @@ trait ProvidesBrowser
* Create the remote web driver instance.
*
* @return \Facebook\WebDriver\Remote\RemoteWebDriver
*
* @throws \Exception
*/
protected function createWebDriver()
@@ -193,6 +201,16 @@ trait ProvidesBrowser
}, 50);
}
/**
* Get the browser caller name.
*
* @return string
*/
protected function getCallerName()
{
return str_replace('\\', '_', get_class($this)).'_'.$this->getName(false);
}
/**
* Create the RemoteWebDriver instance.
*

View File

@@ -2,13 +2,14 @@
namespace Laravel\Dusk\Concerns;
use Carbon\Carbon;
use Closure;
use Exception;
use Carbon\Carbon;
use Illuminate\Support\Str;
use Facebook\WebDriver\Exception\TimeOutException;
use Facebook\WebDriver\Exception\NoSuchElementException;
use Facebook\WebDriver\Exception\TimeOutException;
use Facebook\WebDriver\WebDriverExpectedCondition;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
trait WaitsForElements
{
@@ -16,9 +17,10 @@ trait WaitsForElements
* Execute the given callback in a scoped browser once the selector is available.
*
* @param string $selector
* @param Closure $callback
* @param \Closure $callback
* @param int $seconds
* @return $this
*
* @throws \Facebook\WebDriver\Exception\TimeOutException
*/
public function whenAvailable($selector, Closure $callback, $seconds = null)
@@ -32,13 +34,16 @@ trait WaitsForElements
* @param string $selector
* @param int $seconds
* @return $this
*
* @throws \Facebook\WebDriver\Exception\TimeOutException
*/
public function waitFor($selector, $seconds = null)
{
$message = $this->formatTimeOutMessage('Waited %s seconds for selector', $selector);
return $this->waitUsing($seconds, 100, function () use ($selector) {
return $this->resolver->findOrFail($selector)->isDisplayed();
}, "Waited %s seconds for selector [{$selector}].");
}, $message);
}
/**
@@ -47,10 +52,13 @@ trait WaitsForElements
* @param string $selector
* @param int $seconds
* @return $this
*
* @throws \Facebook\WebDriver\Exception\TimeOutException
*/
public function waitUntilMissing($selector, $seconds = null)
{
$message = $this->formatTimeOutMessage('Waited %s seconds for removal of selector', $selector);
return $this->waitUsing($seconds, 100, function () use ($selector) {
try {
$missing = ! $this->resolver->findOrFail($selector)->isDisplayed();
@@ -59,22 +67,47 @@ trait WaitsForElements
}
return $missing;
}, "Waited %s seconds for removal of selector [{$selector}].");
}, $message);
}
/**
* Wait for the given text to be removed.
*
* @param string $text
* @param int $seconds
* @return $this
*
* @throws \Facebook\WebDriver\Exception\TimeOutException
*/
public function waitUntilMissingText($text, $seconds = null)
{
$text = Arr::wrap($text);
$message = $this->formatTimeOutMessage('Waited %s seconds for removal of text', implode("', '", $text));
return $this->waitUsing($seconds, 100, function () use ($text) {
return ! Str::contains($this->resolver->findOrFail('')->getText(), $text);
}, $message);
}
/**
* Wait for the given text to be visible.
*
* @param string $text
* @param array|string $text
* @param int $seconds
* @return $this
*
* @throws \Facebook\WebDriver\Exception\TimeOutException
*/
public function waitForText($text, $seconds = null)
{
$text = Arr::wrap($text);
$message = $this->formatTimeOutMessage('Waited %s seconds for text', implode("', '", $text));
return $this->waitUsing($seconds, 100, function () use ($text) {
return Str::contains($this->resolver->findOrFail('')->getText(), $text);
}, "Waited %s seconds for text [{$text}].");
}, $message);
}
/**
@@ -83,13 +116,16 @@ trait WaitsForElements
* @param string $link
* @param int $seconds
* @return $this
*
* @throws \Facebook\WebDriver\Exception\TimeOutException
*/
public function waitForLink($link, $seconds = null)
{
$message = $this->formatTimeOutMessage('Waited %s seconds for link', $link);
return $this->waitUsing($seconds, 100, function () use ($link) {
return $this->seeLink($link);
}, "Waited %s seconds for link [{$link}].");
}, $message);
}
/**
@@ -98,11 +134,14 @@ trait WaitsForElements
* @param string $path
* @param int $seconds
* @return $this
*
* @throws \Facebook\WebDriver\Exception\TimeOutException
*/
public function waitForLocation($path, $seconds = null)
{
return $this->waitUntil("window.location.pathname == '{$path}'", $seconds, "Waited %s seconds for location [{$path}].");
$message = $this->formatTimeOutMessage('Waited %s seconds for location', $path);
return $this->waitUntil("window.location.pathname == '{$path}'", $seconds, $message);
}
/**
@@ -112,6 +151,7 @@ trait WaitsForElements
* @param array $parameters
* @param int $seconds
* @return $this
*
* @throws \Facebook\WebDriver\Exception\TimeOutException
*/
public function waitForRoute($route, $parameters = [], $seconds = null)
@@ -126,6 +166,7 @@ trait WaitsForElements
* @param int $seconds
* @param string $message
* @return $this
*
* @throws \Facebook\WebDriver\Exception\TimeOutException
*/
public function waitUntil($script, $seconds = null, $message = null)
@@ -143,6 +184,40 @@ trait WaitsForElements
}, $message);
}
/**
* Wait until the Vue component's attribute at the given key has the given value.
*
* @param string $key
* @param string $value
* @param string|null $componentSelector
* @return $this
*/
public function waitUntilVue($key, $value, $componentSelector = null, $seconds = null)
{
$this->waitUsing($seconds, 100, function () use ($key, $value, $componentSelector) {
return $value == $this->vueAttribute($componentSelector, $key);
});
return $this;
}
/**
* Wait until the Vue component's attribute at the given key does not have the given value.
*
* @param string $key
* @param string $value
* @param string|null $componentSelector
* @return $this
*/
public function waitUntilVueIsNot($key, $value, $componentSelector = null, $seconds = null)
{
$this->waitUsing($seconds, 100, function () use ($key, $value, $componentSelector) {
return $value != $this->vueAttribute($componentSelector, $key);
});
return $this;
}
/**
* Wait for a JavaScript dialog to open.
*
@@ -163,14 +238,15 @@ trait WaitsForElements
/**
* Wait for the current page to reload.
*
* @param Closure $callback
* @param \Closure $callback
* @param int $seconds
* @return $this
*
* @throws \Facebook\WebDriver\Exception\TimeOutException
*/
public function waitForReload($callback = null, $seconds = null)
{
$token = str_random();
$token = Str::random();
$this->driver->executeScript("window['{$token}'] = {};");
@@ -188,9 +264,10 @@ trait WaitsForElements
*
* @param int $seconds
* @param int $interval
* @param Closure $callback
* @param \Closure $callback
* @param string|null $message
* @return $this
*
* @throws \Facebook\WebDriver\Exception\TimeOutException
*/
public function waitUsing($seconds, $interval, Closure $callback, $message = null)
@@ -222,4 +299,16 @@ trait WaitsForElements
return $this;
}
/**
* Prepare custom TimeOutException message for sprintf().
*
* @param string $message
* @param string $expected
* @return string
*/
protected function formatTimeOutMessage($message, $expected)
{
return $message.' ['.str_replace('%', '%%', $expected).'].';
}
}

View File

@@ -0,0 +1,251 @@
<?php
namespace Laravel\Dusk\Console;
use Illuminate\Console\Command;
use Laravel\Dusk\OperatingSystem;
use ZipArchive;
/**
* @copyright Originally created by Jonas Staudenmeir: https://github.com/staudenmeir/dusk-updater
*/
class ChromeDriverCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'dusk:chrome-driver {version?}
{--all : Install a ChromeDriver binary for every OS}
{--proxy= : The proxy to download the binary through (example: "tcp://127.0.0.1:9000")}
{--ssl-no-verify : Bypass SSL certificate verification when installing through a proxy}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Install the ChromeDriver binary';
/**
* URL to the latest stable release version.
*
* @var string
*/
protected $latestVersionUrl = 'https://chromedriver.storage.googleapis.com/LATEST_RELEASE';
/**
* URL to the latest release version for a major Chrome version.
*
* @var string
*/
protected $versionUrl = 'https://chromedriver.storage.googleapis.com/LATEST_RELEASE_%d';
/**
* URL to the ChromeDriver download.
*
* @var string
*/
protected $downloadUrl = 'https://chromedriver.storage.googleapis.com/%s/chromedriver_%s.zip';
/**
* Download slugs for the available operating systems.
*
* @var array
*/
protected $slugs = [
'linux' => 'linux64',
'mac' => 'mac64',
'win' => 'win32',
];
/**
* The legacy versions for the ChromeDriver.
*
* @var array
*/
protected $legacyVersions = [
43 => '2.20',
44 => '2.20',
45 => '2.20',
46 => '2.21',
47 => '2.21',
48 => '2.21',
49 => '2.22',
50 => '2.22',
51 => '2.23',
52 => '2.24',
53 => '2.26',
54 => '2.27',
55 => '2.28',
56 => '2.29',
57 => '2.29',
58 => '2.31',
59 => '2.32',
60 => '2.33',
61 => '2.34',
62 => '2.35',
63 => '2.36',
64 => '2.37',
65 => '2.38',
66 => '2.40',
67 => '2.41',
68 => '2.42',
69 => '2.44',
];
/**
* Path to the bin directory.
*
* @var string
*/
protected $directory = __DIR__.'/../../bin/';
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
$version = $this->version();
$all = $this->option('all');
$currentOS = OperatingSystem::id();
foreach ($this->slugs as $os => $slug) {
if ($all || ($os === $currentOS)) {
$archive = $this->download($version, $slug);
$binary = $this->extract($archive);
$this->rename($binary, $os);
}
}
$message = 'ChromeDriver %s successfully installed for version %s.';
$this->info(sprintf($message, $all ? 'binaries' : 'binary', $version));
}
/**
* Get the desired ChromeDriver version.
*
* @return string
*/
protected function version()
{
$version = $this->argument('version');
if (! $version) {
return $this->latestVersion();
}
if (! ctype_digit($version)) {
return $version;
}
$version = (int) $version;
if ($version < 70) {
return $this->legacyVersions[$version];
}
return trim($this->getUrl(
sprintf($this->versionUrl, $version)
));
}
/**
* Get the latest stable ChromeDriver version.
*
* @return string
*/
protected function latestVersion()
{
return trim(file_get_contents($this->latestVersionUrl));
}
/**
* Download the ChromeDriver archive.
*
* @param string $version
* @param string $slug
* @return string
*/
protected function download($version, $slug)
{
$url = sprintf($this->downloadUrl, $version, $slug);
file_put_contents(
$archive = $this->directory.'chromedriver.zip',
$this->getUrl($url)
);
return $archive;
}
/**
* Extract the ChromeDriver binary from the archive and delete the archive.
*
* @param string $archive
* @return string
*/
protected function extract($archive)
{
$zip = new ZipArchive;
$zip->open($archive);
$zip->extractTo($this->directory);
$binary = $zip->getNameIndex(0);
$zip->close();
unlink($archive);
return $binary;
}
/**
* Rename the ChromeDriver binary and make it executable.
*
* @param string $binary
* @param string $os
* @return void
*/
protected function rename($binary, $os)
{
$newName = str_replace('chromedriver', 'chromedriver-'.$os, $binary);
rename($this->directory.$binary, $this->directory.$newName);
chmod($this->directory.$newName, 0755);
}
/**
* Get the contents of a URL using the 'proxy' and 'ssl-no-verify' command options.
*
* @param string $url
* @return string|bool
*/
protected function getUrl(string $url)
{
$contextOptions = [];
if ($this->option('proxy')) {
$contextOptions['http'] = ['proxy' => $this->option('proxy'), 'request_fulluri' => true];
}
if ($this->option('ssl-no-verify')) {
$contextOptions['ssl'] = ['verify_peer' => false];
}
$streamContext = stream_context_create($contextOptions);
return file_get_contents($url, false, $streamContext);
}
}

View File

@@ -3,6 +3,7 @@
namespace Laravel\Dusk\Console;
use Illuminate\Console\GeneratorCommand;
use Illuminate\Support\Str;
class ComponentCommand extends GeneratorCommand
{
@@ -45,7 +46,7 @@ class ComponentCommand extends GeneratorCommand
*/
protected function getPath($name)
{
$name = str_replace_first($this->rootNamespace(), '', $name);
$name = Str::replaceFirst($this->rootNamespace(), '', $name);
return $this->laravel->basePath().'/tests'.str_replace('\\', '/', $name).'.php';
}

View File

@@ -3,11 +3,12 @@
namespace Laravel\Dusk\Console;
use Dotenv\Dotenv;
use Illuminate\Support\Str;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessSignaledException;
use Symfony\Component\Process\Exception\RuntimeException;
use Symfony\Component\Process\Process;
class DuskCommand extends Command
{
@@ -28,7 +29,7 @@ class DuskCommand extends Command
/**
* Indicates if the project has its own PHPUnit configuration.
*
* @var boolean
* @var bool
*/
protected $hasPhpUnitConfiguration = false;
@@ -68,9 +69,15 @@ class DuskCommand extends Command
$this->output->writeln('Warning: '.$e->getMessage());
}
return $process->run(function ($type, $line) {
$this->output->write($line);
});
try {
return $process->run(function ($type, $line) {
$this->output->write($line);
});
} catch (ProcessSignaledException $e) {
if (extension_loaded('pcntl') && $e->getSignal() !== SIGINT) {
throw $e;
}
}
});
}
@@ -81,6 +88,10 @@ class DuskCommand extends Command
*/
protected function binary()
{
if ('phpdbg' === PHP_SAPI) {
return [PHP_BINARY, '-qrr', 'vendor/phpunit/phpunit/phpunit'];
}
return [PHP_BINARY, 'vendor/phpunit/phpunit/phpunit'];
}
@@ -96,18 +107,28 @@ class DuskCommand extends Command
return ! Str::startsWith($option, '--env=');
}));
return array_merge(['-c', base_path('phpunit.dusk.xml')], $options);
if (! file_exists($file = base_path('phpunit.dusk.xml'))) {
$file = base_path('phpunit.dusk.xml.dist');
}
return array_merge(['-c', $file], $options);
}
/**
* Purge the failure screenshots
* Purge the failure screenshots.
*
* @return void
*/
protected function purgeScreenshots()
{
$path = base_path('tests/Browser/screenshots');
if (! is_dir($path)) {
return;
}
$files = Finder::create()->files()
->in(base_path('tests/Browser/screenshots'))
->in($path)
->name('failure-*');
foreach ($files as $file) {
@@ -122,8 +143,14 @@ class DuskCommand extends Command
*/
protected function purgeConsoleLogs()
{
$path = base_path('tests/Browser/console');
if (! is_dir($path)) {
return;
}
$files = Finder::create()->files()
->in(base_path('tests/Browser/console'))
->in($path)
->name('*.log');
foreach ($files as $file) {
@@ -138,6 +165,22 @@ class DuskCommand extends Command
* @return mixed
*/
protected function withDuskEnvironment($callback)
{
$this->setupDuskEnvironment();
try {
return $callback();
} finally {
$this->teardownDuskEnviroment();
}
}
/**
* Setup the Dusk environment.
*
* @return void
*/
protected function setupDuskEnvironment()
{
if (file_exists(base_path($this->duskFile()))) {
if (file_get_contents(base_path('.env')) !== file_get_contents(base_path($this->duskFile()))) {
@@ -149,13 +192,7 @@ class DuskCommand extends Command
$this->writeConfiguration();
return tap($callback(), function () {
$this->removeConfiguration();
if (file_exists(base_path($this->duskFile())) && file_exists(base_path('.env.backup'))) {
$this->restoreEnvironment();
}
});
$this->setupSignalHandler();
}
/**
@@ -170,18 +207,6 @@ class DuskCommand extends Command
copy(base_path($this->duskFile()), base_path('.env'));
}
/**
* Restore the backed-up environment file.
*
* @return void
*/
protected function restoreEnvironment()
{
copy(base_path('.env.backup'), base_path('.env'));
unlink(base_path('.env.backup'));
}
/**
* Refresh the current environment variables.
*
@@ -189,7 +214,21 @@ class DuskCommand extends Command
*/
protected function refreshEnvironment()
{
(new Dotenv(base_path()))->overload();
// BC fix to support Dotenv ^2.2...
if (! method_exists(Dotenv::class, 'create')) {
(new Dotenv(base_path()))->overload();
return;
}
// BC fix to support Dotenv ^3.0...
if (! method_exists(Dotenv::class, 'createMutable')) {
Dotenv::create(base_path())->overload();
return;
}
Dotenv::createMutable(base_path())->load();
}
/**
@@ -199,10 +238,43 @@ class DuskCommand extends Command
*/
protected function writeConfiguration()
{
if (! file_exists($file = base_path('phpunit.dusk.xml'))) {
if (! file_exists($file = base_path('phpunit.dusk.xml')) &&
! file_exists(base_path('phpunit.dusk.xml.dist'))) {
copy(realpath(__DIR__.'/../../stubs/phpunit.xml'), $file);
} else {
$this->hasPhpUnitConfiguration = true;
return;
}
$this->hasPhpUnitConfiguration = true;
}
/**
* Setup the SIGINT signal handler for CTRL+C exits.
*
* @return void
*/
protected function setupSignalHandler()
{
if (extension_loaded('pcntl')) {
pcntl_async_signals(true);
pcntl_signal(SIGINT, function () {
$this->teardownDuskEnviroment();
});
}
}
/**
* Restore the original environment.
*
* @return void
*/
protected function teardownDuskEnviroment()
{
$this->removeConfiguration();
if (file_exists(base_path($this->duskFile())) && file_exists(base_path('.env.backup'))) {
$this->restoreEnvironment();
}
}
@@ -213,11 +285,23 @@ class DuskCommand extends Command
*/
protected function removeConfiguration()
{
if (! $this->hasPhpUnitConfiguration) {
unlink(base_path('phpunit.dusk.xml'));
if (! $this->hasPhpUnitConfiguration && file_exists($file = base_path('phpunit.dusk.xml'))) {
unlink($file);
}
}
/**
* Restore the backed-up environment file.
*
* @return void
*/
protected function restoreEnvironment()
{
copy(base_path('.env.backup'), base_path('.env'));
unlink(base_path('.env.backup'));
}
/**
* Get the name of the Dusk file for the environment.
*

View File

@@ -0,0 +1,33 @@
<?php
namespace Laravel\Dusk\Console;
class DuskFailsCommand extends DuskCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'dusk:fails {--without-tty : Disable output to TTY}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Run the failing Dusk tests from the last run and stop on failure';
/**
* Get the array of arguments for running PHPUnit.
*
* @param array $options
* @return array
*/
protected function phpunitArguments($options)
{
return array_unique(array_merge(parent::phpunitArguments($options), [
'--cache-result', '--order-by=defects', '--stop-on-failure',
]));
}
}

View File

@@ -11,7 +11,9 @@ class InstallCommand extends Command
*
* @var string
*/
protected $signature = 'dusk:install';
protected $signature = 'dusk:install
{--proxy= : The proxy to download the binary through (example: "tcp://127.0.0.1:9000")}
{--ssl-no-verify : Bypass SSL certificate verification when installing through a proxy}';
/**
* The console command description.
@@ -20,16 +22,6 @@ class InstallCommand extends Command
*/
protected $description = 'Install Dusk into the application';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
@@ -67,6 +59,20 @@ class InstallCommand extends Command
}
$this->info('Dusk scaffolding installed successfully.');
$this->comment('Downloading ChromeDriver binaries...');
$driverCommandArgs = ['--all' => true];
if ($this->option('proxy')) {
$driverCommandArgs['--proxy'] = $this->option('proxy');
}
if ($this->option('ssl-no-verify')) {
$driverCommandArgs['--ssl-no-verify'] = true;
}
$this->call('dusk:chrome-driver', $driverCommandArgs);
}
/**

View File

@@ -3,6 +3,7 @@
namespace Laravel\Dusk\Console;
use Illuminate\Console\GeneratorCommand;
use Illuminate\Support\Str;
class MakeCommand extends GeneratorCommand
{
@@ -45,7 +46,7 @@ class MakeCommand extends GeneratorCommand
*/
protected function getPath($name)
{
$name = str_replace_first($this->rootNamespace(), '', $name);
$name = Str::replaceFirst($this->rootNamespace(), '', $name);
return $this->laravel->basePath().'/tests'.str_replace('\\', '/', $name).'.php';
}

View File

@@ -3,6 +3,7 @@
namespace Laravel\Dusk\Console;
use Illuminate\Console\GeneratorCommand;
use Illuminate\Support\Str;
class PageCommand extends GeneratorCommand
{
@@ -45,7 +46,7 @@ class PageCommand extends GeneratorCommand
*/
protected function getPath($name)
{
$name = str_replace_first($this->rootNamespace(), '', $name);
$name = Str::replaceFirst($this->rootNamespace(), '', $name);
return $this->laravel->basePath().'/tests'.str_replace('\\', '/', $name).'.php';
}

View File

@@ -24,6 +24,8 @@ class Dusk
*
* @param array $options
* @return bool
*
* @throws \InvalidArgumentException
*/
protected static function duskEnvironment($options)
{
@@ -36,7 +38,7 @@ class Dusk
}
if (! is_array($options['environments'])) {
throw new InvalidArgumentException("Dusk environments must be listed as an array.");
throw new InvalidArgumentException('Dusk environments must be listed as an array.');
}
return app()->environment(...$options['environments']);

View File

@@ -2,7 +2,6 @@
namespace Laravel\Dusk;
use Exception;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
@@ -15,41 +14,42 @@ class DuskServiceProvider extends ServiceProvider
*/
public function boot()
{
Route::get('/_dusk/login/{userId}/{guard?}', [
'middleware' => 'web',
'uses' => 'Laravel\Dusk\Http\Controllers\UserController@login',
]);
if (! $this->app->environment('production')) {
Route::get('/_dusk/login/{userId}/{guard?}', [
'middleware' => 'web',
'uses' => 'Laravel\Dusk\Http\Controllers\UserController@login',
]);
Route::get('/_dusk/logout/{guard?}', [
'middleware' => 'web',
'uses' => 'Laravel\Dusk\Http\Controllers\UserController@logout',
]);
Route::get('/_dusk/logout/{guard?}', [
'middleware' => 'web',
'uses' => 'Laravel\Dusk\Http\Controllers\UserController@logout',
]);
Route::get('/_dusk/user/{guard?}', [
'middleware' => 'web',
'uses' => 'Laravel\Dusk\Http\Controllers\UserController@user',
]);
Route::get('/_dusk/user/{guard?}', [
'middleware' => 'web',
'uses' => 'Laravel\Dusk\Http\Controllers\UserController@user',
]);
}
}
/**
* Register any package services.
*
* @return void
* @throws Exception
*
* @throws \Exception
*/
public function register()
{
if ($this->app->environment('production')) {
throw new Exception('It is unsafe to run Dusk in production.');
}
if ($this->app->runningInConsole()) {
$this->commands([
Console\InstallCommand::class,
Console\DuskCommand::class,
Console\DuskFailsCommand::class,
Console\MakeCommand::class,
Console\PageCommand::class,
Console\ComponentCommand::class,
Console\ChromeDriverCommand::class,
]);
}
}

View File

@@ -3,10 +3,10 @@
namespace Laravel\Dusk;
use Exception;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Facebook\WebDriver\WebDriverBy;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Macroable;
use InvalidArgumentException;
class ElementResolver
{
@@ -43,7 +43,7 @@ class ElementResolver
'findButtonBySelector',
'findButtonByName',
'findButtonByValue',
'findButtonByText'
'findButtonByText',
];
/**
@@ -77,6 +77,7 @@ class ElementResolver
*
* @param string $field
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*
* @throws \Exception
*/
public function resolveForTyping($field)
@@ -86,7 +87,7 @@ class ElementResolver
}
return $this->firstOrFail([
$field, "input[name='{$field}']", "textarea[name='{$field}']"
$field, "input[name='{$field}']", "textarea[name='{$field}']",
]);
}
@@ -95,6 +96,7 @@ class ElementResolver
*
* @param string $field
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*
* @throws \Exception
*/
public function resolveForSelection($field)
@@ -104,16 +106,17 @@ class ElementResolver
}
return $this->firstOrFail([
$field, "select[name='{$field}']"
$field, "select[name='{$field}']",
]);
}
/**
* Resolve all the options with the given value on the select field.
*
* @param string $field
* @param array $values
* @param string $field
* @param array $values
* @return \Facebook\WebDriver\Remote\RemoteWebElement[]
*
* @throws \Exception
*/
public function resolveSelectOptions($field, array $values)
@@ -136,7 +139,9 @@ class ElementResolver
* @param string $field
* @param string $value
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*
* @throws \Exception
* @throws \InvalidArgumentException
*/
public function resolveForRadioSelection($field, $value = null)
{
@@ -151,16 +156,17 @@ class ElementResolver
}
return $this->firstOrFail([
$field, "input[type=radio][name='{$field}'][value='{$value}']"
$field, "input[type=radio][name='{$field}'][value='{$value}']",
]);
}
/**
* Resolve the element for a given checkbox "field".
*
* @param string $field
* @param string|null $field
* @param string $value
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*
* @throws \Exception
*/
public function resolveForChecking($field, $value = null)
@@ -169,14 +175,18 @@ class ElementResolver
return $element;
}
$selector = "input[type=checkbox][name='{$field}']";
$selector = 'input[type=checkbox]';
if (! is_null($field)) {
$selector .= "[name='{$field}']";
}
if (! is_null($value)) {
$selector .= "[value='{$value}']";
}
return $this->firstOrFail([
$field, $selector
$field, $selector,
]);
}
@@ -185,6 +195,7 @@ class ElementResolver
*
* @param string $field
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*
* @throws \Exception
*/
public function resolveForAttachment($field)
@@ -194,7 +205,7 @@ class ElementResolver
}
return $this->firstOrFail([
$field, "input[type=file][name='{$field}']"
$field, "input[type=file][name='{$field}']",
]);
}
@@ -203,6 +214,7 @@ class ElementResolver
*
* @param string $field
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*
* @throws \Exception
*/
public function resolveForField($field)
@@ -213,7 +225,7 @@ class ElementResolver
return $this->firstOrFail([
$field, "input[name='{$field}']", "textarea[name='{$field}']",
"select[name='{$field}']", "button[name='{$field}']"
"select[name='{$field}']", "button[name='{$field}']",
]);
}
@@ -222,6 +234,8 @@ class ElementResolver
*
* @param string $button
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*
* @throws \InvalidArgumentException
*/
public function resolveForButtonPress($button)
{
@@ -272,7 +286,7 @@ class ElementResolver
*/
protected function findButtonByValue($button)
{
foreach ($this->all("input[type=submit]") as $element) {
foreach ($this->all('input[type=submit]') as $element) {
if ($element->getAttribute('value') === $button) {
return $element;
}
@@ -327,6 +341,7 @@ class ElementResolver
*
* @param array $selectors
* @return \Facebook\WebDriver\Remote\RemoteWebElement
*
* @throws \Exception
*/
public function firstOrFail($selectors)
@@ -394,7 +409,7 @@ class ElementResolver
array_keys($sortedElements), array_values($sortedElements), $originalSelector = $selector
);
if (starts_with($selector, '@') && $selector === $originalSelector) {
if (Str::startsWith($selector, '@') && $selector === $originalSelector) {
$selector = '[dusk="'.explode('@', $selector)[1].'"]';
}

View File

@@ -3,6 +3,7 @@
namespace Laravel\Dusk\Http\Controllers;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
class UserController
{
@@ -31,6 +32,7 @@ class UserController
*
* @param string $userId
* @param string $guard
* @return void
*/
public function login($userId, $guard = null)
{
@@ -38,7 +40,7 @@ class UserController
$provider = Auth::guard($guard)->getProvider();
$user = str_contains($userId, '@')
$user = Str::contains($userId, '@')
? $provider->retrieveByCredentials(['email' => $userId])
: $provider->retrieveById($userId);
@@ -49,6 +51,7 @@ class UserController
* Log the user out of the application.
*
* @param string $guard
* @return void
*/
public function logout($guard = null)
{

View File

@@ -0,0 +1,38 @@
<?php
namespace Laravel\Dusk;
use Illuminate\Support\Str;
class OperatingSystem
{
/**
* Returns the current OS identifier.
*
* @return string
*/
public static function id()
{
return static::onWindows() ? 'win' : (static::onMac() ? 'mac' : 'linux');
}
/**
* Determine if the operating system is Windows or Windows Subsystem for Linux.
*
* @return bool
*/
public static function onWindows()
{
return PHP_OS === 'WINNT' || Str::contains(php_uname(), 'Microsoft');
}
/**
* Determine if the operating system is macOS.
*
* @return bool
*/
public static function onMac()
{
return PHP_OS === 'Darwin';
}
}

View File

@@ -3,22 +3,22 @@
namespace Laravel\Dusk;
use Exception;
use Laravel\Dusk\Chrome\SupportsChrome;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Illuminate\Foundation\Testing\TestCase as FoundationTestCase;
use Laravel\Dusk\Chrome\SupportsChrome;
use Laravel\Dusk\Concerns\ProvidesBrowser;
abstract class TestCase extends FoundationTestCase
{
use Concerns\ProvidesBrowser,
SupportsChrome;
use ProvidesBrowser, SupportsChrome;
/**
* Register the base URL with Dusk.
*
* @return void
*/
protected function setUp()
protected function setUp(): void
{
parent::setUp();
@@ -28,6 +28,8 @@ abstract class TestCase extends FoundationTestCase
Browser::$storeConsoleLogAt = base_path('tests/Browser/console');
Browser::$storeSourceAt = base_path('tests/Browser/source');
Browser::$userResolver = function () {
return $this->user();
};
@@ -59,10 +61,11 @@ abstract class TestCase extends FoundationTestCase
* Return the default user to authenticate.
*
* @return \App\User|int|null
*
* @throws \Exception
*/
protected function user()
{
throw new Exception("User resolver has not been set.");
throw new Exception('User resolver has not been set.');
}
}

View File

@@ -2,10 +2,10 @@
namespace Tests;
use Laravel\Dusk\TestCase as BaseTestCase;
use Facebook\WebDriver\Chrome\ChromeOptions;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Laravel\Dusk\TestCase as BaseTestCase;
abstract class DuskTestCase extends BaseTestCase
{
@@ -31,7 +31,8 @@ abstract class DuskTestCase extends BaseTestCase
{
$options = (new ChromeOptions)->addArguments([
'--disable-gpu',
'--headless'
'--headless',
'--window-size=1920,1080',
]);
return RemoteWebDriver::create(

View File

@@ -2,9 +2,9 @@
namespace Tests\Browser;
use Tests\DuskTestCase;
use Laravel\Dusk\Browser;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
class ExampleTest extends DuskTestCase
{

View File

@@ -19,7 +19,7 @@ class HomePage extends Page
/**
* Assert that the browser is on the page.
*
* @param Browser $browser
* @param \Laravel\Dusk\Browser $browser
* @return void
*/
public function assert(Browser $browser)

View File

@@ -20,7 +20,7 @@ Laravel is a web application framework with expressive, elegant syntax. We belie
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
Laravel is accessible, yet powerful, providing tools needed for large, robust applications. A superb combination of simplicity, elegance, and innovation gives you a complete toolset required to build any application with which you are tasked
Laravel is accessible, yet powerful, providing tools needed for large, robust applications. A superb combination of simplicity, elegance, and innovation gives you a complete toolset required to build any application with which you are tasked.
## Learning Laravel
@@ -38,7 +38,7 @@ In order to ensure that the Laravel community is welcoming to all, please review
## Security Vulnerabilities
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
Please review [our security policy](https://github.com/laravel/framework/security/policy) on how to report security vulnerabilities.
## License

102
vendor/laravel/framework/SECURITY.md vendored Normal file
View File

@@ -0,0 +1,102 @@
# Security Policy
**PLEASE DON'T DISCLOSE SECURITY-RELATED ISSUES PUBLICLY, [SEE BELOW](#reporting-a-vulnerability).**
## Supported Versions
Version | Security Fixes Until
--- | ---
5.8 | February 26th, 2020
5.7 | September 4th, 2019
5.6 | February 7th, 2019
5.5 (LTS) | August 30th, 2020
5.4 | January 24th, 2018
5.3 | August 23rd, 2017
5.2 | December 21st, 2016
5.1 (LTS) | June 9th, 2018
5.0 | February 4th, 2016
## Reporting a Vulnerability
If you discover a security vulnerability within Laravel, please send an email to Taylor Otwell at taylor@laravel.com. All security vulnerabilities will be promptly addressed.
### Public PGP Key
```
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: OpenPGP v2.0.8
Comment: https://sela.io/pgp/
xsFNBFugFSQBEACxEKhIY9IoJzcouVTIYKJfWFGvwFgbRjQWBiH3QdHId5vCrbWo
s2l+4Rv03gMG+yHLJ3rWElnNdRaNdQv59+lShrZF7Bvu7Zvc0mMNmFOM/mQ/K2Lt
OK/8bh6iwNNbEuyOhNQlarEy/w8hF8Yf55hBeu/rajGtcyURJDloQ/vNzcx4RWGK
G3CLr8ka7zPYIjIFUvHLt27mcYFF9F4/G7b4HKpn75ICKC4vPoQSaYNAHlHQBLFb
Jg/WPl93SySHLugU5F58sICs+fBZadXYQG5dWmbaF5OWB1K2XgRs45BQaBzf/8oS
qq0scN8wVhAdBeYlVFf0ImDOxGlZ2suLK1BKJboR6zCIkBAwufKss4NV1R9KSUMv
YGn3mq13PGme0QoIkvQkua5VjTwWfQx7wFDxZ3VQSsjIlbVyRL/Ac/hq71eAmiIR
t6ZMNMPFpuSwBfYimrXqqb4EOffrfsTzRenG1Cxm4jNZRzX/6P4an7F/euoqXeXZ
h37TiC7df+eHKcBI4mL+qOW4ibBqg8WwWPJ+jvuhANyQpRVzf3NNEOwJYCNbQPM/
PbqYvMruAH+gg7uyu9u0jX3o/yLSxJMV7kF4x/SCDuBKIxSSUI4cgbjIlnxWLXZC
wl7KW4xAKkerO3wgIPnxNfxQoiYiEKA1c3PShWRA0wHIMt3rVRJxwGM4CwARAQAB
zRJ0YXlsb3JAbGFyYXZlbC5jb23CwXAEEwEKABoFAlugFSQCGy8DCwkHAxUKCAIe
AQIXgAIZAQAKCRDKAI7r/Ml7Zo0SD/9zwu9K87rbqXbvZ3TVu7TnN+z7mPvVBzl+
SFEK360TYq8a4GosghZuGm4aNEyZ90CeUjPQwc5fHwa26tIwqgLRppsG21B/mZwu
0m8c5RaBFRFX/mCTEjlpvBkOwMJZ8f05nNdaktq6W98DbMN03neUwnpWlNSLeoNI
u4KYZmJopNFLEax5WGaaDpmqD1J+WDr/aPHx39MUAg2ZVuC3Gj/IjYZbD1nCh0xD
a09uDODje8a9uG33cKRBcKKPRLZjWEt5SWReLx0vsTuqJSWhCybHRBl9BQTc/JJR
gJu5V4X3f1IYMTNRm9GggxcXrlOAiDCjE2J8ZTUt0cSxedQFnNyGfKxe/l94oTFP
wwFHbdKhsSDZ1OyxPNIY5OHlMfMvvJaNbOw0xPPAEutPwr1aqX9sbgPeeiJwAdyw
mPw2x/wNQvKJITRv6atw56TtLxSevQIZGPHCYTSlsIoi9jqh9/6vfq2ruMDYItCq
+8uzei6TyH6w+fUpp/uFmcwZdrDwgNVqW+Ptu+pD2WmthqESF8UEQVoOv7OPgA5E
ofOMaeH2ND74r2UgcXjPxZuUp1RkhHE2jJeiuLtbvOgrWwv3KOaatyEbVl+zHA1e
1RHdJRJRPK+S7YThxxduqfOBX7E03arbbhHdS1HKhPwMc2e0hNnQDoNxQcv0GQp4
2Y6UyCRaus7ATQRboBUkAQgA0h5j3EO2HNvp8YuT1t/VF00uUwbQaz2LIoZogqgC
14Eb77diuIPM9MnuG7bEOnNtPVMFXxI5UYBIlzhLMxf7pfbrsoR4lq7Ld+7KMzdm
eREqJRgUNfjZhtRZ9Z+jiFPr8AGpYxwmJk4v387uQGh1GC9JCc3CCLJoI62I9t/1
K2b25KiOzW/FVZ/vYFj1WbISRd5GqS8SEFh4ifU79LUlJ/nEsFv4JxAXN9RqjU0e
H4S/m1Nb24UCtYAv1JKymcf5O0G7kOzvI0w06uKxk0hNwspjDcOebD8Vv9IdYtGl
0bn7PpBlVO1Az3s8s6Xoyyw+9Us+VLNtVka3fcrdaV/n0wARAQABwsKEBBgBCgAP
BQJboBUkBQkPCZwAAhsuASkJEMoAjuv8yXtmwF0gBBkBCgAGBQJboBUkAAoJEA1I
8aTLtYHmjpIH/A1ZKwTGetHFokJxsd2omvbqv+VtpAjnUbvZEi5g3yZXn+dHJV+K
UR/DNlfGxLWEcY6datJ3ziNzzD5u8zcPp2CqeTlCxOky8F74FjEL9tN/EqUbvvmR
td2LXsSFjHnLJRK5lYfZ3rnjKA5AjqC9MttILBovY2rI7lyVt67kbS3hMHi8AZl8
EgihnHRJxGZjEUxyTxcB13nhfjAvxQq58LOj5754Rpe9ePSKbT8DNMjHbGpLrESz
cmyn0VzDMLfxg8AA9uQFMwdlKqve7yRZXzeqvy08AatUpJaL7DsS4LKOItwvBub6
tHbCE3mqrUw5lSNyUahO3vOcMAHnF7fd4W++eA//WIQKnPX5t3CwCedKn8Qkb3Ow
oj8xUNl2T6kEtQJnO85lKBFXaMOUykopu6uB9EEXEr0ShdunOKX/UdDbkv46F2AB
7TtltDSLB6s/QeHExSb8Jo3qra86JkDUutWdJxV7DYFUttBga8I0GqdPu4yRRoc/
0irVXsdDY9q7jz6l7fw8mSeJR96C0Puhk70t4M1Vg/tu/ONRarXQW7fJ8kl21PcD
UKNWWa242gji/+GLRI8AIpGMsBiX7pHhqmMMth3u7+ne5BZGGJz0uX+CzWboOHyq
kWgfY4a62t3hM0vwnUkl/D7VgSGy4LiKQrapd3LvU2uuEfFsMu3CDicZBRXPqoXj
PBjkkPKhwUTNlwEQrGF3QsZhNe0M9ptM2fC34qtxZtMIMB2NLvE4S621rmQ05oQv
sT0B9WgUL3GYRKdx700+ojHEuwZ79bcLgo1dezvkfPtu/++2CXtieFthDlWHy8x5
XJJjI1pDfGO+BgX0rS3QrQEYlF/uPQynKwxe6cGI62eZ0ug0hNrPvKEcfMLVqBQv
w4VH6iGp9yNKMUOgAECLCs4YCxK+Eka9Prq/Gh4wuqjWiX8m66z8YvKf27sFL3fR
OwGaz3LsnRSxbk/8oSiZuOVLfn44XRcxsHebteZat23lwD93oq54rtKnlJgmZHJY
4vMgk1jpS4laGnvhZj7OwE0EW6AVJAEIAKJSrUvXRyK3XQnLp3Kfj82uj0St8Dt2
h8BMeVbrAbg38wCN8XQZzVR9+bRZRR+aCzpKSqwhEQVtH7gdKgfdNdGNhG2DFAVk
SihMhQz190FKttUZgwY00enzD7uaaA5VwNAZzRIr8skwiASB7UoO+lIhrAYgcQCA
LpwCSMrUNB3gY1IVa2xi9FljEbS2uMABfOsTfl7z4L4T4DRv/ovDf+ihyZOXsXiH
RVoUTIpN8ZILCZiiKubE1sMj4fSQwCs06UyDy17HbOG5/dO9awR/LHW53O3nZCxE
JbCqr5iHa2MdHMC12+luxWJKD9DbVB01LiiPZCTkuKUDswCyi7otpVEAEQEAAcLC
hAQYAQoADwUCW6AVJAUJDwmcAAIbLgEpCRDKAI7r/Ml7ZsBdIAQZAQoABgUCW6AV
JAAKCRDxrCjKN7eORjt2B/9EnKVJ9lwB1JwXcQp6bZgJ21r6ghyXBssv24N9UF+v
5QDz/tuSkTsKK1UoBrBDEinF/xTP2z+xXIeyP4c3mthMHsYdMl7AaGpcCwVJiL62
fZvd+AiYNX3C+Bepwnwoziyhx4uPaqoezSEMD8G2WQftt6Gqttmm0Di5RVysCECF
EyhkHwvCcbpXb5Qq+4XFzCUyaIZuGpe+oeO7U8B1CzOC16hEUu0Uhbk09Xt6dSbS
ZERoxFjrGU+6bk424MkZkKvNS8FdTN2s3kQuHoNmhbMY+fRzKX5JNrcQ4dQQufiB
zFcc2Ba0JVU0nYAMftTeT5ALakhwSqr3AcdD2avJZp3EYfYP/3smPGTeg1cDJV3E
WIlCtSlhbwviUjvWEWJUE+n9MjhoUNU0TJtHIliUYUajKMG/At5wJZTXJaKVUx32
UCWr4ioKfSzlbp1ngBuFlvU7LgZRcKbBZWvKj/KRYpxpfvPyPElmegCjAk6oiZYV
LOV+jFfnMkk9PnR91ZZfTNx/bK+BwjOnO+g7oE8V2g2bA90vHdeSUHR52SnaVN/b
9ytt07R0f+YtyKojuPmlNsbyAaUYUtJ1o+XNCwdVxzarYEuUabhAfDiVTu9n8wTr
YVvnriSFOjNvOY9wdLAa56n7/qM8bzuGpoBS5SilXgJvITvQfWPvg7I9C3QhwK1S
F6B1uquQGbBSze2wlnMbKXmhyGLlv9XpOqpkkejQo3o58B+Sqj4B8DuYixSjoknr
pRbj8gqgqBKlcpf1wD5X9qCrl9vq19asVOHaKhiFZGxZIVbBpBOdvAKaMj4p/uln
yklN3YFIfgmGPYbL0elvXVn7XfvwSV1mCQV5LtMbLHsFf0VsA16UsG8A/tLWtwgt
0antzftRHXb+DI4qr+qEYKFkv9F3oCOXyH4QBhPA42EzKqhMXByEkEK9bu6skioL
mHhDQ7yHjTWcxstqQjkUQ0T/IF9ls+Sm5u7rVXEifpyI7MCb+76kSCDawesvInKt
WBGOG/qJGDlNiqBYYt2xNqzHCJoC
=zXOv
-----END PGP PUBLIC KEY BLOCK-----
```

View File

@@ -16,28 +16,31 @@
],
"require": {
"php": "^7.1.3",
"ext-json": "*",
"ext-mbstring": "*",
"ext-openssl": "*",
"doctrine/inflector": "~1.1",
"dragonmantank/cron-expression": "~2.0",
"erusev/parsedown": "~1.7",
"doctrine/inflector": "^1.1",
"dragonmantank/cron-expression": "^2.0",
"egulias/email-validator": "^2.0",
"erusev/parsedown": "^1.7",
"league/flysystem": "^1.0.8",
"monolog/monolog": "~1.12",
"nesbot/carbon": "1.25.*",
"psr/container": "~1.0",
"monolog/monolog": "^1.12",
"nesbot/carbon": "^1.26.3 || ^2.0",
"opis/closure": "^3.1",
"psr/container": "^1.0",
"psr/simple-cache": "^1.0",
"ramsey/uuid": "^3.7",
"swiftmailer/swiftmailer": "~6.0",
"symfony/console": "~4.0",
"symfony/debug": "~4.0",
"symfony/finder": "~4.0",
"symfony/http-foundation": "~4.0",
"symfony/http-kernel": "~4.0",
"symfony/process": "~4.0",
"symfony/routing": "~4.0",
"symfony/var-dumper": "~4.0",
"swiftmailer/swiftmailer": "^6.0",
"symfony/console": "^4.2",
"symfony/debug": "^4.2",
"symfony/finder": "^4.2",
"symfony/http-foundation": "^4.2",
"symfony/http-kernel": "^4.2",
"symfony/process": "^4.2",
"symfony/routing": "^4.2",
"symfony/var-dumper": "^4.2",
"tijsverkoyen/css-to-inline-styles": "^2.2.1",
"vlucas/phpdotenv": "~2.2"
"vlucas/phpdotenv": "^3.3"
},
"replace": {
"illuminate/auth": "self.version",
@@ -73,18 +76,20 @@
"tightenco/collect": "<5.5.33"
},
"require-dev": {
"aws/aws-sdk-php": "~3.0",
"doctrine/dbal": "~2.6",
"aws/aws-sdk-php": "^3.0",
"doctrine/dbal": "^2.6",
"filp/whoops": "^2.1.4",
"league/flysystem-cached-adapter": "~1.0",
"mockery/mockery": "~1.0",
"guzzlehttp/guzzle": "^6.3",
"league/flysystem-cached-adapter": "^1.0",
"mockery/mockery": "^1.0",
"moontoast/math": "^1.1",
"orchestra/testbench-core": "3.6.*",
"pda/pheanstalk": "~3.0",
"phpunit/phpunit": "~7.0",
"orchestra/testbench-core": "3.8.*",
"pda/pheanstalk": "^4.0",
"phpunit/phpunit": "^7.5|^8.0",
"predis/predis": "^1.1.1",
"symfony/css-selector": "~4.0",
"symfony/dom-crawler": "~4.0"
"symfony/css-selector": "^4.2",
"symfony/dom-crawler": "^4.2",
"true/punycode": "^2.1"
},
"autoload": {
"files": [
@@ -105,28 +110,32 @@
},
"extra": {
"branch-alias": {
"dev-master": "5.6-dev"
"dev-master": "5.8-dev"
}
},
"suggest": {
"ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().",
"ext-pcntl": "Required to use all features of the queue worker.",
"ext-posix": "Required to use all features of the queue worker.",
"aws/aws-sdk-php": "Required to use the SQS queue driver and SES mail driver (~3.0).",
"doctrine/dbal": "Required to rename columns and drop SQLite columns (~2.6).",
"fzaninotto/faker": "Required to use the eloquent factory builder (~1.4).",
"guzzlehttp/guzzle": "Required to use the Mailgun and Mandrill mail drivers and the ping methods on schedules (~6.0).",
"laravel/tinker": "Required to use the tinker console command (~1.0).",
"league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (~1.0).",
"league/flysystem-cached-adapter": "Required to use the Flysystem cache (~1.0).",
"league/flysystem-rackspace": "Required to use the Flysystem Rackspace driver (~1.0).",
"league/flysystem-sftp": "Required to use the Flysystem SFTP driver (~1.0).",
"nexmo/client": "Required to use the Nexmo transport (~1.0).",
"pda/pheanstalk": "Required to use the beanstalk queue driver (~3.0).",
"predis/predis": "Required to use the redis cache and queue drivers (~1.0).",
"pusher/pusher-php-server": "Required to use the Pusher broadcast driver (~3.0).",
"symfony/css-selector": "Required to use some of the crawler integration testing tools (~4.0).",
"symfony/dom-crawler": "Required to use most of the crawler integration testing tools (~4.0).",
"symfony/psr-http-message-bridge": "Required to psr7 bridging features (~1.0)."
"aws/aws-sdk-php": "Required to use the SQS queue driver and SES mail driver (^3.0).",
"doctrine/dbal": "Required to rename columns and drop SQLite columns (^2.6).",
"filp/whoops": "Required for friendly error pages in development (^2.1.4).",
"fzaninotto/faker": "Required to use the eloquent factory builder (^1.4).",
"guzzlehttp/guzzle": "Required to use the Mailgun and Mandrill mail drivers and the ping methods on schedules (^6.0).",
"laravel/tinker": "Required to use the tinker console command (^1.0).",
"league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^1.0).",
"league/flysystem-cached-adapter": "Required to use the Flysystem cache (^1.0).",
"league/flysystem-rackspace": "Required to use the Flysystem Rackspace driver (^1.0).",
"league/flysystem-sftp": "Required to use the Flysystem SFTP driver (^1.0).",
"moontoast/math": "Required to use ordered UUIDs (^1.1).",
"nexmo/client": "Required to use the Nexmo transport (^1.0).",
"pda/pheanstalk": "Required to use the beanstalk queue driver (^4.0).",
"predis/predis": "Required to use the redis cache and queue drivers (^1.0).",
"pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^3.0).",
"symfony/css-selector": "Required to use some of the crawler integration testing tools (^4.2).",
"symfony/dom-crawler": "Required to use most of the crawler integration testing tools (^4.2).",
"symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^1.1).",
"wildbit/swiftmailer-postmark": "Required to use Postmark mail driver (^3.0)."
},
"config": {
"sort-packages": true

View File

@@ -2,6 +2,9 @@
namespace Illuminate\Auth\Access;
use Exception;
use ReflectionClass;
use ReflectionFunction;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use InvalidArgumentException;
@@ -54,6 +57,20 @@ class Gate implements GateContract
*/
protected $afterCallbacks = [];
/**
* All of the defined abilities using class@method notation.
*
* @var array
*/
protected $stringCallbacks = [];
/**
* The callback to be used to guess policy names.
*
* @var callable|null
*/
protected $guessPolicyNamesUsingCallback;
/**
* Create a new gate instance.
*
@@ -63,10 +80,12 @@ class Gate implements GateContract
* @param array $policies
* @param array $beforeCallbacks
* @param array $afterCallbacks
* @param callable|null $guessPolicyNamesUsingCallback
* @return void
*/
public function __construct(Container $container, callable $userResolver, array $abilities = [],
array $policies = [], array $beforeCallbacks = [], array $afterCallbacks = [])
array $policies = [], array $beforeCallbacks = [], array $afterCallbacks = [],
callable $guessPolicyNamesUsingCallback = null)
{
$this->policies = $policies;
$this->container = $container;
@@ -74,6 +93,7 @@ class Gate implements GateContract
$this->userResolver = $userResolver;
$this->afterCallbacks = $afterCallbacks;
$this->beforeCallbacks = $beforeCallbacks;
$this->guessPolicyNamesUsingCallback = $guessPolicyNamesUsingCallback;
}
/**
@@ -108,7 +128,9 @@ class Gate implements GateContract
{
if (is_callable($callback)) {
$this->abilities[$ability] = $callback;
} elseif (is_string($callback) && Str::contains($callback, '@')) {
} elseif (is_string($callback)) {
$this->stringCallbacks[$ability] = $callback;
$this->abilities[$ability] = $this->buildAbilityCallback($ability, $callback);
} else {
throw new InvalidArgumentException("Callback must be a callable or a 'Class@method' string.");
@@ -128,10 +150,11 @@ class Gate implements GateContract
public function resource($name, $class, array $abilities = null)
{
$abilities = $abilities ?: [
'view' => 'view',
'create' => 'create',
'update' => 'update',
'delete' => 'delete',
'viewAny' => 'viewAny',
'view' => 'view',
'create' => 'create',
'update' => 'update',
'delete' => 'delete',
];
foreach ($abilities as $ability => $method) {
@@ -151,7 +174,11 @@ class Gate implements GateContract
protected function buildAbilityCallback($ability, $callback)
{
return function () use ($ability, $callback) {
list($class, $method) = Str::parseCallback($callback);
if (Str::contains($callback, '@')) {
[$class, $method] = Str::parseCallback($callback);
} else {
$class = $callback;
}
$policy = $this->resolvePolicy($class);
@@ -167,7 +194,9 @@ class Gate implements GateContract
return $result;
}
return $policy->{$method}(...func_get_args());
return isset($method)
? $policy->{$method}(...func_get_args())
: $policy(...func_get_args());
};
}
@@ -267,6 +296,18 @@ class Gate implements GateContract
});
}
/**
* Determine if all of the given abilities should be denied for the current user.
*
* @param iterable|string $abilities
* @param array|mixed $arguments
* @return bool
*/
public function none($abilities, $arguments = [])
{
return ! $this->any($abilities, $arguments);
}
/**
* Determine if the given ability should be granted for the current user.
*
@@ -294,14 +335,12 @@ class Gate implements GateContract
* @param array|mixed $arguments
* @return mixed
*/
protected function raw($ability, $arguments = [])
public function raw($ability, $arguments = [])
{
if (! $user = $this->resolveUser()) {
return false;
}
$arguments = Arr::wrap($arguments);
$user = $this->resolveUser();
// First we will call the "before" callbacks for the Gate. If any of these give
// back a non-null response, we will immediately return that result in order
// to let the developers override all checks for some authorization cases.
@@ -316,17 +355,95 @@ class Gate implements GateContract
// After calling the authorization callback, we will call the "after" callbacks
// that are registered with the Gate, which allows a developer to do logging
// if that is required for this application. Then we'll return the result.
$this->callAfterCallbacks(
return $this->callAfterCallbacks(
$user, $ability, $arguments, $result
);
}
return $result;
/**
* Determine whether the callback/method can be called with the given user.
*
* @param \Illuminate\Contracts\Auth\Authenticatable|null $user
* @param \Closure|string|array $class
* @param string|null $method
* @return bool
*/
protected function canBeCalledWithUser($user, $class, $method = null)
{
if (! is_null($user)) {
return true;
}
if (! is_null($method)) {
return $this->methodAllowsGuests($class, $method);
}
if (is_array($class)) {
$className = is_string($class[0]) ? $class[0] : get_class($class[0]);
return $this->methodAllowsGuests($className, $class[1]);
}
return $this->callbackAllowsGuests($class);
}
/**
* Determine if the given class method allows guests.
*
* @param string $class
* @param string $method
* @return bool
*/
protected function methodAllowsGuests($class, $method)
{
try {
$reflection = new ReflectionClass($class);
$method = $reflection->getMethod($method);
} catch (Exception $e) {
return false;
}
if ($method) {
$parameters = $method->getParameters();
return isset($parameters[0]) && $this->parameterAllowsGuests($parameters[0]);
}
return false;
}
/**
* Determine if the callback allows guests.
*
* @param callable $callback
* @return bool
*
* @throws \ReflectionException
*/
protected function callbackAllowsGuests($callback)
{
$parameters = (new ReflectionFunction($callback))->getParameters();
return isset($parameters[0]) && $this->parameterAllowsGuests($parameters[0]);
}
/**
* Determine if the given parameter allows guests.
*
* @param \ReflectionParameter $parameter
* @return bool
*/
protected function parameterAllowsGuests($parameter)
{
return ($parameter->getClass() && $parameter->allowsNull()) ||
($parameter->isDefaultValueAvailable() && is_null($parameter->getDefaultValue()));
}
/**
* Resolve and call the appropriate authorization callback.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param \Illuminate\Contracts\Auth\Authenticatable|null $user
* @param string $ability
* @param array $arguments
* @return bool
@@ -341,17 +458,19 @@ class Gate implements GateContract
/**
* Call all of the before callbacks and return if a result is given.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param \Illuminate\Contracts\Auth\Authenticatable|null $user
* @param string $ability
* @param array $arguments
* @return bool|null
*/
protected function callBeforeCallbacks($user, $ability, array $arguments)
{
$arguments = array_merge([$user, $ability], [$arguments]);
foreach ($this->beforeCallbacks as $before) {
if (! is_null($result = $before(...$arguments))) {
if (! $this->canBeCalledWithUser($user, $before)) {
continue;
}
if (! is_null($result = $before($user, $ability, $arguments))) {
return $result;
}
}
@@ -364,21 +483,27 @@ class Gate implements GateContract
* @param string $ability
* @param array $arguments
* @param bool $result
* @return void
* @return bool|null
*/
protected function callAfterCallbacks($user, $ability, array $arguments, $result)
{
$arguments = array_merge([$user, $ability, $result], [$arguments]);
foreach ($this->afterCallbacks as $after) {
$after(...$arguments);
if (! $this->canBeCalledWithUser($user, $after)) {
continue;
}
$afterResult = $after($user, $ability, $result, $arguments);
$result = $result ?? $afterResult;
}
return $result;
}
/**
* Resolve the callable for the given ability and arguments.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param \Illuminate\Contracts\Auth\Authenticatable|null $user
* @param string $ability
* @param array $arguments
* @return callable
@@ -391,12 +516,20 @@ class Gate implements GateContract
return $callback;
}
if (isset($this->abilities[$ability])) {
if (isset($this->stringCallbacks[$ability])) {
[$class, $method] = Str::parseCallback($this->stringCallbacks[$ability]);
if ($this->canBeCalledWithUser($user, $class, $method ?: '__invoke')) {
return $this->abilities[$ability];
}
}
if (isset($this->abilities[$ability]) &&
$this->canBeCalledWithUser($user, $this->abilities[$ability])) {
return $this->abilities[$ability];
}
return function () {
return false;
};
}
@@ -420,6 +553,12 @@ class Gate implements GateContract
return $this->resolvePolicy($this->policies[$class]);
}
foreach ($this->guessPolicyName($class) as $guessedPolicy) {
if (class_exists($guessedPolicy)) {
return $this->resolvePolicy($guessedPolicy);
}
}
foreach ($this->policies as $expected => $policy) {
if (is_subclass_of($class, $expected)) {
return $this->resolvePolicy($policy);
@@ -427,11 +566,43 @@ class Gate implements GateContract
}
}
/**
* Guess the policy name for the given class.
*
* @param string $class
* @return array
*/
protected function guessPolicyName($class)
{
if ($this->guessPolicyNamesUsingCallback) {
return Arr::wrap(call_user_func($this->guessPolicyNamesUsingCallback, $class));
}
$classDirname = str_replace('/', '\\', dirname(str_replace('\\', '/', $class)));
return [$classDirname.'\\Policies\\'.class_basename($class).'Policy'];
}
/**
* Specify a callback to be used to guess policy names.
*
* @param callable $callback
* @return $this
*/
public function guessPolicyNamesUsing(callable $callback)
{
$this->guessPolicyNamesUsingCallback = $callback;
return $this;
}
/**
* Build a policy class instance of the given type.
*
* @param object|string $class
* @return mixed
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function resolvePolicy($class)
{
@@ -468,18 +639,9 @@ class Gate implements GateContract
return $result;
}
$ability = $this->formatAbilityToMethod($ability);
$method = $this->formatAbilityToMethod($ability);
// If this first argument is a string, that means they are passing a class name
// to the policy. We will remove the first argument from this argument array
// because this policy already knows what type of models it can authorize.
if (isset($arguments[0]) && is_string($arguments[0])) {
array_shift($arguments);
}
return is_callable([$policy, $ability])
? $policy->{$ability}($user, ...$arguments)
: false;
return $this->callPolicyMethod($policy, $method, $user, $arguments);
};
}
@@ -494,11 +656,42 @@ class Gate implements GateContract
*/
protected function callPolicyBefore($policy, $user, $ability, $arguments)
{
if (method_exists($policy, 'before')) {
if (! method_exists($policy, 'before')) {
return;
}
if ($this->canBeCalledWithUser($user, $policy, 'before')) {
return $policy->before($user, $ability, ...$arguments);
}
}
/**
* Call the appropriate method on the given policy.
*
* @param mixed $policy
* @param string $method
* @param \Illuminate\Contracts\Auth\Authenticatable|null $user
* @param array $arguments
* @return mixed
*/
protected function callPolicyMethod($policy, $method, $user, array $arguments)
{
// If this first argument is a string, that means they are passing a class name
// to the policy. We will remove the first argument from this argument array
// because this policy already knows what type of models it can authorize.
if (isset($arguments[0]) && is_string($arguments[0])) {
array_shift($arguments);
}
if (! is_callable([$policy, $method])) {
return;
}
if ($this->canBeCalledWithUser($user, $policy, $method)) {
return $policy->{$method}($user, ...$arguments);
}
}
/**
* Format the policy ability into a method name.
*
@@ -524,7 +717,8 @@ class Gate implements GateContract
return new static(
$this->container, $callback, $this->abilities,
$this->policies, $this->beforeCallbacks, $this->afterCallbacks
$this->policies, $this->beforeCallbacks, $this->afterCallbacks,
$this->guessPolicyNamesUsingCallback
);
}

View File

@@ -35,10 +35,10 @@ class Response
/**
* Get the string representation of the message.
*
* @return string|null
* @return string
*/
public function __toString()
{
return $this->message();
return (string) $this->message();
}
}

View File

@@ -13,7 +13,7 @@ class AuthManager implements FactoryContract
/**
* The application instance.
*
* @var \Illuminate\Foundation\Application
* @var \Illuminate\Contracts\Foundation\Application
*/
protected $app;
@@ -43,7 +43,7 @@ class AuthManager implements FactoryContract
/**
* Create a new Auth manager instance.
*
* @param \Illuminate\Foundation\Application $app
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function __construct($app)
@@ -58,7 +58,7 @@ class AuthManager implements FactoryContract
/**
* Attempt to get the guard from the local cache.
*
* @param string $name
* @param string|null $name
* @return \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard
*/
public function guard($name = null)
@@ -94,7 +94,9 @@ class AuthManager implements FactoryContract
return $this->{$driverMethod}($name, $config);
}
throw new InvalidArgumentException("Auth driver [{$config['driver']}] for guard [{$name}] is not defined.");
throw new InvalidArgumentException(
"Auth driver [{$config['driver']}] for guard [{$name}] is not defined."
);
}
/**
@@ -154,7 +156,10 @@ class AuthManager implements FactoryContract
// user in the database or another persistence layer where users are.
$guard = new TokenGuard(
$this->createUserProvider($config['provider'] ?? null),
$this->app['request']
$this->app['request'],
$config['input_key'] ?? 'api_token',
$config['storage_key'] ?? 'api_token',
$config['hash'] ?? false
);
$this->app->refresh('request', $guard, 'setRequest');

View File

@@ -17,12 +17,10 @@ class AuthServiceProvider extends ServiceProvider
public function register()
{
$this->registerAuthenticator();
$this->registerUserResolver();
$this->registerAccessGate();
$this->registerRequestRebindHandler();
$this->registerEventRebindHandler();
}
/**
@@ -75,7 +73,7 @@ class AuthServiceProvider extends ServiceProvider
}
/**
* Register a resolver for the authenticated user.
* Handle the re-binding of the request binding.
*
* @return void
*/
@@ -87,4 +85,22 @@ class AuthServiceProvider extends ServiceProvider
});
});
}
/**
* Handle the re-binding of the event dispatcher binding.
*
* @return void
*/
protected function registerEventRebindHandler()
{
$this->app->rebinding('events', function ($app, $dispatcher) {
if (! $app->resolved('auth')) {
return;
}
if (method_exists($guard = $app['auth']->guard(), 'setDispatcher')) {
$guard->setDispatcher($dispatcher);
}
});
}
}

View File

@@ -13,18 +13,27 @@ class AuthenticationException extends Exception
*/
protected $guards;
/**
* The path the user should be redirected to.
*
* @var string
*/
protected $redirectTo;
/**
* Create a new authentication exception.
*
* @param string $message
* @param array $guards
* @param string|null $redirectTo
* @return void
*/
public function __construct($message = 'Unauthenticated.', array $guards = [])
public function __construct($message = 'Unauthenticated.', array $guards = [], $redirectTo = null)
{
parent::__construct($message);
$this->guards = $guards;
$this->redirectTo = $redirectTo;
}
/**
@@ -36,4 +45,14 @@ class AuthenticationException extends Exception
{
return $this->guards;
}
/**
* Get the path the user should be redirected to.
*
* @return string
*/
public function redirectTo()
{
return $this->redirectTo;
}
}

View File

@@ -33,6 +33,7 @@ class AuthMakeCommand extends Command
protected $views = [
'auth/login.stub' => 'auth/login.blade.php',
'auth/register.stub' => 'auth/register.blade.php',
'auth/verify.stub' => 'auth/verify.blade.php',
'auth/passwords/email.stub' => 'auth/passwords/email.blade.php',
'auth/passwords/reset.stub' => 'auth/passwords/reset.blade.php',
'layouts/app.stub' => 'layouts/app.blade.php',
@@ -73,11 +74,11 @@ class AuthMakeCommand extends Command
*/
protected function createDirectories()
{
if (! is_dir($directory = resource_path('views/layouts'))) {
if (! is_dir($directory = $this->getViewPath('layouts'))) {
mkdir($directory, 0755, true);
}
if (! is_dir($directory = resource_path('views/auth/passwords'))) {
if (! is_dir($directory = $this->getViewPath('auth/passwords'))) {
mkdir($directory, 0755, true);
}
}
@@ -90,7 +91,7 @@ class AuthMakeCommand extends Command
protected function exportViews()
{
foreach ($this->views as $key => $value) {
if (file_exists($view = resource_path('views/'.$value)) && ! $this->option('force')) {
if (file_exists($view = $this->getViewPath($value)) && ! $this->option('force')) {
if (! $this->confirm("The [{$value}] view already exists. Do you want to replace it?")) {
continue;
}
@@ -116,4 +117,17 @@ class AuthMakeCommand extends Command
file_get_contents(__DIR__.'/stubs/make/controllers/HomeController.stub')
);
}
/**
* Get full view path relative to the app's configured view path.
*
* @param string $path
* @return string
*/
protected function getViewPath($path)
{
return implode(DIRECTORY_SEPARATOR, [
config('view.paths')[0] ?? resource_path('views'), $path,
]);
}
}

View File

@@ -19,7 +19,7 @@ class HomeController extends Controller
/**
* Show the application dashboard.
*
* @return \Illuminate\Http\Response
* @return \Illuminate\Contracts\Support\Renderable
*/
public function index()
{

View File

@@ -8,20 +8,20 @@
<div class="card-header">{{ __('Login') }}</div>
<div class="card-body">
<form method="POST" action="{{ route('login') }}" aria-label="{{ __('Login') }}">
<form method="POST" action="{{ route('login') }}">
@csrf
<div class="form-group row">
<label for="email" class="col-sm-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>
<label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>
<div class="col-md-6">
<input id="email" type="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ old('email') }}" required autofocus>
<input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" required autocomplete="email" autofocus>
@if ($errors->has('email'))
@error('email')
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('email') }}</strong>
<strong>{{ $message }}</strong>
</span>
@endif
@enderror
</div>
</div>
@@ -29,13 +29,13 @@
<label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>
<div class="col-md-6">
<input id="password" type="password" class="form-control{{ $errors->has('password') ? ' is-invalid' : '' }}" name="password" required>
<input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="current-password">
@if ($errors->has('password'))
@error('password')
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('password') }}</strong>
<strong>{{ $message }}</strong>
</span>
@endif
@enderror
</div>
</div>
@@ -57,9 +57,11 @@
{{ __('Login') }}
</button>
<a class="btn btn-link" href="{{ route('password.request') }}">
{{ __('Forgot Your Password?') }}
</a>
@if (Route::has('password.request'))
<a class="btn btn-link" href="{{ route('password.request') }}">
{{ __('Forgot Your Password?') }}
</a>
@endif
</div>
</div>
</form>

View File

@@ -14,20 +14,20 @@
</div>
@endif
<form method="POST" action="{{ route('password.email') }}" aria-label="{{ __('Reset Password') }}">
<form method="POST" action="{{ route('password.email') }}">
@csrf
<div class="form-group row">
<label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>
<div class="col-md-6">
<input id="email" type="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ old('email') }}" required>
<input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" required autocomplete="email" autofocus>
@if ($errors->has('email'))
@error('email')
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('email') }}</strong>
<strong>{{ $message }}</strong>
</span>
@endif
@enderror
</div>
</div>

View File

@@ -8,7 +8,7 @@
<div class="card-header">{{ __('Reset Password') }}</div>
<div class="card-body">
<form method="POST" action="{{ route('password.request') }}" aria-label="{{ __('Reset Password') }}">
<form method="POST" action="{{ route('password.update') }}">
@csrf
<input type="hidden" name="token" value="{{ $token }}">
@@ -17,13 +17,13 @@
<label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>
<div class="col-md-6">
<input id="email" type="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ $email ?? old('email') }}" required autofocus>
<input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ $email ?? old('email') }}" required autocomplete="email" autofocus>
@if ($errors->has('email'))
@error('email')
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('email') }}</strong>
<strong>{{ $message }}</strong>
</span>
@endif
@enderror
</div>
</div>
@@ -31,13 +31,13 @@
<label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>
<div class="col-md-6">
<input id="password" type="password" class="form-control{{ $errors->has('password') ? ' is-invalid' : '' }}" name="password" required>
<input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="new-password">
@if ($errors->has('password'))
@error('password')
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('password') }}</strong>
<strong>{{ $message }}</strong>
</span>
@endif
@enderror
</div>
</div>
@@ -45,7 +45,7 @@
<label for="password-confirm" class="col-md-4 col-form-label text-md-right">{{ __('Confirm Password') }}</label>
<div class="col-md-6">
<input id="password-confirm" type="password" class="form-control" name="password_confirmation" required>
<input id="password-confirm" type="password" class="form-control" name="password_confirmation" required autocomplete="new-password">
</div>
</div>

View File

@@ -8,20 +8,20 @@
<div class="card-header">{{ __('Register') }}</div>
<div class="card-body">
<form method="POST" action="{{ route('register') }}" aria-label="{{ __('Register') }}">
<form method="POST" action="{{ route('register') }}">
@csrf
<div class="form-group row">
<label for="name" class="col-md-4 col-form-label text-md-right">{{ __('Name') }}</label>
<div class="col-md-6">
<input id="name" type="text" class="form-control{{ $errors->has('name') ? ' is-invalid' : '' }}" name="name" value="{{ old('name') }}" required autofocus>
<input id="name" type="text" class="form-control @error('name') is-invalid @enderror" name="name" value="{{ old('name') }}" required autocomplete="name" autofocus>
@if ($errors->has('name'))
@error('name')
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('name') }}</strong>
<strong>{{ $message }}</strong>
</span>
@endif
@enderror
</div>
</div>
@@ -29,13 +29,13 @@
<label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>
<div class="col-md-6">
<input id="email" type="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ old('email') }}" required>
<input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" required autocomplete="email">
@if ($errors->has('email'))
@error('email')
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('email') }}</strong>
<strong>{{ $message }}</strong>
</span>
@endif
@enderror
</div>
</div>
@@ -43,13 +43,13 @@
<label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>
<div class="col-md-6">
<input id="password" type="password" class="form-control{{ $errors->has('password') ? ' is-invalid' : '' }}" name="password" required>
<input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="new-password">
@if ($errors->has('password'))
@error('password')
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('password') }}</strong>
<strong>{{ $message }}</strong>
</span>
@endif
@enderror
</div>
</div>
@@ -57,7 +57,7 @@
<label for="password-confirm" class="col-md-4 col-form-label text-md-right">{{ __('Confirm Password') }}</label>
<div class="col-md-6">
<input id="password-confirm" type="password" class="form-control" name="password_confirmation" required>
<input id="password-confirm" type="password" class="form-control" name="password_confirmation" required autocomplete="new-password">
</div>
</div>

View File

@@ -0,0 +1,24 @@
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Verify Your Email Address') }}</div>
<div class="card-body">
@if (session('resent'))
<div class="alert alert-success" role="alert">
{{ __('A fresh verification link has been sent to your email address.') }}
</div>
@endif
{{ __('Before proceeding, please check your email for a verification link.') }}
{{ __('If you did not receive the email') }}, <a href="{{ route('verification.resend') }}">{{ __('click here to request another') }}</a>.
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -2,7 +2,6 @@
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- CSRF Token -->
@@ -14,15 +13,15 @@
<script src="{{ asset('js/app.js') }}" defer></script>
<!-- Fonts -->
<link rel="dns-prefetch" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet" type="text/css">
<link rel="dns-prefetch" href="//fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet">
<!-- Styles -->
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
<div id="app">
<nav class="navbar navbar-expand-md navbar-light navbar-laravel">
<nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm">
<div class="container">
<a class="navbar-brand" href="{{ url('/') }}">
{{ config('app.name', 'Laravel') }}
@@ -44,9 +43,11 @@
<li class="nav-item">
<a class="nav-link" href="{{ route('login') }}">{{ __('Login') }}</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ route('register') }}">{{ __('Register') }}</a>
</li>
@if (Route::has('register'))
<li class="nav-item">
<a class="nav-link" href="{{ route('register') }}">{{ __('Register') }}</a>
</li>
@endif
@else
<li class="nav-item dropdown">
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>

View File

@@ -47,9 +47,9 @@ class EloquentUserProvider implements UserProvider
{
$model = $this->createModel();
return $model->newQuery()
->where($model->getAuthIdentifierName(), $identifier)
->first();
return $this->newModelQuery($model)
->where($model->getAuthIdentifierName(), $identifier)
->first();
}
/**
@@ -63,21 +63,24 @@ class EloquentUserProvider implements UserProvider
{
$model = $this->createModel();
$model = $model->where($model->getAuthIdentifierName(), $identifier)->first();
$retrievedModel = $this->newModelQuery($model)->where(
$model->getAuthIdentifierName(), $identifier
)->first();
if (! $model) {
return null;
if (! $retrievedModel) {
return;
}
$rememberToken = $model->getRememberToken();
$rememberToken = $retrievedModel->getRememberToken();
return $rememberToken && hash_equals($rememberToken, $token) ? $model : null;
return $rememberToken && hash_equals($rememberToken, $token)
? $retrievedModel : null;
}
/**
* Update the "remember me" token for the given user in storage.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param \Illuminate\Contracts\Auth\Authenticatable|\Illuminate\Database\Eloquent\Model $user
* @param string $token
* @return void
*/
@@ -104,14 +107,14 @@ class EloquentUserProvider implements UserProvider
{
if (empty($credentials) ||
(count($credentials) === 1 &&
array_key_exists('password', $credentials))) {
Str::contains($this->firstCredentialKey($credentials), 'password'))) {
return;
}
// First we will add each credential element to the query as a where clause.
// Then we can execute the query and, if we found a user, return it in a
// Eloquent User "model" that will be utilized by the Guard instances.
$query = $this->createModel()->newQuery();
$query = $this->newModelQuery();
foreach ($credentials as $key => $value) {
if (Str::contains($key, 'password')) {
@@ -128,6 +131,19 @@ class EloquentUserProvider implements UserProvider
return $query->first();
}
/**
* Get the first key from the credential array.
*
* @param array $credentials
* @return string|null
*/
protected function firstCredentialKey(array $credentials)
{
foreach ($credentials as $key => $value) {
return $key;
}
}
/**
* Validate a user against the given credentials.
*
@@ -142,6 +158,19 @@ class EloquentUserProvider implements UserProvider
return $this->hasher->check($plain, $user->getAuthPassword());
}
/**
* Get a new query builder for the model instance.
*
* @param \Illuminate\Database\Eloquent\Model|null $model
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function newModelQuery($model = null)
{
return is_null($model)
? $this->createModel()->newQuery()
: $model->newQuery();
}
/**
* Create a new instance of the model.
*

View File

@@ -4,6 +4,13 @@ namespace Illuminate\Auth\Events;
class Attempting
{
/**
* The authentication guard name.
*
* @var string
*/
public $guard;
/**
* The credentials for the user.
*
@@ -21,12 +28,14 @@ class Attempting
/**
* Create a new event instance.
*
* @param string $guard
* @param array $credentials
* @param bool $remember
* @return void
*/
public function __construct($credentials, $remember)
public function __construct($guard, $credentials, $remember)
{
$this->guard = $guard;
$this->remember = $remember;
$this->credentials = $credentials;
}

View File

@@ -8,6 +8,13 @@ class Authenticated
{
use SerializesModels;
/**
* The authentication guard name.
*
* @var string
*/
public $guard;
/**
* The authenticated user.
*
@@ -18,11 +25,13 @@ class Authenticated
/**
* Create a new event instance.
*
* @param string $guard
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @return void
*/
public function __construct($user)
public function __construct($guard, $user)
{
$this->user = $user;
$this->guard = $guard;
}
}

View File

@@ -4,6 +4,13 @@ namespace Illuminate\Auth\Events;
class Failed
{
/**
* The authentication guard name.
*
* @var string
*/
public $guard;
/**
* The user the attempter was trying to authenticate as.
*
@@ -21,13 +28,15 @@ class Failed
/**
* Create a new event instance.
*
* @param string $guard
* @param \Illuminate\Contracts\Auth\Authenticatable|null $user
* @param array $credentials
* @return void
*/
public function __construct($user, $credentials)
public function __construct($guard, $user, $credentials)
{
$this->user = $user;
$this->guard = $guard;
$this->credentials = $credentials;
}
}

View File

@@ -8,6 +8,13 @@ class Login
{
use SerializesModels;
/**
* The authentication guard name.
*
* @var string
*/
public $guard;
/**
* The authenticated user.
*
@@ -25,13 +32,15 @@ class Login
/**
* Create a new event instance.
*
* @param string $guard
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param bool $remember
* @return void
*/
public function __construct($user, $remember)
public function __construct($guard, $user, $remember)
{
$this->user = $user;
$this->guard = $guard;
$this->remember = $remember;
}
}

View File

@@ -8,6 +8,13 @@ class Logout
{
use SerializesModels;
/**
* The authentication guard name.
*
* @var string
*/
public $guard;
/**
* The authenticated user.
*
@@ -18,11 +25,13 @@ class Logout
/**
* Create a new event instance.
*
* @param string $guard
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @return void
*/
public function __construct($user)
public function __construct($guard, $user)
{
$this->user = $user;
$this->guard = $guard;
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Illuminate\Auth\Events;
use Illuminate\Queue\SerializesModels;
class OtherDeviceLogout
{
use SerializesModels;
/**
* The authentication guard name.
*
* @var string
*/
public $guard;
/**
* The authenticated user.
*
* @var \Illuminate\Contracts\Auth\Authenticatable
*/
public $user;
/**
* Create a new event instance.
*
* @param string $guard
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @return void
*/
public function __construct($guard, $user)
{
$this->user = $user;
$this->guard = $guard;
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Illuminate\Auth\Events;
use Illuminate\Queue\SerializesModels;
class Verified
{
use SerializesModels;
/**
* The verified user.
*
* @var \Illuminate\Contracts\Auth\MustVerifyEmail
*/
public $user;
/**
* Create a new event instance.
*
* @param \Illuminate\Contracts\Auth\MustVerifyEmail $user
* @return void
*/
public function __construct($user)
{
$this->user = $user;
}
}

View File

@@ -25,7 +25,7 @@ trait GuardHelpers
protected $provider;
/**
* Determine if the current user is authenticated.
* Determine if current user is authenticated. If not, throw an exception.
*
* @return \Illuminate\Contracts\Auth\Authenticatable
*

View File

@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) <Taylor Otwell>
Copyright (c) Taylor Otwell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -0,0 +1,22 @@
<?php
namespace Illuminate\Auth\Listeners;
use Illuminate\Auth\Events\Registered;
use Illuminate\Contracts\Auth\MustVerifyEmail;
class SendEmailVerificationNotification
{
/**
* Handle the event.
*
* @param \Illuminate\Auth\Events\Registered $event
* @return void
*/
public function handle(Registered $event)
{
if ($event->user instanceof MustVerifyEmail && ! $event->user->hasVerifiedEmail()) {
$event->user->sendEmailVerificationNotification();
}
}
}

View File

@@ -38,7 +38,7 @@ class Authenticate
*/
public function handle($request, Closure $next, ...$guards)
{
$this->authenticate($guards);
$this->authenticate($request, $guards);
return $next($request);
}
@@ -46,15 +46,16 @@ class Authenticate
/**
* Determine if the user is logged in to any of the given guards.
*
* @param \Illuminate\Http\Request $request
* @param array $guards
* @return void
*
* @throws \Illuminate\Auth\AuthenticationException
*/
protected function authenticate(array $guards)
protected function authenticate($request, array $guards)
{
if (empty($guards)) {
return $this->auth->authenticate();
$guards = [null];
}
foreach ($guards as $guard) {
@@ -63,6 +64,19 @@ class Authenticate
}
}
throw new AuthenticationException('Unauthenticated.', $guards);
throw new AuthenticationException(
'Unauthenticated.', $guards, $this->redirectTo($request)
);
}
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* @param \Illuminate\Http\Request $request
* @return string
*/
protected function redirectTo($request)
{
//
}
}

View File

@@ -31,10 +31,15 @@ class AuthenticateWithBasicAuth
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $guard
* @param string|null $field
* @return mixed
*
* @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException
*/
public function handle($request, Closure $next, $guard = null)
public function handle($request, Closure $next, $guard = null, $field = null)
{
return $this->auth->guard($guard)->basic() ?: $next($request);
$this->auth->guard($guard)->basic($field ?: 'email');
return $next($request);
}
}

View File

@@ -5,17 +5,9 @@ namespace Illuminate\Auth\Middleware;
use Closure;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Contracts\Auth\Access\Gate;
use Illuminate\Contracts\Auth\Factory as Auth;
class Authorize
{
/**
* The authentication factory instance.
*
* @var \Illuminate\Contracts\Auth\Factory
*/
protected $auth;
/**
* The gate instance.
*
@@ -26,13 +18,11 @@ class Authorize
/**
* Create a new middleware instance.
*
* @param \Illuminate\Contracts\Auth\Factory $auth
* @param \Illuminate\Contracts\Auth\Access\Gate $gate
* @return void
*/
public function __construct(Auth $auth, Gate $gate)
public function __construct(Gate $gate)
{
$this->auth = $auth;
$this->gate = $gate;
}
@@ -50,8 +40,6 @@ class Authorize
*/
public function handle($request, Closure $next, $ability, ...$models)
{
$this->auth->authenticate();
$this->gate->authorize($ability, $this->getGateArguments($request, $models));
return $next($request);
@@ -84,7 +72,12 @@ class Authorize
*/
protected function getModel($request, $model)
{
return $this->isClassName($model) ? $model : $request->route($model);
if ($this->isClassName($model)) {
return trim($model);
} else {
return $request->route($model, null) ?:
((preg_match("/^['\"](.*)['\"]$/", trim($model), $matches)) ? $matches[1] : null);
}
}
/**

View File

@@ -0,0 +1,31 @@
<?php
namespace Illuminate\Auth\Middleware;
use Closure;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Contracts\Auth\MustVerifyEmail;
class EnsureEmailIsVerified
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $redirectToRoute
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle($request, Closure $next, $redirectToRoute = null)
{
if (! $request->user() ||
($request->user() instanceof MustVerifyEmail &&
! $request->user()->hasVerifiedEmail())) {
return $request->expectsJson()
? abort(403, 'Your email address is not verified.')
: Redirect::route($redirectToRoute ?: 'verification.notice');
}
return $next($request);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Illuminate\Auth;
trait MustVerifyEmail
{
/**
* Determine if the user has verified their email address.
*
* @return bool
*/
public function hasVerifiedEmail()
{
return ! is_null($this->email_verified_at);
}
/**
* Mark the given user's email as verified.
*
* @return bool
*/
public function markEmailAsVerified()
{
return $this->forceFill([
'email_verified_at' => $this->freshTimestamp(),
])->save();
}
/**
* Send the email verification notification.
*
* @return void
*/
public function sendEmailVerificationNotification()
{
$this->notify(new Notifications\VerifyEmail);
}
}

View File

@@ -59,7 +59,8 @@ class ResetPassword extends Notification
return (new MailMessage)
->subject(Lang::getFromJson('Reset Password Notification'))
->line(Lang::getFromJson('You are receiving this email because we received a password reset request for your account.'))
->action(Lang::getFromJson('Reset Password'), url(config('app.url').route('password.reset', $this->token, false)))
->action(Lang::getFromJson('Reset Password'), url(config('app.url').route('password.reset', ['token' => $this->token, 'email' => $notifiable->getEmailForPasswordReset()], false)))
->line(Lang::getFromJson('This password reset link will expire in :count minutes.', ['count' => config('auth.passwords.'.config('auth.defaults.passwords').'.expire')]))
->line(Lang::getFromJson('If you did not request a password reset, no further action is required.'));
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Illuminate\Auth\Notifications;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Config;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;
class VerifyEmail extends Notification
{
/**
* The callback that should be used to build the mail message.
*
* @var \Closure|null
*/
public static $toMailCallback;
/**
* Get the notification's channels.
*
* @param mixed $notifiable
* @return array|string
*/
public function via($notifiable)
{
return ['mail'];
}
/**
* Build the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
$verificationUrl = $this->verificationUrl($notifiable);
if (static::$toMailCallback) {
return call_user_func(static::$toMailCallback, $notifiable, $verificationUrl);
}
return (new MailMessage)
->subject(Lang::getFromJson('Verify Email Address'))
->line(Lang::getFromJson('Please click the button below to verify your email address.'))
->action(Lang::getFromJson('Verify Email Address'), $verificationUrl)
->line(Lang::getFromJson('If you did not create an account, no further action is required.'));
}
/**
* Get the verification URL for the given notifiable.
*
* @param mixed $notifiable
* @return string
*/
protected function verificationUrl($notifiable)
{
return URL::temporarySignedRoute(
'verification.verify',
Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
['id' => $notifiable->getKey()]
);
}
/**
* Set a callback that should be used when building the notification mail message.
*
* @param \Closure $callback
* @return void
*/
public static function toMailUsing($callback)
{
static::$toMailCallback = $callback;
}
}

View File

@@ -146,7 +146,7 @@ class PasswordBroker implements PasswordBrokerContract
public function validateNewPassword(array $credentials)
{
if (isset($this->passwordValidator)) {
list($password, $confirm) = [
[$password, $confirm] = [
$credentials['password'],
$credentials['password_confirmation'],
];
@@ -167,12 +167,12 @@ class PasswordBroker implements PasswordBrokerContract
*/
protected function validatePasswordWithDefaults(array $credentials)
{
list($password, $confirm) = [
[$password, $confirm] = [
$credentials['password'],
$credentials['password_confirmation'],
];
return $password === $confirm && mb_strlen($password) >= 6;
return $password === $confirm && mb_strlen($password) >= 8;
}
/**

View File

@@ -14,7 +14,7 @@ class PasswordBrokerManager implements FactoryContract
/**
* The application instance.
*
* @var \Illuminate\Foundation\Application
* @var \Illuminate\Contracts\Foundation\Application
*/
protected $app;
@@ -28,7 +28,7 @@ class PasswordBrokerManager implements FactoryContract
/**
* Create a new PasswordBroker manager instance.
*
* @param \Illuminate\Foundation\Application $app
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function __construct($app)
@@ -46,9 +46,7 @@ class PasswordBrokerManager implements FactoryContract
{
$name = $name ?: $this->getDefaultDriver();
return isset($this->brokers[$name])
? $this->brokers[$name]
: $this->brokers[$name] = $this->resolve($name);
return $this->brokers[$name] ?? ($this->brokers[$name] = $this->resolve($name));
}
/**

View File

@@ -3,16 +3,10 @@
namespace Illuminate\Auth\Passwords;
use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Support\DeferrableProvider;
class PasswordResetServiceProvider extends ServiceProvider
class PasswordResetServiceProvider extends ServiceProvider implements DeferrableProvider
{
/**
* Indicates if loading of the provider is deferred.
*
* @var bool
*/
protected $defer = true;
/**
* Register the service provider.
*

View File

@@ -83,6 +83,6 @@ class Recaller
{
$segments = explode('|', $this->recaller);
return count($segments) == 3 && trim($segments[0]) !== '' && trim($segments[1]) !== '';
return count($segments) === 3 && trim($segments[0]) !== '' && trim($segments[1]) !== '';
}
}

View File

@@ -135,9 +135,7 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
// If the user is null, but we decrypt a "recaller" cookie we can attempt to
// pull the user data on that cookie which serves as a remember cookie on
// the application. Once we have a user we can return it to the caller.
$recaller = $this->recaller();
if (is_null($this->user) && ! is_null($recaller)) {
if (is_null($this->user) && ! is_null($recaller = $this->recaller())) {
$this->user = $this->userFromRecaller($recaller);
if ($this->user) {
@@ -329,6 +327,7 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
* Get the response for basic authentication.
*
* @return void
*
* @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException
*/
protected function failedBasicResponse()
@@ -488,12 +487,12 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
// listening for anytime a user signs out of this application manually.
$this->clearUserDataFromStorage();
if (! is_null($this->user)) {
if (! is_null($this->user) && ! empty($user->getRememberToken())) {
$this->cycleRememberToken($user);
}
if (isset($this->events)) {
$this->events->dispatch(new Events\Logout($user));
$this->events->dispatch(new Events\Logout($this->name, $user));
}
// Once we have fired the logout event we will clear the users out of memory
@@ -551,7 +550,12 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
$attribute => Hash::make($password),
]))->save();
$this->queueRecallerCookie($this->user());
if ($this->recaller() ||
$this->getCookieJar()->hasQueued($this->getRecallerName())) {
$this->queueRecallerCookie($this->user());
}
$this->fireOtherDeviceLogoutEvent($this->user());
return $result;
}
@@ -580,7 +584,7 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
{
if (isset($this->events)) {
$this->events->dispatch(new Events\Attempting(
$credentials, $remember
$this->name, $credentials, $remember
));
}
}
@@ -595,7 +599,9 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
protected function fireLoginEvent($user, $remember = false)
{
if (isset($this->events)) {
$this->events->dispatch(new Events\Login($user, $remember));
$this->events->dispatch(new Events\Login(
$this->name, $user, $remember
));
}
}
@@ -608,7 +614,24 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
protected function fireAuthenticatedEvent($user)
{
if (isset($this->events)) {
$this->events->dispatch(new Events\Authenticated($user));
$this->events->dispatch(new Events\Authenticated(
$this->name, $user
));
}
}
/**
* Fire the other device logout event if the dispatcher is set.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @return void
*/
protected function fireOtherDeviceLogoutEvent($user)
{
if (isset($this->events)) {
$this->events->dispatch(new Events\OtherDeviceLogout(
$this->name, $user
));
}
}
@@ -622,7 +645,9 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
protected function fireFailedEvent($user, array $credentials)
{
if (isset($this->events)) {
$this->events->dispatch(new Events\Failed($user, $credentials));
$this->events->dispatch(new Events\Failed(
$this->name, $user, $credentials
));
}
}

View File

@@ -31,6 +31,13 @@ class TokenGuard implements Guard
*/
protected $storageKey;
/**
* Indicates if the API token is hashed in storage.
*
* @var bool
*/
protected $hash = false;
/**
* Create a new authentication guard.
*
@@ -38,10 +45,17 @@ class TokenGuard implements Guard
* @param \Illuminate\Http\Request $request
* @param string $inputKey
* @param string $storageKey
* @param bool $hash
* @return void
*/
public function __construct(UserProvider $provider, Request $request, $inputKey = 'api_token', $storageKey = 'api_token')
public function __construct(
UserProvider $provider,
Request $request,
$inputKey = 'api_token',
$storageKey = 'api_token',
$hash = false)
{
$this->hash = $hash;
$this->request = $request;
$this->provider = $provider;
$this->inputKey = $inputKey;
@@ -67,9 +81,9 @@ class TokenGuard implements Guard
$token = $this->getTokenForRequest();
if (! empty($token)) {
$user = $this->provider->retrieveByCredentials(
[$this->storageKey => $token]
);
$user = $this->provider->retrieveByCredentials([
$this->storageKey => $this->hash ? hash('sha256', $token) : $token,
]);
}
return $this->user = $user;

View File

@@ -15,10 +15,10 @@
],
"require": {
"php": "^7.1.3",
"illuminate/contracts": "5.6.*",
"illuminate/http": "5.6.*",
"illuminate/queue": "5.6.*",
"illuminate/support": "5.6.*"
"illuminate/contracts": "5.8.*",
"illuminate/http": "5.8.*",
"illuminate/queue": "5.8.*",
"illuminate/support": "5.8.*"
},
"autoload": {
"psr-4": {
@@ -27,13 +27,13 @@
},
"extra": {
"branch-alias": {
"dev-master": "5.6-dev"
"dev-master": "5.8-dev"
}
},
"suggest": {
"illuminate/console": "Required to use the auth:clear-resets command (5.6.*).",
"illuminate/queue": "Required to fire login / logout events (5.6.*).",
"illuminate/session": "Required to use the session based guard (5.6.*)."
"illuminate/console": "Required to use the auth:clear-resets command (5.8.*).",
"illuminate/queue": "Required to fire login / logout events (5.8.*).",
"illuminate/session": "Required to use the session based guard (5.8.*)."
},
"config": {
"sort-packages": true

View File

@@ -16,6 +16,10 @@ class BroadcastController extends Controller
*/
public function authenticate(Request $request)
{
if ($request->hasSession()) {
$request->session()->reflash();
}
return Broadcast::auth($request);
}
}

View File

@@ -21,7 +21,7 @@ class BroadcastManager implements FactoryContract
/**
* The application instance.
*
* @var \Illuminate\Foundation\Application
* @var \Illuminate\Contracts\Foundation\Application
*/
protected $app;
@@ -42,7 +42,7 @@ class BroadcastManager implements FactoryContract
/**
* Create a new manager instance.
*
* @param \Illuminate\Foundation\Application $app
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function __construct($app)
@@ -132,7 +132,7 @@ class BroadcastManager implements FactoryContract
/**
* Get a driver instance.
*
* @param string $driver
* @param string|null $driver
* @return mixed
*/
public function connection($driver = null)
@@ -165,7 +165,7 @@ class BroadcastManager implements FactoryContract
}
/**
* Resolve the given store.
* Resolve the given broadcaster.
*
* @param string $name
* @return \Illuminate\Contracts\Broadcasting\Broadcaster
@@ -176,10 +176,6 @@ class BroadcastManager implements FactoryContract
{
$config = $this->getConfig($name);
if (is_null($config)) {
throw new InvalidArgumentException("Broadcaster [{$name}] is not defined.");
}
if (isset($this->customCreators[$config['driver']])) {
return $this->callCustomCreator($config);
}
@@ -212,10 +208,16 @@ class BroadcastManager implements FactoryContract
*/
protected function createPusherDriver(array $config)
{
return new PusherBroadcaster(
new Pusher($config['key'], $config['secret'],
$config['app_id'], $config['options'] ?? [])
$pusher = new Pusher(
$config['key'], $config['secret'],
$config['app_id'], $config['options'] ?? []
);
if ($config['log'] ?? false) {
$pusher->setLogger($this->app->make(LoggerInterface::class));
}
return new PusherBroadcaster($pusher);
}
/**
@@ -263,7 +265,11 @@ class BroadcastManager implements FactoryContract
*/
protected function getConfig($name)
{
return $this->app['config']["broadcasting.connections.{$name}"];
if (! is_null($name) && $name !== 'null') {
return $this->app['config']["broadcasting.connections.{$name}"];
}
return ['driver' => 'null'];
}
/**

View File

@@ -3,18 +3,12 @@
namespace Illuminate\Broadcasting;
use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Contracts\Broadcasting\Factory as BroadcastingFactory;
use Illuminate\Contracts\Broadcasting\Broadcaster as BroadcasterContract;
class BroadcastServiceProvider extends ServiceProvider
class BroadcastServiceProvider extends ServiceProvider implements DeferrableProvider
{
/**
* Indicates if loading of the provider is deferred.
*
* @var bool
*/
protected $defer = true;
/**
* Register the service provider.
*

View File

@@ -5,6 +5,7 @@ namespace Illuminate\Broadcasting\Broadcasters;
use Exception;
use ReflectionClass;
use ReflectionFunction;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Container\Container;
use Illuminate\Contracts\Routing\UrlRoutable;
@@ -21,10 +22,17 @@ abstract class Broadcaster implements BroadcasterContract
*/
protected $channels = [];
/**
* The registered channel options.
*
* @var array
*/
protected $channelOptions = [];
/**
* The binding registrar instance.
*
* @var BindingRegistrar
* @var \Illuminate\Contracts\Routing\BindingRegistrar
*/
protected $bindingRegistrar;
@@ -33,12 +41,15 @@ abstract class Broadcaster implements BroadcasterContract
*
* @param string $channel
* @param callable|string $callback
* @param array $options
* @return $this
*/
public function channel($channel, $callback)
public function channel($channel, $callback, $options = [])
{
$this->channels[$channel] = $callback;
$this->channelOptions[$channel] = $options;
return $this;
}
@@ -48,12 +59,13 @@ abstract class Broadcaster implements BroadcasterContract
* @param \Illuminate\Http\Request $request
* @param string $channel
* @return mixed
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/
protected function verifyUserCanAccessChannel($request, $channel)
{
foreach ($this->channels as $pattern => $callback) {
if (! Str::is(preg_replace('/\{(.*?)\}/', '*', $pattern), $channel)) {
if (! $this->channelNameMatchesPattern($channel, $pattern)) {
continue;
}
@@ -61,7 +73,7 @@ abstract class Broadcaster implements BroadcasterContract
$handler = $this->normalizeChannelHandlerToCallable($callback);
if ($result = $handler($request->user(), ...$parameters)) {
if ($result = $handler($this->retrieveUser($request, $channel), ...$parameters)) {
return $this->validAuthenticationResponse($request, $result);
}
}
@@ -93,6 +105,7 @@ abstract class Broadcaster implements BroadcasterContract
*
* @param callable|string $callback
* @return \ReflectionParameter[]
*
* @throws \Exception
*/
protected function extractParameters($callback)
@@ -111,6 +124,7 @@ abstract class Broadcaster implements BroadcasterContract
*
* @param string $callback
* @return \ReflectionParameter[]
*
* @throws \Exception
*/
protected function extractParametersFromClass($callback)
@@ -180,6 +194,7 @@ abstract class Broadcaster implements BroadcasterContract
* @param mixed $value
* @param array $callbackParameters
* @return mixed
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/
protected function resolveImplicitBindingIfPossible($key, $value, $callbackParameters)
@@ -256,4 +271,59 @@ abstract class Broadcaster implements BroadcasterContract
->join(...$args);
};
}
/**
* Retrieve the authenticated user using the configured guard (if any).
*
* @param \Illuminate\Http\Request $request
* @param string $channel
* @return mixed
*/
protected function retrieveUser($request, $channel)
{
$options = $this->retrieveChannelOptions($channel);
$guards = $options['guards'] ?? null;
if (is_null($guards)) {
return $request->user();
}
foreach (Arr::wrap($guards) as $guard) {
if ($user = $request->user($guard)) {
return $user;
}
}
}
/**
* Retrieve options for a certain channel.
*
* @param string $channel
* @return array
*/
protected function retrieveChannelOptions($channel)
{
foreach ($this->channelOptions as $pattern => $options) {
if (! $this->channelNameMatchesPattern($channel, $pattern)) {
continue;
}
return $options;
}
return [];
}
/**
* Check if channel name from request match a pattern from registered channels.
*
* @param string $channel
* @param string $pattern
* @return bool
*/
protected function channelNameMatchesPattern($channel, $pattern)
{
return Str::is(preg_replace('/\{(.*?)\}/', '*', $pattern), $channel);
}
}

View File

@@ -10,6 +10,8 @@ use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class PusherBroadcaster extends Broadcaster
{
use UsePusherChannelConventions;
/**
* The Pusher SDK instance.
*
@@ -33,19 +35,18 @@ class PusherBroadcaster extends Broadcaster
*
* @param \Illuminate\Http\Request $request
* @return mixed
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/
public function auth($request)
{
if (Str::startsWith($request->channel_name, ['private-', 'presence-']) &&
! $request->user()) {
$channelName = $this->normalizeChannelName($request->channel_name);
if ($this->isGuardedChannel($request->channel_name) &&
! $this->retrieveUser($request, $channelName)) {
throw new AccessDeniedHttpException;
}
$channelName = Str::startsWith($request->channel_name, 'private-')
? Str::replaceFirst('private-', '', $request->channel_name)
: Str::replaceFirst('presence-', '', $request->channel_name);
return parent::verifyUserCanAccessChannel(
$request, $channelName
);
@@ -66,11 +67,13 @@ class PusherBroadcaster extends Broadcaster
);
}
$channelName = $this->normalizeChannelName($request->channel_name);
return $this->decodePusherResponse(
$request,
$this->pusher->presence_auth(
$request->channel_name, $request->socket_id,
$request->user()->getAuthIdentifier(), $result
$this->retrieveUser($request, $channelName)->getAuthIdentifier(), $result
)
);
}

View File

@@ -3,12 +3,13 @@
namespace Illuminate\Broadcasting\Broadcasters;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Contracts\Redis\Factory as Redis;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class RedisBroadcaster extends Broadcaster
{
use UsePusherChannelConventions;
/**
* The Redis instance.
*
@@ -41,19 +42,18 @@ class RedisBroadcaster extends Broadcaster
*
* @param \Illuminate\Http\Request $request
* @return mixed
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/
public function auth($request)
{
if (Str::startsWith($request->channel_name, ['private-', 'presence-']) &&
! $request->user()) {
$channelName = $this->normalizeChannelName($request->channel_name);
if ($this->isGuardedChannel($request->channel_name) &&
! $this->retrieveUser($request, $channelName)) {
throw new AccessDeniedHttpException;
}
$channelName = Str::startsWith($request->channel_name, 'private-')
? Str::replaceFirst('private-', '', $request->channel_name)
: Str::replaceFirst('presence-', '', $request->channel_name);
return parent::verifyUserCanAccessChannel(
$request, $channelName
);
@@ -72,8 +72,10 @@ class RedisBroadcaster extends Broadcaster
return json_encode($result);
}
$channelName = $this->normalizeChannelName($request->channel_name);
return json_encode(['channel_data' => [
'user_id' => $request->user()->getAuthIdentifier(),
'user_id' => $this->retrieveUser($request, $channelName)->getAuthIdentifier(),
'user_info' => $result,
]]);
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Illuminate\Broadcasting\Broadcasters;
use Illuminate\Support\Str;
trait UsePusherChannelConventions
{
/**
* Return true if channel is protected by authentication.
*
* @param string $channel
* @return bool
*/
public function isGuardedChannel($channel)
{
return Str::startsWith($channel, ['private-', 'presence-']);
}
/**
* Remove prefix from channel name.
*
* @param string $channel
* @return string
*/
public function normalizeChannelName($channel)
{
if ($this->isGuardedChannel($channel)) {
return Str::startsWith($channel, 'private-')
? Str::replaceFirst('private-', '', $channel)
: Str::replaceFirst('presence-', '', $channel);
}
return $channel;
}
}

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Taylor Otwell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -15,11 +15,12 @@
],
"require": {
"php": "^7.1.3",
"psr/log": "~1.0",
"illuminate/bus": "5.6.*",
"illuminate/contracts": "5.6.*",
"illuminate/queue": "5.6.*",
"illuminate/support": "5.6.*"
"ext-json": "*",
"psr/log": "^1.0",
"illuminate/bus": "5.8.*",
"illuminate/contracts": "5.8.*",
"illuminate/queue": "5.8.*",
"illuminate/support": "5.8.*"
},
"autoload": {
"psr-4": {
@@ -28,11 +29,11 @@
},
"extra": {
"branch-alias": {
"dev-master": "5.6-dev"
"dev-master": "5.8-dev"
}
},
"suggest": {
"pusher/pusher-php-server": "Required to use the Pusher broadcast driver (~3.0)."
"pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^3.0)."
},
"config": {
"sort-packages": true

View File

@@ -3,19 +3,13 @@
namespace Illuminate\Bus;
use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Contracts\Bus\Dispatcher as DispatcherContract;
use Illuminate\Contracts\Queue\Factory as QueueFactoryContract;
use Illuminate\Contracts\Bus\QueueingDispatcher as QueueingDispatcherContract;
class BusServiceProvider extends ServiceProvider
class BusServiceProvider extends ServiceProvider implements DeferrableProvider
{
/**
* Indicates if loading of the provider is deferred.
*
* @var bool
*/
protected $defer = true;
/**
* Register the service provider.
*

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Taylor Otwell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -15,9 +15,9 @@
],
"require": {
"php": "^7.1.3",
"illuminate/contracts": "5.6.*",
"illuminate/pipeline": "5.6.*",
"illuminate/support": "5.6.*"
"illuminate/contracts": "5.8.*",
"illuminate/pipeline": "5.8.*",
"illuminate/support": "5.8.*"
},
"autoload": {
"psr-4": {
@@ -26,7 +26,7 @@
},
"extra": {
"branch-alias": {
"dev-master": "5.6-dev"
"dev-master": "5.8-dev"
}
},
"config": {

View File

@@ -2,9 +2,7 @@
namespace Illuminate\Cache;
use Illuminate\Contracts\Cache\Store;
class ApcStore extends TaggableStore implements Store
class ApcStore extends TaggableStore
{
use RetrievesMultipleKeys;
@@ -51,16 +49,16 @@ class ApcStore extends TaggableStore implements Store
}
/**
* Store an item in the cache for a given number of minutes.
* Store an item in the cache for a given number of seconds.
*
* @param string $key
* @param mixed $value
* @param float|int $minutes
* @return void
* @param int $seconds
* @return bool
*/
public function put($key, $value, $minutes)
public function put($key, $value, $seconds)
{
$this->apc->put($this->prefix.$key, $value, (int) ($minutes * 60));
return $this->apc->put($this->prefix.$key, $value, $seconds);
}
/**
@@ -92,11 +90,11 @@ class ApcStore extends TaggableStore implements Store
*
* @param string $key
* @param mixed $value
* @return void
* @return bool
*/
public function forever($key, $value)
{
$this->put($key, $value, 0);
return $this->put($key, $value, 0);
}
/**

View File

@@ -2,11 +2,11 @@
namespace Illuminate\Cache;
use Illuminate\Contracts\Cache\Store;
use Illuminate\Support\InteractsWithTime;
class ArrayStore extends TaggableStore implements Store
class ArrayStore extends TaggableStore
{
use RetrievesMultipleKeys;
use InteractsWithTime, RetrievesMultipleKeys;
/**
* The array of stored values.
@@ -23,20 +23,39 @@ class ArrayStore extends TaggableStore implements Store
*/
public function get($key)
{
return $this->storage[$key] ?? null;
if (! isset($this->storage[$key])) {
return;
}
$item = $this->storage[$key];
$expiresAt = $item['expiresAt'] ?? 0;
if ($expiresAt !== 0 && $this->currentTime() > $expiresAt) {
$this->forget($key);
return;
}
return $item['value'];
}
/**
* Store an item in the cache for a given number of minutes.
* Store an item in the cache for a given number of seconds.
*
* @param string $key
* @param mixed $value
* @param float|int $minutes
* @return void
* @param int $seconds
* @return bool
*/
public function put($key, $value, $minutes)
public function put($key, $value, $seconds)
{
$this->storage[$key] = $value;
$this->storage[$key] = [
'value' => $value,
'expiresAt' => $this->calculateExpiration($seconds),
];
return true;
}
/**
@@ -48,10 +67,15 @@ class ArrayStore extends TaggableStore implements Store
*/
public function increment($key, $value = 1)
{
$this->storage[$key] = ! isset($this->storage[$key])
? $value : ((int) $this->storage[$key]) + $value;
if (! isset($this->storage[$key])) {
$this->forever($key, $value);
return $this->storage[$key];
return $this->storage[$key]['value'];
}
$this->storage[$key]['value'] = ((int) $this->storage[$key]['value']) + $value;
return $this->storage[$key]['value'];
}
/**
@@ -71,11 +95,11 @@ class ArrayStore extends TaggableStore implements Store
*
* @param string $key
* @param mixed $value
* @return void
* @return bool
*/
public function forever($key, $value)
{
$this->put($key, $value, 0);
return $this->put($key, $value, 0);
}
/**
@@ -86,9 +110,13 @@ class ArrayStore extends TaggableStore implements Store
*/
public function forget($key)
{
unset($this->storage[$key]);
if (array_key_exists($key, $this->storage)) {
unset($this->storage[$key]);
return true;
return true;
}
return false;
}
/**
@@ -112,4 +140,26 @@ class ArrayStore extends TaggableStore implements Store
{
return '';
}
/**
* Get the expiration time of the key.
*
* @param int $seconds
* @return int
*/
protected function calculateExpiration($seconds)
{
return $this->toTimestamp($seconds);
}
/**
* Get the UNIX timestamp for the given number of seconds.
*
* @param int $seconds
* @return int
*/
protected function toTimestamp($seconds)
{
return $seconds > 0 ? $this->availableAt($seconds) : 0;
}
}

View File

@@ -3,7 +3,9 @@
namespace Illuminate\Cache;
use Closure;
use Illuminate\Support\Arr;
use InvalidArgumentException;
use Aws\DynamoDb\DynamoDbClient;
use Illuminate\Contracts\Cache\Store;
use Illuminate\Contracts\Cache\Factory as FactoryContract;
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
@@ -16,7 +18,7 @@ class CacheManager implements FactoryContract
/**
* The application instance.
*
* @var \Illuminate\Foundation\Application
* @var \Illuminate\Contracts\Foundation\Application
*/
protected $app;
@@ -37,7 +39,7 @@ class CacheManager implements FactoryContract
/**
* Create a new Cache manager instance.
*
* @param \Illuminate\Foundation\Application $app
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function __construct($app)
@@ -46,7 +48,7 @@ class CacheManager implements FactoryContract
}
/**
* Get a cache store instance by name.
* Get a cache store instance by name, wrapped in a repository.
*
* @param string|null $name
* @return \Illuminate\Contracts\Cache\Repository
@@ -62,7 +64,7 @@ class CacheManager implements FactoryContract
* Get a cache driver instance.
*
* @param string|null $driver
* @return mixed
* @return \Illuminate\Contracts\Cache\Repository
*/
public function driver($driver = null)
{
@@ -124,7 +126,7 @@ class CacheManager implements FactoryContract
* Create an instance of the APC cache driver.
*
* @param array $config
* @return \Illuminate\Cache\ApcStore
* @return \Illuminate\Cache\Repository
*/
protected function createApcDriver(array $config)
{
@@ -136,7 +138,7 @@ class CacheManager implements FactoryContract
/**
* Create an instance of the array cache driver.
*
* @return \Illuminate\Cache\ArrayStore
* @return \Illuminate\Cache\Repository
*/
protected function createArrayDriver()
{
@@ -147,7 +149,7 @@ class CacheManager implements FactoryContract
* Create an instance of the file cache driver.
*
* @param array $config
* @return \Illuminate\Cache\FileStore
* @return \Illuminate\Cache\Repository
*/
protected function createFileDriver(array $config)
{
@@ -158,7 +160,7 @@ class CacheManager implements FactoryContract
* Create an instance of the Memcached cache driver.
*
* @param array $config
* @return \Illuminate\Cache\MemcachedStore
* @return \Illuminate\Cache\Repository
*/
protected function createMemcachedDriver(array $config)
{
@@ -177,7 +179,7 @@ class CacheManager implements FactoryContract
/**
* Create an instance of the Null cache driver.
*
* @return \Illuminate\Cache\NullStore
* @return \Illuminate\Cache\Repository
*/
protected function createNullDriver()
{
@@ -188,7 +190,7 @@ class CacheManager implements FactoryContract
* Create an instance of the Redis cache driver.
*
* @param array $config
* @return \Illuminate\Cache\RedisStore
* @return \Illuminate\Cache\Repository
*/
protected function createRedisDriver(array $config)
{
@@ -203,7 +205,7 @@ class CacheManager implements FactoryContract
* Create an instance of the database cache driver.
*
* @param array $config
* @return \Illuminate\Cache\DatabaseStore
* @return \Illuminate\Cache\Repository
*/
protected function createDatabaseDriver(array $config)
{
@@ -216,6 +218,38 @@ class CacheManager implements FactoryContract
);
}
/**
* Create an instance of the DynamoDB cache driver.
*
* @param array $config
* @return \Illuminate\Cache\Repository
*/
protected function createDynamodbDriver(array $config)
{
$dynamoConfig = [
'region' => $config['region'],
'version' => 'latest',
'endpoint' => $config['endpoint'] ?? null,
];
if ($config['key'] && $config['secret']) {
$dynamoConfig['credentials'] = Arr::only(
$config, ['key', 'secret', 'token']
);
}
return $this->repository(
new DynamoDbStore(
new DynamoDbClient($dynamoConfig),
$config['table'],
$config['attributes']['key'] ?? 'key',
$config['attributes']['value'] ?? 'value',
$config['attributes']['expiration'] ?? 'expires_at',
$this->getPrefix($config)
)
);
}
/**
* Create a new cache repository with the given implementation.
*
@@ -278,10 +312,29 @@ class CacheManager implements FactoryContract
$this->app['config']['cache.default'] = $name;
}
/**
* Unset the given driver instances.
*
* @param array|string|null $name
* @return $this
*/
public function forgetDriver($name = null)
{
$name = $name ?? $this->getDefaultDriver();
foreach ((array) $name as $cacheName) {
if (isset($this->stores[$cacheName])) {
unset($this->stores[$cacheName]);
}
}
return $this;
}
/**
* Register a custom driver creator Closure.
*
* @param string $driver
* @param string $driver
* @param \Closure $callback
* @return $this
*/
@@ -296,7 +349,7 @@ class CacheManager implements FactoryContract
* Dynamically call the default driver instance.
*
* @param string $method
* @param array $parameters
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)

View File

@@ -3,16 +3,10 @@
namespace Illuminate\Cache;
use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Support\DeferrableProvider;
class CacheServiceProvider extends ServiceProvider
class CacheServiceProvider extends ServiceProvider implements DeferrableProvider
{
/**
* Indicates if loading of the provider is deferred.
*
* @var bool
*/
protected $defer = true;
/**
* Register the service provider.
*

View File

@@ -60,15 +60,19 @@ class ClearCommand extends Command
*/
public function handle()
{
$this->laravel['events']->fire(
$this->laravel['events']->dispatch(
'cache:clearing', [$this->argument('store'), $this->tags()]
);
$this->cache()->flush();
$successful = $this->cache()->flush();
$this->flushFacades();
$this->laravel['events']->fire(
if (! $successful) {
return $this->error('Failed to clear cache. Make sure you have the appropriate permissions.');
}
$this->laravel['events']->dispatch(
'cache:cleared', [$this->argument('store'), $this->tags()]
);
@@ -123,7 +127,7 @@ class ClearCommand extends Command
protected function getArguments()
{
return [
['store', InputArgument::OPTIONAL, 'The name of the store you would like to clear.'],
['store', InputArgument::OPTIONAL, 'The name of the store you would like to clear'],
];
}
@@ -135,7 +139,7 @@ class ClearCommand extends Command
protected function getOptions()
{
return [
['tags', null, InputOption::VALUE_OPTIONAL, 'The cache tags you would like to clear.', null],
['tags', null, InputOption::VALUE_OPTIONAL, 'The cache tags you would like to clear', null],
];
}
}

View File

@@ -4,8 +4,10 @@ namespace Illuminate\Cache;
use Closure;
use Exception;
use Illuminate\Support\Str;
use Illuminate\Contracts\Cache\Store;
use Illuminate\Support\InteractsWithTime;
use Illuminate\Database\PostgresConnection;
use Illuminate\Database\ConnectionInterface;
class DatabaseStore implements Store
@@ -78,29 +80,31 @@ class DatabaseStore implements Store
return;
}
return unserialize($cache->value);
return $this->unserialize($cache->value);
}
/**
* Store an item in the cache for a given number of minutes.
* Store an item in the cache for a given number of seconds.
*
* @param string $key
* @param mixed $value
* @param float|int $minutes
* @return void
* @param int $seconds
* @return bool
*/
public function put($key, $value, $minutes)
public function put($key, $value, $seconds)
{
$key = $this->prefix.$key;
$value = serialize($value);
$value = $this->serialize($value);
$expiration = $this->getTime() + (int) ($minutes * 60);
$expiration = $this->getTime() + $seconds;
try {
$this->table()->insert(compact('key', 'value', 'expiration'));
return $this->table()->insert(compact('key', 'value', 'expiration'));
} catch (Exception $e) {
$this->table()->where('key', $key)->update(compact('value', 'expiration'));
$result = $this->table()->where('key', $key)->update(compact('value', 'expiration'));
return $result > 0;
}
}
@@ -157,7 +161,7 @@ class DatabaseStore implements Store
$cache = is_array($cache) ? (object) $cache : $cache;
$current = unserialize($cache->value);
$current = $this->unserialize($cache->value);
// Here we'll call this callback function that was given to the function which
// is used to either increment or decrement the function. We use a callback
@@ -172,7 +176,7 @@ class DatabaseStore implements Store
// since database cache values are encrypted by default with secure storage
// that can't be easily read. We will return the new value after storing.
$this->table()->where('key', $prefixed)->update([
'value' => serialize($new),
'value' => $this->serialize($new),
]);
return $new;
@@ -194,11 +198,11 @@ class DatabaseStore implements Store
*
* @param string $key
* @param mixed $value
* @return void
* @return bool
*/
public function forever($key, $value)
{
$this->put($key, $value, 5256000);
return $this->put($key, $value, 315360000);
}
/**
@@ -221,7 +225,9 @@ class DatabaseStore implements Store
*/
public function flush()
{
return (bool) $this->table()->delete();
$this->table()->delete();
return true;
}
/**
@@ -253,4 +259,36 @@ class DatabaseStore implements Store
{
return $this->prefix;
}
/**
* Serialize the given value.
*
* @param mixed $value
* @return string
*/
protected function serialize($value)
{
$result = serialize($value);
if ($this->connection instanceof PostgresConnection && Str::contains($result, "\0")) {
$result = base64_encode($result);
}
return $result;
}
/**
* Unserialize the given value.
*
* @param string $value
* @return mixed
*/
protected function unserialize($value)
{
if ($this->connection instanceof PostgresConnection && ! Str::contains($value, [':', ';'])) {
$value = base64_decode($value);
}
return unserialize($value);
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Illuminate\Cache;
class DynamoDbLock extends Lock
{
/**
* The DynamoDB client instance.
*
* @var \Illuminate\Cache\DynamoDbStore
*/
protected $dynamo;
/**
* Create a new lock instance.
*
* @param \Illuminate\Cache\DynamoDbStore $dynamo
* @param string $name
* @param int $seconds
* @param string|null $owner
* @return void
*/
public function __construct(DynamoDbStore $dynamo, $name, $seconds, $owner = null)
{
parent::__construct($name, $seconds, $owner);
$this->dynamo = $dynamo;
}
/**
* Attempt to acquire the lock.
*
* @return bool
*/
public function acquire()
{
return $this->dynamo->add(
$this->name, $this->owner, $this->seconds
);
}
/**
* Release the lock.
*
* @return void
*/
public function release()
{
if ($this->isOwnedByCurrentProcess()) {
$this->dynamo->forget($this->name);
}
}
/**
* Release this lock in disregard of ownership.
*
* @return void
*/
public function forceRelease()
{
$this->dynamo->forget($this->name);
}
/**
* Returns the owner value written into the driver for this lock.
*
* @return mixed
*/
protected function getCurrentOwner()
{
return $this->dynamo->get($this->name);
}
}

View File

@@ -0,0 +1,525 @@
<?php
namespace Illuminate\Cache;
use RuntimeException;
use Illuminate\Support\Str;
use Illuminate\Support\Carbon;
use Aws\DynamoDb\DynamoDbClient;
use Illuminate\Contracts\Cache\Store;
use Illuminate\Support\InteractsWithTime;
use Illuminate\Contracts\Cache\LockProvider;
use Aws\DynamoDb\Exception\DynamoDbException;
class DynamoDbStore implements Store, LockProvider
{
use InteractsWithTime;
/**
* The DynamoDB client instance.
*
* @var \Aws\DynamoDb\DynamoDbClient
*/
protected $dynamo;
/**
* The table name.
*
* @var string
*/
protected $table;
/**
* The name of the attribute that should hold the key.
*
* @var string
*/
protected $keyAttribute;
/**
* The name of the attribute that should hold the value.
*
* @var string
*/
protected $valueAttribute;
/**
* The name of the attribute that should hold the expiration timestamp.
*
* @var string
*/
protected $expirationAttribute;
/**
* A string that should be prepended to keys.
*
* @var string
*/
protected $prefix;
/**
* Create a new store instance.
*
* @param \Aws\DynamoDb\DynamoDbClient $dynamo
* @param string $table
* @param string $keyAttribute
* @param string $valueAttribute
* @param string $expirationAttribute
* @param string $prefix
* @return void
*/
public function __construct(DynamoDbClient $dynamo,
$table,
$keyAttribute = 'key',
$valueAttribute = 'value',
$expirationAttribute = 'expires_at',
$prefix = '')
{
$this->table = $table;
$this->dynamo = $dynamo;
$this->keyAttribute = $keyAttribute;
$this->valueAttribute = $valueAttribute;
$this->expirationAttribute = $expirationAttribute;
$this->setPrefix($prefix);
}
/**
* Retrieve an item from the cache by key.
*
* @param string $key
* @return mixed
*/
public function get($key)
{
$response = $this->dynamo->getItem([
'TableName' => $this->table,
'ConsistentRead' => false,
'Key' => [
$this->keyAttribute => [
'S' => $this->prefix.$key,
],
],
]);
if (! isset($response['Item'])) {
return;
}
if ($this->isExpired($response['Item'])) {
return;
}
if (isset($response['Item'][$this->valueAttribute])) {
return $this->unserialize(
$response['Item'][$this->valueAttribute]['S'] ??
$response['Item'][$this->valueAttribute]['N'] ??
null
);
}
}
/**
* Retrieve multiple items from the cache by key.
*
* Items not found in the cache will have a null value.
*
* @param array $keys
* @return array
*/
public function many(array $keys)
{
$prefixedKeys = array_map(function ($key) {
return $this->prefix.$key;
}, $keys);
$response = $this->dynamo->batchGetItem([
'RequestItems' => [
$this->table => [
'ConsistentRead' => false,
'Keys' => collect($prefixedKeys)->map(function ($key) {
return [
$this->keyAttribute => [
'S' => $key,
],
];
})->all(),
],
],
]);
$now = Carbon::now();
return array_merge(collect(array_flip($keys))->map(function () {
})->all(), collect($response['Responses'][$this->table])->mapWithKeys(function ($response) use ($now) {
if ($this->isExpired($response, $now)) {
$value = null;
} else {
$value = $this->unserialize(
$response[$this->valueAttribute]['S'] ??
$response[$this->valueAttribute]['N'] ??
null
);
}
return [Str::replaceFirst($this->prefix, '', $response[$this->keyAttribute]['S']) => $value];
})->all());
}
/**
* Determine if the given item is expired.
*
* @param array $item
* @param \DateTimeInterface|null $expiration
* @return bool
*/
protected function isExpired(array $item, $expiration = null)
{
$expiration = $expiration ?: Carbon::now();
return isset($item[$this->expirationAttribute]) &&
$expiration->getTimestamp() >= $item[$this->expirationAttribute]['N'];
}
/**
* Store an item in the cache for a given number of seconds.
*
* @param string $key
* @param mixed $value
* @param int $seconds
* @return bool
*/
public function put($key, $value, $seconds)
{
$this->dynamo->putItem([
'TableName' => $this->table,
'Item' => [
$this->keyAttribute => [
'S' => $this->prefix.$key,
],
$this->valueAttribute => [
$this->type($value) => $this->serialize($value),
],
$this->expirationAttribute => [
'N' => (string) $this->toTimestamp($seconds),
],
],
]);
return true;
}
/**
* Store multiple items in the cache for a given number of $seconds.
*
* @param array $values
* @param int $seconds
* @return bool
*/
public function putMany(array $values, $seconds)
{
$expiration = $this->toTimestamp($seconds);
$this->dynamo->batchWriteItem([
'RequestItems' => [
$this->table => collect($values)->map(function ($value, $key) use ($expiration) {
return [
'PutRequest' => [
'Item' => [
$this->keyAttribute => [
'S' => $this->prefix.$key,
],
$this->valueAttribute => [
$this->type($value) => $this->serialize($value),
],
$this->expirationAttribute => [
'N' => (string) $expiration,
],
],
],
];
})->values()->all(),
],
]);
return true;
}
/**
* Store an item in the cache if the key doesn't exist.
*
* @param string $key
* @param mixed $value
* @param int $seconds
* @return bool
*/
public function add($key, $value, $seconds)
{
try {
$this->dynamo->putItem([
'TableName' => $this->table,
'Item' => [
$this->keyAttribute => [
'S' => $this->prefix.$key,
],
$this->valueAttribute => [
$this->type($value) => $this->serialize($value),
],
$this->expirationAttribute => [
'N' => (string) $this->toTimestamp($seconds),
],
],
'ConditionExpression' => 'attribute_not_exists(#key) OR #expires_at < :now',
'ExpressionAttributeNames' => [
'#key' => $this->keyAttribute,
'#expires_at' => $this->expirationAttribute,
],
'ExpressionAttributeValues' => [
':now' => [
'N' => (string) Carbon::now()->getTimestamp(),
],
],
]);
return true;
} catch (DynamoDbException $e) {
if (Str::contains($e->getMessage(), 'ConditionalCheckFailed')) {
return false;
}
throw $e;
}
}
/**
* Increment the value of an item in the cache.
*
* @param string $key
* @param mixed $value
* @return int|bool
*/
public function increment($key, $value = 1)
{
try {
$response = $this->dynamo->updateItem([
'TableName' => $this->table,
'Key' => [
$this->keyAttribute => [
'S' => $this->prefix.$key,
],
],
'ConditionExpression' => 'attribute_exists(#key) AND #expires_at > :now',
'UpdateExpression' => 'SET #value = #value + :amount',
'ExpressionAttributeNames' => [
'#key' => $this->keyAttribute,
'#value' => $this->valueAttribute,
'#expires_at' => $this->expirationAttribute,
],
'ExpressionAttributeValues' => [
':now' => [
'N' => (string) Carbon::now()->getTimestamp(),
],
':amount' => [
'N' => (string) $value,
],
],
'ReturnValues' => 'UPDATED_NEW',
]);
return (int) $response['Attributes'][$this->valueAttribute]['N'];
} catch (DynamoDbException $e) {
if (Str::contains($e->getMessage(), 'ConditionalCheckFailed')) {
return false;
}
throw $e;
}
}
/**
* Decrement the value of an item in the cache.
*
* @param string $key
* @param mixed $value
* @return int|bool
*/
public function decrement($key, $value = 1)
{
try {
$response = $this->dynamo->updateItem([
'TableName' => $this->table,
'Key' => [
$this->keyAttribute => [
'S' => $this->prefix.$key,
],
],
'ConditionExpression' => 'attribute_exists(#key) AND #expires_at > :now',
'UpdateExpression' => 'SET #value = #value - :amount',
'ExpressionAttributeNames' => [
'#key' => $this->keyAttribute,
'#value' => $this->valueAttribute,
'#expires_at' => $this->expirationAttribute,
],
'ExpressionAttributeValues' => [
':now' => [
'N' => (string) Carbon::now()->getTimestamp(),
],
':amount' => [
'N' => (string) $value,
],
],
'ReturnValues' => 'UPDATED_NEW',
]);
return (int) $response['Attributes'][$this->valueAttribute]['N'];
} catch (DynamoDbException $e) {
if (Str::contains($e->getMessage(), 'ConditionalCheckFailed')) {
return false;
}
throw $e;
}
}
/**
* Store an item in the cache indefinitely.
*
* @param string $key
* @param mixed $value
* @return bool
*/
public function forever($key, $value)
{
return $this->put($key, $value, now()->addYears(5)->getTimestamp());
}
/**
* Get a lock instance.
*
* @param string $name
* @param int $seconds
* @param string|null $owner
* @return \Illuminate\Contracts\Cache\Lock
*/
public function lock($name, $seconds = 0, $owner = null)
{
return new DynamoDbLock($this, $this->prefix.$name, $seconds, $owner);
}
/**
* Restore a lock instance using the owner identifier.
*
* @param string $name
* @param string $owner
* @return \Illuminate\Contracts\Cache\Lock
*/
public function restoreLock($name, $owner)
{
return $this->lock($name, 0, $owner);
}
/**
* Remove an item from the cache.
*
* @param string $key
* @return bool
*/
public function forget($key)
{
$this->dynamo->deleteItem([
'TableName' => $this->table,
'Key' => [
$this->keyAttribute => [
'S' => $this->prefix.$key,
],
],
]);
return true;
}
/**
* Remove all items from the cache.
*
* @return bool
*/
public function flush()
{
throw new RuntimeException('DynamoDb does not support flushing an entire table. Please create a new table.');
}
/**
* Get the UNIX timestamp for the given number of seconds.
*
* @param int $seconds
* @return int
*/
protected function toTimestamp($seconds)
{
return $seconds > 0
? $this->availableAt($seconds)
: Carbon::now()->getTimestamp();
}
/**
* Serialize the value.
*
* @param mixed $value
* @return mixed
*/
protected function serialize($value)
{
return is_numeric($value) ? (string) $value : serialize($value);
}
/**
* Unserialize the value.
*
* @param mixed $value
* @return mixed
*/
protected function unserialize($value)
{
if (filter_var($value, FILTER_VALIDATE_INT) !== false) {
return (int) $value;
}
if (is_numeric($value)) {
return (float) $value;
}
return unserialize($value);
}
/**
* Get the DynamoDB type for the given value.
*
* @param mixed $value
* @return string
*/
protected function type($value)
{
return is_numeric($value) ? 'N' : 'S';
}
/**
* Get the cache key prefix.
*
* @return string
*/
public function getPrefix()
{
return $this->prefix;
}
/**
* Set the cache key prefix.
*
* @param string $prefix
* @return void
*/
public function setPrefix($prefix)
{
$this->prefix = ! empty($prefix) ? $prefix.':' : '';
}
}

View File

@@ -12,26 +12,26 @@ class KeyWritten extends CacheEvent
public $value;
/**
* The number of minutes the key should be valid.
* The number of seconds the key should be valid.
*
* @var int
* @var int|null
*/
public $minutes;
public $seconds;
/**
* Create a new event instance.
*
* @param string $key
* @param mixed $value
* @param int $minutes
* @param int|null $seconds
* @param array $tags
* @return void
*/
public function __construct($key, $value, $minutes, $tags = [])
public function __construct($key, $value, $seconds = null, $tags = [])
{
parent::__construct($key, $tags);
$this->value = $value;
$this->minutes = $minutes;
$this->seconds = $seconds;
}
}

View File

@@ -50,20 +50,22 @@ class FileStore implements Store
}
/**
* Store an item in the cache for a given number of minutes.
* Store an item in the cache for a given number of seconds.
*
* @param string $key
* @param mixed $value
* @param float|int $minutes
* @return void
* @param int $seconds
* @return bool
*/
public function put($key, $value, $minutes)
public function put($key, $value, $seconds)
{
$this->ensureCacheDirectoryExists($path = $this->path($key));
$this->files->put(
$path, $this->expiration($minutes).serialize($value), true
$result = $this->files->put(
$path, $this->expiration($seconds).serialize($value), true
);
return $result !== false && $result > 0;
}
/**
@@ -112,11 +114,11 @@ class FileStore implements Store
*
* @param string $key
* @param mixed $value
* @return void
* @return bool
*/
public function forever($key, $value)
{
$this->put($key, $value, 0);
return $this->put($key, $value, 0);
}
/**
@@ -184,12 +186,18 @@ class FileStore implements Store
return $this->emptyPayload();
}
$data = unserialize(substr($contents, 10));
try {
$data = unserialize(substr($contents, 10));
} catch (Exception $e) {
$this->forget($key);
// Next, we'll extract the number of minutes that are remaining for a cache
return $this->emptyPayload();
}
// Next, we'll extract the number of seconds that are remaining for a cache
// so that we can properly retain the time for things like the increment
// operation that may be performed on this cache on a later operation.
$time = ($expire - $this->currentTime()) / 60;
$time = $expire - $this->currentTime();
return compact('data', 'time');
}
@@ -218,16 +226,16 @@ class FileStore implements Store
}
/**
* Get the expiration time based on the given minutes.
* Get the expiration time based on the given seconds.
*
* @param float|int $minutes
* @param int $seconds
* @return int
*/
protected function expiration($minutes)
protected function expiration($seconds)
{
$time = $this->availableAt((int) ($minutes * 60));
$time = $this->availableAt($seconds);
return $minutes === 0 || $time > 9999999999 ? 9999999999 : (int) $time;
return $seconds === 0 || $time > 9999999999 ? 9999999999 : $time;
}
/**

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Taylor Otwell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

Some files were not shown because too many files have changed in this diff Show More