diff --git a/.travis.yml b/.travis.yml index 3fa03ab9f..c4285a5f4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,3 @@ -sudo: false dist: xenial language: php @@ -6,7 +5,7 @@ services: - mysql - postgresql - memcached - - redis-server + - redis php: - 7.1 @@ -15,13 +14,13 @@ php: - 7.4 env: - matrix: + jobs: - DB=mysql - DB=sqlite - DB=pgsql - DB=memory -matrix: +jobs: fast_finish: true cache: diff --git a/CHANGELOG.md b/CHANGELOG.md index c3c52ffdc..767b7e973 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [v4.4.2] + +### Fixed +- Locale matcher can fail when client provided locale identifier has incorrect casing ([#1087]) +- Sprunje applyTransformations method not returning the $collections object ([#1068]) +- Old assets in `app/assets/browser_modules` not being deleted during install ([#1092]) +- Added `SKIP_PERMISSION_CHECK` env to skip check for local directories that needs to be write protected. This can be used for local production env testing. + ## [v4.4.1] ### Fixed @@ -948,8 +956,11 @@ See [http://learn.userfrosting.com/upgrading/40-to-41](Upgrading 4.0.x to 4.1.x [#1057]: https://github.com/userfrosting/UserFrosting/issues/1057 [#1061]: https://github.com/userfrosting/UserFrosting/issues/1061 [#1062]: https://github.com/userfrosting/UserFrosting/issues/1062 +[#1068]: https://github.com/userfrosting/UserFrosting/issues/1068 [#1073]: https://github.com/userfrosting/UserFrosting/issues/1073 [#1078]: https://github.com/userfrosting/UserFrosting/issues/1078 +[#1087]: https://github.com/userfrosting/UserFrosting/issues/1087 +[#1092]: https://github.com/userfrosting/UserFrosting/issues/1092 [v4.2.0]: https://github.com/userfrosting/UserFrosting/compare/v4.1.22...v4.2.0 [v4.2.1]: https://github.com/userfrosting/UserFrosting/compare/v4.2.0...v.4.2.1 @@ -960,3 +971,5 @@ See [http://learn.userfrosting.com/upgrading/40-to-41](Upgrading 4.0.x to 4.1.x [v4.3.2]: https://github.com/userfrosting/UserFrosting/compare/v4.3.1...v4.3.2 [v4.3.3]: https://github.com/userfrosting/UserFrosting/compare/v4.3.2...v4.3.3 [v4.4.0]: https://github.com/userfrosting/UserFrosting/compare/v4.3.3...v4.4.0 +[v4.4.1]: https://github.com/userfrosting/UserFrosting/compare/v4.4.0...v4.4.1 +[v4.4.2]: https://github.com/userfrosting/UserFrosting/compare/v4.4.1...v4.4.2 diff --git a/app/sprinkles/core/src/I18n/SiteLocale.php b/app/sprinkles/core/src/I18n/SiteLocale.php index 97b4447d5..71c61f11f 100644 --- a/app/sprinkles/core/src/I18n/SiteLocale.php +++ b/app/sprinkles/core/src/I18n/SiteLocale.php @@ -169,7 +169,10 @@ protected function getBrowserLocale(): ?string $identifier = trim(str_replace('-', '_', $parts[0])); // Ensure locale available - if (in_array(strtolower($identifier), array_map('strtolower', $availableLocales))) { + $localeIndex = array_search(strtolower($identifier), array_map('strtolower', $availableLocales)); + + if ($localeIndex !== false) { + $matchedLocale = $availableLocales[$localeIndex]; // Determine preference level (q=0.x), and add to $foundLocales // If no preference level, set as 1 @@ -181,7 +184,7 @@ protected function getBrowserLocale(): ?string } // Add to list, and format for UF's i18n. - $foundLocales[$identifier] = $preference; + $foundLocales[$matchedLocale] = $preference; } } } diff --git a/app/sprinkles/core/src/Sprunje/Sprunje.php b/app/sprinkles/core/src/Sprunje/Sprunje.php index 3d782d784..35391159d 100644 --- a/app/sprinkles/core/src/Sprunje/Sprunje.php +++ b/app/sprinkles/core/src/Sprunje/Sprunje.php @@ -11,6 +11,8 @@ namespace UserFrosting\Sprinkle\Core\Sprunje; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Arr; +use Illuminate\Support\Str; use League\Csv\Writer; use Psr\Http\Message\ResponseInterface as Response; use UserFrosting\Sprinkle\Core\Util\ClassMapper; @@ -48,7 +50,7 @@ abstract class Sprunje /** * Default HTTP request parameters. * - * @var array[string] + * @var array */ protected $options = [ 'sorts' => [], @@ -62,28 +64,28 @@ abstract class Sprunje /** * Fields to allow filtering upon. * - * @var array[string] + * @var string[] */ protected $filterable = []; /** * Fields to allow listing (enumeration) upon. * - * @var array[string] + * @var string[] */ protected $listable = []; /** * Fields to allow sorting upon. * - * @var array[string] + * @var string[] */ protected $sortable = []; /** * List of fields to exclude when processing an "_all" filter. * - * @var array[string] + * @var string[] */ protected $excludeForAll = []; @@ -227,7 +229,7 @@ public function getArray() /** * Run the query and build a CSV object by flattening the resulting collection. Ignores any pagination. * - * @return \SplTempFileObject + * @return Writer */ public function getCsv() { @@ -242,7 +244,7 @@ public function getCsv() $collection = collect($filteredQuery->get()); // Perform any additional transformations on the dataset - $this->applyTransformations($collection); + $collection = $this->applyTransformations($collection); $csv = Writer::createFromFileObject(new \SplTempFileObject()); @@ -250,7 +252,7 @@ public function getCsv() // Flatten collection while simultaneously building the column names from the union of each element's keys $collection->transform(function ($item, $key) use (&$columnNames) { - $item = array_dot($item->toArray()); + $item = Arr::dot($item->toArray()); foreach ($item as $itemKey => $itemValue) { if (!in_array($itemKey, $columnNames)) { $columnNames[] = $itemKey; @@ -311,7 +313,7 @@ public function getModels() $collection = collect($filteredQuery->get()); // Perform any additional transformations on the dataset - $this->applyTransformations($collection); + $collection = $this->applyTransformations($collection); return [$count, $countFiltered, $collection]; } @@ -319,7 +321,7 @@ public function getModels() /** * Get lists of values for specified fields in 'lists' option, calling a custom lister callback when appropriate. * - * @return array + * @return array */ public function getListable() { @@ -327,7 +329,7 @@ public function getListable() foreach ($this->listable as $name) { // Determine if a custom filter method has been defined - $methodName = 'list' . studly_case($name); + $methodName = 'list' . Str::studly($name); if (method_exists($this, $methodName)) { $result[$name] = $this->$methodName(); @@ -408,7 +410,7 @@ public function applySorts($query) } // Determine if a custom sort method has been defined - $methodName = 'sort' . studly_case($name); + $methodName = 'sort' . Str::studly($name); if (method_exists($this, $methodName)) { $this->$methodName($query, $direction); @@ -453,7 +455,7 @@ public function applyPagination($query) protected function filterAll($query, $value) { foreach ($this->filterable as $name) { - if (studly_case($name) != 'all' && !in_array($name, $this->excludeForAll)) { + if (Str::studly($name) != 'all' && !in_array($name, $this->excludeForAll)) { // Since we want to match _any_ of the fields, we wrap the field callback in a 'orWhere' callback $query->orWhere(function ($fieldQuery) use ($name, $value) { $this->buildFilterQuery($fieldQuery, $name, $value); @@ -475,7 +477,7 @@ protected function filterAll($query, $value) */ protected function buildFilterQuery($query, $name, $value) { - $methodName = 'filter' . studly_case($name); + $methodName = 'filter' . Str::studly($name); // Determine if a custom filter method has been defined if (method_exists($this, $methodName)) { @@ -512,9 +514,9 @@ protected function buildFilterDefaultFieldQuery($query, $name, $value) /** * Set any transformations you wish to apply to the collection, after the query is executed. * - * @param \Illuminate\Database\Eloquent\Collection $collection + * @param \Illuminate\Support\Collection $collection * - * @return \Illuminate\Database\Eloquent\Collection + * @return \Illuminate\Support\Collection */ protected function applyTransformations($collection) { @@ -534,7 +536,7 @@ abstract protected function baseQuery(); * * @param string $column * - * @return array + * @return array> */ protected function getColumnValues($column) { diff --git a/app/sprinkles/core/src/Twig/CoreExtension.php b/app/sprinkles/core/src/Twig/CoreExtension.php index f00ff390f..d7f18ae59 100755 --- a/app/sprinkles/core/src/Twig/CoreExtension.php +++ b/app/sprinkles/core/src/Twig/CoreExtension.php @@ -40,30 +40,21 @@ public function __construct(ContainerInterface $services) $this->services = $services; } - /** - * Get the name of this extension. - * - * @return string - */ - public function getName() - { - return 'userfrosting/core'; - } - /** * Adds Twig functions `getAlerts` and `translate`. * - * @return array[TwigFunction] + * @return TwigFunction[] */ public function getFunctions() { return [ // Add Twig function for fetching alerts new TwigFunction('getAlerts', function ($clear = true) { + $alerts = $this->services->alerts; if ($clear) { - return $this->services['alerts']->getAndClearMessages(); + return $alerts->getAndClearMessages(); } else { - return $this->services['alerts']->messages(); + return $alerts->messages(); } }), new TwigFunction('translate', function ($hook, $params = []) { @@ -77,7 +68,7 @@ public function getFunctions() /** * Adds Twig filters `unescape`. * - * @return array[TwigFilter] + * @return TwigFilter[] */ public function getFilters() { diff --git a/app/sprinkles/core/src/Util/CheckEnvironment.php b/app/sprinkles/core/src/Util/CheckEnvironment.php index 599c5fdaa..055fe4ec8 100644 --- a/app/sprinkles/core/src/Util/CheckEnvironment.php +++ b/app/sprinkles/core/src/Util/CheckEnvironment.php @@ -311,7 +311,7 @@ public function checkPermissions() $this->locator->findResource('session://') => true, ]; - if ($this->isProduction()) { + if ($this->isProduction() && !$this->skipPermissionsCheck()) { // Should be write-protected in production! $shouldBeWriteable = array_merge($shouldBeWriteable, [ \UserFrosting\SPRINKLES_DIR => false, @@ -384,4 +384,14 @@ public function isProduction() { return getenv('UF_MODE') == 'production'; } + + /** + * Determine whether or not directory that required write-protection in production mode should be checked. + * + * @return bool True if we should skip the check, false will proceed. + */ + public function skipPermissionsCheck() + { + return getenv('SKIP_PERMISSION_CHECK') ? true : false; + } } diff --git a/app/sprinkles/core/tests/Integration/I18n/SiteLocaleTest.php b/app/sprinkles/core/tests/Integration/I18n/SiteLocaleTest.php index 88c5779bf..15746ea7e 100644 --- a/app/sprinkles/core/tests/Integration/I18n/SiteLocaleTest.php +++ b/app/sprinkles/core/tests/Integration/I18n/SiteLocaleTest.php @@ -168,8 +168,35 @@ public function testGetLocaleIndentifierWithBrowserAndComplexLocale(): void $this->ci->config['site.locales.default'] = 'fr_FR'; $this->ci->request = $request; + + /** @var SiteLocale */ $service = $this->ci->locale; - $this->assertSame('en_US', $service->getLocaleIndentifier()); + + // Get locale + $locale = $service->getLocaleIndentifier(); + + // Assertions + $this->assertSame('en_US', $locale); + $this->assertTrue($service->isAvailable($locale)); + } + + public function testGetLocaleIndentifierWithBrowserAndComplexLocaleInLowerCase(): void + { + $request = m::mock(\Psr\Http\Message\ServerRequestInterface::class); + $request->shouldReceive('hasHeader')->with('Accept-Language')->once()->andReturn(true); + $request->shouldReceive('getHeaderLine')->with('Accept-Language')->once()->andReturn('en-us, en;q=0.9, fr;q=0.8, de;q=0.7, *;q=0.5'); + + $this->ci->config['site.locales.default'] = 'fr_FR'; + $this->ci->request = $request; + + /** @var SiteLocale */ + $service = $this->ci->locale; + + // Get locale + $locale = $service->getLocaleIndentifier(); + + $this->assertSame('en_US', $locale); + $this->assertTrue($service->isAvailable($locale)); } public function testGetLocaleIndentifierWithBrowserAndMultipleLocale(): void diff --git a/app/sprinkles/core/tests/Integration/Twig/CoreExtensionTest.php b/app/sprinkles/core/tests/Integration/Twig/CoreExtensionTest.php new file mode 100644 index 000000000..550b8cfb1 --- /dev/null +++ b/app/sprinkles/core/tests/Integration/Twig/CoreExtensionTest.php @@ -0,0 +1,82 @@ +ci->alerts = Mockery::mock(AlertStream::class)->shouldReceive('getAndClearMessages')->once()->andReturn([ + ['message' => 'foo'], + ['message' => 'bar'], + ])->getMock(); + + $result = $this->ci->view->fetchFromString('{% for alert in getAlerts() %}{{alert.message}}{% endfor %}'); + $this->assertSame('foobar', $result); + } + + public function testGetAlertsNoClear(): void + { + $this->ci->alerts = Mockery::mock(AlertStream::class)->shouldReceive('messages')->once()->andReturn([ + ['message' => 'foo'], + ['message' => 'bar'], + ])->getMock(); + + $result = $this->ci->view->fetchFromString('{% for alert in getAlerts(false) %}{{alert.message}}{% endfor %}'); + $this->assertSame('foobar', $result); + } + + /** + * @see https://github.com/userfrosting/UserFrosting/issues/1090 + */ + public function testTranslateFunction(): void + { + $result = $this->ci->view->fetchFromString('{{ translate("USER", 2) }}'); + $this->assertSame('Users', $result); + } + + public function testPhoneFilter(): void + { + $result = $this->ci->view->fetchFromString('{{ data|phone }}', ['data' => '5551234567']); + $this->assertSame('(555) 123-4567', $result); + } + + public function testUnescapeFilter(): void + { + $string = "I'll \"walk\" the dog now"; + $this->assertNotSame($string, $this->ci->view->fetchFromString("{{ foo }}", ['foo' => htmlentities($string)])); + $this->assertNotSame($string, $this->ci->view->fetchFromString("{{ foo|unescape }}", ['foo' => htmlentities($string)])); + $this->assertNotSame($string, $this->ci->view->fetchFromString("{{ foo|raw }}", ['foo' => htmlentities($string)])); + $this->assertSame($string, $this->ci->view->fetchFromString("{{ foo|unescape|raw }}", ['foo' => htmlentities($string)])); + } + + public function testCurrentLocaleGlobal(): void + { + $this->ci->locale = Mockery::mock(SiteLocale::class)->shouldReceive('getLocaleIndentifier')->once()->andReturn('zz-ZZ')->getMock(); + + $this->assertSame('zz-ZZ', $this->ci->view->fetchFromString("{{ currentLocale }}")); + } +} diff --git a/app/sprinkles/core/tests/Unit/Sprunje/SprunjeTest.php b/app/sprinkles/core/tests/Unit/Sprunje/SprunjeTest.php index d2a0c3678..7cf21e018 100644 --- a/app/sprinkles/core/tests/Unit/Sprunje/SprunjeTest.php +++ b/app/sprinkles/core/tests/Unit/Sprunje/SprunjeTest.php @@ -66,6 +66,25 @@ public function testSprunjeApplySortsDefault() $builder->shouldReceive('orderBy')->once()->with('species', 'asc'); $sprunje->applySorts($builder); } + + public function testSprunjeApplyTransformations(): void + { + $sprunje = new SprunjeStub([]); + + $builder = $sprunje->getQuery(); + $builder->shouldReceive('count')->andReturn(2); + $builder->shouldReceive('get')->andReturn([ + ['id' => '1', 'name' => 'Foo'], + ['id' => '2', 'name' => 'Bar'], + ]); + + $result = $sprunje->getModels(); + + $this->assertSame([ + ['id' => '1', 'name' => 'FooFoo'], + ['id' => '2', 'name' => 'BarBar'], + ], $result[2]->toArray()); + } } class SprunjeStub extends Sprunje @@ -93,6 +112,17 @@ protected function baseQuery() return $builder; } + + protected function applyTransformations($collection) + { + $collection = $collection->map(function ($item, $key) { + $item['name'] = $item['name'] . $item['name']; + + return $item; + }); + + return $collection; + } } class SprunjeTestModelStub extends Model diff --git a/build/tasks/assets-install.js b/build/tasks/assets-install.js index 092e103cc..85843a529 100644 --- a/build/tasks/assets-install.js +++ b/build/tasks/assets-install.js @@ -90,7 +90,7 @@ export async function assetsInstall() { // Browserify dependencies log.info("Compiling compatible node modules into UMD bundles with browserify"); - deleteSync(vendorAssetsDir + "browserify_modules/", { force: true }); + deleteSync(vendorAssetsDir + "browser_modules/", { force: true }); await browserifyDependencies({ dependencies: Object.keys(pkg.dependencies), inputDir: vendorAssetsDir + "node_modules/",