diff options
497 files changed, 56763 insertions, 1 deletions
@@ -1,5 +1,4 @@ assets/php/DataBaseConf.php -login/_meta/* login/app/.env login/composer.lock login/composer.phar diff --git a/login b/login deleted file mode 160000 -Subproject c78d17f4f439711af7a78c5ea9823a5ba7f995a diff --git a/login/.github/CONTRIBUTING.md b/login/.github/CONTRIBUTING.md new file mode 100755 index 0000000..99b92e0 --- /dev/null +++ b/login/.github/CONTRIBUTING.md @@ -0,0 +1,76 @@ +# Guidelines for Getting Help with UserFrosting + +**Before** you open a new issue or ask a question in chat, you **must** read these guidelines. If it is evident from your issue that you failed to research your question properly, your issue may be closed without being answered. + +## Troubleshooting + +There are a few common stumbling blocks that new users face when setting up UserFrosting for the first time. If you are new to the current version of UserFrosting, please first look at the [basic requirements and installation instructions](https://learn.userfrosting.com/basics/requirements/basic-stack). + +If you don't find what you're looking for in the troubleshooting page, then please check the [wiki](https://github.com/userfrosting/UserFrosting/wiki) and [existing issues](https://github.com/alexweissman/UserFrosting/issues?utf8=%E2%9C%93&q=is%3Aissue), both opened and closed. Your question may have already been asked and answered before! + +You can also search for help on Stack Overflow or in our [Forums](https://forums.userfrosting.com/). In addition to the tags for the components that UF builds upon, such as [Slim](http://stackoverflow.com/questions/tagged/slim), [Twig](http://stackoverflow.com/questions/tagged/twig), [Eloquent](http://stackoverflow.com/questions/tagged/eloquent), [jQuery Validate](http://stackoverflow.com/questions/tagged/jquery-validate), [Select2](http://stackoverflow.com/questions/tagged/jquery-select2), there is now a [UserFrosting tag](http://stackoverflow.com/questions/tagged/userfrosting) as well. + +There are also tags for the utilities upon which UserFrosting depends, such as [Composer](http://stackoverflow.com/questions/tagged/composer-php) and [Git](http://stackoverflow.com/questions/tagged/git). + +## Asking for Help + +In general, the Github issue tracker should only be used for bug reports and feature requests. If you're just having trouble getting something to work, you should ask on Stack Overflow or in our [Forums](https://forums.userfrosting.com/) instead. Tag your question with the `userfrosting` tag, and optionally with any tags specific to the relevant underlying technologies, such as `slim`, `twig`, `eloquent`, `composer`, etc. You should also mention the version of UserFrosting that you are using. + +After posting a question on Stack Overflow or in our [Forums](https://forums.userfrosting.com/), please [link to it in chat](https://chat.userfrosting.com). This will ensure that more people see it, and provide a place where we can discuss and help clarify your question. + +On Github, Chat, and Stack Overflow, please keep in mind the following: + +1. Remember that courtesy and proper grammar go a long way. Please take the time to craft a **precise, polite issue**. We will do our best to help, but remember that this is an open-source project - none of us are getting paid a salary to develop this project, or act as your personal support hotline :wink: + +2. Report any errors in detail. Vague issues like "it doesn't work when I do this" are not helpful. Show that you have put some effort into identifying the cause of the error. + +3. There are three main places where you may find error messages: + +- Backend (PHP-related) fatal errors: in your PHP error log. This is usually a file called `php_error_log` or something like that. In XAMPP, the default location of this file is `XAMPP/xamppfiles/logs/`. For other web hosting platforms, please consult the documentation or do a quick Google search (i.e. "where is the php error log in _____"). Some web hosts may provide a special interface for accessing the php error log, through ssh, cpanel, etc. Please ask them directly for help with this. + +- Non-fatal PHP errors will be logged in your UserFrosting error log. Look for your `app/logs/errors.log` file. + +- Frontend (Javascript-related) errors: in your browser's Javascript console. See [this guide](https://learn.userfrosting.com/background/client-side) to using your browser console. + +You should also try testing your code in a local development environment, to separate **code-related** issues from **server** issues. In general, we recommend that you install a local development server on your computer, rather than [testing your code directly on the production server](https://pbs.twimg.com/media/BxfENwpIYAAcHqQ.png). This means you can test your code directly on your own computer, making development faster and without the risk of exposing sensitive information to the public. We recommend installing [XAMPP](https://www.apachefriends.org) if you don't already have a local server set up. + +## Contributing to the Codebase + +We welcome your technical expertise! But first, please join us in [chat](https://chat.userfrosting.com) to discuss your proposed changes/fixes/enhancements before you get started. At least one member of our development team will usually be around. + +Please also be sure to read our [style guidelines](../STYLE-GUIDE.md). + +When it's time to integrate changes, our git flow more or less follows http://nvie.com/posts/a-successful-git-branching-model/. + +### Branches + +- `master`: The current release or release candidate. Always numbered as `major.minor.revision`, possibly with an `-alpha` or `-beta` extension as well. +- `develop`: During alpha/beta, contains major changes to a release candidate. After beta, contains breaking changes that will need to wait for the next version to be integrated. Always numbered as `major.minor.x`, possibly with an `-alpha` or `-beta` extension as well. + +### Changes + +#### Hotfixes + +Hotfixes should be created in a separate branch, and then merged into both **master** and **develop**. + +#### Features + +New features that introduce some breaking changes should be created in a separate branch. When they are ready, they can be merged into `develop`. + +### Releases + +After every release, the `master` branch (and possibly `develop`, for minor/major releases) should immediately be version-bumped. That way, new changes can be accumulated until the next release. + +When a new version is created, the version number need to be changed in `app/define.php`. `CHANGELOG.md` should also be updated and the associated tag should be created on Github. + +#### Alpha/beta releases + +During alpha/beta, a release candidate sits on the `master` branch. Minor improvements should be treated as hotfixes, while major changes should be treated as features. In alpha/beta, major changes can still be integrated into `master` from `develop`. However, this should bump the revision number instead of the minor/major number. + +## Building the API documentation + +To build the API documentation, install [ApiGen](http://www.apigen.org/) globally and then run: + +`apigen generate --source UserFrosting/app,userfrosting-assets/src,userfrosting-config/Config,userfrosting-fortress/Fortress,userfrosting-i18n/I18n,userfrosting-session/Session,userfrosting-support/Support --destination userfrosting-api --exclude *vendor*,*_meta* --template-theme "bootstrap"` + +from inside your dev directory. diff --git a/login/.github/ISSUE_TEMPLATE.md b/login/.github/ISSUE_TEMPLATE.md new file mode 100755 index 0000000..e46fc9f --- /dev/null +++ b/login/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,2 @@ +<!-- Love userfrosting? Please consider supporting our collective: +👉 https://opencollective.com/userfrosting/donate -->
\ No newline at end of file diff --git a/login/.travis.yml b/login/.travis.yml new file mode 100755 index 0000000..4b516f7 --- /dev/null +++ b/login/.travis.yml @@ -0,0 +1,36 @@ +sudo: false +dist: trusty +language: php + +services: + - mysql + - postgresql + +php: + - 5.6 + - 7 + - 7.1 + +env: + matrix: + - DB=mysql + - DB=sqlite + - DB=pgsql + +before_install: + # copy sprinkles.json + - cp app/sprinkles.example.json app/sprinkles.json + # set up db + - bash build/before_install.sh $DB + +before_script: + # install deps and UF + - composer install + - php bakery migrate + +script: + # run unit tests + - composer test + +after_failure: + - cat app/log/userfrosting.log diff --git a/login/CHANGELOG.md b/login/CHANGELOG.md new file mode 100755 index 0000000..44f7530 --- /dev/null +++ b/login/CHANGELOG.md @@ -0,0 +1,553 @@ +# Change Log + +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.1.17-alpha +- Lock `gulp-uf-bundle-assets` at v2.28.0 until Silic0nS0ldier/gulp-uf-bundle-assets#5 is resolved (see #859) +- Add missing getInfo methods for GroupController and RoleController (#837) + +## v4.1.16-alpha +- Fix for `merge` bundling rule (#660) +- Fix for undefined variable exception under strict mode in `ufAlerts` (#809) +- Fix for site cache reset upon login (#828) +- Changed global cache tag to proper prefix +- Fix broken alert message in registration process (#843) +- Add partial Turkish translation + +## v4.1.15-alpha +- Refactor `Password` into a instantiable `Hasher` class, service, and `Password` facade (#827) +- Change default hash cost back to 10 and fix legacy hash detection issue + +## v4.1.14-alpha +- Fix issue with scopes being applied twice in `Unique::getPaginatedQuery` (https://github.com/userfrosting/extend-user/issues/2) +- Update Bower dependencies in core Sprinkle +- Refactor the `Password` class to use `hash_equals` for legacy passwords (prevent timing-based attacks) and factor out the default cost (#814) +- Check if `require_email_verification` is set in `Authenticator` and sign-in page (#815) +- Factor out hardcoded `sprinkles.json` filename (partially addresses #813) +- Add Farsi translations (#816) +- `ufTable`: Make `tableElement` configurable, and check for existence (#824) +- Put AdminLTE menu toggle button back (Revert e8a26fb and part of a46205f) + +## v4.1.13-alpha +- `ufTable`: Implement `rowTemplate` for customizing how rows are rendered (#787) +- `ufTable`: Support for passing callbacks for column templates instead of Handlebars templates +- Remove explicit references to `id` columns (#803) +- Fix Docker +- Append full name to User API + +## v4.1.12-alpha +- Separate out user action column from user status column +- Improve table row menus in mobile views (#724) +- Hide side menu toggle button in desktop sizes +- Add chevron to user menu +- Change "remember me" text +- Improve table tool buttons +- Twig extensions now implement `Twig_Extension_GlobalsInterface` as required by https://twig.symfony.com/doc/2.x/advanced.html#id1 (#788) +- Display element based on permissions for group list/info pages +- Factor the admin user creation out of migrations and into its own Bakery command (See #778) +- Bakery `clear-cache` command now clears Twig and router cache (Fix #750) +- Add Russian translations +- Add Travis for automated test/build on push + +## v4.1.11-alpha +- Updated `composer/installers` dependency. +- Patch `composer.json` to fix `illuminate/*` dependencies at 5.4 for now + +## v4.1.10-alpha +- Add support for PHP7 runtime errors to be handled in the same way as Exceptions +- Implement NotFoundExceptionHandler and pass through all NotFoundExceptions to this handler. +- Implement `redirect.onAlreadyLoggedIn` service (fixes #680) +- Deprecate `determineRedirectOnLogin` and replace with `redirect.onLogin` service +- Fix some PSR-2 compliance issues + +## v4.1.9-alpha +- Fixes #780, and more efficient way to collect ids in Unique::getPaginatedQuery +- Show "user deleted" in activities table (#782) +- Patched version of `widget-sort2Hash.js` to prevent writing extraneous browser history entries (#712) +- Improve handling of fatal/parse errors + +## v4.1.8-alpha +- Normalize paths to always have a leading slash when comparing against the CSRF blacklist (#775) (possible breaking change for some environments - please see updated docs at https://learn.userfrosting.com/routes-and-controllers/client-input/csrf-guard#blacklisting-routes) +- Set `display_errors` to `true` for development configs (#762), move php settings into a common `php` subkey in config files +- `ShutdownHandler` no longer responsible for logging fatal errors +- Set up PHP config values in `Core.php` instead of inside the `config` service definition. +- Reimplement `Builder::exclude` to maintain a list of excluded columns, and then automatically update list of columns to fetch in `get()` +- Deprecate `Model::queryBuilder` and `Model::export` +- Update nginx config file from spdy to http2 +- Add Pagespeed block (commented out) to nginx config file +- Make fpm-php7.0 the default CGI nginx config file + +## v4.1.7-alpha +- Add the `withTernary` method to the `Unique` trait to allow loading nested child models on ternary relationships. +- Add skip, take, limit, offset and deprecate withLimit and withOffset in `Unique` trait +- Support for `withPivot` on `Unique` relationships (in tertiary models) +- Factor out common code from `PermissionUserSprunje` into `UserSprunje` +- Rework internals of `Sprunje` to make it more testable. Filters, sorts, and paginations are now applied to a clone of the original queriable object. Deprecated `getResults` and added `getArray` and `getModels`. Result keys can now be customized. +- Table of user permissions on user info page +- Simplify by combining `permission-users.html.twig` into options on `users.html.twig` +- Add Chinese translations +- Deprecate User::exists() (#771) +- Add 'password' to hidden fields for User model +- Replace hardcoded `Builder` with classMapper reference + +## v4.1.6-alpha +- Fix missing permission check when `uri_account_settings` is not in role (#768) +- Add `getLastRow` method and `transformDropdownSelection` option to `ufCollection` +- Fix missing slug for permissions in "manage permissions" dropdown +- Add "manage permissions" to role page menu +- Factor out custom relation methods into `HasRelationships` trait on `Model` +- Add `withoutGlobalScopes` to `Syncable::sync` +- Add option to use `forceCreate` in `Syncable::sync` +- Add option to use custom key in `Syncable::sync` +- Complete redesign of `BelongsToManyThrough` - possible BC for a few people, as you now need to load the "via" models explicitly using `withVia`. This fixes a lot of issues with `BelongsToManyThrough`. +- Deprecate `BelongsToManyConstrained` +- Add `MorphToManyUnique` +- Integration tests now use an in-memory sqlite database (`test_integration`) by default + +## v4.1.5-alpha +- Spanish language support (#770) +- Show current filter in select-menu filters (#744) +- Cursor styling for ufCopy +- Transition overlay for ufTables +- Minor fix to ufTable.cleanHash +- Correctly target pager container in Tablesorter options +- Add table of users to role pages +- Fix issue with links in permission users table + +## v4.1.4-alpha +- Permissions rows get duplicated when upgrading from 4.0 to 4.1 (fix #759) +- Fix migrate:rollback not running down the migration in the correct order +- Updated type in `composer.json` for default sprinkles +- Added missing french translations & more default validation messages +- Bump Fortress version (fix #766) +- Support SQLite in Bakery setup +- Fix for PostgreSQL charset in Bakery (#745) + +## v4.1.3-alpha +- Add Italian translations +- Add `data-priority` attributes to built-in tables (#752) +- Use `this.slug` to avoid conflict with helper names (#748) +- Add block `table_search` to `table-paginated.html.twig` + +## v4.1.2-alpha +- Remove call to setFilters that was causing problems with pagination (#688) +- Update German translations and factor out some hardcoded text (#725) +- Update French translations +- Update Arabic translations (#733, #734, #735) + +## v4.1.1-alpha +- Fixed missing template in ExceptionHandler and `notFoundHandler` +- Migration rollback will throw a warning if a class is not found instead of aborting +- Temporary fix for trouble with `npm install` in Windows (#742) + +## v4.1.0-alpha +- Switch from pagination "plugin" to "widget" for Tablesorter. Allows us to update to the latest version of TS (fix #688, #715) +- Implement `WhoopsRenderer` for pretty debug pages. See (#674) +- Refactor error handling. Move responsibility for displayErrorDetails to handlers, and factor our ErrorRenderers. Addresses (#702) +- Move `composer.json` to root directory to allow installing UF via composer create-project +- Move `sprinkles.json` to app directory to make it easier to find +- Eliminate the `root` theme Sprinkle. Custom styling for the root user is now handled with some Twig logic in the `admin` Sprinkle (#726) +- Rename bundle.config.json -> asset-bundles.json (#726) +- Reorganize assets (#726) +- Heavily reorganize templates (#726) +- Move request schema from `schema/` to `schema/requests/` (#726) +- Factor out "system" classes from core Sprinkle +- Refactor overall application lifecycle; move main lifecycle into UserFrosting\System\UserFrosting +- SprinkleManager now better focused on a single responsibility +- Sprinkle initializer classes now use events to hook into application lifecycle +- Support for allowing Sprinkles to register middleware (#617) +- Automatically load Sprinkle service providers (see #636) +- Get rid of "implicit loading" for core Sprinkle - core is now just an ordinary Sprinkle like any other. +- The `sprinkles://` stream now represents a virtual filesystem for the root directory of each loaded sprinkle, rather than the `sprinkles/` directory itself. +- Separate out `localePathBuilder` from the `translator` service. Makes it easier to add/remove paths before actually loading the translations. +- Only present locale options with non-null names. +- Rebased ufTable and ufModal with new jQuery plugin template. (part of #646) +- Removed the search bar from the Dashboard layout +- Added Tablesorter pagination translation +- New Translator Facade +- New CLI tool (Bakery). +- New migration system based on bakery CLI +- Listable sprunjing +- Refactor groups and user routes (Fix #721) +- Added the `config` alert stream to save ufAlerts to the cache instead of sessions. Fix #633. The old `session` is still the default alertStream in 4.1. +- Added support for the Redis cache driver and refactored the cache config values. +- Added user and session cache. +- Common log file for db queries, auth checks, smtp, errors, and debug messages (#709). +- Use YAML as default format for request schema (#690) + +See [http://learn.userfrosting.com/upgrading/40-to-41](Upgrading 4.0.x to 4.1.x documentation) for complete list of changes and breaking changes. + +## v4.0.24-alpha +- Fixes to nginx config file, and add location block for LE acme challenge +- Fix JS errors when `#alerts-page` is not present on a page +- Fix hardcoded User class in AdminController (#753) +- Update message PASSWORD.FORGET.REQUEST_SENT (#749) + +## v4.0.23-alpha +- Set module dependency versions to ~4.0.0 instead of ^4.0.0 (since 4.1.x will introduce breaking changes) +- Fix bug in ufCollection + +## v4.0.22-alpha +- Fix issue where 'Change User Password' popup form couldn't handle specifying a new password. +- Display message when there are no results in `ufTable` + +## v4.0.21-alpha +- Implement reflow and column selector for tables (#670) +- Overhauled ufAlerts, improving efficiency, reliability, and fixed a discovered edge case that caused `render` to never complete. (part of #646) +- ufAlerts will only auto-scroll when outside the viewport (even if only partially). Can be overriden with `scrollWhenVisible: true`. (#714) +- Rebased ufCollection, and ufForm with new jQuery plugin template. (part of #646) +- Misc UI update +- Added Twig blocks +- Fix issue with duplicate query logs when using multiple databases + +## v4.0.20-alpha +- Remove pivot columns from pagination subquery in BelongsToManyThrough, to deal with MySQL's `only_full_group_by` warning + +## v4.0.19-alpha +- Explicit column names in new user permissions relations + +## v4.0.18-alpha +- Permission info page (#638) +- New custom relationships 'BelongsToManyThrough', 'BelongsToManyUnique', 'BelongsToManyConstrained', 'HasManySyncable', 'MorphManySyncable' +- Change implementation of User::permissions() to use BelongsToManyThrough +- New ufForm options: setting reqParams, encType, submittingText +- ufCollection now triggers a check for virgin rows when _any_ control is touched +- Fix issue with Sprunje when generating CSV with empty child collections (#697) +- Authorizer now correctly interprets string literals (#482) +- Authorizer now correctly interprets numeric types in access conditions. **Caution**: this causes the `equals()` callback to return true in situations where it would have (incorrectly) returned false before. For example, `equals(self.group_id,2)` would have returned false for users in group 2, because it was interpreting `2` as a string and then performing its strict comparison. It now (correctly) returns true. Notice that `equals(self.group_id,'2')`, on the other hand, will now return `false`. +- User object caches permissions loaded from DB to reduce number of queries (#612) +- Type declarations in authorization classes (#652) +- Fix issue with Twig debug (#356) +- Show disabled/unactivated badges on user info page + +## v4.0.17-alpha +- Add IIS config file (#371) +- New ufCollection now supports free text input mode +- New design and layout for user, group, and role summary boxes (also fixes #703) +- Registration page returns 404 when registration disabled (#705) + +## v4.0.16-alpha +- Add Docker configuration files +- Begin work on Bakery, the command-line debug tool +- Lock version of tablesorter due to breaking changes +- Fix bugs in GroupController and RoleController +- Fix bug in URLs for redirect-on-login +- Added UTF-8 as default mail charset + +## v4.0.15-alpha +- Prevent mixed content on demo pages +- Fixed some missing translations +- Fixed error in ufAlerts push method +- Fixed usage of hard coded path +- Fixed default OS option in migration script +- Prevents empty locale's from displaying as empty options in profile form +- Unignore .gitkeeps of directories that need to exist + +## v4.0.14-alpha +- Fix ajax.delay in ufCollection +- Fix missing translations +- Minor fix in French translation +- Fix alert margin when displayed inside a modal + +## v4.0.13-alpha +- Update to RememberMe 2.0 (https://github.com/userfrosting/UserFrosting/issues/635) +- Remove database checks, as they are apparently no longer needed (https://github.com/userfrosting/UserFrosting/issues/655) +- Bump dependencies + +## v4.0.12-alpha +- Separate out the registration and sign-in pages (https://github.com/userfrosting/UserFrosting/issues/657) **BC** +- Slightly change behavior of form validation icons +- Sprunje input validation (https://github.com/userfrosting/UserFrosting/issues/640) +- Sprunje sort/filter fields now must be explicitly listed in a whitelist (https://github.com/userfrosting/UserFrosting/issues/640) **BC** +- Errors from tablesorter now get displayed +- Support for OR expressions using `||` in Sprunje filters (https://github.com/userfrosting/UserFrosting/issues/647) + +## v4.0.11-alpha +- Fix [#663](https://github.com/userfrosting/UserFrosting/issues/663) +- Adding more Twig `blocks` +- ufAlerts now scroll to alert location, if and only if alerts are output. +- Updated Dutch locale +- Minor update in French locale +- Added comments in `.env.example` + +## v4.0.10-alpha +- Move suggestion button outta-da-way +- Add email to registration success message +- Separate out some page content into smaller blocks +- Factor out select2 options in ufCollection, into the 'dropdown' key so that any select2 option can be set + +## v4.0.9-alpha +- Oops, `exists` needs to be static + +## v4.0.8-alpha +- Autogenerate and suggestion features for usernames during account registration (partially addresses https://github.com/userfrosting/UserFrosting/issues/569) +- Restrict username characters to a-z0-9.-_ +- Require first name by default +- Throttle registration attempts +- Implement User::exists method +- keyupDelay option in ufForm +- More logging of group and role CRUD +- Implement extra:// stream +- Lots of missing translation keys + +## v4.0.7-alpha +- Separate "profile settings" from "account settings" + +## v4.0.6-alpha +- Fix throttling issue #656 +- Other miscellaneous fixes + +## v4.0.5-alpha +- Allow nulling out of throttle rules (to disable) +- Disable Google Analytics by default (but enabled in production) +- Other miscellaneous fixes + +## v4.0.4-alpha +- UfAlert style customization (See [#634](https://github.com/userfrosting/UserFrosting/issues/634)) +- Translation function can now display raw placeholder using the `|raw` filter in the placeholder name. Other Twig filters are also avaiable. Requires latest version of the [i18n](https://github.com/userfrosting/i18n) component (See [#621](https://github.com/userfrosting/UserFrosting/issues/621)). +- Fix the "Root account" message breaking the UI on smaller screens (See [#641](https://github.com/userfrosting/UserFrosting/issues/641)) - Thanks @brunomnsilva ! +- Added `DB_DRIVER` and `DB_PORT` as environment variables to allow better out-of-box database configuration support, and to provide additional protection by obscurity. +- Normalised default values to `null` for environment variables in configuration. +- Added `getCallbacks` public method to `AuthorizationManager` to enable drop-in extensions to `AuthorizationManager`. +- Fixed broken links in generated asset bundles. +- Introduced `clean` gulp task to act as a shotcut for removing all frontend vendor packages, all generated asset bundles, and copied assets. Accessible via `npm run uf-clean`. +- Merged `copy` task with `bundle-build`. +- Fixed missing translations +- Added Thai translation - Thanks @popiazaza ! + +## v4.0.3-alpha +- Add config file for nginx (https://github.com/userfrosting/UserFrosting/issues/373) +- Add Portuguese translations (thanks to @brunomnsilva!) +- Add Arabic (MSA) translations (thanks to @abdullah.seba!) +- Add Dispatcher to db service to allow registering model events. +- Specify foreign keys explicitly in all relationships. +- Use classMapper for admin Sprunjes. + +## v4.0.2-alpha +- Specify foreign key explicitly in `User::activities()` relationship. +- Database checks in installer and Authenticator now respect custom database ports. (See [#628](https://github.com/userfrosting/UserFrosting/issues/628)) +- Fixed edge case where `5%C` would appear in generated urls. +- Improved stability and added php version check in `migrations/intall.php` +- Update ClassMapper to throw exception when class is not found +- Fix minor errors in French locale +- Fix translation error on the Legal page + +## v4.0.1-alpha +- Bump min version of PHP to 5.6 +- Added German translation (See [#625](https://github.com/userfrosting/UserFrosting/issues/625)) - Thanks @X-Anonymous-Y +- Improved Gulp Build task +- Remove site-dev from example sprinkles.json +- Fix some styling issues on the Dashboard and footer +- Display group link in menu for group admins +- Keep dashboard sidebar collapsed across page load (See [#616](https://github.com/userfrosting/UserFrosting/issues/616)) +- Fixed missing translation keys inside Handlebar tables (See [#624](https://github.com/userfrosting/UserFrosting/issues/624)) +- Admin panel link style in main dropdown menu (See [#627](https://github.com/userfrosting/UserFrosting/issues/627)) +- Implement AuthGuard middleware +- Handling of redirect after login (See [#627#issuecomment-275607492](https://github.com/userfrosting/UserFrosting/issues/627#issuecomment-275607492)) +- Directly check database in installer using PDO +- Refactor installer and how version are displayed in system info panel. Added notice when a migration is available for a sprinkle +- Etc. + +## v4.0.0-alpha + +**Initial release of UserFrosting V4** + +- The [Sprinkle](https://learn.userfrosting.com/sprinkles) system, which keeps your code completely separate from the core UF codebase; +- We're upgraded from Slim 2 to Slim 3, which is significantly different; +- Completely redesigned [database structure](https://learn.userfrosting.com/database/default-tables); +- Initialization is now achieved through [services](https://learn.userfrosting.com/services), with the Pimple dependency injection container; +- [Composer](https://learn.userfrosting.com/installation/requirements/essential-tools-for-php#composer) is now a mandatory part of the installation process; +- [Bower](https://learn.userfrosting.com/sprinkles/contents#-bower-json) is now used to install third-party client-side assets (Javascript/CSS packages); +- "Groups" and "Primary Group" have been replaced by "Roles" and "Group", respectively; +- Tables no longer need to be "registered" in any kind of initialization routine. Simply set the table names directly in your data models; +- Twig templates have been [reorganized and redesigned](https://learn.userfrosting.com/templating-with-twig/sprinkle-templates); +- SB Admin has been removed and we now use the [AdminLTE](https://adminlte.io/) front-end theme; +- Client-side code has been heavily refactored into reusable [components](https://learn.userfrosting.com/client-side-code/components). + +## v0.3.1.23 +- Also fix the `occurred_at` timestamp in the `user_event` table to allow null, for newer versions of MySQL that don't allow a zero date (see #605). + +## v0.3.1.22 +- Use `nullableTimestamps` instead of `timestamps` in installer, to prevent conflict with MySQL modes 'NO_ZERO_IN_DATE' and 'NO_ZERO_DATE'. + +## v0.3.1.21 +- Use Laravel's Schema interface to create tables and default rows, instead of constructing them with SQL + +## v0.3.1.20 +- Added `pushAlert()`,`clearAlerts()` in `public/js/userfrosting.js` and updated `flashAlerts()` +- Revert changes to User::fresh() but leave comment regarding upgrading Eloquent + +## v0.3.1.19 +- Fix some minor error screen layout issues +- Make User::fresh() compatible with Eloquent\Model v5.2.40+ +- Update composer require to allow for Fortress 1.x bugfixes +- Allow database port definitions in config-userfrosting.php +- Fix fatal error when evaluateCondition is called before the router populates current route information + +## v0.3.1.18 +- Add check for logging being enabled but log file not existing yet + +## v0.3.1.17 +- Fix occasional bug when end-of-file is reached on log file before requested number of lines is reached +- Roll back database connection checking to fix installer routines (frostbitten) +- UI fixes for smaller screens (frostbitten) +- Update Gitter references to Rocket.chat +- Clarify hotfix branch procedure for contributions + +## v0.3.1.16 +- Fix comment reference to \Fortress\JqueryValidationAdaptor +- CONTRIBUTING.md - Add note about proper Pull Requests +- French language file fixes (#565) (lcharette) +- Add HTTP status codes to 404 errors and database errors (frostbitten) +- Change database errors to use BaseController instead of DatabaseController (frostbitten) + +## v0.3.1.15 +- Fix unattached submitHandler bug in Group and Auth edit interfaces (#465) +- Remove references to nonexistent `formUserView` and `formGroupView` (#478) +- Gracefully handle session destruction due to missing or disabled accounts (#510) +- Add `attributeExists` and `relationExists` for models (#520) + +## v0.3.1.14 +- Stop reading entire log files to avoid out-of-memory errors (#497) +- Deploy [league/csv](https://github.com/thephpleague/csv) to properly generate CSV files (#557) +- Fix typos in language files + +## v0.3.1.13 +- Bump dependencies +- userfrosting/fortress now has a release version + +## v0.3.1.12 +- Add sendmail support in Notification class +- Fix problem with strict comparison in Handlebars templates and inconsistent data types among different database technologies +- Override paths to font files for Bootstrap Glyphicons to support the UserFrosting directory structure +- Added missing lines of Thai language (popiazaza) +- Fix a vulnerability where users still logged in wouldn't automatically be logged out if they were disabled +- Add option for HTTPS in `.htaccess`, commented out by default +- Minor syntax fixes in `public/js/userfrosting.js`, `widget-auth.js`, `widget-groups.js`, and `widget-users.js` + +## v0.3.1.11 +- Composer can now include composer.json files from plugin folders (added "wikimedia/composer-merge-plugin" to composer) + +## v0.3.1.10 +- Select correct versions (PHP 5.x compatible) of packages in `composer.json` +- Turkish language translation +- Return `User` object created in `AccountController::register` + +## v0.3.1.9 +- Revert to loose comparison for `user_id`s because of issues with Ubuntu's PDO driver (see http://stackoverflow.com/questions/5323146/mysql-integer-field-is-returned-as-string-in-php#comment41836471_5323169) + +## v0.3.1.8 +- Finish replacing all usages of `*Loader` classes with Eloquent syntax +- Installer warning for missing `imagepng` +- Fix bug in CSV generation for user table + +## v0.3.1.7 +- Change "default theme" to "guest theme" and fix loading issues (#463). What used to be called "default theme" is now base theme, i.e. the theme to fall back to when a template file cannot be found in the current theme (user group or guest theme) +- New public template for "nyx" theme +- Remove trailing slash from configuration JS/CSS paths to make uniform with site.uri.public +- Make routes for config.js and theme.css dynamically generated from configuration variables (#461) +- Make cookie name for "remember me" use session name +- Fix potential bug in configuration user_id's for guest, master accounts + +## v0.3.1.6 +- Fix exception-handling for mail server errors +- Notify if account creation was successful, even if mail server failed. + +## v0.3.1.5 +- Add Romanian translation +- Upgrade Tablesorter and pretty URLs for searched/sorted/paginated tables +- Fix bug in default value for user `secret_token` + +## v0.3.1.4 +- .htaccess redirect trailing slash: change to only redirect GET requests +- Natural sort order in API +- Fix bug in table pagination +- Fix bug in loading user primary group properties as user properties +- Fix mailto link bug in tables +- Warn if config file missing (#445) +- Fix dutch error (#447) + +## v0.3.1.3 +- Implement CSV download feature + +## v0.3.1.2 +- Implement `no_leading_whitespace` and `no_trailing_whitespace` rules + +## v0.3.1 +- Improved initialization routine as middleware +- Implemented "remember me" for persistent sessions - see https://github.com/gbirke/rememberme +- Converted page templates to inheritance architecture, using Twig `extends` +- Start using the `.twig` extension for template files +- All content is now part of a theme, and site can be configured so that one theme is the default theme for unauthenticated users +- User session stored via `user_id`, rather than the entire User object +- Data model is now built on Eloquent, instead of in-house +- Cleaned up some of the per-page Javascript, refactoring repetitive code +- Implement server-side pagination +- Upgrade to Tablesorter v2.23.4 +- Switch from DateJS to momentjs +- Switch to jQueryValidation from FormValidation +- Implement basic interface for modifying group authorization rules +- User events - timestamps for things like sign-in, sign-up, password reset, etc are now stored in a `user_event` table +- Wrapper class Notification for sending emails, other notifications to users +- Remove username requirement for password reset. It is more likely that an attacker would know the user's username, than the user themselves. For the next version, we can try to implement some real multi-factor authentication. +- When a user creates another user, they don't need to set a password. Instead, an email is sent out to the new user, with a token allowing them to set their own password. +- Admins can manually generate a password reset request for another user, or directly change the user's password. + +## v0.3.0 +- [Autoloading with Composer](https://v3.userfrosting.com/navigating/#composer) +- [MVC Architecture](https://v3.userfrosting.com/navigating/#structure) +- [Front Controllers and the Slim Microframework](https://v3.userfrosting.com/navigating/#slim) +- [Twig - Templating](http://twig.sensiolabs.org/) +- [Theming](https://v3.userfrosting.com/components/#theming) +- [Plugins](https://v3.userfrosting.com/components/#plugins) + +## v0.2.1 +- Implemented db-driven menu system. Menu items are pulled from the database, and can be modified via plugins. +- Implemented backend templating of forms and tables via [Bootsole](https://github.com/alexweissman/bootsole). + +## v0.2.0 (butterflyknife) +- Converted all DB calls to PDO. +- Renamed "permissions" to "groups". Same concept, but using the word "group" suggests that it can be used for more than just access control. +- Implemented "primary group" membership for users. A user can belong to multiple groups, but only one of those will be their primary group. +- Implemented DB-driven home pages for groups. Upon login, a user will be redirected to the `home_page` for their primary group. +- Implemented templated menus. Every group has a corresponding menu template in `models/menu-templates`. Upon login, the menu for a user's primary group is automatically loaded and rendered. +- Implemented function-level user authorization. Whenever a function in `secure_functions` is called, the `user_action_permits` table is checked to see whether or not that user has access to the function (the `action` column), conditional on the boolean functions specified in the `permits` column. +- Organized pages into four categories: account pages, API pages, form pages, and public pages. Public pages reside in the root directory and can be accessed by anyone. Account pages are in the `account` directory and are only accessible after logging in. API pages are in the `api` directory, and consist of all the pages that process or fetch data from the DB and interact with the frontend via AJAX/JSON. They are accessible by any logged in user, but will only perform a function if the user is authorized. Form pages are in the `forms` directory, and consist of pages that generate forms (for creating/updating users, groups, etc.) +- Converted registration page to AJAX. +- Improved installer with site configuration. + +## v0.1.7 +- Page scrolls back to top after AJAX submit. +- "Website url" is automatically suffixed with "/" if necessary. +- Fixed bad link to forgot_password.php. +- Began implementing action authorization scheme. + +## v0.1.6 +- Implemented CSRF token checking for creating and updating users +- Moved much of the nuts and bolts for generating the user-create and user-update forms to the server side, so as to streamline rendering process and require fewer requests by the client (see load_form_user.php) +- Improved responsive layout for rendering nicely on mobile devices + +## v0.1.5 +- More improvements to error-handling/rendering +- HTTPS/SSL compatible +- Fixed bug with different table name prefixes +- Improvements to CSRF tokens + +## v0.1.4 +- Updated password hashing from md5 to modern bcrypt (more secure) - thanks to contributor @r3wt +- Included better functions for sanitizing user input, validating user ip, generating csrf (cross-site request forgery) tokens - thanks to contributor @r3wt + +## v0.1.3 +- Root account (user id = 1) : created upon installation, cannot be deleted or disabled. +- Special color scheme for when logged in as root user. +- Installer now guides user through creation of root account +- Moved common JS and CSS includes to "includes.php" + +## v0.1.2 +- Improved error and exception handling +- Added 404 error page +- Standardized JSON interface for backend scripts +- Front-end should now be able to catch virtually any backend error and take an appropriate action (instead of white screen of death) diff --git a/login/LICENSE.md b/login/LICENSE.md new file mode 100755 index 0000000..476c3ca --- /dev/null +++ b/login/LICENSE.md @@ -0,0 +1,7 @@ +Copyright (c) 2017 by Alexander Weissman (https://alexanderweissman.com) + +UserFrosting is 100% free and open-source. + +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. diff --git a/login/README.md b/login/README.md new file mode 100755 index 0000000..d136dc8 --- /dev/null +++ b/login/README.md @@ -0,0 +1,124 @@ +# UserFrosting 4.1 + +[https://www.userfrosting.com](https://www.userfrosting.com) + +[](https://travis-ci.org/userfrosting/UserFrosting) +[](https://chat.userfrosting.com/channel/support) +[](#backers) [](#sponsors) + +<a href="https://opencollective.com/userfrosting#backer" target="_blank"> + <img src="https://opencollective.com/userfrosting/donate/button.png?color=blue" width=300 /> +</a> + +If you simply want to show that you like this project, or want to remember it for later, you should **star**, not **fork**, this repository. Forking is only for when you are ready to create your own copy of the code to work on. + +## By [Alex Weissman](https://alexanderweissman.com) + +Copyright (c) 2018, free to use in personal and commercial software as per the [license](LICENSE.md). + +UserFrosting is a secure, modern user management system written in PHP and built on top of the [Slim Microframework](http://www.slimframework.com/), [Twig](http://twig.sensiolabs.org/) templating engine, and [Eloquent](https://laravel.com/docs/5.4/eloquent#introduction) ORM. + +## Features + +### User login screen + + +### User management page + + +### Permissions management page + + +## [Demo](https://demo.userfrosting.com) + +## Installation + +Please see our [installation guide](https://learn.userfrosting.com/installation). + +## Troubleshooting + +If you are having trouble installing UserFrosting, please [join us in chat](https://chat.userfrosting.com) or try our [forums](https://forums.userfrosting.com). + +If you are generally confused about the structure and layout of the code, or it doesn't look like the kind of PHP code that you're used to, please [start from the beginning](https://learn.userfrosting.com/background). + +## Mission Objectives + +UserFrosting seeks to balance modern programming principles, like DRY and MVC, with a shallow learning curve for new developers. Our goals are to: + +- Create a fully-functioning user management script that can be set up in just a few minutes +- Make it easy for users to quickly adapt the code for their needs +- Introduce novice developers to best practices such as separation of concerns and DRY programming +- Introduce novice developers to modern constructs such as front-end controllers, RESTful URLs, namespacing, and object-oriented modeling +- Build on existing, widely used server- and client-side components +- Clean, consistent, and well-documented code + +## Documentation + +### [Learning UserFrosting](https://learn.userfrosting.com) + +### [API documentation](http://api.userfrosting.com) + +### [Change log](CHANGELOG.md) + +## Running tests + +Run `php bakery test` from the root project directory. Any tests included in `sprinkles/*/tests` will be run. + +## Development Team + +### Alexander Weissman + +Alex is the founder and co-owner of two companies, one that does [math tutoring at Indiana University](https://bloomingtontutors.com) in Bloomington, IN and another company that does [math tutoring at UMD](https://collegeparktutors.com) in College Park, MD. He is a PhD student in the School of Informatics and Computing at Indiana University. + +### Louis Charette + +Louis's a civil engineer in Montréal, Québec who also has a passion for coding. He is one of the main contributors for SimpsonsCity.com and likes to share his knowledge by helping others the same way he was helped when he first started coding. + +### Jordan Mele + +Jordan's a developer at Mayvin Training and a student studying Computer Science at the University of Wollongong. His passion is creating software-based solutions to overcomplicated problems, without taking control away from the user. He's also Australian. + +### Sarah Baghdadi + +Sarah is UserFrosting's UX specialist and frontend designer. In addition to her work on the UF application itself, she is responsible for the amazing design of https://www.userfrosting.com and https://learn.userfrosting.com. + +### Srinivas Nukala + +Srinivas's a web applications architect, with a passion for open source technologies. He is experienced in building SaaS (software as a service) web applications and enjoys working on open source projects and contributing to the community. He has a Masters in Computer Science from Pune University, India. + +## Contributing + +This project exists thanks to all the people who contribute. If you're interested in contributing to the UserFrosting codebase, please see our [contributing guidelines](.github/CONTRIBUTING.md) as well as our [style guidelines](STYLE-GUIDE.md). + +<a href="graphs/contributors"><img src="https://opencollective.com/userfrosting/contributors.svg?width=890" /></a> + +### Thanks to our translators! + +- Louis Charette (@lcharette) - French +- Karuhut Komol (@popiazaza) - Thai +- Pietro Marangon (@Pe46dro) - Italian +- Abdullah Seba (@abdullahseba) - Arabic +- Bruno Silva (@brunomnsilva) - Portuguese +- @BruceGui - Chinese +- @kevinrombach - German +- @rafa31gz - Spanish +- @splitt3r - German +- @X-Anonymous-Y - German +- Dmitriy (@rendername) - Russian +- Amin Akbari (@aminakbari) - Farsi +- Dumblledore - Turkish + +## Supporting UserFrosting + +### Backers + +Backers help us continue to develop UserFrosting by pledging a regular monthly contribution of $5 or more. [[Become a backer](https://opencollective.com/userfrosting#contribute)] + +<a href="https://opencollective.com/userfrosting#backers" target="_blank"><img src="https://opencollective.com/userfrosting/backers.svg?width=890"></a> + +#### Sponsors + +Support this project by becoming a sponsor. Sponsors have contributed a total of $500 or more to UserFrosting (either as an ongoing backer or one-time contributions). Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/userfrosting#sponsor)] + +[](https://usorgames.com) +[](https://nextgi.com) diff --git a/login/STYLE-GUIDE.md b/login/STYLE-GUIDE.md new file mode 100755 index 0000000..94c10b2 --- /dev/null +++ b/login/STYLE-GUIDE.md @@ -0,0 +1,44 @@ +# Style guide for contributing to UserFrosting + +## PHP + +All PHP contributions must adhere to [PSR-1](http://www.php-fig.org/psr/psr-1/) and [PSR-2](http://www.php-fig.org/psr/psr-2/) specifications. + +In addition: + +### Documentation + +- All documentation blocks must adhere to the [PHPDoc](https://phpdoc.org/) format and syntax. +- All PHP files MUST contain the following documentation block immediately after the opening `<?php` tag: + +``` +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ + ``` + +### Classes + +- All classes MUST be prefaced with a documentation block containing a description and the author(s) of that class. You SHOULD add other descriptive properties as well. +- All class members and methods MUST be prefaced with a documentation block. Any parameters and return values MUST be documented. +- The contents of a class should be organized in the following order: constants, member variables, constructor, other magic methods, public methods, protected methods, private methods, and finally, deprecated methods (of any type or visibility). +- Setter methods SHOULD return the parent object. + +### Routes + +- Front controller (Slim) routes should be alphabetized, first by route type and then by route URL. If you have route groups, those should come first and be alphabetized as well. + +### Variables + + - All class member variables and local variables MUST be declared in `camelCase`. + +### Arrays + + - Array keys MUST be defined using `snake_case`. This is so they can be referenced in Twig and other templating languages. + - Array keys MUST NOT contain `.`. This is because `.` is a reserved operator in Laravel and Twig's [dot syntax](https://medium.com/@assertchris/dot-notation-3fd3e42edc61). + - Multidimensional arrays SHOULD be referenced using dot syntax whenever possible. So, instead of doing `$myArray['person1']['email']`, you should use `$myArray['person1.email']` if your array structure supports it. + +### Twig Templates diff --git a/login/app/.env.example b/login/app/.env.example new file mode 100755 index 0000000..3d22da1 --- /dev/null +++ b/login/app/.env.example @@ -0,0 +1,13 @@ +# Sample UserFrosting environment variables definition file. +# Note that in production, it is strongly recommended that you set these variables directly in your environment instead. + +UF_MODE="" +DB_DRIVER="mysql" +DB_HOST="localhost" # Some developer environments, like some MAMP installations, require you to instead set DB_HOST="127.0.0.1" +DB_PORT="3306" +DB_NAME="userfrosting" +DB_USER="userfrosting" +DB_PASSWORD="password" +SMTP_HOST="host.example.com" +SMTP_USER="relay@example.com" +SMTP_PASSWORD="password" diff --git a/login/app/.htaccess b/login/app/.htaccess new file mode 100755 index 0000000..912b0e9 --- /dev/null +++ b/login/app/.htaccess @@ -0,0 +1,15 @@ +# The `resources` directory should not be made publicly accessible (i.e., in the public document directory) at all. +# But just in case you're an idiot, this should at least give you protection from exposing passwords and other sensitive info in your .env files. + +<IfModule mod_rewrite.c> + +RewriteEngine On + +## Begin - Security +# Block all direct access to files and folders beginning with a dot +RewriteRule (^\.|/\.) - [F] +# Block access to specific files in the root folder +RewriteRule ^(LICENSE.txt|composer.lock|composer.json|\.htaccess|\.env)$ error [F] +## End - Security + +</IfModule> diff --git a/login/app/cache/.gitkeep b/login/app/cache/.gitkeep new file mode 100755 index 0000000..e69de29 --- /dev/null +++ b/login/app/cache/.gitkeep diff --git a/login/app/defines.php b/login/app/defines.php new file mode 100755 index 0000000..d129424 --- /dev/null +++ b/login/app/defines.php @@ -0,0 +1,52 @@ +<?php + +namespace UserFrosting; + +// Some standard defines +define('UserFrosting\VERSION', '4.1.17-alpha'); +define('UserFrosting\DS', '/'); +define('UserFrosting\PHP_MIN_VERSION', '5.6'); +define('UserFrosting\DEBUG_CONFIG', false); + +// Directories and Paths + +// The directory in which the non-public files reside. Should be the same as the directory that this file is in. +if (!defined('UserFrosting\APP_DIR')) { + define('UserFrosting\APP_DIR', str_replace(DIRECTORY_SEPARATOR, DS, __DIR__)); +} + +// The directory containing APP_DIR. Usually, this will contain the entire website. +define('UserFrosting\ROOT_DIR', realpath(__DIR__ . '/..')); + +define('UserFrosting\APP_DIR_NAME', basename(__DIR__)); +define('UserFrosting\BUILD_DIR_NAME', 'build'); +define('UserFrosting\CACHE_DIR_NAME', 'cache'); +define('UserFrosting\DB_DIR_NAME', 'database'); +define('UserFrosting\SESSION_DIR_NAME', 'sessions'); +define('UserFrosting\SPRINKLES_DIR_NAME', 'sprinkles'); + +// Full path to Sprinkles directory +define('UserFrosting\SPRINKLES_DIR', APP_DIR . DS . SPRINKLES_DIR_NAME); + +// Sprinkles schema file +define('UserFrosting\SPRINKLES_SCHEMA_FILE', APP_DIR . DS . 'sprinkles.json'); + +define('UserFrosting\LOG_DIR_NAME', 'logs'); +define('UserFrosting\VENDOR_DIR_NAME', 'vendor'); + +// Full path to Composer's vendor directory +define('UserFrosting\VENDOR_DIR', APP_DIR . DS . VENDOR_DIR_NAME); + +// Full path to database directory (SQLite only) +define('UserFrosting\DB_DIR', APP_DIR . DS . DB_DIR_NAME); + +// Names of directories within Sprinkles +define('UserFrosting\ASSET_DIR_NAME', 'assets'); +define('UserFrosting\EXTRA_DIR_NAME', 'extra'); +define('UserFrosting\CONFIG_DIR_NAME', 'config'); +define('UserFrosting\LOCALE_DIR_NAME', 'locale'); +define('UserFrosting\ROUTE_DIR_NAME', 'routes'); +define('UserFrosting\SCHEMA_DIR_NAME', 'schema'); +define('UserFrosting\SRC_DIR_NAME', 'src'); +define('UserFrosting\TEMPLATE_DIR_NAME', 'templates'); +define('UserFrosting\FACTORY_DIR_NAME', 'factories'); diff --git a/login/app/logs/.gitkeep b/login/app/logs/.gitkeep new file mode 100755 index 0000000..e69de29 --- /dev/null +++ b/login/app/logs/.gitkeep diff --git a/login/app/sessions/.gitkeep b/login/app/sessions/.gitkeep new file mode 100755 index 0000000..e69de29 --- /dev/null +++ b/login/app/sessions/.gitkeep diff --git a/login/app/sprinkles.example.json b/login/app/sprinkles.example.json new file mode 100755 index 0000000..d54584e --- /dev/null +++ b/login/app/sprinkles.example.json @@ -0,0 +1,10 @@ +{ + "require": { + + }, + "base": [ + "core", + "account", + "admin" + ] +} diff --git a/login/app/sprinkles/account/asset-bundles.json b/login/app/sprinkles/account/asset-bundles.json new file mode 100755 index 0000000..77ee559 --- /dev/null +++ b/login/app/sprinkles/account/asset-bundles.json @@ -0,0 +1,79 @@ +{ + "bundle": { + "js/pages/account-settings": { + "scripts": [ + "userfrosting/js/pages/account-settings.js" + ], + "options": { + "result": { + "type": { + "scripts": "plain" + } + } + } + }, + "js/pages/forgot-password": { + "scripts": [ + "userfrosting/js/pages/forgot-password.js" + ], + "options": { + "result": { + "type": { + "scripts": "plain" + } + } + } + }, + "js/pages/resend-verification": { + "scripts": [ + "userfrosting/js/pages/resend-verification.js" + ], + "options": { + "result": { + "type": { + "scripts": "plain" + } + } + } + }, + "js/pages/set-or-reset-password": { + "scripts": [ + "userfrosting/js/pages/set-or-reset-password.js" + ], + "options": { + "result": { + "type": { + "scripts": "plain" + } + } + } + }, + "js/pages/register": { + "scripts": [ + "vendor/speakingurl/speakingurl.min.js", + "userfrosting/js/uf-captcha.js", + "userfrosting/js/pages/register.js" + ], + "options": { + "result": { + "type": { + "scripts": "plain" + } + } + } + }, + "js/pages/sign-in": { + "scripts": [ + "vendor/urijs/src/URI.js", + "userfrosting/js/pages/sign-in.js" + ], + "options": { + "result": { + "type": { + "scripts": "plain" + } + } + } + } + } +}
\ No newline at end of file diff --git a/login/app/sprinkles/account/assets/userfrosting/js/pages/account-settings.js b/login/app/sprinkles/account/assets/userfrosting/js/pages/account-settings.js new file mode 100755 index 0000000..8d8d2e7 --- /dev/null +++ b/login/app/sprinkles/account/assets/userfrosting/js/pages/account-settings.js @@ -0,0 +1,29 @@ +/** + * Page-specific Javascript file. Should generally be included as a separate asset bundle in your page template. + * example: {{ assets.js('js/pages/sign-in-or-register') | raw }} + * + * This script depends on validation rules specified in pages/partials/page.js.twig. + * + * Target page: account/settings + */ +$(document).ready(function() { + + // Apply select2 to locale field + $('.js-select2').select2(); + + $("#account-settings").ufForm({ + validators: page.validators.account_settings, + msgTarget: $("#alerts-page") + }).on("submitSuccess.ufForm", function() { + // Reload the page on success + window.location.reload(); + }); + + $("#profile-settings").ufForm({ + validators: page.validators.profile_settings, + msgTarget: $("#alerts-page") + }).on("submitSuccess.ufForm", function() { + // Reload the page on success + window.location.reload(); + }); +}); diff --git a/login/app/sprinkles/account/assets/userfrosting/js/pages/forgot-password.js b/login/app/sprinkles/account/assets/userfrosting/js/pages/forgot-password.js new file mode 100755 index 0000000..3f24311 --- /dev/null +++ b/login/app/sprinkles/account/assets/userfrosting/js/pages/forgot-password.js @@ -0,0 +1,19 @@ +/** + * Page-specific Javascript file. Should generally be included as a separate asset bundle in your page template. + * example: {{ assets.js('js/pages/sign-in-or-register') | raw }} + * + * This script depends on validation rules specified in pages/partials/page.js.twig. + * + * Target page: account/forgot-password + */ +$(document).ready(function() { + + // TODO: Process form + $("#request-password-reset").ufForm({ + validators: page.validators.forgot_password, + msgTarget: $("#alerts-page") + }).on("submitSuccess.ufForm", function() { + // Forward to login page on success + window.location.replace(site.uri.public + "/account/sign-in"); + }); +}); diff --git a/login/app/sprinkles/account/assets/userfrosting/js/pages/register.js b/login/app/sprinkles/account/assets/userfrosting/js/pages/register.js new file mode 100755 index 0000000..d855bb9 --- /dev/null +++ b/login/app/sprinkles/account/assets/userfrosting/js/pages/register.js @@ -0,0 +1,94 @@ +/** + * Page-specific Javascript file. Should generally be included as a separate asset bundle in your page template. + * example: {{ assets.js('js/pages/sign-in-or-register') | raw }} + * + * This script depends on validation rules specified in pages/partials/page.js.twig. + * + * Target page: account/register + */ +$(document).ready(function() { + // TOS modal + $(this).find('.js-show-tos').click(function() { + $("body").ufModal({ + sourceUrl: site.uri.public + "/modals/account/tos", + msgTarget: $("#alerts-page") + }); + }); + + // Auto-generate username when name is filled in + var autoGenerate = true; + $("#register").find('input[name=first_name], input[name=last_name]').on('input change', function() { + if (!autoGenerate) { + return; + } + + var form = $("#register"); + + var firstName = form.find('input[name=first_name]').val().trim(); + var lastName = form.find('input[name=last_name]').val().trim(); + + if (!firstName && !lastName) { + return; + } + + var userName = getSlug(firstName + ' ' + lastName, { + separator: '.' + }); + // Set slug + form.find('input[name=user_name]').val(userName); + }); + + // Autovalidate username field on a delay + var timer; + $("#register").find('input[name=first_name], input[name=last_name], input[name=user_name]').on('input change', function() { + clearTimeout(timer); // Clear the timer so we don't end up with dupes. + timer = setTimeout(function() { // assign timer a new timeout + $("#register").find('input[name=user_name]').valid(); + }, 500); + }); + + // Enable/disable username suggestions in registration page + $("#register").find('#form-register-username-suggest').on('click', function(e) { + e.preventDefault(); + var form = $("#register"); + $.getJSON(site.uri.public + '/account/suggest-username') + .done(function (data) { + // Set suggestion + form.find('input[name=user_name]').val(data.user_name); + }); + }); + + // Turn off autogenerate when someone enters stuff manually in user_name + $("#register").find('input[name=user_name]').on('input', function() { + autoGenerate = false; + }); + + // Add remote rule for checking usernames on the fly + var registrationValidators = $.extend( + true, // deep extend + page.validators.register, + { + rules: { + user_name: { + remote: { + url: site.uri.public + '/account/check-username', + dataType: 'text' + } + } + } + } + ); + + // Handles form submission + $("#register").ufForm({ + validators: registrationValidators, + msgTarget: $("#alerts-page"), + keyupDelay: 500 + }).on("submitSuccess.ufForm", function() { + // Reload to clear form and show alerts + window.location.reload(); + }).on("submitError.ufForm", function() { + // Reload captcha + $("#captcha").captcha(); + }); +}); diff --git a/login/app/sprinkles/account/assets/userfrosting/js/pages/resend-verification.js b/login/app/sprinkles/account/assets/userfrosting/js/pages/resend-verification.js new file mode 100755 index 0000000..5c3eaf8 --- /dev/null +++ b/login/app/sprinkles/account/assets/userfrosting/js/pages/resend-verification.js @@ -0,0 +1,19 @@ +/** + * Page-specific Javascript file. Should generally be included as a separate asset bundle in your page template. + * example: {{ assets.js('js/pages/sign-in-or-register') | raw }} + * + * This script depends on validation rules specified in pages/partials/page.js.twig. + * + * Target page: account/resend-verification + */ +$(document).ready(function() { + + // TODO: Process form + $("#request-verification-email").ufForm({ + validators: page.validators.resend_verification, + msgTarget: $("#alerts-page") + }).on("submitSuccess.ufForm", function() { + // Forward to login page on success + window.location.replace(site.uri.public + "/account/sign-in"); + }); +}); diff --git a/login/app/sprinkles/account/assets/userfrosting/js/pages/set-or-reset-password.js b/login/app/sprinkles/account/assets/userfrosting/js/pages/set-or-reset-password.js new file mode 100755 index 0000000..39cfd16 --- /dev/null +++ b/login/app/sprinkles/account/assets/userfrosting/js/pages/set-or-reset-password.js @@ -0,0 +1,19 @@ +/** + * Page-specific Javascript file. Should generally be included as a separate asset bundle in your page template. + * example: {{ assets.js('js/pages/sign-in-or-register') | raw }} + * + * This script depends on validation rules specified in pages/partials/page.js.twig. + * + * Target pages: account/set-password, account/reset-password + */ +$(document).ready(function() { + + $("#set-or-reset-password").ufForm({ + validators: page.validators.set_password, + msgTarget: $("#alerts-page") + }).on("submitSuccess.ufForm", function() { + // Forward to home page on success + // TODO: forward to landing/last page + window.location.replace(site.uri.public + "/account/sign-in"); + }); +}); diff --git a/login/app/sprinkles/account/assets/userfrosting/js/pages/sign-in.js b/login/app/sprinkles/account/assets/userfrosting/js/pages/sign-in.js new file mode 100755 index 0000000..40a8628 --- /dev/null +++ b/login/app/sprinkles/account/assets/userfrosting/js/pages/sign-in.js @@ -0,0 +1,39 @@ +/** + * Page-specific Javascript file. Should generally be included as a separate asset bundle in your page template. + * example: {{ assets.js('js/pages/sign-in-or-register') | raw }} + * + * This script depends on validation rules specified in pages/partials/page.js.twig. + * + * Target page: account/sign-in + */ +$(document).ready(function() { + /** + * If there is a redirect parameter in the query string, redirect to that page. + * Otherwise, if there is a UF-Redirect header, redirect to that page. + * Otherwise, redirect to the home page. + */ + function redirectOnLogin(jqXHR) { + var components = URI.parse(window.location.href); + var query = URI.parseQuery(components['query']); + + if (query && query['redirect']) { + // Strip leading slashes from redirect strings + var redirectString = site.uri.public + '/' + query['redirect'].replace(/^\/+/, ""); + // Strip excess trailing slashes for clean URLs. e.g. if redirect=%2F + redirectString = redirectString.replace(/\/+$/, "/"); + // Redirect + window.location.replace(redirectString); + } else if (jqXHR.getResponseHeader('UF-Redirect')) { + window.location.replace(jqXHR.getResponseHeader('UF-Redirect')); + } else { + window.location.replace(site.uri.public); + } + } + + $("#sign-in").ufForm({ + validators: page.validators.login, + msgTarget: $("#alerts-page") + }).on("submitSuccess.ufForm", function(event, data, textStatus, jqXHR) { + redirectOnLogin(jqXHR); + }); +}); diff --git a/login/app/sprinkles/account/bower.json b/login/app/sprinkles/account/bower.json new file mode 100755 index 0000000..8e7ef39 --- /dev/null +++ b/login/app/sprinkles/account/bower.json @@ -0,0 +1,28 @@ +{ + "name": "userfrosting-sprinkle-account", + "description": "Authentication and account management module for UserFrosting.", + "homepage": "https://github.com/userfrosting", + "license": "MIT", + "authors": [ + { + "name": "Alexander Weissman", + "homepage": "https://alexanderweissman.com" + }, + "ssnukala" + ], + "dependencies": {}, + "moduleType": [ + "node" + ], + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "assets/vendor", + "examples", + "demo-resources", + "demo", + "test", + "tests" + ] +} diff --git a/login/app/sprinkles/account/composer.json b/login/app/sprinkles/account/composer.json new file mode 100755 index 0000000..fa2e178 --- /dev/null +++ b/login/app/sprinkles/account/composer.json @@ -0,0 +1,24 @@ +{ + "name": "userfrosting/sprinkle-account", + "type": "userfrosting-sprinkle", + "description": "Authentication and account management module for UserFrosting.", + "keywords": ["php user management", "usercake", "bootstrap"], + "homepage": "https://github.com/userfrosting/UserFrosting", + "license" : "MIT", + "authors" : [ + { + "name": "Alexander Weissman", + "homepage": "https://alexanderweissman.com" + } + ], + "require": { + "birke/rememberme" : "^2.0", + "nikic/php-parser" : "^1", + "php": ">=5.6" + }, + "autoload": { + "psr-4": { + "UserFrosting\\Sprinkle\\Account\\": "src/" + } + } +} diff --git a/login/app/sprinkles/account/config/default.php b/login/app/sprinkles/account/config/default.php new file mode 100755 index 0000000..e154643 --- /dev/null +++ b/login/app/sprinkles/account/config/default.php @@ -0,0 +1,79 @@ +<?php + + /** + * Account configuration file for UserFrosting. + * + */ + + return [ + 'debug' => [ + 'auth' => false + ], + // configuration for the 'password reset' feature + 'password_reset' => [ + 'algorithm' => 'sha512', + 'timeouts' => [ + 'create' => 86400, + 'reset' => 10800 + ] + ], + // See https://github.com/gbirke/rememberme for an explanation of these settings + 'remember_me' => [ + 'cookie' => [ + 'name' => 'rememberme' + ], + 'expire_time' => 604800, + 'session' => [ + 'path' => '/' + ], + 'table' => [ + 'tableName' => 'persistences', + 'credentialColumn' => 'user_id', + 'tokenColumn' => 'token', + 'persistentTokenColumn' => 'persistent_token', + 'expiresColumn' => 'expires_at' + ] + ], + 'reserved_user_ids' => [ + 'guest' => -1, + 'master' => 1 + ], + 'session' => [ + // The keys used in the session to store info about authenticated users + 'keys' => [ + 'current_user_id' => 'account.current_user_id', // the key to use for storing the authenticated user's id + 'captcha' => 'account.captcha' // Key used to store a captcha hash during captcha verification + ] + ], + // "Site" settings that are automatically passed to Twig + 'site' => [ + 'login' => [ + 'enable_email' => true + ], + 'registration' => [ + 'enabled' => true, + 'captcha' => true, + 'require_email_verification' => true, + 'user_defaults' => [ + 'locale' => 'en_US', + 'group' => 'terran', + // Default roles for newly registered users + 'roles' => [ + 'user' => true + ] + ] + ] + ], + 'throttles' => [ + 'check_username_request' => null, + 'password_reset_request' => null, + 'registration_attempt' => null, + 'sign_in_attempt' => null, + 'verification_request' => null + ], + // configuration for the 'email verification' feature + 'verification' => [ + 'algorithm' => 'sha512', + 'timeout' => 10800 + ] + ]; diff --git a/login/app/sprinkles/account/config/production.php b/login/app/sprinkles/account/config/production.php new file mode 100755 index 0000000..b7c3288 --- /dev/null +++ b/login/app/sprinkles/account/config/production.php @@ -0,0 +1,67 @@ +<?php + + /** + * Account production config file for UserFrosting. You may override/extend this in your site's configuration file to customize deploy settings. + * + */ + + return [ + // See http://security.stackexchange.com/a/59550/74909 for the inspiration for our throttling system + 'throttles' => [ + 'check_username_request' => [ + 'method' => 'ip', + 'interval' => 3600, + 'delays' => [ + 40 => 1000 + ] + ], + 'password_reset_request' => [ + 'method' => 'ip', + 'interval' => 3600, + 'delays' => [ + 2 => 5, + 3 => 10, + 4 => 20, + 5 => 40, + 6 => 80, + 7 => 600 + ] + ], + 'registration_attempt' => [ + 'method' => 'ip', + 'interval' => 3600, + 'delays' => [ + 2 => 5, + 3 => 10, + 4 => 20, + 5 => 40, + 6 => 80, + 7 => 600 + ] + ], + 'sign_in_attempt' => [ + 'method' => 'ip', + 'interval' => 3600, + 'delays' => [ + 4 => 5, + 5 => 10, + 6 => 20, + 7 => 40, + 8 => 80, + 9 => 600 + ] + ], + 'verification_request' => [ + 'method' => 'ip', + 'interval' => 3600, + 'delays' => [ + 2 => 5, + 3 => 10, + 4 => 20, + 5 => 40, + 6 => 80, + 7 => 600 + ] + ] + ] + ]; diff --git a/login/app/sprinkles/account/factories/Permissions.php b/login/app/sprinkles/account/factories/Permissions.php new file mode 100755 index 0000000..591f5fd --- /dev/null +++ b/login/app/sprinkles/account/factories/Permissions.php @@ -0,0 +1,19 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ + +use League\FactoryMuffin\Faker\Facade as Faker; + +/** + * General factory for the Permission Model + */ +$fm->define('UserFrosting\Sprinkle\Account\Database\Models\Permission')->setDefinitions([ + 'slug' => Faker::word(), + 'name' => Faker::word(), + 'description' => Faker::paragraph(), + 'conditions' => Faker::word() +]); diff --git a/login/app/sprinkles/account/factories/Roles.php b/login/app/sprinkles/account/factories/Roles.php new file mode 100755 index 0000000..cdbb5a3 --- /dev/null +++ b/login/app/sprinkles/account/factories/Roles.php @@ -0,0 +1,18 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ + +use League\FactoryMuffin\Faker\Facade as Faker; + +/** + * General factory for the Role Model + */ +$fm->define('UserFrosting\Sprinkle\Account\Database\Models\Role')->setDefinitions([ + 'slug' => Faker::unique()->word(), + 'name' => Faker::word(), + 'description' => Faker::paragraph() +]); diff --git a/login/app/sprinkles/account/factories/Users.php b/login/app/sprinkles/account/factories/Users.php new file mode 100755 index 0000000..7390c44 --- /dev/null +++ b/login/app/sprinkles/account/factories/Users.php @@ -0,0 +1,23 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ + +use League\FactoryMuffin\Faker\Facade as Faker; + +/** + * General factory for the User Model + */ +$fm->define('UserFrosting\Sprinkle\Account\Database\Models\User')->setDefinitions([ + 'user_name' => Faker::unique()->firstNameMale(), + 'first_name' => Faker::firstNameMale(), + 'last_name' => Faker::firstNameMale(), + 'email' => Faker::unique()->email(), + 'locale' => 'en_US', + 'flag_verified' => 1, + 'flag_enabled' => 1, + 'password' => Faker::password() +]); diff --git a/login/app/sprinkles/account/locale/ar/messages.php b/login/app/sprinkles/account/locale/ar/messages.php new file mode 100755 index 0000000..7203904 --- /dev/null +++ b/login/app/sprinkles/account/locale/ar/messages.php @@ -0,0 +1,176 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Modern Standard Arabic message token translations for the 'account' sprinkle. + * + * @package userfrosting\i18n\ar + * @author Alexander Weissman and Abdullah Seba + */ + +return [ + "ACCOUNT" => [ + "@TRANSLATION" => "الحساب", + + "ACCESS_DENIED" => "يبدو أنك لا تملك صلاحية للقيام بذلك", + + "DISABLED" => "هذا الحساب معطل يمكنك الاتصال بنا للحصول على مزيد من المعلومات", + + "EMAIL_UPDATED" => "تم تجديد البريد الإلكتروني بالحساب", + + "INVALID" => "هذا الحساب غير موجود قد تم حذفه يمكنك الاتصا بنا للحصول على مزيد من المعلومات", + + "MASTER_NOT_EXISTS" => "لا يمكنك تسجيل حساب جديد حتى تم إنشاء الحساب الرئيسي", + "MY" => "حسابي", + + "SESSION_COMPROMISED" => [ + "@TRANSLATION" => "تم اختراق جلسنك يجب عليك الخروج على كافة الأجهزة، ثم تسجيل الدخول مرة أخرى والتأكد من أن المعلومات الخاصة بك لم يعبث بها", + "TITLE" => "من الممكن أن حسابك قد اخترق", + "TEXT" => "ربما استخدم شخص معلومات التسجيل الدخول للدخول إلى هذه الصفحة. لسلامتك، تم انتهاء جميع الجلسات يرجا <a href=\"{{url}}\">التسجيل مرة اخرى</a> وتحقق من حسابك بسبب النشاط الغريب قد ترغب في تغيير كلمة المرور" + ], + + "SESSION_EXPIRED" => "انتهت جلستك تستطيع تسجيل الدخول مرة أخرى", + + "SETTINGS" => [ + "@TRANSLATION" => "إعدادات الحساب", + "DESCRIPTION" => "غير إعدادات حسابك، بما في ذلك البريد الإلكتروني، واسم وكلمة المرور +", + "UPDATED" => "تم تجديد إعدادات الحساب" + ], + + "TOOLS" => "أدوات الحساب", + + "UNVERIFIED" => "لم يتم التحقق من حسابك بعد افحص في رسائل البريد الإلكتروني و ملف البريد المزعج للحصول على تعليمات تفعيل الحساب", + + "VERIFICATION" => [ + "NEW_LINK_SENT" => "لقد أرسلنا رابط جديدا لتحقق عبر البريد الإلكتروني إلى {{email}} افحص في رسائل البريد الإلكتروني و ملف البريد المزعج", + "RESEND" => "إعادة ارسال بريد التحقق", + "COMPLETE" => "لقد تم التحقق من حسابك بنجاح يمكنك الآن تسجيل الدخول", + "EMAIL" => "ادخل عنوان البريد الإلكتروني الذي استخدمته للتسجيل، و سوف نرسل البريد الإلكتروني لتحقق مرة أخرى", + "PAGE" => "إعادة إرسال البريد الإلكتروني التحقق من حسابك الجديد", + "SEND" => "ارسل رابط للتحقق عبر البريد الالكتروني", + "TOKEN_NOT_FOUND" => "رمز التحقق غير موجود أو تم تحقق الحساب من قبل", + ] + ], + + "EMAIL" => [ + "INVALID" => "لا يوجد حساب ل <strong>{{email}}</strong>", + "IN_USE" => "البريد الإلكتروني <strong>{{email}}</strong> قيد الاستخدام" + ], + + "FIRST_NAME" => "الاسم الاول", + + "HEADER_MESSAGE_ROOT" => "تسجيل الدخول باسم المستخدم ROOT", + + "LAST_NAME" => "اسم العائلة", + + "LOCALEACCOUNT" => "اللغة التي تستخدم لحسابك", + + "LOGIN" => [ + "@TRANSLATION" => "تسجيل الدخول", + + "ALREADY_COMPLETE" => "انت بالفعل داخل", + "SOCIAL" => "أو الدخول مع", + "REQUIRED" => "عذرا، يجب عليك تسجيل الدخول للوصول إلى هذا المكان" + ], + + "LOGOUT" => "تسجيل الخروج", + + "NAME" => "اسم", + + "PAGE" => [ + "LOGIN" => [ + "DESCRIPTION" => "سجل الدخول إلى حسابك في {{site_name}} أو سجيل للحصول على حساب جديد", + "SUBTITLE" => "التسجيل مجانا أو قم بتسجيل الدخول باستخدام حساب موجود", + "TITLE" => "هيا نبدأ", + ] + ], + + "PASSWORD" => [ + "@TRANSLATION" => "كلمه المرور", + + "BETWEEN" => "ما بين {{min}}-{{max}} حروف", + + "CONFIRM" => "تأكيد كلمة المرور", + "CONFIRM_CURRENT" => "تأكيد كلمه المرور الحالي", + "CONFIRM_NEW" => "تأكيد كلمة المرور الجديدة", + "CONFIRM_NEW_EXPLAIN" => "إعادة إدخال كلمة المرور الجديدة", + "CONFIRM_NEW_HELP" => "لازم إذا كان المطلوب اختيار كلمة مرور جديدة", + "CURRENT" => "كلمة المرور الحالية", + "CURRENT_EXPLAIN" => "يجب عليك تأكيد كلمة المرور الحالية لإجراء التغييرات", + + "FORGOTTEN" => "كلمه المرور منسية", + "FORGET" => [ + "@TRANSLATION" => "لقد نسيت كلمة المرور", + + "COULD_NOT_UPDATE" => "لا يمكن تحديث كلمة المرور", + "EMAIL" => "ادخل عنوان البريد الإلكتروني الذي استخدمته للتسجيل وسوف نرسل تعليمات لإعادة تعيين كلمة المرور", + "EMAIL_SEND" => "أرسل رابط تعيين كلمة المرور عبر البريد الالكتروني", + "INVALID" => "لم يتم العثور على إعادة تعيين كلمة المرور، أو انتهت صلاحية رابط حاول <a href=\"{{url}}\">إعادة تقديم طلبك<a>", + "PAGE" => "الحصول على رابط لإعادة تعيين كلمة المرور", + "REQUEST_CANNED" => "إلغاء طلب كلمة المرور", + "REQUEST_SENT" => "إذا تطابق البريد الإلكتروني <strong>{{email}}</strong> حسابا في نظامنا، فسيتم إرسال رابط إعادة تعيين كلمة المرور إلى <strong>{{email}}</strong>." + ], + + "RESET" => [ + "@TRANSLATION" => "إعادة تعيين كلمة المرور", + "CHOOSE" => "اختيار كلمة مرور جديدة للتواصل", + "PAGE" => "اختيار كلمة مرور جديدة لحسابك", + "SEND" => "تعيين كلمة المرور الجديدة وتسجيل الدخول" + ], + + "HASH_FAILED" => "فشلت التجزئة كلمة المرور يرجى الاتصال بمسؤول الموقع", + "INVALID" => "كلمة مرور الحالية لا تتطابق مع ما لدينا", + "NEW" => "كلمة مرور الجديدة", + "NOTHING_TO_UPDATE" => "لا يمكنك تحديث مع نفس كلمة مرور", + "UPDATED" => "جدد كلمة مرور", + + "CREATE" => [ + "@TRANSLATION" => "إنشاء كلمة مرور", + "PAGE" => "اختر كلمة مرور لحسابك الجديد", + "SET" => "تعيين كلمة المرور وتسجيل الدخول" + ] + ], + + "REGISTER" => "تسجيل", + "REGISTER_ME" => "سجلني", + "SIGN_IN_HERE" => "هل لديك حساب؟ <a href=\"{{url}}\">تسجيل الدخول هنا</a>", + + "REGISTRATION" => [ + "BROKEN" => "نحن آسفون، هناك مشكلة مع عملية تسجيل الحساب يرجى الاتصال بنا مباشرة للحصول على المساعدة", + "COMPLETE_TYPE1" => "لقد سجلت بنجاح يمكنك الآن تسجيل الدخول", + "COMPLETE_TYPE2" => "لقد سجلت بنجاح سوف تتلقى قريبا رسالة التحقق تحتوي على رابط لتفعيل حسابك لن تكون قادرا على تسجيل الدخول حتى الانتهاء من هذه الخطوة", + "DISABLED" => "عذرا، لقد تم تعطيل تسجيل اي حساب", + "LOGOUT" => "لا يمكنك التسجيل للحصول على حساب أثناء تسجيل الدخول", + "WELCOME" => "التسجيل سريع وبسيط" + ], + + "RATE_LIMIT_EXCEEDED" => "تم تجاوز الحد عددا لهذا الإجراء يجب الانتظار {{delay}} ثواني قبل القيام بمحاولة أخرى", + "REMEMBER_ME" => "تذكرنى", + "REMEMBER_ME_ON_COMPUTER" => "تذكرني على هذا الحاسوب (غير مستحسن للحواسب العامة)", + + "SIGNIN" => "تسجيل الدخول", + "SIGNIN_OR_REGISTER" => "تسجيل الدخول أو التسجيل", + "SIGNUP" => "تسجيل", + + "TOS" => "الأحكام والشروط", + "TOS_AGREEMENT" => "من خلال تسجيل حساب جديد في {{site_title}}, انت تقبل <a {{link_attributes | raw}}>الأحكام والشروط</a>", + "TOS_FOR" => "الأحكام والشروط ل {{title}}", + + "USERNAME" => [ + "@TRANSLATION" => "اسم المستخدم", + + "CHOOSE" => "اختيار اسم مستخدم فريد", + "INVALID" => "اسم المستخدم غير صالح", + "IN_USE" => "اسم المستخدم <strong>{{user_name}}</strong> قيد الاستخدام" + ], + + "USER_ID_INVALID" => "عدم وجود هوية المستخدم المطلوب", + "USER_OR_EMAIL_INVALID" => "اسم المستخدم أو عنوان البريد الإلكتروني غير صالح", + "USER_OR_PASS_INVALID" => "اسم المستخدم أو كلمة المرور غير صالحة", + + "WELCOME" => "مرحبا بعودتك, {{first_name}}" +]; diff --git a/login/app/sprinkles/account/locale/ar/validate.php b/login/app/sprinkles/account/locale/ar/validate.php new file mode 100755 index 0000000..37693fb --- /dev/null +++ b/login/app/sprinkles/account/locale/ar/validate.php @@ -0,0 +1,18 @@ +<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * Modern Standard Arabic message token translations for the 'account' sprinkle.
+ *
+ * @package userfrosting\i18n\ar
+ * @author Alexander Weissman and Abdullah Seba
+ */
+
+return [
+ "VALIDATE" => [
+ "PASSWORD_MISMATCH" => "يجب أن تكون كلمة المرور وكلمة المرور التأكيدية نفس"
+ ]
+];
diff --git a/login/app/sprinkles/account/locale/de_DE/messages.php b/login/app/sprinkles/account/locale/de_DE/messages.php new file mode 100755 index 0000000..b331552 --- /dev/null +++ b/login/app/sprinkles/account/locale/de_DE/messages.php @@ -0,0 +1,188 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * German message token translations for the 'account' sprinkle. + * + * @package userfrosting\i18n\de + * @author X-Anonymous-Y + * @author kevinrombach + * @author splitt3r + */ + +return [ + "ACCOUNT" => [ + "@TRANSLATION" => "Konto", + + "ACCESS_DENIED" => "Hmm, sieht aus als hätten Sie keine Berechtigung, um dies zu tun.", + + "DISABLED" => "Dieses Konto wurde deaktiviert. Bitte Kontaktieren Sie uns für weitere Informationen.", + + "EMAIL_UPDATED" => "E-Mail-Adresse aktualisiert.", + + "INVALID" => "Dieses Konto existiert nicht. Es wurde möglicherweise gelöscht. Bitte kontaktieren Sie uns für weitere Informationen.", + + "MASTER_NOT_EXISTS" => "Sie können kein neues Konto anlegen solange kein Root-Konto angelegt wurde!", + "MY" => "Mein Konto", + + "SESSION_COMPROMISED" => [ + "@TRANSLATION" => "Ihre Sitzung wurde beeinträchtigt. Sie sollten sich auf allen Geräten abmelden, sich dann wieder anmelden und sicherstellen, dass Ihre Daten nicht manipuliert wurden.", + "TITLE" => "Ihr Konto wurde möglicherweise beeinträchtigt", + "TEXT" => "Möglicherweise ist es jemandem gelungen, Ihren Zugang zu dieser Seite zu übernehmen. Aus Sicherheitsgründen wurden Sie überall abgemeldet. Bitte <a href=\"{{url}}\">melden Sie sich neu an</a> und untersuchen Sie das Konto nach verdächtigen Aktivitäten. Außerdem sollten Sie Ihr Passwort ändern." + ], + "SESSION_EXPIRED" => "Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.", + + "SETTINGS" => [ + "@TRANSLATION" => "Kontoeinstellungen", + "DESCRIPTION" => "Aktualisieren Sie Ihre Kontoeinstellungen, einschließlich E-Mail, Name und Passwort.", + "UPDATED" => "Kontoeinstellungen aktualisiert" + ], + + "TOOLS" => "Konto-Werkzeuge", + + "UNVERIFIED" => "Ihr Konto wurde noch nicht bestätigt. Überprüfen Sie Ihr E-Mails/Spam-Ordner für die Konto-Aktivierungsanleitung.", + + "VERIFICATION" => [ + "NEW_LINK_SENT" => "Wir haben einen neuen Bestätigungslink an {{email}} gesendet. Überprüfen Sie Ihr E-Mail/Spam-Ordner oder versuchen Sie es später noch einmal.", + "RESEND" => "Bestätigungsmail erneut senden", + "COMPLETE" => "Sie haben Ihr Konto erfolgreich Verifiziert. Sie können sich jetzt anmelden.", + "EMAIL" => "Bitte geben Sie die E-Mail-Adresse ein, mit der Sie sich registriert haben, Überprüfen Sie Ihr E-Mails/Spam-Ordner für die Bestätigungs-E-Mail.", + "PAGE" => "Senden Sie die Bestätigungs-E-Mail erneut für Ihr neues Konto.", + "SEND" => "Bestätigungslink erneut per E-Mail zusenden", + "TOKEN_NOT_FOUND" => "Verifizierungstoken existiert nicht / Konto wurde bereits verifiziert" + ] + ], + + "EMAIL" => [ + "INVALID" => "Es gibt kein Konto für <strong>{{email}}</strong>.", + "IN_USE" => "Die E-Mail Adresse <strong>{{email}}</strong> wird bereits verwendet.", + "VERIFICATION_REQUIRED" => "E-Mail (Bestätigung benötigt - Benutzen Sie eine echte E-Mail Adresse!)" + ], + + "EMAIL_OR_USERNAME" => "Benutzername oder E-mail Adresse", + + "FIRST_NAME" => "Vorname", + + "HEADER_MESSAGE_ROOT" => "Sie sind als Root-Benutzer angemeldet.", + + "LAST_NAME" => "Nachname", + + "LOCALE" => [ + "ACCOUNT" => "Die Sprache und das Gebietsschema für Ihr Konto", + "INVALID" => "<strong>{{locale}}</strong> ist kein gültiges Gebietsschema." + ], + + "LOGIN" => [ + "@TRANSLATION" => "Anmelden", + "ALREADY_COMPLETE" => "Sie sind bereits eingeloggt!", + "SOCIAL" => "Oder loggen Sie sich ein mit", + "REQUIRED" => "Sorry, Sie müssen angemeldet sein. Um auf diese Ressource zugreifen zu können." + ], + + "LOGOUT" => "Ausloggen", + + "NAME" => "Name", + + "NAME_AND_EMAIL" => "Name und E-Mail", + + "PAGE" => [ + "LOGIN" => [ + "DESCRIPTION" => "Melden Sie sich in Ihr {{site_name}} Konto an oder registrieren Sie sich für ein neues Konto.", + "SUBTITLE" => "Registrieren Sie sich kostenlos oder melden Sie sich mit einem bestehenden Konto an.", + "TITLE" => "Lass uns anfangen!" + ] + ], + + "PASSWORD" => [ + "@TRANSLATION" => "Passwort", + + "BETWEEN" => "Zwischen {{min}}-{{max}} Zeichen", + + "CONFIRM" => "Bestätige das Passwort", + "CONFIRM_CURRENT" => "Bitte bestätige dein jetziges Passwort", + "CONFIRM_NEW" => "Neues Passwort bestätigen", + "CONFIRM_NEW_EXPLAIN" => "Geben Sie Ihr neues Passwort erneut ein", + "CONFIRM_NEW_HELP" => "Erforderlich, wenn Sie ein neues Passwort wählen", + "CREATE" => [ + "@TRANSLATION" => "Passwort setzen", + "PAGE" => "Setzen Sie ein Passwort für den Account.", + "SET" => "Passwort setzen und anmelden" + ], + "CURRENT" => "Aktuelles Passwort", + "CURRENT_EXPLAIN" => "Sie müssen Ihr aktuelles Passwort bestätigen, um Änderungen vorzunehmen", + + "FORGOTTEN" => "Passwort vergessen", + "FORGET" => [ + "@TRANSLATION" => "Ich habe mein Passwort vergessen", + + "COULD_NOT_UPDATE" => "Das Passwort konnte nicht aktualisiert werden.", + "EMAIL" => "Bitte geben Sie die E-Mail-Adresse ein, mit der Sie sich registriert haben. Ein Link mit der Anweisungen zum Zurücksetzen Ihres Passworts wird Ihnen per E-Mail zugeschickt.", + "EMAIL_SEND" => "Neue Passwort zurücksetzen E-Mail senden", + "INVALID" => "Diese Anforderung zum Zurücksetzen des Passworts wurde nicht gefunden oder ist abgelaufen.Bitte versuchen Sie <a href=\'{{url}}\'>Ihre Anfrage erneut einzureichen<a>.", + "PAGE" => "Holen Sie sich einen Link, um Ihr Passwort zurückzusetzen.", + "REQUEST_CANNED" => "Verlorene Passwortanforderung abgebrochen.", + "REQUEST_SENT" => "Wenn die E-Mail <strong>{{email}}</strong> mit einem Account in unserem System übereinstimmt, wird ein Passwort-Reset-Link an <strong>{{email}}</strong> gesendet." + ], + + "HASH_FAILED" => "Passwort Hashing fehlgeschlagen. Bitte kontaktieren Sie einen Administrator.", + "INVALID" => "Das aktuelle Passwort stimmt nicht mit dem Datensatz überein", + "NEW" => "Neues Passwort", + "NOTHING_TO_UPDATE" => "Sie können nicht das gleiche Passwort zum Aktualisieren verwenden", + + "RESET" => [ + "@TRANSLATION" => "Passwort zurücksetzen", + "CHOOSE" => "Bitte wählen Sie ein neues Passwort, um fortzufahren.", + "PAGE" => "Wählen Sie ein neues Passwort für Ihr Konto.", + "SEND" => "Neues Passwort festlegen und anmelden" + ], + + "UPDATED" => "Konto Passwort aktualisiert" + ], + + "PROFILE" => [ + "SETTINGS" => "Profileinstellungen", + "UPDATED" => "Profileinstellungen aktualisiert" + ], + + "RATE_LIMIT_EXCEEDED" => "Die grenze für diese Maßnahme wurde überschritten. Sie müssen weitere {{delay}} Sekunden warten, bevor Sie einen weiteren Versuch machen dürfen.", + + "REGISTER" => "Registrieren", + "REGISTER_ME" => "Melden Sie mich an", + "REGISTRATION" => [ + "BROKEN" => "Es tut uns leid, es gibt ein Problem mit unserer Registrierung. Bitte kontaktieren Sie uns direkt für Hilfe.", + "COMPLETE_TYPE1" => "Sie haben sich erfolgreich registriert. Sie können sich jetzt anmelden.", + "COMPLETE_TYPE2" => "Sie haben sich erfolgreich registriert. Sie erhalten in Kürze eine Bestätigungs-E-Mail mit einem Link zur Aktivierung Ihres Kontos. Sie können sich nicht anmelden, bis Sie diesen Schritt abgeschlossen haben.", + "DISABLED" => "Es tut uns leid, Die Registrierung des Kontos ist deaktiviert.", + "LOGOUT" => "Es tut uns leid, Sie können kein neues Konto registrieren, während Sie angemeldet sind. Bitte melden Sie sich zuerst ab.", + "WELCOME" => "Die Registrierung ist schnell und einfach." + ], + "REMEMBER_ME" => "Erinnere dich an mich!", + "REMEMBER_ME_ON_COMPUTER" => "Erinnere dich an mich auf diesem Computer (nicht für öffentliche Computer empfohlen)", + + "SIGN_IN_HERE" => "Sie haben bereits einen Account? <a href=\"{{url}}\">Melden Sie sich hier an.</a>", + "SIGNIN" => "Anmelden", + "SIGNIN_OR_REGISTER" => "Anmelden oder registrieren", + "SIGNUP" => "Anmelden", + + "TOS" => "Geschäftsbedingungen", + "TOS_AGREEMENT" => "Durch die Registrierung eines Kontos auf {{site_title}} akzeptieren Sie die <a {{link_attributes | raw}}> Bedingungen </a>.", + "TOS_FOR" => "Allgemeine Geschäftsbedingungen für {{title}}", + + "USERNAME" => [ + "@TRANSLATION" => "Benutzername", + + "CHOOSE" => "Wählen Sie einen eindeutigen Benutzernamen", + "INVALID" => "Ungültiger Benutzername", + "IN_USE" => "Benutzername <strong>{{user_name}}</strong> wird bereits verwendet.", + "NOT_AVAILABLE" => "Benutzername <strong>{{user_name}}</strong> ist nicht verfügbar. Wähle einen anderen Namen, der klicken Sie auf 'vorschlagen'." + ], + + "USER_ID_INVALID" => "Die angeforderte Benutzer-ID existiert nicht.", + "USER_OR_EMAIL_INVALID" => "Benutzername oder E-Mail-Adresse ist ungültig.", + "USER_OR_PASS_INVALID" => "Benutzername oder Passwort ist ungültig.", + + "WELCOME" => "Willkommen zurück, {{first_name}}" +]; diff --git a/login/app/sprinkles/account/locale/de_DE/validate.php b/login/app/sprinkles/account/locale/de_DE/validate.php new file mode 100755 index 0000000..30cf98b --- /dev/null +++ b/login/app/sprinkles/account/locale/de_DE/validate.php @@ -0,0 +1,21 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * German message token translations for the 'account' sprinkle. + * + * @package userfrosting\i18n\de + * @author X-Anonymous-Y + * @author kevinrombach + * @author splitt3r + */ + +return [ + "VALIDATE" => [ + "PASSWORD_MISMATCH" => "Ihr Passwort und das Bestätigungspasswort müssen übereinstimmen.", + "USERNAME" => "Benutzernamen dürfen nur aus Kleinbuchstaben, Zahlen, '.', '-' und '_' bestehen." + ] +]; diff --git a/login/app/sprinkles/account/locale/en_US/messages.php b/login/app/sprinkles/account/locale/en_US/messages.php new file mode 100755 index 0000000..17d7582 --- /dev/null +++ b/login/app/sprinkles/account/locale/en_US/messages.php @@ -0,0 +1,183 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * US English message token translations for the 'account' sprinkle. + * + * @package userfrosting\i18n\en_US + * @author Alexander Weissman + */ + +return [ + "ACCOUNT" => [ + "@TRANSLATION" => "Account", + + "ACCESS_DENIED" => "Hmm, looks like you don't have permission to do that.", + + "DISABLED" => "This account has been disabled. Please contact us for more information.", + + "EMAIL_UPDATED" => "Account email updated", + + "INVALID" => "This account does not exist. It may have been deleted. Please contact us for more information.", + + "MASTER_NOT_EXISTS" => "You cannot register an account until the master account has been created!", + "MY" => "My Account", + + "SESSION_COMPROMISED" => [ + "@TRANSLATION" => "Your session has been compromised. You should log out on all devices, then log back in and make sure that your data has not been tampered with.", + "TITLE" => "Your account may have been compromised", + "TEXT" => "Someone may have used your login information to acccess this page. For your safety, all sessions were logged out. Please <a href=\"{{url}}\">log in</a> and check your account for suspicious activity. You may also wish to change your password." + ], + "SESSION_EXPIRED" => "Your session has expired. Please sign in again.", + + "SETTINGS" => [ + "@TRANSLATION" => "Account settings", + "DESCRIPTION" => "Update your account settings, including email, name, and password.", + "UPDATED" => "Account settings updated" + ], + + "TOOLS" => "Account tools", + + "UNVERIFIED" => "Your account has not yet been verified. Check your emails / spam folder for account activation instructions.", + + "VERIFICATION" => [ + "NEW_LINK_SENT" => "We have emailed a new verification link to {{email}}. Please check your inbox and spam folders for this email.", + "RESEND" => "Resend verification email", + "COMPLETE" => "You have successfully verified your account. You can now login.", + "EMAIL" => "Please enter the email address you used to sign up, and your verification email will be resent.", + "PAGE" => "Resend the verification email for your new account.", + "SEND" => "Email the verification link for my account", + "TOKEN_NOT_FOUND" => "Verification token does not exist / Account is already verified", + ] + ], + + "EMAIL" => [ + "INVALID" => "There is no account for <strong>{{email}}</strong>.", + "IN_USE" => "Email <strong>{{email}}</strong> is already in use.", + "VERIFICATION_REQUIRED" => "Email (verification required - use a real address!)" + ], + + "EMAIL_OR_USERNAME" => "Username or email address", + + "FIRST_NAME" => "First name", + + "HEADER_MESSAGE_ROOT" => "YOU ARE SIGNED IN AS THE ROOT USER", + + "LAST_NAME" => "Last name", + "LOCALE" => [ + "ACCOUNT" => "The language and locale to use for your account", + "INVALID" => "<strong>{{locale}}</strong> is not a valid locale." + ], + "LOGIN" => [ + "@TRANSLATION" => "Login", + "ALREADY_COMPLETE" => "You are already logged in!", + "SOCIAL" => "Or login with", + "REQUIRED" => "Sorry, you must be logged in to access this resource." + ], + "LOGOUT" => "Logout", + + "NAME" => "Name", + + "NAME_AND_EMAIL" => "Name and email", + + "PAGE" => [ + "LOGIN" => [ + "DESCRIPTION" => "Sign in to your {{site_name}} account, or register for a new account.", + "SUBTITLE" => "Register for free, or sign in with an existing account.", + "TITLE" => "Let's get started!", + ] + ], + + "PASSWORD" => [ + "@TRANSLATION" => "Password", + + "BETWEEN" => "Between {{min}}-{{max}} characters", + + "CONFIRM" => "Confirm password", + "CONFIRM_CURRENT" => "Please confirm your current password", + "CONFIRM_NEW" => "Confirm New Password", + "CONFIRM_NEW_EXPLAIN" => "Re-enter your new password", + "CONFIRM_NEW_HELP" => "Required only if selecting a new password", + "CREATE" => [ + "@TRANSLATION" => "Create Password", + "PAGE" => "Choose a password for your new account.", + "SET" => "Set Password and Sign In" + ], + "CURRENT" => "Current Password", + "CURRENT_EXPLAIN" => "You must confirm your current password to make changes", + + "FORGOTTEN" => "Forgotten Password", + "FORGET" => [ + "@TRANSLATION" => "I forgot my password", + + "COULD_NOT_UPDATE" => "Couldn't update password.", + "EMAIL" => "Please enter the email address you used to sign up. A link with instructions to reset your password will be emailed to you.", + "EMAIL_SEND" => "Email Password Reset Link", + "INVALID" => "This password reset request could not be found, or has expired. Please try <a href=\"{{url}}\">resubmitting your request<a>.", + "PAGE" => "Get a link to reset your password.", + "REQUEST_CANNED" => "Lost password request cancelled.", + "REQUEST_SENT" => "If the email <strong>{{email}}</strong> matches an account in our system, a password reset link will be sent to <strong>{{email}}</strong>." + ], + + "HASH_FAILED" => "Password hashing failed. Please contact a site administrator.", + "INVALID" => "Current password doesn't match the one we have on record", + "NEW" => "New Password", + "NOTHING_TO_UPDATE" => "You cannot update with the same password", + + "RESET" => [ + "@TRANSLATION" => "Reset Password", + "CHOOSE" => "Please choose a new password to continue.", + "PAGE" => "Choose a new password for your account.", + "SEND" => "Set New Password and Sign In" + ], + + "UPDATED" => "Account password updated" + ], + + "PROFILE" => [ + "SETTINGS" => "Profile settings", + "UPDATED" => "Profile settings updated" + ], + + "RATE_LIMIT_EXCEEDED" => "The rate limit for this action has been exceeded. You must wait another {{delay}} seconds before you will be allowed to make another attempt.", + + "REGISTER" => "Register", + "REGISTER_ME" => "Sign me up", + "REGISTRATION" => [ + "BROKEN" => "We're sorry, there is a problem with our account registration process. Please contact us directly for assistance.", + "COMPLETE_TYPE1" => "You have successfully registered. You can now sign in.", + "COMPLETE_TYPE2" => "You have successfully registered. A link to activate your account has been sent to <strong>{{email}}</strong>. You will not be able to sign in until you complete this step.", + "DISABLED" => "We're sorry, account registration has been disabled.", + "LOGOUT" => "I'm sorry, you cannot register for an account while logged in. Please log out first.", + "WELCOME" => "Registration is fast and simple." + ], + "REMEMBER_ME" => "Keep me signed in", + "REMEMBER_ME_ON_COMPUTER" => "Remember me on this computer (not recommended for public computers)", + + "SIGN_IN_HERE" => "Already have an account? <a href=\"{{url}}\">Sign in here.</a>", + "SIGNIN" => "Sign in", + "SIGNIN_OR_REGISTER" => "Sign in or register", + "SIGNUP" => "Sign Up", + + "TOS" => "Terms and Conditions", + "TOS_AGREEMENT" => "By registering an account with {{site_title}}, you accept the <a {{link_attributes | raw}}>terms and conditions</a>.", + "TOS_FOR" => "Terms and Conditions for {{title}}", + + "USERNAME" => [ + "@TRANSLATION" => "Username", + + "CHOOSE" => "Choose a unique username", + "INVALID" => "Invalid username", + "IN_USE" => "Username <strong>{{user_name}}</strong> is already in use.", + "NOT_AVAILABLE" => "Username <strong>{{user_name}}</strong> is not available. Choose a different name, or click 'suggest'." + ], + + "USER_ID_INVALID" => "The requested user id does not exist.", + "USER_OR_EMAIL_INVALID" => "Username or email address is invalid.", + "USER_OR_PASS_INVALID" => "User not found or password is invalid.", + + "WELCOME" => "Welcome back, {{first_name}}" +]; diff --git a/login/app/sprinkles/account/locale/en_US/validate.php b/login/app/sprinkles/account/locale/en_US/validate.php new file mode 100755 index 0000000..00c0aef --- /dev/null +++ b/login/app/sprinkles/account/locale/en_US/validate.php @@ -0,0 +1,19 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * US English message token translations for the 'account' sprinkle. + * + * @package userfrosting\i18n\en_US + * @author Alexander Weissman + */ + +return [ + "VALIDATE" => [ + "PASSWORD_MISMATCH" => "Your password and confirmation password must match.", + "USERNAME" => "Username may consist only of lowercase letters, numbers, '.', '-', and '_'." + ] +]; diff --git a/login/app/sprinkles/account/locale/es_ES/messages.php b/login/app/sprinkles/account/locale/es_ES/messages.php new file mode 100755 index 0000000..aa8b8ed --- /dev/null +++ b/login/app/sprinkles/account/locale/es_ES/messages.php @@ -0,0 +1,189 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Spanish message token translations for the 'account' sprinkle. + * + * @package userfrosting\i18n\es_ES + * @author rafa31gz + */ + +return [ + "ACCOUNT" => [ + "@TRANSLATION" => "Perfil", + + "ACCESS_DENIED" => "Hmm, parece que no tienes permiso para hacer eso.", + + "DISABLED" => "Esta cuenta se ha inhabilitado. Por favor contáctenos para más información.", + + "EMAIL_UPDATED" => "Correo electrónico de la cuenta actualizado", + + "INVALID" => "Esta cuenta no existe. Puede haber sido eliminado. Por favor contáctenos para más información.", + + "MASTER_NOT_EXISTS" => "No puede registrar una cuenta hasta que se haya creado la cuenta principal.", + "MY" => "Mi Perfil", + + "SESSION_COMPROMISED" => [ + "@TRANSLATION" => "Su sesión ha sido comprometida. Debe desconectarse de todos los dispositivos y, a continuación, volver a iniciar sesión y asegurarse de que sus datos no han sido manipulados.", + "TITLE" => "Es posible que su cuenta se haya visto comprometida.", + "TEXT" => "Alguien puede haber utilizado su información de acceso para acceder a esta página. Para su seguridad, todas las sesiones se cerraron. <a href=\"{{url}}\"> ingrese </a> y compruebe si su actividad es sospechosa en su cuenta. También puede cambiar su contraseña." + ], + "SESSION_EXPIRED" => "Su sesión ha caducado. Inicie sesión nuevamente.", + + "SETTINGS" => [ + "@TRANSLATION" => "Configuraciones de la cuenta", + "DESCRIPTION" => "Actualice la configuración de su cuenta, incluido el correo electrónico, el nombre y la contraseña.", + "UPDATED" => "Configuración de la cuenta actualizada" + ], + + "TOOLS" => "Herramientas de la cuenta", + + "UNVERIFIED" => "Tu cuenta aún no se ha verificado. Revise sus correos electrónicos / carpeta de spam para obtener instrucciones sobre la activación de la cuenta.", + + "VERIFICATION" => [ + "NEW_LINK_SENT" => "Hemos enviado por correo electrónico un nuevo enlace de verificación a {{email}}. Comprueba tu bandeja de entrada y las carpetas de spam para este correo electrónico.", + "RESEND" => "Reenviar correo electrónico de verificación", + "COMPLETE" => "Ha verificado correctamente su cuenta. Ahora puede iniciar sesión.", + "EMAIL" => "Ingrese la dirección de correo electrónico que utilizó para registrarse y su correo electrónico de verificación será enviado de nuevo.", + "PAGE" => "Vuelva a enviar el correo electrónico de verificación de su nueva cuenta.", + "SEND" => "Reenviar correo de verificación", + "TOKEN_NOT_FOUND" => "El token de verificación no existe / La cuenta ya está verificada", + ] + ], + + "EMAIL" => [ + "INVALID" => "No hay cuenta para <strong> {{email}} </strong>.", + "IN_USE" => "El correo electrónico <strong> {{email}} </strong> ya está en uso.", + "VERIFICATION_REQUIRED" => "Correo electrónico (se requiere verificación - ¡use una dirección real!)" + ], + + "EMAIL_OR_USERNAME" => "Nombre de usuario o dirección de correo electrónico", + + "FIRST_NAME" => "Nombre", + + "HEADER_MESSAGE_ROOT" => "USTED HA INGRESADO COMO USUARIO ROOT", + + "LAST_NAME" => "Apellidos", + + "LOCALE" => [ + "ACCOUNT" => "El idioma y la configuración regional para utilizar en su cuenta", + "INVALID" => "<strong>{{locale}}</strong> no es un idioma válido." + ], + + "LOGIN" => [ + "@TRANSLATION" => "Acceder", + "ALREADY_COMPLETE" => "¡Ya se ha autentificado!", + "SOCIAL" => "O ingrese con", + "REQUIRED" => "Lo sentimos, debes iniciar sesión para acceder a este recurso." + ], + + "LOGOUT" => "Cerrar sesión", + + "NAME" => "Nombre", + + "NAME_AND_EMAIL" => "Nombre y correo electrónico", + + "PAGE" => [ + "LOGIN" => [ + "DESCRIPTION" => "Inicie sesión en su cuenta de {{site_name}} o regístrese para obtener una nueva cuenta.", + "SUBTITLE" => "Regístrese gratis o inicie sesión con una cuenta existente.", + "TITLE" => "¡Empecemos!", + ] + ], + + "PASSWORD" => [ + "@TRANSLATION" => "Contraseña", + + "BETWEEN" => "Entre {{min}} - {{max}} (recomendado 12)", + + "CONFIRM" => "Confirmar contraseña", + "CONFIRM_CURRENT" => "Por favor, confirma tu contraseña actual", + "CONFIRM_NEW" => "Confirmar nueva contraseña", + "CONFIRM_NEW_EXPLAIN" => "Vuelve a ingresar tu nueva contraseña", + "CONFIRM_NEW_HELP" => "Sólo se requiere si se selecciona una nueva contraseña", + "CREATE" => [ + "@TRANSLATION" => "Crear contraseña", + "PAGE" => "Elija una contraseña para su nueva cuenta.", + "SET" => "Establecer contraseña e iniciar sesión" + ], + "CURRENT" => "Contraseña actual", + "CURRENT_EXPLAIN" => "Debe confirmar su contraseña actual para realizar cambios", + + "FORGOTTEN" => "Contraseña olvidada", + "FORGET" => [ + "@TRANSLATION" => "Olvidé mi contraseña", + + "COULD_NOT_UPDATE" => "No se pudo actualizar la contraseña.", + "EMAIL" => "Introduce la dirección de correo electrónico que utilizaste para registrarte. Se le enviará por correo electrónico un enlace con las instrucciones para restablecer su contraseña.", + "EMAIL_SEND" => "Contraseña de correo electrónico Restablecer enlace", + "INVALID" => "No se pudo encontrar esta solicitud de restablecimiento de contraseña o ha caducado. Intenta <a href=\"{{url}}\"> volver a enviar tu solicitud <a>.", + "PAGE" => "Obtenga un enlace para restablecer su contraseña.", + "REQUEST_CANNED" => "Se ha cancelado la solicitud de contraseña perdida.", + "REQUEST_SENT" => "Se ha enviado un enlace de restablecimiento de contraseña a <strong> {{email}} </strong>." + ], + + "RESET" => [ + "@TRANSLATION" => "Restablecer la contraseña", + "CHOOSE" => "Por favor, elija una nueva contraseña para continuar.", + "PAGE" => "Elige una nueva contraseña para tu cuenta.", + "SEND" => "Establecer nueva contraseña e iniciar sesión" + ], + + "HASH_FAILED" => "El hash de la contraseña ha fallado. Póngase en contacto con un administrador del sitio.", + "INVALID" => "La contraseña actual no coincide con la que tenemos registrada", + "NEW" => "Nueva contraseña", + "NOTHING_TO_UPDATE" => "No se puede actualizar con la misma contraseña", + "UPDATED" => "Contraseña de la cuenta actualizada" + ], + + "PROFILE" => [ + "SETTINGS" => "Configuración de perfil", + "UPDATED" => "Configuración del perfil actualizada" + ], + + "RATE_LIMIT_EXCEEDED" => "Se ha superado el límite de velocidad para esta acción. Debe esperar otro {{delay}} segundos antes de que se le permita hacer otro intento.", + + "REGISTER" => "Registro", + "REGISTER_ME" => "Inscríbeme", + "REGISTRATION" => [ + "BROKEN" => "Lo sentimos, hay un problema con nuestro proceso de registro de cuenta. Póngase en contacto con nosotros directamente para obtener ayuda.", + "COMPLETE_TYPE1" => "Se ha registrado exitosamente. Ahora puede iniciar sesión.", + "COMPLETE_TYPE2" => "Se ha registrado exitosamente. Se ha enviado un enlace para activar tu cuenta a <strong> {{email}} </strong>. No podrá iniciar sesión hasta que complete este paso.", + "DISABLED" => "Lo sentimos, el registro de cuenta se ha deshabilitado.", + "LOGOUT" => "Lo siento, no puede registrarse para una cuenta mientras está conectado. Por favor, cierra la sesión primero.", + "WELCOME" => "El registro es rápido y sencillo." + ], + + "REMEMBER_ME" => "¡Recuérdame!", + "REMEMBER_ME_ON_COMPUTER" => "Recuérdeme en este ordenador (no se recomienda para ordenadores públicos)", + + "SIGNIN" => "Iniciar sesión", + "SIGNIN_OR_REGISTER" => "Ingresa o Registro", + "SIGNUP" => "Regístrate", + "SUGGEST" => "Sugerencia", + "HAVE_ACCOUNT" => "¿Ya tienes una cuenta?", + "SIGN_IN_HERE"=> "¿Ya tienes una cuenta? <a href=\"{{url}}\"> Acceda aquí. </a>", + + + "TOS" => "Términos y Condiciones", + "TOS_AGREEMENT" => "Al registrar una cuenta con {{site_title}}, acepta los <a {{link_attributes | raw}}> términos y condiciones </a>.", + "TOS_FOR" => "Términos y condiciones para {{title}}", + + "USERNAME" => [ + "@TRANSLATION" => "Nombre de usuario", + + "CHOOSE" => "Elige un nombre de usuario único", + "INVALID" => "Nombre de usuario no válido", + "IN_USE" => "El nombre de usuario <strong> {{user_name}} </strong> ya está en uso.", + "NOT_AVAILABLE" => "El nombre de usuario <strong> {{user_name}} </strong> no está disponible. Elija otro nombre o haga clic en \"sugerir\"." + ], + + "USER_ID_INVALID" => "El ID de usuario solicitado no existe.", + "USER_OR_EMAIL_INVALID" => "El nombre de usuario o la dirección de correo electrónico no son válidos.", + "USER_OR_PASS_INVALID" => "Usuario no encontrado o la contraseña no es válida.", + + "WELCOME" => "Bienvenido de nuevo, {{first_name}}" +]; diff --git a/login/app/sprinkles/account/locale/es_ES/validate.php b/login/app/sprinkles/account/locale/es_ES/validate.php new file mode 100755 index 0000000..c8ea0a4 --- /dev/null +++ b/login/app/sprinkles/account/locale/es_ES/validate.php @@ -0,0 +1,19 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Spanish message token translations for the 'account' sprinkle. + * + * @package userfrosting\i18n\es_ES + * @author rafa31gz + */ + +return [ + "VALIDATE" => [ + "PASSWORD_MISMATCH" => "Su contraseña y contraseña de confirmación deben coincidir.", + "USERNAME" => "El nombre de usuario puede consistir sólo en letras minúsculas, números, '.', '-' y '_'." + ] +]; diff --git a/login/app/sprinkles/account/locale/fa/messages.php b/login/app/sprinkles/account/locale/fa/messages.php new file mode 100755 index 0000000..22623ba --- /dev/null +++ b/login/app/sprinkles/account/locale/fa/messages.php @@ -0,0 +1,178 @@ +<?php + +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Standard Farsi/Persian message token translations for the 'account' sprinkle. + * + * @package userfrosting\i18n\fa + * @author aminakbari + */ + +return [ + "ACCOUNT" => [ + "@TRANSLATION" => "حساب", + + "ACCESS_DENIED" => "به نظر می آید که شما اجازه انجام این کار را ندارید", + + "DISABLED" => "این حساب کاربری غیر فعال شده است. برای اطلاعات بیشتر، لطفا با ما تماس برقرار کنید.", + + "EMAIL_UPDATED" => "آدرس پست الکترونیکی حساب، به روز رسانی شد", + + "INVALID" => "این اکانت موجود نیست. ممکن است که حذف شده باشد. برای اطلاعات بیشتر، لطفا با ما تماس برقرار کنید.", + + "MASTER_NOT_EXISTS" => "تا زمانی که حساب اصلی ساخته نشده است نمیتوانید حساب کاربری جدیدی بسازید.", + "MY" => "حساب من", + + "SESSION_COMPROMISED" => "ممکن است سژن شما مورد حمله واقع شده باشد. بهتر است با همه دستگاه های خود از وب سایت خارج شوید و دوباره وارد شوید. همچنین توجه بفرمایید که اطلاعات حسابتان، مورد حمله واقع نشده باشد. ", + "SESSION_COMPROMISED_TITLE" => "ممکن است که اکانت شما مورد حمله واقع شده باشد", + "SESSION_EXPIRED" => "سژن شما به پایان رسیده است. لطفا دوباره وارد شوید.", + + "SETTINGS" => [ + "@TRANSLATION" => "تنظیمات حساب", + "DESCRIPTION" => "اطلاعات حسابتان یعنی پست الکترونیکی،نام و گذرواژه خود را به روز رسانی کنید", + "UPDATED" => "تنظیمات حساب به روز رسانی شد" + ], + + "TOOLS" => "ابزار حساب", + + "UNVERIFIED" => "شما هنوز آدرس پست الکترونیکی خود را فعال نکرده اید. برای فعال سازی لطفا ایمیل خود را چک کنید.", + + "VERIFICATION" => [ + "NEW_LINK_SENT" => "لینک فعال سازی برای ایمیل {{email}} ارسال شد. لطفا ایمیل خود را چک کنید.", + "RESEND" => "ارسال دوباره ایمیل فعال سازی", + "COMPLETE" => "شما پست الکترونیکی خود را با موفقیت فعال سازی کردید. حالا می توانید وارد شوید.", + "EMAIL" => "لطفا آدرس پست الکترونیکی که با آن ثبت نام کردید وارد کنید تا ایمیل فعال سازی دوباره برایتان ارسال شود.", + "PAGE" => "ارسال دوباره ایمیل فعال سازی برای حساب جدید شما", + "SEND" => "ارسال ایمیل فعال سازی برای حساب کاربری", + "TOKEN_NOT_FOUND" => "این حساب کاربری یا قبلا فعال شده است و یا کد فعال سازی موجود نیست.", + ] + ], + + "EMAIL" => [ + "INVALID" => "حساب کاربری با <strong>{{email}}</strong> ثبت نشده است.", + "IN_USE" => "ایمیل <strong>{{email}}</strong> قبلا استفاده شده است", + "VERIFICATION_REQUIRED" => "آدرس پست الکترونیکی را بصورت صحیح وارد کنید" + ], + + "EMAIL_OR_USERNAME" => "نام کاربری یا آدرس پست الکترونیکی", + + "FIRST_NAME" => "نام", + + "HEADER_MESSAGE_ROOT" => "شما بعنوان کاربر اصلی وارد شده اید", + + "LAST_NAME" => "نام خانوادگی", + + "LOCALE" => [ + "ACCOUNT" => "زبان انتخابی برای حساب شما", + "INVALID" => "<strong>{{locale}}</strong> زبان صحیحی نیست" + ], + + "LOGIN" => [ + "@TRANSLATION" => "ورود", + "ALREADY_COMPLETE" => "شما قبلا وارد شده اید.", + "SOCIAL" => "یا با روش های زیر وارد شوید", + "REQUIRED" => "برای دیدن این صفحه لازم است که وارد شوید" + ], + + "LOGOUT" => "خروج", + + "NAME" => "نام", + + "NAME_AND_EMAIL" => "نام و پست الکترونیکی", + + "PAGE" => [ + "LOGIN" => [ + "DESCRIPTION" => "به حساب کاربری خود در {{site_name}} وارد شوید و یا حساب کاربری جدیدی بسازید", + "SUBTITLE" => "ثبت نام کنید و یا با حساب کاربری خود وارد شوید", + "TITLE" => "بیایید شروع کنیم!", + ] + ], + + "PASSWORD" => [ + "@TRANSLATION" => "گذرواژه", + + "BETWEEN" => "بین {{min}}-{{max}} حرف", + + "CONFIRM" => "رمز عبور را وارد کنید", + "CONFIRM_CURRENT" => "لطفا رمز عبور فعلی را تایید کنید", + "CONFIRM_NEW" => "رمز عبور جدید را وارد کنید", + "CONFIRM_NEW_EXPLAIN" => "رمز عبور جدید را تکرار کنید", + "CONFIRM_NEW_HELP" => "فقط زمانی لازم است که می خواهید گذرواژه جدیدی انتخاب کنید", + "CURRENT" => "گذرواژه فعلی", + "CURRENT_EXPLAIN" => "شما باید گذرواژه فعلی خود را وارد کنید تا بتوانید اطلاعات را به روز رسانی کنید", + + "FORGOTTEN" => "فراموشی گذرواژه", + "FORGET" => [ + "@TRANSLATION" => "گذرواژه خود را فراموش کرده ام", + + "COULD_NOT_UPDATE" => "گذرواژه به روز رسانی نشد", + "EMAIL" => "لطفا آدرس پست الکترونیکی که در زمان ثبت نام استفاده کردید، وارد کنید. لینک بازیابی گذرواژه برای شما ایمیل خواهد شد.", + "EMAIL_SEND" => "لینک بازیابی گذرواژه ایمیل شود", + "INVALID" => "درخواست بازیابی کذرواژه پیدا نشد و یا منقضی شده است. لطفا درخواست را <a href=\"{{url}}\">دوباره ارسال کنید<a>", + "PAGE" => "دریافت لینک بازیابی گذرواژه", + "REQUEST_CANNED" => "درخواست فراموشی گذرواژه، حذف شد.", + "REQUEST_SENT" => "ایمیل بازیابی گذرواژه به <strong>{{email}}</strong> ارسال شد." + ], + + "RESET" => [ + "@TRANSLATION" => "تغییر گذرواژه", + "CHOOSE" => "لطفا گذرواژه جدید را انتخاب کنید", + "PAGE" => "برای حساب خود، گذرواژه جدیدی انتخاب کنید.", + "SEND" => "گذرواژه جدید را انتخاب کرده و وارد شوید" + ], + + "HASH_FAILED" => "هشینگ گذرواژه با مشکل روبرو شد. لطفا با مسولین وب سایت تماس برقرار کنید", + "INVALID" => "گذرواژه فعلی درست وارد نشده است", + "NEW" => "گذرواژه جدید", + "NOTHING_TO_UPDATE" => "شما نمیتوانید همان گذرواژه را دوباره وارد کنید", + "UPDATED" => "گذرواژه به روز رسانی شد" + ], + + "PROFILE" => [ + "SETTINGS" => "تنظیمات شخصی حساب", + "UPDATED" => "تنظیمات شخصی حساب به روز رسانی شد" + ], + + "REGISTER" => "ثبت نام", + "REGISTER_ME" => "ثبت نام کن", + + "REGISTRATION" => [ + "BROKEN" => "متاسفانه پروسه ثبت نام با مشکلی روبرو شد. برای دریافت کمک لطفا با ما تماس بگیرید.", + "COMPLETE_TYPE1" => "شما با موفقیت ثبت نام کردید. حالا میتوانید وارد شوید.", + "COMPLETE_TYPE2" => "شما با موفقیت ثبت نام کردید. لینک فعال سازی حساب به آدرس پست الکترونیکیتان <strong>{{email}}</strong> ارسال شد. بدون فعال سازی نمیتوانید وارد شوید.", + "DISABLED" => "با عرض تاسف، امکان ثبت در وب سایت، غیر فعال شده است.", + "LOGOUT" => "شما همزمان این که وارد شده اید نمیتوانید حساب کاربری جدیدی بسازید. لطفا ابتدا خارج شوید.", + "WELCOME" => "سریع و ساده ثبت نام کنید" + ], + + "RATE_LIMIT_EXCEEDED" => "شما محدودیت تعداد انجام این کار را پشت سر گذاشتید. لطفا {{delay}} ثانیه دیگر صبر کرده و دوباره تلاش کنید.", + "REMEMBER_ME" => "من را به خاطر بسپار!", + "REMEMBER_ME_ON_COMPUTER" => "من را در این دستگاه به خاطر بسپار (برای دستگاه های عمومی پیشنهاد نمی شود)", + + "SIGNIN" => "ورود", + "SIGNIN_OR_REGISTER" => "ثبت نام کنید و یا وارد شوید", + "SIGNUP" => "ثبت نام", + + "TOS" => "شرایط و مقررات", + "TOS_AGREEMENT" => "با ثبت نام در {{site_title}} موافقت خود با <a {{link_attributes | raw}}>شرایط و مقررات</a> را نشان میدهید.", + "TOS_FOR" => "شرایط و مقررات {{title}}", + + "USERNAME" => [ + "@TRANSLATION" => "نام کاربری", + + "CHOOSE" => "یک نام کاربری منحصر به فرد انتخاب کنید", + "INVALID" => "نام کاربری معتبر نیست", + "IN_USE" => "نام کاربری <strong>{{user_name}}</strong> قبلا استفاده شده است", + "NOT_AVAILABLE" => "نام کاربری <strong>{{user_name}}</strong> موجود نیست. لطفا نام کاربری دیگری انتخاب کنید" + ], + + "USER_ID_INVALID" => "آی دی کاربری مد نظر شما موجود نیست", + "USER_OR_EMAIL_INVALID" => "نام کاربری و یا آدرس پست الکترونیکی معتبر نیست", + "USER_OR_PASS_INVALID" => "کاربری یافت نشد و یا گذرواژه صحیح نیست", + + "WELCOME" => "خوش آمدید {{first_name}}" +]; diff --git a/login/app/sprinkles/account/locale/fa/validate.php b/login/app/sprinkles/account/locale/fa/validate.php new file mode 100755 index 0000000..a63cae1 --- /dev/null +++ b/login/app/sprinkles/account/locale/fa/validate.php @@ -0,0 +1,20 @@ +<?php + +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Standard Farsi/Persian message token translations for the 'account' sprinkle. + * + * @package userfrosting\i18n\fa + * @author aminakbari + */ + +return [ + "VALIDATE" => [ + "PASSWORD_MISMATCH" => "گذرواژه و تکرار آن باید با یکدیگر تطبیق پیدا کنند", + "USERNAME" => "نام کاربری فقط میتواند از حروف کوچک، اعداد، '.'، '-' و '_' متشکل شوند." + ] +]; diff --git a/login/app/sprinkles/account/locale/fr_FR/messages.php b/login/app/sprinkles/account/locale/fr_FR/messages.php new file mode 100755 index 0000000..6e5a032 --- /dev/null +++ b/login/app/sprinkles/account/locale/fr_FR/messages.php @@ -0,0 +1,179 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * French message token translations for the 'account' sprinkle. + * + * @package userfrosting\i18n\fr + * @author Louis Charette + */ + +return [ + "ACCOUNT" => [ + "@TRANSLATION" => "Compte d'utilisateur", + + "ACCESS_DENIED" => "Hmm, on dirait que vous n'avez pas la permission de faire ceci.", + + "DISABLED" => "Ce compte a été désactivé. Veuillez nous contacter pour plus d'informations.", + + "EMAIL_UPDATED" => "Adresse email mise à jour", + + "INVALID" => "Ce compte n'existe pas. Il a peut-être été supprimé. Veuillez nous contacter pour plus d'informations.", + + "MASTER_NOT_EXISTS" => "Vous ne pouvez pas enregistrer un compte tant que le compte principal n'a pas été créé!", + "MY" => "Mon compte", + + "SESSION_COMPROMISED" => [ + "@TRANSLATION" => "Votre session a été compromise. Vous devez vous déconnecter de tous les périphériques, puis vous reconnecter et vous assurer que vos données n'ont pas été altérées.", + "TITLE" => "Votre compte peut avoir été compromis" + ], + "SESSION_EXPIRED" => "Votre session a expiré. Veuillez vous connecter à nouveau.", + + "SETTINGS" => [ + "@TRANSLATION" => "Paramètres du compte", + "DESCRIPTION" => "Mettez à jour les paramètres de votre compte, y compris votre adresse e-mail, votre nom et votre mot de passe.", + "UPDATED" => "Paramètres du compte mis à jour" + ], + + "TOOLS" => "Outils du compte", + + "UNVERIFIED" => "Votre compte n'a pas encore été vérifié. Vérifiez vos emails / dossier spam pour les instructions d'activation du compte.", + + "VERIFICATION" => [ + "NEW_LINK_SENT" => "Nous avons envoyé un nouveau lien de vérification à {{email}}. Veuillez vérifier vos dossiers de boîte de réception et de spam pour ce courriel.", + "RESEND" => "Renvoyer le courriel de validation", + "COMPLETE" => "Votre compte a été validé. Vous pouvez maintenant vous connecter.", + "EMAIL" => "Veuillez saisir l'adresse email que vous avez utilisée pour vous inscrire et votre courriel de vérification sera renvoyé.", + "PAGE" => "Renvoyer l'email de validation de votre nouveau compte.", + "SEND" => "Envoyer le lien de validation de mon compte", + "TOKEN_NOT_FOUND" => "Le jeton de vérification n'existe pas / Le compte est déjà vérifié", + ] + ], + + "EMAIL" => [ + "INVALID" => "Il n'y a aucun compte pour <strong>{{email}}</strong>.", + "IN_USE" => "Le email <strong>{{email}}</strong> est déjà utilisé.", + "VERIFICATION_REQUIRED" => "Email (vérification requise - utiliser une adresse réelle!)" + ], + + "EMAIL_OR_USERNAME" => "Nom d'utilisateur ou adresse email", + + "FIRST_NAME" => "Prénom", + + "HEADER_MESSAGE_ROOT" => "VOUS ÊTES CONNECTÉ EN TANT QUE L'UTILISATEUR ROOT", + + "LAST_NAME" => "Nom de famille", + + "LOCALE" => [ + "ACCOUNT" => "La langue utilisé pour votre compte d'utilisateur", + "INVALID" => "<strong>{{locale}}</strong> n'est pas une langue valide." + ], + + "LOGIN" => [ + "@TRANSLATION" => "Connexion", + "ALREADY_COMPLETE" => "Vous êtes déjà connecté!", + "SOCIAL" => "Ou se connecter avec", + "REQUIRED" => "Désolé, vous devez être connecté pour accéder à cette ressource." + ], + + "LOGOUT" => "Déconnexion", + + "NAME" => "Nom", + + "NAME_AND_EMAIL" => "Nom et email", + + "PAGE" => [ + "LOGIN" => [ + "DESCRIPTION" => "Connectez-vous à votre compte {{site_name}} ou enregistrez-vous pour un nouveau compte.", + "SUBTITLE" => "Inscrivez-vous gratuitement ou connectez-vous avec un compte existant.", + "TITLE" => "Commençons!", + ] + ], + + "PASSWORD" => [ + "@TRANSLATION" => "Mot de passe", + + "BETWEEN" => "Entre {{min}} et {{max}} charactères", + + "CONFIRM" => "Confirmer le mot de passe", + "CONFIRM_CURRENT" => "Veuillez confirmer votre mot de passe actuel", + "CONFIRM_NEW" => "Confirmer le nouveau mot de passe", + "CONFIRM_NEW_EXPLAIN" => "Confirmer le mot de passe", + "CONFIRM_NEW_HELP" => "Obligatoire uniquement si vous sélectionnez un nouveau mot de passe", + "CURRENT" => "Mot de passe actuel", + "CURRENT_EXPLAIN" => "Vous devez confirmer votre mot de passe actuel pour apporter des modifications", + + "FORGOTTEN" => "Mot de passe oublié", + "FORGET" => [ + "@TRANSLATION" => "J'ai oublié mon mot de passe", + + "COULD_NOT_UPDATE" => "Impossible de mettre à jour le mot de passe.", + "EMAIL" => "Veuillez saisir l'adresse e-mail que vous avez utilisée pour vous inscrire. Un lien avec les instructions pour réinitialiser votre mot de passe vous sera envoyé par email.", + "EMAIL_SEND" => "Envoyer le lien de réinitialisation", + "INVALID" => "Cette requête de réinitialisation de mot de passe n'a pas pu être trouvée ou a expiré. Veuillez réessayer <a href=\"{{url}}\"> de soumettre votre demande <a>.", + "PAGE" => "Obtenir un lien pour réinitialiser votre mot de passe.", + "REQUEST_CANNED" => "Demande de mot de passe perdu annulée.", + "REQUEST_SENT" => "Si l'adresse e-mail <strong>{{email}}</strong> correspond à un compte dans notre système, un lien de réinitialisation de mot de passe sera envoyé à <strong>{{email}}</strong>." + ], + + "RESET" => [ + "@TRANSLATION" => "Réinitialiser le mot de passe", + "CHOOSE" => "Veuillez choisir un nouveau mot de passe pour continuer.", + "PAGE" => "Choisissez un nouveau mot de passe pour votre compte.", + "SEND" => "Définir un nouveau mot de passe" + ], + + "HASH_FAILED" => "Le hachage du mot de passe a échoué. Veuillez contacter un administrateur de site.", + "INVALID" => "Le mot de passe actuel ne correspond pas à celui que nous avons au dossier", + "NEW" => "Nouveau mot de passe", + "NOTHING_TO_UPDATE" => "Vous ne pouvez pas mettre à jour avec le même mot de passe", + "UPDATED" => "Mot de passe du compte mis à jour" + ], + + "PROFILE" => [ + "SETTINGS" => "Paramètres du profil", + "UPDATED" => "Paramètres du profil mis à jour" + ], + + "REGISTER" => "S'inscrire", + "REGISTER_ME" => "S'inscrire", + + "REGISTRATION" => [ + "BROKEN" => "Nous sommes désolés, il ya un problème avec notre processus d'enregistrement de compte. Veuillez nous contacter directement pour obtenir de l'aide.", + "COMPLETE_TYPE1" => "Vous êtes inscrit avec succès. Vous pouvez maintenant vous connecter.", + "COMPLETE_TYPE2" => "Vous êtes inscrit avec succès. Vous recevrez bientôt un e-mail de validation contenant un lien pour activer votre compte. Vous ne pourrez pas vous connecter avant d'avoir terminé cette étape.", + "DISABLED" => "Désolé, l'enregistrement de compte a été désactivé.", + "LOGOUT" => "Désolé, vous ne pouvez pas vous inscrire tout en étant connecté. Veuillez vous déconnecter en premier.", + "WELCOME" => "L'inscription est rapide et simple." + ], + + "RATE_LIMIT_EXCEEDED" => "La limite de tentatives pour cette action a été dépassée. Vous devez attendre {{delay}} secondes avant de pouvoir effectuer une autre tentative.", + "REMEMBER_ME" => "Se souvenir de moi!", + "REMEMBER_ME_ON_COMPUTER" => "Se souvenir de moi sur cet ordinateur (non recommandé pour les ordinateurs publics)", + + "SIGNIN" => "Se connecter", + "SIGNIN_OR_REGISTER" => "Se connecter ou s'inscrire", + "SIGNUP" => "S'inscrire", + + "TOS" => "Termes et conditions", + "TOS_AGREEMENT" => "En créant un compte avec {{site_title}}, vous acceptez les <a {{link_attributes | raw}}>termes et conditions</a>.", + "TOS_FOR" => "Termes et conditions pour {{title}}", + + "USERNAME" => [ + "@TRANSLATION" => "Nom d'utilisateur", + + "CHOOSE" => "Choisissez un nom d'utilisateur unique", + "INVALID" => "Nom d'utilisateur invalide", + "IN_USE" => "Le nom d'utilisateur '{{username}}' est déjà utilisé.", + "NOT_AVAILABLE" => "Le nom d'utilisateur <strong>{{user_name}}</strong> n'est pas disponible. Choisissez un autre nom, ou cliquez sur « suggérer »." + ], + + "USER_ID_INVALID" => "L'identifiant d'utilisateur demandé n'existe pas.", + "USER_OR_EMAIL_INVALID" => "Nom d'utilisateur ou adresse e-mail non valide.", + "USER_OR_PASS_INVALID" => "Nom d'utilisateur ou mot de passe incorrect.", + + "WELCOME" => "Bienvenue {{first_name}}" +]; diff --git a/login/app/sprinkles/account/locale/fr_FR/validate.php b/login/app/sprinkles/account/locale/fr_FR/validate.php new file mode 100755 index 0000000..44b1bc1 --- /dev/null +++ b/login/app/sprinkles/account/locale/fr_FR/validate.php @@ -0,0 +1,18 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * French message token translations for the 'account' sprinkle. + * + * @package userfrosting\i18n\fr + * @author Louis Charette + */ + +return [ + "VALIDATE" => [ + "PASSWORD_MISMATCH" => "Votre mot de passe et votre mot de passe de confirmation doivent correspondre." + ] +]; diff --git a/login/app/sprinkles/account/locale/it_IT/messages.php b/login/app/sprinkles/account/locale/it_IT/messages.php new file mode 100755 index 0000000..fee2e8c --- /dev/null +++ b/login/app/sprinkles/account/locale/it_IT/messages.php @@ -0,0 +1,186 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Italian message token translations for the 'account' sprinkle. + * This translation was generated with Google translate. Please contribute if you are a native speaker. + * + * @package userfrosting\i18n\it + * @author Alexander Weissman + * @author Pietro Marangon (@Pe46dro) + */ + +return [ + "ACCOUNT" => [ + "@TRANSLATION" => "Account", + + "ACCESS_DENIED" => "Sembra tu non abbiamo il permesso di fare questo.", + + "DISABLED" => "Questo account è stato disattivato, contattaci per maggiori informazioni", + + "EMAIL_UPDATED" => "Email aggiornata", + + "INVALID" => "Questo account non esiste. Può essere stato cancellato. Vi preghiamo di contattarci per ulteriori informazioni.", + + "MASTER_NOT_EXISTS" => "Non puoi registrare un account finche l'account primario non sarà creato!", + "MY" => "Il mio account", + + "SESSION_COMPROMISED" => [ + "@TRANSLATION" => "La tua sessione è stata compromessa. Devi eseguire il logout su tutti i dispositivi, quindi riaccenderti e assicurati che i tuoi dati non siano stati manomessi.", + "TITLE" => "Il tuo account potrebbe essere stato compromesso", + "TEXT" => "Qualcuno potrebbe aver utilizzato le tue informazioni di accesso per accedere a questa pagina. Per la tua sicurezza tutte le sessioni sono state disconnesse. <a href=\"{{url}}\">Accedi</a> e controlla l'account per attività sospette. Potresti anche desiderare di cambiare la tua password." + ], + "SESSION_EXPIRED" => "La tua sessione è scaduta. Accedi nuovamente.", + + "SETTINGS" => [ + "@TRANSLATION" => "Impostazioni dell 'account", + "DESCRIPTION" => "Aggiorna le impostazioni del tuo account, tra cui email, nome e password.", + "UPDATED" => "Impostazioni account aggiornate" + ], + + "TOOLS" => "Account tools", + + "UNVERIFIED" => "Il tuo account non è stato attivato. Controlla nella tua mail ( anche nella cartella dello spam ) per riceve le instruzioni per attivare il tuo account", + + "VERIFICATION" => [ + "NEW_LINK_SENT" => "Ti è stato inviato un nuovo codice di attivazione, controlla la tua email ({{email}}).", + "RESEND" => "Invia nuovamente email di verifica.", + "COMPLETE" => "Hai verificato con successo il tuo account. È ora possibile accedere.", + "EMAIL" => "Inserisci l'indirizzo email che hai utilizzato per registrarti e la tua email di verifica sarà resentata.", + "PAGE" => "Ripeti l'email di verifica per il tuo nuovo account.", + "SEND" => "Inviilo il collegamento di verifica per il mio account", + "TOKEN_NOT_FOUND" => "Il token non esiste / l'account è già stato attivato" + ] + ], + + "EMAIL" => [ + "INVALID" => "Non esiste alcun account per <strong>{{email}}</strong>.", + "IN_USE" => "L'email '{{email}}' è già in uso", + "VERIFICATION_REQUIRED" => "Email (verifica richiesta - utilizzare un indirizzo reale!)" + ], + + "EMAIL_OR_USERNAME" => "Username o Indirizzo Email", + + "FIRST_NAME" => "Nome", + + "HEADER_MESSAGE_ROOT" => "LOGGATO COME ROOT", + + "LAST_NAME" => "Cognome", + "LOCALE" => [ + "ACCOUNT" => "La lingua e la località da utilizzare per il tuo account", + "INVALID" => "<strong>{{locale}}</strong> non è una località valida.", + + + ], + "LOGIN" => [ + "@TRANSLATION" => "Accesso", + "ALREADY_COMPLETE" => "Sei già loggato!", + "SOCIAL" => "O accedi con", + "REQUIRED" => "Devi essere loggato per accedere a questa risorsa" + ], + "LOGOUT" => "Logout", + + "NAME" => "Nome", + + "NAME_AND_EMAIL" => "Nome e email", + + "PAGE" => [ + "LOGIN" => [ + "DESCRIPTION" => "Accedi al tuo account {{site_name}} o registrati per un nuovo account.", + "SUBTITLE" => "Registrati gratuitamente o accedi con un account esistente.", + "TITLE" => "Iniziamo!", + ] + ], + + "PASSWORD" => [ + "@TRANSLATION" => "Password", + + "BETWEEN" => "La password deve essere tra {{min}} e i {{max}} caratteri", + + "CONFIRM" => "Conferma la password", + "CONFIRM_CURRENT" => "Conferma la password attuale", + "CONFIRM_NEW" => "Conferma la tua nuova password", + "CONFIRM_NEW_EXPLAIN" => "Inserisci nuovamente la nuova password", + "CONFIRM_NEW_HELP" => "Richiesto solo se si seleziona una nuova password", + "CREATE" => [ + "@TRANSLATION" => "Crea password", + "PAGE" => "Scegli una password per il tuo nuovo account.", + "SET" => "Imposta password e accedi" + ], + "CURRENT" => "Password attuale", + "CURRENT_EXPLAIN" => "Devi confermare la tua password corrente per apportare modifiche", + + "FORGOTTEN" => "Password dimenticata", + "FORGET" => [ + "@TRANSLATION" => "Ho dimenticato la mia password", + + "COULD_NOT_UPDATE" => "Password non aggiornata", + "EMAIL" => "Inserisci l'indirizzo email che hai utilizzato per iscriverti. Un collegamento con le istruzioni per reimpostare la tua password verrà inviata via email.", + "EMAIL_SEND" => "Email link di resetta password", + "INVALID" => "Questa richiesta di ripristino della password non è stata trovata o è scaduta. Prova a <a href=\"{{url}}\">riprovare</a> la tua richiesta.", + "PAGE" => "Ottieni un collegamento per reimpostare la tua password.", + "REQUEST_CANNED" => "Richiesta di recupero password cancellata.", + "REQUEST_SENT" => "Se l'email <strong>{{email}}</strong> corrisponde a un account nel nostro sistema, verrà inviato un collegamento per la reimpostazione della password a <strong>{{email}}</strong>." + ], + + "HASH_FAILED" => "Hash della password fallito. Contatta l'amministratore di sistema.", + "INVALID" => "La password corrente non corrisponde con quella in memoria", + "NEW" => "Nuova Password", + "NOTHING_TO_UPDATE" => "Non puoi aggiornare con la stessa password", + + "RESET" => [ + "@TRANSLATION" => "Resetta la Password", + "CHOOSE" => "Inserisci la tua nuova password", + "PAGE" => "Scegli una nuova password per il tuo account.", + "SEND" => "Impostare nuova password e accedere" + ], + + "UPDATED" => "Password aggiornata" + ], + + "PROFILE" => [ + "SETTINGS" => "Impostazioni del profilo", + "UPDATED" => "Le impostazioni del profilo sono aggiornate" + ], + + "RATE_LIMIT_EXCEEDED" => "Il limite di velocità per questa azione è stato superato. Devi aspettare un altro {{delay}} secondi prima che ti sia permesso di fare un altro tentativo.", + "REGISTER" => "Registrare", + "REGISTER_ME" => "Iscrivimi", + "REGISTRATION" => [ + "BROKEN" => "Ci dispiace, c'è un problema con il nostro processo di registrazione dell'account. Vi preghiamo di contattarci direttamente per assistenza.", + "COMPLETE_TYPE1" => "Sei stato registrato con successo ora puoi eseguire il login", + "COMPLETE_TYPE2" => "Sei stato registrato con successo. Riceverai presto una mail a <strong>{{email}}</strong> per l'attivazione. Devi attivare il tuo account prima di eseguire il login.", + "DISABLED" => "La registrazione di nuovi account è stata bloccata", + "LOGOUT" => "Non è possibile registrare un account mentre si è loggati", + "WELCOME" => "La registrazione è semplice e veloce" + ], + "REMEMBER_ME" => "Ricordami!", + "REMEMBER_ME_ON_COMPUTER" => "Ricordami su questo computer (non consigliato per i computer pubblici)", + + "SIGN_IN_HERE" => "Hai già un account? <a href=\"{{url}}\">Accedi qui</a>", + "SIGNIN" => "Accedi", + "SIGNIN_OR_REGISTER" => "Accedi o registri", + "SIGNUP" => "Registrazione", + + "TOS" => "Termini e condizioni", + "TOS_AGREEMENT" => "Registrando un account con {{site_title}}, accetti il <a {{link_attributes | raw}}>termini e condizioni</a>.", + "TOS_FOR" => "Termini e condizioni per {{title}}", + + "USERNAME" => [ + "@TRANSLATION" => "Username", + + "CHOOSE" => "Inserisci il tuo username", + "INVALID" => "Username non valido", + "IN_USE" => "Il nome utente '{{user_name}}' è già in uso", + "NOT_AVAILABLE" => "Il nome utente <strong>{{user_name}}</strong> non è disponibile. Scegli un nome diverso, oppure fai clic su \"suggerisci\"." + ], + + "USER_ID_INVALID" => "User ID richiesto non è valido", + "USER_OR_EMAIL_INVALID" => "L'indirizzo mail o il nome utente non sono validi", + "USER_OR_PASS_INVALID" => "Il nome utente o la password non sono validi", + + "WELCOME" => "Bentornato, {{display_name}}" +]; diff --git a/login/app/sprinkles/account/locale/it_IT/validate.php b/login/app/sprinkles/account/locale/it_IT/validate.php new file mode 100755 index 0000000..713ccba --- /dev/null +++ b/login/app/sprinkles/account/locale/it_IT/validate.php @@ -0,0 +1,21 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Italian message token translations for the 'account' sprinkle. + * This translation was generated with Google translate. Please contribute if you are a native speaker. + * + * @package userfrosting\i18n\it + * @author Alexander Weissman + * @author Pietro Marangon (@Pe46dro) + */ + +return [ + "VALIDATE" => [ + "PASSWORD_MISMATCH" => "I due campi devono combaciare", + "USERNAME" => "L'username può essere composto da caratteri alfanumerici, '.', '-', e '_'." + ] +]; diff --git a/login/app/sprinkles/account/locale/pt_PT/messages.php b/login/app/sprinkles/account/locale/pt_PT/messages.php new file mode 100755 index 0000000..3db4200 --- /dev/null +++ b/login/app/sprinkles/account/locale/pt_PT/messages.php @@ -0,0 +1,166 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Portuguese message token translations for the 'account' sprinkle. + * + * @package userfrosting\i18n\pt + * @author Bruno Silva (brunomnsilva@gmail.com) + */ + +return [ + "ACCOUNT" => [ + "@TRANSLATION" => "Conta", + + "ACCESS_DENIED" => "Hmm, parece que não tem permissões para fazer isso.", + + "DISABLED" => "Esta conta foi desativada. Por favor contacte-nos para mais informações.", + + "EMAIL_UPDATED" => "Email da conta atualizado", + + "INVALID" => "Esta conta não existe. Pode ter sido removida. Por favor contacte-nos para mais informações.", + + "MASTER_NOT_EXISTS" => "Não pode registrar uma conta enquanto a conta principal não for criada!", + "MY" => "A minha conta", + + "SESSION_COMPROMISED" => [ + "@TRANSLATION" => "A sua sessão foi comprometida. Deverá fechar todas as sessões, voltar a iniciar sessão e verificar que os seus dados não foram alterados por alheios.", + "TITLE" => "A sua sessão pode ter sido comprometida" + ], + "SESSION_EXPIRED" => "A sua sessão expirou. Por favor inicie nova sessão.", + + "SETTINGS" => [ + "@TRANSLATION" => "Definições de conta", + "DESCRIPTION" => "Atualize as suas definições, incluindo email, nome e password.", + "UPDATED" => "Definições de conta atualizadas" + ], + + "TOOLS" => "Ferramentas de conta", + + "UNVERIFIED" => "A sua conta ainda não foi verificada. Consulte o seu email (incluindo a pasta de spam) para instruções de ativação.", + + "VERIFICATION" => [ + "NEW_LINK_SENT" => "Enviámos um link de verificação para o endereço {{email}}. Por favor consulte o seu email (incluindo a pasta de spam).", + "RESEND" => "Enviar novamente email de verificação", + "COMPLETE" => "Verificou com sucesso a sua conta. Pode iniciar sessão.", + "EMAIL" => "Por favor introduza o endereço de email que utilizou no registro e um email de verificação será enviado.", + "PAGE" => "Reenviar email de verificação para a sua nova conta.", + "SEND" => "Enviar email com link de verificação", + "TOKEN_NOT_FOUND" => "Token de verificação inexistente / Conta já verificada", + ] + ], + + "EMAIL" => [ + "INVALID" => "Não existe nenhuma conta para <strong>{{email}}</strong>.", + "IN_USE" => "O email <strong>{{email}}</strong> já se encontra em uso." + ], + + "FIRST_NAME" => "Primeiro nome", + + "HEADER_MESSAGE_ROOT" => "INICIOU SESSÃO COM A CONTA ROOT", + + "LAST_NAME" => "Último nome", + + "LOCALE.ACCOUNT" => "Linguagem e localização a utilizar na sua conta", + + "LOGIN" => [ + "@TRANSLATION" => "Entrar", + + "ALREADY_COMPLETE" => "Sessão já iniciada!", + "SOCIAL" => "Ou inicie sessão com", + "REQUIRED" => "Lamentamos, tem de iniciar sessão para aceder a este recurso." + ], + + "LOGOUT" => "Sair", + + "NAME" => "Nome", + + "PAGE" => [ + "LOGIN" => [ + "DESCRIPTION" => "Inicie sessão na sua conta {{site_name}}, ou registre-se para uma nova conta.", + "SUBTITLE" => "Registre-se gratuitamente, ou inicie sessão com uma conta existente.", + "TITLE" => "Vamos começar!", + ] + ], + + "PASSWORD" => [ + "@TRANSLATION" => "Password", + + "BETWEEN" => "Entre {{min}}-{{max}} carateres", + + "CONFIRM" => "Confirme a password", + "CONFIRM_CURRENT" => "Por favor confirme a sua password atual", + "CONFIRM_NEW" => "Confirmar Nova Password", + "CONFIRM_NEW_EXPLAIN" => "Re-introduza a sua nova password", + "CONFIRM_NEW_HELP" => "Apenas necessário se escolher uma nova password", + "CURRENT" => "Password Atual", + "CURRENT_EXPLAIN" => "Tem de confirmar a sua password atual para efetuar alterações", + + "FORGOTTEN" => "Password Esquecida", + "FORGET" => [ + "@TRANSLATION" => "Esqueci a minha password", + + "COULD_NOT_UPDATE" => "Não foi possível atualizar a password.", + "EMAIL" => "Por favor introduza o endereço de email que utilizou no registro. Enviaremos um email com instruções para efetuar o reset à sua password.", + "EMAIL_SEND" => "Enviar email com link de reset da password", + "INVALID" => "This password reset request could not be found, or has expired. Please try <a href=\"{{url}}\">resubmitting your request<a>.", + "PAGE" => "Obtenha um link para fazer reset à sua password.", + "REQUEST_CANNED" => "Pedido de password esquecida foi cancelado.", + "REQUEST_SENT" => "Se o email <strong>{{email}}</strong> corresponder a uma conta em nosso sistema, um link de redefinição de senha será enviado para <strong>{{email}}</strong>." + ], + + "RESET" => [ + "@TRANSLATION" => "Reset Password", + "CHOOSE" => "Por favor escolha uma nova password para continuar.", + "PAGE" => "Escolha uma nova password para a sua conta.", + "SEND" => "Definir nova password e registrar" + ], + + "HASH_FAILED" => "Falhou o hashing da password. Por favor contacte um administrador do site.", + "INVALID" => "A password atual não coincide com a que temos em sistema", + "NEW" => "Nova Password", + "NOTHING_TO_UPDATE" => "Não pode atualizar para a mesma password", + "UPDATED" => "Password da conta foi atualizada" + ], + + "REGISTER" => "Registrar", + "REGISTER_ME" => "Registrar-me", + + "REGISTRATION" => [ + "BROKEN" => "Lamentamos, existe um problema com o nosso processo de registro. Contacte-nos diretamente para assistência.", + "COMPLETE_TYPE1" => "Registrou-se com sucesso. Pode iniciar sessão.", + "COMPLETE_TYPE2" => "Registrou-se com sucesso. Receberá em breve um email de verificação contendo um link para verificar a sua conta. Não será possível iniciar sessão até completar este passo.", + "DISABLED" => "Lamentamos, o registro de novas contas foi desativado.", + "LOGOUT" => "Não pode registrar uma nova conta enquanto tiver sessão iniciada. Por favor feche a sua sessão primeiro.", + "WELCOME" => "O registro é rápido e simples." + ], + + "RATE_LIMIT_EXCEEDED" => "Excedeu o número de tentativas para esta ação. Tem de aguardar {{delay}} segundos até lhe ser permitida nova tentativa.", + "REMEMBER_ME" => "Lembrar de mim!", + "REMEMBER_ME_ON_COMPUTER" => "Lembrar de mim neste computador (não recomendado em computadores públicos)", + + "SIGNIN" => "Iniciar Sessão", + "SIGNIN_OR_REGISTER" => "Iniciar sessão ou registrar", + "SIGNUP" => "Registrar", + + "TOS" => "Termos e Condições", + "TOS_AGREEMENT" => "Ao registrar uma conta em {{site_title}}, está a aceitar os <a {{link_attributes | raw}}>termos e condições</a>.", + "TOS_FOR" => "Termos e Condições para {{title}}", + + "USERNAME" => [ + "@TRANSLATION" => "Nome de utilizador", + + "CHOOSE" => "Escolha um nome de utilizador único", + "INVALID" => "Nome de utilizador inválido", + "IN_USE" => "O nome de utilizador <strong>{{user_name}}</strong> já se encontra em uso." + ], + + "USER_ID_INVALID" => "O id de utilizador solicitado não existe.", + "USER_OR_EMAIL_INVALID" => "Nome de utilizador ou endereço de email inválidos.", + "USER_OR_PASS_INVALID" => "Nome de utilizador ou password inválidos.", + + "WELCOME" => "Bem-vindo, {{first_name}}" +]; diff --git a/login/app/sprinkles/account/locale/pt_PT/validate.php b/login/app/sprinkles/account/locale/pt_PT/validate.php new file mode 100755 index 0000000..c05f14c --- /dev/null +++ b/login/app/sprinkles/account/locale/pt_PT/validate.php @@ -0,0 +1,18 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Portuguese message token translations for the 'account' sprinkle. + * + * @package userfrosting\i18n\pt + * @author Bruno Silva (brunomnsilva@gmail.com) + */ + +return [ + "VALIDATE" => [ + "PASSWORD_MISMATCH" => "A password e respetiva confirmação têm de coincidir." + ] +]; diff --git a/login/app/sprinkles/account/locale/ru_RU/messages.php b/login/app/sprinkles/account/locale/ru_RU/messages.php new file mode 100755 index 0000000..328db13 --- /dev/null +++ b/login/app/sprinkles/account/locale/ru_RU/messages.php @@ -0,0 +1,183 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Russian message token translations for the 'account' sprinkle. + * + * @package userfrosting\i18n\ru_RU + * @author @rendername + */ + +return [ + "ACCOUNT" => [ + "@TRANSLATION" => "Аккаунт", + + "ACCESS_DENIED" => "Для получения доступа у вас недостаточно прав.", + + "DISABLED" => "Аккаунт отключен. Пожалуйста, свяжитесь с нами для получения дополнительной информации.", + + "EMAIL_UPDATED" => "Email аккаунта обновлён", + + "INVALID" => "Этот аккаунт не существует. Возможно, он удалён. Пожалуйста, свяжитесь с нами для получения дополнительной информации.", + + "MASTER_NOT_EXISTS" => "Вы не можете зарегистрировать аккаунт до тех пор, пока основная учётная запись не будет создана!", + "MY" => "Мой профиль", + + "SESSION_COMPROMISED" => [ + "@TRANSLATION" => "Ваша сессия была скомпрометирована. Вы должны выйти на всех устройствах, а затем снова войти и убедиться, что ваши данные не были изменены.", + "TITLE" => "Возможно, ваш аккаунт был скомпрометированн", + "TEXT" => "Возможно, кто-то использовал ваши данные для входа на эту страницу. В целях безопасности все сеансы были завершены. Пожалуйста, повторно <a href=\"{{url}}\"> войдите </a> и проверьте свой аккаунт на подозрительную активность. Рекомендуем сменить пароль." + ], + "SESSION_EXPIRED" => "Срок вашей сессии истек. Пожалуйста войдите еще раз.", + + "SETTINGS" => [ + "@TRANSLATION" => "Настройки аккаунта", + "DESCRIPTION" => "Обновите настройки своего аккаунта, включая адрес электронной почты, имя и пароль.", + "UPDATED" => "Данные аккаунта обновлены" + ], + + "TOOLS" => "Инструменты аккаунта", + + "UNVERIFIED" => "Ваш аккаунт ещё не подтверждён. Проверьте вашу email почту, в том числе папку спам и следуйте инструкциям.", + + "VERIFICATION" => [ + "NEW_LINK_SENT" => "Мы отправили на ваш email новую ссылку для активации {{email}}. Пожалуйста, проверьте папку \"Входящие\" и \"Спам\".", + "RESEND" => "Повторно отправить письмо с подтверждением", + "COMPLETE" => "Вы успешно подтвердили свой аккаунт. Теперь вы можете войти.", + "EMAIL" => "Введите email, который вы использовали для регистрации, вам будет повторно отправлено письмо с подтверждением.", + "PAGE" => "Повторно оправить письмо подтверждения на email для нового аккаунта.", + "SEND" => "Проверка по электронной почте для аккаунта", + "TOKEN_NOT_FOUND" => "Код подтверждения не действителен либо аккаунт уже подтверждён", + ] + ], + + "EMAIL" => [ + "INVALID" => "Нет не одного аккаунта с <strong> {{email}} </strong>.", + "IN_USE" => "Email <strong>{{email}}</strong> уже используется.", + "VERIFICATION_REQUIRED" => "Email (указывайте верный - необходим для активации!)" + ], + + "EMAIL_OR_USERNAME" => "Имя пользователя или Email", + + "FIRST_NAME" => "Имя", + + "HEADER_MESSAGE_ROOT" => "ВЫ АВТОРИЗОВАНЫ С СУПЕР-ПРАВАМИ", + + "LAST_NAME" => "Фамилия", + "LOCALE" => [ + "ACCOUNT" => "Основной язык для вашего аккаунта", + "INVALID" => "<strong>{{locale}}</strong> язык недопустим." + ], + "LOGIN" => [ + "@TRANSLATION" => "Вход", + "ALREADY_COMPLETE" => "Вы уже выполнили вход!", + "SOCIAL" => "Или войти через", + "REQUIRED" => "Извините, Вы должны авторизоваться для доступа к этому ресурсу." + ], + "LOGOUT" => "Выход", + + "NAME" => "Имя", + + "NAME_AND_EMAIL" => "Имя и email", + + "PAGE" => [ + "LOGIN" => [ + "DESCRIPTION" => "Войдите в свой аккаунт {{site_name}}, или Зарегистрируйтесь.", + "SUBTITLE" => "Зарегистрироваться или войти в существующий аккаунт.", + "TITLE" => "Приступим!", + ] + ], + + "PASSWORD" => [ + "@TRANSLATION" => "Пароль", + + "BETWEEN" => "Кол-во {{min}}-{{max}} символов", + + "CONFIRM" => "Подтверждение пароля", + "CONFIRM_CURRENT" => "Пожалуйста, введите ваш текущий пароль", + "CONFIRM_NEW" => "Подтвердите новый пароль", + "CONFIRM_NEW_EXPLAIN" => "Повторно введите Ваш новый пароль", + "CONFIRM_NEW_HELP" => "Требуется только при выборе нового пароля", + "CREATE" => [ + "@TRANSLATION" => "Создать пароль", + "PAGE" => "Выберите пароль для вашего аккаунта.", + "SET" => "Установить пароль и войти" + ], + "CURRENT" => "Текущий пароль", + "CURRENT_EXPLAIN" => "Для продолжения вы должны ввести текущий пароль", + + "FORGOTTEN" => "Забытый пароль?", + "FORGET" => [ + "@TRANSLATION" => "Я забыл свой пароль", + + "COULD_NOT_UPDATE" => "Не удалось обновить пароль.", + "EMAIL" => "Пожалуйста, введите адрес электронной почты, который Вы использовали при регистрации. Ссылка с инструкцией по сбросу пароля будет отправлена вам по электронной почте.", + "EMAIL_SEND" => "Ссылка сброса пароля по Email", + "INVALID" => "Этот запрос сброса пароля не может быть найден, или истек. Пожалуйста, попробуйте <a href=\"{{url}}\"> повторно сбросить пароль<a>.", + "PAGE" => "Получите ссылку для сброса пароля.", + "REQUEST_CANNED" => "Запрос на сброс пароля отменен.", + "REQUEST_SENT" => "Если email <strong>{{email}}</strong> существует в нашей системе у какого-либо аккаунта, ссылка на сброс пароля будет направлена на <strong>{{email}}</strong>." + ], + + "HASH_FAILED" => "Хэширование пароля не удалось. Пожалуйста, попробуйте другой пароль, либо свяжитесь с администратором сайта.", + "INVALID" => "Текущий пароль не соответствует тому, который задан в системе.", + "NEW" => "Новый пароль", + "NOTHING_TO_UPDATE" => "Невозможно обновить с тем же паролем", + + "RESET" => [ + "@TRANSLATION" => "Сбросить пароль", + "CHOOSE" => "Пожалуйста, выберите новый пароль, чтобы продолжить.", + "PAGE" => "Выберите новый пароль для вашего аккаунта.", + "SEND" => "Задать новый пароль и войти" + ], + + "UPDATED" => "Пароль аккаунта обновлён" + ], + + "PROFILE" => [ + "SETTINGS" => "Настройки профиля", + "UPDATED" => "Настройки профиля обновлены" + ], + + "RATE_LIMIT_EXCEEDED" => "Превышен лимит попыток для этого действия. Вы должны подождать еще {{delay}} секунд, прежде чем вам вам будет разрешено сделать ещё попытку.", + + "REGISTER" => "Регистрация", + "REGISTER_ME" => "Зарегистрируйте меня", + "REGISTRATION" => [ + "BROKEN" => "К сожалению, есть проблема с регистрации аккаунта. Свяжитесь с нами напрямую для получения помощи.", + "COMPLETE_TYPE1" => "Вы успешно зарегистрировались. Теперь вы можете войти.", + "COMPLETE_TYPE2" => "Вы успешно зарегистрировались. Ссылка для активации вашего аккаунта была отправлена на <strong>{{email}}</strong>. Вы сможете войти в систему только после активации аккаунта.", + "DISABLED" => "Извините, регистрация аккаунта была отключена.", + "LOGOUT" => "Извините, вы не можете зарегистрироваться когда уже авторизовались в системе. Сначала выйдите из системы.", + "WELCOME" => "Быстрая и простая регистрация." + ], + "REMEMBER_ME" => "Запомнить", + "REMEMBER_ME_ON_COMPUTER" => "Запомнить меня на этом компьютере (не рекомендуется для общедоступных компьютеров)", + + "SIGN_IN_HERE" => "Уже есть аккаунт? <a href=\"{{url}}\"> войти.</a>", + "SIGNIN" => "Вход", + "SIGNIN_OR_REGISTER" => "Регистрация или вход", + "SIGNUP" => "Вход", + + "TOS" => "Пользовательское соглашение", + "TOS_AGREEMENT" => "Регистрируя аккаунт на {{site_title}}, вы принимаете <a {{link_attributes | raw}}> условия и положения</a>.", + "TOS_FOR" => "Правила и условия для {{title}}", + + "USERNAME" => [ + "@TRANSLATION" => "Пользователь", + + "CHOOSE" => "Выберите имя пользователя", + "INVALID" => "Недопустимое имя пользователя", + "IN_USE" => "<strong>{{user_name}}</strong> имя пользователя уже используется.", + "NOT_AVAILABLE" => "Имя пользователя <strong>{{user_name}}</strong> не доступно. Выберите другое имя или нажмите кнопку «предложить»." + ], + + "USER_ID_INVALID" => "ID запрашиваемого пользователя не существует.", + "USER_OR_EMAIL_INVALID" => "Имя пользователя или email не верный.", + "USER_OR_PASS_INVALID" => "Пользователь не найден или пароль является недействительным.", + + "WELCOME" => "Добро пожаловать, {{first_name}}" +]; diff --git a/login/app/sprinkles/account/locale/ru_RU/validate.php b/login/app/sprinkles/account/locale/ru_RU/validate.php new file mode 100755 index 0000000..8ede5d8 --- /dev/null +++ b/login/app/sprinkles/account/locale/ru_RU/validate.php @@ -0,0 +1,19 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Russian message token translations for the 'account' sprinkle. + * + * @package userfrosting\i18n\ru_RU + * @author @rendername + */ + +return [ + "VALIDATE" => [ + "PASSWORD_MISMATCH" => "Пароли не совпадают.", + "USERNAME" => "Имя может состоять только из строчных букв, цифр, '.', '-' и «_»." + ] +]; diff --git a/login/app/sprinkles/account/locale/th_TH/messages.php b/login/app/sprinkles/account/locale/th_TH/messages.php new file mode 100755 index 0000000..642a7c5 --- /dev/null +++ b/login/app/sprinkles/account/locale/th_TH/messages.php @@ -0,0 +1,164 @@ +<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * Thai message token translations for the 'account' sprinkle.
+ *
+ * @package userfrosting\i18n\th
+ * @author Karuhut Komol
+ */
+
+return [
+ "ACCOUNT" => [
+ "@TRANSLATION" => "บัญชี",
+
+ "ACCESS_DENIED" => "หืมม ดูเหมือนว่าคุณไม่ได้รับอนุญาตให้ทำเช่นนั้น",
+
+ "DISABLED" => "บัญชีนี้ถูกปิดการใช้งานไปแล้ว กรุณาติดต่อเราสำหรับข้อมูลเพิ่มเติม",
+
+ "EMAIL_UPDATED" => "ปรับปรุงบัญชีอีเมลแล้ว",
+
+ "INVALID" => "ไม่พบบัญชีนี้ มันอาจถูกลบไปแล้ว กรุณาติดต่อเราสำหรับข้อมูลเพิ่มเติม",
+
+ "MASTER_NOT_EXISTS" => "คุณไม่สามารถสมัครสมาชิกได้จนกว่าจะสร้างบัญชีหลัก!",
+ "MY" => "บัญชีของฉัน",
+
+ "SESSION_COMPROMISED" => "เซสชันของคุณถูกลักลอบใช้ คุณควรจะออกจากระบบบนอุปกรณ์ทั้งหมดแล้วกลับเข้าสู่ระบบและตรวจสอบให้แน่ใจว่าไม่มีการแก้ไขข้อมูลของคุณ",
+ "SESSION_COMPROMISED_TITLE" => "บัญชีของคุณอาจถูกบุกรุก",
+ "SESSION_EXPIRED" => "เซสชันของคุณหมดอายุ กรุณาเข้าสู่ระบบอีกครั้ง",
+
+ "SETTINGS" => [
+ "@TRANSLATION" => "การตั้งค่าบัญชี",
+ "DESCRIPTION" => "ปรับปรุงการตั้งค่าบัญชีของคุณ รวมไปถึงอีเมล ชื่อ และรหัสผ่าน",
+ "UPDATED" => "ปรับปรุงการตั้งค่าบัญชีของคุณแล้ว"
+ ],
+
+ "TOOLS" => "เครื่องมือบัญชี",
+
+ "UNVERIFIED" => "บัญชีของคุณยังไม่ได้รับการยืนยัน กรุณาตรวจสอบกล่องอีเมลและจดหมายขยะของคุณสำหรับขั้นตอนการเปิดใช้งานบัญชี",
+
+ "VERIFICATION" => [
+ "NEW_LINK_SENT" => "เราได้ส่งลิงก์สำหรับการยืนยันใหม่ไปยังอีเมล {{email}} กรุณาตรวจสอบอีเมลนี้ในกล่องอีเมลและจดหมายขยะของคุณ",
+ "RESEND" => "ส่งอีเมลยืนยันอีกครั้ง",
+ "COMPLETE" => "คุณได้ยืนยันอีเมลของคุณเรียบร้อยแล้ว คุณสามารถเข้าสู่ระบบได้ทันที",
+ "EMAIL" => "กรุณากรอกอีเมลที่คุณได้ใช้สมัครไว้แล้วอีเมลยืนยันจะถูกส่งไปให้ใหม่",
+ "PAGE" => "ส่งอีเมลยืนยันสำหรับบัญชีของฉันใหม่",
+ "SEND" => "ส่งอีเมลยืนยันให้บัญชีของฉัน",
+ "TOKEN_NOT_FOUND" => "ไม่พบโทเคนยืนยันอีเมล / บัญชีนี้ได้ยืนยันแล้ว",
+ ]
+ ],
+
+ "EMAIL" => [
+ "INVALID" => "อีเมล <strong>{{email}}</strong> ไม่มีอยู่จริง",
+ "IN_USE" => "อีเมล <strong>{{email}}</strong> ได้ถูกใช้งานแล้ว"
+ ],
+
+ "FIRST_NAME" => "ชื่อจริง",
+
+ "HEADER_MESSAGE_ROOT" => "คุณได้เข้าสู่ระบบเป็นผู้ดูแลสูงสุด",
+
+ "LAST_NAME" => "นามสกุล",
+
+ "LOCALE.ACCOUNT" => "ภาษาและสถานที่ที่จะใช้สำหรับบัญชีของคุณ",
+
+ "LOGIN" => [
+ "@TRANSLATION" => "เข้าสู่ะระบบ",
+
+ "ALREADY_COMPLETE" => "คุณได้เข้าสู่ระบบอยู่แล้ว!",
+ "SOCIAL" => "หรือเข้าสู่ระบบด้วย",
+ "REQUIRED" => "ขออภัย คุณจะต้องเข้าสู่ระบบเพื่อเข้าถึงส่วนนี้"
+ ],
+
+ "LOGOUT" => "ออกจากระบบ",
+
+ "NAME" => "ชื่อ",
+
+ "PAGE" => [
+ "LOGIN" => [
+ "DESCRIPTION" => "เข้าสู่ระบบไปยังบัญชี {{site_name}} หรือสมัครสมาชิกสำหรับบัญชีใหม่",
+ "SUBTITLE" => "สมัครสมาชิกฟรี หรือเข้าสู่ระบบด้วยบัญชีที่มีอยู่",
+ "TITLE" => "มาเริ่มกันเลย!",
+ ]
+ ],
+
+ "PASSWORD" => [
+ "@TRANSLATION" => "รหัสผ่าน",
+
+ "BETWEEN" => "ระหว่าง {{min}}-{{max}} ตัวอักษร",
+
+ "CONFIRM" => "ยืนยันรหัสผ่าน",
+ "CONFIRM_CURRENT" => "กรุณายืนยันรหัสผ่านปัจจุบันของคุณ",
+ "CONFIRM_NEW" => "ยืนยันรหัสผ่านใหม่",
+ "CONFIRM_NEW_EXPLAIN" => "กรอกรหัสผ่านใหม่ของคุณอีกครั้ง",
+ "CONFIRM_NEW_HELP" => "กรอกเฉพาะเมื่อคุณต้องการตั้งรหัสผ่านใหม่",
+ "CURRENT" => "รหัสผ่านปัจจุบัน",
+ "CURRENT_EXPLAIN" => "คุณจะต้องยืนยันรหัสผ่านปัจจุบันเพื่อแก้ไขข้อมูล",
+
+ "FORGOTTEN" => "ลืมรหัสผ่าน",
+ "FORGET" => [
+ "@TRANSLATION" => "ฉันลืมรหัสผ่านของฉัน",
+
+ "COULD_NOT_UPDATE" => "ไม่สามารถปรับปรุงรหัสผ่าน",
+ "EMAIL" => "กรุณากรอกที่อยู่อีเมลที่คุณเคยใช้เข้าสู่ระบบ ลิงก์ขั้นตอนการรีเซ็ตรหัสผ่านของคุณจะถูกส่งไปให้คุณ",
+ "EMAIL_SEND" => "ลิงก์รีเซ็ตรหัสผ่านจากอีเมล",
+ "INVALID" => "ขอรีเซ็ตรหัสผ่านนี้ไม่มีอยู่ หรือหมดอายุไปแล้ว กรุณาลอง <a href=\"{{url}}\">ส่งคำขอของคุณอีกครั้ง<a>",
+ "PAGE" => "รับลิงก์สำหรับการรีเซ็ตรหัสผ่านของคุณ",
+ "REQUEST_CANNED" => "คำขอลืมรหัสผ่านได้ถูกยกเลิก",
+ "REQUEST_SENT" => "หากอีเมล <strong>{{email}}</strong> ตรงกับบัญชีในระบบของเราลิงก์การรีเซ็ตรหัสผ่านจะถูกส่งไปที่ <strong>{{email}}</strong>"
+ ],
+
+ "RESET" => [
+ "@TRANSLATION" => "รีเซ็ตรหัสผ่าน",
+ "CHOOSE" => "กรุณาเลือกรหัสผ่านใหม่เพื่อดำเนินการต่อ",
+ "PAGE" => "เลือกรหัสผ่านใหม่สำหรับบัญชีของคุณ",
+ "SEND" => "ตั้งรหัสผ่านใหม่และเข้าสู่ระบบ"
+ ],
+
+ "HASH_FAILED" => "เข้ารหัสรหัสผ่านล้มเหลว กรุณาติดต่อผู้ดูแลระบบของเว็บไซต์",
+ "INVALID" => "รหัสผ่านปัจจุบันไม่ตรงกับรหัสผ่านที่เราบันทึกไว้",
+ "NEW" => "รหัสผ่านใหม่",
+ "NOTHING_TO_UPDATE" => "คุณไม่สามารถปรังปรุงด้วยรหัสผ่านเดียวกัน",
+ "UPDATED" => "ปรังปรุงรหัสผ่านของบัญชีแล้ว"
+ ],
+
+ "REGISTER" => "สมัครสมาชิก",
+ "REGISTER_ME" => "ให้ฉันสมัครสมาชิกด้วย",
+
+ "REGISTRATION" => [
+ "BROKEN" => "เราขออภัย มันมีปัญหาในการดำเนินการสมัครสมาชิกของเรา กรุณาติดต่อเราโดยตรงเพื่อขอความช่วยเหลือ",
+ "COMPLETE_TYPE1" => "คุณได้สมัครสมาชิกเรียบร้อยแล้ว คุณสามารถเข้าสู่ระบบได้ทันที",
+ "COMPLETE_TYPE2" => "คุณได้สมัครสมาชิกเรียบร้อยแล้ว คุณจะได้รับอีเมลยืนยันที่มีลิงก์สำหรับเปิดใช้งานบัญชีของคุณอยู่ คุณจะไม่สามารถเข้าสู่ระบบจนกว่าคุณจะยืนยันอีเมลแล้ว",
+ "DISABLED" => "เราขออภัย ระบบสมัครสมาชิกได้ถูกปิดไว้",
+ "LOGOUT" => "เราขออภัย คุณไม่สามารถสมัครสมาชิกขณะที่เข้าสู่ระบบอยู่ กรุณาออกจากระบบก่อน",
+ "WELCOME" => "การสมัครสมาชิกนั้นรวดเร็ว และง่ายดาย"
+ ],
+
+ "RATE_LIMIT_EXCEEDED" => "ถึงขีดจำกัดสำหรับการกระทำนี้แล้ว คุณจะต้องรออีก {{delay}} วินาที ก่อนที่คุณจะได้รับอนุญาตให้ลองใหม่อีกครั้ง",
+ "REMEMBER_ME" => "จำฉันไว้ในระบบ!",
+ "REMEMBER_ME_ON_COMPUTER" => "จำฉันไว้ในระบบบนคอมพิวเตอร์นี้ (ไม่แนะนำสำหรับคอมพิวเตอร์สาธารณะ)",
+
+ "SIGNIN" => "เข้าสู่ะระบบ",
+ "SIGNIN_OR_REGISTER" => "เข้าสู่ระบบหรือสมัครสมาชิก",
+ "SIGNUP" => "สมัครสมาชิก",
+
+ "TOS" => "ข้อตกลงและเงื่อนไข",
+ "TOS_AGREEMENT" => "ในการสมัครสมาชิกกับ {{site_title}} หมายถึงคุณยอมรับ <a {{link_attributes}}>ข้อตกลงและเงื่อนไข</a> แล้ว",
+ "TOS_FOR" => "ข้อตกลงและเงื่อนไขสำหรับ {{title}}",
+
+ "USERNAME" => [
+ "@TRANSLATION" => "ชื่อผู้ใช้",
+
+ "CHOOSE" => "เลือกชื่อผู้ใช้ที่เป็นเป็นเอกลักษณ์",
+ "INVALID" => "ชื่อผู้ใช้ไม่ถูกต้อง",
+ "IN_USE" => "ชื่อผู้ใช้ <strong>{{user_name}}</strong> ถูกใช้งานแล้ว"
+ ],
+
+ "USER_ID_INVALID" => "ไม่พบหมายเลขผู้ใช้ที่ร้องขอมา",
+ "USER_OR_EMAIL_INVALID" => "ชื่อผู้ใช้หรือที่อยู่อีเมลไม่ถูกต้อง",
+ "USER_OR_PASS_INVALID" => "ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง",
+
+ "WELCOME" => "ยินดีต้อนรับ {{first_name}}"
+];
diff --git a/login/app/sprinkles/account/locale/th_TH/validate.php b/login/app/sprinkles/account/locale/th_TH/validate.php new file mode 100755 index 0000000..1a2e90a --- /dev/null +++ b/login/app/sprinkles/account/locale/th_TH/validate.php @@ -0,0 +1,18 @@ +<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * Thai message token translations for the 'account' sprinkle.
+ *
+ * @package userfrosting\i18n\th
+ * @author Karuhut Komol
+ */
+
+return [
+ "VALIDATE" => [
+ "PASSWORD_MISMATCH" => "รหัสผ่านและรหัสผ่านยืนยันของคุณจะต้องตรงกัน"
+ ]
+];
diff --git a/login/app/sprinkles/account/locale/tr/messages.php b/login/app/sprinkles/account/locale/tr/messages.php new file mode 100755 index 0000000..5213490 --- /dev/null +++ b/login/app/sprinkles/account/locale/tr/messages.php @@ -0,0 +1,183 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Turkish message token translations for the 'account' sprinkle. + * + * @package userfrosting\i18n\tr + * @author Dumblledore + */ + +return [ + "ACCOUNT" => [ + "@TRANSLATION" => "Hesap", + + "ACCESS_DENIED" => "Hmm. görünüşe göre böyle bir şey için izne sahip değilsiniz.", + + "DISABLED" => "Bu hesap durduruldu. Daha çok bilgi için bizimle iletişime geçin.", + + "EMAIL_UPDATED" => "Hesap maili güncellendi", + + "INVALID" => "Bu hesap bulunamadı. Silinmiş olabilir. Daha çok bilgi için bizimle iletişime geçin.", + + "MASTER_NOT_EXISTS" => "Ana hesap oluşturuluncaya kadar bir hesap oluşturamazsın!", + "MY" => "Hesabım", + + "SESSION_COMPROMISED" => [ + "@TRANSLATION" => "Oturumunuz tehlikeye atıldı. Tüm cihazlardan çıkmanız, daha sonra giriş yapmanız ve bilgilerinizin değiştirilmediğini kontrol etmeniz gerekir.", + "TITLE" => "Hesabınız tehlikeye atılmış olabilir", + "TEXT" => "Birisi bu sayfayı ele geçirmek için giriş verilerinizi kullanmış olabilir. Güvenliğiniz için tüm oturumlar günlüğe kaydedildi. Lütfen <a href=\"{{url}}\">giriş yapın</a>ve şüpheli hareketler için hesabınızı kontrol edin. Ayrıca şifrenizi değiştirmek isteyebilirsiniz." + ], + "SESSION_EXPIRED" => "Oturumunuz sona erdi. Lütfen tekrar oturum açın.", + + "SETTINGS" => [ + "@TRANSLATION" => "Hesap ayarları", + "DESCRIPTION" => "E-posta, isim ve parolanız da dahil olmak üzere hesap ayarlarınızı güncelleyin.", + "UPDATED" => "Hesap ayarları güncellendi" + ], + + "TOOLS" => "Hesap araçları", + + "UNVERIFIED" => "Hesap henüz onaylanmadı. Hesap etkinleştirme talimatları için e-postalarınızı ve spam klasörünüzü kontrol edin.", + + "VERIFICATION" => [ + "NEW_LINK_SENT" => "{{email}} için yeni bir doğrulama bağlantısı e-posta ile gönderildi. Lütfen bu e-postanın gelen kutusunu ve spam klasörlerini kontrol edin.", + "RESEND" => "Doğrulama e-postasını tekrar gönder", + "COMPLETE" => "Hesabınızı başarıyla doğruladınız. Şimdi giriş yapabilirsiniz.", + "EMAIL" => "Kaydolmak için kullandığınız e-posta adresinizi giriniz, ve doğrulama e-postanızı tekrar gönderin.", + "PAGE" => "Yeni hesabınız için doğrulama e-postasını tekrar gönder.", + "SEND" => "Hesabım için doğrulama bağlantısını e-posta ile gönder", + "TOKEN_NOT_FOUND" => "Doğrulama belirteci bulunumadı / Hesap zaten doğrulandı", + ] + ], + + "EMAIL" => [ + "INVALID" => "<strong>{{email}}</strong> için hesap yoktur.", + "IN_USE" => "E-posta <strong>{{email}}</strong> zaten kullanılıyor.", + "VERIFICATION_REQUIRED" => "E-posta (doğrulama gerekli - gerçek bir adres kullanın!)" + ], + + "EMAIL_OR_USERNAME" => "Kullanıcı adı veya e-posta adresi", + + "FIRST_NAME" => "Adınız", + + "HEADER_MESSAGE_ROOT" => "Kök kullanıcı olarak giriş yaptın", + + "LAST_NAME" => "Soyadı", + "LOCALE" => [ + "ACCOUNT" => "Hesabınız için kullanılacak dil ve yerel ayar", + "INVALID" => "<strong>{{locale}}</strong> geçersiz bir yerel." + ], + "LOGIN" => [ + "@TRANSLATION" => "Oturum Aç", + "ALREADY_COMPLETE" => "Zaten oturum açtınız!", + "SOCIAL" => "Veya şununla oturum aç", + "REQUIRED" => "Üzgünüm, bu sayfaya ulaşmak için oturum açmalısın." + ], + "LOGOUT" => "Oturumu kapat", + + "NAME" => "Ad", + + "NAME_AND_EMAIL" => "Ad ve e-posta", + + "PAGE" => [ + "LOGIN" => [ + "DESCRIPTION" => "{{site_name}} hesabınız ile giriş yapın ya da yeni bir hesap oluşturun.", + "SUBTITLE" => "Ücretsiz üye ol veya mevcut bir hesap ile giriş yapın.", + "TITLE" => "Hadi başlayalım!", + ] + ], + + "PASSWORD" => [ + "@TRANSLATION" => "Parola", + + "BETWEEN" => "{{min}}-{{max}} karakterler arasında", + + "CONFIRM" => "Şifreyi onayla", + "CONFIRM_CURRENT" => "Lütfen şuanki parolanızı giriniz", + "CONFIRM_NEW" => "Yeni parolayı onayla", + "CONFIRM_NEW_EXPLAIN" => "Yeni parolayı tekrar gir", + "CONFIRM_NEW_HELP" => "Sadece yeni bir şifre seçerseniz gerekli", + "CREATE" => [ + "@TRANSLATION" => "Parola Oluştur", + "PAGE" => "Yeni hesabınız için bir şifre belirleyin.", + "SET" => "Parolayı Ayarla ve Giriş Yap" + ], + "CURRENT" => "Şimdiki Parola", + "CURRENT_EXPLAIN" => "Değişiklikler için şimdiki parolanız ile onaylamalısınız", + + "FORGOTTEN" => "Unutulan Şifre", + "FORGET" => [ + "@TRANSLATION" => "Şifremi unuttum", + + "COULD_NOT_UPDATE" => "Şifre güncellenemedi.", + "EMAIL" => "Lütfen kaydolmak için kullandığınız e-posta adresini giriniz. Şifrenizi sıfırlama talimatlarıyla bir bir bağlantı e-postanıza gönderilecektir.", + "EMAIL_SEND" => "E-posta şifre sıfırlama bağlantısı", + "INVALID" => "Bu şifre sıfırlama isteği bulunamadı ya da süresi bitmiş. Lütfen <a href=\"{{url}}\">isteğinizi yeniden göndermeyi<a>deneyin.", + "PAGE" => "Şifrenizi sıfırlamak için bir bağlantı oluşturun.", + "REQUEST_CANNED" => "Kayıp parola isteği iptal edildi.", + "REQUEST_SENT" => "Eğer e-posta<strong>{{email}}</strong> sistemdeki bir hesap ile eşleşirse, bir şifre yenileme bağlantısı<strong>{{email}}</strong> gönderilir." + ], + + "HASH_FAILED" => "Parola karma başarısız oldu. Lütfen bir site yöneticisiyle iletişime geçin.", + "INVALID" => "Şimdiki şifre kayıt edilen şifre ile eşleşmiyor", + "NEW" => "Yeni Şifre", + "NOTHING_TO_UPDATE" => "Aynı şifre ile güncelleyemezsiniz", + + "RESET" => [ + "@TRANSLATION" => "Şifre sıfırlama", + "CHOOSE" => "Lütfen devam etmek için yeni bir şifre belirleyiniz.", + "PAGE" => "Hesabınız için yeni bir şifre belirleyiniz.", + "SEND" => "Yeni şifre ayarla ve giriş yap" + ], + + "UPDATED" => "Hesap şifresi güncellendi" + ], + + "PROFILE" => [ + "SETTINGS" => "Profil ayarları", + "UPDATED" => "Profil ayarları güncellendi" + ], + + "RATE_LIMIT_EXCEEDED" => "Bu işlem için belirlenen son oran aşıldı. Başka bir deneme yapmanıza izin verilene kadar {{delay}} bir süre beklemelisiniz.", + + "REGISTER" => "Kaydol", + "REGISTER_ME" => "Beni kaydet", + "REGISTRATION" => [ + "BROKEN" => "Üzgünüz, hesap kayıt işlemimizde bir sorun var. Lütfen destek almak için doğrudan bizimle iletişime geçin.", + "COMPLETE_TYPE1" => "Kaydınız başarıyla tamamlandı. Şimdi giriş yapabilirsiniz.", + "COMPLETE_TYPE2" => "Kaydınız başarıyla tamamlandı. Hesabınızı aktifleştirmek için bir bağlantı gönderildi<strong>{{email}}</strong>. Bu adımı tamamlayana kadar oturum açamazsınız.", + "DISABLED" => "Üzgünüz, hesap kaydı devre dışı bırakıldı.", + "LOGOUT" => "Üzgünüm, oturumunuz açıkken yeni bir hesap oluşturamazsınız. Lütfen önce oturumunuzdan çıkış yapınız.", + "WELCOME" => "Kaydolmak hızlı ve basittir." + ], + "REMEMBER_ME" => "Beni hatırla!", + "REMEMBER_ME_ON_COMPUTER" => "Bu bilgisayarda beni hatırla ( genel bilgisayarlar için önerilmez)", + + "SIGN_IN_HERE" => "Zaten bir hesaba sahip misiniz?<a href=\"{{url}}\">burada giriş yap</a>", + "SIGNIN" => "Giriş yap", + "SIGNIN_OR_REGISTER" => "Giriş yap veya kayıt ol", + "SIGNUP" => "Üye ol", + + "TOS" => "Şartlar ve Koşullar", + "TOS_AGREEMENT" => "Bir hesap ile kaydolarak {{site_title}} sen kabul edersin <a {{link_attributes | raw}}>şartlar ve koşulları</a>.", + "TOS_FOR" => "{{title}} için şartlar ve koşullar", + + "USERNAME" => [ + "@TRANSLATION" => "Kullanıcı Adı", + + "CHOOSE" => "Benzersiz bir kullanıcı adı seç", + "INVALID" => "Geçersiz kullanıcı adı", + "IN_USE" => "<strong>{{user_name}}</strong> kullanıcı adı zaten mevcut.", + "NOT_AVAILABLE" => "<strong>{{user_name}}</strong> kullanıcı adı kullanılamaz. Farklı bir isim veya 'öneriye' tıklayın." + ], + + "USER_ID_INVALID" => "İstenen kullanıcı adı mevcut değil.", + "USER_OR_EMAIL_INVALID" => "Kullanıcı adı veya e-posta adresi hatalı.", + "USER_OR_PASS_INVALID" => "Kullanıcı bulunamadı ya da şifre hatalı.", + + "WELCOME" => "Tekrar Hoşgeldiniz.{{first_name}}" +]; diff --git a/login/app/sprinkles/account/locale/tr/validate.php b/login/app/sprinkles/account/locale/tr/validate.php new file mode 100755 index 0000000..298bdbc --- /dev/null +++ b/login/app/sprinkles/account/locale/tr/validate.php @@ -0,0 +1,19 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Turkish message token translations for the 'account' sprinkle. + * + * @package userfrosting\i18n\tr + * @author Dumblledore + */ + +return [ + "VALIDATE" => [ + "PASSWORD_MISMATCH" => "Şifreniz ve onaylama şifreniz eşleşmiyor.", + "USERNAME" => "Kullanıcı adınız sadece küçük harfler, sayılar, '.', '-', ve '_' içerebilir." + ] +]; diff --git a/login/app/sprinkles/account/locale/zh_CN/messages.php b/login/app/sprinkles/account/locale/zh_CN/messages.php new file mode 100755 index 0000000..60adcf0 --- /dev/null +++ b/login/app/sprinkles/account/locale/zh_CN/messages.php @@ -0,0 +1,177 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Chinese message token translations for the 'account' sprinkle. + * + * @package userfrosting\i18n\zh_CN + * @author @BruceGui (https://github.com/BruceGui) + */ + +return [ + "ACCOUNT" => [ + "@TRANSLATION" => "账户", + + "ACCESS_DENIED" => "噢, 你好像没有权限这么做.", + + "DISABLED" => "这个账户已被禁用. 请联系我们获取更多信息.", + + "EMAIL_UPDATED" => "账户邮箱更新成功", + + "INVALID" => "此账户不存在. 可能已被删除. 请联系我们获取更多信息.", + + "MASTER_NOT_EXISTS" => "在创建超级账户之前你不能注册", + "MY" => "我的账户", + + "SESSION_COMPROMISED" => "你的会话已泄露. 你应该在所有的设备上注销, 然后再登陆确保你的数据没被修改.", + "SESSION_COMPROMISED_TITLE" => "你的账户可能被盗用", + "SESSION_EXPIRED" => "会话已过期. 请重新登陆.", + + "SETTINGS" => [ + "@TRANSLATION" => "账户设置", + "DESCRIPTION" => "更新你的账户, 包括邮箱、姓名和密码.", + "UPDATED" => "账户更新成功" + ], + + "TOOLS" => "账户工具", + + "UNVERIFIED" => "你的账户还没有验证. 检查你的(垃圾)邮箱文件夹进行验证.", + + "VERIFICATION" => [ + "NEW_LINK_SENT" => "我们发送了新的验证链接 {{email}}. 请检查你的收件箱或垃圾邮件进行验证.", + "RESEND" => "重新发送验证邮件", + "COMPLETE" => "你已成功验证. 现在可以登陆了.", + "EMAIL" => "请输入你登陆时的邮箱, 然后将会发送验证邮件.", + "PAGE" => "重新发送验证邮件给你的新账户.", + "SEND" => "为我的账户发送验证邮件", + "TOKEN_NOT_FOUND" => "验证令牌不存在 / 账户已经验证", + ] + ], + + "EMAIL" => [ + "INVALID" => "<strong>{{email}}</strong> 没有账户注册.", + "IN_USE" => "邮箱 <strong>{{email}}</strong> 已被使用.", + "VERIFICATION_REQUIRED" => "邮箱 (需要进行验证 - 请使用一个有效的!)" + ], + + "EMAIL_OR_USERNAME" => "用户名或邮箱地址", + + "FIRST_NAME" => "名字", + + "HEADER_MESSAGE_ROOT" => "你现在以超级用户登陆", + + "LAST_NAME" => "姓氏", + + "LOCALE" => [ + "ACCOUNT" => "设置你账户的地区和语言", + "INVALID" => "<strong>{{locale}}</strong> 不是一个有效的地区." + ], + + "LOGIN" => [ + "@TRANSLATION" => "登陆", + "ALREADY_COMPLETE" => "你已经登陆!", + "SOCIAL" => "用其他方式登陆", + "REQUIRED" => "对不起, 你需要登陆才能获取资源." + ], + + "LOGOUT" => "注销", + + "NAME" => "名字", + + "NAME_AND_EMAIL" => "名字和邮箱", + + "PAGE" => [ + "LOGIN" => [ + "DESCRIPTION" => "用 {{site_name}} 账户登陆, 或者创建新账户.", + "SUBTITLE" => "免费注册, 或用已有账户登陆.", + "TITLE" => "让我们开始吧!", + ] + ], + + "PASSWORD" => [ + "@TRANSLATION" => "密码", + + "BETWEEN" => "字符长度 {{min}}-{{max}} ", + + "CONFIRM" => "确认密码", + "CONFIRM_CURRENT" => "请确认当前密码", + "CONFIRM_NEW" => "确认新密码", + "CONFIRM_NEW_EXPLAIN" => "重新输入新密码", + "CONFIRM_NEW_HELP" => "选择了新密码时才需要", + "CURRENT" => "密码正确", + "CURRENT_EXPLAIN" => "你必须要确认密码再进行修改", + + "FORGOTTEN" => "忘记密码", + "FORGET" => [ + "@TRANSLATION" => "我忘记了密码", + + "COULD_NOT_UPDATE" => "无法更新密码.", + "EMAIL" => "请输入你登陆时的邮箱. 重置密码的链接将会发送给你.", + "EMAIL_SEND" => "发送重置密码链接", + "INVALID" => "这个重置密码请求无法使用, 或已过期. 请 <a href=\"{{url}}\">重新发送请求<a>.", + "PAGE" => "获取重置密码的链接.", + "REQUEST_CANNED" => "取消重置请求.", + "REQUEST_SENT" => "重置密码的链接已经发送 <strong>{{email}}</strong>." + ], + + "RESET" => [ + "@TRANSLATION" => "重置密码", + "CHOOSE" => "请输入新密码.", + "PAGE" => "为账户设置新密码.", + "SEND" => "设置密码并登陆" + ], + + "HASH_FAILED" => "密码验证失败. 请联系网站管理.", + "INVALID" => "当前密码无法与记录匹配", + "NEW" => "新密码", + "NOTHING_TO_UPDATE" => "新密码不能与旧密码相同", + "UPDATED" => "账户密码更新成功" + ], + + "PROFILE" => [ + "SETTINGS" => "简介设置", + "UPDATED" => "简介设置成功" + ], + + "REGISTER" => "注册", + "REGISTER_ME" => "注册", + + "REGISTRATION" => [ + "BROKEN" => "抱歉, 账户注册过程发送错误. 请联系我们寻求帮助.", + "COMPLETE_TYPE1" => "你已注册成功. 现在可以登陆了.", + "COMPLETE_TYPE2" => "成功注册. 激活链接已经发送给 <strong>{{email}}</strong>. 激活之前无法登陆.", + "DISABLED" => "抱歉, 账户注册以禁用.", + "LOGOUT" => "抱歉, 登陆时不能注册. 请先注销.", + "WELCOME" => "注册简单快速." + ], + + "RATE_LIMIT_EXCEEDED" => "行动速度过快. 请等 {{delay}} 秒后再尝试新的操作.", + "REMEMBER_ME" => "记住我!", + "REMEMBER_ME_ON_COMPUTER" => "在此电脑上记住我 (不推荐在公共电脑上)", + + "SIGNIN" => "登陆", + "SIGNIN_OR_REGISTER" => "登陆或注册", + "SIGNUP" => "注销", + + "TOS" => "条款和说明", + "TOS_AGREEMENT" => "在 {{site_title}} 注册, 你需要接收 <a {{link_attributes | raw}}>条款和说明</a>.", + "TOS_FOR" => "{{title}}的条款和说明", + + "USERNAME" => [ + "@TRANSLATION" => "用户名", + + "CHOOSE" => "取一个唯一的用户名", + "INVALID" => "无效的用户名", + "IN_USE" => "用户名 <strong>{{user_name}}</strong> 已存在.", + "NOT_AVAILABLE" => "用户名 <strong>{{user_name}}</strong> 不可用. 重新选择用户名, 或者点击 '建议'." + ], + + "USER_ID_INVALID" => "请求的用户不存在.", + "USER_OR_EMAIL_INVALID" => "用户名或邮箱无效.", + "USER_OR_PASS_INVALID" => "没有发现用户或密码错误.", + + "WELCOME" => "欢迎回来, {{first_name}}" +]; diff --git a/login/app/sprinkles/account/locale/zh_CN/validate.php b/login/app/sprinkles/account/locale/zh_CN/validate.php new file mode 100755 index 0000000..3ca368a --- /dev/null +++ b/login/app/sprinkles/account/locale/zh_CN/validate.php @@ -0,0 +1,19 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Chinese message token translations for the 'account' sprinkle. + * + * @package userfrosting\i18n\zh_CN + * @author @BruceGui (https://github.com/BruceGui) + */ + +return [ + "VALIDATE" => [ + "PASSWORD_MISMATCH" => "密码不一致.", + "USERNAME" => "用户名必须以小写字母, 数字, '.', '-', 和 '_'组成." + ] +]; diff --git a/login/app/sprinkles/account/routes/routes.php b/login/app/sprinkles/account/routes/routes.php new file mode 100755 index 0000000..8198255 --- /dev/null +++ b/login/app/sprinkles/account/routes/routes.php @@ -0,0 +1,59 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ + +$app->group('/account', function () { + $this->get('/captcha', 'UserFrosting\Sprinkle\Account\Controller\AccountController:imageCaptcha'); + + $this->get('/check-username', 'UserFrosting\Sprinkle\Account\Controller\AccountController:checkUsername'); + + $this->get('/forgot-password', 'UserFrosting\Sprinkle\Account\Controller\AccountController:pageForgotPassword') + ->setName('forgot-password'); + + $this->get('/logout', 'UserFrosting\Sprinkle\Account\Controller\AccountController:logout') + ->add('authGuard'); + + $this->get('/resend-verification', 'UserFrosting\Sprinkle\Account\Controller\AccountController:pageResendVerification'); + + $this->get('/set-password/confirm', 'UserFrosting\Sprinkle\Account\Controller\AccountController:pageResetPassword'); + + $this->get('/set-password/deny', 'UserFrosting\Sprinkle\Account\Controller\AccountController:denyResetPassword'); + + $this->get('/register', 'UserFrosting\Sprinkle\Account\Controller\AccountController:pageRegister') + ->add('checkEnvironment') + ->setName('register'); + + $this->get('/settings', 'UserFrosting\Sprinkle\Account\Controller\AccountController:pageSettings') + ->add('authGuard'); + + $this->get('/sign-in', 'UserFrosting\Sprinkle\Account\Controller\AccountController:pageSignIn') + ->add('checkEnvironment') + ->setName('login'); + + $this->get('/suggest-username', 'UserFrosting\Sprinkle\Account\Controller\AccountController:suggestUsername'); + + $this->get('/verify', 'UserFrosting\Sprinkle\Account\Controller\AccountController:verify'); + + $this->post('/forgot-password', 'UserFrosting\Sprinkle\Account\Controller\AccountController:forgotPassword'); + + $this->post('/login', 'UserFrosting\Sprinkle\Account\Controller\AccountController:login'); + + $this->post('/register', 'UserFrosting\Sprinkle\Account\Controller\AccountController:register'); + + $this->post('/resend-verification', 'UserFrosting\Sprinkle\Account\Controller\AccountController:resendVerification'); + + $this->post('/set-password', 'UserFrosting\Sprinkle\Account\Controller\AccountController:setPassword'); + + $this->post('/settings', 'UserFrosting\Sprinkle\Account\Controller\AccountController:settings') + ->add('authGuard') + ->setName('settings'); + + $this->post('/settings/profile', 'UserFrosting\Sprinkle\Account\Controller\AccountController:profile') + ->add('authGuard'); +}); + +$app->get('/modals/account/tos', 'UserFrosting\Sprinkle\Account\Controller\AccountController:getModalAccountTos'); diff --git a/login/app/sprinkles/account/schema/requests/account-settings.yaml b/login/app/sprinkles/account/schema/requests/account-settings.yaml new file mode 100755 index 0000000..4a2d368 --- /dev/null +++ b/login/app/sprinkles/account/schema/requests/account-settings.yaml @@ -0,0 +1,35 @@ +--- +passwordcheck: + validators: + required: + message: PASSWORD.CONFIRM_CURRENT +email: + validators: + required: + label: "&EMAIL" + message: VALIDATE.REQUIRED + length: + label: "&EMAIL" + min: 1 + max: 150 + message: VALIDATE.LENGTH_RANGE + email: + message: VALIDATE.INVALID_EMAIL +password: + validators: + length: + label: "&PASSWORD" + min: 12 + max: 100 + message: VALIDATE.LENGTH_RANGE +passwordc: + validators: + matches: + field: password + label: "&PASSWORD.CONFIRM" + message: VALIDATE.PASSWORD_MISMATCH + length: + label: "&PASSWORD.CONFIRM" + min: 12 + max: 100 + message: VALIDATE.LENGTH_RANGE diff --git a/login/app/sprinkles/account/schema/requests/account-verify.yaml b/login/app/sprinkles/account/schema/requests/account-verify.yaml new file mode 100755 index 0000000..01f3155 --- /dev/null +++ b/login/app/sprinkles/account/schema/requests/account-verify.yaml @@ -0,0 +1,6 @@ +--- +token: + validators: + required: + label: validation token + message: VALIDATION.REQUIRED diff --git a/login/app/sprinkles/account/schema/requests/check-username.yaml b/login/app/sprinkles/account/schema/requests/check-username.yaml new file mode 100755 index 0000000..778b5e5 --- /dev/null +++ b/login/app/sprinkles/account/schema/requests/check-username.yaml @@ -0,0 +1,17 @@ +--- +user_name: + validators: + length: + label: "&USERNAME" + min: 1 + max: 50 + message: VALIDATE.LENGTH_RANGE + no_leading_whitespace: + label: "&USERNAME" + message: VALIDATE.NO_LEAD_WS + no_trailing_whitespace: + label: "&USERNAME" + message: VALIDATE.NO_TRAIL_WS + username: + label: "&USERNAME" + message: VALIDATE.USERNAME diff --git a/login/app/sprinkles/account/schema/requests/deny-password.yaml b/login/app/sprinkles/account/schema/requests/deny-password.yaml new file mode 100755 index 0000000..3b3e919 --- /dev/null +++ b/login/app/sprinkles/account/schema/requests/deny-password.yaml @@ -0,0 +1,5 @@ +--- +token: + validators: + required: + message: PASSWORD.FORGET.INVALID diff --git a/login/app/sprinkles/account/schema/requests/forgot-password.yaml b/login/app/sprinkles/account/schema/requests/forgot-password.yaml new file mode 100755 index 0000000..70072b5 --- /dev/null +++ b/login/app/sprinkles/account/schema/requests/forgot-password.yaml @@ -0,0 +1,6 @@ +--- +email: + validators: + required: + label: "&EMAIL" + message: VALIDATE.REQUIRED diff --git a/login/app/sprinkles/account/schema/requests/login.yaml b/login/app/sprinkles/account/schema/requests/login.yaml new file mode 100755 index 0000000..b78596a --- /dev/null +++ b/login/app/sprinkles/account/schema/requests/login.yaml @@ -0,0 +1,19 @@ +--- +user_name: + validators: + required: + label: "&USERNAME" + message: VALIDATE.REQUIRED + no_leading_whitespace: + label: "&USERNAME" + message: VALIDATE.NO_LEAD_WS + no_trailing_whitespace: + label: "&USERNAME" + message: VALIDATE.NO_TRAIL_WS +password: + validators: + required: + label: "&PASSWORD" + message: VALIDATE.REQUIRED +rememberme: + default: false diff --git a/login/app/sprinkles/account/schema/requests/profile-settings.yaml b/login/app/sprinkles/account/schema/requests/profile-settings.yaml new file mode 100755 index 0000000..c2b5ee8 --- /dev/null +++ b/login/app/sprinkles/account/schema/requests/profile-settings.yaml @@ -0,0 +1,24 @@ +--- +first_name: + validators: + length: + label: "&FIRST_NAME" + min: 1 + max: 20 + message: VALIDATE.LENGTH_RANGE + required: + label: "&FIRST_NAME" + message: VALIDATE.REQUIRED +last_name: + validators: + length: + label: "&LAST_NAME" + min: 1 + max: 30 + message: VALIDATE.LENGTH_RANGE +locale: + validators: + required: + label: "&LOCALE" + domain: server + message: VALIDATE.REQUIRED diff --git a/login/app/sprinkles/account/schema/requests/register.yaml b/login/app/sprinkles/account/schema/requests/register.yaml new file mode 100755 index 0000000..75dae59 --- /dev/null +++ b/login/app/sprinkles/account/schema/requests/register.yaml @@ -0,0 +1,75 @@ +--- +user_name: + validators: + length: + label: "&USERNAME" + min: 1 + max: 50 + message: VALIDATE.LENGTH_RANGE + no_leading_whitespace: + label: "&USERNAME" + message: VALIDATE.NO_LEAD_WS + no_trailing_whitespace: + label: "&USERNAME" + message: VALIDATE.NO_TRAIL_WS + required: + label: "&USERNAME" + message: VALIDATE.REQUIRED + username: + label: "&USERNAME" + message: VALIDATE.USERNAME +first_name: + validators: + length: + label: "&FIRST_NAME" + min: 1 + max: 20 + message: VALIDATE.LENGTH_RANGE + required: + label: "&FIRST_NAME" + message: VALIDATE.REQUIRED +last_name: + validators: + length: + label: "&LAST_NAME" + min: 1 + max: 30 + message: VALIDATE.LENGTH_RANGE +email: + validators: + required: + label: "&EMAIL" + message: VALIDATE.REQUIRED + length: + label: "&EMAIL" + min: 1 + max: 150 + message: VALIDATE.LENGTH_RANGE + email: + message: VALIDATE.INVALID_EMAIL +password: + validators: + required: + label: "&PASSWORD" + message: VALIDATE.REQUIRED + length: + label: "&PASSWORD" + min: 12 + max: 100 + message: VALIDATE.LENGTH_RANGE +passwordc: + validators: + required: + label: "&PASSWORD.CONFIRM" + message: VALIDATE.REQUIRED + matches: + field: password + label: "&PASSWORD.CONFIRM" + message: VALIDATE.PASSWORD_MISMATCH + length: + label: "&PASSWORD.CONFIRM" + min: 12 + max: 100 + message: VALIDATE.LENGTH_RANGE +captcha: + validators: diff --git a/login/app/sprinkles/account/schema/requests/resend-verification.yaml b/login/app/sprinkles/account/schema/requests/resend-verification.yaml new file mode 100755 index 0000000..70072b5 --- /dev/null +++ b/login/app/sprinkles/account/schema/requests/resend-verification.yaml @@ -0,0 +1,6 @@ +--- +email: + validators: + required: + label: "&EMAIL" + message: VALIDATE.REQUIRED diff --git a/login/app/sprinkles/account/schema/requests/set-password.yaml b/login/app/sprinkles/account/schema/requests/set-password.yaml new file mode 100755 index 0000000..ae59d1c --- /dev/null +++ b/login/app/sprinkles/account/schema/requests/set-password.yaml @@ -0,0 +1,29 @@ +--- +password: + validators: + required: + label: "&PASSWORD" + message: VALIDATE.REQUIRED + length: + label: "&PASSWORD" + min: 12 + max: 100 + message: VALIDATE.LENGTH_RANGE +passwordc: + validators: + required: + label: "&PASSWORD.CONFIRM" + message: VALIDATE.REQUIRED + matches: + field: password + label: "&PASSWORD.CONFIRM" + message: VALIDATE.PASSWORD_MISMATCH + length: + label: "&PASSWORD.CONFIRM" + min: 12 + max: 100 + message: VALIDATE.LENGTH_RANGE +token: + validators: + required: + message: PASSWORD.FORGET.INVALID diff --git a/login/app/sprinkles/account/src/Account.php b/login/app/sprinkles/account/src/Account.php new file mode 100755 index 0000000..49c2de9 --- /dev/null +++ b/login/app/sprinkles/account/src/Account.php @@ -0,0 +1,20 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account; + +use UserFrosting\System\Sprinkle\Sprinkle; + +/** + * Bootstrapper class for the 'account' sprinkle. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class Account extends Sprinkle +{ + +} diff --git a/login/app/sprinkles/account/src/Authenticate/AuthGuard.php b/login/app/sprinkles/account/src/Authenticate/AuthGuard.php new file mode 100755 index 0000000..efcfaae --- /dev/null +++ b/login/app/sprinkles/account/src/Authenticate/AuthGuard.php @@ -0,0 +1,56 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Authenticate; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Slim\Http\Body; +use UserFrosting\Sprinkle\Account\Authenticate\Exception\AuthExpiredException; + +/** + * Middleware to catch requests that fail because they require user authentication. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class AuthGuard +{ + /** + * @var Authenticator + */ + protected $authenticator; + + /** + * Constructor. + * + * @param $authenticator Authenticator The current authentication object. + */ + public function __construct($authenticator) + { + $this->authenticator = $authenticator; + } + + /** + * Invoke the AuthGuard middleware, throwing an exception if there is no authenticated user in the session. + * + * @param \Psr\Http\Message\ServerRequestInterface $request PSR7 request + * @param \Psr\Http\Message\ResponseInterface $response PSR7 response + * @param callable $next Next middleware + * + * @return \Psr\Http\Message\ResponseInterface + */ + public function __invoke($request, $response, $next) + { + if (!$this->authenticator->check()) { + throw new AuthExpiredException(); + } else { + return $next($request, $response); + } + + return $response; + } +} diff --git a/login/app/sprinkles/account/src/Authenticate/Authenticator.php b/login/app/sprinkles/account/src/Authenticate/Authenticator.php new file mode 100755 index 0000000..5fb8920 --- /dev/null +++ b/login/app/sprinkles/account/src/Authenticate/Authenticator.php @@ -0,0 +1,419 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Authenticate; + +use Birke\Rememberme\Authenticator as RememberMe; +use Birke\Rememberme\Storage\PDOStorage as RememberMePDO; +use Birke\Rememberme\Triplet as RememberMeTriplet; +use Illuminate\Database\Capsule\Manager as Capsule; +use UserFrosting\Session\Session; +use UserFrosting\Sprinkle\Account\Authenticate\Exception\AccountDisabledException; +use UserFrosting\Sprinkle\Account\Authenticate\Exception\AccountInvalidException; +use UserFrosting\Sprinkle\Account\Authenticate\Exception\AccountNotVerifiedException; +use UserFrosting\Sprinkle\Account\Authenticate\Exception\AuthCompromisedException; +use UserFrosting\Sprinkle\Account\Authenticate\Exception\AuthExpiredException; +use UserFrosting\Sprinkle\Account\Authenticate\Exception\InvalidCredentialsException; +use UserFrosting\Sprinkle\Account\Database\Models\User; +use UserFrosting\Sprinkle\Account\Facades\Password; +use UserFrosting\Sprinkle\Core\Util\ClassMapper; + +/** + * Handles authentication tasks. + * + * @author Alex Weissman (https://alexanderweissman.com) + * Partially inspired by Laravel's Authentication component: https://github.com/laravel/framework/blob/5.3/src/Illuminate/Auth/SessionGuard.php + */ +class Authenticator +{ + /** + * @var ClassMapper + */ + protected $classMapper; + + /** + * @var Session + */ + protected $session; + + /** + * @var Config + */ + protected $config; + + /** + * @var Cache + */ + protected $cache; + + /** + * @var bool + */ + protected $loggedOut = false; + + /** + * @var RememberMePDO + */ + protected $rememberMeStorage; + + /** + * @var RememberMe + */ + protected $rememberMe; + + /** + * @var User + */ + protected $user; + + /** + * Indicates if the user was authenticated via a rememberMe cookie. + * + * @var bool + */ + protected $viaRemember = false; + + /** + * Create a new Authenticator object. + * + * @param ClassMapper $classMapper Maps generic class identifiers to specific class names. + * @param Session $session The session wrapper object that will store the user's id. + * @param Config $config Config object that contains authentication settings. + * @param mixed $cache Cache service instance + */ + public function __construct(ClassMapper $classMapper, Session $session, $config, $cache) + { + $this->classMapper = $classMapper; + $this->session = $session; + $this->config = $config; + $this->cache = $cache; + + // Initialize RememberMe storage + $this->rememberMeStorage = new RememberMePDO($this->config['remember_me.table']); + + // Get the actual PDO instance from Eloquent + $pdo = Capsule::connection()->getPdo(); + + $this->rememberMeStorage->setConnection($pdo); + + // Set up RememberMe + $this->rememberMe = new RememberMe($this->rememberMeStorage); + // Set cookie name + $cookieName = $this->config['session.name'] . '-' . $this->config['remember_me.cookie.name']; + $this->rememberMe->getCookie()->setName($cookieName); + + // Change cookie path + $this->rememberMe->getCookie()->setPath($this->config['remember_me.session.path']); + + // Set expire time, if specified + if ($this->config->has('remember_me.expire_time') && ($this->config->has('remember_me.expire_time') != null)) { + $this->rememberMe->getCookie()->setExpireTime($this->config['remember_me.expire_time']); + } + + $this->user = null; + + $this->viaRemember = false; + } + + /** + * Attempts to authenticate a user based on a supplied identity and password. + * + * If successful, the user's id is stored in session. + */ + public function attempt($identityColumn, $identityValue, $password, $rememberMe = false) + { + // Try to load the user, using the specified conditions + $user = $this->classMapper->staticMethod('user', 'where', $identityColumn, $identityValue)->first(); + + if (!$user) { + throw new InvalidCredentialsException(); + } + + // Check that the user has a password set (so, rule out newly created accounts without a password) + if (!$user->password) { + throw new InvalidCredentialsException(); + } + + // Check that the user's account is enabled + if ($user->flag_enabled == 0) { + throw new AccountDisabledException(); + } + + // Check that the user's account is verified (if verification is required) + if ($this->config['site.registration.require_email_verification'] && $user->flag_verified == 0) { + throw new AccountNotVerifiedException(); + } + + // Here is my password. May I please assume the identify of this user now? + if (Password::verify($password, $user->password)) { + $this->login($user, $rememberMe); + return $user; + } else { + // We know the password is at fault here (as opposed to the identity), but lets not give away the combination in case of someone bruteforcing + throw new InvalidCredentialsException(); + } + } + + /** + * Determine if the current user is authenticated. + * + * @return bool + */ + public function check() + { + return !is_null($this->user()); + } + + /** + * Determine if the current user is a guest (unauthenticated). + * + * @return bool + */ + public function guest() + { + return !$this->check(); + } + + /** + * Process an account login request. + * + * This method logs in the specified user, allowing the client to assume the user's identity for the duration of the session. + * @param User $user The user to log in. + * @param bool $rememberMe Set to true to make this a "persistent session", i.e. one that will re-login even after the session expires. + * @todo Figure out a way to update the currentUser service to reflect the logged-in user *immediately* in the service provider. + * As it stands, the currentUser service will still reflect a "guest user" for the remainder of the request. + */ + public function login($user, $rememberMe = false) + { + $oldId = session_id(); + $this->session->regenerateId(true); + + // Since regenerateId deletes the old session, we'll do the same in cache + $this->flushSessionCache($oldId); + + // If the user wants to be remembered, create Rememberme cookie + if ($rememberMe) { + $this->rememberMe->createCookie($user->id); + } else { + $this->rememberMe->clearCookie(); + } + + // Assume identity + $key = $this->config['session.keys.current_user_id']; + $this->session[$key] = $user->id; + + // Set auth mode + $this->viaRemember = false; + + // User login actions + $user->onLogin(); + } + + /** + * Processes an account logout request. + * + * Logs the currently authenticated user out, destroying the PHP session and clearing the persistent session. + * This can optionally remove persistent sessions across all browsers/devices, since there can be a "RememberMe" cookie + * and corresponding database entries in multiple browsers/devices. See http://jaspan.com/improved_persistent_login_cookie_best_practice. + * + * @param bool $complete If set to true, will ensure that the user is logged out from *all* browsers on all devices. + */ + public function logout($complete = false) + { + $currentUserId = $this->session->get($this->config['session.keys.current_user_id']); + + // This removes all of the user's persistent logins from the database + if ($complete) { + $this->storage->cleanAllTriplets($currentUserId); + } + + // Clear the rememberMe cookie + $this->rememberMe->clearCookie(); + + // User logout actions + if ($currentUserId) { + $currentUser = $this->classMapper->staticMethod('user', 'find', $currentUserId); + if ($currentUser) { + $currentUser->onLogout(); + } + } + + $this->user = null; + $this->loggedOut = true; + + $oldId = session_id(); + + // Completely destroy the session + $this->session->destroy(); + + // Since regenerateId deletes the old session, we'll do the same in cache + $this->flushSessionCache($oldId); + + // Restart the session service + $this->session->start(); + } + + /** + * Try to get the currently authenticated user, returning a guest user if none was found. + * + * Tries to re-establish a session for "remember-me" users who have been logged out due to an expired session. + * @return User|null + * @throws AuthExpiredException + * @throws AuthCompromisedException + * @throws AccountInvalidException + * @throws AccountDisabledException + */ + public function user() + { + $user = null; + + if (!$this->loggedOut) { + + // Return any cached user + if (!is_null($this->user)) { + return $this->user; + } + + // If this throws a PDOException we catch it and return null than allowing the exception to propagate. + // This is because the error handler relies on Twig, which relies on a Twig Extension, which relies on the global current_user variable. + // So, we really don't want this method to throw any database exceptions. + try { + // Now, check to see if we have a user in session + $user = $this->loginSessionUser(); + + // If no user was found in the session, try to login via RememberMe cookie + if (!$user) { + $user = $this->loginRememberedUser(); + } + } catch (\PDOException $e) { + $user = null; + } + } + + return $this->user = $user; + } + + /** + * Determine whether the current user was authenticated using a remember me cookie. + * + * This function is useful when users are performing sensitive operations, and you may want to force them to re-authenticate. + * @return bool + */ + public function viaRemember() + { + return $this->viaRemember; + } + + /** + * Attempt to log in the client from their rememberMe token (in their cookie). + * + * @return User|bool If successful, the User object of the remembered user. Otherwise, return false. + * @throws AuthCompromisedException The client attempted to log in with an invalid rememberMe token. + */ + protected function loginRememberedUser() + { + /** @var \Birke\Rememberme\LoginResult $loginResult */ + $loginResult = $this->rememberMe->login(); + + if ($loginResult->isSuccess()) { + // Update in session + $this->session[$this->config['session.keys.current_user_id']] = $loginResult->getCredential(); + // There is a chance that an attacker has stolen the login token, + // so we store the fact that the user was logged in via RememberMe (instead of login form) + $this->viaRemember = true; + } else { + // If $rememberMe->login() was not successfull, check if the token was invalid as well. This means the cookie was stolen. + if ($loginResult->hasPossibleManipulation()) { + throw new AuthCompromisedException(); + } + } + + return $this->validateUserAccount($loginResult->getCredential()); + } + + /** + * Attempt to log in the client from the session. + * + * @return User|null If successful, the User object of the user in session. Otherwise, return null. + * @throws AuthExpiredException The client attempted to use an expired rememberMe token. + */ + protected function loginSessionUser() + { + $userId = $this->session->get($this->config['session.keys.current_user_id']); + + // If a user_id was found in the session, check any rememberMe cookie that was submitted. + // If they submitted an expired rememberMe cookie, then we need to log them out. + if ($userId) { + if (!$this->validateRememberMeCookie()) { + $this->logout(); + throw new AuthExpiredException(); + } + } + + return $this->validateUserAccount($userId); + } + + /** + * Determine if the cookie contains a valid rememberMe token. + * + * @return bool + */ + protected function validateRememberMeCookie() + { + $cookieValue = $this->rememberMe->getCookie()->getValue(); + if (!$cookieValue) { + return true; + } + $triplet = RememberMeTriplet::fromString($cookieValue); + if (!$triplet->isValid()) { + return false; + } + + return true; + } + + /** + * Tries to load the specified user by id from the database. + * + * Checks that the account is valid and enabled, throwing an exception if not. + * @param int $userId + * @return User|null + * @throws AccountInvalidException + * @throws AccountDisabledException + */ + protected function validateUserAccount($userId) + { + if ($userId) { + $user = $this->classMapper->staticMethod('user', 'find', $userId); + + // If the user doesn't exist any more, throw an exception. + if (!$user) { + throw new AccountInvalidException(); + } + + // If the user has been disabled since their last request, throw an exception. + if (!$user->flag_enabled) { + throw new AccountDisabledException(); + } + + return $user; + } else { + return null; + } + } + + /** + * Flush the cache associated with a session id + * + * @param string $id The session id + * @return bool + */ + public function flushSessionCache($id) + { + return $this->cache->tags('_s' . $id)->flush(); + } +} diff --git a/login/app/sprinkles/account/src/Authenticate/Exception/AccountDisabledException.php b/login/app/sprinkles/account/src/Authenticate/Exception/AccountDisabledException.php new file mode 100755 index 0000000..e79ceb5 --- /dev/null +++ b/login/app/sprinkles/account/src/Authenticate/Exception/AccountDisabledException.php @@ -0,0 +1,21 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Authenticate\Exception; + +use UserFrosting\Support\Exception\HttpException; + +/** + * Disabled account exception. Used when an account has been disabled. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class AccountDisabledException extends HttpException +{ + protected $defaultMessage = 'ACCOUNT.DISABLED'; + protected $httpErrorCode = 403; +} diff --git a/login/app/sprinkles/account/src/Authenticate/Exception/AccountInvalidException.php b/login/app/sprinkles/account/src/Authenticate/Exception/AccountInvalidException.php new file mode 100755 index 0000000..607235b --- /dev/null +++ b/login/app/sprinkles/account/src/Authenticate/Exception/AccountInvalidException.php @@ -0,0 +1,21 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Authenticate\Exception; + +use UserFrosting\Support\Exception\HttpException; + +/** + * Invalid account exception. Used when an account has been removed during an active session. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class AccountInvalidException extends HttpException +{ + protected $defaultMessage = 'ACCOUNT.INVALID'; + protected $httpErrorCode = 403; +} diff --git a/login/app/sprinkles/account/src/Authenticate/Exception/AccountNotVerifiedException.php b/login/app/sprinkles/account/src/Authenticate/Exception/AccountNotVerifiedException.php new file mode 100755 index 0000000..7eb56a6 --- /dev/null +++ b/login/app/sprinkles/account/src/Authenticate/Exception/AccountNotVerifiedException.php @@ -0,0 +1,21 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Authenticate\Exception; + +use UserFrosting\Support\Exception\HttpException; + +/** + * Unverified account exception. Used when an account is required to complete email verification, but hasn't done so yet. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class AccountNotVerifiedException extends HttpException +{ + protected $defaultMessage = 'ACCOUNT.UNVERIFIED'; + protected $httpErrorCode = 403; +} diff --git a/login/app/sprinkles/account/src/Authenticate/Exception/AuthCompromisedException.php b/login/app/sprinkles/account/src/Authenticate/Exception/AuthCompromisedException.php new file mode 100755 index 0000000..df3efbe --- /dev/null +++ b/login/app/sprinkles/account/src/Authenticate/Exception/AuthCompromisedException.php @@ -0,0 +1,20 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Authenticate\Exception; + +use UserFrosting\Support\Exception\ForbiddenException; + +/** + * Compromised authentication exception. Used when we suspect theft of the rememberMe cookie. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class AuthCompromisedException extends ForbiddenException +{ + protected $defaultMessage = 'ACCOUNT.SESSION_COMPROMISED'; +} diff --git a/login/app/sprinkles/account/src/Authenticate/Exception/AuthExpiredException.php b/login/app/sprinkles/account/src/Authenticate/Exception/AuthExpiredException.php new file mode 100755 index 0000000..5583746 --- /dev/null +++ b/login/app/sprinkles/account/src/Authenticate/Exception/AuthExpiredException.php @@ -0,0 +1,21 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Authenticate\Exception; + +use UserFrosting\Support\Exception\HttpException; + +/** + * Expired authentication exception. Used when the user needs to authenticate/reauthenticate. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class AuthExpiredException extends HttpException +{ + protected $defaultMessage = 'ACCOUNT.SESSION_EXPIRED'; + protected $httpErrorCode = 401; +} diff --git a/login/app/sprinkles/account/src/Authenticate/Exception/InvalidCredentialsException.php b/login/app/sprinkles/account/src/Authenticate/Exception/InvalidCredentialsException.php new file mode 100755 index 0000000..18d4a5c --- /dev/null +++ b/login/app/sprinkles/account/src/Authenticate/Exception/InvalidCredentialsException.php @@ -0,0 +1,21 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Authenticate\Exception; + +use UserFrosting\Support\Exception\HttpException; + +/** + * Invalid credentials exception. Used when an account fails authentication for some reason. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class InvalidCredentialsException extends HttpException +{ + protected $defaultMessage = 'USER_OR_PASS_INVALID'; + protected $httpErrorCode = 403; +} diff --git a/login/app/sprinkles/account/src/Authenticate/Hasher.php b/login/app/sprinkles/account/src/Authenticate/Hasher.php new file mode 100755 index 0000000..e277eef --- /dev/null +++ b/login/app/sprinkles/account/src/Authenticate/Hasher.php @@ -0,0 +1,108 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Authenticate; + +/** + * Password hashing and validation class + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class Hasher +{ + /** + * Default crypt cost factor. + * + * @var int + */ + protected $defaultRounds = 10; + + /** + * Returns the hashing type for a specified password hash. + * + * Automatically detects the hash type: "sha1" (for UserCake legacy accounts), "legacy" (for 0.1.x accounts), and "modern" (used for new accounts). + * @param string $password the hashed password. + * @return string "sha1"|"legacy"|"modern". + */ + public function getHashType($password) + { + // If the password in the db is 65 characters long, we have an sha1-hashed password. + if (strlen($password) == 65) { + return 'sha1'; + } elseif (strlen($password) == 82) { + return 'legacy'; + } + + return 'modern'; + } + + /** + * Hashes a plaintext password using bcrypt. + * + * @param string $password the plaintext password. + * @param array $options + * @return string the hashed password. + * @throws HashFailedException + */ + public function hash($password, array $options = []) + { + $hash = password_hash($password, PASSWORD_BCRYPT, [ + 'cost' => $this->cost($options), + ]); + + if (!$hash) { + throw new HashFailedException(); + } + + return $hash; + } + + /** + * Verify a plaintext password against the user's hashed password. + * + * @param string $password The plaintext password to verify. + * @param string $hash The hash to compare against. + * @param array $options + * @return boolean True if the password matches, false otherwise. + */ + public function verify($password, $hash, array $options = []) + { + $hashType = $this->getHashType($hash); + + if ($hashType == 'sha1') { + // Legacy UserCake passwords + $salt = substr($hash, 0, 25); // Extract the salt from the hash + $inputHash = $salt . sha1($salt . $password); + + return (hash_equals($inputHash, $hash) === true); + + } elseif ($hashType == 'legacy') { + // Homegrown implementation (assuming that current install has been using a cost parameter of 12) + // Used for manual implementation of bcrypt. + // Note that this legacy hashing put the salt at the _end_ for some reason. + $salt = substr($hash, 60); + $inputHash = crypt($password, '$2y$12$' . $salt); + $correctHash = substr($hash, 0, 60); + + return (hash_equals($inputHash, $correctHash) === true); + } + + // Modern implementation + return password_verify($password, $hash); + } + + /** + * Extract the cost value from the options array. + * + * @param array $options + * @return int + */ + protected function cost(array $options = []) + { + return isset($options['rounds']) ? $options['rounds'] : $this->defaultRounds; + } +} diff --git a/login/app/sprinkles/account/src/Authorize/AccessConditionExpression.php b/login/app/sprinkles/account/src/Authorize/AccessConditionExpression.php new file mode 100755 index 0000000..dd5647e --- /dev/null +++ b/login/app/sprinkles/account/src/Authorize/AccessConditionExpression.php @@ -0,0 +1,139 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Authorize; + +use Monolog\Logger; +use PhpParser\Lexer\Emulative as EmulativeLexer; +use PhpParser\Node; +use PhpParser\NodeTraverser; +use PhpParser\Parser as Parser; +use PhpParser\PrettyPrinter\Standard as StandardPrettyPrinter; +use PhpParser\Error as PhpParserException; +use Psr\Http\Message\ServerRequestInterface as Request; +use UserFrosting\Sprinkle\Account\Database\Models\User; + +/** + * AccessConditionExpression class + * + * This class models the evaluation of an authorization condition expression, as associated with permissions. + * A condition is built as a boolean expression composed of AccessCondition method calls. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class AccessConditionExpression +{ + /** + * @var User A user object, which for convenience can be referenced as 'self' in access conditions. + */ + protected $user; + + /** + * @var ParserNodeFunctionEvaluator The node visitor, which evaluates access condition callbacks used in a permission condition. + */ + protected $nodeVisitor; + + /** + * @var \PhpParser\Parser The PhpParser object to use (initialized in the ctor) + */ + protected $parser; + + /** + * @var NodeTraverser The NodeTraverser object to use (initialized in the ctor) + */ + protected $traverser; + + /** + * @var StandardPrettyPrinter The PrettyPrinter object to use (initialized in the ctor) + */ + protected $prettyPrinter; + + /** + * @var Logger + */ + protected $logger; + + /** + * @var bool Set to true if you want debugging information printed to the auth log. + */ + protected $debug; + + /** + * Create a new AccessConditionExpression object. + * + * @param User $user A user object, which for convenience can be referenced as 'self' in access conditions. + * @param Logger $logger A Monolog logger, used to dump debugging info for authorization evaluations. + * @param bool $debug Set to true if you want debugging information printed to the auth log. + */ + public function __construct(ParserNodeFunctionEvaluator $nodeVisitor, User $user, Logger $logger, $debug = false) + { + $this->nodeVisitor = $nodeVisitor; + $this->user = $user; + $this->parser = new Parser(new EmulativeLexer); + $this->traverser = new NodeTraverser; + $this->traverser->addVisitor($nodeVisitor); + $this->prettyPrinter = new StandardPrettyPrinter; + $this->logger = $logger; + $this->debug = $debug; + } + + /** + * Evaluates a condition expression, based on the given parameters. + * + * The special parameter `self` is an array of the current user's data. + * This get included automatically, and so does not need to be passed in. + * @param string $condition a boolean expression composed of calls to AccessCondition functions. + * @param array[mixed] $params the parameters to be used when evaluating the expression. + * @return bool true if the condition is passed for the given parameters, otherwise returns false. + */ + public function evaluateCondition($condition, $params) + { + // Set the reserved `self` parameters. + // This replaces any values of `self` specified in the arguments, thus preventing them from being overridden in malicious user input. + // (For example, from an unfiltered request body). + $params['self'] = $this->user->export(); + + $this->nodeVisitor->setParams($params); + + $code = "<?php $condition;"; + + if ($this->debug) { + $this->logger->debug("Evaluating access condition '$condition' with parameters:", $params); + } + + // Traverse the parse tree, and execute any callbacks found using the supplied parameters. + // Replace the function node with the return value of the callback. + try { + // parse + $stmts = $this->parser->parse($code); + + // traverse + $stmts = $this->traverser->traverse($stmts); + + // Evaluate boolean statement. It is safe to use eval() here, because our expression has been reduced entirely to a boolean expression. + $expr = $this->prettyPrinter->prettyPrintExpr($stmts[0]); + $expr_eval = "return " . $expr . ";\n"; + $result = eval($expr_eval); + + if ($this->debug) { + $this->logger->debug("Expression '$expr' evaluates to " . ($result == true ? "true" : "false")); + } + + return $result; + } catch (PhpParserException $e) { + if ($this->debug) { + $this->logger->debug("Error parsing access condition '$condition':" . $e->getMessage()); + } + return false; // Access fails if the access condition can't be parsed. + } catch (AuthorizationException $e) { + if ($this->debug) { + $this->logger->debug("Error parsing access condition '$condition':" . $e->getMessage()); + } + return false; + } + } +} diff --git a/login/app/sprinkles/account/src/Authorize/AuthorizationException.php b/login/app/sprinkles/account/src/Authorize/AuthorizationException.php new file mode 100755 index 0000000..251b67f --- /dev/null +++ b/login/app/sprinkles/account/src/Authorize/AuthorizationException.php @@ -0,0 +1,23 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Authorize; + +use UserFrosting\Support\Exception\ForbiddenException; + +/** + * AuthorizationException class + * + * Exception for AccessConditionExpression. + * + * @author Alex Weissman (https://alexanderweissman.com) + * @see http://www.userfrosting.com/components/#authorization + */ +class AuthorizationException extends ForbiddenException +{ + +} diff --git a/login/app/sprinkles/account/src/Authorize/AuthorizationManager.php b/login/app/sprinkles/account/src/Authorize/AuthorizationManager.php new file mode 100755 index 0000000..def152b --- /dev/null +++ b/login/app/sprinkles/account/src/Authorize/AuthorizationManager.php @@ -0,0 +1,157 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Authorize; + +use Interop\Container\ContainerInterface; +use UserFrosting\Sprinkle\Account\Database\Models\User; + +/** + * AuthorizationManager class. + * + * Manages a collection of access condition callbacks, and uses them to perform access control checks on user objects. + * @author Alex Weissman (https://alexanderweissman.com) + */ +class AuthorizationManager +{ + /** + * @var ContainerInterface The global container object, which holds all your services. + */ + protected $ci; + + /** + * @var array[callable] An array of callbacks that accept some parameters and evaluate to true or false. + */ + protected $callbacks = []; + + /** + * Create a new AuthorizationManager object. + * + * @param ContainerInterface $ci The global container object, which holds all your services. + */ + public function __construct(ContainerInterface $ci, array $callbacks = []) + { + $this->ci = $ci; + $this->callbacks = $callbacks; + } + + /** + * Register an authorization callback, which can then be used in permission conditions. + * + * To add additional callbacks, simply extend the `authorizer` service in your Sprinkle's service provider. + * @param string $name + * @param callable $callback + */ + public function addCallback($name, $callback) + { + $this->callbacks[$name] = $callback; + return $this; + } + + /** + * Get all authorization callbacks. + * + * @return callable[] + */ + public function getCallbacks() + { + return $this->callbacks; + } + + /** + * Checks whether or not a user has access on a particular permission slug. + * + * Determine if this user has access to the given $slug under the given $params. + * + * @param UserFrosting\Sprinkle\Account\Database\Models\User $user + * @param string $slug The permission slug to check for access. + * @param array $params[optional] An array of field names => values, specifying any additional data to provide the authorization module + * when determining whether or not this user has access. + * @return boolean True if the user has access, false otherwise. + */ + public function checkAccess(User $user, $slug, array $params = []) + { + $debug = $this->ci->config['debug.auth']; + + if ($debug) { + $trace = array_slice(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3), 1); + $this->ci->authLogger->debug("Authorization check requested at: ", $trace); + $this->ci->authLogger->debug("Checking authorization for user {$user->id} ('{$user->user_name}') on permission '$slug'..."); + } + + if ($this->ci->authenticator->guest()) { + if ($debug) { + $this->ci->authLogger->debug("User is not logged in. Access denied."); + } + return false; + } + + // The master (root) account has access to everything. + // Need to use loose comparison for now, because some DBs return `id` as a string. + + if ($user->id == $this->ci->config['reserved_user_ids.master']) { + if ($debug) { + $this->ci->authLogger->debug("User is the master (root) user. Access granted."); + } + return true; + } + + // Find all permissions that apply to this user (via roles), and check if any evaluate to true. + $permissions = $user->getCachedPermissions(); + + if (empty($permissions) || !isset($permissions[$slug])) { + if ($debug) { + $this->ci->authLogger->debug("No matching permissions found. Access denied."); + } + return false; + } + + $permissions = $permissions[$slug]; + + if ($debug) { + $this->ci->authLogger->debug("Found matching permissions: \n" . print_r($this->getPermissionsArrayDebugInfo($permissions), true)); + } + + $nodeVisitor = new ParserNodeFunctionEvaluator($this->callbacks, $this->ci->authLogger, $debug); + $ace = new AccessConditionExpression($nodeVisitor, $user, $this->ci->authLogger, $debug); + + foreach ($permissions as $permission) { + $pass = $ace->evaluateCondition($permission->conditions, $params); + if ($pass) { + if ($debug) { + $this->ci->authLogger->debug("User passed conditions '{$permission->conditions}' . Access granted."); + } + return true; + } + } + + if ($debug) { + $this->ci->authLogger->debug("User failed to pass any of the matched permissions. Access denied."); + } + + return false; + } + + /** + * Remove extraneous information from the permission to reduce verbosity. + * + * @param array + * @return array + */ + protected function getPermissionsArrayDebugInfo($permissions) + { + $permissionsInfo = []; + foreach ($permissions as $permission) { + $permissionData = array_only($permission->toArray(), ['id', 'slug', 'name', 'conditions', 'description']); + // Remove this until we can find an efficient way to only load these once during debugging + //$permissionData['roles_via'] = $permission->roles_via->pluck('id')->all(); + $permissionsInfo[] = $permissionData; + } + + return $permissionsInfo; + } +} diff --git a/login/app/sprinkles/account/src/Authorize/ParserNodeFunctionEvaluator.php b/login/app/sprinkles/account/src/Authorize/ParserNodeFunctionEvaluator.php new file mode 100755 index 0000000..e8e5cde --- /dev/null +++ b/login/app/sprinkles/account/src/Authorize/ParserNodeFunctionEvaluator.php @@ -0,0 +1,193 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Authorize; + +use Monolog\Logger; +use PhpParser\Node; +use PhpParser\NodeVisitorAbstract; +use PhpParser\PrettyPrinter\Standard as StandardPrettyPrinter; + +/** + * ParserNodeFunctionEvaluator class + * + * This class parses access control condition expressions. + * + * @author Alex Weissman (https://alexanderweissman.com) + * @see http://www.userfrosting.com/components/#authorization + */ +class ParserNodeFunctionEvaluator extends NodeVisitorAbstract +{ + + /** + * @var array[callable] An array of callback functions to be used when evaluating a condition expression. + */ + protected $callbacks; + /** + * @var \PhpParser\PrettyPrinter\Standard The PrettyPrinter object to use (initialized in the ctor) + */ + protected $prettyPrinter; + /** + * @var array The parameters to be used when evaluating the methods in the condition expression, as an array. + */ + protected $params = []; + + /** + * @var Logger + */ + protected $logger; + + /** + * @var bool Set to true if you want debugging information printed to the auth log. + */ + protected $debug; + + /** + * Create a new ParserNodeFunctionEvaluator object. + * + * @param array $params The parameters to be used when evaluating the methods in the condition expression, as an array. + * @param Logger $logger A Monolog logger, used to dump debugging info for authorization evaluations. + * @param bool $debug Set to true if you want debugging information printed to the auth log. + */ + public function __construct($callbacks, $logger, $debug = false) + { + $this->callbacks = $callbacks; + $this->prettyPrinter = new StandardPrettyPrinter; + $this->logger = $logger; + $this->debug = $debug; + $this->params = []; + } + + public function leaveNode(Node $node) + { + // Look for function calls + if ($node instanceof \PhpParser\Node\Expr\FuncCall) { + $eval = new \PhpParser\Node\Scalar\LNumber; + + // Get the method name + $callbackName = $node->name->toString(); + // Get the method arguments + $argNodes = $node->args; + + $args = []; + $argsInfo = []; + foreach ($argNodes as $arg) { + $argString = $this->prettyPrinter->prettyPrintExpr($arg->value); + + // Debugger info + $currentArgInfo = [ + 'expression' => $argString + ]; + // Resolve parameter placeholders ('variable' names (either single-word or array-dot identifiers)) + if (($arg->value instanceof \PhpParser\Node\Expr\BinaryOp\Concat) || ($arg->value instanceof \PhpParser\Node\Expr\ConstFetch)) { + $value = $this->resolveParamPath($argString); + $currentArgInfo['type'] = "parameter"; + $currentArgInfo['resolved_value'] = $value; + // Resolve arrays + } elseif ($arg->value instanceof \PhpParser\Node\Expr\Array_) { + $value = $this->resolveArray($arg); + $currentArgInfo['type'] = "array"; + $currentArgInfo['resolved_value'] = print_r($value, true); + // Resolve strings + } elseif ($arg->value instanceof \PhpParser\Node\Scalar\String_) { + $value = $arg->value->value; + $currentArgInfo['type'] = "string"; + $currentArgInfo['resolved_value'] = $value; + // Resolve numbers + } elseif ($arg->value instanceof \PhpParser\Node\Scalar\DNumber) { + $value = $arg->value->value; + $currentArgInfo['type'] = "float"; + $currentArgInfo['resolved_value'] = $value; + } elseif ($arg->value instanceof \PhpParser\Node\Scalar\LNumber) { + $value = $arg->value->value; + $currentArgInfo['type'] = "integer"; + $currentArgInfo['resolved_value'] = $value; + // Anything else is simply interpreted as its literal string value + } else { + $value = $argString; + $currentArgInfo['type'] = "unknown"; + $currentArgInfo['resolved_value'] = $value; + } + + $args[] = $value; + $argsInfo[] = $currentArgInfo; + } + + if ($this->debug) { + if (count($args)) { + $this->logger->debug("Evaluating callback '$callbackName' on: ", $argsInfo); + } else { + $this->logger->debug("Evaluating callback '$callbackName'..."); + } + } + + // Call the specified access condition callback with the specified arguments. + if (isset($this->callbacks[$callbackName]) && is_callable($this->callbacks[$callbackName])) { + $result = call_user_func_array($this->callbacks[$callbackName], $args); + } else { + throw new AuthorizationException("Authorization failed: Access condition method '$callbackName' does not exist."); + } + + if ($this->debug) { + $this->logger->debug("Result: " . ($result ? "1" : "0")); + } + + return new \PhpParser\Node\Scalar\LNumber($result ? "1" : "0"); + } + } + + public function setParams($params) + { + $this->params = $params; + } + + /** + * Resolve an array expression in a condition expression into an actual array. + * + * @param string $arg the array, represented as a string. + * @return array[mixed] the array, as a plain ol' PHP array. + */ + private function resolveArray($arg) + { + $arr = []; + $items = (array) $arg->value->items; + foreach ($items as $item) { + if ($item->key) { + $arr[$item->key] = $item->value->value; + } else { + $arr[] = $item->value->value; + } + } + return $arr; + } + + /** + * Resolve a parameter path (e.g. "user.id", "post", etc) into its value. + * + * @param string $path the name of the parameter to resolve, based on the parameters set in this object. + * @throws Exception the path could not be resolved. Path is malformed or key does not exist. + * @return mixed the value of the specified parameter. + */ + private function resolveParamPath($path) + { + $pathTokens = explode(".", $path); + $value = $this->params; + foreach ($pathTokens as $token) { + $token = trim($token); + if (is_array($value) && isset($value[$token])) { + $value = $value[$token]; + continue; + } elseif (is_object($value) && isset($value->$token)) { + $value = $value->$token; + continue; + } else { + throw new AuthorizationException("Cannot resolve the path \"$path\". Error at token \"$token\"."); + } + } + return $value; + } +} diff --git a/login/app/sprinkles/account/src/Bakery/CreateAdminUser.php b/login/app/sprinkles/account/src/Bakery/CreateAdminUser.php new file mode 100755 index 0000000..cfaacef --- /dev/null +++ b/login/app/sprinkles/account/src/Bakery/CreateAdminUser.php @@ -0,0 +1,334 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Bakery; + +use Illuminate\Database\Capsule\Manager as Capsule; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use UserFrosting\System\Bakery\BaseCommand; +use UserFrosting\System\Bakery\DatabaseTest; +use UserFrosting\System\Database\Model\Migrations; +use UserFrosting\Sprinkle\Account\Database\Models\User; +use UserFrosting\Sprinkle\Account\Database\Models\Role; +use UserFrosting\Sprinkle\Account\Facades\Password; + +/** + * Create root user CLI command. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class CreateAdminUser extends BaseCommand +{ + use DatabaseTest; + + /** + * @var string[] Migration dependencies for this command to work + */ + protected $dependencies = [ + '\UserFrosting\Sprinkle\Account\Database\Migrations\v400\UsersTable', + '\UserFrosting\Sprinkle\Account\Database\Migrations\v400\RolesTable', + '\UserFrosting\Sprinkle\Account\Database\Migrations\v400\RoleUsersTable' + ]; + + /** + * {@inheritDoc} + */ + protected function configure() + { + $this->setName("create-admin") + ->setDescription("Create the initial admin (root) user account"); + } + + /** + * {@inheritDoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->io->title("Root account setup"); + + // Need the database + try { + $this->io->writeln("<info>Testing database connection</info>", OutputInterface::VERBOSITY_VERBOSE); + $this->testDB(); + $this->io->writeln("Ok", OutputInterface::VERBOSITY_VERBOSE); + } catch (\Exception $e) { + $this->io->error($e->getMessage()); + exit(1); + } + + // Need migration table + if (!Capsule::schema()->hasColumn('migrations', 'id')) { + $this->io->error("Migrations doesn't appear to have been run! Make sure the database is properly migrated by using the `php bakery migrate` command."); + exit(1); + } + + // Make sure the required mirgations have been run + foreach ($this->dependencies as $migration) { + if (!Migrations::where('migration', $migration)->exists()) { + $this->io->error("Migration `$migration` doesn't appear to have been run! Make sure all migrations are up to date by using the `php bakery migrate` command."); + exit(1); + } + } + + // Make sure that there are no users currently in the user table + // We setup the root account here so it can be done independent of the version check + if (User::count() > 0) { + + $this->io->note("Table 'users' is not empty. Skipping root account setup. To set up the root account again, please truncate or drop the table and try again."); + + } else { + + $this->io->writeln("Please answer the following questions to create the root account:\n"); + + // Get the account details + $userName = $this->askUsername(); + $email = $this->askEmail(); + $firstName = $this->askFirstName(); + $lastName = $this->askLastName(); + $password = $this->askPassword(); + + // Ok, now we've got the info and we can create the new user. + $this->io->write("\n<info>Saving the root user details...</info>"); + $rootUser = new User([ + "user_name" => $userName, + "email" => $email, + "first_name" => $firstName, + "last_name" => $lastName, + "password" => Password::hash($password) + ]); + + $rootUser->save(); + + $defaultRoles = [ + 'user' => Role::where('slug', 'user')->first(), + 'group-admin' => Role::where('slug', 'group-admin')->first(), + 'site-admin' => Role::where('slug', 'site-admin')->first() + ]; + + foreach ($defaultRoles as $slug => $role) { + if ($role) { + $rootUser->roles()->attach($role->id); + } + } + + $this->io->success("Root user creation successful!"); + } + } + + /** + * Ask for the username + * + * @access protected + * @return void + */ + protected function askUsername() + { + while (!isset($userName) || !$this->validateUsername($userName)) { + $userName = $this->io->ask("Choose a root username (1-50 characters, no leading or trailing whitespace)"); + } + return $userName; + } + + /** + * Validate the username. + * + * @access protected + * @param mixed $userName + * @return void + */ + protected function validateUsername($userName) + { + // Validate length + if (strlen($userName) < 1 || strlen($userName) > 50) { + $this->io->error("Username must be between 1-50 characters"); + return false; + } + + // Validate format + $options = [ + 'options' => [ + 'regexp' => "/^\S((.*\S)|)$/" + ] + ]; + $validate = filter_var($userName, FILTER_VALIDATE_REGEXP, $options); + if (!$validate) { + $this->io->error("Username can't have any leading or trailing whitespace"); + return false; + } + + return true; + } + + /** + * Ask for the email + * + * @access protected + * @return void + */ + protected function askEmail() + { + while (!isset($email) || !$this->validateEmail($email)) { + $email = $this->io->ask("Enter a valid email address (1-254 characters, must be compatible with FILTER_VALIDATE_EMAIL)"); + } + return $email; + } + + /** + * Validate the email. + * + * @access protected + * @param mixed $email + * @return void + */ + protected function validateEmail($email) + { + // Validate length + if (strlen($email) < 1 || strlen($email) > 254) { + $this->io->error("Email must be between 1-254 characters"); + return false; + } + + // Validate format + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + $this->io->error("Email must be compatible with FILTER_VALIDATE_EMAIL"); + return false; + } + + return true; + } + + /** + * Ask for the first name + * + * @access protected + * @return void + */ + protected function askFirstName() + { + while (!isset($firstName) || !$this->validateFirstName($firstName)) { + $firstName = $this->io->ask("Enter the user first name (1-20 characters)"); + } + return $firstName; + } + + /** + * validateFirstName function. + * + * @access protected + * @param mixed $name + * @return void + */ + protected function validateFirstName($firstName) + { + // Validate length + if (strlen($firstName) < 1 || strlen($firstName) > 20) { + $this->io->error("First name must be between 1-20 characters"); + return false; + } + + return true; + } + + /** + * Ask for the last name + * + * @access protected + * @return void + */ + protected function askLastName() + { + while (!isset($lastName) || !$this->validateLastName($lastName)) { + $lastName = $this->io->ask("Enter the user last name (1-30 characters)"); + } + return $lastName; + } + + /** + * validateLastName function. + * + * @access protected + * @param mixed $lastName + * @return void + */ + protected function validateLastName($lastName) + { + // Validate length + if (strlen($lastName) < 1 || strlen($lastName) > 30) { + $this->io->error("Last name must be between 1-30 characters"); + return false; + } + + return true; + } + + /** + * Ask for the password + * + * @access protected + * @return void + */ + protected function askPassword() + { + while (!isset($password) || !$this->validatePassword($password) || !$this->confirmPassword($password)) { + $password = $this->io->askHidden("Enter password (12-255 characters)"); + } + return $password; + } + + /** + * validatePassword function. + * + * @access protected + * @param mixed $password + * @return void + */ + protected function validatePassword($password) + { + if (strlen($password) < 12 || strlen($password) > 255) { + $this->io->error("Password must be between 12-255 characters"); + return false; + } + + return true; + } + + /** + * confirmPassword function. + * + * @access protected + * @param mixed $passwordToConfirm + * @return void + */ + protected function confirmPassword($passwordToConfirm) + { + while (!isset($password)) { + $password = $this->io->askHidden("Please re-enter the chosen password"); + } + return $this->validatePasswordConfirmation($password, $passwordToConfirm); + } + + /** + * validatePasswordConfirmation function. + * + * @access protected + * @param mixed $password + * @param mixed $passwordToConfirm + * @return void + */ + protected function validatePasswordConfirmation($password, $passwordToConfirm) + { + if ($password != $passwordToConfirm) { + $this->io->error("Passwords do not match, please try again."); + return false; + } + + return true; + } +}
\ No newline at end of file diff --git a/login/app/sprinkles/account/src/Controller/AccountController.php b/login/app/sprinkles/account/src/Controller/AccountController.php new file mode 100755 index 0000000..ce99370 --- /dev/null +++ b/login/app/sprinkles/account/src/Controller/AccountController.php @@ -0,0 +1,1293 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Controller; + +use Carbon\Carbon; +use Illuminate\Database\Capsule\Manager as Capsule; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; +use Slim\Exception\NotFoundException as NotFoundException; +use UserFrosting\Fortress\RequestDataTransformer; +use UserFrosting\Fortress\RequestSchema; +use UserFrosting\Fortress\ServerSideValidator; +use UserFrosting\Fortress\Adapter\JqueryValidationAdapter; +use UserFrosting\Sprinkle\Account\Controller\Exception\SpammyRequestException; +use UserFrosting\Sprinkle\Account\Facades\Password; +use UserFrosting\Sprinkle\Account\Util\Util as AccountUtil; +use UserFrosting\Sprinkle\Core\Controller\SimpleController; +use UserFrosting\Sprinkle\Core\Mail\EmailRecipient; +use UserFrosting\Sprinkle\Core\Mail\TwigMailMessage; +use UserFrosting\Sprinkle\Core\Util\Captcha; +use UserFrosting\Support\Exception\BadRequestException; +use UserFrosting\Support\Exception\ForbiddenException; +use UserFrosting\Support\Exception\HttpException; + +/** + * Controller class for /account/* URLs. Handles account-related activities, including login, registration, password recovery, and account settings. + * + * @author Alex Weissman (https://alexanderweissman.com) + * @see http://www.userfrosting.com/navigating/#structure + */ +class AccountController extends SimpleController +{ + /** + * Check a username for availability. + * + * This route is throttled by default, to discourage abusing it for account enumeration. + * This route is "public access". + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function checkUsername(Request $request, Response $response, $args) + { + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + // GET parameters + $params = $request->getQueryParams(); + + // Load request schema + $schema = new RequestSchema('schema://requests/check-username.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and halt on validation errors. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + // TODO: encapsulate the communication of error messages from ServerSideValidator to the BadRequestException + $e = new BadRequestException('Missing or malformed request data!'); + foreach ($validator->errors() as $idx => $field) { + foreach($field as $eidx => $error) { + $e->addUserMessage($error); + } + } + throw $e; + } + + /** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */ + $throttler = $this->ci->throttler; + $delay = $throttler->getDelay('check_username_request'); + + // Throttle requests + if ($delay > 0) { + return $response->withStatus(429); + } + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\I18n\MessageTranslator $translator */ + $translator = $this->ci->translator; + + // Log throttleable event + $throttler->logEvent('check_username_request'); + + if ($classMapper->staticMethod('user', 'findUnique', $data['user_name'], 'user_name')) { + $message = $translator->translate('USERNAME.NOT_AVAILABLE', $data); + return $response->write($message)->withStatus(200); + } else { + return $response->write('true')->withStatus(200); + } + } + + /** + * Processes a request to cancel a password reset request. + * + * This is provided so that users can cancel a password reset request, if they made it in error or if it was not initiated by themselves. + * Processes the request from the password reset link, checking that: + * 1. The provided token is associated with an existing user account, who has a pending password reset request. + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function denyResetPassword(Request $request, Response $response, $args) + { + // GET parameters + $params = $request->getQueryParams(); + + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + $loginPage = $this->ci->router->pathFor('login'); + + // Load validation rules + $schema = new RequestSchema('schema://requests/deny-password.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and halt on validation errors. Since this is a GET request, we need to redirect on failure + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + // 400 code + redirect is perfectly fine, according to user Dilaz in #laravel + return $response->withRedirect($loginPage, 400); + } + + $passwordReset = $this->ci->repoPasswordReset->cancel($data['token']); + + if (!$passwordReset) { + $ms->addMessageTranslated('danger', 'PASSWORD.FORGET.INVALID'); + return $response->withRedirect($loginPage, 400); + } + + $ms->addMessageTranslated('success', 'PASSWORD.FORGET.REQUEST_CANNED'); + return $response->withRedirect($loginPage); + } + + /** + * Processes a request to email a forgotten password reset link to the user. + * + * Processes the request from the form on the "forgot password" page, checking that: + * 1. The rate limit for this type of request is being observed. + * 2. The provided email address belongs to a registered account; + * 3. The submitted data is valid. + * Note that we have removed the requirement that a password reset request not already be in progress. + * This is because we need to allow users to re-request a reset, even if they lose the first reset email. + * This route is "public access". + * Request type: POST + * @todo require additional user information + * @todo prevent password reset requests for root account? + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function forgotPassword(Request $request, Response $response, $args) + { + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + // Get POST parameters + $params = $request->getParsedBody(); + + // Load the request schema + $schema = new RequestSchema('schema://requests/forgot-password.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and halt on validation errors. Failed validation attempts do not count towards throttling limit. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + return $response->withStatus(400); + } + + // Throttle requests + + /** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */ + $throttler = $this->ci->throttler; + + $throttleData = [ + 'email' => $data['email'] + ]; + $delay = $throttler->getDelay('password_reset_request', $throttleData); + + if ($delay > 0) { + $ms->addMessageTranslated('danger', 'RATE_LIMIT_EXCEEDED', ['delay' => $delay]); + return $response->withStatus(429); + } + + // All checks passed! log events/activities, update user, and send email + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction( function() use ($classMapper, $data, $throttler, $throttleData, $config) { + + // Log throttleable event + $throttler->logEvent('password_reset_request', $throttleData); + + // Load the user, by email address + $user = $classMapper->staticMethod('user', 'where', 'email', $data['email'])->first(); + + // Check that the email exists. + // If there is no user with that email address, we should still pretend like we succeeded, to prevent account enumeration + if ($user) { + // Try to generate a new password reset request. + // Use timeout for "reset password" + $passwordReset = $this->ci->repoPasswordReset->create($user, $config['password_reset.timeouts.reset']); + + // Create and send email + $message = new TwigMailMessage($this->ci->view, 'mail/password-reset.html.twig'); + $message->from($config['address_book.admin']) + ->addEmailRecipient(new EmailRecipient($user->email, $user->full_name)) + ->addParams([ + 'user' => $user, + 'token' => $passwordReset->getToken(), + 'request_date' => Carbon::now()->format('Y-m-d H:i:s') + ]); + + $this->ci->mailer->send($message); + } + }); + + // TODO: create delay to prevent timing-based attacks + + $ms->addMessageTranslated('success', 'PASSWORD.FORGET.REQUEST_SENT', ['email' => $data['email']]); + return $response->withStatus(200); + } + + /** + * Returns a modal containing account terms of service. + * + * This does NOT render a complete page. Instead, it renders the HTML for the form, which can be embedded in other pages. + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function getModalAccountTos(Request $request, Response $response, $args) + { + return $this->ci->view->render($response, 'modals/tos.html.twig'); + } + + /** + * Generate a random captcha, store it to the session, and return the captcha image. + * + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function imageCaptcha(Request $request, Response $response, $args) + { + $captcha = new Captcha($this->ci->session, $this->ci->config['session.keys.captcha']); + $captcha->generateRandomCode(); + + return $response->withStatus(200) + ->withHeader('Content-Type', 'image/png;base64') + ->write($captcha->getImage()); + } + + /** + * Processes an account login request. + * + * Processes the request from the form on the login page, checking that: + * 1. The user is not already logged in. + * 2. The rate limit for this type of request is being observed. + * 3. Email login is enabled, if an email address was used. + * 4. The user account exists. + * 5. The user account is enabled and verified. + * 6. The user entered a valid username/email and password. + * This route, by definition, is "public access". + * Request type: POST + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function login(Request $request, Response $response, $args) + { + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ + $authenticator = $this->ci->authenticator; + + // Return 200 success if user is already logged in + if ($authenticator->check()) { + $ms->addMessageTranslated('warning', 'LOGIN.ALREADY_COMPLETE'); + return $response->withStatus(200); + } + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + // Get POST parameters + $params = $request->getParsedBody(); + + // Load the request schema + $schema = new RequestSchema('schema://requests/login.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and halt on validation errors. Failed validation attempts do not count towards throttling limit. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + return $response->withStatus(400); + } + + // Determine whether we are trying to log in with an email address or a username + $isEmail = filter_var($data['user_name'], FILTER_VALIDATE_EMAIL); + + // Throttle requests + + /** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */ + $throttler = $this->ci->throttler; + + $userIdentifier = $data['user_name']; + + $throttleData = [ + 'user_identifier' => $userIdentifier + ]; + + $delay = $throttler->getDelay('sign_in_attempt', $throttleData); + if ($delay > 0) { + $ms->addMessageTranslated('danger', 'RATE_LIMIT_EXCEEDED', [ + 'delay' => $delay + ]); + return $response->withStatus(429); + } + + // Log throttleable event + $throttler->logEvent('sign_in_attempt', $throttleData); + + // If credential is an email address, but email login is not enabled, raise an error. + // Note that we do this after logging throttle event, so this error counts towards throttling limit. + if ($isEmail && !$config['site.login.enable_email']) { + $ms->addMessageTranslated('danger', 'USER_OR_PASS_INVALID'); + return $response->withStatus(403); + } + + // Try to authenticate the user. Authenticator will throw an exception on failure. + /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ + $authenticator = $this->ci->authenticator; + + $currentUser = $authenticator->attempt(($isEmail ? 'email' : 'user_name'), $userIdentifier, $data['password'], $data['rememberme']); + + $ms->addMessageTranslated('success', 'WELCOME', $currentUser->export()); + + // Set redirect, if relevant + $redirectOnLogin = $this->ci->get('redirect.onLogin'); + + return $redirectOnLogin($request, $response, $args); + } + + /** + * Log the user out completely, including destroying any "remember me" token. + * + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function logout(Request $request, Response $response, $args) + { + // Destroy the session + $this->ci->authenticator->logout(); + + // Return to home page + $config = $this->ci->config; + return $response->withStatus(302)->withHeader('Location', $config['site.uri.public']); + } + + /** + * Render the "forgot password" page. + * + * This creates a simple form to allow users who forgot their password to have a time-limited password reset link emailed to them. + * By default, this is a "public page" (does not require authentication). + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function pageForgotPassword(Request $request, Response $response, $args) + { + // Load validation rules + $schema = new RequestSchema('schema://requests/forgot-password.yaml'); + $validator = new JqueryValidationAdapter($schema, $this->ci->translator); + + return $this->ci->view->render($response, 'pages/forgot-password.html.twig', [ + 'page' => [ + 'validators' => [ + 'forgot_password' => $validator->rules('json', false) + ] + ] + ]); + } + + + /** + * Render the account registration page for UserFrosting. + * + * This allows new (non-authenticated) users to create a new account for themselves on your website (if enabled). + * By definition, this is a "public page" (does not require authentication). + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function pageRegister(Request $request, Response $response, $args) + { + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + if (!$config['site.registration.enabled']) { + throw new NotFoundException($request, $response); + } + + /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ + $authenticator = $this->ci->authenticator; + + // Redirect if user is already logged in + if ($authenticator->check()) { + $redirect = $this->ci->get('redirect.onAlreadyLoggedIn'); + + return $redirect($request, $response, $args); + } + + // Load validation rules + $schema = new RequestSchema('schema://requests/register.yaml'); + $validatorRegister = new JqueryValidationAdapter($schema, $this->ci->translator); + + return $this->ci->view->render($response, 'pages/register.html.twig', [ + 'page' => [ + 'validators' => [ + 'register' => $validatorRegister->rules('json', false) + ] + ] + ]); + } + + /** + * Render the "resend verification email" page. + * + * This is a form that allows users who lost their account verification link to have the link resent to their email address. + * By default, this is a "public page" (does not require authentication). + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function pageResendVerification(Request $request, Response $response, $args) + { + // Load validation rules + $schema = new RequestSchema('schema://requests/resend-verification.yaml'); + $validator = new JqueryValidationAdapter($schema, $this->ci->translator); + + return $this->ci->view->render($response, 'pages/resend-verification.html.twig', [ + 'page' => [ + 'validators' => [ + 'resend_verification' => $validator->rules('json', false) + ] + ] + ]); + } + + /** + * Reset password page. + * + * Renders the new password page for password reset requests. + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function pageResetPassword(Request $request, Response $response, $args) + { + // Insert the user's secret token from the link into the password reset form + $params = $request->getQueryParams(); + + // Load validation rules - note this uses the same schema as "set password" + $schema = new RequestSchema('schema://requests/set-password.yaml'); + $validator = new JqueryValidationAdapter($schema, $this->ci->translator); + + return $this->ci->view->render($response, 'pages/reset-password.html.twig', [ + 'page' => [ + 'validators' => [ + 'set_password' => $validator->rules('json', false) + ] + ], + 'token' => isset($params['token']) ? $params['token'] : '', + ]); + } + + /** + * Render the "set password" page. + * + * Renders the page where new users who have had accounts created for them by another user, can set their password. + * By default, this is a "public page" (does not require authentication). + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function pageSetPassword(Request $request, Response $response, $args) + { + // Insert the user's secret token from the link into the password set form + $params = $request->getQueryParams(); + + // Load validation rules + $schema = new RequestSchema('schema://requests/set-password.yaml'); + $validator = new JqueryValidationAdapter($schema, $this->ci->translator); + + return $this->ci->view->render($response, 'pages/set-password.html.twig', [ + 'page' => [ + 'validators' => [ + 'set_password' => $validator->rules('json', false) + ] + ], + 'token' => isset($params['token']) ? $params['token'] : '', + ]); + } + + /** + * Account settings page. + * + * Provides a form for users to modify various properties of their account, such as name, email, locale, etc. + * Any fields that the user does not have permission to modify will be automatically disabled. + * This page requires authentication. + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function pageSettings(Request $request, Response $response, $args) + { + /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ + $authorizer = $this->ci->authorizer; + + /** @var \UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_account_settings')) { + throw new ForbiddenException(); + } + + // Load validation rules + $schema = new RequestSchema('schema://requests/account-settings.yaml'); + $validatorAccountSettings = new JqueryValidationAdapter($schema, $this->ci->translator); + + $schema = new RequestSchema('schema://requests/profile-settings.yaml'); + $validatorProfileSettings = new JqueryValidationAdapter($schema, $this->ci->translator); + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + // Get a list of all locales + $locales = $config->getDefined('site.locales.available'); + + return $this->ci->view->render($response, 'pages/account-settings.html.twig', [ + 'locales' => $locales, + 'page' => [ + 'validators' => [ + 'account_settings' => $validatorAccountSettings->rules('json', false), + 'profile_settings' => $validatorProfileSettings->rules('json', false) + ], + 'visibility' => ($authorizer->checkAccess($currentUser, 'update_account_settings') ? '' : 'disabled') + ] + ]); + } + + /** + * Render the account sign-in page for UserFrosting. + * + * This allows existing users to sign in. + * By definition, this is a "public page" (does not require authentication). + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function pageSignIn(Request $request, Response $response, $args) + { + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ + $authenticator = $this->ci->authenticator; + + // Redirect if user is already logged in + if ($authenticator->check()) { + $redirect = $this->ci->get('redirect.onAlreadyLoggedIn'); + + return $redirect($request, $response, $args); + } + + // Load validation rules + $schema = new RequestSchema('schema://requests/login.yaml'); + $validatorLogin = new JqueryValidationAdapter($schema, $this->ci->translator); + + return $this->ci->view->render($response, 'pages/sign-in.html.twig', [ + 'page' => [ + 'validators' => [ + 'login' => $validatorLogin->rules('json', false) + ] + ] + ]); + } + + /** + * Processes a request to update a user's profile information. + * + * Processes the request from the user profile settings form, checking that: + * 1. They have the necessary permissions to update the posted field(s); + * 2. The submitted data is valid. + * This route requires authentication. + * Request type: POST + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function profile(Request $request, Response $response, $args) + { + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ + $authorizer = $this->ci->authorizer; + + /** @var \UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access control for entire resource - check that the current user has permission to modify themselves + // See recipe "per-field access control" for dynamic fine-grained control over which properties a user can modify. + if (!$authorizer->checkAccess($currentUser, 'update_account_settings')) { + $ms->addMessageTranslated('danger', 'ACCOUNT.ACCESS_DENIED'); + return $response->withStatus(403); + } + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + // POST parameters + $params = $request->getParsedBody(); + + // Load the request schema + $schema = new RequestSchema('schema://requests/profile-settings.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + $error = false; + + // Validate, and halt on validation errors. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + $error = true; + } + + // Check that locale is valid + $locales = $config->getDefined('site.locales.available'); + if (!array_key_exists($data['locale'], $locales)) { + $ms->addMessageTranslated('danger', 'LOCALE.INVALID', $data); + $error = true; + } + + if ($error) { + return $response->withStatus(400); + } + + // Looks good, let's update with new values! + // Note that only fields listed in `profile-settings.yaml` will be permitted in $data, so this prevents the user from updating all columns in the DB + $currentUser->fill($data); + + $currentUser->save(); + + // Create activity record + $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated their profile settings.", [ + 'type' => 'update_profile_settings' + ]); + + $ms->addMessageTranslated('success', 'PROFILE.UPDATED'); + return $response->withStatus(200); + } + + /** + * Processes an new account registration request. + * + * This is throttled to prevent account enumeration, since it needs to divulge when a username/email has been used. + * Processes the request from the form on the registration page, checking that: + * 1. The honeypot was not modified; + * 2. The master account has already been created (during installation); + * 3. Account registration is enabled; + * 4. The user is not already logged in; + * 5. Valid information was entered; + * 6. The captcha, if enabled, is correct; + * 7. The username and email are not already taken. + * Automatically sends an activation link upon success, if account activation is enabled. + * This route is "public access". + * Request type: POST + * Returns the User Object for the user record that was created. + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function register(Request $request, Response $response, $args) + { + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + // Get POST parameters: user_name, first_name, last_name, email, password, passwordc, captcha, spiderbro, csrf_token + $params = $request->getParsedBody(); + + // Check the honeypot. 'spiderbro' is not a real field, it is hidden on the main page and must be submitted with its default value for this to be processed. + if (!isset($params['spiderbro']) || $params['spiderbro'] != 'http://') { + throw new SpammyRequestException('Possible spam received:' . print_r($params, true)); + } + + // Security measure: do not allow registering new users until the master account has been created. + if (!$classMapper->staticMethod('user', 'find', $config['reserved_user_ids.master'])) { + $ms->addMessageTranslated('danger', 'ACCOUNT.MASTER_NOT_EXISTS'); + return $response->withStatus(403); + } + + // Check if registration is currently enabled + if (!$config['site.registration.enabled']) { + $ms->addMessageTranslated('danger', 'REGISTRATION.DISABLED'); + return $response->withStatus(403); + } + + /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ + $authenticator = $this->ci->authenticator; + + // Prevent the user from registering if he/she is already logged in + if ($authenticator->check()) { + $ms->addMessageTranslated('danger', 'REGISTRATION.LOGOUT'); + return $response->withStatus(403); + } + + // Load the request schema + $schema = new RequestSchema('schema://requests/register.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + $error = false; + + // Validate request data + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + $error = true; + } + + /** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */ + $throttler = $this->ci->throttler; + $delay = $throttler->getDelay('registration_attempt'); + + // Throttle requests + if ($delay > 0) { + return $response->withStatus(429); + } + + // Check if username or email already exists + if ($classMapper->staticMethod('user', 'findUnique', $data['user_name'], 'user_name')) { + $ms->addMessageTranslated('danger', 'USERNAME.IN_USE', $data); + $error = true; + } + + if ($classMapper->staticMethod('user', 'findUnique', $data['email'], 'email')) { + $ms->addMessageTranslated('danger', 'EMAIL.IN_USE', $data); + $error = true; + } + + // Check captcha, if required + if ($config['site.registration.captcha']) { + $captcha = new Captcha($this->ci->session, $this->ci->config['session.keys.captcha']); + if (!$data['captcha'] || !$captcha->verifyCode($data['captcha'])) { + $ms->addMessageTranslated('danger', 'CAPTCHA.FAIL'); + $error = true; + } + } + + if ($error) { + return $response->withStatus(400); + } + + // Remove captcha, password confirmation from object data after validation + unset($data['captcha']); + unset($data['passwordc']); + + if ($config['site.registration.require_email_verification']) { + $data['flag_verified'] = false; + } else { + $data['flag_verified'] = true; + } + + // Load default group + $groupSlug = $config['site.registration.user_defaults.group']; + $defaultGroup = $classMapper->staticMethod('group', 'where', 'slug', $groupSlug)->first(); + + if (!$defaultGroup) { + $e = new HttpException("Account registration is not working because the default group '$groupSlug' does not exist."); + $e->addUserMessage('REGISTRATION.BROKEN'); + throw $e; + } + + // Set default group + $data['group_id'] = $defaultGroup->id; + + // Set default locale + $data['locale'] = $config['site.registration.user_defaults.locale']; + + // Hash password + $data['password'] = Password::hash($data['password']); + + // All checks passed! log events/activities, create user, and send verification email (if required) + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction( function() use ($classMapper, $data, $ms, $config, $throttler) { + // Log throttleable event + $throttler->logEvent('registration_attempt'); + + // Create the user + $user = $classMapper->createInstance('user', $data); + + // Store new user to database + $user->save(); + + // Create activity record + $this->ci->userActivityLogger->info("User {$user->user_name} registered for a new account.", [ + 'type' => 'sign_up', + 'user_id' => $user->id + ]); + + // Load default roles + $defaultRoleSlugs = $classMapper->staticMethod('role', 'getDefaultSlugs'); + $defaultRoles = $classMapper->staticMethod('role', 'whereIn', 'slug', $defaultRoleSlugs)->get(); + $defaultRoleIds = $defaultRoles->pluck('id')->all(); + + // Attach default roles + $user->roles()->attach($defaultRoleIds); + + // Verification email + if ($config['site.registration.require_email_verification']) { + // Try to generate a new verification request + $verification = $this->ci->repoVerification->create($user, $config['verification.timeout']); + + // Create and send verification email + $message = new TwigMailMessage($this->ci->view, 'mail/verify-account.html.twig'); + + $message->from($config['address_book.admin']) + ->addEmailRecipient(new EmailRecipient($user->email, $user->full_name)) + ->addParams([ + 'user' => $user, + 'token' => $verification->getToken() + ]); + + $this->ci->mailer->send($message); + + $ms->addMessageTranslated('success', 'REGISTRATION.COMPLETE_TYPE2', $user->toArray()); + } else { + // No verification required + $ms->addMessageTranslated('success', 'REGISTRATION.COMPLETE_TYPE1'); + } + }); + + return $response->withStatus(200); + } + + /** + * Processes a request to resend the verification email for a new user account. + * + * Processes the request from the resend verification email form, checking that: + * 1. The rate limit on this type of request is observed; + * 2. The provided email is associated with an existing user account; + * 3. The user account is not already verified; + * 4. The submitted data is valid. + * This route is "public access". + * Request type: POST + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function resendVerification(Request $request, Response $response, $args) + { + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + // Get POST parameters + $params = $request->getParsedBody(); + + // Load the request schema + $schema = new RequestSchema('schema://requests/resend-verification.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and halt on validation errors. Failed validation attempts do not count towards throttling limit. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + return $response->withStatus(400); + } + + // Throttle requests + + /** @var \UserFrosting\Sprinkle\Core\Throttle\Throttler $throttler */ + $throttler = $this->ci->throttler; + + $throttleData = [ + 'email' => $data['email'] + ]; + $delay = $throttler->getDelay('verification_request', $throttleData); + + if ($delay > 0) { + $ms->addMessageTranslated('danger', 'RATE_LIMIT_EXCEEDED', ['delay' => $delay]); + return $response->withStatus(429); + } + + // All checks passed! log events/activities, create user, and send verification email (if required) + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction( function() use ($classMapper, $data, $throttler, $throttleData, $config) { + // Log throttleable event + $throttler->logEvent('verification_request', $throttleData); + + // Load the user, by email address + $user = $classMapper->staticMethod('user', 'where', 'email', $data['email'])->first(); + + // Check that the user exists and is not already verified. + // If there is no user with that email address, or the user exists and is already verified, + // we pretend like we succeeded to prevent account enumeration + if ($user && $user->flag_verified != '1') { + // We're good to go - record user activity and send the email + $verification = $this->ci->repoVerification->create($user, $config['verification.timeout']); + + // Create and send verification email + $message = new TwigMailMessage($this->ci->view, 'mail/resend-verification.html.twig'); + + $message->from($config['address_book.admin']) + ->addEmailRecipient(new EmailRecipient($user->email, $user->full_name)) + ->addParams([ + 'user' => $user, + 'token' => $verification->getToken() + ]); + + $this->ci->mailer->send($message); + } + }); + + $ms->addMessageTranslated('success', 'ACCOUNT.VERIFICATION.NEW_LINK_SENT', ['email' => $data['email']]); + return $response->withStatus(200); + } + + /** + * Processes a request to set the password for a new or current user. + * + * Processes the request from the password create/reset form, which should have the secret token embedded in it, checking that: + * 1. The provided secret token is associated with an existing user account; + * 2. The user has a password set/reset request in progress; + * 3. The token has not expired; + * 4. The submitted data (new password) is valid. + * This route is "public access". + * Request type: POST + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function setPassword(Request $request, Response $response, $args) + { + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + // Get POST parameters + $params = $request->getParsedBody(); + + // Load the request schema + $schema = new RequestSchema('schema://requests/set-password.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and halt on validation errors. Failed validation attempts do not count towards throttling limit. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + return $response->withStatus(400); + } + + $forgotPasswordPage = $this->ci->router->pathFor('forgot-password'); + + // Ok, try to complete the request with the specified token and new password + $passwordReset = $this->ci->repoPasswordReset->complete($data['token'], [ + 'password' => $data['password'] + ]); + + if (!$passwordReset) { + $ms->addMessageTranslated('danger', 'PASSWORD.FORGET.INVALID', ['url' => $forgotPasswordPage]); + return $response->withStatus(400); + } + + $ms->addMessageTranslated('success', 'PASSWORD.UPDATED'); + + /** @var \UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ + $authenticator = $this->ci->authenticator; + + // Log out any existing user, and create a new session + if ($authenticator->check()) { + $authenticator->logout(); + } + + // Auto-login the user (without "remember me") + $user = $passwordReset->user; + $authenticator->login($user); + + $ms->addMessageTranslated('success', 'WELCOME', $user->export()); + return $response->withStatus(200); + } + + /** + * Processes a request to update a user's account information. + * + * Processes the request from the user account settings form, checking that: + * 1. The user correctly input their current password; + * 2. They have the necessary permissions to update the posted field(s); + * 3. The submitted data is valid. + * This route requires authentication. + * Request type: POST + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function settings(Request $request, Response $response, $args) + { + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ + $authorizer = $this->ci->authorizer; + + /** @var \UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access control for entire resource - check that the current user has permission to modify themselves + // See recipe "per-field access control" for dynamic fine-grained control over which properties a user can modify. + if (!$authorizer->checkAccess($currentUser, 'update_account_settings')) { + $ms->addMessageTranslated('danger', 'ACCOUNT.ACCESS_DENIED'); + return $response->withStatus(403); + } + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + // POST parameters + $params = $request->getParsedBody(); + + // Load the request schema + $schema = new RequestSchema('schema://requests/account-settings.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + $error = false; + + // Validate, and halt on validation errors. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + $error = true; + } + + // Confirm current password + if (!isset($data['passwordcheck']) || !Password::verify($data['passwordcheck'], $currentUser->password)) { + $ms->addMessageTranslated('danger', 'PASSWORD.INVALID'); + $error = true; + } + + // Remove password check, password confirmation from object data after validation + unset($data['passwordcheck']); + unset($data['passwordc']); + + // If new email was submitted, check that the email address is not in use + if (isset($data['email']) && $data['email'] != $currentUser->email && $classMapper->staticMethod('user', 'findUnique', $data['email'], 'email')) { + $ms->addMessageTranslated('danger', 'EMAIL.IN_USE', $data); + $error = true; + } + + if ($error) { + return $response->withStatus(400); + } + + // Hash new password, if specified + if (isset($data['password']) && !empty($data['password'])) { + $data['password'] = Password::hash($data['password']); + } else { + // Do not pass to model if no password is specified + unset($data['password']); + } + + // Looks good, let's update with new values! + // Note that only fields listed in `account-settings.yaml` will be permitted in $data, so this prevents the user from updating all columns in the DB + $currentUser->fill($data); + + $currentUser->save(); + + // Create activity record + $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated their account settings.", [ + 'type' => 'update_account_settings' + ]); + + $ms->addMessageTranslated('success', 'ACCOUNT.SETTINGS.UPDATED'); + return $response->withStatus(200); + } + + /** + * Suggest an available username for a specified first/last name. + * + * This route is "public access". + * Request type: GET + * @todo Can this route be abused for account enumeration? If so we should throttle it as well. + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function suggestUsername(Request $request, Response $response, $args) + { + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + $suggestion = AccountUtil::randomUniqueUsername($classMapper, 50, 10); + + // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. + // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). + return $response->withJson([ + 'user_name' => $suggestion + ], 200, JSON_PRETTY_PRINT); + } + + /** + * Processes an new email verification request. + * + * Processes the request from the email verification link that was emailed to the user, checking that: + * 1. The token provided matches a user in the database; + * 2. The user account is not already verified; + * This route is "public access". + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * @return void + */ + public function verify(Request $request, Response $response, $args) + { + /** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */ + $ms = $this->ci->alerts; + + /** @var \UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var \UserFrosting\Support\Repository\Repository $config */ + $config = $this->ci->config; + + $loginPage = $this->ci->router->pathFor('login'); + + // GET parameters + $params = $request->getQueryParams(); + + // Load request schema + $schema = new RequestSchema('schema://requests/account-verify.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and halt on validation errors. This is a GET request, so we redirect on validation error. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + // 400 code + redirect is perfectly fine, according to user Dilaz in #laravel + return $response->withRedirect($loginPage, 400); + } + + $verification = $this->ci->repoVerification->complete($data['token']); + + if (!$verification) { + $ms->addMessageTranslated('danger', 'ACCOUNT.VERIFICATION.TOKEN_NOT_FOUND'); + return $response->withRedirect($loginPage, 400); + } + + $ms->addMessageTranslated('success', 'ACCOUNT.VERIFICATION.COMPLETE'); + + // Forward to login page + return $response->withRedirect($loginPage); + } +} diff --git a/login/app/sprinkles/account/src/Controller/Exception/SpammyRequestException.php b/login/app/sprinkles/account/src/Controller/Exception/SpammyRequestException.php new file mode 100755 index 0000000..9713360 --- /dev/null +++ b/login/app/sprinkles/account/src/Controller/Exception/SpammyRequestException.php @@ -0,0 +1,20 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Controller\Exception; + +use UserFrosting\Support\Exception\HttpException; + +/** + * Spammy request exception. Used when a bot has attempted to spam a public form, and fallen into our honeypot. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class SpammyRequestException extends HttpException +{ + +} diff --git a/login/app/sprinkles/account/src/Database/Migrations/v400/ActivitiesTable.php b/login/app/sprinkles/account/src/Database/Migrations/v400/ActivitiesTable.php new file mode 100755 index 0000000..4e55c7c --- /dev/null +++ b/login/app/sprinkles/account/src/Database/Migrations/v400/ActivitiesTable.php @@ -0,0 +1,54 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Database\Migrations\v400; + +use UserFrosting\System\Bakery\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Schema\Builder; + +/** + * Sessions table migration + * Version 4.0.0 + * + * See https://laravel.com/docs/5.4/migrations#tables + * @extends Migration + * @author Alex Weissman (https://alexanderweissman.com) + */ +class ActivitiesTable extends Migration +{ + /** + * {@inheritDoc} + */ + public function up() + { + if (!$this->schema->hasTable('activities')) { + $this->schema->create('activities', function (Blueprint $table) { + $table->increments('id'); + $table->string('ip_address', 45)->nullable(); + $table->integer('user_id')->unsigned(); + $table->string('type', 255)->comment('An identifier used to track the type of activity.'); + $table->timestamp('occurred_at')->nullable(); + $table->text('description')->nullable(); + + $table->engine = 'InnoDB'; + $table->collation = 'utf8_unicode_ci'; + $table->charset = 'utf8'; + //$table->foreign('user_id')->references('id')->on('users'); + $table->index('user_id'); + }); + } + } + + /** + * {@inheritDoc} + */ + public function down() + { + $this->schema->drop('activities'); + } +}
\ No newline at end of file diff --git a/login/app/sprinkles/account/src/Database/Migrations/v400/GroupsTable.php b/login/app/sprinkles/account/src/Database/Migrations/v400/GroupsTable.php new file mode 100755 index 0000000..c74615f --- /dev/null +++ b/login/app/sprinkles/account/src/Database/Migrations/v400/GroupsTable.php @@ -0,0 +1,82 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Database\Migrations\v400; + +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Schema\Builder; +use UserFrosting\Sprinkle\Account\Database\Models\Group; +use UserFrosting\System\Bakery\Migration; + +/** + * Groups table migration + * "Group" now replaces the notion of "primary group" in earlier versions of UF. A user can belong to exactly one group. + * Version 4.0.0 + * + * See https://laravel.com/docs/5.4/migrations#tables + * @extends Migration + * @author Alex Weissman (https://alexanderweissman.com) + */ +class GroupsTable extends Migration +{ + /** + * {@inheritDoc} + */ + public function up() + { + if (!$this->schema->hasTable('groups')) { + $this->schema->create('groups', function(Blueprint $table) { + $table->increments('id'); + $table->string('slug'); + $table->string('name'); + $table->text('description')->nullable(); + $table->string('icon', 100)->nullable(false)->default('fa fa-user')->comment('The icon representing users in this group.'); + $table->timestamps(); + + $table->engine = 'InnoDB'; + $table->collation = 'utf8_unicode_ci'; + $table->charset = 'utf8'; + $table->unique('slug'); + $table->index('slug'); + }); + + // Add default groups + $groups = [ + 'terran' => new Group([ + 'slug' => 'terran', + 'name' => 'Terran', + 'description' => 'The terrans are a young species with psionic potential. The terrans of the Koprulu sector descend from the survivors of a disastrous 23rd century colonization mission from Earth.', + 'icon' => 'sc sc-terran' + ]), + 'zerg' => new Group([ + 'slug' => 'zerg', + 'name' => 'Zerg', + 'description' => 'Dedicated to the pursuit of genetic perfection, the zerg relentlessly hunt down and assimilate advanced species across the galaxy, incorporating useful genetic code into their own.', + 'icon' => 'sc sc-zerg' + ]), + 'protoss' => new Group([ + 'slug' => 'protoss', + 'name' => 'Protoss', + 'description' => 'The protoss, a.k.a. the Firstborn, are a sapient humanoid race native to Aiur. Their advanced technology complements and enhances their psionic mastery.', + 'icon' => 'sc sc-protoss' + ]) + ]; + + foreach ($groups as $slug => $group) { + $group->save(); + } + } + } + + /** + * {@inheritDoc} + */ + public function down() + { + $this->schema->drop('groups'); + } +} diff --git a/login/app/sprinkles/account/src/Database/Migrations/v400/PasswordResetsTable.php b/login/app/sprinkles/account/src/Database/Migrations/v400/PasswordResetsTable.php new file mode 100755 index 0000000..e785ccc --- /dev/null +++ b/login/app/sprinkles/account/src/Database/Migrations/v400/PasswordResetsTable.php @@ -0,0 +1,57 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Database\Migrations\v400; + +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Schema\Builder; +use UserFrosting\System\Bakery\Migration; + +/** + * password_resets table migration + * Manages requests for password resets. + * Version 4.0.0 + * + * See https://laravel.com/docs/5.4/migrations#tables + * @extends Migration + * @author Alex Weissman (https://alexanderweissman.com) + */ +class passwordResetsTable extends Migration +{ + /** + * {@inheritDoc} + */ + public function up() + { + if (!$this->schema->hasTable('password_resets')) { + $this->schema->create('password_resets', function (Blueprint $table) { + $table->increments('id'); + $table->integer('user_id')->unsigned(); + $table->string('hash'); + $table->boolean('completed')->default(0); + $table->timestamp('expires_at')->nullable(); + $table->timestamp('completed_at')->nullable(); + $table->timestamps(); + + $table->engine = 'InnoDB'; + $table->collation = 'utf8_unicode_ci'; + $table->charset = 'utf8'; + //$table->foreign('user_id')->references('id')->on('users'); + $table->index('user_id'); + $table->index('hash'); + }); + } + } + + /** + * {@inheritDoc} + */ + public function down() + { + $this->schema->drop('password_resets'); + } +} diff --git a/login/app/sprinkles/account/src/Database/Migrations/v400/PermissionRolesTable.php b/login/app/sprinkles/account/src/Database/Migrations/v400/PermissionRolesTable.php new file mode 100755 index 0000000..2c2990c --- /dev/null +++ b/login/app/sprinkles/account/src/Database/Migrations/v400/PermissionRolesTable.php @@ -0,0 +1,55 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Database\Migrations\v400; + +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Schema\Builder; +use UserFrosting\System\Bakery\Migration; + +/** + * Permission_roles table migration + * Many-to-many mapping between permissions and roles. + * Version 4.0.0 + * + * See https://laravel.com/docs/5.4/migrations#tables + * @extends Migration + * @author Alex Weissman (https://alexanderweissman.com) + */ +class PermissionRolesTable extends Migration +{ + /** + * {@inheritDoc} + */ + public function up() + { + if (!$this->schema->hasTable('permission_roles')) { + $this->schema->create('permission_roles', function (Blueprint $table) { + $table->integer('permission_id')->unsigned(); + $table->integer('role_id')->unsigned(); + $table->timestamps(); + + $table->engine = 'InnoDB'; + $table->collation = 'utf8_unicode_ci'; + $table->charset = 'utf8'; + $table->primary(['permission_id', 'role_id']); + //$table->foreign('permission_id')->references('id')->on('permissions'); + //$table->foreign('role_id')->references('id')->on('roles'); + $table->index('permission_id'); + $table->index('role_id'); + }); + } + } + + /** + * {@inheritDoc} + */ + public function down() + { + $this->schema->drop('permission_roles'); + } +} diff --git a/login/app/sprinkles/account/src/Database/Migrations/v400/PermissionsTable.php b/login/app/sprinkles/account/src/Database/Migrations/v400/PermissionsTable.php new file mode 100755 index 0000000..684b01a --- /dev/null +++ b/login/app/sprinkles/account/src/Database/Migrations/v400/PermissionsTable.php @@ -0,0 +1,262 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Database\Migrations\v400; + +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Schema\Builder; +use UserFrosting\Sprinkle\Account\Database\Models\Permission; +use UserFrosting\Sprinkle\Account\Database\Models\Role; +use UserFrosting\System\Bakery\Migration; + +/** + * Permissions table migration + * Permissions now replace the 'authorize_group' and 'authorize_user' tables. + * Also, they now map many-to-many to roles. + * Version 4.0.0 + * + * See https://laravel.com/docs/5.4/migrations#tables + * @extends Migration + * @author Alex Weissman (https://alexanderweissman.com) + */ +class PermissionsTable extends Migration +{ + /** + * {@inheritDoc} + */ + public $dependencies = [ + '\UserFrosting\Sprinkle\Account\Database\Migrations\v400\RolesTable', + '\UserFrosting\Sprinkle\Account\Database\Migrations\v400\PermissionRolesTable' + ]; + + /** + * {@inheritDoc} + */ + public function up() + { + if (!$this->schema->hasTable('permissions')) { + $this->schema->create('permissions', function(Blueprint $table) { + $table->increments('id'); + $table->string('slug')->comment('A code that references a specific action or URI that an assignee of this permission has access to.'); + $table->string('name'); + $table->text('conditions')->comment('The conditions under which members of this group have access to this hook.'); + $table->text('description')->nullable(); + $table->timestamps(); + + $table->engine = 'InnoDB'; + $table->collation = 'utf8_unicode_ci'; + $table->charset = 'utf8'; + }); + } + } + + /** + * {@inheritDoc} + */ + public function down() + { + $this->schema->drop('permissions'); + } + + /** + * {@inheritDoc} + */ + public function seed() + { + // Skip this if table is not empty + if (Permission::count() == 0) { + + $defaultRoleIds = [ + 'user' => Role::where('slug', 'user')->first()->id, + 'group-admin' => Role::where('slug', 'group-admin')->first()->id, + 'site-admin' => Role::where('slug', 'site-admin')->first()->id + ]; + + // Add default permissions + $permissions = [ + 'create_group' => new Permission([ + 'slug' => 'create_group', + 'name' => 'Create group', + 'conditions' => 'always()', + 'description' => 'Create a new group.' + ]), + 'create_user' => new Permission([ + 'slug' => 'create_user', + 'name' => 'Create user', + 'conditions' => 'always()', + 'description' => 'Create a new user in your own group and assign default roles.' + ]), + 'create_user_field' => new Permission([ + 'slug' => 'create_user_field', + 'name' => 'Set new user group', + 'conditions' => "subset(fields,['group'])", + 'description' => 'Set the group when creating a new user.' + ]), + 'delete_group' => new Permission([ + 'slug' => 'delete_group', + 'name' => 'Delete group', + 'conditions' => "always()", + 'description' => 'Delete a group.' + ]), + 'delete_user' => new Permission([ + 'slug' => 'delete_user', + 'name' => 'Delete user', + 'conditions' => "!has_role(user.id,{$defaultRoleIds['site-admin']}) && !is_master(user.id)", + 'description' => 'Delete users who are not Site Administrators.' + ]), + 'update_account_settings' => new Permission([ + 'slug' => 'update_account_settings', + 'name' => 'Edit user', + 'conditions' => 'always()', + 'description' => 'Edit your own account settings.' + ]), + 'update_group_field' => new Permission([ + 'slug' => 'update_group_field', + 'name' => 'Edit group', + 'conditions' => 'always()', + 'description' => 'Edit basic properties of any group.' + ]), + 'update_user_field' => new Permission([ + 'slug' => 'update_user_field', + 'name' => 'Edit user', + 'conditions' => "!has_role(user.id,{$defaultRoleIds['site-admin']}) && subset(fields,['name','email','locale','group','flag_enabled','flag_verified','password'])", + 'description' => 'Edit users who are not Site Administrators.' + ]), + 'update_user_field_group' => new Permission([ + 'slug' => 'update_user_field', + 'name' => 'Edit group user', + 'conditions' => "equals_num(self.group_id,user.group_id) && !is_master(user.id) && !has_role(user.id,{$defaultRoleIds['site-admin']}) && (!has_role(user.id,{$defaultRoleIds['group-admin']}) || equals_num(self.id,user.id)) && subset(fields,['name','email','locale','flag_enabled','flag_verified','password'])", + 'description' => 'Edit users in your own group who are not Site or Group Administrators, except yourself.' + ]), + 'uri_account_settings' => new Permission([ + 'slug' => 'uri_account_settings', + 'name' => 'Account settings page', + 'conditions' => 'always()', + 'description' => 'View the account settings page.' + ]), + 'uri_activities' => new Permission([ + 'slug' => 'uri_activities', + 'name' => 'Activity monitor', + 'conditions' => 'always()', + 'description' => 'View a list of all activities for all users.' + ]), + 'uri_dashboard' => new Permission([ + 'slug' => 'uri_dashboard', + 'name' => 'Admin dashboard', + 'conditions' => 'always()', + 'description' => 'View the administrative dashboard.' + ]), + 'uri_group' => new Permission([ + 'slug' => 'uri_group', + 'name' => 'View group', + 'conditions' => 'always()', + 'description' => 'View the group page of any group.' + ]), + 'uri_group_own' => new Permission([ + 'slug' => 'uri_group', + 'name' => 'View own group', + 'conditions' => 'equals_num(self.group_id,group.id)', + 'description' => 'View the group page of your own group.' + ]), + 'uri_groups' => new Permission([ + 'slug' => 'uri_groups', + 'name' => 'Group management page', + 'conditions' => 'always()', + 'description' => 'View a page containing a list of groups.' + ]), + 'uri_user' => new Permission([ + 'slug' => 'uri_user', + 'name' => 'View user', + 'conditions' => 'always()', + 'description' => 'View the user page of any user.' + ]), + 'uri_user_in_group' => new Permission([ + 'slug' => 'uri_user', + 'name' => 'View user', + 'conditions' => "equals_num(self.group_id,user.group_id) && !is_master(user.id) && !has_role(user.id,{$defaultRoleIds['site-admin']}) && (!has_role(user.id,{$defaultRoleIds['group-admin']}) || equals_num(self.id,user.id))", + 'description' => 'View the user page of any user in your group, except the master user and Site and Group Administrators (except yourself).' + ]), + 'uri_users' => new Permission([ + 'slug' => 'uri_users', + 'name' => 'User management page', + 'conditions' => 'always()', + 'description' => 'View a page containing a table of users.' + ]), + 'view_group_field' => new Permission([ + 'slug' => 'view_group_field', + 'name' => 'View group', + 'conditions' => "in(property,['name','icon','slug','description','users'])", + 'description' => 'View certain properties of any group.' + ]), + 'view_group_field_own' => new Permission([ + 'slug' => 'view_group_field', + 'name' => 'View group', + 'conditions' => "equals_num(self.group_id,group.id) && in(property,['name','icon','slug','description','users'])", + 'description' => 'View certain properties of your own group.' + ]), + 'view_user_field' => new Permission([ + 'slug' => 'view_user_field', + 'name' => 'View user', + 'conditions' => "in(property,['user_name','name','email','locale','theme','roles','group','activities'])", + 'description' => 'View certain properties of any user.' + ]), + 'view_user_field_group' => new Permission([ + 'slug' => 'view_user_field', + 'name' => 'View user', + 'conditions' => "equals_num(self.group_id,user.group_id) && !is_master(user.id) && !has_role(user.id,{$defaultRoleIds['site-admin']}) && (!has_role(user.id,{$defaultRoleIds['group-admin']}) || equals_num(self.id,user.id)) && in(property,['user_name','name','email','locale','roles','group','activities'])", + 'description' => 'View certain properties of any user in your own group, except the master user and Site and Group Administrators (except yourself).' + ]) + ]; + + foreach ($permissions as $slug => $permission) { + $permission->save(); + } + + // Add default mappings to permissions + $roleUser = Role::where('slug', 'user')->first(); + if ($roleUser) { + $roleUser->permissions()->sync([ + $permissions['update_account_settings']->id, + $permissions['uri_account_settings']->id, + $permissions['uri_dashboard']->id + ]); + } + + $roleSiteAdmin = Role::where('slug', 'site-admin')->first(); + if ($roleSiteAdmin) { + $roleSiteAdmin->permissions()->sync([ + $permissions['create_group']->id, + $permissions['create_user']->id, + $permissions['create_user_field']->id, + $permissions['delete_group']->id, + $permissions['delete_user']->id, + $permissions['update_user_field']->id, + $permissions['update_group_field']->id, + $permissions['uri_activities']->id, + $permissions['uri_group']->id, + $permissions['uri_groups']->id, + $permissions['uri_user']->id, + $permissions['uri_users']->id, + $permissions['view_group_field']->id, + $permissions['view_user_field']->id + ]); + } + + $roleGroupAdmin = Role::where('slug', 'group-admin')->first(); + if ($roleGroupAdmin) { + $roleGroupAdmin->permissions()->sync([ + $permissions['create_user']->id, + $permissions['update_user_field_group']->id, + $permissions['uri_group_own']->id, + $permissions['uri_user_in_group']->id, + $permissions['view_group_field_own']->id, + $permissions['view_user_field_group']->id + ]); + } + } + } +} diff --git a/login/app/sprinkles/account/src/Database/Migrations/v400/PersistencesTable.php b/login/app/sprinkles/account/src/Database/Migrations/v400/PersistencesTable.php new file mode 100755 index 0000000..b96e327 --- /dev/null +++ b/login/app/sprinkles/account/src/Database/Migrations/v400/PersistencesTable.php @@ -0,0 +1,57 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Database\Migrations\v400; + +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Schema\Builder; +use UserFrosting\System\Bakery\Migration; + +/** + * Persistences table migration + * Many-to-many mapping between roles and users. + * Version 4.0.0 + * + * See https://laravel.com/docs/5.4/migrations#tables + * @extends Migration + * @author Alex Weissman (https://alexanderweissman.com) + */ +class PersistencesTable extends Migration +{ + /** + * {@inheritDoc} + */ + public function up() + { + if (!$this->schema->hasTable('persistences')) { + $this->schema->create('persistences', function (Blueprint $table) { + $table->increments('id'); + $table->integer('user_id')->unsigned(); + $table->string('token', 40); + $table->string('persistent_token', 40); + $table->timestamp('expires_at')->nullable(); + $table->timestamps(); + + $table->engine = 'InnoDB'; + $table->collation = 'utf8_unicode_ci'; + $table->charset = 'utf8'; + //$table->foreign('user_id')->references('id')->on('users'); + $table->index('user_id'); + $table->index('token'); + $table->index('persistent_token'); + }); + } + } + + /** + * {@inheritDoc} + */ + public function down() + { + $this->schema->drop('persistences'); + } +} diff --git a/login/app/sprinkles/account/src/Database/Migrations/v400/RoleUsersTable.php b/login/app/sprinkles/account/src/Database/Migrations/v400/RoleUsersTable.php new file mode 100755 index 0000000..7f3648b --- /dev/null +++ b/login/app/sprinkles/account/src/Database/Migrations/v400/RoleUsersTable.php @@ -0,0 +1,55 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Database\Migrations\v400; + +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Schema\Builder; +use UserFrosting\System\Bakery\Migration; + +/** + * Role_users table migration + * Many-to-many mapping between roles and users. + * Version 4.0.0 + * + * See https://laravel.com/docs/5.4/migrations#tables + * @extends Migration + * @author Alex Weissman (https://alexanderweissman.com) + */ +class RoleUsersTable extends Migration +{ + /** + * {@inheritDoc} + */ + public function up() + { + if (!$this->schema->hasTable('role_users')) { + $this->schema->create('role_users', function (Blueprint $table) { + $table->integer('user_id')->unsigned(); + $table->integer('role_id')->unsigned(); + $table->timestamps(); + + $table->engine = 'InnoDB'; + $table->collation = 'utf8_unicode_ci'; + $table->charset = 'utf8'; + $table->primary(['user_id', 'role_id']); + //$table->foreign('user_id')->references('id')->on('users'); + //$table->foreign('role_id')->references('id')->on('roles'); + $table->index('user_id'); + $table->index('role_id'); + }); + } + } + + /** + * {@inheritDoc} + */ + public function down() + { + $this->schema->drop('role_users'); + } +} diff --git a/login/app/sprinkles/account/src/Database/Migrations/v400/RolesTable.php b/login/app/sprinkles/account/src/Database/Migrations/v400/RolesTable.php new file mode 100755 index 0000000..9cef494 --- /dev/null +++ b/login/app/sprinkles/account/src/Database/Migrations/v400/RolesTable.php @@ -0,0 +1,78 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Database\Migrations\v400; + +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Schema\Builder; +use UserFrosting\Sprinkle\Account\Database\Models\Role; +use UserFrosting\System\Bakery\Migration; + +/** + * Roles table migration + * Roles replace "groups" in UF 0.3.x. Users acquire permissions through roles. + * Version 4.0.0 + * + * See https://laravel.com/docs/5.4/migrations#tables + * @extends Migration + * @author Alex Weissman (https://alexanderweissman.com) + */ +class RolesTable extends Migration +{ + /** + * {@inheritDoc} + */ + public function up() + { + if (!$this->schema->hasTable('roles')) { + $this->schema->create('roles', function (Blueprint $table) { + $table->increments('id'); + $table->string('slug'); + $table->string('name'); + $table->text('description')->nullable(); + $table->timestamps(); + + $table->engine = 'InnoDB'; + $table->collation = 'utf8_unicode_ci'; + $table->charset = 'utf8'; + $table->unique('slug'); + $table->index('slug'); + }); + + // Add default roles + $roles = [ + 'user' => new Role([ + 'slug' => 'user', + 'name' => 'User', + 'description' => 'This role provides basic user functionality.' + ]), + 'site-admin' => new Role([ + 'slug' => 'site-admin', + 'name' => 'Site Administrator', + 'description' => 'This role is meant for "site administrators", who can basically do anything except create, edit, or delete other administrators.' + ]), + 'group-admin' => new Role([ + 'slug' => 'group-admin', + 'name' => 'Group Administrator', + 'description' => 'This role is meant for "group administrators", who can basically do anything with users in their own group, except other administrators of that group.' + ]) + ]; + + foreach ($roles as $slug => $role) { + $role->save(); + } + } + } + + /** + * {@inheritDoc} + */ + public function down() + { + $this->schema->drop('roles'); + } +} diff --git a/login/app/sprinkles/account/src/Database/Migrations/v400/UsersTable.php b/login/app/sprinkles/account/src/Database/Migrations/v400/UsersTable.php new file mode 100755 index 0000000..a65eeed --- /dev/null +++ b/login/app/sprinkles/account/src/Database/Migrations/v400/UsersTable.php @@ -0,0 +1,69 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Database\Migrations\v400; + +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Schema\Builder; +use UserFrosting\System\Bakery\Migration; + +/** + * Users table migration + * Removed the 'display_name', 'title', 'secret_token', and 'flag_password_reset' fields, and added first and last name and 'last_activity_id'. + * Version 4.0.0 + * + * See https://laravel.com/docs/5.4/migrations#tables + * @extends Migration + * @author Alex Weissman (https://alexanderweissman.com) + */ +class UsersTable extends Migration +{ + /** + * {@inheritDoc} + */ + public function up() + { + if (!$this->schema->hasTable('users')) { + $this->schema->create('users', function (Blueprint $table) { + $table->increments('id'); + $table->string('user_name', 50); + $table->string('email', 254); + $table->string('first_name', 20); + $table->string('last_name', 30); + $table->string('locale', 10)->default('en_US')->comment('The language and locale to use for this user.'); + $table->string('theme', 100)->nullable()->comment("The user theme."); + $table->integer('group_id')->unsigned()->default(1)->comment("The id of the user group."); + $table->boolean('flag_verified')->default(1)->comment("Set to 1 if the user has verified their account via email, 0 otherwise."); + $table->boolean('flag_enabled')->default(1)->comment("Set to 1 if the user account is currently enabled, 0 otherwise. Disabled accounts cannot be logged in to, but they retain all of their data and settings."); + $table->integer('last_activity_id')->unsigned()->nullable()->comment("The id of the last activity performed by this user."); + $table->string('password', 255); + $table->softDeletes(); + $table->timestamps(); + + $table->engine = 'InnoDB'; + $table->collation = 'utf8_unicode_ci'; + $table->charset = 'utf8'; + //$table->foreign('group_id')->references('id')->on('groups'); + //$table->foreign('last_activity_id')->references('id')->on('activities'); + $table->unique('user_name'); + $table->index('user_name'); + $table->unique('email'); + $table->index('email'); + $table->index('group_id'); + $table->index('last_activity_id'); + }); + } + } + + /** + * {@inheritDoc} + */ + public function down() + { + $this->schema->drop('users'); + } +} diff --git a/login/app/sprinkles/account/src/Database/Migrations/v400/VerificationsTable.php b/login/app/sprinkles/account/src/Database/Migrations/v400/VerificationsTable.php new file mode 100755 index 0000000..fa54da6 --- /dev/null +++ b/login/app/sprinkles/account/src/Database/Migrations/v400/VerificationsTable.php @@ -0,0 +1,57 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Database\Migrations\v400; + +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Schema\Builder; +use UserFrosting\System\Bakery\Migration; + +/** + * Verifications table migration + * Manages requests for email account verification. + * Version 4.0.0 + * + * See https://laravel.com/docs/5.4/migrations#tables + * @extends Migration + * @author Alex Weissman (https://alexanderweissman.com) + */ +class VerificationsTable extends Migration +{ + /** + * {@inheritDoc} + */ + public function up() + { + if (!$this->schema->hasTable('verifications')) { + $this->schema->create('verifications', function (Blueprint $table) { + $table->increments('id'); + $table->integer('user_id')->unsigned(); + $table->string('hash'); + $table->boolean('completed')->default(0); + $table->timestamp('expires_at')->nullable(); + $table->timestamp('completed_at')->nullable(); + $table->timestamps(); + + $table->engine = 'InnoDB'; + $table->collation = 'utf8_unicode_ci'; + $table->charset = 'utf8'; + //$table->foreign('user_id')->references('id')->on('users'); + $table->index('user_id'); + $table->index('hash'); + }); + } + } + + /** + * {@inheritDoc} + */ + public function down() + { + $this->schema->drop('verifications'); + } +} diff --git a/login/app/sprinkles/account/src/Database/Models/Activity.php b/login/app/sprinkles/account/src/Database/Models/Activity.php new file mode 100755 index 0000000..d5be589 --- /dev/null +++ b/login/app/sprinkles/account/src/Database/Models/Activity.php @@ -0,0 +1,86 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Database\Models; + +use Illuminate\Database\Capsule\Manager as Capsule; +use UserFrosting\Sprinkle\Core\Database\Models\Model; + +/** + * Activity Class + * + * Represents a single user activity at a specified point in time. + * @author Alex Weissman (https://alexanderweissman.com) + * @property string ip_address + * @property int user_id + * @property string type + * @property datetime occurred_at + * @property string description + */ +class Activity extends Model +{ + /** + * @var string The name of the table for the current model. + */ + protected $table = "activities"; + + protected $fillable = [ + "ip_address", + "user_id", + "type", + "occurred_at", + "description" + ]; + + /** + * Joins the activity's user, so we can do things like sort, search, paginate, etc. + */ + public function scopeJoinUser($query) + { + $query = $query->select('activities.*'); + + $query = $query->leftJoin('users', 'activities.user_id', '=', 'users.id'); + + return $query; + } + + /** + * Add clauses to select the most recent event of each type for each user, to the query. + * + * @return \Illuminate\Database\Query\Builder + */ + public function scopeMostRecentEvents($query) + { + return $query->select('user_id', 'event_type', Capsule::raw('MAX(occurred_at) as occurred_at')) + ->groupBy('user_id') + ->groupBy('type'); + } + + /** + * Add clauses to select the most recent event of a given type for each user, to the query. + * + * @param string $type The type of event, matching the `event_type` field in the user_event table. + * @return \Illuminate\Database\Query\Builder + */ + public function scopeMostRecentEventsByType($query, $type) + { + return $query->select('user_id', Capsule::raw('MAX(occurred_at) as occurred_at')) + ->where('type', $type) + ->groupBy('user_id'); + } + + /** + * Get the user associated with this activity. + */ + public function user() + { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->belongsTo($classMapper->getClassMapping('user'), 'user_id'); + } +} diff --git a/login/app/sprinkles/account/src/Database/Models/Group.php b/login/app/sprinkles/account/src/Database/Models/Group.php new file mode 100755 index 0000000..f10e066 --- /dev/null +++ b/login/app/sprinkles/account/src/Database/Models/Group.php @@ -0,0 +1,69 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Database\Models; + +use Illuminate\Database\Capsule\Manager as Capsule; +use UserFrosting\Sprinkle\Core\Database\Models\Model; + +/** + * Group Class + * + * Represents a group object as stored in the database. + * + * @package UserFrosting + * @author Alex Weissman + * @see http://www.userfrosting.com/tutorials/lesson-3-data-model/ + * + * @property string slug + * @property string name + * @property string description + * @property string icon + */ +class Group extends Model +{ + /** + * @var string The name of the table for the current model. + */ + protected $table = "groups"; + + protected $fillable = [ + "slug", + "name", + "description", + "icon" + ]; + + /** + * @var bool Enable timestamps for this class. + */ + public $timestamps = true; + + /** + * Delete this group from the database, along with any user associations + * + * @todo What do we do with users when their group is deleted? Reassign them? Or, can a user be "groupless"? + */ + public function delete() + { + // Delete the group + $result = parent::delete(); + + return $result; + } + + /** + * Lazily load a collection of Users which belong to this group. + */ + public function users() + { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->hasMany($classMapper->getClassMapping('user'), 'group_id'); + } +} diff --git a/login/app/sprinkles/account/src/Database/Models/PasswordReset.php b/login/app/sprinkles/account/src/Database/Models/PasswordReset.php new file mode 100755 index 0000000..ac8a930 --- /dev/null +++ b/login/app/sprinkles/account/src/Database/Models/PasswordReset.php @@ -0,0 +1,76 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Database\Models; + +use Illuminate\Database\Capsule\Manager as Capsule; +use UserFrosting\Sprinkle\Core\Database\Models\Model; + +/** + * Password Reset Class + * + * Represents a password reset request for a specific user. + * @author Alex Weissman (https://alexanderweissman.com) + * @property int user_id + * @property hash token + * @property bool completed + * @property datetime expires_at + * @property datetime completed_at + */ +class PasswordReset extends Model +{ + /** + * @var string The name of the table for the current model. + */ + protected $table = "password_resets"; + + protected $fillable = [ + "user_id", + "hash", + "completed", + "expires_at", + "completed_at" + ]; + + /** + * @var bool Enable timestamps for PasswordResets. + */ + public $timestamps = true; + + /** + * Stores the raw (unhashed) token when created, so that it can be emailed out to the user. NOT persisted. + */ + protected $token; + + /** + * @return string + */ + public function getToken() + { + return $this->token; + } + + /** + * @param string $value + */ + public function setToken($value) + { + $this->token = $value; + return $this; + } + + /** + * Get the user associated with this reset request. + */ + public function user() + { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->belongsTo($classMapper->getClassMapping('user'), 'user_id'); + } +} diff --git a/login/app/sprinkles/account/src/Database/Models/Permission.php b/login/app/sprinkles/account/src/Database/Models/Permission.php new file mode 100755 index 0000000..463af8d --- /dev/null +++ b/login/app/sprinkles/account/src/Database/Models/Permission.php @@ -0,0 +1,121 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Database\Models; + +use Illuminate\Database\Capsule\Manager as Capsule; +use UserFrosting\Sprinkle\Core\Database\Models\Model; + +/** + * Permission Class. + * + * Represents a permission for a role or user. + * @author Alex Weissman (https://alexanderweissman.com) + * @property string slug + * @property string name + * @property string conditions + * @property string description + */ +class Permission extends Model +{ + /** + * @var string The name of the table for the current model. + */ + protected $table = "permissions"; + + protected $fillable = [ + "slug", + "name", + "conditions", + "description" + ]; + + /** + * @var bool Enable timestamps for this class. + */ + public $timestamps = true; + + /** + * Delete this permission from the database, removing associations with roles. + * + */ + public function delete() + { + // Remove all role associations + $this->roles()->detach(); + + // Delete the permission + $result = parent::delete(); + + return $result; + } + + /** + * Get a list of roles to which this permission is assigned. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + public function roles() + { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->belongsToMany($classMapper->getClassMapping('role'), 'permission_roles', 'permission_id', 'role_id')->withTimestamps(); + } + + /** + * Query scope to get all permissions assigned to a specific role. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param int $roleId + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeForRole($query, $roleId) + { + return $query->join('permission_roles', function ($join) use ($roleId) { + $join->on('permission_roles.permission_id', 'permissions.id') + ->where('role_id', $roleId); + }); + } + + /** + * Query scope to get all permissions NOT associated with a specific role. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param int $roleId + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeNotForRole($query, $roleId) + { + return $query->join('permission_roles', function ($join) use ($roleId) { + $join->on('permission_roles.permission_id', 'permissions.id') + ->where('role_id', '!=', $roleId); + }); + } + + /** + * Get a list of users who have this permission, along with a list of roles through which each user has the permission. + * + * @return \UserFrosting\Sprinkle\Core\Database\Relations\BelongsToManyThrough + */ + public function users() + { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->belongsToManyThrough( + $classMapper->getClassMapping('user'), + $classMapper->getClassMapping('role'), + 'permission_roles', + 'permission_id', + 'role_id', + 'role_users', + 'role_id', + 'user_id' + ); + } +} diff --git a/login/app/sprinkles/account/src/Database/Models/Role.php b/login/app/sprinkles/account/src/Database/Models/Role.php new file mode 100755 index 0000000..ce9cb8c --- /dev/null +++ b/login/app/sprinkles/account/src/Database/Models/Role.php @@ -0,0 +1,105 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Database\Models; + +use Illuminate\Database\Capsule\Manager as Capsule; +use UserFrosting\Sprinkle\Core\Database\Models\Model; + +/** + * Role Class + * + * Represents a role, which aggregates permissions and to which a user can be assigned. + * @author Alex Weissman (https://alexanderweissman.com) + * @property string slug + * @property string name + * @property string description + */ +class Role extends Model +{ + /** + * @var string The name of the table for the current model. + */ + protected $table = "roles"; + + protected $fillable = [ + "slug", + "name", + "description" + ]; + + /** + * @var bool Enable timestamps for this class. + */ + public $timestamps = true; + + /** + * Delete this role from the database, removing associations with permissions and users. + * + */ + public function delete() + { + // Remove all permission associations + $this->permissions()->detach(); + + // Remove all user associations + $this->users()->detach(); + + // Delete the role + $result = parent::delete(); + + return $result; + } + + /** + * Get a list of default roles. + */ + public static function getDefaultSlugs() + { + /** @var UserFrosting\Config $config */ + $config = static::$ci->config; + + return array_map('trim', array_keys($config['site.registration.user_defaults.roles'], true)); + } + + /** + * Get a list of permissions assigned to this role. + */ + public function permissions() + { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->belongsToMany($classMapper->getClassMapping('permission'), 'permission_roles', 'role_id', 'permission_id')->withTimestamps(); + } + + /** + * Query scope to get all roles assigned to a specific user. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param int $userId + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeForUser($query, $userId) + { + return $query->join('role_users', function ($join) use ($userId) { + $join->on('role_users.role_id', 'roles.id') + ->where('user_id', $userId); + }); + } + + /** + * Get a list of users who have this role. + */ + public function users() + { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->belongsToMany($classMapper->getClassMapping('user'), 'role_users', 'role_id', 'user_id'); + } +} diff --git a/login/app/sprinkles/account/src/Database/Models/User.php b/login/app/sprinkles/account/src/Database/Models/User.php new file mode 100755 index 0000000..235f2ef --- /dev/null +++ b/login/app/sprinkles/account/src/Database/Models/User.php @@ -0,0 +1,493 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Database\Models; + +use Carbon\Carbon; +use Illuminate\Database\Capsule\Manager as Capsule; +use Illuminate\Database\Eloquent\SoftDeletes; +use UserFrosting\Sprinkle\Account\Facades\Password; +use UserFrosting\Sprinkle\Core\Database\Models\Model; +use UserFrosting\Sprinkle\Core\Facades\Debug; + +/** + * User Class + * + * Represents a User object as stored in the database. + * + * @author Alex Weissman (https://alexanderweissman.com) + * @property int id + * @property string user_name + * @property string first_name + * @property string last_name + * @property string email + * @property string locale + * @property string theme + * @property int group_id + * @property bool flag_verified + * @property bool flag_enabled + * @property int last_activity_id + * @property timestamp created_at + * @property timestamp updated_at + * @property string password + * @property timestamp deleted_at + */ +class User extends Model +{ + use SoftDeletes; + + /** + * The name of the table for the current model. + * + * @var string + */ + protected $table = 'users'; + + /** + * Fields that should be mass-assignable when creating a new User. + * + * @var string[] + */ + protected $fillable = [ + 'user_name', + 'first_name', + 'last_name', + 'email', + 'locale', + 'theme', + 'group_id', + 'flag_verified', + 'flag_enabled', + 'last_activity_id', + 'password', + 'deleted_at' + ]; + + /** + * A list of attributes to hide by default when using toArray() and toJson(). + * + * @link https://laravel.com/docs/5.4/eloquent-serialization#hiding-attributes-from-json + * @var string[] + */ + protected $hidden = [ + 'password' + ]; + + /** + * The attributes that should be mutated to dates. + * + * @var string[] + */ + protected $dates = [ + 'deleted_at' + ]; + + protected $appends = [ + 'full_name' + ]; + + /** + * Cached dictionary of permissions for the user. + * + * @var array + */ + protected $cachedPermissions; + + /** + * Enable timestamps for Users. + * + * @var bool + */ + public $timestamps = true; + + /** + * Determine if the property for this object exists. + * + * We add relations here so that Twig will be able to find them. + * See http://stackoverflow.com/questions/29514081/cannot-access-eloquent-attributes-on-twig/35908957#35908957 + * Every property in __get must also be implemented here for Twig to recognize it. + * @param string $name the name of the property to check. + * @return bool true if the property is defined, false otherwise. + */ + public function __isset($name) + { + if (in_array($name, [ + 'group', + 'last_sign_in_time', + 'avatar' + ])) { + return true; + } else { + return parent::__isset($name); + } + } + + /** + * Get a property for this object. + * + * @param string $name the name of the property to retrieve. + * @throws Exception the property does not exist for this object. + * @return string the associated property. + */ + public function __get($name) + { + if ($name == 'last_sign_in_time') { + return $this->lastActivityTime('sign_in'); + } elseif ($name == 'avatar') { + // Use Gravatar as the user avatar + $hash = md5(strtolower(trim( $this->email))); + return 'https://www.gravatar.com/avatar/' . $hash . '?d=mm'; + } else { + return parent::__get($name); + } + } + + /** + * Get all activities for this user. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function activities() + { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->hasMany($classMapper->getClassMapping('activity'), 'user_id'); + } + + /** + * Delete this user from the database, along with any linked roles and activities. + * + * @param bool $hardDelete Set to true to completely remove the user and all associated objects. + * @return bool true if the deletion was successful, false otherwise. + */ + public function delete($hardDelete = false) + { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + if ($hardDelete) { + // Remove all role associations + $this->roles()->detach(); + + // Remove all user activities + $classMapper->staticMethod('activity', 'where', 'user_id', $this->id)->delete(); + + // Remove all user tokens + $classMapper->staticMethod('password_reset', 'where', 'user_id', $this->id)->delete(); + $classMapper->staticMethod('verification', 'where', 'user_id', $this->id)->delete(); + + // TODO: remove any persistences + + // Delete the user + $result = parent::forceDelete(); + } else { + // Soft delete the user, leaving all associated records alone + $result = parent::delete(); + } + + return $result; + } + + /** + * Determines whether a user exists, including checking soft-deleted records + * + * @deprecated since 4.1.7 This method conflicts with and overrides the Builder::exists() method. Use Model::findUnique instead. + * @param mixed $value + * @param string $identifier + * @param bool $checkDeleted set to true to include soft-deleted records + * @return User|null + */ + public static function exists($value, $identifier = 'user_name', $checkDeleted = true) + { + return static::findUnique($value, $identifier, $checkDeleted); + } + + /** + * Return a cache instance specific to that user + * + * @return \Illuminate\Contracts\Cache\Store + */ + public function getCache() + { + return static::$ci->cache->tags('_u'.$this->id); + } + + /** + * Allows you to get the full name of the user using `$user->full_name` + * + * @return string + */ + public function getFullNameAttribute() + { + return $this->first_name . ' ' . $this->last_name; + } + + /** + * Retrieve the cached permissions dictionary for this user. + * + * @return array + */ + public function getCachedPermissions() + { + if (!isset($this->cachedPermissions)) { + $this->reloadCachedPermissions(); + } + + return $this->cachedPermissions; + } + + /** + * Retrieve the cached permissions dictionary for this user. + * + * @return User + */ + public function reloadCachedPermissions() + { + $this->cachedPermissions = $this->buildPermissionsDictionary(); + + return $this; + } + + /** + * Get the amount of time, in seconds, that has elapsed since the last activity of a certain time for this user. + * + * @param string $type The type of activity to search for. + * @return int + */ + public function getSecondsSinceLastActivity($type) + { + $time = $this->lastActivityTime($type); + $time = $time ? $time : '0000-00-00 00:00:00'; + $time = new Carbon($time); + + return $time->diffInSeconds(); + } + + /** + * Return this user's group. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function group() + { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->belongsTo($classMapper->getClassMapping('group'), 'group_id'); + } + + /** + * Returns whether or not this user is the master user. + * + * @return bool + */ + public function isMaster() + { + $masterId = static::$ci->config['reserved_user_ids.master']; + + // Need to use loose comparison for now, because some DBs return `id` as a string + return ($this->id == $masterId); + } + + /** + * Get the most recent activity for this user, based on the user's last_activity_id. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function lastActivity() + { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->belongsTo($classMapper->getClassMapping('activity'), 'last_activity_id'); + } + + /** + * Find the most recent activity for this user of a particular type. + * + * @param string $type + * @return \Illuminate\Database\Eloquent\Builder + */ + public function lastActivityOfType($type = null) + { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + $query = $this->hasOne($classMapper->getClassMapping('activity'), 'user_id'); + + if ($type) { + $query = $query->where('type', $type); + } + + return $query->latest('occurred_at'); + } + + /** + * Get the most recent time for a specified activity type for this user. + * + * @param string $type + * @return string|null The last activity time, as a SQL formatted time (YYYY-MM-DD HH:MM:SS), or null if an activity of this type doesn't exist. + */ + public function lastActivityTime($type) + { + $result = $this->activities() + ->where('type', $type) + ->max('occurred_at'); + return $result ? $result : null; + } + + /** + * Performs tasks to be done after this user has been successfully authenticated. + * + * By default, adds a new sign-in activity and updates any legacy hash. + * @param mixed[] $params Optional array of parameters used for this event handler. + * @todo Transition to Laravel Event dispatcher to handle this + */ + public function onLogin($params = []) + { + // Add a sign in activity (time is automatically set by database) + static::$ci->userActivityLogger->info("User {$this->user_name} signed in.", [ + 'type' => 'sign_in' + ]); + + // Update password if we had encountered an outdated hash + $passwordType = Password::getHashType($this->password); + + if ($passwordType != 'modern') { + if (!isset($params['password'])) { + Debug::debug('Notice: Unhashed password must be supplied to update to modern password hashing.'); + } else { + // Hash the user's password and update + $passwordHash = Password::hash($params['password']); + if ($passwordHash === null) { + Debug::debug('Notice: outdated password hash could not be updated because the new hashing algorithm is not supported. Are you running PHP >= 5.3.7?'); + } else { + $this->password = $passwordHash; + Debug::debug('Notice: outdated password hash has been automatically updated to modern hashing.'); + } + } + } + + // Save changes + $this->save(); + + return $this; + } + + /** + * Performs tasks to be done after this user has been logged out. + * + * By default, adds a new sign-out activity. + * @param mixed[] $params Optional array of parameters used for this event handler. + * @todo Transition to Laravel Event dispatcher to handle this + */ + public function onLogout($params = []) + { + static::$ci->userActivityLogger->info("User {$this->user_name} signed out.", [ + 'type' => 'sign_out' + ]); + + return $this; + } + + /** + * Get all password reset requests for this user. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function passwordResets() + { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->hasMany($classMapper->getClassMapping('password_reset'), 'user_id'); + } + + /** + * Get all of the permissions this user has, via its roles. + * + * @return \UserFrosting\Sprinkle\Core\Database\Relations\BelongsToManyThrough + */ + public function permissions() + { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->belongsToManyThrough( + $classMapper->getClassMapping('permission'), + $classMapper->getClassMapping('role'), + 'role_users', + 'user_id', + 'role_id', + 'permission_roles', + 'role_id', + 'permission_id' + ); + } + + /** + * Get all roles to which this user belongs. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + public function roles() + { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->belongsToMany($classMapper->getClassMapping('role'), 'role_users', 'user_id', 'role_id')->withTimestamps(); + } + + /** + * Query scope to get all users who have a specific role. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param int $roleId + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeForRole($query, $roleId) + { + return $query->join('role_users', function ($join) use ($roleId) { + $join->on('role_users.user_id', 'users.id') + ->where('role_id', $roleId); + }); + } + + /** + * Joins the user's most recent activity directly, so we can do things like sort, search, paginate, etc. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeJoinLastActivity($query) + { + $query = $query->select('users.*'); + + $query = $query->leftJoin('activities', 'activities.id', '=', 'users.last_activity_id'); + + return $query; + } + + /** + * Loads permissions for this user into a cached dictionary of slugs -> arrays of permissions, + * so we don't need to keep requerying the DB for every call of checkAccess. + * + * @return array + */ + protected function buildPermissionsDictionary() + { + $permissions = $this->permissions()->get(); + $cachedPermissions = []; + + foreach ($permissions as $permission) { + $cachedPermissions[$permission->slug][] = $permission; + } + + return $cachedPermissions; + } +} diff --git a/login/app/sprinkles/account/src/Database/Models/Verification.php b/login/app/sprinkles/account/src/Database/Models/Verification.php new file mode 100755 index 0000000..cd5166d --- /dev/null +++ b/login/app/sprinkles/account/src/Database/Models/Verification.php @@ -0,0 +1,70 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Database\Models; + +use Illuminate\Database\Capsule\Manager as Capsule; +use UserFrosting\Sprinkle\Core\Database\Models\Model; + +/** + * Verification Class + * + * Represents a pending email verification for a new user account. + * @author Alex Weissman (https://alexanderweissman.com) + * @property int user_id + * @property hash token + * @property bool completed + * @property datetime expires_at + * @property datetime completed_at + */ +class Verification extends Model +{ + /** + * @var string The name of the table for the current model. + */ + protected $table = "verifications"; + + protected $fillable = [ + "user_id", + "hash", + "completed", + "expires_at", + "completed_at" + ]; + + /** + * @var bool Enable timestamps for Verifications. + */ + public $timestamps = true; + + /** + * Stores the raw (unhashed) token when created, so that it can be emailed out to the user. NOT persisted. + */ + protected $token; + + public function getToken() + { + return $this->token; + } + + public function setToken($value) + { + $this->token = $value; + return $this; + } + + /** + * Get the user associated with this verification request. + */ + public function user() + { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + return $this->belongsTo($classMapper->getClassMapping('user'), 'user_id'); + } +} diff --git a/login/app/sprinkles/account/src/Error/Handler/AuthCompromisedExceptionHandler.php b/login/app/sprinkles/account/src/Error/Handler/AuthCompromisedExceptionHandler.php new file mode 100755 index 0000000..330ca65 --- /dev/null +++ b/login/app/sprinkles/account/src/Error/Handler/AuthCompromisedExceptionHandler.php @@ -0,0 +1,34 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Error\Handler; + +use UserFrosting\Sprinkle\Core\Error\Handler\HttpExceptionHandler; + +/** + * Handler for AuthCompromisedExceptions. + * + * Warns the user that their account may have been compromised due to a stolen "remember me" cookie. + * @author Alex Weissman (https://alexanderweissman.com) + */ +class AuthCompromisedExceptionHandler extends HttpExceptionHandler +{ + /** + * Render a generic, user-friendly response without sensitive debugging information. + * + * @return ResponseInterface + */ + public function renderGenericResponse() + { + $template = $this->ci->view->getEnvironment()->loadTemplate('pages/error/compromised.html.twig'); + + return $this->response + ->withStatus($this->statusCode) + ->withHeader('Content-type', $this->contentType) + ->write($template->render()); + } +} diff --git a/login/app/sprinkles/account/src/Error/Handler/AuthExpiredExceptionHandler.php b/login/app/sprinkles/account/src/Error/Handler/AuthExpiredExceptionHandler.php new file mode 100755 index 0000000..c651f77 --- /dev/null +++ b/login/app/sprinkles/account/src/Error/Handler/AuthExpiredExceptionHandler.php @@ -0,0 +1,50 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Error\Handler; + +use UserFrosting\Sprinkle\Core\Error\Handler\HttpExceptionHandler; + +/** + * Handler for AuthExpiredExceptions. + * + * Forwards the user to the login page when their session has expired. + * @author Alex Weissman (https://alexanderweissman.com) + */ +class AuthExpiredExceptionHandler extends HttpExceptionHandler +{ + /** + * Custom handling for requests that did not pass authentication. + */ + public function handle() + { + // For auth expired exceptions, we always add messages to the alert stream. + $this->writeAlerts(); + + $response = $this->response; + + // For non-AJAX requests, we forward the user to the login page. + if (!$this->request->isXhr()) { + $uri = $this->request->getUri(); + $path = $uri->getPath(); + $query = $uri->getQuery(); + $fragment = $uri->getFragment(); + + $path = $path + . ($query ? '?' . $query : '') + . ($fragment ? '#' . $fragment : ''); + + $loginPage = $this->ci->router->pathFor('login', [], [ + 'redirect' => $path + ]); + + $response = $response->withRedirect($loginPage); + } + + return $response; + } +} diff --git a/login/app/sprinkles/account/src/Error/Handler/ForbiddenExceptionHandler.php b/login/app/sprinkles/account/src/Error/Handler/ForbiddenExceptionHandler.php new file mode 100755 index 0000000..e22f02b --- /dev/null +++ b/login/app/sprinkles/account/src/Error/Handler/ForbiddenExceptionHandler.php @@ -0,0 +1,31 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Error\Handler; + +use UserFrosting\Sprinkle\Core\Error\Handler\HttpExceptionHandler; +use UserFrosting\Support\Message\UserMessage; + +/** + * Handler for ForbiddenExceptions. Only really needed to override the default error message. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class ForbiddenExceptionHandler extends HttpExceptionHandler +{ + /** + * Resolve a list of error messages to present to the end user. + * + * @return array + */ + protected function determineUserMessages() + { + return [ + new UserMessage("ACCOUNT.ACCESS_DENIED") + ]; + } +} diff --git a/login/app/sprinkles/account/src/Facades/Password.php b/login/app/sprinkles/account/src/Facades/Password.php new file mode 100755 index 0000000..e5bf967 --- /dev/null +++ b/login/app/sprinkles/account/src/Facades/Password.php @@ -0,0 +1,28 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Facades; + +use UserFrosting\System\Facade; + +/** + * Implements facade for the "password" service + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class Password extends Facade +{ + /** + * Get the registered name of the component. + * + * @return string + */ + protected static function getFacadeAccessor() + { + return 'passwordHasher'; + } +} diff --git a/login/app/sprinkles/account/src/Log/UserActivityDatabaseHandler.php b/login/app/sprinkles/account/src/Log/UserActivityDatabaseHandler.php new file mode 100755 index 0000000..d7ceeef --- /dev/null +++ b/login/app/sprinkles/account/src/Log/UserActivityDatabaseHandler.php @@ -0,0 +1,33 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Log; + +use UserFrosting\Sprinkle\Core\Log\DatabaseHandler; + +/** + * Monolog handler for storing user activities to the database. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class UserActivityDatabaseHandler extends DatabaseHandler +{ + /** + * {@inheritDoc} + */ + protected function write(array $record) + { + $log = $this->classMapper->createInstance($this->modelName, $record['extra']); + $log->save(); + + if (isset($record['extra']['user_id'])) { + $user = $this->classMapper->staticMethod('user', 'find', $record['extra']['user_id']); + $user->last_activity_id = $log->id; + $user->save(); + } + } +} diff --git a/login/app/sprinkles/account/src/Log/UserActivityProcessor.php b/login/app/sprinkles/account/src/Log/UserActivityProcessor.php new file mode 100755 index 0000000..2575270 --- /dev/null +++ b/login/app/sprinkles/account/src/Log/UserActivityProcessor.php @@ -0,0 +1,45 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Log; + +use Monolog\Logger; + +/** + * Monolog processor for constructing the user activity message. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class UserActivityProcessor +{ + /** + * @var int + */ + protected $userId; + + /** + * @param int $userId The id of the user for whom we will be logging activities. + */ + public function __construct($userId) + { + $this->userId = $userId; + } + + public function __invoke(array $record) + { + $additionalFields = [ + 'ip_address' => $_SERVER['REMOTE_ADDR'], + 'user_id' => $this->userId, + 'occurred_at' => $record['datetime'], + 'description' => $record['message'] + ]; + + $record['extra'] = array_replace_recursive($record['extra'], $additionalFields, $record['context']); + + return $record; + } +} diff --git a/login/app/sprinkles/account/src/Repository/PasswordResetRepository.php b/login/app/sprinkles/account/src/Repository/PasswordResetRepository.php new file mode 100755 index 0000000..2dcffd3 --- /dev/null +++ b/login/app/sprinkles/account/src/Repository/PasswordResetRepository.php @@ -0,0 +1,34 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Repository; + +use UserFrosting\Sprinkle\Account\Facades\Password; + +/** + * Token repository class for password reset requests. + * + * @author Alex Weissman (https://alexanderweissman.com) + * @see https://learn.userfrosting.com/users/user-accounts + */ +class PasswordResetRepository extends TokenRepository +{ + /** + * {@inheritDoc} + */ + protected $modelIdentifier = 'password_reset'; + + /** + * {@inheritDoc} + */ + protected function updateUser($user, $args) + { + $user->password = Password::hash($args['password']); + // TODO: generate user activity? or do this in controller? + $user->save(); + } +} diff --git a/login/app/sprinkles/account/src/Repository/TokenRepository.php b/login/app/sprinkles/account/src/Repository/TokenRepository.php new file mode 100755 index 0000000..a299439 --- /dev/null +++ b/login/app/sprinkles/account/src/Repository/TokenRepository.php @@ -0,0 +1,230 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Repository; + +use Carbon\Carbon; +use UserFrosting\Sprinkle\Account\Database\Models\User; +use UserFrosting\Sprinkle\Core\Database\Models\Model; +use UserFrosting\Sprinkle\Core\Util\ClassMapper; + +/** + * An abstract class for interacting with a repository of time-sensitive user tokens. + * + * User tokens are used, for example, to perform password resets and new account email verifications. + * @author Alex Weissman (https://alexanderweissman.com) + * @see https://learn.userfrosting.com/users/user-accounts + */ +abstract class TokenRepository +{ + + /** + * @var ClassMapper + */ + protected $classMapper; + + /** + * @var string + */ + protected $algorithm; + + /** + * @var string + */ + protected $modelIdentifier; + + /** + * Create a new TokenRepository object. + * + * @param ClassMapper $classMapper Maps generic class identifiers to specific class names. + * @param string $algorithm The hashing algorithm to use when storing generated tokens. + */ + public function __construct(ClassMapper $classMapper, $algorithm = 'sha512') + { + $this->classMapper = $classMapper; + $this->algorithm = $algorithm; + } + + /** + * Cancels a specified token by removing it from the database. + * + * @param int $token The token to remove. + * @return Model|false + */ + public function cancel($token) + { + // Hash the password reset token for the stored version + $hash = hash($this->algorithm, $token); + + // Find an incomplete reset request for the specified hash + $model = $this->classMapper + ->staticMethod($this->modelIdentifier, 'where', 'hash', $hash) + ->where('completed', false) + ->first(); + + if ($model === null) { + return false; + } + + $model->delete(); + + return $model; + } + + /** + * Completes a token-based process, invoking updateUser() in the child object to do the actual action. + * + * @param int $token The token to complete. + * @param mixed[] $userParams An optional list of parameters to pass to updateUser(). + * @return Model|false + */ + public function complete($token, $userParams = []) + { + // Hash the token for the stored version + $hash = hash($this->algorithm, $token); + + // Find an unexpired, incomplete token for the specified hash + $model = $this->classMapper + ->staticMethod($this->modelIdentifier, 'where', 'hash', $hash) + ->where('completed', false) + ->where('expires_at', '>', Carbon::now()) + ->first(); + + if ($model === null) { + return false; + } + + // Fetch user for this token + $user = $this->classMapper->staticMethod('user', 'find', $model->user_id); + + if (is_null($user)) { + return false; + } + + $this->updateUser($user, $userParams); + + $model->fill([ + 'completed' => true, + 'completed_at' => Carbon::now() + ]); + + $model->save(); + + return $model; + } + + /** + * Create a new token for a specified user. + * + * @param User $user The user object to associate with this token. + * @param int $timeout The time, in seconds, after which this token should expire. + * @return Model The model (PasswordReset, Verification, etc) object that stores the token. + */ + public function create(User $user, $timeout) + { + // Remove any previous tokens for this user + $this->removeExisting($user); + + // Compute expiration time + $expiresAt = Carbon::now()->addSeconds($timeout); + + $model = $this->classMapper->createInstance($this->modelIdentifier); + + // Generate a random token + $model->setToken($this->generateRandomToken()); + + // Hash the password reset token for the stored version + $hash = hash($this->algorithm, $model->getToken()); + + $model->fill([ + 'hash' => $hash, + 'completed' => false, + 'expires_at' => $expiresAt + ]); + + $model->user_id = $user->id; + + $model->save(); + + return $model; + } + + /** + * Determine if a specified user has an incomplete and unexpired token. + * + * @param User $user The user object to look up. + * @param int $token Optionally, try to match a specific token. + * @return Model|false + */ + public function exists(User $user, $token = null) + { + $model = $this->classMapper + ->staticMethod($this->modelIdentifier, 'where', 'user_id', $user->id) + ->where('completed', false) + ->where('expires_at', '>', Carbon::now()); + + if ($token) { + // get token hash + $hash = hash($this->algorithm, $token); + $model->where('hash', $hash); + } + + return $model->first() ?: false; + } + + /** + * Delete all existing tokens from the database for a particular user. + * + * @param User $user + * @return int + */ + protected function removeExisting(User $user) + { + return $this->classMapper + ->staticMethod($this->modelIdentifier, 'where', 'user_id', $user->id) + ->delete(); + } + + /** + * Remove all expired tokens from the database. + * + * @return bool|null + */ + public function removeExpired() + { + return $this->classMapper + ->staticMethod($this->modelIdentifier, 'where', 'completed', false) + ->where('expires_at', '<', Carbon::now()) + ->delete(); + } + + /** + * Generate a new random token for this user. + * + * This generates a token to use for verifying a new account, resetting a lost password, etc. + * @param string $gen specify an existing token that, if we happen to generate the same value, we should regenerate on. + * @return string + */ + protected function generateRandomToken($gen = null) + { + do { + $gen = md5(uniqid(mt_rand(), false)); + } while($this->classMapper + ->staticMethod($this->modelIdentifier, 'where', 'hash', hash($this->algorithm, $gen)) + ->first()); + return $gen; + } + + /** + * Modify the user during the token completion process. + * + * This method is called during complete(), and is a way for concrete implementations to modify the user. + * @param User $user the user object to modify. + * @return mixed[] $args the list of parameters that were supplied to the call to `complete()` + */ + abstract protected function updateUser($user, $args); +} diff --git a/login/app/sprinkles/account/src/Repository/VerificationRepository.php b/login/app/sprinkles/account/src/Repository/VerificationRepository.php new file mode 100755 index 0000000..b0cf048 --- /dev/null +++ b/login/app/sprinkles/account/src/Repository/VerificationRepository.php @@ -0,0 +1,32 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Repository; + +/** + * Token repository class for new account verifications. + * + * @author Alex Weissman (https://alexanderweissman.com) + * @see https://learn.userfrosting.com/users/user-accounts + */ +class VerificationRepository extends TokenRepository +{ + /** + * {@inheritDoc} + */ + protected $modelIdentifier = 'verification'; + + /** + * {@inheritDoc} + */ + protected function updateUser($user, $args) + { + $user->flag_verified = 1; + // TODO: generate user activity? or do this in controller? + $user->save(); + } +} diff --git a/login/app/sprinkles/account/src/ServicesProvider/ServicesProvider.php b/login/app/sprinkles/account/src/ServicesProvider/ServicesProvider.php new file mode 100755 index 0000000..4c3ab15 --- /dev/null +++ b/login/app/sprinkles/account/src/ServicesProvider/ServicesProvider.php @@ -0,0 +1,444 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\ServicesProvider; + +use Birke\Rememberme\Authenticator as RememberMe; +use Illuminate\Database\Capsule\Manager as Capsule; +use Monolog\Formatter\LineFormatter; +use Monolog\Handler\ErrorLogHandler; +use Monolog\Handler\StreamHandler; +use Monolog\Logger; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; +use UserFrosting\Sprinkle\Account\Authenticate\Authenticator; +use UserFrosting\Sprinkle\Account\Authenticate\AuthGuard; +use UserFrosting\Sprinkle\Account\Authenticate\Hasher; +use UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager; +use UserFrosting\Sprinkle\Account\Database\Models\User; +use UserFrosting\Sprinkle\Account\Log\UserActivityDatabaseHandler; +use UserFrosting\Sprinkle\Account\Log\UserActivityProcessor; +use UserFrosting\Sprinkle\Account\Repository\PasswordResetRepository; +use UserFrosting\Sprinkle\Account\Repository\VerificationRepository; +use UserFrosting\Sprinkle\Account\Twig\AccountExtension; +use UserFrosting\Sprinkle\Core\Facades\Debug; +use UserFrosting\Sprinkle\Core\Log\MixedFormatter; + +/** + * Registers services for the account sprinkle, such as currentUser, etc. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class ServicesProvider +{ + /** + * Register UserFrosting's account services. + * + * @param Container $container A DI container implementing ArrayAccess and container-interop. + */ + public function register($container) + { + /** + * Extend the asset manager service to see assets for the current user's theme. + */ + $container->extend('assets', function ($assets, $c) { + + // Register paths for user theme, if a user is logged in + // We catch any authorization-related exceptions, so that error pages can be rendered. + try { + /** @var UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ + $authenticator = $c->authenticator; + $currentUser = $c->currentUser; + } catch (\Exception $e) { + return $assets; + } + + if ($authenticator->check()) { + $c->sprinkleManager->addResource('assets', $currentUser->theme); + } + + return $assets; + }); + + /** + * Extend the 'classMapper' service to register model classes. + * + * Mappings added: User, Group, Role, Permission, Activity, PasswordReset, Verification + */ + $container->extend('classMapper', function ($classMapper, $c) { + $classMapper->setClassMapping('user', 'UserFrosting\Sprinkle\Account\Database\Models\User'); + $classMapper->setClassMapping('group', 'UserFrosting\Sprinkle\Account\Database\Models\Group'); + $classMapper->setClassMapping('role', 'UserFrosting\Sprinkle\Account\Database\Models\Role'); + $classMapper->setClassMapping('permission', 'UserFrosting\Sprinkle\Account\Database\Models\Permission'); + $classMapper->setClassMapping('activity', 'UserFrosting\Sprinkle\Account\Database\Models\Activity'); + $classMapper->setClassMapping('password_reset', 'UserFrosting\Sprinkle\Account\Database\Models\PasswordReset'); + $classMapper->setClassMapping('verification', 'UserFrosting\Sprinkle\Account\Database\Models\Verification'); + return $classMapper; + }); + + /** + * Extends the 'errorHandler' service with custom exception handlers. + * + * Custom handlers added: ForbiddenExceptionHandler + */ + $container->extend('errorHandler', function ($handler, $c) { + // Register the ForbiddenExceptionHandler. + $handler->registerHandler('\UserFrosting\Support\Exception\ForbiddenException', '\UserFrosting\Sprinkle\Account\Error\Handler\ForbiddenExceptionHandler'); + // Register the AuthExpiredExceptionHandler + $handler->registerHandler('\UserFrosting\Sprinkle\Account\Authenticate\Exception\AuthExpiredException', '\UserFrosting\Sprinkle\Account\Error\Handler\AuthExpiredExceptionHandler'); + // Register the AuthCompromisedExceptionHandler. + $handler->registerHandler('\UserFrosting\Sprinkle\Account\Authenticate\Exception\AuthCompromisedException', '\UserFrosting\Sprinkle\Account\Error\Handler\AuthCompromisedExceptionHandler'); + return $handler; + }); + + /** + * Extends the 'localePathBuilder' service, adding any locale files from the user theme. + * + */ + $container->extend('localePathBuilder', function ($pathBuilder, $c) { + // Add paths for user theme, if a user is logged in + // We catch any authorization-related exceptions, so that error pages can be rendered. + try { + /** @var UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ + $authenticator = $c->authenticator; + $currentUser = $c->currentUser; + } catch (\Exception $e) { + return $pathBuilder; + } + + if ($authenticator->check()) { + // Add paths to locale files for user theme + $themePath = $c->sprinkleManager->addResource('locale', $currentUser->theme); + + // Add user locale + $pathBuilder->addLocales($currentUser->locale); + } + + return $pathBuilder; + }); + + /** + * Extends the 'view' service with the AccountExtension for Twig. + * + * Adds account-specific functions, globals, filters, etc to Twig, and the path to templates for the user theme. + */ + $container->extend('view', function ($view, $c) { + $twig = $view->getEnvironment(); + $extension = new AccountExtension($c); + $twig->addExtension($extension); + + // Add paths for user theme, if a user is logged in + // We catch any authorization-related exceptions, so that error pages can be rendered. + try { + /** @var UserFrosting\Sprinkle\Account\Authenticate\Authenticator $authenticator */ + $authenticator = $c->authenticator; + $currentUser = $c->currentUser; + } catch (\Exception $e) { + return $view; + } + + if ($authenticator->check()) { + $theme = $currentUser->theme; + $themePath = $c->sprinkleManager->addResource('templates', $theme); + if ($themePath) { + $loader = $twig->getLoader(); + $loader->prependPath($themePath); + // Add namespaced path as well + $loader->addPath($themePath, $theme); + } + } + + return $view; + }); + + /** + * Authentication service. + * + * Supports logging in users, remembering their sessions, etc. + */ + $container['authenticator'] = function ($c) { + $classMapper = $c->classMapper; + $config = $c->config; + $session = $c->session; + $cache = $c->cache; + + // Force database connection to boot up + $c->db; + + // Fix RememberMe table name + $config['remember_me.table.tableName'] = Capsule::connection()->getTablePrefix() . $config['remember_me.table.tableName']; + + $authenticator = new Authenticator($classMapper, $session, $config, $cache); + return $authenticator; + }; + + /** + * Sets up the AuthGuard middleware, used to limit access to authenticated users for certain routes. + */ + $container['authGuard'] = function ($c) { + $authenticator = $c->authenticator; + return new AuthGuard($authenticator); + }; + + /** + * Authorization check logging with Monolog. + * + * Extend this service to push additional handlers onto the 'auth' log stack. + */ + $container['authLogger'] = function ($c) { + $logger = new Logger('auth'); + + $logFile = $c->get('locator')->findResource('log://userfrosting.log', true, true); + + $handler = new StreamHandler($logFile); + + $formatter = new MixedFormatter(null, null, true); + + $handler->setFormatter($formatter); + $logger->pushHandler($handler); + + return $logger; + }; + + /** + * Authorization service. + * + * Determines permissions for user actions. Extend this service to add additional access condition callbacks. + */ + $container['authorizer'] = function ($c) { + $config = $c->config; + + // Default access condition callbacks. Add more in your sprinkle by using $container->extend(...) + $callbacks = [ + /** + * Unconditionally grant permission - use carefully! + * @return bool returns true no matter what. + */ + 'always' => function () { + return true; + }, + + /** + * Check if the specified values are identical to one another (strict comparison). + * @param mixed $val1 the first value to compare. + * @param mixed $val2 the second value to compare. + * @return bool true if the values are strictly equal, false otherwise. + */ + 'equals' => function ($val1, $val2) { + return ($val1 === $val2); + }, + + /** + * Check if the specified values are numeric, and if so, if they are equal to each other. + * @param mixed $val1 the first value to compare. + * @param mixed $val2 the second value to compare. + * @return bool true if the values are numeric and equal, false otherwise. + */ + 'equals_num' => function ($val1, $val2) { + if (!is_numeric($val1)) { + return false; + } + if (!is_numeric($val2)) { + return false; + } + + return ($val1 == $val2); + }, + + /** + * Check if the specified user (by user_id) has a particular role. + * + * @param int $user_id the id of the user. + * @param int $role_id the id of the role. + * @return bool true if the user has the role, false otherwise. + */ + 'has_role' => function ($user_id, $role_id) { + return Capsule::table('role_users') + ->where('user_id', $user_id) + ->where('role_id', $role_id) + ->count() > 0; + }, + + /** + * Check if the specified value $needle is in the values of $haystack. + * + * @param mixed $needle the value to look for in $haystack + * @param array[mixed] $haystack the array of values to search. + * @return bool true if $needle is present in the values of $haystack, false otherwise. + */ + 'in' => function ($needle, $haystack) { + return in_array($needle, $haystack); + }, + + /** + * Check if the specified user (by user_id) is in a particular group. + * + * @param int $user_id the id of the user. + * @param int $group_id the id of the group. + * @return bool true if the user is in the group, false otherwise. + */ + 'in_group' => function ($user_id, $group_id) { + $user = User::find($user_id); + return ($user->group_id == $group_id); + }, + + /** + * Check if the specified user (by user_id) is the master user. + * + * @param int $user_id the id of the user. + * @return bool true if the user id is equal to the id of the master account, false otherwise. + */ + 'is_master' => function ($user_id) use ($config) { + // Need to use loose comparison for now, because some DBs return `id` as a string + return ($user_id == $config['reserved_user_ids.master']); + }, + + /** + * Check if all values in the array $needle are present in the values of $haystack. + * + * @param array[mixed] $needle the array whose values we should look for in $haystack + * @param array[mixed] $haystack the array of values to search. + * @return bool true if every value in $needle is present in the values of $haystack, false otherwise. + */ + 'subset' => function ($needle, $haystack) { + return count($needle) == count(array_intersect($needle, $haystack)); + }, + + /** + * Check if all keys of the array $needle are present in the values of $haystack. + * + * This function is useful for whitelisting an array of key-value parameters. + * @param array[mixed] $needle the array whose keys we should look for in $haystack + * @param array[mixed] $haystack the array of values to search. + * @return bool true if every key in $needle is present in the values of $haystack, false otherwise. + */ + 'subset_keys' => function ($needle, $haystack) { + return count($needle) == count(array_intersect(array_keys($needle), $haystack)); + } + ]; + + $authorizer = new AuthorizationManager($c, $callbacks); + return $authorizer; + }; + + /** + * Loads the User object for the currently logged-in user. + */ + $container['currentUser'] = function ($c) { + $authenticator = $c->authenticator; + + return $authenticator->user(); + }; + + $container['passwordHasher'] = function ($c) { + $hasher = new Hasher(); + return $hasher; + }; + + /** + * Returns a callback that forwards to dashboard if user is already logged in. + */ + $container['redirect.onAlreadyLoggedIn'] = function ($c) { + /** + * This method is invoked when a user attempts to perform certain public actions when they are already logged in. + * + * @todo Forward to user's landing page or last visited page + * @param \Psr\Http\Message\ServerRequestInterface $request + * @param \Psr\Http\Message\ResponseInterface $response + * @param array $args + * @return \Psr\Http\Message\ResponseInterface + */ + return function (Request $request, Response $response, array $args) use ($c) { + $redirect = $c->router->pathFor('dashboard'); + + return $response->withRedirect($redirect, 302); + }; + }; + + /** + * Returns a callback that handles setting the `UF-Redirect` header after a successful login. + */ + $container['redirect.onLogin'] = function ($c) { + /** + * This method is invoked when a user completes the login process. + * + * Returns a callback that handles setting the `UF-Redirect` header after a successful login. + * @param \Psr\Http\Message\ServerRequestInterface $request + * @param \Psr\Http\Message\ResponseInterface $response + * @param array $args + * @return \Psr\Http\Message\ResponseInterface + */ + return function (Request $request, Response $response, array $args) use ($c) { + // Backwards compatibility for the deprecated determineRedirectOnLogin service + if ($c->has('determineRedirectOnLogin')) { + $determineRedirectOnLogin = $c->determineRedirectOnLogin; + + return $determineRedirectOnLogin($response)->withStatus(200); + } + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ + $authorizer = $c->authorizer; + + $currentUser = $c->authenticator->user(); + + if ($authorizer->checkAccess($currentUser, 'uri_account_settings')) { + return $response->withHeader('UF-Redirect', $c->router->pathFor('settings')); + } else { + return $response->withHeader('UF-Redirect', $c->router->pathFor('index')); + } + }; + }; + + /** + * Repository for password reset requests. + */ + $container['repoPasswordReset'] = function ($c) { + $classMapper = $c->classMapper; + $config = $c->config; + + $repo = new PasswordResetRepository($classMapper, $config['password_reset.algorithm']); + return $repo; + }; + + /** + * Repository for verification requests. + */ + $container['repoVerification'] = function ($c) { + $classMapper = $c->classMapper; + $config = $c->config; + + $repo = new VerificationRepository($classMapper, $config['verification.algorithm']); + return $repo; + }; + + /** + * Logger for logging the current user's activities to the database. + * + * Extend this service to push additional handlers onto the 'userActivity' log stack. + */ + $container['userActivityLogger'] = function ($c) { + $classMapper = $c->classMapper; + $config = $c->config; + $session = $c->session; + + $logger = new Logger('userActivity'); + + $handler = new UserActivityDatabaseHandler($classMapper, 'activity'); + + // Note that we get the user id from the session, not the currentUser service. + // This is because the currentUser service may not reflect the actual user during login/logout requests. + $currentUserIdKey = $config['session.keys.current_user_id']; + $userId = isset($session[$currentUserIdKey]) ? $session[$currentUserIdKey] : $config['reserved_user_ids.guest']; + $processor = new UserActivityProcessor($userId); + + $logger->pushProcessor($processor); + $logger->pushHandler($handler); + + return $logger; + }; + } +} diff --git a/login/app/sprinkles/account/src/Twig/AccountExtension.php b/login/app/sprinkles/account/src/Twig/AccountExtension.php new file mode 100755 index 0000000..12bacba --- /dev/null +++ b/login/app/sprinkles/account/src/Twig/AccountExtension.php @@ -0,0 +1,65 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Twig; + +use Interop\Container\ContainerInterface; +use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; +use Slim\Http\Uri; + +/** + * Extends Twig functionality for the Account sprinkle. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class AccountExtension extends \Twig_Extension implements \Twig_Extension_GlobalsInterface +{ + + protected $services; + protected $config; + + public function __construct(ContainerInterface $services) + { + $this->services = $services; + $this->config = $services->config; + } + + public function getName() + { + return 'userfrosting/account'; + } + + public function getFunctions() + { + return array( + // Add Twig function for checking permissions during dynamic menu rendering + new \Twig_SimpleFunction('checkAccess', function ($slug, $params = []) { + $authorizer = $this->services->authorizer; + $currentUser = $this->services->currentUser; + + return $authorizer->checkAccess($currentUser, $slug, $params); + }), + new \Twig_SimpleFunction('checkAuthenticated', function () { + $authenticator = $this->services->authenticator; + return $authenticator->check(); + }) + ); + } + + public function getGlobals() + { + try { + $currentUser = $this->services->currentUser; + } catch (\Exception $e) { + $currentUser = null; + } + + return [ + 'current_user' => $currentUser + ]; + } +} diff --git a/login/app/sprinkles/account/src/Util/HashFailedException.php b/login/app/sprinkles/account/src/Util/HashFailedException.php new file mode 100755 index 0000000..a0b37d1 --- /dev/null +++ b/login/app/sprinkles/account/src/Util/HashFailedException.php @@ -0,0 +1,21 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Util; + +use UserFrosting\Support\Exception\HttpException; + +/** + * Password hash failure exception. Used when the supplied password could not be hashed for some reason. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class HashFailedException extends HttpException +{ + protected $defaultMessage = 'PASSWORD.HASH_FAILED'; + protected $httpErrorCode = 500; +} diff --git a/login/app/sprinkles/account/src/Util/Util.php b/login/app/sprinkles/account/src/Util/Util.php new file mode 100755 index 0000000..6452990 --- /dev/null +++ b/login/app/sprinkles/account/src/Util/Util.php @@ -0,0 +1,39 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Account\Util; + +use UserFrosting\Sprinkle\Core\Util\Util as CoreUtil; + +/** + * Util Class + * + * Static utility functions for the account Sprinkle. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class Util +{ + /** + * Generate a random, unique username from a list of adjectives and nouns. + */ + static public function randomUniqueUsername($classMapper, $maxLength, $maxTries = 10) + { + for ($n = 1; $n <= 3; $n++) { + for ($m = 0; $m < 10; $m++) { + // Generate a random phrase with $n adjectives + $suggestion = CoreUtil::randomPhrase($n, $maxLength, $maxTries, '.'); + if (!$classMapper->staticMethod('user', 'where', 'user_name', $suggestion)->first()) { + return $suggestion; + } + } + } + + return ''; + } + +} diff --git a/login/app/sprinkles/account/templates/forms/settings-account.html.twig b/login/app/sprinkles/account/templates/forms/settings-account.html.twig new file mode 100755 index 0000000..996b27b --- /dev/null +++ b/login/app/sprinkles/account/templates/forms/settings-account.html.twig @@ -0,0 +1,37 @@ +<form id="account-settings" role="form" action="{{site.uri.public}}/account/settings" method="post"> + <div class="box-header"> + <h3 class="box-title"><i class="fa fa-gear fa-fw"></i> {{translate("ACCOUNT.SETTINGS")}}</h3> + </div> + <div class="box-body"> + {% include "forms/csrf.html.twig" %} + <!-- Prevent browsers from trying to autofill the password field. See http://stackoverflow.com/a/23234498/2970321 --> + <input type="text" style="display:none"> + <input type="password" style="display:none"> + + {% block settings_account %} + <div class="form-group"> + <label for="input-email" class="ccontrol-label">{{translate("EMAIL")}}</label> + <input type="text" id="input-email" class="form-control" name="email" value="{{current_user.email}}" autocomplete="off" placeholder="{{translate("EMAIL.YOUR")}}" {{page.visibility}}> + </div> + {% if page.visibility != "disabled" %} + <div class="form-group"> + <label for="input-password" class="control-label">{{translate("PASSWORD.NEW")}}</label> + <input type="password" id="input-password" class="form-control" name="password" placeholder="{{translate("PASSWORD.BETWEEN", {min: 12, max: 100})}} ({{translate("OPTIONAL")}})"> + </div> + <div class="form-group"> + <label for="input-passwordc" class="control-label">{{translate("PASSWORD.CONFIRM_NEW")}}</label> + <input type="password" id="input-passwordc" class="form-control" name="passwordc" placeholder="{{translate("PASSWORD.CONFIRM_NEW_HELP")}}"> + </div> + <hr> + <div class="form-group"> + <label for="input-passwordcheck" class="control-label">{{translate("PASSWORD.CURRENT")}}</label> + <input type="password" id="input-passwordcheck" class="form-control" name="passwordcheck" placeholder="{{translate("PASSWORD.CURRENT_EXPLAIN")}}"> + </div> + {% endif %} + {% endblock %} + </div> + <div class="box-footer text-center"> + <button type="reset" class="btn btn-default">{{translate('RESET')}}</button> + <button type="submit" class="btn btn-primary js-submit">{{translate('SAVE')}}</button> + </div> +</form>
\ No newline at end of file diff --git a/login/app/sprinkles/account/templates/forms/settings-profile.html.twig b/login/app/sprinkles/account/templates/forms/settings-profile.html.twig new file mode 100755 index 0000000..0b0a788 --- /dev/null +++ b/login/app/sprinkles/account/templates/forms/settings-profile.html.twig @@ -0,0 +1,40 @@ +<form id="profile-settings" role="form" action="{{site.uri.public}}/account/settings/profile" method="post"> + <div class="box-header"> + <h3 class="box-title"><i class="fa fa-user fa-fw"></i> {{translate("PROFILE.SETTINGS")}}</h3> + </div> + <div class="box-body"> + {% include "forms/csrf.html.twig" %} + + {% block settings_profile %} + <label for="input-first-name" class="control-label">{{translate("NAME")}}</label> + <div class="row"> + <div class="col-sm-6"> + <div class="form-group"> + <input type="text" id="input-first-name" class="form-control" name="first_name" value="{{current_user.first_name}}" placeholder="{{translate("FIRST_NAME")}}" {{page.visibility}}> + </div> + </div> + <div class="col-sm-6"> + <div class="form-group"> + <input type="text" id="input-last-name" class="form-control" name="last_name" value="{{current_user.last_name}}" placeholder="{{translate("LAST_NAME")}}" {{page.visibility}}> + </div> + </div> + </div> + + <div class="form-group"> + <label for="input-locale" class="control-label">{{translate("LOCALE")}}</label> + <select id="input-locale" class="form-control js-select2" name="locale" {{page.visibility}}> + {% for option, label in locales %} + {% if label is not empty %} + <option value="{{option}}" {% if (option == current_user.locale) %}selected{% endif %}>{{label}}</option> + {% endif %} + {% endfor %} + </select> + <p class="help-block">{{translate("LOCALE.ACCOUNT")}}.</p> + </div> + {% endblock %} + </div> + <div class="box-footer text-center"> + <button type="reset" class="btn btn-default">{{translate('RESET')}}</button> + <button type="submit" class="btn btn-primary js-submit">{{translate('SAVE')}}</button> + </div> +</form> diff --git a/login/app/sprinkles/account/templates/mail/password-reset.html.twig b/login/app/sprinkles/account/templates/mail/password-reset.html.twig new file mode 100755 index 0000000..37096ce --- /dev/null +++ b/login/app/sprinkles/account/templates/mail/password-reset.html.twig @@ -0,0 +1,22 @@ +{% block subject %} + {{site.title}} - your password reset request +{% endblock %} + +{% block body %} +<p>Dear {{user.first_name}}, +</p> +<p> +A lost password request has been submitted for your account with {{site.title}} ({{site.uri.public}}) on {{request_date | date('m/d/Y g:i A')}}. +</p> +<p> +If you or someone you trust sent this request, and you wish to set a new password, please click this link: <a href="{{site.uri.public}}/account/set-password/confirm?token={{token}}">{{site.uri.public}}/account/set-password/confirm?token={{token}}</a> +</p> + +<p> +If you did <b>not</b> expect this email, you may click this link to cancel the request: <a href="{{site.uri.public}}/account/set-password/deny?token={{token}}">{{site.uri.public}}/account/set-password/deny?token={{token}}</a>, or simply do nothing and the request will expire on its own. +</p> +<p> +With regards,<br> +The {{site.title}} Team +</p> +{% endblock %}
\ No newline at end of file diff --git a/login/app/sprinkles/account/templates/mail/resend-verification.html.twig b/login/app/sprinkles/account/templates/mail/resend-verification.html.twig new file mode 100755 index 0000000..ba1c243 --- /dev/null +++ b/login/app/sprinkles/account/templates/mail/resend-verification.html.twig @@ -0,0 +1,17 @@ +{% block subject %} + {{site.title}} - verify your account +{% endblock %} + +{% block body %} +<p>Dear {{user.first_name}}, +</p> +<p> +We have received a new verification request for your account with {{site.title}} ({{site.uri.public}}). Please follow the link below to verify your account. If your account is already active, please disregard this message. +</p> +<a href="{{site.uri.public}}/account/verify?token={{token}}">{{site.uri.public}}/account/verify?token={{token}}</a> +</p> +<p> +With regards,<br> +The {{site.title}} Team +</p> +{% endblock %} diff --git a/login/app/sprinkles/account/templates/mail/verify-account.html.twig b/login/app/sprinkles/account/templates/mail/verify-account.html.twig new file mode 100755 index 0000000..aa342c7 --- /dev/null +++ b/login/app/sprinkles/account/templates/mail/verify-account.html.twig @@ -0,0 +1,21 @@ +{% block subject %} + Welcome to {{site.title}} - please verify your account +{% endblock %} + +{% block body %} +<p>Dear {{user.first_name}}, +</p> +<p> +You are receiving this email because you registered with {{site.title}} ({{site.uri.public}}). +</p> +<p> +You will need to verify your account before you can login. Please follow the link below to verify your account. +</p> +<p> +<a href="{{site.uri.public}}/account/verify?token={{token}}">{{site.uri.public}}/account/verify?token={{token}}</a> +</p> +<p> +With regards,<br> +The {{site.title}} Team +</p> +{% endblock %} diff --git a/login/app/sprinkles/account/templates/modals/tos.html.twig b/login/app/sprinkles/account/templates/modals/tos.html.twig new file mode 100755 index 0000000..d51d897 --- /dev/null +++ b/login/app/sprinkles/account/templates/modals/tos.html.twig @@ -0,0 +1,16 @@ +{% extends 'modals/modal.html.twig' %} + +{% block modal_title %} + {{translate("TOS_FOR", {title: site.title})}} +{% endblock %} + +{% block modal_body %} + <div class="text-left"> + {% include 'pages/partials/legal.html.twig' %} + {% include 'pages/partials/privacy.html.twig' %} + </div> +{% endblock %} + +{% block modal_footer %} + <button type="button" data-dismiss="modal" class="btn btn-primary btn-block">Got it!</button> +{% endblock %} diff --git a/login/app/sprinkles/account/templates/navigation/main-nav.html.twig b/login/app/sprinkles/account/templates/navigation/main-nav.html.twig new file mode 100755 index 0000000..e44c9c8 --- /dev/null +++ b/login/app/sprinkles/account/templates/navigation/main-nav.html.twig @@ -0,0 +1,13 @@ +{# This extend the same file from core to add a sign-up/sign-in or "my account" link to the "home page" nav menu. #} +{% extends "@core/navigation/main-nav.html.twig" %} + +{% block secondary_nav %} + {{parent()}} + {% if not checkAuthenticated() %} + <li> + <a href="{{site.uri.public}}/account/sign-in" class="nav-highlight">{{translate("SIGNIN")}}</a> + </li> + {% else %} + {% include "navigation/user-card.html.twig" %} + {% endif %} +{% endblock %} diff --git a/login/app/sprinkles/account/templates/navigation/user-card.html.twig b/login/app/sprinkles/account/templates/navigation/user-card.html.twig new file mode 100755 index 0000000..47e18f1 --- /dev/null +++ b/login/app/sprinkles/account/templates/navigation/user-card.html.twig @@ -0,0 +1,33 @@ +{% block userCard %} +<li class="dropdown user user-menu"> + {% block userCard_nav %} + <a href="#" class="dropdown-toggle" data-toggle="dropdown"> + <img src="{{ current_user.avatar }}" class="user-image" alt="User Image"> + <span class="hidden-xs">{{current_user.first_name}} {{current_user.last_name}}</span> + <i class="fa fa-chevron-down"></i> + </a> + {% endblock %} + <ul class="dropdown-menu"> + {% block userCard_userInfo %} + <!-- User image --> + <li class="user-header"> + <img src="{{ current_user.avatar }}" class="img-circle" alt="User Image"> + <p> + {{current_user.first_name}} {{current_user.last_name}} + <small>({{current_user.user_name}})</small> + </p> + </li> + {% endblock %} + + <!-- Menu Footer--> + <li class="user-footer"> + {% block userCard_menu %} + {% if checkAccess('uri_account_settings') %} + <a href="{{site.uri.public}}/account/settings" class="btn btn-default btn-flat btn-block">{{translate("ACCOUNT.MY")}}</a> + {% endif %} + <a href="{{site.uri.public}}/account/logout" class="btn btn-default btn-flat btn-block">{{translate("LOGOUT")}}</a> + {% endblock %} + </li> + </ul> +</li> +{% endblock %} diff --git a/login/app/sprinkles/account/templates/pages/account-settings.html.twig b/login/app/sprinkles/account/templates/pages/account-settings.html.twig new file mode 100755 index 0000000..61cd3d0 --- /dev/null +++ b/login/app/sprinkles/account/templates/pages/account-settings.html.twig @@ -0,0 +1,45 @@ +{% extends forcedLayout ? forcedLayout : "pages/abstract/default.html.twig" %} + +{% set page_active = "account-settings" %} + +{% block stylesheets_page %} + <!-- Page-specific CSS asset bundle --> + {{ assets.css('css/form-widgets') | raw }} +{% endblock %} + +{# Overrides blocks in head of base template #} +{% block page_title %}{{translate("ACCOUNT.SETTINGS")}}{% endblock %} + +{% block page_description %}{{translate("ACCOUNT.SETTINGS.DESCRIPTION")}}{% endblock %} + +{% block body_matter %} + + <div class="row"> + <div class="col-lg-6"> + {% block settings_profile_box %} + <div class="box box-primary"> + {% include "forms/settings-profile.html.twig" %} + </div> + {% endblock %} + </div> + <div class="col-lg-6"> + {% block settings_account_box %} + <div class="box box-primary"> + {% include "forms/settings-account.html.twig" %} + </div> + {% endblock %} + </div> + </div> +{% endblock %} +{% block scripts_page %} + <!-- Include validation rules --> + <script> + {% include "pages/partials/page.js.twig" %} + </script> + + <!-- Include form widgets JS --> + {{ assets.js('js/form-widgets') | raw }} + + <!-- Include page-specific JS --> + {{ assets.js('js/pages/account-settings') | raw }} +{% endblock %} diff --git a/login/app/sprinkles/account/templates/pages/error/compromised.html.twig b/login/app/sprinkles/account/templates/pages/error/compromised.html.twig new file mode 100755 index 0000000..6048619 --- /dev/null +++ b/login/app/sprinkles/account/templates/pages/error/compromised.html.twig @@ -0,0 +1,11 @@ +{% extends "pages/abstract/error.html.twig" %} + +{% block page_title %}{{ translate('ACCOUNT.SESSION_COMPROMISED.TITLE') }}{% endblock %} + +{% block page_description %}{{ translate('ACCOUNT.SESSION_COMPROMISED.TITLE') }}{% endblock %} + +{% block heading %} + <i class="fa fa-warning text-yellow"></i> {{ translate('ACCOUNT.SESSION_COMPROMISED.TEXT', { + 'url' : site.uri.public ~ '/account/sign-in' + }) | raw }} +{% endblock %} diff --git a/login/app/sprinkles/account/templates/pages/forgot-password.html.twig b/login/app/sprinkles/account/templates/pages/forgot-password.html.twig new file mode 100755 index 0000000..72b1a2a --- /dev/null +++ b/login/app/sprinkles/account/templates/pages/forgot-password.html.twig @@ -0,0 +1,46 @@ +{% extends "pages/abstract/base.html.twig" %} + +{# Overrides blocks in head of base template #} +{% block page_title %}{{translate("PASSWORD.FORGOTTEN")}}{% endblock %} + +{% block page_description %}{{translate("PASSWORD.FORGET.PAGE")}}{% endblock %} + +{% block body_attributes %} + class="hold-transition login-page" +{% endblock %} + +{% block content %} +<div class="login-box"> + <div class="login-logo"> + <a href="{{site.uri.public}}">{{site.title}}</a> + </div> + <!-- /.login-logo --> + + <div class="login-box-body login-form"> + <p class="login-box-msg"><strong>{{translate("PASSWORD.FORGOTTEN")}}</strong></p> + <p class="login-box-msg">{{translate("PASSWORD.FORGET.EMAIL")}}</p> + + <div class="form-alerts" id="alerts-page"></div> + + <form id="request-password-reset" role="form" action="{{site.uri.public}}/account/forgot-password" method="post" class="r-form"> + {% include "forms/csrf.html.twig" %} + <div class="form-group"> + <label class="sr-only" for="reset-form-email">{{translate("EMAIL")}}</label> + <input type="text" name="email" placeholder="{{translate("EMAIL")}}" class="form-control" id="reset-form-email"> + </div> + <button type="submit" class="btn btn-block btn-primary">{{translate("PASSWORD.FORGET.EMAIL_SEND")}}</button> + </form> + </div> + <!-- /.login-box-body --> +{% endblock %} + +{% block scripts_page %} + <!-- Include validation rules --> + <script> + {% include "pages/partials/page.js.twig" %} + </script> + + <!-- Include page-specific JS bundle --> + {{ assets.js('js/pages/forgot-password') | raw }} + +{% endblock %} diff --git a/login/app/sprinkles/account/templates/pages/register.html.twig b/login/app/sprinkles/account/templates/pages/register.html.twig new file mode 100755 index 0000000..bd155ba --- /dev/null +++ b/login/app/sprinkles/account/templates/pages/register.html.twig @@ -0,0 +1,105 @@ +{% extends "pages/abstract/base.html.twig" %} + +{# Overrides blocks in head of base template #} +{% block page_title %}{{translate('REGISTER')}}{% endblock %} + +{% block page_description %}{{translate('PAGE.LOGIN.DESCRIPTION', {'site_name': site.title })}}{% endblock %} + +{% block body_attributes %} + class="hold-transition login-page" +{% endblock %} + +{% block content %} +<div class="login-box"> + <div class="login-logo"> + <a href="{{site.uri.public}}">{{site.title}}</a> + </div> + <!-- /.login-logo --> + + <div class="login-box-body register-form"> + <p class="login-box-msg"><strong>{{translate('REGISTER')}}</strong></p> + <div class="form-alerts" id="alerts-page"></div> + + <form id="register" role="form" action="{{site.uri.public}}/account/register" method="post" class="r-form"> + {% include "forms/csrf.html.twig" %} + <label for="r-form-first-name">{{translate('NAME_AND_EMAIL')}}</label> + <div class="row"> + <div class="col-md-6"> + <div class="form-group"> + <label class="sr-only" for="r-form-first-name">{{translate('FIRST_NAME')}}</label> + <input type="text" name="first_name" placeholder="{{translate('FIRST_NAME')}}" class="form-control" id="r-form-first-name" autocomplete="off"> + </div> + </div> + <div class="col-md-6"> + <div class="form-group"> + <label class="sr-only" for="r-form-last-name">{{translate('LAST_NAME')}}</label> + <input type="text" name="last_name" placeholder="{{translate('LAST_NAME')}}" class="form-control" id="r-form-last-name" autocomplete="off"> + </div> + </div> + </div> + <div class="form-group"> + <input type="text" name="email" placeholder="{% if site.registration.require_email_verification %}{{translate('EMAIL.VERIFICATION_REQUIRED')}}{% else %}{{translate('EMAIL.YOUR')}}{% endif %}" class="form-control" id="r-form-email"> + </div> + <div class="form-group"> + <label for="r-form-username">{{translate('USERNAME')}}</label> + <span class="pull-right"><a href="#" id="form-register-username-suggest">[{{translate('SUGGEST')}}]</a></span> + <input type="text" name="user_name" placeholder="{{translate('USERNAME.CHOOSE')}}" class="form-control" id="r-form-username" autocomplete="off"> + </div> + <div class="form-group"> + <label for="r-form-password">{{translate('PASSWORD')}}</label> + <input type="password" name="password" placeholder="{{translate('PASSWORD.BETWEEN', {min: 12, max: 100})}}" class="form-control" id="r-form-password"> + </div> + <div class="form-group"> + <label class="sr-only" for="r-form-passwordc">{{translate('PASSWORD.CONFIRM')}}</label> + <input type="password" name="passwordc" placeholder="{{translate('PASSWORD.CONFIRM')}}" class="form-control" id="r-form-passwordc"> + </div> + {% if site.registration.captcha %} + <div class="form-group"> + <label class="sr-only" for="r-form-passwordc">{{translate('CAPTCHA.VERIFY')}}</label> + <div class="row"> + <div class="col-md-6"> + <input type="text" name="captcha" placeholder="{{translate('CAPTCHA.SPECIFY')}}" class="form-control" id="r-form-captcha"> + </div> + <div class="col-md-6 form-col-captcha"> + <img src="{{site.uri.public}}/account/captcha" id="captcha" data-target="#r-form-captcha"> + </div> + </div> + </div> + {% endif %} + <div class="collapse"> + <label>Spiderbro: Don't change me bro, I'm tryin'a catch some flies!</label> + <input name="spiderbro" id="spiderbro" value="http://"/> + </div> + <div class="text-left"> + <p> + {{translate('TOS_AGREEMENT', { + 'site_title' : site.title, + 'link_attributes' : 'class="js-show-tos" href="#" data-toggle="modal"' + }) | raw}} + </p> + </div> + <div> + <button type="submit" class="btn btn-block btn-primary">{{translate('REGISTER_ME')}}</button> + </div> + <div style="padding-top: 10px;"> + {{translate('SIGN_IN_HERE', { + 'url' : site.uri.public ~'/account/sign-in' + }) | raw}} + </div> + </form> + </div> + <!-- /.login-box-body --> + +</div> +<!-- /.login-box --> +{% endblock %} + +{% block scripts_page %} + <!-- Include validation rules --> + <script> + {% include "pages/partials/page.js.twig" %} + </script> + + <!-- Include page-specific JS --> + {{ assets.js('js/pages/register') | raw }} +{% endblock %} diff --git a/login/app/sprinkles/account/templates/pages/resend-verification.html.twig b/login/app/sprinkles/account/templates/pages/resend-verification.html.twig new file mode 100755 index 0000000..627dce0 --- /dev/null +++ b/login/app/sprinkles/account/templates/pages/resend-verification.html.twig @@ -0,0 +1,46 @@ +{% extends "pages/abstract/base.html.twig" %} + +{# Overrides blocks in head of base template #} +{% block page_title %}{{translate("ACCOUNT.VERIFICATION.RESEND")}}{% endblock %} + +{% block page_description %}{{translate("ACCOUNT.VERIFICATION.PAGE")}}{% endblock %} + +{% block body_attributes %} + class="hold-transition login-page" +{% endblock %} + +{% block content %} +<div class="login-box"> + <div class="login-logo"> + <a href="{{site.uri.public}}">{{site.title}}</a> + </div> + <!-- /.login-logo --> + + <div class="login-box-body login-form"> + <p class="login-box-msg"><strong>{{translate("ACCOUNT.VERIFICATION.RESEND")}}</strong></p> + <p class="login-box-msg">{{translate("ACCOUNT.VERIFICATION.EMAIL")}}</p> + + <div class="form-alerts" id="alerts-page"></div> + + <form id="request-verification-email" role="form" action="{{site.uri.public}}/account/resend-verification" method="post" class="r-form"> + {% include "forms/csrf.html.twig" %} + <div class="form-group"> + <label class="sr-only" for="verification-form-email">{{translate("EMAIL")}}</label> + <input type="text" name="email" placeholder="{{translate("EMAIL")}}" class="form-control" id="verification-form-email"> + </div> + <button type="submit" class="btn btn-block btn-primary">{{translate("ACCOUNT.VERIFICATION.SEND")}}</button> + </form> + </div> + <!-- /.login-box-body --> +{% endblock %} + +{% block scripts_page %} + <!-- Include validation rules --> + <script> + {% include "pages/partials/page.js.twig" %} + </script> + + <!-- Include page-specific JS --> + {{ assets.js('js/pages/resend-verification') | raw }} + +{% endblock %} diff --git a/login/app/sprinkles/account/templates/pages/reset-password.html.twig b/login/app/sprinkles/account/templates/pages/reset-password.html.twig new file mode 100755 index 0000000..8e3a24a --- /dev/null +++ b/login/app/sprinkles/account/templates/pages/reset-password.html.twig @@ -0,0 +1,56 @@ +{% extends "pages/abstract/base.html.twig" %} + +{# Overrides blocks in head of base template #} +{% block page_title %}{{translate("PASSWORD.RESET")}}{% endblock %} + +{% block page_description %}{{translate("PASSWORD.RESET.PAGE")}}{% endblock %} + +{% block body_attributes %} + class="hold-transition login-page" +{% endblock %} + +{% block content %} +<div class="login-box"> + <div class="login-logo"> + <a href="{{site.uri.public}}">{{site.title}}</a> + </div> + <!-- /.login-logo --> + + <div class="login-box-body login-form"> + <p class="login-box-msg"><strong>{{translate("PASSWORD.RESET")}}</strong></p> + <p class="login-box-msg">{{translate("PASSWORD.RESET.CHOOSE")}}</p> + + <div class="form-alerts" id="alerts-page"></div> + + <form id="set-or-reset-password" role="form" action="{{site.uri.public}}/account/set-password" method="post" class="r-form"> + {% include "forms/csrf.html.twig" %} + {# Prevent browsers from trying to autofill the password field. See http://stackoverflow.com/a/23234498/2970321 #} + <input type="text" style="display:none"> + <input type="password" style="display:none"> + + <div class="form-group"> + <label class="sr-only" for="form-password">{{translate("PASSWORD.NEW")}}</label> + <input type="password" name="password" placeholder="{{translate("PASSWORD.BETWEEN", {min: 12, max: 100})}}" class="form-control" id="form-password"> + </div> + + <div class="form-group"> + <label class="sr-only" for="form-passwordc">{{translate("PASSWORD.CONFIRM_NEW")}}</label> + <input type="password" name="passwordc" placeholder="{{translate("PASSWORD.CONFIRM_NEW_EXPLAIN")}}" class="form-control" id="form-passwordc"> + </div> + <input type="hidden" name="token" value="{{token}}"> + <button type="submit" class="btn btn-block btn-primary">{{translate("PASSWORD.RESET.SEND")}}</button> + </form> + </div> + <!-- /.login-box-body --> +{% endblock %} + +{% block scripts_page %} + <!-- Include validation rules --> + <script> + {% include "pages/partials/page.js.twig" %} + </script> + + <!-- Include page-specific JS bundle --> + {{ assets.js('js/pages/set-or-reset-password') | raw }} + +{% endblock %} diff --git a/login/app/sprinkles/account/templates/pages/set-password.html.twig b/login/app/sprinkles/account/templates/pages/set-password.html.twig new file mode 100755 index 0000000..3c4fe2b --- /dev/null +++ b/login/app/sprinkles/account/templates/pages/set-password.html.twig @@ -0,0 +1,55 @@ +{% extends "pages/abstract/base.html.twig" %} + +{# Overrides blocks in head of base template #} +{% block page_title %}{{translate("PASSWORD.CREATE")}}{% endblock %} + +{% block page_description %}{{translate("PASSWORD.CREATE.PAGE")}}{% endblock %} + +{% block body_attributes %} + class="hold-transition login-page" +{% endblock %} + +{% block content %} +<div class="login-box"> + <div class="login-logo"> + <a href="{{site.uri.public}}">{{site.title}}</a> + </div> + <!-- /.login-logo --> + + <div class="login-box-body login-form"> + <p class="login-box-msg"><strong>{{translate("PASSWORD.CREATE")}}</strong></p> + <p class="login-box-msg">{{translate("WELCOME_TO", {'title': site.title})}} {{translate("PASSWORD.CREATE.PAGE")}}</p> + + <div class="form-alerts" id="alerts-page"></div> + + <form id="set-or-reset-password" role="form" action="{{site.uri.public}}/account/set-password" method="post" class="r-form"> + {% include "forms/csrf.html.twig" %} + {# Prevent browsers from trying to autofill the password field. See http://stackoverflow.com/a/23234498/2970321 #} + <input type="text" style="display:none"> + <input type="password" style="display:none"> + + <div class="form-group"> + <label class="sr-only" for="form-password">{{translate('PASSWORD')}}</label> + <input type="password" name="password" placeholder="{{translate('PASSWORD.BETWEEN', {min: 12, max: 100})}}" class="form-control" id="form-password"> + </div> + <div class="form-group"> + <label class="sr-only" for="form-passwordc">{{translate('PASSWORD.CONFIRM')}}</label> + <input type="password" name="passwordc" placeholder="{{translate('PASSWORD.CONFIRM')}}" class="form-control" id="form-passwordc"> + </div> + <input type="hidden" name="token" value="{{token}}"> + <button type="submit" class="btn btn-block btn-primary">{{translate('PASSWORD.CREATE.SET')}}</button> + </form> + </div> + <!-- /.login-box-body --> +{% endblock %} + +{% block scripts_page %} + <!-- Include validation rules --> + <script> + {% include "pages/partials/page.js.twig" %} + </script> + + <!-- Include page-specific JS bundle --> + {{ assets.js('js/pages/set-or-reset-password') | raw }} + +{% endblock %}
\ No newline at end of file diff --git a/login/app/sprinkles/account/templates/pages/sign-in.html.twig b/login/app/sprinkles/account/templates/pages/sign-in.html.twig new file mode 100755 index 0000000..2fb6e1c --- /dev/null +++ b/login/app/sprinkles/account/templates/pages/sign-in.html.twig @@ -0,0 +1,84 @@ +{% extends "pages/abstract/base.html.twig" %} + +{# Overrides blocks in head of base template #} +{% block page_title %}{{translate('SIGNIN')}}{% endblock %} + +{% block page_description %}{{translate('PAGE.LOGIN.DESCRIPTION', {'site_name': site.title })}}{% endblock %} + +{% block body_attributes %} + class="hold-transition login-page" +{% endblock %} + +{% block content %} +<div class="login-box"> + <div class="login-logo"> + <a href="{{site.uri.public}}">{{site.title}}</a> + </div> + <!-- /.login-logo --> + + <div class="login-box-body login-form"> + <p class="login-box-msg"><strong>{{translate('SIGNIN')}}</strong></p> + + <div class="form-alerts" id="alerts-page"></div> + + <form action="{{site.uri.public}}/account/login" id="sign-in" method="post"> + {% include "forms/csrf.html.twig" %} + <div class="form-group has-feedback"> + <input type="text" class="form-control" placeholder="{% if site.login.enable_email %}{{translate('EMAIL_OR_USERNAME')}}{% else %}{{translate('USERNAME')}}{% endif %}" name="user_name"> + <i class="glyphicon glyphicon-user form-control-icon" aria-hidden="true"></i> + </div> + <div class="form-group has-feedback"> + <input type="password" class="form-control" placeholder="{{translate('PASSWORD')}}" name="password"> + <i class="glyphicon glyphicon-lock form-control-icon" aria-hidden="true"></i> + </div> + <div class="row"> + <div class="col-xs-8"> + <div class="checkbox icheck"> + <label> + <input type="checkbox" class="js-icheck" name="rememberme"> {{translate('REMEMBER_ME')}} + </label> + </div> + </div> + <!-- /.col --> + <div class="col-xs-4"> + <button type="submit" class="btn btn-primary btn-block btn-flat">{{translate('LOGIN')}}</button> + </div> + <!-- /.col --> + </div> + </form> + + <a href="{{site.uri.public}}/account/forgot-password">{{translate('PASSWORD.FORGET')}}</a><br> + {% if site.registration.require_email_verification %} + <a href="{{site.uri.public}}/account/resend-verification">{{translate('ACCOUNT.VERIFICATION.RESEND')}}</a><br> + {% endif %} + {% if site.registration.enabled %} + <a href="{{site.uri.public}}/account/register">{{translate('REGISTER')}}</a> + {% endif %} + + </div> + <!-- /.login-box-body --> +</div> +<!-- /.login-box --> +{% endblock %} + +{% block scripts_page %} + <!-- Include validation rules --> + <script> + {% include "pages/partials/page.js.twig" %} + </script> + + <script> + site = $.extend( + true, // deep extend + { + "registration" : { + "enabled" : "{{site.registration.enabled}}" + } + }, + site + ); + </script> + + <!-- Include page-specific JS --> + {{ assets.js('js/pages/sign-in') | raw }} +{% endblock %} diff --git a/login/app/sprinkles/account/tests/Unit/FactoriesTest.php b/login/app/sprinkles/account/tests/Unit/FactoriesTest.php new file mode 100755 index 0000000..ee2bf23 --- /dev/null +++ b/login/app/sprinkles/account/tests/Unit/FactoriesTest.php @@ -0,0 +1,30 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Tests\Unit; + +use UserFrosting\Tests\TestCase; +use UserFrosting\Tests\DatabaseTransactions; + +/** + * FactoriesTest class. + * Tests the factories defined in this sprinkle are working + * + * @extends TestCase + */ +class FactoriesTest extends TestCase +{ + use DatabaseTransactions; + + function testUserFactory() + { + $fm = $this->ci->factory; + + $user = $fm->create('UserFrosting\Sprinkle\Account\Database\Models\User'); + $this->assertInstanceOf('UserFrosting\Sprinkle\Account\Database\Models\User', $user); + } +} diff --git a/login/app/sprinkles/account/tests/Unit/HasherTest.php b/login/app/sprinkles/account/tests/Unit/HasherTest.php new file mode 100755 index 0000000..711e3cb --- /dev/null +++ b/login/app/sprinkles/account/tests/Unit/HasherTest.php @@ -0,0 +1,71 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Tests\Unit; + +use UserFrosting\Sprinkle\Account\Authenticate\Hasher; +use UserFrosting\Tests\TestCase; + +/** + * Tests the password Hasher class. + * + * @extends TestCase + */ +class HasherTest extends TestCase +{ + protected $plainText = 'hodleth'; + + /** + * @var string Legacy hash from UserCake (sha1) + */ + protected $userCakeHash = '87e995bde9ebdc73fc58cc75a9fadc4ae630d8207650fbe94e148ccc8058d5de5'; + + /** + * @var string Legacy hash from UF 0.1.x + */ + protected $legacyHash = '$2y$12$rsXGznS5Ky23lX9iNzApAuDccKRhQFkiy5QfKWp0ACyDWBPOylPB.rsXGznS5Ky23lX9iNzApA9'; + + /** + * @var string Modern hash that uses password_hash() + */ + protected $modernHash = '$2y$10$ucxLwloFso6wJoct1baBQefdrttws/taEYvavi6qoPsw/vd1u4Mha'; + + public function testGetHashType() + { + $hasher = new Hasher; + + $type = $hasher->getHashType($this->modernHash); + + $this->assertEquals('modern', $type); + + $type = $hasher->getHashType($this->legacyHash); + + $this->assertEquals('legacy', $type); + + $type = $hasher->getHashType($this->userCakeHash); + + $this->assertEquals('sha1', $type); + } + + public function testVerify() + { + $hasher = new Hasher; + + $this->assertTrue($hasher->verify($this->plainText, $this->modernHash)); + $this->assertTrue($hasher->verify($this->plainText, $this->legacyHash)); + $this->assertTrue($hasher->verify($this->plainText, $this->userCakeHash)); + } + + public function testVerifyReject() + { + $hasher = new Hasher; + + $this->assertFalse($hasher->verify('selleth', $this->modernHash)); + $this->assertFalse($hasher->verify('selleth', $this->legacyHash)); + $this->assertFalse($hasher->verify('selleth', $this->userCakeHash)); + } +} diff --git a/login/app/sprinkles/admin/asset-bundles.json b/login/app/sprinkles/admin/asset-bundles.json new file mode 100755 index 0000000..1bc1706 --- /dev/null +++ b/login/app/sprinkles/admin/asset-bundles.json @@ -0,0 +1,170 @@ +{ + "bundle": { + "js/admin": { + "scripts": [ + "vendor/moment/moment.js", + "userfrosting/js/handlebars-helpers.js", + "vendor/tablesorter/dist/js/jquery.tablesorter.js", + "vendor/tablesorter/dist/js/jquery.tablesorter.widgets.js", + "userfrosting/js/tablesorter/widget-sort2Hash.js", + "vendor/tablesorter/dist/js/widgets/widget-columnSelector.min.js", + "vendor/tablesorter/dist/js/widgets/widget-reflow.min.js", + "vendor/tablesorter/dist/js/widgets/widget-pager.min.js", + "userfrosting/js/query-string.js", + "userfrosting/js/uf-table.js" + ], + "options": { + "result": { + "type": { + "scripts": "plain" + } + } + } + }, + "js/pages/activities": { + "scripts": [ + "userfrosting/js/pages/activities.js" + ], + "options": { + "result": { + "type": { + "scripts": "plain" + } + } + } + }, + "js/pages/dashboard": { + "scripts": [ + "userfrosting/js/widgets/users.js", + "userfrosting/js/pages/dashboard.js" + ], + "options": { + "result": { + "type": { + "scripts": "plain" + } + } + } + }, + "js/pages/group": { + "scripts": [ + "userfrosting/js/widgets/users.js", + "userfrosting/js/widgets/groups.js", + "userfrosting/js/pages/group.js" + ], + "options": { + "result": { + "type": { + "scripts": "plain" + } + } + } + }, + "js/pages/permission": { + "scripts": [ + "userfrosting/js/widgets/users.js", + "userfrosting/js/pages/permission.js" + ], + "options": { + "result": { + "type": { + "scripts": "plain" + } + } + } + }, + "js/pages/role": { + "scripts": [ + "userfrosting/js/widgets/roles.js", + "userfrosting/js/pages/role.js" + ], + "options": { + "result": { + "type": { + "scripts": "plain" + } + } + } + }, + "js/pages/user": { + "scripts": [ + "userfrosting/js/widgets/users.js", + "userfrosting/js/pages/user.js" + ], + "options": { + "result": { + "type": { + "scripts": "plain" + } + } + } + }, + "js/pages/users": { + "scripts": [ + "userfrosting/js/widgets/users.js", + "userfrosting/js/pages/users.js" + ], + "options": { + "result": { + "type": { + "scripts": "plain" + } + } + } + }, + "js/pages/groups": { + "scripts": [ + "userfrosting/js/widgets/groups.js", + "userfrosting/js/pages/groups.js" + ], + "options": { + "result": { + "type": { + "scripts": "plain" + } + } + } + }, + "js/pages/permissions": { + "scripts": [ + "userfrosting/js/pages/permissions.js" + ], + "options": { + "result": { + "type": { + "scripts": "plain" + } + } + } + }, + "js/pages/roles": { + "scripts": [ + "userfrosting/js/widgets/roles.js", + "userfrosting/js/pages/roles.js" + ], + "options": { + "result": { + "type": { + "scripts": "plain" + } + } + } + }, + "css/admin": { + "styles": [ + "font-starcraft/css/font-starcraft.css", + "vendor/tablesorter/dist/css/theme.bootstrap.min.css", + "vendor/tablesorter/dist/css/jquery.tablesorter.pager.min.css", + "userfrosting/css/tablesorter-reflow.css", + "userfrosting/css/tablesorter-custom.css" + ], + "options": { + "result": { + "type": { + "styles": "plain" + } + } + } + } + } +}
\ No newline at end of file diff --git a/login/app/sprinkles/admin/assets/userfrosting/css/tablesorter-custom.css b/login/app/sprinkles/admin/assets/userfrosting/css/tablesorter-custom.css new file mode 100755 index 0000000..405c1ca --- /dev/null +++ b/login/app/sprinkles/admin/assets/userfrosting/css/tablesorter-custom.css @@ -0,0 +1,30 @@ +/* ========================================================================== + Custom styling for tablesorter tables + ========================================================================== */ + +/* Custom styling for pager controls + ========================================================================== */ + +.pager-lg { + font-size: large; +} + +.pager-control { + font-size: x-large; + padding: 4px 8px; + cursor: pointer; +} + +/* Custom styling for panels that contain buttons in the header (eg, CSV download buttons) + ========================================================================== */ + +.panel-heading-buttons h3 { + padding-top: 7.5px; +} + +/* Don't display tablesorter filter field when disabled + ========================================================================== */ + +.tablesorter thead .disabled { + display: none +}
\ No newline at end of file diff --git a/login/app/sprinkles/admin/assets/userfrosting/js/pages/activities.js b/login/app/sprinkles/admin/assets/userfrosting/js/pages/activities.js new file mode 100755 index 0000000..89ac5c1 --- /dev/null +++ b/login/app/sprinkles/admin/assets/userfrosting/js/pages/activities.js @@ -0,0 +1,16 @@ +/** + * Page-specific Javascript file. Should generally be included as a separate asset bundle in your page template. + * example: {{ assets.js('js/pages/sign-in-or-register') | raw }} + * + * This script depends on uf-table.js, moment.js, handlebars-helpers.js + * + * Target page: /activities + */ + +$(document).ready(function() { + // Set up table of activities + $("#widget-activities").ufTable({ + dataUrl: site.uri.public + "/api/activities", + useLoadingTransition: site.uf_table.use_loading_transition + }); +}); diff --git a/login/app/sprinkles/admin/assets/userfrosting/js/pages/dashboard.js b/login/app/sprinkles/admin/assets/userfrosting/js/pages/dashboard.js new file mode 100755 index 0000000..f2b8a4f --- /dev/null +++ b/login/app/sprinkles/admin/assets/userfrosting/js/pages/dashboard.js @@ -0,0 +1,49 @@ +/** + * Page-specific Javascript file. Should generally be included as a separate asset bundle in your page template. + * example: {{ assets.js('js/pages/sign-in-or-register') | raw }} + * + * Target page: /dashboard + */ + +$(document).ready(function() { + $('.js-clear-cache').click(function() { + $("body").ufModal({ + sourceUrl: site.uri.public + "/modals/dashboard/clear-cache", + ajaxParams: { + slug: $(this).data('slug') + }, + msgTarget: $("#alerts-page") + }); + + $("body").on('renderSuccess.ufModal', function (data) { + var modal = $(this).ufModal('getModal'); + var form = modal.find('.js-form'); + + form.ufForm() + .on("submitSuccess.ufForm", function() { + // Reload page on success + window.location.reload(); + }); + }); + }); + + // Table of site activities + $("#widget-activities").ufTable({ + dataUrl: site.uri.public + "/api/activities", + useLoadingTransition: site.uf_table.use_loading_transition + }); + + // Table of users in current user's group + $("#widget-group-users").ufTable({ + dataUrl: site.uri.public + "/api/groups/g/" + page.group_slug + "/users", + useLoadingTransition: site.uf_table.use_loading_transition + }); + + // Bind user creation button + bindUserCreationButton($("#widget-group-users")); + + // Bind user table buttons + $("#widget-group-users").on("pagerComplete.ufTable", function () { + bindUserButtons($(this)); + }); +}); diff --git a/login/app/sprinkles/admin/assets/userfrosting/js/pages/group.js b/login/app/sprinkles/admin/assets/userfrosting/js/pages/group.js new file mode 100755 index 0000000..a1ca959 --- /dev/null +++ b/login/app/sprinkles/admin/assets/userfrosting/js/pages/group.js @@ -0,0 +1,24 @@ +/** + * Page-specific Javascript file. Should generally be included as a separate asset bundle in your page template. + * example: {{ assets.js('js/pages/sign-in-or-register') | raw }} + * + * This script depends on uf-table.js, moment.js, handlebars-helpers.js + * + * Target page: /groups/g/{slug} + */ + +$(document).ready(function() { + // Control buttons + bindGroupButtons($("#view-group")); + + // Table of users in this group + $("#widget-group-users").ufTable({ + dataUrl: site.uri.public + '/api/groups/g/' + page.group_slug + '/users', + useLoadingTransition: site.uf_table.use_loading_transition + }); + + // Bind user table buttons + $("#widget-group-users").on("pagerComplete.ufTable", function () { + bindUserButtons($(this)); + }); +}); diff --git a/login/app/sprinkles/admin/assets/userfrosting/js/pages/groups.js b/login/app/sprinkles/admin/assets/userfrosting/js/pages/groups.js new file mode 100755 index 0000000..0bfc65a --- /dev/null +++ b/login/app/sprinkles/admin/assets/userfrosting/js/pages/groups.js @@ -0,0 +1,24 @@ +/** + * Page-specific Javascript file. Should generally be included as a separate asset bundle in your page template. + * example: {{ assets.js('js/pages/sign-in-or-register') | raw }} + * + * This script depends on widgets/groups.js, uf-table.js, moment.js, handlebars-helpers.js + * + * Target page: /groups + */ + +$(document).ready(function() { + + $("#widget-groups").ufTable({ + dataUrl: site.uri.public + "/api/groups", + useLoadingTransition: site.uf_table.use_loading_transition + }); + + // Bind creation button + bindGroupCreationButton($("#widget-groups")); + + // Bind table buttons + $("#widget-groups").on("pagerComplete.ufTable", function () { + bindGroupButtons($(this)); + }); +}); diff --git a/login/app/sprinkles/admin/assets/userfrosting/js/pages/permission.js b/login/app/sprinkles/admin/assets/userfrosting/js/pages/permission.js new file mode 100755 index 0000000..87e851f --- /dev/null +++ b/login/app/sprinkles/admin/assets/userfrosting/js/pages/permission.js @@ -0,0 +1,20 @@ +/** + * Page-specific Javascript file. Should generally be included as a separate asset bundle in your page template. + * example: {{ assets.js('js/pages/sign-in-or-register') | raw }} + * + * This script depends on uf-table.js, moment.js, handlebars-helpers.js + * + * Target page: /permissions/p/{id} + */ + +$(document).ready(function() { + $("#widget-permission-users").ufTable({ + dataUrl: site.uri.public + '/api/permissions/p/' + page.permission_id + '/users', + useLoadingTransition: site.uf_table.use_loading_transition + }); + + // Bind table buttons + $("#widget-permission-users").on("pagerComplete.ufTable", function () { + bindUserButtons($(this)); + }); +}); diff --git a/login/app/sprinkles/admin/assets/userfrosting/js/pages/permissions.js b/login/app/sprinkles/admin/assets/userfrosting/js/pages/permissions.js new file mode 100755 index 0000000..6266ff4 --- /dev/null +++ b/login/app/sprinkles/admin/assets/userfrosting/js/pages/permissions.js @@ -0,0 +1,16 @@ +/** + * Page-specific Javascript file. Should generally be included as a separate asset bundle in your page template. + * example: {{ assets.js('js/pages/sign-in-or-register') | raw }} + * + * This script depends on widgets/permissions.js, uf-table.js, moment.js, handlebars-helpers.js + * + * Target page: /permissions + */ + +$(document).ready(function() { + // Set up table of permissions + $("#widget-permissions").ufTable({ + dataUrl: site.uri.public + "/api/permissions", + useLoadingTransition: site.uf_table.use_loading_transition + }); +}); diff --git a/login/app/sprinkles/admin/assets/userfrosting/js/pages/role.js b/login/app/sprinkles/admin/assets/userfrosting/js/pages/role.js new file mode 100755 index 0000000..8dae7f5 --- /dev/null +++ b/login/app/sprinkles/admin/assets/userfrosting/js/pages/role.js @@ -0,0 +1,23 @@ +/** + * Page-specific Javascript file. Should generally be included as a separate asset bundle in your page template. + * example: {{ assets.js('js/pages/sign-in-or-register') | raw }} + * + * This script depends on uf-table.js, moment.js, handlebars-helpers.js + * + * Target page: /roles/r/{slug} + */ + +$(document).ready(function() { + // Control buttons + bindRoleButtons($("#view-role")); + + $("#widget-role-permissions").ufTable({ + dataUrl: site.uri.public + '/api/roles/r/' + page.role_slug + '/permissions', + useLoadingTransition: site.uf_table.use_loading_transition + }); + + $("#widget-role-users").ufTable({ + dataUrl: site.uri.public + '/api/roles/r/' + page.role_slug + '/users', + useLoadingTransition: site.uf_table.use_loading_transition + }); +}); diff --git a/login/app/sprinkles/admin/assets/userfrosting/js/pages/roles.js b/login/app/sprinkles/admin/assets/userfrosting/js/pages/roles.js new file mode 100755 index 0000000..b1febb2 --- /dev/null +++ b/login/app/sprinkles/admin/assets/userfrosting/js/pages/roles.js @@ -0,0 +1,24 @@ +/** + * Page-specific Javascript file. Should generally be included as a separate asset bundle in your page template. + * example: {{ assets.js('js/pages/sign-in-or-register') | raw }} + * + * This script depends on widgets/roles.js, uf-table.js, moment.js, handlebars-helpers.js + * + * Target page: /roles + */ + +$(document).ready(function() { + // Set up table of roles + $("#widget-roles").ufTable({ + dataUrl: site.uri.public + "/api/roles", + useLoadingTransition: site.uf_table.use_loading_transition + }); + + // Bind creation button + bindRoleCreationButton($("#widget-roles")); + + // Bind table buttons + $("#widget-roles").on("pagerComplete.ufTable", function () { + bindRoleButtons($(this)); + }); +}); diff --git a/login/app/sprinkles/admin/assets/userfrosting/js/pages/user.js b/login/app/sprinkles/admin/assets/userfrosting/js/pages/user.js new file mode 100755 index 0000000..70acf7c --- /dev/null +++ b/login/app/sprinkles/admin/assets/userfrosting/js/pages/user.js @@ -0,0 +1,25 @@ +/** + * Page-specific Javascript file. Should generally be included as a separate asset bundle in your page template. + * example: {{ assets.js('js/pages/sign-in-or-register') | raw }} + * + * This script depends on uf-table.js, moment.js, handlebars-helpers.js + * + * Target page: /users/u/{user_name} + */ + +$(document).ready(function() { + // Control buttons + bindUserButtons($("#view-user")); + + // Table of activities + $("#widget-user-activities").ufTable({ + dataUrl: site.uri.public + '/api/users/u/' + page.user_name + '/activities', + useLoadingTransition: site.uf_table.use_loading_transition + }); + + // Table of permissions + $("#widget-permissions").ufTable({ + dataUrl: site.uri.public + '/api/users/u/' + page.user_name + '/permissions', + useLoadingTransition: site.uf_table.use_loading_transition + }); +}); diff --git a/login/app/sprinkles/admin/assets/userfrosting/js/pages/users.js b/login/app/sprinkles/admin/assets/userfrosting/js/pages/users.js new file mode 100755 index 0000000..d9e4bb7 --- /dev/null +++ b/login/app/sprinkles/admin/assets/userfrosting/js/pages/users.js @@ -0,0 +1,24 @@ +/** + * Page-specific Javascript file. Should generally be included as a separate asset bundle in your page template. + * example: {{ assets.js('js/pages/sign-in-or-register') | raw }} + * + * This script depends on widgets/users.js, uf-table.js, moment.js, handlebars-helpers.js + * + * Target page: /users + */ + +$(document).ready(function() { + // Set up table of users + $("#widget-users").ufTable({ + dataUrl: site.uri.public + "/api/users", + useLoadingTransition: site.uf_table.use_loading_transition + }); + + // Bind creation button + bindUserCreationButton($("#widget-users")); + + // Bind table buttons + $("#widget-users").on("pagerComplete.ufTable", function () { + bindUserButtons($(this)); + }); +}); diff --git a/login/app/sprinkles/admin/assets/userfrosting/js/widgets/groups.js b/login/app/sprinkles/admin/assets/userfrosting/js/widgets/groups.js new file mode 100755 index 0000000..d701d81 --- /dev/null +++ b/login/app/sprinkles/admin/assets/userfrosting/js/widgets/groups.js @@ -0,0 +1,111 @@ +/** + * Groups widget. Sets up dropdowns, modals, etc for a table of groups. + */ + +/** + * Set up the form in a modal after being successfully attached to the body. + */ +function attachGroupForm() { + $("body").on('renderSuccess.ufModal', function (data) { + var modal = $(this).ufModal('getModal'); + var form = modal.find('.js-form'); + + /** + * Set up modal widgets + */ + // Set up any widgets inside the modal + form.find(".js-select2").select2({ + width: '100%' + }); + + // Auto-generate slug + form.find('input[name=name]').on('input change', function() { + var manualSlug = form.find('#form-group-slug-override').prop('checked'); + if (!manualSlug) { + var slug = getSlug($(this).val()); + form.find('input[name=slug]').val(slug); + } + }); + + form.find('#form-group-slug-override').on('change', function() { + if ($(this).prop('checked')) { + form.find('input[name=slug]').prop('readonly', false); + } else { + form.find('input[name=slug]').prop('readonly', true); + form.find('input[name=name]').trigger('change'); + } + }); + + // Set icon when changed + form.find('input[name=icon]').on('input change', function() { + $(this).prev(".icon-preview").find("i").removeClass().addClass($(this).val()); + }); + + // Set up the form for submission + form.ufForm({ + validators: page.validators + }).on("submitSuccess.ufForm", function() { + // Reload page on success + window.location.reload(); + }); + }); +} + +/** + * Link group action buttons, for example in a table or on a specific group's page. + */ +function bindGroupButtons(el) { + /** + * Link row buttons after table is loaded. + */ + + /** + * Buttons that launch a modal dialog + */ + // Edit group details button + el.find('.js-group-edit').click(function() { + $("body").ufModal({ + sourceUrl: site.uri.public + "/modals/groups/edit", + ajaxParams: { + slug: $(this).data('slug') + }, + msgTarget: $("#alerts-page") + }); + + attachGroupForm(); + }); + + // Delete group button + el.find('.js-group-delete').click(function() { + $("body").ufModal({ + sourceUrl: site.uri.public + "/modals/groups/confirm-delete", + ajaxParams: { + slug: $(this).data('slug') + }, + msgTarget: $("#alerts-page") + }); + + $("body").on('renderSuccess.ufModal', function (data) { + var modal = $(this).ufModal('getModal'); + var form = modal.find('.js-form'); + + form.ufForm() + .on("submitSuccess.ufForm", function() { + // Reload page on success + window.location.reload(); + }); + }); + }); +} + +function bindGroupCreationButton(el) { + // Link create button + el.find('.js-group-create').click(function() { + $("body").ufModal({ + sourceUrl: site.uri.public + "/modals/groups/create", + msgTarget: $("#alerts-page") + }); + + attachGroupForm(); + }); +}; diff --git a/login/app/sprinkles/admin/assets/userfrosting/js/widgets/roles.js b/login/app/sprinkles/admin/assets/userfrosting/js/widgets/roles.js new file mode 100755 index 0000000..0e32651 --- /dev/null +++ b/login/app/sprinkles/admin/assets/userfrosting/js/widgets/roles.js @@ -0,0 +1,148 @@ +/** + * Roles widget. Sets up dropdowns, modals, etc for a table of roles. + */ + +/** + * Set up the form in a modal after being successfully attached to the body. + */ +function attachRoleForm() { + $("body").on('renderSuccess.ufModal', function (data) { + var modal = $(this).ufModal('getModal'); + var form = modal.find('.js-form'); + + /** + * Set up modal widgets + */ + + // Auto-generate slug + form.find('input[name=name]').on('input change', function() { + var manualSlug = form.find('#form-role-slug-override').prop('checked'); + if (!manualSlug) { + var slug = getSlug($(this).val()); + form.find('input[name=slug]').val(slug); + } + }); + + form.find('#form-role-slug-override').on('change', function() { + if ($(this).prop('checked')) { + form.find('input[name=slug]').prop('readonly', false); + } else { + form.find('input[name=slug]').prop('readonly', true); + form.find('input[name=name]').trigger('change'); + } + }); + + // Set up the form for submission + form.ufForm({ + validators: page.validators + }).on("submitSuccess.ufForm", function() { + // Reload page on success + window.location.reload(); + }); + }); +} + +/** + * Link role action buttons, for example in a table or on a specific role's page. + */ +function bindRoleButtons(el) { + /** + * Link row buttons after table is loaded. + */ + + // Manage permissions button + el.find('.js-role-permissions').click(function() { + var slug = $(this).data('slug'); + $("body").ufModal({ + sourceUrl: site.uri.public + "/modals/roles/permissions", + ajaxParams: { + slug: slug + }, + msgTarget: $("#alerts-page") + }); + + $("body").on('renderSuccess.ufModal', function (data) { + var modal = $(this).ufModal('getModal'); + var form = modal.find('.js-form'); + + // Set up collection widget + var permissionWidget = modal.find('.js-form-permissions'); + permissionWidget.ufCollection({ + dropdown: { + ajax: { + url : site.uri.public + '/api/permissions' + }, + placeholder : "Select a permission" + }, + dropdownTemplate: modal.find('#role-permissions-select-option').html(), + rowTemplate : modal.find('#role-permissions-row').html() + }); + + // Get current roles and add to widget + $.getJSON(site.uri.public + '/api/roles/r/' + slug + '/permissions') + .done(function (data) { + $.each(data.rows, function (idx, permission) { + permission.text = permission.name; + permissionWidget.ufCollection('addRow', permission); + }); + }); + + // Set up form for submission + form.ufForm({ + }).on("submitSuccess.ufForm", function() { + // Reload page on success + window.location.reload(); + }); + }); + }); + + /** + * Buttons that launch a modal dialog + */ + // Edit role details button + el.find('.js-role-edit').click(function() { + $("body").ufModal({ + sourceUrl: site.uri.public + "/modals/roles/edit", + ajaxParams: { + slug: $(this).data('slug') + }, + msgTarget: $("#alerts-page") + }); + + attachRoleForm(); + }); + + // Delete role button + el.find('.js-role-delete').click(function() { + $("body").ufModal({ + sourceUrl: site.uri.public + "/modals/roles/confirm-delete", + ajaxParams: { + slug: $(this).data('slug') + }, + msgTarget: $("#alerts-page") + }); + + $("body").on('renderSuccess.ufModal', function (data) { + var modal = $(this).ufModal('getModal'); + var form = modal.find('.js-form'); + + form.ufForm() + .on("submitSuccess.ufForm", function() { + // Reload page on success + window.location.reload(); + }); + }); + }); +} + +function bindRoleCreationButton(el) { + // Link create button + el.find('.js-role-create').click(function() { + $("body").ufModal({ + sourceUrl: site.uri.public + "/modals/roles/create", + msgTarget: $("#alerts-page") + }); + + attachRoleForm(); + }); +}; diff --git a/login/app/sprinkles/admin/assets/userfrosting/js/widgets/users.js b/login/app/sprinkles/admin/assets/userfrosting/js/widgets/users.js new file mode 100755 index 0000000..2e153e5 --- /dev/null +++ b/login/app/sprinkles/admin/assets/userfrosting/js/widgets/users.js @@ -0,0 +1,277 @@ +/** + * Users widget. Sets up dropdowns, modals, etc for a table of users. + */ + +/** + * Set up the form in a modal after being successfully attached to the body. + */ +function attachUserForm() { + $("body").on('renderSuccess.ufModal', function (data) { + var modal = $(this).ufModal('getModal'); + var form = modal.find('.js-form'); + + // Set up any widgets inside the modal + form.find(".js-select2").select2({ + width: '100%' + }); + + // Set up the form for submission + form.ufForm({ + validators: page.validators + }).on("submitSuccess.ufForm", function() { + // Reload page on success + window.location.reload(); + }); + }); +} + +/** + * Enable/disable password fields when switch is toggled + */ +function toggleChangePasswordMode(el, userName, changePasswordMode) { + var form = el.find("form"); + if (changePasswordMode == 'link') { + $(".controls-password").find("input[type='password']").prop('disabled', true); + // Form submits password reset request + form.attr({ + method: 'POST', + action: site.uri.public + '/api/users/u/' + userName + '/password-reset' + }); + + var validator = form.validate(); + if (validator) { + //Iterate through named elements inside of the form, and mark them as error free + el.find("input[type='password']").each(function() { + validator.successList.push(this); //mark as error free + }); + validator.resetForm();//remove error class on name elements and clear history + validator.reset();//remove all error and success data + } + el.find("input[type='password']").closest('.form-group') + .removeClass('has-error has-success'); + el.find('.form-control-feedback').each(function () { + $(this).remove(); + }); + } else { + $(".controls-password").find("input[type='password']").prop('disabled', false); + // Form submits direct password update + form.attr({ + method: 'PUT', + action: site.uri.public + '/api/users/u/' + userName + '/password' + }); + } +} + +/** + * Update user field(s) + */ +function updateUser(userName, fieldName, fieldValue) { + var data = { + 'value': fieldValue + }; + + data[site.csrf.keys.name] = site.csrf.name; + data[site.csrf.keys.value] = site.csrf.value; + + var url = site.uri.public + '/api/users/u/' + userName + '/' + fieldName; + var debugAjax = (typeof site !== "undefined") && site.debug.ajax; + + return $.ajax({ + type: "PUT", + url: url, + data: data, + dataType: debugAjax ? 'html' : 'json', + converters: { + // Override jQuery's strict JSON parsing + 'text json': function(result) { + try { + // First try to use native browser parsing + if (typeof JSON === 'object' && typeof JSON.parse === 'function') { + return JSON.parse(result); + } else { + return $.parseJSON(result); + } + } catch (e) { + // statements to handle any exceptions + console.log("Warning: Could not parse expected JSON response."); + return {}; + } + } + } + }).fail(function (jqXHR) { + // Error messages + if (debugAjax && jqXHR.responseText) { + document.write(jqXHR.responseText); + document.close(); + } else { + console.log("Error (" + jqXHR.status + "): " + jqXHR.responseText ); + + // Display errors on failure + // TODO: ufAlerts widget should have a 'destroy' method + if (!$("#alerts-page").data('ufAlerts')) { + $("#alerts-page").ufAlerts(); + } else { + $("#alerts-page").ufAlerts('clear'); + } + + $("#alerts-page").ufAlerts('fetch').ufAlerts('render'); + } + + return jqXHR; + }).done(function (response) { + window.location.reload(); + }); +} + +/** + * Link user action buttons, for example in a table or on a specific user's page. + */ + function bindUserButtons(el) { + + /** + * Buttons that launch a modal dialog + */ + // Edit general user details button + el.find('.js-user-edit').click(function() { + $("body").ufModal({ + sourceUrl: site.uri.public + "/modals/users/edit", + ajaxParams: { + user_name: $(this).data('user_name') + }, + msgTarget: $("#alerts-page") + }); + + attachUserForm(); + }); + + // Manage user roles button + el.find('.js-user-roles').click(function() { + var userName = $(this).data('user_name'); + $("body").ufModal({ + sourceUrl: site.uri.public + "/modals/users/roles", + ajaxParams: { + user_name: userName + }, + msgTarget: $("#alerts-page") + }); + + $("body").on('renderSuccess.ufModal', function (data) { + var modal = $(this).ufModal('getModal'); + var form = modal.find('.js-form'); + + // Set up collection widget + var roleWidget = modal.find('.js-form-roles'); + roleWidget.ufCollection({ + dropdown : { + ajax: { + url : site.uri.public + '/api/roles' + }, + placeholder : "Select a role" + }, + dropdownTemplate: modal.find('#user-roles-select-option').html(), + rowTemplate : modal.find('#user-roles-row').html() + }); + + // Get current roles and add to widget + $.getJSON(site.uri.public + '/api/users/u/' + userName + '/roles') + .done(function (data) { + $.each(data.rows, function (idx, role) { + role.text = role.name; + roleWidget.ufCollection('addRow', role); + }); + }); + + // Set up form for submission + form.ufForm({ + }).on("submitSuccess.ufForm", function() { + // Reload page on success + window.location.reload(); + }); + }); + }); + + // Change user password button + el.find('.js-user-password').click(function() { + var userName = $(this).data('user_name'); + $("body").ufModal({ + sourceUrl: site.uri.public + "/modals/users/password", + ajaxParams: { + user_name: userName + }, + msgTarget: $("#alerts-page") + }); + + $("body").on('renderSuccess.ufModal', function (data) { + var modal = $(this).ufModal('getModal'); + var form = modal.find('.js-form'); + + // Set up form for submission + form.ufForm({ + validators: page.validators + }).on("submitSuccess.ufForm", function() { + // Reload page on success + window.location.reload(); + }); + + toggleChangePasswordMode(modal, userName, 'link'); + + // On submission, submit either the PUT request, or POST for a password reset, depending on the toggle state + modal.find("input[name='change_password_mode']").click(function() { + var changePasswordMode = $(this).val(); + toggleChangePasswordMode(modal, userName, changePasswordMode); + }); + }); + }); + + // Delete user button + el.find('.js-user-delete').click(function() { + $("body").ufModal({ + sourceUrl: site.uri.public + "/modals/users/confirm-delete", + ajaxParams: { + user_name: $(this).data('user_name') + }, + msgTarget: $("#alerts-page") + }); + + $("body").on('renderSuccess.ufModal', function (data) { + var modal = $(this).ufModal('getModal'); + var form = modal.find('.js-form'); + + form.ufForm() + .on("submitSuccess.ufForm", function() { + // Reload page on success + window.location.reload(); + }); + }); + }); + + /** + * Direct action buttons + */ + el.find('.js-user-activate').click(function() { + var btn = $(this); + updateUser(btn.data('user_name'), 'flag_verified', '1'); + }); + + el.find('.js-user-enable').click(function () { + var btn = $(this); + updateUser(btn.data('user_name'), 'flag_enabled', '1'); + }); + + el.find('.js-user-disable').click(function () { + var btn = $(this); + updateUser(btn.data('user_name'), 'flag_enabled', '0'); + }); +} + +function bindUserCreationButton(el) { + // Link create button + el.find('.js-user-create').click(function() { + $("body").ufModal({ + sourceUrl: site.uri.public + "/modals/users/create", + msgTarget: $("#alerts-page") + }); + + attachUserForm(); + }); +}; diff --git a/login/app/sprinkles/admin/composer.json b/login/app/sprinkles/admin/composer.json new file mode 100755 index 0000000..8ccd5c0 --- /dev/null +++ b/login/app/sprinkles/admin/composer.json @@ -0,0 +1,22 @@ +{ + "name": "userfrosting/sprinkle-admin", + "type": "userfrosting-sprinkle", + "description": "Administrative management module for UserFrosting.", + "keywords": ["php user management", "usercake", "bootstrap"], + "homepage": "https://github.com/userfrosting/UserFrosting", + "license" : "MIT", + "authors" : [ + { + "name": "Alexander Weissman", + "homepage": "https://alexanderweissman.com" + } + ], + "require": { + "php": ">=5.6" + }, + "autoload": { + "psr-4": { + "UserFrosting\\Sprinkle\\Admin\\": "src/" + } + } +} diff --git a/login/app/sprinkles/admin/locale/ar/messages.php b/login/app/sprinkles/admin/locale/ar/messages.php new file mode 100755 index 0000000..d4a3a44 --- /dev/null +++ b/login/app/sprinkles/admin/locale/ar/messages.php @@ -0,0 +1,135 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Modern Standard Arabic message token translations for the 'admin' sprinkle. + * + * @package userfrosting\i18n\ar + * @author Alexander Weissman and Abdullah Seba + */ + +return [ + "ACTIVITY" => [ + 1 => "نشاط", + 2 => "أنشطة", + + "LAST" => "النشاط الاخير", + "PAGE" => "قائمة من أنشطة المستخدم", + "TIME" => "وقت نشاط" + ], + + "CACHE" => [ + "CLEAR" => "مسح ذاكرة التخزين", + "CLEAR_CONFIRM" => "هل أنت متأكد أنك تريد مسح ذاكرة التخزين بالموقع؟", + "CLEAR_CONFIRM_YES" => "نعم، إمسح ذاكرة التخزين", + "CLEARED" => "تم مسح ذاكرة التخزين بنجاح" + ], + + "DASHBOARD" => "لوحة القيادة", + "NO_FEATURES_YET" => "لا يبدو أن أي ميزات تم إعدادها لهذا الحساب حتى الآن. ربما لم يتم تنفيذها بعد، أو ربما شخص نسي أن يعطيك الوصول. في كلتا الحالتين، نحن سعداء أن يكون لك على متن!", + "DELETE_MASTER" => "لا يمكنك حذف الحساب الرئيسي", + "DELETION_SUCCESSFUL" => "المستعمل <strong>{{user_name}}</strong> حذف بنجاح", + "DETAILS_UPDATED" => "جدد تفاصيل الحساب للمستخدم <strong>{{user_name}}</strong>", + "DISABLE_MASTER" => "لا يمكنك تعطيل الحساب الرئيسي", + "DISABLE_SUCCESSFUL" => "حساب المستخدم <strong>{{user_name}}</strong> عطيل بنجاح", + + "ENABLE_SUCCESSFUL" => "حساب المستخدم <strong>{{user_name}}</strong> مكين بنجاح", + + "GROUP" => [ + 1 => "مجموعة", + 2 => "مجموعات", + + "CREATE" => "إنشاء مجموعة", + "DELETE" => "حذف مجموعة", + "DELETE_CONFIRM" => "هل أنت متأكد أنك تريد حذف مجموعة <strong>{{name}}</strong>?", + "DELETE_YES" => "نعم، إحذف مجموعة", + "EDIT" => "تعديل مجموعة", + "ICON" => "رمز المجموعة", + "ICON_EXPLAIN" => "رمز المستخدمين في المجموعه", + "INFO_PAGE" => "صفحة معلومات المجموعة ل {{name}}", + //"MANAGE" => "Manage group", + "NAME" => "أسم المجموعة", + "NAME_EXPLAIN" => "ادخال اسم للمجموعة", + "PAGE_DESCRIPTION" => "قائمة المجموعات لموقعك يوفر أدوات لإدارة التحرير وحذف مجموعات" + ], + + "MANUALLY_ACTIVATED" => "تم تفعيل حساب{{user_name}}", + "MASTER_ACCOUNT_EXISTS" => "الحساب الرئيسي موجود بالفعل", + "MIGRATION" => [ + "REQUIRED" => "تحديث قاعدة البيانات مطلوب" + ], + + "PERMISSION" => [ + 1 => "الإذن", + 2 => "مأذونيات", + + "ASSIGN_NEW" => "تعيين إذن جديد", + "HOOK_CONDITION" => "الظروف", + "MANAGE" => "إدارة المأذونات", + "PAGE_DESCRIPTION" => "قائمة المأذونات لموقعك", + "UPDATE" => "تحديث المأذونات" + ], + + "ROLE" => [ + 1 => "وظيفة", + 2 => "وظائف", + + "ASSIGN_NEW" => "تعيين دور جديد", + "CREATE" => "إنشاء دور", + "DELETE" => "حذف دور", + "DELETE_CONFIRM" => "هل أنت متأكد أنك تريد حذف الدور <strong>{{name}}</strong>?", + "DELETE_YES" => "نعم، حذف دور", + "EDIT" => "إدارة دور", + "INFO_PAGE" => "صفحة معلومات دور {{name}}", + "MANAGE" => "إدارة الوظائف", + "NAME" => "اسم", + "NAME_EXPLAIN" => "أدخل اسما للدور", + "PAGE_DESCRIPTION" => "قائمة الوظائف لموقعك", + "UPDATED" => "تحديث الوظائف" + ], + + "SYSTEM_INFO" => [ + "@TRANSLATION" => "معلومات الجهاز", + + "DB_NAME" => "اسم قاعدة البيانات", + "DB_VERSION" => "إصدار قاعدة البيانات", + "DIRECTORY" => "دليل المشروع", + "PHP_VERSION" => "الإصدار PHP", + "SERVER" => "برنامج الخادم", + "SPRINKLES" => "sprinkles المحمل", + "UF_VERSION" => "إصدار UserFrosting", + "URL" => "رابط قاعدة الموقع" + ], + + "USER" => [ + 1 => "مستخدم", + 2 => "المستخدمين", + + "ADMIN" => [ + "CHANGE_PASSWORD" => "تغيير كلمة المرور للمستخدم", + "SEND_PASSWORD_LINK" => "إرسال المستخدم وصلة من شأنها أن تسمح لهم لاختيار كلمة المرور الخاصة بهم", + "SET_PASSWORD" => "تعيين كلمة المرور الخاصة بالمستخدم" + ], + + "ACTIVATE" => "تفعيل المستخدم", + "CREATE" => "إنشاء مستخدم", + "DELETE" => "مسح المستخدم", + "DELETE_CONFIRM" => "هل أنت متأكد أنك تريد حذف المستخدم <strong>{{name}}</strong>?", + "DELETE_YES" => "نعم، حذف المستخدم", + "DISABLE" => "تعطيل المستخدم ", + "EDIT" => "إدارة المستخدم", + "ENABLE" => "تمكين المستخدم", + "INFO_PAGE" => "صفحة معلومات المستخدم {{name}}", + "PAGE_DESCRIPTION" => "قائمة المستخدمين لموقعك", + "LATEST" => "أحدث المستخدمين", + "VIEW_ALL" => "عرض جميع المستخدمين" + ], + "X_USER" => [ + 0 => "لا يوجد اي مستخدمين", + 1 => "{{plural}} مستخدم", + 2 => "{{plural}} المستخدمين" + ] +]; diff --git a/login/app/sprinkles/admin/locale/de_DE/messages.php b/login/app/sprinkles/admin/locale/de_DE/messages.php new file mode 100755 index 0000000..6e21ab5 --- /dev/null +++ b/login/app/sprinkles/admin/locale/de_DE/messages.php @@ -0,0 +1,161 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * German message token translations for the 'admin' sprinkle. + * + * @package userfrosting\i18n\de + * @author X-Anonymous-Y + * @author kevinrombach + * @author splitt3r + */ + +return [ + "ACTIVITY" => [ + 1 => "Aktivität", + 2 => "Aktivitäten", + + "LAST" => "Letzte Aktivität", + "PAGE" => "Eine Auflistung der Benutzeraktivitäten", + "TIME" => "Aktivitätszeit" + ], + + "CACHE" => [ + "CLEAR" => "Cache löschen", + "CLEAR_CONFIRM" => "Sind Sie sicher, dass Sie den Seiten-Cache löschen möchten?", + "CLEAR_CONFIRM_YES" => "Ja, Cache löschen", + "CLEARED" => "Cache wurde erfolgreich gelöscht!" + ], + + "DASHBOARD" => "Übersicht", + "NO_FEATURES_YET" => "Es sieht aus, als wären für Ihren Account noch keine Funktionen aktiviert... bisher. Entweder sie wurden bisher noch nicht implementiert, oder Ihnen fehlen noch die Berechtigungen. Trotzdem ist es schön, dass Sie auf unsere Seite gekommen sind!", + "DELETE_MASTER" => "Sie können das Root-Konto nicht löschen!", + "DELETION_SUCCESSFUL" => "Benutzer <strong>{{user_name}}</strong> wurde erfolgreich gelöscht.", + "DETAILS_UPDATED" => "Konto-Daten für <strong>{{user_name}}</strong> aktualisiert.", + "DISABLE_MASTER" => "Sie können das Root-Konto nicht deaktivieren!", + "DISABLE_SELF" => "Sie können Ihr eigenes Konto nicht deaktivieren!", + "DISABLE_SUCCESSFUL" => "Konto von <strong>{{user_name}}</strong> wurde erfolgreich deaktiviert.", + + "ENABLE_SUCCESSFUL" => "Konto von {{user_name}} wurde erfolgreich aktiviert.", + + "GROUP" => [ + 1 => "Gruppe", + 2 => "Gruppen", + + "CREATE" => "Gruppe erstellen", + "CREATION_SUCCESSFUL" => "Die Gruppe <strong>{{name}}</strong> wurde erfolgreich erstellt", + "DELETE" => "Gruppe löschen", + "DELETE_CONFIRM" => "Möchten Sie die Gruppe <strong>{{name}}</strong> wirklich löschen?", + "DELETE_DEFAULT" => "Sie können die Gruppe <strong>{{name}}</strong> nicht löschen, da es die Standardgruppe für neu registrierte Benutzer ist.", + "DELETE_YES" => "Ja, Gruppe löschen", + "DELETION_SUCCESSFUL" => "Die Gruppe <strong>{{name}}</strong> wurde erfolgreich gelöscht", + "EDIT" => "Gruppe bearbeiten", + "ICON" => "Gruppensymbol", + "ICON_EXPLAIN" => "Symbol für Gruppenmitglieder", + "INFO_PAGE" => "Gruppeninformationsseite für {{name}}", + "MANAGE" => "Gruppe verwalten", + "NAME" => "Gruppenname", + "NAME_EXPLAIN" => "Geben Sie einen Namen für die Gruppe ein", + "NOT_EMPTY" => "Sie können das nicht tun, denn es sind noch Benutzer mit der Gruppe <strong>{{name}}</strong> verbunden.", + "PAGE_DESCRIPTION" => "Eine Liste der Gruppen für Ihre Website. Bietet Verwaltungstools für das Bearbeiten und Löschen von Gruppen.", + "SUMMARY" => "Gruppen Zusammenfassung", + "UPDATE" => "Details für die Gruppe <strong>{{name}}</strong> aktualisiert" + ], + + "MANUALLY_ACTIVATED" => "{{user_name}}'s Konto wurde manuell aktiviert.", + "MASTER_ACCOUNT_EXISTS" => "Das Root-Konto existiert bereits!", + "MIGRATION" => [ + "REQUIRED" => "Datenbankaktualisierung erforderlich" + ], + + "PERMISSION" => [ + 1 => "Berechtigung", + 2 => "Berechtigungen", + + "ASSIGN_NEW" => "Neue Berechtigung zuweisen", + "HOOK_CONDITION" => "Haken/Bedingungen", + "ID" => "Berechtigungs-ID", + "INFO_PAGE" => "Berechtigungs Informationen für '{{name}}'", + "MANAGE" => "Berechtigungen verwalten", + "NOTE_READ_ONLY" => "<strong>Bitte beachten Sie:</strong> Berechtigungen werden als \"Teil des Quelltexts\" gesehen und können hier nicht bearbeitet werden. Um Berechtigungen hinzuzufügen, zu bearbeiten, oder zu löschen, benutzen Sie bitte folgende Dokumentation zur <a href=\"https://learn.userfrosting.com/database/extending-the-database\" target=\"about:_blank\">Datenbank Migration.</a>", + "PAGE_DESCRIPTION" => "Eine Liste der Berechtigungen für Ihre Website. Bietet Verwaltungstools zum Bearbeiten und Löschen von Berechtigungen.", + "SUMMARY" => "Berechtigungs Zusammenfassung", + "UPDATE" => "Berechtigungen aktualisieren", + "VIA_ROLES" => "Besitzt die Berechtigung durch die Rolle" + ], + + "ROLE" => [ + 1 => "Rolle", + 2 => "Rollen", + + "ASSIGN_NEW" => "Neue Rolle zuweisen", + "CREATE" => "Rolle erstellen", + "CREATION_SUCCESSFUL" => "Die Rolle <strong>{{name}}</strong> wurde erfolgreich erstellt", + "DELETE" => "Rolle löschen", + "DELETE_CONFIRM" => "Sind Sie sicher, dass Sie die Rolle <strong>{{name}}</strong> löschen möchten?", + "DELETE_DEFAULT" => "Sie können die Rolle <strong>{{name}}</strong> nicht löschen, da es eine Standardrolle für neu registrierte Benutzer ist.", + "DELETE_YES" => "Ja, Rolle löschen", + "DELETION_SUCCESSFUL" => "Die Rolle <strong>{{name}}</strong> wurde erfolgreich gelöscht", + "EDIT" => "Rolle bearbeiten", + "HAS_USERS" => "Sie können das nicht machen weil es noch Benutzer gibt, die die Rolle <strong>{{name}}</strong> haben.", + "INFO_PAGE" => "Rolleninformationsseite für {{name}}", + "MANAGE" => "Rollen verwalten", + "NAME" => "Name", + "NAME_EXPLAIN" => "Geben Sie einen Namen für die Rolle ein", + "NAME_IN_USE" => "Eine Rolle mit dem Namen <strong>{{name}}</strong> existiert bereits", + "PAGE_DESCRIPTION" => "Eine Liste der Rollen für Ihre Website. Bietet Verwaltungstools zum Bearbeiten und Löschen von Rollen.", + "PERMISSIONS_UPDATED" => "Berechtigungen für die Rolle <strong>{{name}}</strong> aktualisiert", + "SUMMARY" => "Rollen Zusammenfassung", + "UPDATED" => "Rollen aktualisieren" + ], + + "SYSTEM_INFO" => [ + "@TRANSLATION" => "System Information", + + "DB_NAME" => "Name der Datenbank", + "DB_VERSION" => "Datenbankversion", + "DIRECTORY" => "Projektverzeichnis", + "PHP_VERSION" => "PHP-Version", + "SERVER" => "Web-Server-Software", + "SPRINKLES" => "Geladene Sprinkles", + "UF_VERSION" => "UserFrosting Version", + "URL" => "Website-Stamm-Url" + ], + + "TOGGLE_COLUMNS" => "Spalten anpassen", + + "USER" => [ + 1 => "Benutzer", + 2 => "Benutzer", + + "ADMIN" => [ + "CHANGE_PASSWORD" => "Benutzerpasswort ändern", + "SEND_PASSWORD_LINK" => "Senden Sie dem Benutzer einen Link, der ihnen erlaubt, ihr eigenes Passwort zu wählen", + "SET_PASSWORD" => "Setzen Sie das Passwort des Benutzers als" + ], + + "ACTIVATE" => "Benutzer aktivieren", + "CREATE" => "Benutzer erstellen", + "CREATED" => "Benutzer <strong>{{user_name}}</strong> wurde erfolgreich erstellt", + "DELETE" => "Benutzer löschen", + "DELETE_CONFIRM" => "Sind Sie sicher, dass Sie den Benutzer <strong>{{name}}</strong> löschen möchten?", + "DELETE_YES" => "Ja, Benutzer löschen", + "DISABLE" => "Benutzer deaktivieren", + "EDIT" => "Benutzer bearbeiten", + "ENABLE" => "Benutzer aktivieren", + "INFO_PAGE" => "Benutzerinformationsseite für {{name}}", + "LATEST" => "Neueste Benutzer", + "PAGE_DESCRIPTION" => "Eine Liste der Benutzer für Ihre Website. Bietet Management-Tools, einschließlich der Möglichkeit, Benutzerdaten bearbeiten, manuell aktivieren, Benutzer aktivieren/deaktivieren, und vieles mehr.", + "SUMMARY" => "Benutzer Zusammenfassung", + "VIEW_ALL" => "Alle Benutzer anzeigen", + "WITH_PERMISSION" => "Benutzer mit dieser Berechtigung" + ], + "X_USER" => [ + 0 => "Keine Benutzer", + 1 => "{{plural}} Benutzer", + 2 => "{{plural}} Benutzer" + ] +]; diff --git a/login/app/sprinkles/admin/locale/en_US/messages.php b/login/app/sprinkles/admin/locale/en_US/messages.php new file mode 100755 index 0000000..a21e325 --- /dev/null +++ b/login/app/sprinkles/admin/locale/en_US/messages.php @@ -0,0 +1,160 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * US English message token translations for the 'admin' sprinkle. + * + * @package userfrosting\i18n\en_US + * @author Alexander Weissman + */ + +return [ + "ACTIVITY" => [ + 1 => "Activity", + 2 => "Activities", + + "LAST" => "Last Activity", + "PAGE" => "A listing of user activities", + "TIME" => "Activity Time" + ], + + "CACHE" => [ + "CLEAR" => "Clear cache", + "CLEAR_CONFIRM" => "Are you sure you want to clear the site cache?", + "CLEAR_CONFIRM_YES" => "Yes, clear cache", + "CLEARED" => "Cache cleared successfully !" + ], + + "DASHBOARD" => "Dashboard", + "NO_FEATURES_YET" => "It doesn't look like any features have been set up for this account...yet. Maybe they haven't been implemented yet, or maybe someone forgot to give you access. Either way, we're glad to have you aboard!", + "DELETE_MASTER" => "You cannot delete the master account!", + "DELETION_SUCCESSFUL" => "User <strong>{{user_name}}</strong> has been successfully deleted.", + "DETAILS_UPDATED" => "Account details updated for user <strong>{{user_name}}</strong>", + "DISABLE_MASTER" => "You cannot disable the master account!", + "DISABLE_SELF" => "You cannot disable your own account!", + "DISABLE_SUCCESSFUL" => "Account for user <strong>{{user_name}}</strong> has been successfully disabled.", + + "ENABLE_SUCCESSFUL" => "Account for user <strong>{{user_name}}</strong> has been successfully enabled.", + + "GROUP" => [ + 1 => "Group", + 2 => "Groups", + + "CREATE" => "Create group", + "CREATION_SUCCESSFUL" => "Successfully created group <strong>{{name}}</strong>", + "DELETE" => "Delete group", + "DELETE_CONFIRM" => "Are you sure you want to delete the group <strong>{{name}}</strong>?", + "DELETE_DEFAULT" => "You can't delete the group <strong>{{name}}</strong> because it is the default group for newly registered users.", + "DELETE_YES" => "Yes, delete group", + "DELETION_SUCCESSFUL" => "Successfully deleted group <strong>{{name}}</strong>", + "EDIT" => "Edit group", + "ICON" => "Group icon", + "ICON_EXPLAIN" => "Icon for group members", + "INFO_PAGE" => "Group information page for {{name}}", + "MANAGE" => "Manage group", + "NAME" => "Group name", + "NAME_EXPLAIN" => "Please enter a name for the group", + "NOT_EMPTY" => "You can't do that because there are still users associated with the group <strong>{{name}}</strong>.", + "PAGE_DESCRIPTION" => "A listing of the groups for your site. Provides management tools for editing and deleting groups.", + "SUMMARY" => "Group Summary", + "UPDATE" => "Details updated for group <strong>{{name}}</strong>" + ], + + "MANUALLY_ACTIVATED" => "{{user_name}}'s account has been manually activated", + "MASTER_ACCOUNT_EXISTS" => "The master account already exists!", + "MIGRATION" => [ + "REQUIRED" => "Database update required" + ], + + "PERMISSION" => [ + 1 => "Permission", + 2 => "Permissions", + + "ASSIGN_NEW" => "Assign new permission", + "HOOK_CONDITION" => "Hook/Conditions", + "ID" => "Permission ID", + "INFO_PAGE" => "Permission information page for '{{name}}'", + "MANAGE" => "Manage permissions", + "NOTE_READ_ONLY" => "<strong>Please note:</strong> permissions are considered \"part of the code\" and cannot be modified through the interface. To add, remove, or modify permissions, the site maintainers will need to use a <a href=\"https://learn.userfrosting.com/database/extending-the-database\" target=\"about:_blank\">database migration.</a>", + "PAGE_DESCRIPTION" => "A listing of the permissions for your site. Provides management tools for editing and deleting permissions.", + "SUMMARY" => "Permission Summary", + "UPDATE" => "Update permissions", + "VIA_ROLES" => "Has permission via roles" + ], + + "ROLE" => [ + 1 => "Role", + 2 => "Roles", + + "ASSIGN_NEW" => "Assign new role", + "CREATE" => "Create role", + "CREATION_SUCCESSFUL" => "Successfully created role <strong>{{name}}</strong>", + "DELETE" => "Delete role", + "DELETE_CONFIRM" => "Are you sure you want to delete the role <strong>{{name}}</strong>?", + "DELETE_DEFAULT" => "You can't delete the role <strong>{{name}}</strong> because it is a default role for newly registered users.", + "DELETE_YES" => "Yes, delete role", + "DELETION_SUCCESSFUL" => "Successfully deleted role <strong>{{name}}</strong>", + "EDIT" => "Edit role", + "HAS_USERS" => "You can't do that because there are still users who have the role <strong>{{name}}</strong>.", + "INFO_PAGE" => "Role information page for {{name}}", + "MANAGE" => "Manage Roles", + "NAME" => "Name", + "NAME_EXPLAIN" => "Please enter a name for the role", + "NAME_IN_USE" => "A role named <strong>{{name}}</strong> already exist", + "PAGE_DESCRIPTION" => "A listing of the roles for your site. Provides management tools for editing and deleting roles.", + "PERMISSIONS_UPDATED" => "Permissions updated for role <strong>{{name}}</strong>", + "SUMMARY" => "Role Summary", + "UPDATED" => "Details updated for role <strong>{{name}}</strong>" + ], + + "SYSTEM_INFO" => [ + "@TRANSLATION" => "System information", + + "DB_NAME" => "Database name", + "DB_VERSION" => "Database version", + "DIRECTORY" => "Project directory", + "PHP_VERSION" => "PHP version", + "SERVER" => "Webserver software", + "SPRINKLES" => "Loaded sprinkles", + "UF_VERSION" => "UserFrosting version", + "URL" => "Site root url" + ], + + "TOGGLE_COLUMNS" => "Toggle columns", + + "USER" => [ + 1 => "User", + 2 => "Users", + + "ADMIN" => [ + "CHANGE_PASSWORD" => "Change User Password", + "SEND_PASSWORD_LINK" => "Send the user a link that will allow them to choose their own password", + "SET_PASSWORD" => "Set the user's password as" + ], + + "ACTIVATE" => "Activate user", + "CREATE" => "Create user", + "CREATED" => "User <strong>{{user_name}}</strong> has been successfully created", + "DELETE" => "Delete user", + "DELETE_CONFIRM" => "Are you sure you want to delete the user <strong>{{name}}</strong>?", + "DELETE_YES" => "Yes, delete user", + "DELETED" => "User deleted", + "DISABLE" => "Disable user", + "EDIT" => "Edit user", + "ENABLE" => "Enable user", + "INFO_PAGE" => "User information page for {{name}}", + "LATEST" => "Latest Users", + "PAGE_DESCRIPTION" => "A listing of the users for your site. Provides management tools including the ability to edit user details, manually activate users, enable/disable users, and more.", + "SUMMARY" => "Account Summary", + "VIEW_ALL" => "View all users", + "WITH_PERMISSION" => "Users with this permission" + ], + "X_USER" => [ + 0 => "No users", + 1 => "{{plural}} user", + 2 => "{{plural}} users" + ] +]; diff --git a/login/app/sprinkles/admin/locale/es_ES/messages.php b/login/app/sprinkles/admin/locale/es_ES/messages.php new file mode 100755 index 0000000..a8950c0 --- /dev/null +++ b/login/app/sprinkles/admin/locale/es_ES/messages.php @@ -0,0 +1,164 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Spanish message token translations for the 'admin' sprinkle. + * + * @package userfrosting\i18n\es_ES + * @author rafa31gz + */ + +return [ + "ACTIVITY" => [ + 1 => "Actividad", + 2 => "Actividades", + + "LAST" => "Última actividad", + "PAGE" => "Una lista de las actividades del usuario", + "TIME" => "Tiempo de Actividad" + ], + + "ADMIN" => [ + "PANEL" => "Panel de administración" + ], + + "CACHE" => [ + "CLEAR" => "Limpiar cache", + "CLEAR_CONFIRM" => "¿Está seguro de que desea borrar la caché del sitio?", + "CLEAR_CONFIRM_YES" => "Sí, borrar caché", + "CLEARED" => "¡Cache borrado correctamente!" + ], + + "DASHBOARD" => "Tablero", + "NO_FEATURES_YET" => "No parece que se hayan configurado funciones para esta cuenta ... todavía. Tal vez no se han implementado todavía, o tal vez alguien se olvidó de darle acceso. De cualquier manera, ¡estamos encantados de tenerte a bordo!", + "DELETE_MASTER" => "¡No puede eliminar la cuenta principal!", + "DELETION_SUCCESSFUL" => "El usuario <strong> {{user_name}} </strong> se ha eliminado correctamente.", + "DETAILS_UPDATED" => "Detalles de la cuenta actualizados para el usuario <strong> {{user_name}} </strong>", + "DISABLE_MASTER" => "¡No puedes deshabilitar la cuenta principal!", + "DISABLE_SELF" => "¡No puedes inhabilitar tu propia cuenta!", + "DISABLE_SUCCESSFUL" => "La cuenta para el usuario <strong> {{user_name}} </strong> se ha desactivado correctamente.", + + "ENABLE_SUCCESSFUL" => "La cuenta para el usuario <strong> {{user_name}} </strong> se ha habilitado correctamente.", + + "GROUP" => [ + 1 => "Grupo", + 2 => "Grupos", + + "CREATE" => "Crea un grupo", + "CREATION_SUCCESSFUL" => "Grupo creado correctamente <strong> {{name}} </strong>", + "DELETE" => "Delete group", + "DELETE_CONFIRM" => "¿Seguro que quieres eliminar el grupo <strong> {{name}} </strong>?", + "DELETE_DEFAULT" => "No puedes eliminar el grupo <strong> {{name}} </strong> porque es el grupo predeterminado para los usuarios recién registrados.", + "DELETE_YES" => "Sí, eliminar grupo", + "DELETION_SUCCESSFUL" => "Grupo eliminado correctamente <strong> {{name}} </strong>", + "EDIT" => "Editar grupo", + "ICON" => "Icono de grupo", + "ICON_EXPLAIN" => "Icono para los miembros del grupo", + "INFO_PAGE" => "Página de información de grupo para {{name}}", + "MANAGE" => "Administrar grupo", + "NAME" => "Nombre del grupo", + "NAME_EXPLAIN" => "Introduzca un nombre para el grupo", + "NOT_EMPTY" => "No puedes hacerlo porque todavía hay usuarios asociados con el grupo <strong> {{name}} </strong>.", + "PAGE_DESCRIPTION" => "Un listado de los grupos para su sitio. Proporciona herramientas de administración para editar y eliminar grupos.", + "SUMMARY" => "Resumen del grupo", + "UPDATE" => "Detalles actualizados para el grupo <strong> {{name}} </strong>" + ], + + "MANUALLY_ACTIVATED" => "La cuenta de {{user_name}} se ha activado manualmente", + "MASTER_ACCOUNT_EXISTS" => "¡La cuenta maestra ya existe!", + "MIGRATION" => [ + "REQUIRED" => "Se requiere actualizar la base de datos" + ], + + "PERMISSION" => [ + 1 => "Permiso", + 2 => "Permisos", + + "ASSIGN_NEW" => "Asignar nuevo permiso", + "HOOK_CONDITION" => "Hook/Condiciones", + "ID" => "ID de permiso", + "INFO_PAGE" => "Página de autor del permiso de '{{name}}'", + "MANAGE" => "Administrar permisos", + "NOTE_READ_ONLY" => "<strong> Tenga en cuenta: </strong> los permisos se consideran \"parte del código\" y no se pueden modificar a través de la interfaz. Para agregar, eliminar o modificar permisos, los mantenedores del sitio necesitarán usar una <a href=\"https://learn.userfrosting.com/database/extended-the-database\" target=\"about:_blank\"> migración de la base de datos . </a>", + "PAGE_DESCRIPTION" => "Una lista de los permisos para su sitio. Proporciona herramientas de administración para editar y eliminar permisos.", + "SUMMARY" => "Resumen del permiso", + "UPDATE" => "Actualizar permisos", + "VIA_ROLES" => "Tiene permiso para los roles" + ], + + "ROLE" => [ + 1 => "Rol(funcion)", + 2 => "Roles(funciones)", + + "ASSIGN_NEW" => "Asignar nueva rol", + "CREATE" => "Crear un rol", + "CREATION_SUCCESSFUL" => "Función creada correctamente <strong> {{name}} </strong>", + "DELETE" => "Eliminar rol", + "DELETE_CONFIRM" => "¿Seguro que quieres eliminar la función <strong> {{name}} </strong>?", + "DELETE_DEFAULT" => "No puedes eliminar el rol <strong> {{name}} </strong> porque es un rol predeterminado para los usuarios recién registrados.", + "DELETE_YES" => "Sí, borrar función", + "DELETION_SUCCESSFUL" => "Se ha eliminado la función <strong> {{nombre}} </strong>", + "EDIT" => "Editar función", + "HAS_USERS" => "No puedes hacerlo porque todavía hay usuarios que tienen el rol <strong> {{name}} </strong>.", + "INFO_PAGE" => "Página de información de funciones de {{name}}", + "MANAGE" => "Administrar roles", + "NAME" => "Nombre", + "NAME_EXPLAIN" => "Ingrese un nombre para el rol", + "NAME_IN_USE" => "Ya existe un rol denominado <strong> {{name}} </strong>", + "PAGE_DESCRIPTION" => "Una lista de las funciones de su sitio. Proporciona herramientas de administración para editar y eliminar roles.", + "PERMISSIONS_UPDATED" => "Permisos actualizados para el rol <strong> {{name}} </strong>", + "SUMMARY" => "Resumen del rol", + "UPDATED" => "Detalles actualizados para el rol <strong> {{name}} </strong>" + ], + + "SYSTEM_INFO" => [ + "@TRANSLATION" => "Información del sistema", + + "DB_NAME" => "Nombre de la base de datos", + "DB_VERSION" => "Versión de base de datos", + "DIRECTORY" => "Directorio del proyecto", + "PHP_VERSION" => "Versión de PHP", + "SERVER" => "Software de servidor Web", + "SPRINKLES" => "Sprinkles cargados", + "UF_VERSION" => "UserFrosting versión", + "URL" => "URL root del sitio" + ], + + "TOGGLE_COLUMNS" => "Alternar columnas", + "NO_DATA" => "No puede quedar vacio.", + + "USER" => [ + 1 => "Usuario", + 2 => "Usuarios", + + "ADMIN" => [ + "CHANGE_PASSWORD" => "Cambiar contraseña de usuario", + "SEND_PASSWORD_LINK" => "Enviar al usuario un enlace que les permita elegir su propia contraseña", + "SET_PASSWORD" => "Establezca la contraseña del usuario como" + ], + + "ACTIVATE" => "Activar usuario", + "CREATE" => "Crear usuario", + "CREATED" => "Se ha creado correctamente el usuario <strong> {{user_name}} </strong>", + "DELETE" => "Borrar usuario", + "DELETE_CONFIRM" => "¿Seguro que desea eliminar el usuario <strong> {{name}} </strong>?", + "DELETE_YES" => "Sí, eliminar usuario", + "DISABLE" => "Deshabilitar usuario", + "EDIT" => "Editar usuario", + "ENABLE" => "Habilitar usuario", + "INFO_PAGE" => "Página de información de usuario de {{name}}", + "LATEST" => "Usuarios más recientes", + "PAGE_DESCRIPTION" => "Una lista de los usuarios para su sitio. Proporciona herramientas de administración que incluyen la capacidad de editar detalles de usuario, activar manualmente usuarios, habilitar / deshabilitar usuarios y más.", + "SUMMARY" => "Resumen de la cuenta", + "VIEW_ALL" => "Ver todos los usuarios", + "WITH_PERMISSION" => "Usuarios con este permiso" + ], + "X_USER" => [ + 0 => "No hay usuarios", + 1 => "{{plural}} usuario", + 2 => "{{plural}} usuarios" + ] +]; diff --git a/login/app/sprinkles/admin/locale/fa/messages.php b/login/app/sprinkles/admin/locale/fa/messages.php new file mode 100755 index 0000000..75a8dee --- /dev/null +++ b/login/app/sprinkles/admin/locale/fa/messages.php @@ -0,0 +1,158 @@ +<?php + +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Standard Farsi/Persian message token translations for the 'admin' sprinkle. + * + * @package userfrosting\i18n\fa + * @author aminakbari + */ + +return [ + "ACTIVITY" => [ + 1 => "فعالیت", + 2 => "فعالیت ها", + + "LAST" => "آخرین فعالیت", + "PAGE" => "لیستی از فعالیت های کاربر", + "TIME" => "زمان فعالیت" + ], + + "CACHE" => [ + "CLEAR" => "پاک سازی کش", + "CLEAR_CONFIRM" => "آیا مطمئن هستید که میخواهید کش سایت را پاک سازی کنید؟", + "CLEAR_CONFIRM_YES" => "بله، کش پاک سازی شود", + "CLEARED" => "کش با موفقیت پاک سازی شد" + ], + + "DASHBOARD" => "کارتابل", + "DELETE_MASTER" => "شما نمیتوانید کاربر اصلی را حذف کنید", + "DELETION_SUCCESSFUL" => "<strong>{{user_name}}</strong> با موفقیت حذف شد.", + "DETAILS_UPDATED" => "جزئیات <strong>{{user_name}}</strong> با موفقیت ذخیره شد.", + "DISABLE_MASTER" => "شما نمیتوانید کاربر اصلی را غیر فعال کنید.", + "DISABLE_SUCCESSFUL" => "حساب کاربری <strong>{{user_name}}</strong> با موفقیت غیر فعال شد.", + + "ENABLE_SUCCESSFUL" => "حساب کاربری <strong>{{user_name}}</strong> با موفقیت فعال شد.", + + "GROUP" => [ + 1 => "گروه", + 2 => "گروه ها", + + "CREATE" => "اضافه کردن گروه", + "CREATION_SUCCESSFUL" => "گروه <strong>{{name}}</strong> با موفقیت اضافه شد", + "DELETE" => "حذف گروه", + "DELETE_CONFIRM" => "آیا مطمئن هستید که میخواهید گروه <strong>{{name}}</strong> را حذف کنید؟", + "DELETE_DEFAULT" => "شما نمیتوانید گروه <strong>{{name}}</strong> را حذف کنید چون به عنوان گروه پیش فرض برای کاربران جدید انتخاب شده است.", + "DELETE_YES" => "بله، گروه حذف شود", + "DELETION_SUCCESSFUL" => "گروه <strong>{{name}}</strong> با موفقیت حذف شد.", + "EDIT" => "ویرایش گروه", + "ICON" => "آیکن گروه", + "ICON_EXPLAIN" => "آیکن برای اعضای گروه", + "INFO_PAGE" => "صفحه توضیحات گروه برای {{name}}", + "MANAGE" => "مدیریت گروه", + "NAME" => "نام گروه", + "NAME_EXPLAIN" => "لطفا نام گروه را وارد کنید", + "NOT_EMPTY" => "نمیتوان این کار را کرد چون هنوز کاربرانی عضو گروه <strong>{{name}}</strong> هستند.", + "PAGE_DESCRIPTION" => "لیست گروه های وب سایت شما. امکان مدیریت این گروه ها در این صفحه وجود دارد.", + "SUMMARY" => "توضیحات گروه", + "UPDATE" => "اطلاعات گروه <strong>{{name}}</strong> به روز رسانی شد." + ], + + "MANUALLY_ACTIVATED" => "حساب کاربری {{user_name}} بصورت دستی فعال شد.", + "MASTER_ACCOUNT_EXISTS" => "حساب کاربری اصلی وجود دارد!", + "MIGRATION" => [ + "REQUIRED" => "به روز رسانی پایگاه داده ها باید انجام شود" + ], + + "PERMISSION" => [ + 1 => "دسترسی", + 2 => "دسترسی ها", + + "ASSIGN_NEW" => "دادن دسترسی", + "HOOK_CONDITION" => "قلاب/شرط", + "ID" => "آی دی دسترسی", + "INFO_PAGE" => "توضیحات دسترسی {{name}}", + "MANAGE" => "مدیریت دسترسی ها", + "NOTE_READ_ONLY" => "<strong>توجه بفرمایید</strong>دسترسی ها بخشی از کد میباشند و آن ها را نمیتوان از اینترفیس تغییر داد. برای این تغییرات، مبایستی که مدیر، از <a href=\"https://learn.userfrosting.com/database/extending-the-database\" target=\"about:_blank\">دیتابیس مایگریشن</a> استفاده کند. ", + "PAGE_DESCRIPTION" => "لیست دسترسی های وب سایت شما. امکان مدیریت این دسترسی ها در این صفحه وجود دارد.", + "SUMMARY" => "توضیحات دسترسی ها", + "UPDATE" => "به روز رسانی دسترسی ها", + "VIA_ROLES" => "از طریق وظیفه ها دسترسی دارد" + ], + + "ROLE" => [ + 1 => "وظیفه", + 2 => "وظیفه ها", + + "ASSIGN_NEW" => "دادن وظیفه", + "CREATE" => "ساخت وظیفه", + "CREATION_SUCCESSFUL" => "وظیفه <strong>{{name}}</strong> با موفقیت ساخته شد", + "DELETE" => "حذف وظیفه", + "DELETE_CONFIRM" => "اطمینان دارید که میخواهید وظیفه <strong>{{name}}</strong> را حذف کنید؟", + "DELETE_DEFAULT" => "شما نمیتوانید وظیفه <strong>{{name}}</strong> را حذف کنید زیرا کاربرانی که تازه ثبت نام کنند، این وظیفه را دریافت خواهند کرد.", + "DELETE_YES" => "بله، وظیفه حذف شود", + "DELETION_SUCCESSFUL" => "وظیفه <strong>{{name}}</strong> با موفقیت حذف شد", + "EDIT" => "ویرایش وظیفه", + "HAS_USERS" => "نمیتوانید این کار را انجام دهید زیرا کاربرانی وظیفه <strong>{{name}}</strong> را هنوز دارند.", + "INFO_PAGE" => "صفحه توضیحات وظیفه {{name}}", + "MANAGE" => "مدیریت وظیفه ها", + "NAME" => "نام", + "NAME_EXPLAIN" => "لطفا برای وظیفه نامی انتخاب کنید", + "NAME_IN_USE" => "وظیفه ای با نام <strong>{{name}}</strong> موجود است", + "PAGE_DESCRIPTION" => "لیست وظیفه های وب سایت شما. امکان مدیریت این وظیفه ها در این صفحه وجود دارد.", + "PERMISSIONS_UPDATED" => "دسترسی ها برای وظیفه <strong>{{name}}</strong> به روز رسانی شد", + "SUMMARY" => "خلاصه وظیفه", + "UPDATED" => "اطلاعات وظیفه <strong>{{name}}</strong> به روز رسانی شد" + ], + + "SYSTEM_INFO" => [ + "@TRANSLATION" => "توضیحات سیستم", + + "DB_NAME" => "نام پایگاه داده", + "DB_VERSION" => "نسخه پایگاه داده", + "DIRECTORY" => "دایرکتوری پروژه", + "PHP_VERSION" => "نسخه پی اچ پی", + "SERVER" => "نرمافزار وب سرور", + "SPRINKLES" => "اسپرینکل های بارگذاری شده", + "UF_VERSION" => "نسخه یوزرفروستینگ", + "URL" => "آدرس رووت وب سایت" + ], + + "TOGGLE_COLUMNS" => "تغییر ستون", + + "USER" => [ + 1 => "کاربر", + 2 => "کاربران", + + "ADMIN" => [ + "CHANGE_PASSWORD" => "تغییر گذرواژه کاربر", + "SEND_PASSWORD_LINK" => "برای کاربر ایمیلی ارسال شود تا گذرواژه خود را تغییر دهد", + "SET_PASSWORD" => "گذرواژه کاربر را انتخاب کنید" + ], + + "ACTIVATE" => "کاربر فعال", + "CREATE" => "اضافه کردن کاربر", + "CREATED" => "کاربر <strong>{{user_name}}</strong> با موفقیت اضافه شد", + "DELETE" => "حذف کاربر", + "DELETE_CONFIRM" => "آیا اطمینان دارید که میخواهید کاربر <strong>{{name}}</strong> را حذف کنید؟", + "DELETE_YES" => "بله، کاربر حذف شود", + "DISABLE" => "غیر فعال سازی کاربر", + "EDIT" => "ویرایش کاربر", + "ENABLE" => "فعال سازی کاربر", + "INFO_PAGE" => "صفحه توضیحات کاربر {{name}}", + "LATEST" => "آخرین کاربران", + "PAGE_DESCRIPTION" => "لیستی از کاربران سایت. این صفحه به شما امکان ویرایش، فعال سازی و غیر فعال سازی کاربران را می دهد.", + "SUMMARY" => "خلاصه حساب", + "VIEW_ALL" => "تماشای همه ی کاربران", + "WITH_PERMISSION" => "کاربرانی که این دسترسی را دارند" + ], + "X_USER" => [ + 0 => "هیچ کاربری", + 1 => "{{plural}} کاربر", + 2 => "{{plural}} کاربر" + ] +]; diff --git a/login/app/sprinkles/admin/locale/fr_FR/messages.php b/login/app/sprinkles/admin/locale/fr_FR/messages.php new file mode 100755 index 0000000..82bdf3e --- /dev/null +++ b/login/app/sprinkles/admin/locale/fr_FR/messages.php @@ -0,0 +1,147 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * French message token translations for the 'admin' sprinkle. + * + * @package userfrosting\i18n\fr + * @author Louis Charette + */ + +return [ + "ACTIVITY" => [ + 1 => "Activité", + 2 => "Activités", + + "LAST" => "Dernière activité", + "PAGE" => "Une liste des activités des utilisateurs", + "TIME" => "Date de l'activité" + ], + + "CACHE" => [ + "CLEAR" => "Vider le cache", + "CLEAR_CONFIRM" => "Voulez-vous vraiment supprimer le cache du site?", + "CLEAR_CONFIRM_YES" => "Oui, vider le cache", + "CLEARED" => "Cache effacé avec succès !" + ], + + "DASHBOARD" => "Tableau de bord", + "DELETE_MASTER" => "Vous ne pouvez pas supprimer le compte principal !", + "DELETION_SUCCESSFUL" => "L'utilisateur <strong>{{user_name}}</strong> a été supprimé avec succès.", + "DETAILS_UPDATED" => "Les détails du compte de <strong>{{user_name}}</strong> ont été mis à jour", + "DISABLE_MASTER" => "Vous ne pouvez pas désactiver le compte principal !", + "DISABLE_SELF" => "Vous ne pouvez pas désactiver votre propre compte !", + "DISABLE_SUCCESSFUL" => "Le compte de l'utilisateur <strong>{{user_name}}</strong> a été désactivé avec succès.", + + "ENABLE_SUCCESSFUL" => "Le compte de l'utilisateur <strong>{{user_name}}</strong> a été activé avec succès.", + + "GROUP" => [ + 1 => "Groupe", + 2 => "Groupes", + + "CREATE" => "Créer un groupe", + "CREATION_SUCCESSFUL" => "Successfully created group <strong>{{name}}</strong>", + "DELETE" => "Supprimer le groupe", + "DELETE_CONFIRM" => "Êtes-vous certain de vouloir supprimer le groupe <strong>{{name}}</strong>?", + "DELETE_DEFAULT" => "Vous ne pouvez pas supprimer le groupe <strong>{{name}}</strong> parce que c'est le groupe par défaut pour les utilisateurs nouvellement enregistrés.", + "DELETE_YES" => "Oui, supprimer le groupe", + "DELETION_SUCCESSFUL" => "Groupe <strong>{{name}}</strong> supprimé avec succès", + "EDIT" => "Modifier le groupe", + "ICON" => "Icône", + "ICON_EXPLAIN" => "Icône des membres du groupe", + "INFO_PAGE" => "Informations sur le groupe {{name}}", + "MANAGE" => "Gérer le groupe", + "NAME" => "Nom du groupe", + "NAME_EXPLAIN" => "Spécifiez le nom du groupe", + "NOT_EMPTY" => "Vous ne pouvez pas le faire car il y a encore des utilisateurs associés au groupe <strong>{{name}}</strong>.", + "PAGE_DESCRIPTION" => "Une liste des groupes pour votre site. Fournit des outils de gestion pour éditer et supprimer des groupes.", + "UPDATE" => "Les détails du groupe <strong>{{name}}</strong> ont été enregistrés" + ], + + "MANUALLY_ACTIVATED" => "Le compte de {{user_name}} a été activé manuellement", + "MASTER_ACCOUNT_EXISTS" => "Le compte principal existe déjà !", + "MIGRATION" => [ + "REQUIRED" => "Mise à jour de la base de données requise" + ], + + "PERMISSION" => [ + 1 => "Autorisation", + 2 => "Autorisations", + + "ASSIGN_NEW" => "Assigner une nouvelle autorisation", + "HOOK_CONDITION" => "Hook/Conditions", + "MANAGE" => "Gestion des autorisations", + "PAGE_DESCRIPTION" => "Une liste des autorisations pour votre site. Fournit des outils de gestion pour modifier et supprimer des autorisations.", + "UPDATE" => "Mettre à jour les autorisations" + ], + + "ROLE" => [ + 1 => "Rôle", + 2 => "Rôles", + + "ASSIGN_NEW" => "Assigner un nouveau rôle", + "CREATE" => "Créer un rôle", + "CREATION_SUCCESSFUL" => "Rôle <strong>{{name}}</strong> créé avec succès", + "DELETE" => "Supprimer le rôle", + "DELETE_CONFIRM" => "Êtes-vous certain de vouloir supprimer le rôle <strong>{{name}}</strong>?", + "DELETE_DEFAULT" => "Vous ne pouvez pas supprimer le rôle <strong>{{name}}</strong> parce que c'est un rôle par défaut pour les utilisateurs nouvellement enregistrés.", + "DELETE_YES" => "Oui, supprimer le rôle", + "DELETION_SUCCESSFUL" => "Rôle <strong>{{name}}</strong> supprimé avec succès", + "EDIT" => "Modifier le rôle", + "HAS_USERS" => "Vous ne pouvez pas le faire parce qu'il y a encore des utilisateurs qui ont le rôle <strong>{{name}}</strong>.", + "INFO_PAGE" => "Page d'information pour le rôle {{name}}", + "MANAGE" => "Gérer les rôles", + "NAME" => "Nom du rôle", + "NAME_EXPLAIN" => "Spécifiez le nom du rôle", + "NAME_IN_USE" => "Un rôle nommé <strong>{{name}}</strong> existe déjà", + "PAGE_DESCRIPTION" => "Une liste des rôles de votre site. Fournit des outils de gestion pour modifier et supprimer des rôles.", + "PERMISSIONS_UPDATED" => "Autorisations mises à jour pour le rôle <strong>{{name}}</strong>", + "UPDATED" => "Détails mis à jour pour le rôle <strong>{{name}}</strong>" + ], + + "SYSTEM_INFO" => [ + "@TRANSLATION" => "Informations sur le système", + + "DB_NAME" => "Base de donnée", + "DB_VERSION" => "Version DB", + "DIRECTORY" => "Répertoire du projet", + "PHP_VERSION" => "Version de PHP", + "SERVER" => "Logiciel server", + "SPRINKLES" => "Sprinkles chargés", + "UF_VERSION" => "Version de UserFrosting", + "URL" => "Url racine" + ], + + "USER" => [ + 1 => "Utilisateur", + 2 => "Utilisateurs", + + "ADMIN" => [ + "CHANGE_PASSWORD" => "Changer le mot de passe", + "SEND_PASSWORD_LINK" => "Envoyer à l'utilisateur un lien qui lui permettra de choisir son propre mot de passe", + "SET_PASSWORD" => "Définissez le mot de passe de l'utilisateur comme" + ], + + "ACTIVATE" => "Autoriser l'utilisateur", + "CREATE" => "Créer un utilisateur", + "CREATED" => "L'utilisateur <strong>{{user_name}}</strong> a été créé avec succès", + "DELETE" => "Supprimer l'utilisateur", + "DELETE_CONFIRM" => "Êtes-vous certain de vouloir supprimer l'utilisateur <strong>{{name}}</strong>?", + "DELETE_YES" => "Oui, supprimer l'utilisateur", + "DISABLE" => "Désactiver l'utilisateur", + "EDIT" => "Modifier l'utilisateur", + "ENABLE" => "Activer l'utilisateur", + "INFO_PAGE" => "Page d'information de l'utilisateur pour {{name}}", + "PAGE_DESCRIPTION" => "Une liste des utilisateurs de votre site. Fournit des outils de gestion incluant la possibilité de modifier les détails de l'utilisateur, d'activer manuellement les utilisateurs, d'activer / désactiver les utilisateurs et plus.", + "LATEST" => "Derniers utilisateurs", + "VIEW_ALL" => "Voir tous les utilisateurs" + ], + "X_USER" => [ + 0 => "Aucun utilisateur", + 1 => "{{plural}} utilisateur", + 2 => "{{plural}} utilisateurs" + ] +];
\ No newline at end of file diff --git a/login/app/sprinkles/admin/locale/it_IT/messages.php b/login/app/sprinkles/admin/locale/it_IT/messages.php new file mode 100755 index 0000000..c40d5b3 --- /dev/null +++ b/login/app/sprinkles/admin/locale/it_IT/messages.php @@ -0,0 +1,160 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Italian message token translations for the 'admin' sprinkle. + * This translation was generated with Google translate. Please contribute if you are a native speaker. + * + * @package userfrosting\i18n\it + * @author Alexander Weissman + * @author Pietro Marangon (@Pe46dro) + */ + +return [ + "ACTIVITY" => [ + 1 => "Attività", + 2 => "Attività", + + "LAST" => "Ultima attività", + "PAGE" => "Un elenco delle attività degli utenti", + "TIME" => "Tempo di attività" + ], + + "CACHE" => [ + "CLEAR" => "Cancellare la cache", + "CLEAR_CONFIRM" => "Sei sicuro di voler cancellare la cache del sito?", + "CLEAR_CONFIRM_YES" => "Sì, cancellare la cache", + "CLEARED" => "La cache è stata eliminata correttamente!" + ], + + "DASHBOARD" => "Pannello di Controllo", + "NO_FEATURES_YET" => "Non sembra che alcune funzioni siano state create per questo account ... ancora. Forse non sono ancora state implementate, o forse qualcuno ha dimenticato di dare accesso. In entrambi i casi, siamo contenti di averti qui!", + "DELETE_MASTER" => "Non puoi eliminare l'account principale!", + "DELETION_SUCCESSFUL" => "Hai eliminato utente <strong>{{user_name}}</strong>.", + "DETAILS_UPDATED" => "Dettagli degli account aggiornati per l'utente <strong>{{user_name}}</strong>", + "DISABLE_MASTER" => "Non puoi disattivare l'account principale!", + "DISABLE_SELF" => "Non puoi disattivare il tuo account!", + "DISABLE_SUCCESSFUL" => "Account per l'utente <strong>{{user_name}}</strong> disattivato con successo!", + "ENABLE_SUCCESSFUL" => "Account per l'utente <strong>{{user_name}}</strong> attivato con successo.", + + "GROUP" => [ + 1 => "Gruppo", + 2 => "Gruppi", + + "CREATE" => "Creare un gruppo", + "CREATION_SUCCESSFUL" => "Ha creato con successo il gruppo <strong>{{name}}</strong>", + "DELETE" => "Elimina gruppo", + "DELETE_CONFIRM" => "Sei sicuro di voler eliminare il gruppo <strong>{{name}}</strong>?", + "DELETE_DEFAULT" => "Non puoi eliminare il gruppo <strong>{{name}}</strong> perché è il gruppo predefinito per gli utenti appena registrati.", + "DELETE_YES" => "Sì, elimini il gruppo", + "DELETION_SUCCESSFUL" => "Eliminato il gruppo <strong>{{name}}</strong> con successo", + "EDIT" => "Modifica gruppo", + "ICON" => "Icona del gruppo", + "ICON_EXPLAIN" => "Icona per i membri del gruppo", + "INFO_PAGE" => "Pagina informazioni di gruppo per <strong>{{name}}</strong>", + "MANAGE" => "Gestisci gruppo", + "NAME" => "Nome del gruppo", + "NAME_EXPLAIN" => "Inserisci un nome per il gruppo", + "NOT_EMPTY" => "Non puoi farlo perché ci sono ancora utenti associati al gruppo <strong>{{name}}</strong>.", + "PAGE_DESCRIPTION" => "Un elenco dei gruppi per il tuo sito. Fornisce strumenti di gestione per la modifica e l'eliminazione di gruppi.", + "SUMMARY" => "Riepilogo del gruppo", + "UPDATE" => "Dettagli aggiornati per il gruppo <strong>{{name}}</strong>." + ], + + "MANUALLY_ACTIVATED" => "<strong>{{user_name}}</strong> è stato attivato manualmente", + "MASTER_ACCOUNT_EXISTS" => "L'account primario esiste già!", + "MIGRATION" => [ + "REQUIRED" => "È necessario aggiornare il database" + ], + + "PERMISSION" => [ + 1 => "Autorizzazione", + 2 => "Autorizzazioni", + + "ASSIGN_NEW" => "Assegna nuova autorizzazione", + "HOOK_CONDITION" => "Hook/Condizioni", + "ID" => "ID di autorizzazione", + "INFO_PAGE" => "Pagina di informazioni sulle autorizzazioni per <strong>{{name}}</strong>", + "MANAGE" => "Gestione delle autorizzazioni", + "NOTE_READ_ONLY" => "<strong>Si prega di notare: le autorizzazioni</strong> sono considerate \"parte del codice\" e non possono essere modificate tramite l'interfaccia. Per aggiungere, rimuovere o modificare le autorizzazioni, i gestori del sito devono utilizzare <a href=\"https://learn.userfrosting.com/database/extending-the-database\" target=\"about: _blank \">migrazione del database.</a>", + "PAGE_DESCRIPTION" => "Un elenco delle autorizzazioni per il tuo sito. Fornisce strumenti di gestione per la modifica e l'eliminazione delle autorizzazioni.", + "SUMMARY" => "Sommario delle Autorizzazioni", + "UPDATE" => "Aggiorna le autorizzazioni", + "VIA_ROLES" => "Ha autorizzazione tramite ruoli" + ], + + "ROLE" => [ + 1 => "Ruolo", + 2 => "Ruoli", + + "ASSIGN_NEW" => "Assegna un nuovo ruolo", + "CREATE" => "Crea ruolo", + "CREATION_SUCCESSFUL" => "Creato con successo il ruolo <strong>{{name}}</strong>", + "DELETE" => "Elimina il ruolo", + "DELETE_CONFIRM" => "Sei sicuro di voler eliminare il ruolo <strong>{{name}}</strong>?", + "DELETE_DEFAULT" => "Non puoi eliminare il ruolo <strong>{{name}}</strong> perché è un ruolo predefinito per gli utenti appena registrati.", + "DELETE_YES" => "Sì, elimini il ruolo", + "DELETION_SUCCESSFUL" => "Eliminato il ruolo <strong>{{name}}</strong>", + "EDIT" => "Modifica ruolo", + "HAS_USERS" => "Non puoi farlo perché ci sono ancora utenti che hanno il ruolo <strong>{{name}}</strong>.", + "INFO_PAGE" => "Pagina di informazioni sui ruoli per <strong>{{name}}</strong>", + "MANAGE" => "Gestisci Ruoli", + "NAME" => "Nome", + "NAME_EXPLAIN" => "Inserisci un nome per il ruolo", + "NAME_IN_USE" => "Esiste già un ruolo denominato <strong>{{name}}</strong>", + "PAGE_DESCRIPTION" => "Un elenco dei ruoli per il tuo sito. Fornisce strumenti di gestione per la modifica e l'eliminazione di ruoli.", + "PERMISSIONS_UPDATED" => "Autorizzazioni aggiornate per ruolo <strong>{{name}}</strong>", + "SUMMARY" => "Riepilogo dei Ruoli", + "UPDATED" => "Dettagli aggiornati per ruolo <strong>{{name}}</strong>" + ], + + "SYSTEM_INFO" => [ + "@TRANSLATION" => "Informazioni sul sistema", + + "DB_NAME" => "Nome del database", + "DB_VERSION" => "Versione del database", + "DIRECTORY" => "Directory del progetto", + "PHP_VERSION" => "Versione PHP", + "SERVER" => "Software del webserver", + "SPRINKLES" => "Sprinkles caricati", + "UF_VERSION" => "Versione UserFrosting", + "URL" => "Url della radice del sito" + ], + + "TOGGLE_COLUMNS" => "Scambia le colonne", + + "USER" => [ + 1 => "Utente", + 2 => "Utenti", + + "ADMIN" => [ + "CHANGE_PASSWORD" => "Cambia Password Utente", + "SEND_PASSWORD_LINK" => "Inviare all'utente un collegamento che permetterà loro di scegliere la propria password", + "SET_PASSWORD" => "Impostare la password dell'utente come" + ], + + "ACTIVATE" => "Attiva l'utente", + "CREATE" => "Creare un utente", + "CREATED" => "Account per l'utente <strong>{{user_name}}</strong> è stato creato.", + "DELETE" => "Elimina utente", + "DELETE_CONFIRM" => "Sei sicuro di voler eliminare l'utente <strong>{{name}}</strong>?", + "DELETE_YES" => "Sì, elimina l'utente", + "DISABLE" => "Disabilita l'utente", + "EDIT" => "Modifica utente", + "ENABLE" => "Abilita l'utente", + "INFO_PAGE" => "Pagina informazioni utente per <strong>{{name}}</strong>", + "LATEST" => "Ultimi Utenti", + "PAGE_DESCRIPTION" => "Un elenco degli utenti del tuo sito. Fornisce strumenti di gestione, tra cui la possibilità di modificare i dettagli utente, attivare manualmente gli utenti, abilitare / disabilitare gli utenti e altro ancora.", + "SUMMARY" => "Riepilogo account", + "VIEW_ALL" => "Visualizza tutti gli utenti", + "WITH_PERMISSION" => "Utenti con questa autorizzazione" + ], + "X_USER" => [ + 0 => "Nessun utente", + 1 => "{{plural}} utente", + 2 => "{{plural}} utenti" + ] +]; diff --git a/login/app/sprinkles/admin/locale/pt_PT/messages.php b/login/app/sprinkles/admin/locale/pt_PT/messages.php new file mode 100755 index 0000000..0faf818 --- /dev/null +++ b/login/app/sprinkles/admin/locale/pt_PT/messages.php @@ -0,0 +1,139 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Portuguese message token translations for the 'admin' sprinkle. + * + * @package userfrosting\i18n\pt + * @author Bruno Silva (brunomnsilva@gmail.com) + */ + +return [ + "ACTIVITY" => [ + 1 => "Atividade", + 2 => "Atividades", + + "LAST" => "Última atividade", + "PAGE" => "Lista de atividade dos utilizadores", + "TIME" => "Tempo da Atividade" + ], + + "CACHE" => [ + "CLEAR" => "Limpar cache", + "CLEAR_CONFIRM" => "Tem a certeza que pretende limpar a cache do site?", + "CLEAR_CONFIRM_YES" => "Sim, limpar cache", + "CLEARED" => "Cache limpa com sucesso!" + ], + + "DASHBOARD" => "Painel de Controlo", + "DELETE_MASTER" => "Não pode apagar a conta principal!", + "DELETION_SUCCESSFUL" => "Utilizador <strong>{{user_name}}</strong> foi removido com sucesso.", + "DETAILS_UPDATED" => "Detalhes de conta atualizados para o utilizador <strong>{{user_name}}</strong>", + "DISABLE_MASTER" => "Não pode desativar a conta principal!", + "DISABLE_SUCCESSFUL" => "Conta do utilizador <strong>{{user_name}}</strong> foi desativada com sucesso.", + + "ENABLE_SUCCESSFUL" => "Conta do utilizador <strong>{{user_name}}</strong> foi ativada com sucesso.", + + "GROUP" => [ + 1 => "Grupo", + 2 => "Grupos", + + "CREATE" => "Criar grupo", + "CREATION_SUCCESSFUL" => "Grupo criado com sucesso", + "DELETE" => "Remover grupo", + "DELETION_SUCCESSFUL" => "Grupo removido com sucesso", + "DELETE_CONFIRM" => "Tem a certeza que pretende remover o grupo <strong>{{name}}</strong>?", + "DELETE_YES" => "Sim, remover grupo", + "EDIT" => "Editar grupo", + "ICON" => "Icon do grupo", + "ICON_EXPLAIN" => "Icon para membros do grupo", + "INFO_PAGE" => "Página informativa do grupo {{name}}", + //"MANAGE" => "Manage group", + "NAME" => "Nome do grupo", + "NAME_EXPLAIN" => "Por favor introduza um nome para o grupo", + "PAGE_DESCRIPTION" => "Lista de grupos do site. Contém opções para editar e remover grupos." + ], + + "MANUALLY_ACTIVATED" => "A conta de {{user_name}} foi ativada manualmente.", + "MASTER_ACCOUNT_EXISTS" => "A contra principal já existe!", + "MIGRATION" => [ + "REQUIRED" => "É necessário uma atualização da base de dados." + ], + + "PERMISSION" => [ + 1 => "Permissão", + 2 => "Permissões", + + "ASSIGN_NEW" => "Atribuir nova permissão", + "HOOK_CONDITION" => "Hook/Condições", + "MANAGE" => "Gerir permissões", + "PAGE_DESCRIPTION" => "Lista de permissões do site. Contém opções para editar e remover permissões.", + "UPDATE" => "Atualizar permissões" + ], + + "ROLE" => [ + 1 => "Cargo", + 2 => "Cargos", + + "ASSIGN_NEW" => "Atribuir novo cargo", + "CREATE" => "Criar cargo", + "CREATION_SUCCESSFUL" => "Cargo criado com sucesso", + "DELETE" => "Remover cargo", + "DELETION_SUCCESSFUL" => "Cargo removido com sucesso", + "DELETE_CONFIRM" => "Tem a certeza que pretende remover o cargo <strong>{{name}}</strong>?", + "DELETE_YES" => "Sim, remover cargo", + "EDIT" => "Editar cargo", + "INFO_PAGE" => "Página informativa do cargo {{name}}", + "MANAGE" => "Gerir cargos", + "NAME" => "Nome", + "NAME_EXPLAIN" => "Por favor introduza um nome para o cargo", + "PAGE_DESCRIPTION" => "Lista de cargos do site. Contém opções para editar e remover cargos.", + "UPDATE" => "Atualizar cargos", + "UPDATED" => "Cargo <strong>{{name}}</strong> atualizado" + ], + + "SYSTEM_INFO" => [ + "@TRANSLATION" => "Informação do sistema", + + "DB_NAME" => "Nome da base de dados", + "DB_VERSION" => "Versão da base de dados", + "DIRECTORY" => "Diretório do projeto", + "PHP_VERSION" => "Versão PHP", + "SERVER" => "Software do servidor web", + "SPRINKLES" => "Sprinkles carregados", + "UF_VERSION" => "Versão do UserFrosting", + "URL" => "Raiz (url) do site" + ], + + "USER" => [ + 1 => "Utilizador", + 2 => "Utilizadores", + + "ADMIN" => [ + "CHANGE_PASSWORD" => "Alterar password", + "SEND_PASSWORD_LINK" => "Enviar um link ao utilizador que lhe permita escolher a sua password", + "SET_PASSWORD" => "Definir a password do utilizador como" + ], + + "ACTIVATE" => "Ativar utilizador", + "CREATE" => "Criar utilizador", + "DELETE" => "Remover utilizador", + "DELETE_CONFIRM" => "Tem a certeza que pretende remover o utilizador <strong>{{name}}</strong>?", + "DELETE_YES" => "Sim, remover utilizador", + "DISABLE" => "Desativar utilizador", + "EDIT" => "Editar utilizador", + "ENABLE" => "Ativar utilizador", + "INFO_PAGE" => "Página informativa do utilizador {{name}}", + "PAGE_DESCRIPTION" => "Lista de utilizadores do site. Contém opções para editar detalhes, ativar/desativar utilizadores e outras.", + "LATEST" => "Últimos Utilizadores", + "VIEW_ALL" => "Ver todos os utilizadores" + ], + "X_USER" => [ + 0 => "Nenhum utilizador", + 1 => "{{plural}} utilizador", + 2 => "{{plural}} utilizadores" + ] +]; diff --git a/login/app/sprinkles/admin/locale/ru_RU/messages.php b/login/app/sprinkles/admin/locale/ru_RU/messages.php new file mode 100755 index 0000000..d6f6e1a --- /dev/null +++ b/login/app/sprinkles/admin/locale/ru_RU/messages.php @@ -0,0 +1,160 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Russian message token translations for the 'admin' sprinkle. + * + * @package userfrosting\i18n\ru_RU + * @author @rendername + */ + +return [ + "ACTIVITY" => [ + 1 => "Активность", + 2 => "Активность", + + "LAST" => "Последняя активность", + "PAGE" => "Список действий пользователя", + "TIME" => "Время действия" + ], + + "CACHE" => [ + "CLEAR" => "Очистить кэш", + "CLEAR_CONFIRM" => "Уверены, что хотите очистить кэш сайта?", + "CLEAR_CONFIRM_YES" => "Да, очистить кэш", + "CLEARED" => "Кэш успешно очищен !" + ], + + "DASHBOARD" => "Панель управления", + "NO_FEATURES_YET" => "Похоже, некоторые функции не были настроены для этого аккаунта... пока. Возможно, они еще не реализованы, или, может быть, кто-то забыл дать вам доступ. В любом случае, мы рады вас видеть здесь!", + "DELETE_MASTER" => "Нельзя удалить главный аккаунт!", + "DELETION_SUCCESSFUL" => "Пользователь <strong>{{user_name}}</strong> был успешно удален.", + "DETAILS_UPDATED" => "Данные для аккаунта <strong>{{user_name}}</strong> обновлены", + "DISABLE_MASTER" => "Нельзя отключить главный аккаунт!", + "DISABLE_SELF" => "Вы не можете отключить собственный аккаунт!", + "DISABLE_SUCCESSFUL" => "Пользователь <strong>{{user_name}}</strong> был успешно отключен.", + + "ENABLE_SUCCESSFUL" => "Пользователь <strong>{{user_name}}</strong> был успешно включен.", + + "GROUP" => [ + 1 => "Группа", + 2 => "Группы", + + "CREATE" => "Создать группу", + "CREATION_SUCCESSFUL" => "Успешно создана группа <strong>{{name}}</strong>", + "DELETE" => "Удалить группу", + "DELETE_CONFIRM" => "Вы уверены, что вы хотите удалить группу <strong>{{name}}</strong>?", + "DELETE_DEFAULT" => "Нельзя удалить группу <strong>{{name}}</strong>, потому что это группа по умолчанию для новых пользователей.", + "DELETE_YES" => "Да, удалить группу", + "DELETION_SUCCESSFUL" => "Успешно удалена группа <strong>{{name}}</strong>", + "EDIT" => "Редактор группы", + "ICON" => "Значок группы", + "ICON_EXPLAIN" => "Значок для членов группы", + "INFO_PAGE" => "Описание группы {{name}}", + "MANAGE" => "Управление группой", + "NAME" => "Имя группы", + "NAME_EXPLAIN" => "Пожалуйста, введите имя группы", + "NOT_EMPTY" => "Вы не можете сделать это, потому что до сих пор есть пользователи, связанные с группой <strong>{{name}}</strong>.", + "PAGE_DESCRIPTION" => "Список групп для вашего сайта. Предоставляет инструменты управления для редактирования и удаления групп.", + "SUMMARY" => "Резюме группы", + "UPDATE" => "Информация обновлена для группы <strong>{{name}}</strong>" + ], + + "MANUALLY_ACTIVATED" => "{{user_name}} аккаунт был активирован вручную", + "MASTER_ACCOUNT_EXISTS" => "Мастер-аккаунт уже существует!", + "MIGRATION" => [ + "REQUIRED" => "Необходимо обновление базы данных" + ], + + "PERMISSION" => [ + 1 => "Доступ", + 2 => "Права доступа", + + "ASSIGN_NEW" => "Назначить новые права", + "HOOK_CONDITION" => "Привязки/Условия", + "ID" => "ID доступа", + "INFO_PAGE" => "Информация о доступах для '{{name}}'", + "MANAGE" => "Управление правами", + "NOTE_READ_ONLY" => "<strong>Пожалуйста, обратите внимание:</strong> <u>права и доступы</u> считаются \"частью кода\" и не могут быть изменены через интерфейс. Чтобы добавить, удалить или изменить доступы, нужно будет использовать сайт <a href=\"https://learn.userfrosting.com/database/extending-the-database\" target=\"about:_blank\"> базы данных миграции.</a>", + "PAGE_DESCRIPTION" => "Список прав доступа для вашего сайта. Предоставляет инструменты управления для редактирования и удаления прав.", + "SUMMARY" => "Сводка доступов", + "UPDATE" => "Обновление прав", + "VIA_ROLES" => "Имеет права через роли" + ], + + "ROLE" => [ + 1 => "Роль", + 2 => "Роли", + + "ASSIGN_NEW" => "Назначение новой роли", + "CREATE" => "Создать роль", + "CREATION_SUCCESSFUL" => "Успешно создана роль <strong>{{name}}</strong>", + "DELETE" => "Удалить роль", + "DELETE_CONFIRM" => "Вы уверены, что вы хотите удалить роль <strong>{{name}}</strong>?", + "DELETE_DEFAULT" => "Невозможно удалить роль <strong>{{name}}</strong>, потому что это роль по умолчанию для новых пользователей.", + "DELETE_YES" => "Да, удалить роль", + "DELETION_SUCCESSFUL" => "Успешно удалена роль <strong>{{name}}</strong>", + "EDIT" => "Изменить роль", + "HAS_USERS" => "Вы не можете сделать это, потому что до сих пор есть пользователи, имеющие роль <strong>{{name}}</strong>.", + "INFO_PAGE" => "Информации роли для {{name}}", + "MANAGE" => "Управление ролями", + "NAME" => "Имя", + "NAME_EXPLAIN" => "Пожалуйста, укажите имя для роли", + "NAME_IN_USE" => "Роль с именем <strong>{{name}}</strong> уже существует", + "PAGE_DESCRIPTION" => "Список ролей для вашего сайта. Предоставляет инструменты управления для редактирования и удаления ролей.", + "PERMISSIONS_UPDATED" => "Права обновлены для роли <strong>{{name}}</strong>", + "SUMMARY" => "Основная информация", + "UPDATED" => "Информация для роли <strong>{{name}}</strong>" + ], + + "SYSTEM_INFO" => [ + "@TRANSLATION" => "Системная информация", + + "DB_NAME" => "Имя базы данных", + "DB_VERSION" => "Версия базы данных", + "DIRECTORY" => "Каталог проекта", + "PHP_VERSION" => "Версия PHP", + "SERVER" => "ПО Сервера", + "SPRINKLES" => "Загружены модули", + "UF_VERSION" => "Версия UserFrosting", + "URL" => "URL-адрес сайта" + ], + + "TOGGLE_COLUMNS" => "Переключатель столбцов", + + "USER" => [ + 1 => "Пользователь", + 2 => "Пользователи", + + "ADMIN" => [ + "CHANGE_PASSWORD" => "Сменить пароль пользователя", + "SEND_PASSWORD_LINK" => "Отправить пользователю ссылку, которая позволит выбрать свой собственный пароль", + "SET_PASSWORD" => "Установите пароль пользователя как" + ], + + "ACTIVATE" => "Активировать пользователя", + "CREATE" => "Создать пользователя", + "CREATED" => "Пользователь <strong>{{user_name}}</strong> был успешно создан", + "DELETE" => "Удалить пользователя", + "DELETE_CONFIRM" => "Вы уверены, что вы хотите удалить пользователя <strong>{{name}}</strong>?", + "DELETE_YES" => "Да, удалить пользователя", + "DELETED" => "Пользователь удален", + "DISABLE" => "Отключить пользователя", + "EDIT" => "Изменить пользователя", + "ENABLE" => "Включить пользователя", + "INFO_PAGE" => "Информация о пользователе для {{name}}", + "LATEST" => "Последние пользователи", + "PAGE_DESCRIPTION" => "Список пользователей для вашего сайта. Предоставляет средства управления, включая возможность редактирования сведений о пользователях, ручной активации пользователей, включения/отключения пользователей и многое другое.", + "SUMMARY" => "Сводка аккаунта", + "VIEW_ALL" => "Все пользователи", + "WITH_PERMISSION" => "Пользователи с этим доступом" + ], + "X_USER" => [ + 0 => "Нет пользователей", + 1 => "{{plural}} пользователя", + 2 => "{{plural}} пользователей" + ] +]; diff --git a/login/app/sprinkles/admin/locale/th_TH/messages.php b/login/app/sprinkles/admin/locale/th_TH/messages.php new file mode 100755 index 0000000..546232d --- /dev/null +++ b/login/app/sprinkles/admin/locale/th_TH/messages.php @@ -0,0 +1,134 @@ +<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * Thai message token translations for the 'admin' sprinkle.
+ *
+ * @package userfrosting\i18n\th
+ * @author Karuhut Komol
+ */
+
+return [
+ "ACTIVITY" => [
+ 1 => "กิจกรรม",
+ 2 => "กิจกรรม",
+
+ "LAST" => "กิจกรรมล่าสุด",
+ "PAGE" => "รายการกิจกรรมของผู้ใช้",
+ "TIME" => "เวลาที่ทำกิจกรรม"
+ ],
+
+ "CACHE" => [
+ "CLEAR" => "ล้างแคช",
+ "CLEAR_CONFIRM" => "คุณแน่ใจหรือที่จะล้างแคชของเว็บ?",
+ "CLEAR_CONFIRM_YES" => "ใช่ ล้างแคชเลย",
+ "CLEARED" => "ล้างแคชเรียบร้อยแล้ว!"
+ ],
+
+ "DASHBOARD" => "แผงควบคุม",
+ "DELETE_MASTER" => "คุณไม่สามารถลบบัญชีหลักได้!",
+ "DELETION_SUCCESSFUL" => "ลบผู้ใช้ <strong>{{user_name}}</strong> เรียบร้อยแล้ว",
+ "DETAILS_UPDATED" => "ปรับปรุงรายระเอียดบัญชีให้กับ <strong>{{user_name}}</strong> แล้ว",
+ "DISABLE_MASTER" => "คุณไม่สามารถปิดการใช้งานบัญชีหลัก!",
+ "DISABLE_SUCCESSFUL" => "ปิดการใช้งานบัญชีของผู้ใช้ <strong>{{user_name}}</strong> เรียบร้อยแล้ว",
+
+ "ENABLE_SUCCESSFUL" => "เปิดการใช้งานบัญชีของผู้ใช้ <strong>{{user_name}}</strong> เรียบร้อยแล้ว",
+
+ "GROUP" => [
+ 1 => "กลุ่ม",
+ 2 => "กลุ่ม",
+
+ "CREATE" => "สร้างกลุ่ม",
+ "DELETE" => "ลบกลุ่ม",
+ "DELETE_CONFIRM" => "คุณแน่ใจหรือที่จะลบกลุ่ม <strong>{{name}}</strong>?",
+ "DELETE_YES" => "ใช่ ลบกลุ่มนี้เลย",
+ "EDIT" => "แก้ไขกลุ่ม",
+ "ICON" => "ไอคอนกลุ่ม",
+ "ICON_EXPLAIN" => "ไอคอนสำหรับสมาชิกกลุ่ม",
+ "INFO_PAGE" => "หน้าข้อมูลกลุ่มสำหรับ {{name}}",
+ //"MANAGE" => "Manage group",
+ "NAME" => "ชื่อกลุ่ม",
+ "NAME_EXPLAIN" => "กรุณาตั้งชื่อสำหรับกลุ่มนี้",
+ "PAGE_DESCRIPTION" => "รายชื่อกลุ่มในเว็บของคุณ ประกอบไปด้วยเครื่องมือในการจัดการสำหรับการแก้ไขและลบกลุ่ม"
+ ],
+
+ "MANUALLY_ACTIVATED" => "บัญชีของ {{user_name}} ได้เปิดใช้งานเองแล้ว",
+ "MASTER_ACCOUNT_EXISTS" => "มีบัญชีหลักอยู่แล้ว!",
+ "MIGRATION" => [
+ "REQUIRED" => "ต้องการการปรับปรุงฐานข้อมูล"
+ ],
+
+ "PERMISSION" => [
+ 1 => "สิทธิการเข้าถึง",
+ 2 => "สิทธิการเข้าถึง",
+
+ "ASSIGN_NEW" => "กำหนดสิทธิการเข้าถึงใหม่",
+ "HOOK_CONDITION" => "ข้อกำหนด/เงื่อนไข",
+ "MANAGE" => "จัดการสิทธิการเข้าถึง",
+ "PAGE_DESCRIPTION" => "รายการสิทธิการเข้าถึงในเว็บของคุณ ประกอบไปด้วยเครื่องมือในการจัดการสำหรับการแก้ไขและลบสิทธิการเข้าถึง",
+ "UPDATE" => "ปรับปรุงสิทธิการเข้าถึง"
+ ],
+
+ "ROLE" => [
+ 1 => "ตำแหน่ง",
+ 2 => "ตำแหน่ง",
+
+ "ASSIGN_NEW" => "กำหนดตำแหน่งใหม่",
+ "CREATE" => "สร้างตำแหน่ง",
+ "DELETE" => "ลบตำแหน่ง",
+ "DELETE_CONFIRM" => "คุณแน่ใจหรือที่จะลบตำแหน่ง <strong>{{name}}</strong>?",
+ "DELETE_YES" => "ใช่ ลบตำแหน่งนี้เลย",
+ "EDIT" => "แก้ไขตำแหน่ง",
+ "INFO_PAGE" => "หน้าข้อมูลตำแหน่งสำหรับ {{name}}",
+ "MANAGE" => "จัดการตำแหน่ง",
+ "NAME" => "ชื่อ",
+ "NAME_EXPLAIN" => "กรุณาตั้งชื่อสำหรับตำแหน่งนี้",
+ "PAGE_DESCRIPTION" => "รายชื่อตำแหน่งในเว็บของคุณ ประกอบไปด้วยเครื่องมือในการจัดการสำหรับแก้ไขและลบตำแหน่ง",
+ "UPDATED" => "ปรับปรุงตำแหน่ง"
+ ],
+
+ "SYSTEM_INFO" => [
+ "@TRANSLATION" => "ข้อมูลระบบ",
+
+ "DB_NAME" => "ชื่อฐานข้อมูล",
+ "DB_VERSION" => "เวอร์ชั่นฐานข้อมูล",
+ "DIRECTORY" => "ไดเรกทอรีของโปรเจค",
+ "PHP_VERSION" => "เวอร์ชั่น PHP",
+ "SERVER" => "ซอฟต์แวร์เว็บเซิร์ฟเวอร์",
+ "SPRINKLES" => "Sprinkles ที่ถูกโหลด",
+ "UF_VERSION" => "เวอร์ชั่น UserFrosting",
+ "URL" => "URL ของรากเว็บไซต์"
+ ],
+
+ "USER" => [
+ 1 => "ผู้ใช้",
+ 2 => "ผู้ใช้",
+
+ "ADMIN" => [
+ "CHANGE_PASSWORD" => "เปลี่ยนรหัสผ่านผู้ใช้",
+ "SEND_PASSWORD_LINK" => "ส่งลิงก์ที่จะอนุญาตให้ผู้ใช้เลือกรหัสผ่านเองให้กับผู้ใช้",
+ "SET_PASSWORD" => "ตั้งรหัสผ่านของผู้ใช้เป็น"
+ ],
+
+ "ACTIVATE" => "เปิดใช้งานผู้ใช้",
+ "CREATE" => "สร้างผู้ใช้",
+ "DELETE" => "ลบผู้ใช้",
+ "DELETE_CONFIRM" => "คุณแน่ใจหรือที่จะลบผู้ใช้ <strong>{{name}}</strong>?",
+ "DELETE_YES" => "ใช่ ลบผู้ใช้นี้เลย",
+ "DISABLE" => "ปิดการใช้งานผู้ใช้",
+ "EDIT" => "แก้ไขผู้ใช้",
+ "ENABLE" => "เปิดการใช้งานผู้ใช้",
+ "INFO_PAGE" => "หน้าข้อมูลของผู้ใช้ {{name}}",
+ "PAGE_DESCRIPTION" => "รายชื่อผู้ใช้ในเว็บของคุณ ประกอบไปด้วยเครื่องมือสำหรับการจัดการ รวมทั้งความสามารถในการแก้ไขรายละเอียดผู้ใช้ การเปิดใช้งานผู้ใช้ การเปิด/ปิดบัญชีผู้ใช้และอื่น ๆ",
+ "LATEST" => "ผู้ใช้ล่าสุด",
+ "VIEW_ALL" => "ดูผู้ใช้ทั้งหมด"
+ ],
+ "X_USER" => [
+ 0 => "ไม่มีผู้ใช้",
+ 1 => "{{plural}} ผู้ใช้",
+ 2 => "{{plural}} ผู้ใช้"
+ ]
+];
\ No newline at end of file diff --git a/login/app/sprinkles/admin/locale/tr/messages.php b/login/app/sprinkles/admin/locale/tr/messages.php new file mode 100755 index 0000000..25d5633 --- /dev/null +++ b/login/app/sprinkles/admin/locale/tr/messages.php @@ -0,0 +1,160 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Turkish message token translations for the 'admin' sprinkle. + * + * @package userfrosting\i18n\tr + * @author Dumblledore + */ + +return [ + "ACTIVITY" => [ + 1 => "Etkinlik", + 2 => "Etkinlikler", + + "LAST" => "Son Etkinlik", + "PAGE" => "Kullanıcı etkinliklerinin listesi", + "TIME" => "Aktivite zamanı" + ], + + "CACHE" => [ + "CLEAR" => "Önbelleği temizle", + "CLEAR_CONFIRM" => "Site önbelleğini temizlemek istediğine emin misin?", + "CLEAR_CONFIRM_YES" => "Evet, önbelleği temizle", + "CLEARED" => "Önbellek temizlenmesi başarıyla tamamlandı!" + ], + + "DASHBOARD" => "Pano", + "NO_FEATURES_YET" => "Bu hesap için herhangi bir özellik ayarlanmış gibi görünmüyor... Henüz. Belki de henüz uygulanmadı, veya belki birisi size erişim vermeyi unutttu. Her iki durumda da sizi aramızda gördüğümüze sevindik!", + "DELETE_MASTER" => "Ana hesabı silemezsiniz!", + "DELETION_SUCCESSFUL" => "Kullanıcı<strong>{{user_name}}</strong> silme işlemi başarıyla tamamlandı.", + "DETAILS_UPDATED" => "Kullanıcı<strong>{{user_name}}</strong> için güncel hesap detayları", + "DISABLE_MASTER" => "Ana hesabı devre dışı bırakamazsınız!", + "DISABLE_SELF" => "Kendi hesabınızın etkinliğini sonlandıramazsınız!", + "DISABLE_SUCCESSFUL" => "Kullanıcı hesabın <strong>{{user_name}}</strong>başarıyla devre dışı bırakıldı.", + + "ENABLE_SUCCESSFUL" => "Kullanıcı hesabın<strong>{{user_name}}</strong>başarıyla etkinleştirildi.", + + "GROUP" => [ + 1 => "Grup", + 2 => "Grıplar", + + "CREATE" => "Grup oluşturmak", + "CREATION_SUCCESSFUL" => "Grup oluşturma başarılı<strong>{{name}}</strong>", + "DELETE" => "Grubu sil", + "DELETE_CONFIRM" => "Grubu silmek istediğine emin misin<strong>{{name}}</strong>?", + "DELETE_DEFAULT" => "Grubu silemezsin<strong>{{name}}</strong> çünkü o yeni kayıtlanan kullanıcılar için varsayılan grup.", + "DELETE_YES" => "Evet, grubu sil", + "DELETION_SUCCESSFUL" => "Grup silme başarılı<strong>{{name}}</strong>", + "EDIT" => "Grubu düzenle", + "ICON" => "Grup ikonu", + "ICON_EXPLAIN" => "Grup iyileri için ikon", + "INFO_PAGE" => "Grup bilgisi sayfası {{name}} için", + "MANAGE" => "Grubu yönet", + "NAME" => "Grup adı", + "NAME_EXPLAIN" => "Lütfen grup için bir isim giriniz", + "NOT_EMPTY" => "Bunu yapamazsınız çünkü hala grupla ilişkili kullanıcılar var<strong>{{name}}</strong>.", + "PAGE_DESCRIPTION" => "Siten için grupların bir listesi. Grupları silmek ve düzenlemek için yönetim araçları sağlar.", + "SUMMARY" => "Grup özeti", + "UPDATE" => "Grup için detaylar güncellendi<strong>{{name}}</strong>" + ], + + "MANUALLY_ACTIVATED" => "{{user_name}}'ın hesabı el ile aktifleştirildi", + "MASTER_ACCOUNT_EXISTS" => "Ana hesap zaten mevcut!", + "MIGRATION" => [ + "REQUIRED" => "Veritabanını güncellemek gerek" + ], + + "PERMISSION" => [ + 1 => "İzin", + 2 => "İzinler", + + "ASSIGN_NEW" => "Yeni izin ata", + "HOOK_CONDITION" => "Kanca/Koşullar", + "ID" => "İzin Kimliği", + "INFO_PAGE" => "{{name}} için izin bilgi sayfası", + "MANAGE" => "İzinleri yönet", + "NOTE_READ_ONLY" => "<strong>Lütfen Dikkat</strong> izinler ''bir kodun parçası'' olarak kabul edilir ve arayüz aracılığıyla değiştirilemez. İzln eklemek, kaldırmak ya da değiştirmek için site bakımcıları bir <a href=\"https://learn.userfrosting.com/database/extending-the-database\" target=\"about:_blank\">veritabanı geçişi</a> kullanmalıdır", + "PAGE_DESCRIPTION" => "Siteniz için izinlerin bir listesi. Düzenleme yapmak ve izinleri kaldırmak yönetim araçları temin eder.", + "SUMMARY" => "İzin Özeti", + "UPDATE" => "İzinlerin Güncellenmesi", + "VIA_ROLES" => "Roller ile izin alımı" + ], + + "ROLE" => [ + 1 => "Rol", + 2 => "Roller", + + "ASSIGN_NEW" => "Yeni rol ata", + "CREATE" => "Rol oluştur", + "CREATION_SUCCESSFUL" => "Rol oluşturma başarılı <strong>{{name}}</strong>", + "DELETE" => "Rolü sil", + "DELETE_CONFIRM" => "Rolü silmek istediğine emin misin <strong>{{name}}</strong>?", + "DELETE_DEFAULT" => "Rolü silemezsin <strong>{{name}}</strong> çünkü o kaydolmuş kullanıcılar için varsayılan bir rol.", + "DELETE_YES" => "Evet, rolü sil", + "DELETION_SUCCESSFUL" => "Rol başarıyla silindi<strong>{{name}}</strong>", + "EDIT" => "Rolü düzenle", + "HAS_USERS" => "Bunu yapamazsın çünkü hala bu rol ile bağlantılı kullanıcılar var<strong>{{name}}</strong>.", + "INFO_PAGE" => "{{name}} için rol bilgi sayfası", + "MANAGE" => "Rolleri yönet", + "NAME" => "Ad", + "NAME_EXPLAIN" => "Lütfen rol için bir ad giriniz", + "NAME_IN_USE" => "<strong>{{name}}</strong> adında bir rol zaten mevcut", + "PAGE_DESCRIPTION" => "Siteniz için rollerin bir listesi. Düzenlemek ve rolleri silmek için yönetim araçları sağlar.", + "PERMISSIONS_UPDATED" => "Rol için izinler güncellendi<strong>{{name}}</strong>", + "SUMMARY" => "Rol özeti", + "UPDATED" => "Rol için detaylar güncellendi<strong>{{name}}</strong>" + ], + + "SYSTEM_INFO" => [ + "@TRANSLATION" => "Sistem bilgisi", + + "DB_NAME" => "Veritabanı adı", + "DB_VERSION" => "Veritabanı sürümü", + "DIRECTORY" => "Proje dizini", + "PHP_VERSION" => "PHP sürümü", + "SERVER" => "Web sunucu yazılımı", + "SPRINKLES" => "Yüklü serpintiler", + "UF_VERSION" => "UserFrosting sürümü", + "URL" => "Site kök url" + ], + + "TOGGLE_COLUMNS" => "Sütünları değiştirme", + + "USER" => [ + 1 => "Kullanıcı", + 2 => "Kullanıcılar", + + "ADMIN" => [ + "CHANGE_PASSWORD" => "Kullanıcı şifresini değiştir", + "SEND_PASSWORD_LINK" => "Kullanıcıya kendi şifresini seçebileceği bir bağlantı gönder", + "SET_PASSWORD" => "Kullanıcının şifresi olarak ayarla" + ], + + "ACTIVATE" => "Aktif Kullanıcı", + "CREATE" => "Kullanıcı oluştur", + "CREATED" => "Kullanıcı <strong>{{user_name}}</strong> başarıyla oluşturuldu", + "DELETE" => "Kullanıcıyı sil", + "DELETE_CONFIRM" => "Kullanıcıyı silmek istediğinden emin misin?<strong>{{name}}</strong>?", + "DELETE_YES" => "Evet, kullanıcıyı sil", + "DELETED" => "Kullanıcı silindi", + "DISABLE" => "Kullanıcı devre dışı", + "EDIT" => "Kullanıcıyı düzenle", + "ENABLE" => "Kullanıcı etkin", + "INFO_PAGE" => "{{name}} için kullanıcı bilgisi", + "LATEST" => "Son Kullanıcılar", + "PAGE_DESCRIPTION" => "Siten için kullanıcıların listesi. Kullanıcı detaylarını düzenlemek, elle kullanıcıları aktifleştirmek, kullanıcıları etkinleştirme/devre dışı bırakma, ve daha fazlası için yönetimsel araçlar sağlar.", + "SUMMARY" => "Hesap özeti", + "VIEW_ALL" => "Tüm kullanıcıları göster", + "WITH_PERMISSION" => "Bu izni olan kullanıcılar" + ], + "X_USER" => [ + 0 => "Kullanıcı yok", + 1 => "{{plural}} kullanıcı", + 2 => "{{plural}} kullanıcılar" + ] +]; diff --git a/login/app/sprinkles/admin/locale/zh_CN/messages.php b/login/app/sprinkles/admin/locale/zh_CN/messages.php new file mode 100755 index 0000000..2adc8c8 --- /dev/null +++ b/login/app/sprinkles/admin/locale/zh_CN/messages.php @@ -0,0 +1,161 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Chinese message token translations for the 'admin' sprinkle. + * + * @package userfrosting\i18n\zh_CN + * @author @BruceGui (https://github.com/BruceGui) + */ + +return [ + "ACTIVITY" => [ + 1 => "活动", + 2 => "活动", + + "LAST" => "最后活动", + "PAGE" => "用户活动列表", + "TIME" => "活动时间" + ], + + "ADMIN" => [ + "PANEL" => "控制台" + ], + + "CACHE" => [ + "CLEAR" => "清除缓存", + "CLEAR_CONFIRM" => "你确定要清除此网站的缓存?", + "CLEAR_CONFIRM_YES" => "是的, 清除缓存", + "CLEARED" => "缓存成功清除 !" + ], + + "DASHBOARD" => "仪表盘", + "DELETE_MASTER" => "你不能删除超级账户!", + "DELETION_SUCCESSFUL" => "用户 <strong>{{user_name}}</strong> 删除成功.", + "DETAILS_UPDATED" => "用户 <strong>{{user_name}}</strong> 更新成功", + "DISABLE_MASTER" => "你不能禁用超级用户!", + "DISABLE_SELF" => "不能禁用你自己的账户!", + "DISABLE_SUCCESSFUL" => "用户名<strong>{{user_name}}</strong> 成功禁用.", + + "ENABLE_SUCCESSFUL" => "用户名 <strong>{{user_name}}</strong> 启用成功.", + + "GROUP" => [ + 1 => "组", + 2 => "组", + + "CREATE" => "新建组", + "CREATION_SUCCESSFUL" => "成功创建组 <strong>{{name}}</strong>", + "DELETE" => "删除组", + "DELETE_CONFIRM" => "确定要删除组 <strong>{{name}}</strong>?", + "DELETE_DEFAULT" => "你无法删除组 <strong>{{name}}</strong> 因为这是新注册用户的默认组.", + "DELETE_YES" => "是, 删除组", + "DELETION_SUCCESSFUL" => "成功删除组<strong>{{name}}</strong>", + "EDIT" => "编辑组", + "ICON" => "组图标", + "ICON_EXPLAIN" => "组成员图标", + "INFO_PAGE" => "{{name}} 组的信息页", + "NAME" => "组名", + "NAME_EXPLAIN" => "请为组取一个名字", + "NOT_EMPTY" => "你不能这样做,因为还有用户在组 <strong>{{name}}</strong>.", + "PAGE_DESCRIPTION" => "网站组列表. 请管理编辑和删除组的工具.", + "SUMMARY" => "组简介", + "UPDATE" => "组<strong>{{name}}</strong>的信息已经更新" + ], + + "MANUALLY_ACTIVATED" => "{{user_name}} 账户已手动激活", + "MASTER_ACCOUNT_EXISTS" => "超级账户已存在!", + "MIGRATION" => [ + "REQUIRED" => "数据库需要更新" + ], + + "PERMISSION" => [ + 1 => "权限", + 2 => "权限", + + "ASSIGN_NEW" => "分配新权限", + "HOOK_CONDITION" => "Hook/条件", + "ID" => "权限ID", + "INFO_PAGE" => "用户 '{{name}}' 的权限页", + "MANAGE" => "管理权限", + "NOTE_READ_ONLY" => "<strong>请注意:</strong> 权限是 \"part of the code\" 不能通过界面修改. 如果要添加、删除、编辑权限, 网站维护者需要 <a href=\"https://learn.userfrosting.com/database/extending-the-database\" target=\"about:_blank\">进行数据库迁移.</a>", + "PAGE_DESCRIPTION" => "网站的权限列表. 提供编辑和删除权限的工具.", + "SUMMARY" => "权限概要", + "UPDATE" => "更新权限", + "VIA_ROLES" => "权限通过用户了" + ], + + "ROLE" => [ + 1 => "角色", + 2 => "角色", + + "ASSIGN_NEW" => "分配角色", + "CREATE" => "创建角色", + "CREATION_SUCCESSFUL" => "成功创建角色 <strong>{{name}}</strong>", + "DELETE" => "删除角色", + "DELETE_CONFIRM" => "你确定要删除角色 <strong>{{name}}</strong>?", + "DELETE_DEFAULT" => "你无法删除角色<strong>{{name}}</strong> 因为这是新注册用户的默认角色.", + "DELETE_YES" => "是, 删除角色", + "DELETION_SUCCESSFUL" => "成功删除角色 <strong>{{name}}</strong>", + "EDIT" => "编辑角色", + "HAS_USERS" => "你不能这样做,因为还有用户拥有角色 <strong>{{name}}</strong>.", + "INFO_PAGE" => "{{name}}角色的权限页", + "MANAGE" => "管理角色", + "NAME" => "名字", + "NAME_EXPLAIN" => "请为角色输入名字", + "NAME_IN_USE" => "角色名 <strong>{{name}}</strong> 以存在", + "PAGE_DESCRIPTION" => "网站的角色列表. 请管理编辑和删除角色的工具.", + "PERMISSIONS_UPDATED" => "角色 <strong>{{name}}</strong> 的权限更新成功", + "SUMMARY" => "角色概要", + "UPDATED" => "角色 <strong>{{name}}</strong> 更新成功" + ], + + "SYSTEM_INFO" => [ + "@TRANSLATION" => "系统信息", + + "DB_NAME" => "数据库", + "DB_VERSION" => "数据库版本", + "DIRECTORY" => "工程目录", + "PHP_VERSION" => "PHP 版本", + "SERVER" => "网站服务器", + "SPRINKLES" => "加载的 sprinkles", + "UF_VERSION" => "UserFrosting 版本", + "URL" => "网站根目录" + ], + + "TOGGLE_COLUMNS" => "加载列", + + "USER" => [ + 1 => "用户", + 2 => "用户", + + "ADMIN" => [ + "CHANGE_PASSWORD" => "修改用户密码", + "SEND_PASSWORD_LINK" => "发送允许该用户修改密码的链接", + "SET_PASSWORD" => "设置用户的密码为" + ], + + "ACTIVATE" => "激活用户", + "CREATE" => "新建用户", + "CREATED" => "用户 <strong>{{user_name}}</strong> 新建成功", + "DELETE" => "删除用户", + "DELETE_CONFIRM" => "确定要删除用户 <strong>{{name}}</strong>?", + "DELETE_YES" => "是, 删除用户", + "DISABLE" => "禁用用户", + "EDIT" => "编辑用户", + "ENABLE" => "启用用户", + "INFO_PAGE" => "用户 {{name}} 的信息页", + "LATEST" => "最新用户", + "PAGE_DESCRIPTION" => "网站用户列表. 提供编辑用户信息, 手动激活用户, 启用/禁用 用户等工具.", + "SUMMARY" => "账户概要", + "VIEW_ALL" => "浏览所有用户", + "WITH_PERMISSION" => "有此权限的用户" + ], + "X_USER" => [ + 0 => "没有用户", + 1 => "{{plural}} 用户", + 2 => "{{plural}} 用户" + ] +]; diff --git a/login/app/sprinkles/admin/routes/activities.php b/login/app/sprinkles/admin/routes/activities.php new file mode 100755 index 0000000..b324553 --- /dev/null +++ b/login/app/sprinkles/admin/routes/activities.php @@ -0,0 +1,19 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ + +/** + * Routes for administrative activity monitoring. + */ +$app->group('/activities', function () { + $this->get('', 'UserFrosting\Sprinkle\Admin\Controller\ActivityController:pageList') + ->setName('uri_activities'); +})->add('authGuard'); + +$app->group('/api/activities', function () { + $this->get('', 'UserFrosting\Sprinkle\Admin\Controller\ActivityController:getList'); +})->add('authGuard'); diff --git a/login/app/sprinkles/admin/routes/admin.php b/login/app/sprinkles/admin/routes/admin.php new file mode 100755 index 0000000..1a8c31a --- /dev/null +++ b/login/app/sprinkles/admin/routes/admin.php @@ -0,0 +1,23 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ + +/** + * Routes for administrative panel management. + */ +$app->group('/dashboard', function () { + $this->get('', 'UserFrosting\Sprinkle\Admin\Controller\AdminController:pageDashboard') + ->setName('dashboard'); +})->add('authGuard'); + +$app->group('/api/dashboard', function () { + $this->post('/clear-cache', 'UserFrosting\Sprinkle\Admin\Controller\AdminController:clearCache'); +})->add('authGuard'); + +$app->group('/modals/dashboard', function () { + $this->get('/clear-cache', 'UserFrosting\Sprinkle\Admin\Controller\AdminController:getModalConfirmClearCache'); +})->add('authGuard'); diff --git a/login/app/sprinkles/admin/routes/groups.php b/login/app/sprinkles/admin/routes/groups.php new file mode 100755 index 0000000..e861960 --- /dev/null +++ b/login/app/sprinkles/admin/routes/groups.php @@ -0,0 +1,39 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ + +/** + * Routes for administrative group management. + */ +$app->group('/groups', function () { + $this->get('', 'UserFrosting\Sprinkle\Admin\Controller\GroupController:pageList') + ->setName('uri_groups'); + + $this->get('/g/{slug}', 'UserFrosting\Sprinkle\Admin\Controller\GroupController:pageInfo'); +})->add('authGuard'); + +$app->group('/api/groups', function () { + $this->delete('/g/{slug}', 'UserFrosting\Sprinkle\Admin\Controller\GroupController:delete'); + + $this->get('', 'UserFrosting\Sprinkle\Admin\Controller\GroupController:getList'); + + $this->get('/g/{slug}', 'UserFrosting\Sprinkle\Admin\Controller\GroupController:getInfo'); + + $this->get('/g/{slug}/users', 'UserFrosting\Sprinkle\Admin\Controller\GroupController:getUsers'); + + $this->post('', 'UserFrosting\Sprinkle\Admin\Controller\GroupController:create'); + + $this->put('/g/{slug}', 'UserFrosting\Sprinkle\Admin\Controller\GroupController:updateInfo'); +})->add('authGuard'); + +$app->group('/modals/groups', function () { + $this->get('/confirm-delete', 'UserFrosting\Sprinkle\Admin\Controller\GroupController:getModalConfirmDelete'); + + $this->get('/create', 'UserFrosting\Sprinkle\Admin\Controller\GroupController:getModalCreate'); + + $this->get('/edit', 'UserFrosting\Sprinkle\Admin\Controller\GroupController:getModalEdit'); +})->add('authGuard'); diff --git a/login/app/sprinkles/admin/routes/permissions.php b/login/app/sprinkles/admin/routes/permissions.php new file mode 100755 index 0000000..4df04c8 --- /dev/null +++ b/login/app/sprinkles/admin/routes/permissions.php @@ -0,0 +1,25 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ + +/** + * Routes for administrative permission management. + */ +$app->group('/permissions', function () { + $this->get('', 'UserFrosting\Sprinkle\Admin\Controller\PermissionController:pageList') + ->setName('uri_permissions'); + + $this->get('/p/{id}', 'UserFrosting\Sprinkle\Admin\Controller\PermissionController:pageInfo'); +})->add('authGuard'); + +$app->group('/api/permissions', function () { + $this->get('', 'UserFrosting\Sprinkle\Admin\Controller\PermissionController:getList'); + + $this->get('/p/{id}', 'UserFrosting\Sprinkle\Admin\Controller\PermissionController:getInfo'); + + $this->get('/p/{id}/users', 'UserFrosting\Sprinkle\Admin\Controller\PermissionController:getUsers'); +})->add('authGuard'); diff --git a/login/app/sprinkles/admin/routes/roles.php b/login/app/sprinkles/admin/routes/roles.php new file mode 100755 index 0000000..1de12e8 --- /dev/null +++ b/login/app/sprinkles/admin/routes/roles.php @@ -0,0 +1,45 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ + +/** + * Routes for administrative role management. + */ +$app->group('/roles', function () { + $this->get('', 'UserFrosting\Sprinkle\Admin\Controller\RoleController:pageList') + ->setName('uri_roles'); + + $this->get('/r/{slug}', 'UserFrosting\Sprinkle\Admin\Controller\RoleController:pageInfo'); +})->add('authGuard'); + +$app->group('/api/roles', function () { + $this->delete('/r/{slug}', 'UserFrosting\Sprinkle\Admin\Controller\RoleController:delete'); + + $this->get('', 'UserFrosting\Sprinkle\Admin\Controller\RoleController:getList'); + + $this->get('/r/{slug}', 'UserFrosting\Sprinkle\Admin\Controller\RoleController:getInfo'); + + $this->get('/r/{slug}/permissions', 'UserFrosting\Sprinkle\Admin\Controller\RoleController:getPermissions'); + + $this->get('/r/{slug}/users', 'UserFrosting\Sprinkle\Admin\Controller\RoleController:getUsers'); + + $this->post('', 'UserFrosting\Sprinkle\Admin\Controller\RoleController:create'); + + $this->put('/r/{slug}', 'UserFrosting\Sprinkle\Admin\Controller\RoleController:updateInfo'); + + $this->put('/r/{slug}/{field}', 'UserFrosting\Sprinkle\Admin\Controller\RoleController:updateField'); +})->add('authGuard'); + +$app->group('/modals/roles', function () { + $this->get('/confirm-delete', 'UserFrosting\Sprinkle\Admin\Controller\RoleController:getModalConfirmDelete'); + + $this->get('/create', 'UserFrosting\Sprinkle\Admin\Controller\RoleController:getModalCreate'); + + $this->get('/edit', 'UserFrosting\Sprinkle\Admin\Controller\RoleController:getModalEdit'); + + $this->get('/permissions', 'UserFrosting\Sprinkle\Admin\Controller\RoleController:getModalEditPermissions'); +})->add('authGuard'); diff --git a/login/app/sprinkles/admin/routes/users.php b/login/app/sprinkles/admin/routes/users.php new file mode 100755 index 0000000..f1b2243 --- /dev/null +++ b/login/app/sprinkles/admin/routes/users.php @@ -0,0 +1,51 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ + +/** + * Routes for administrative user management. + */ +$app->group('/users', function () { + $this->get('', 'UserFrosting\Sprinkle\Admin\Controller\UserController:pageList') + ->setName('uri_users'); + + $this->get('/u/{user_name}', 'UserFrosting\Sprinkle\Admin\Controller\UserController:pageInfo'); +})->add('authGuard'); + +$app->group('/api/users', function () { + $this->delete('/u/{user_name}', 'UserFrosting\Sprinkle\Admin\Controller\UserController:delete'); + + $this->get('', 'UserFrosting\Sprinkle\Admin\Controller\UserController:getList'); + + $this->get('/u/{user_name}', 'UserFrosting\Sprinkle\Admin\Controller\UserController:getInfo'); + + $this->get('/u/{user_name}/activities', 'UserFrosting\Sprinkle\Admin\Controller\UserController:getActivities'); + + $this->get('/u/{user_name}/roles', 'UserFrosting\Sprinkle\Admin\Controller\UserController:getRoles'); + + $this->get('/u/{user_name}/permissions', 'UserFrosting\Sprinkle\Admin\Controller\UserController:getPermissions'); + + $this->post('', 'UserFrosting\Sprinkle\Admin\Controller\UserController:create'); + + $this->post('/u/{user_name}/password-reset', 'UserFrosting\Sprinkle\Admin\Controller\UserController:createPasswordReset'); + + $this->put('/u/{user_name}', 'UserFrosting\Sprinkle\Admin\Controller\UserController:updateInfo'); + + $this->put('/u/{user_name}/{field}', 'UserFrosting\Sprinkle\Admin\Controller\UserController:updateField'); +})->add('authGuard'); + +$app->group('/modals/users', function () { + $this->get('/confirm-delete', 'UserFrosting\Sprinkle\Admin\Controller\UserController:getModalConfirmDelete'); + + $this->get('/create', 'UserFrosting\Sprinkle\Admin\Controller\UserController:getModalCreate'); + + $this->get('/edit', 'UserFrosting\Sprinkle\Admin\Controller\UserController:getModalEdit'); + + $this->get('/password', 'UserFrosting\Sprinkle\Admin\Controller\UserController:getModalEditPassword'); + + $this->get('/roles', 'UserFrosting\Sprinkle\Admin\Controller\UserController:getModalEditRoles'); +})->add('authGuard'); diff --git a/login/app/sprinkles/admin/schema/requests/group/create.yaml b/login/app/sprinkles/admin/schema/requests/group/create.yaml new file mode 100755 index 0000000..8f5261c --- /dev/null +++ b/login/app/sprinkles/admin/schema/requests/group/create.yaml @@ -0,0 +1,35 @@ +--- +name: + validators: + required: + label: "&NAME" + message: VALIDATE.REQUIRED + length: + label: "&NAME" + min: 1 + max: 255 + message: VALIDATE.LENGTH_RANGE + transformations: + - trim +slug: + validators: + required: + label: "&SLUG" + message: VALIDATE.REQUIRED + length: + label: "&SLUG" + min: 1 + max: 255 + message: VALIDATE.LENGTH_RANGE + transformations: + - trim +icon: + validators: + length: + label: "&ICON" + min: 1 + max: 100 + message: VALIDATE.LENGTH_RANGE + required: + message: VALIDATE.REQUIRED +description: diff --git a/login/app/sprinkles/admin/schema/requests/group/edit-info.yaml b/login/app/sprinkles/admin/schema/requests/group/edit-info.yaml new file mode 100755 index 0000000..6aa3f28 --- /dev/null +++ b/login/app/sprinkles/admin/schema/requests/group/edit-info.yaml @@ -0,0 +1,27 @@ +--- +name: + validators: + length: + label: "&NAME" + min: 1 + max: 255 + message: VALIDATE.LENGTH_RANGE + transformations: + - trim +slug: + validators: + length: + label: "&SLUG" + min: 1 + max: 255 + message: VALIDATE.LENGTH_RANGE + transformations: + - trim +icon: + validators: + length: + label: "&ICON" + min: 1 + max: 100 + message: VALIDATE.LENGTH_RANGE +description: diff --git a/login/app/sprinkles/admin/schema/requests/group/get-by-slug.yaml b/login/app/sprinkles/admin/schema/requests/group/get-by-slug.yaml new file mode 100755 index 0000000..2aa41b5 --- /dev/null +++ b/login/app/sprinkles/admin/schema/requests/group/get-by-slug.yaml @@ -0,0 +1,6 @@ +--- +slug: + validators: + required: + label: "&SLUG" + message: VALIDATE.REQUIRED diff --git a/login/app/sprinkles/admin/schema/requests/role/create.yaml b/login/app/sprinkles/admin/schema/requests/role/create.yaml new file mode 100755 index 0000000..8004184 --- /dev/null +++ b/login/app/sprinkles/admin/schema/requests/role/create.yaml @@ -0,0 +1,26 @@ +--- +name: + validators: + required: + label: "&NAME" + message: VALIDATE.REQUIRED + length: + label: "&NAME" + min: 1 + max: 255 + message: VALIDATE.LENGTH_RANGE + transformations: + - trim +slug: + validators: + required: + label: "&SLUG" + message: VALIDATE.REQUIRED + length: + label: "&SLUG" + min: 1 + max: 255 + message: VALIDATE.LENGTH_RANGE + transformations: + - trim +description: diff --git a/login/app/sprinkles/admin/schema/requests/role/edit-field.yaml b/login/app/sprinkles/admin/schema/requests/role/edit-field.yaml new file mode 100755 index 0000000..05c1b2d --- /dev/null +++ b/login/app/sprinkles/admin/schema/requests/role/edit-field.yaml @@ -0,0 +1,24 @@ +--- +name: + validators: + length: + label: "&NAME" + min: 1 + max: 255 + message: VALIDATE.LENGTH_RANGE + transformations: + - trim +slug: + validators: + length: + label: "&SLUG" + min: 1 + max: 255 + message: VALIDATE.LENGTH_RANGE + transformations: + - trim +description: +permissions: + validators: + array: + message: VALIDATE.ARRAY diff --git a/login/app/sprinkles/admin/schema/requests/role/edit-info.yaml b/login/app/sprinkles/admin/schema/requests/role/edit-info.yaml new file mode 100755 index 0000000..1fa36c8 --- /dev/null +++ b/login/app/sprinkles/admin/schema/requests/role/edit-info.yaml @@ -0,0 +1,20 @@ +--- +name: + validators: + length: + label: "&NAME" + min: 1 + max: 255 + message: VALIDATE.LENGTH_RANGE + transformations: + - trim +slug: + validators: + length: + label: "&SLUG" + min: 1 + max: 255 + message: VALIDATE.LENGTH_RANGE + transformations: + - trim +description: diff --git a/login/app/sprinkles/admin/schema/requests/role/get-by-slug.yaml b/login/app/sprinkles/admin/schema/requests/role/get-by-slug.yaml new file mode 100755 index 0000000..2aa41b5 --- /dev/null +++ b/login/app/sprinkles/admin/schema/requests/role/get-by-slug.yaml @@ -0,0 +1,6 @@ +--- +slug: + validators: + required: + label: "&SLUG" + message: VALIDATE.REQUIRED diff --git a/login/app/sprinkles/admin/schema/requests/user/create.yaml b/login/app/sprinkles/admin/schema/requests/user/create.yaml new file mode 100755 index 0000000..7e575bc --- /dev/null +++ b/login/app/sprinkles/admin/schema/requests/user/create.yaml @@ -0,0 +1,72 @@ +--- +user_name: + validators: + length: + label: "&USERNAME" + min: 1 + max: 50 + message: VALIDATE.LENGTH_RANGE + no_leading_whitespace: + label: "&USERNAME" + message: VALIDATE.NO_LEAD_WS + no_trailing_whitespace: + label: "&USERNAME" + message: VALIDATE.NO_TRAIL_WS + required: + label: "&USERNAME" + message: VALIDATE.REQUIRED + username: + label: "&USERNAME" + message: VALIDATE.USERNAME +first_name: + validators: + length: + label: "&FIRST_NAME" + min: 1 + max: 20 + message: VALIDATE.LENGTH_RANGE + required: + label: "&FIRST_NAME" + message: VALIDATE.REQUIRED + transformations: + - trim +last_name: + validators: + length: + label: "&LAST_NAME" + min: 1 + max: 30 + message: VALIDATE.LENGTH_RANGE + transformations: + - trim +email: + validators: + required: + label: "&EMAIL" + message: VALIDATE.REQUIRED + length: + label: "&EMAIL" + min: 1 + max: 150 + message: VALIDATE.LENGTH_RANGE + email: + message: VALIDATE.INVALID_EMAIL +locale: + default: en_US + validators: + required: + label: "&LOCALE" + domain: server + message: VALIDATE.REQUIRED + length: + label: "&LOCALE" + min: 1 + max: 10 + domain: server + message: VALIDATE.LENGTH_RANGE +group_id: + validators: + integer: + label: "&GROUP" + domain: server + message: VALIDATE.INTEGER diff --git a/login/app/sprinkles/admin/schema/requests/user/edit-field.yaml b/login/app/sprinkles/admin/schema/requests/user/edit-field.yaml new file mode 100755 index 0000000..ab3b3aa --- /dev/null +++ b/login/app/sprinkles/admin/schema/requests/user/edit-field.yaml @@ -0,0 +1,60 @@ +--- +first_name: + validators: + length: + label: "&FIRST_NAME" + min: 1 + max: 20 + message: VALIDATE.LENGTH_RANGE +last_name: + validators: + length: + label: "&LAST_NAME" + min: 1 + max: 30 + message: VALIDATE.LENGTH_RANGE +email: + validators: + length: + label: "&EMAIL" + min: 1 + max: 150 + message: VALIDATE.LENGTH_RANGE + email: + message: VALIDATE.INVALID_EMAIL +locale: + validators: + length: + label: "&LOCALE" + min: 1 + max: 10 + message: VALIDATE.LENGTH_RANGE +group_id: + validators: + integer: + message: VALIDATE.INTEGER +flag_enabled: + validators: + member_of: + values: + - '0' + - '1' + message: VALIDATE.BOOLEAN +flag_verified: + validators: + member_of: + values: + - '0' + - '1' + message: VALIDATE.BOOLEAN +password: + validators: + length: + label: "&PASSWORD" + min: 12 + max: 100 + message: VALIDATE.LENGTH_RANGE +roles: + validators: + array: + message: VALIDATE.ARRAY diff --git a/login/app/sprinkles/admin/schema/requests/user/edit-info.yaml b/login/app/sprinkles/admin/schema/requests/user/edit-info.yaml new file mode 100755 index 0000000..30ae920 --- /dev/null +++ b/login/app/sprinkles/admin/schema/requests/user/edit-info.yaml @@ -0,0 +1,36 @@ +--- +first_name: + validators: + length: + label: "&FIRST_NAME" + min: 1 + max: 20 + message: VALIDATE.LENGTH_RANGE +last_name: + validators: + length: + label: "&LAST_NAME" + min: 1 + max: 30 + message: VALIDATE.LENGTH_RANGE +email: + validators: + length: + label: "&EMAIL" + min: 1 + max: 150 + message: VALIDATE.LENGTH_RANGE + email: + message: VALIDATE.INVALID_EMAIL +locale: + validators: + length: + label: "&LOCALE" + min: 1 + max: 10 + message: VALIDATE.LENGTH_RANGE +group_id: + validators: + integer: + label: "&GROUP" + message: VALIDATE.INTEGER diff --git a/login/app/sprinkles/admin/schema/requests/user/edit-password.yaml b/login/app/sprinkles/admin/schema/requests/user/edit-password.yaml new file mode 100755 index 0000000..1d751ff --- /dev/null +++ b/login/app/sprinkles/admin/schema/requests/user/edit-password.yaml @@ -0,0 +1,30 @@ +--- +value: + validators: + required: + domain: client + label: "&PASSWORD" + message: VALIDATE.REQUIRED + length: + domain: client + label: "&PASSWORD" + min: 12 + max: 100 + message: VALIDATE.LENGTH_RANGE +passwordc: + validators: + required: + domain: client + label: "&PASSWORD.CONFIRM" + message: VALIDATE.REQUIRED + matches: + domain: client + field: value + label: "&PASSWORD.CONFIRM" + message: VALIDATE.PASSWORD_MISMATCH + length: + domain: client + label: "&PASSWORD.CONFIRM" + min: 12 + max: 100 + message: VALIDATE.LENGTH_RANGE diff --git a/login/app/sprinkles/admin/schema/requests/user/get-by-username.yaml b/login/app/sprinkles/admin/schema/requests/user/get-by-username.yaml new file mode 100755 index 0000000..97170dd --- /dev/null +++ b/login/app/sprinkles/admin/schema/requests/user/get-by-username.yaml @@ -0,0 +1,6 @@ +--- +user_name: + validators: + required: + label: "&USERNAME" + message: VALIDATE.REQUIRED diff --git a/login/app/sprinkles/admin/src/Admin.php b/login/app/sprinkles/admin/src/Admin.php new file mode 100755 index 0000000..8a6dcc1 --- /dev/null +++ b/login/app/sprinkles/admin/src/Admin.php @@ -0,0 +1,20 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Admin; + +use UserFrosting\System\Sprinkle\Sprinkle; + +/** + * Bootstrapper class for the 'admin' sprinkle. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class Admin extends Sprinkle +{ + +} diff --git a/login/app/sprinkles/admin/src/Controller/ActivityController.php b/login/app/sprinkles/admin/src/Controller/ActivityController.php new file mode 100755 index 0000000..2fbe0d9 --- /dev/null +++ b/login/app/sprinkles/admin/src/Controller/ActivityController.php @@ -0,0 +1,85 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Admin\Controller; + +use Illuminate\Database\Capsule\Manager as Capsule; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; +use Slim\Exception\NotFoundException; +use UserFrosting\Sprinkle\Core\Controller\SimpleController; +use UserFrosting\Sprinkle\Core\Facades\Debug; +use UserFrosting\Support\Exception\BadRequestException; +use UserFrosting\Support\Exception\ForbiddenException; +use UserFrosting\Support\Exception\HttpException; + +/** + * Controller class for activity-related requests. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class ActivityController extends SimpleController +{ + /** + * Returns a list of Activities + * + * Generates a list of activities, optionally paginated, sorted and/or filtered. + * This page requires authentication. + * Request type: GET + */ + public function getList($request, $response, $args) + { + // GET parameters + $params = $request->getQueryParams(); + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_activities')) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + $sprunje = $classMapper->createInstance('activity_sprunje', $classMapper, $params); + $sprunje->extendQuery(function ($query) { + return $query->with('user'); + }); + + // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. + // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). + return $sprunje->toResponse($response); + } + + /** + * Renders the activity listing page. + * + * This page renders a table of user activities. + * This page requires authentication. + * Request type: GET + */ + public function pageList($request, $response, $args) + { + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_activities')) { + throw new ForbiddenException(); + } + + return $this->ci->view->render($response, 'pages/activities.html.twig'); + } +} diff --git a/login/app/sprinkles/admin/src/Controller/AdminController.php b/login/app/sprinkles/admin/src/Controller/AdminController.php new file mode 100755 index 0000000..da4da8a --- /dev/null +++ b/login/app/sprinkles/admin/src/Controller/AdminController.php @@ -0,0 +1,150 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Admin\Controller; + +use Carbon\Carbon; +use UserFrosting\Sprinkle\Core\Controller\SimpleController; +use UserFrosting\Sprinkle\Account\Database\Models\Group; +use UserFrosting\Sprinkle\Account\Database\Models\User; +use UserFrosting\Sprinkle\Account\Database\Models\Role; +use UserFrosting\Sprinkle\Core\Database\Models\Version; +use UserFrosting\Sprinkle\Core\Util\EnvironmentInfo; +use UserFrosting\Support\Exception\ForbiddenException; + +/** + * AdminController Class + * + * Controller class for /dashboard URL. Handles admin-related activities + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class AdminController extends SimpleController +{ + + /** + * Renders the admin panel dashboard + * + */ + public function pageDashboard($request, $response, $args) + { + //** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_dashboard')) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // Probably a better way to do this + $users = $classMapper->staticMethod('user', 'orderBy', 'created_at', 'desc') + ->take(8) + ->get(); + + // Transform the `create_at` date in "x days ago" type of string + $users->transform(function ($item, $key) { + $item->registered = Carbon::parse($item->created_at)->diffForHumans(); + return $item; + }); + + /** @var Config $config */ + $config = $this->ci->config; + + /** @var Config $config */ + $cache = $this->ci->cache; + + // Get each sprinkle db version + $sprinkles = $this->ci->sprinkleManager->getSprinkleNames(); + + return $this->ci->view->render($response, 'pages/dashboard.html.twig', [ + 'counter' => [ + 'users' => $classMapper->staticMethod('user', 'count'), + 'roles' => $classMapper->staticMethod('role', 'count'), + 'groups' => $classMapper->staticMethod('group', 'count') + ], + 'info' => [ + 'version' => [ + 'UF' => \UserFrosting\VERSION, + 'php' => phpversion(), + 'database' => EnvironmentInfo::database() + ], + 'database' => [ + 'name' => $config['db.default.database'] + ], + 'environment' => $this->ci->environment, + 'path' => [ + 'project' => \UserFrosting\ROOT_DIR + ] + ], + 'sprinkles' => $sprinkles, + 'users' => $users + ]); + } + + /** + * Clear the site cache. + * + * This route requires authentication. + * Request type: POST + */ + public function clearCache($request, $response, $args) + { + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'clear_cache')) { + throw new ForbiddenException(); + } + + // Flush cache + $this->ci->cache->flush(); + + /** @var MessageStream $ms */ + $ms = $this->ci->alerts; + + $ms->addMessageTranslated('success', 'CACHE.CLEARED'); + + return $response->withStatus(200); + } + + /** + * Renders the modal form to confirm cache deletion. + * + * This does NOT render a complete page. Instead, it renders the HTML for the modal, which can be embedded in other pages. + * This page requires authentication. + * Request type: GET + */ + public function getModalConfirmClearCache($request, $response, $args) + { + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'clear_cache')) { + throw new ForbiddenException(); + } + + return $this->ci->view->render($response, 'modals/confirm-clear-cache.html.twig', [ + 'form' => [ + 'action' => 'api/dashboard/clear-cache', + ] + ]); + } +} diff --git a/login/app/sprinkles/admin/src/Controller/GroupController.php b/login/app/sprinkles/admin/src/Controller/GroupController.php new file mode 100755 index 0000000..7ca94b1 --- /dev/null +++ b/login/app/sprinkles/admin/src/Controller/GroupController.php @@ -0,0 +1,725 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Admin\Controller; + +use Carbon\Carbon; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Capsule\Manager as Capsule; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; +use Slim\Exception\NotFoundException; +use UserFrosting\Fortress\RequestDataTransformer; +use UserFrosting\Fortress\RequestSchema; +use UserFrosting\Fortress\ServerSideValidator; +use UserFrosting\Fortress\Adapter\JqueryValidationAdapter; +use UserFrosting\Sprinkle\Account\Database\Models\Group; +use UserFrosting\Sprinkle\Account\Database\Models\User; +use UserFrosting\Sprinkle\Core\Controller\SimpleController; +use UserFrosting\Sprinkle\Core\Facades\Debug; +use UserFrosting\Support\Exception\BadRequestException; +use UserFrosting\Support\Exception\ForbiddenException; +use UserFrosting\Support\Exception\HttpException; + +/** + * Controller class for group-related requests, including listing groups, CRUD for groups, etc. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class GroupController extends SimpleController +{ + /** + * Processes the request to create a new group. + * + * Processes the request from the group creation form, checking that: + * 1. The group name and slug are not already in use; + * 2. The user has permission to create a new group; + * 3. The submitted data is valid. + * This route requires authentication (and should generally be limited to admins or the root user). + * Request type: POST + * @see getModalCreateGroup + */ + public function create($request, $response, $args) + { + // Get POST parameters: name, slug, icon, description + $params = $request->getParsedBody(); + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'create_group')) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */ + $ms = $this->ci->alerts; + + // Load the request schema + $schema = new RequestSchema('schema://requests/group/create.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + $error = false; + + // Validate request data + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + $error = true; + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // Check if name or slug already exists + if ($classMapper->staticMethod('group', 'where', 'name', $data['name'])->first()) { + $ms->addMessageTranslated('danger', 'GROUP.NAME.IN_USE', $data); + $error = true; + } + + if ($classMapper->staticMethod('group', 'where', 'slug', $data['slug'])->first()) { + $ms->addMessageTranslated('danger', 'GROUP.SLUG.IN_USE', $data); + $error = true; + } + + if ($error) { + return $response->withStatus(400); + } + + /** @var UserFrosting\Config\Config $config */ + $config = $this->ci->config; + + // All checks passed! log events/activities and create group + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction( function() use ($classMapper, $data, $ms, $config, $currentUser) { + // Create the group + $group = $classMapper->createInstance('group', $data); + + // Store new group to database + $group->save(); + + // Create activity record + $this->ci->userActivityLogger->info("User {$currentUser->user_name} created group {$group->name}.", [ + 'type' => 'group_create', + 'user_id' => $currentUser->id + ]); + + $ms->addMessageTranslated('success', 'GROUP.CREATION_SUCCESSFUL', $data); + }); + + return $response->withStatus(200); + } + + /** + * Processes the request to delete an existing group. + * + * Deletes the specified group. + * Before doing so, checks that: + * 1. The user has permission to delete this group; + * 2. The group is not currently set as the default for new users; + * 3. The group is empty (does not have any users); + * 4. The submitted data is valid. + * This route requires authentication (and should generally be limited to admins or the root user). + * Request type: DELETE + */ + public function delete($request, $response, $args) + { + $group = $this->getGroupFromParams($args); + + // If the group doesn't exist, return 404 + if (!$group) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'delete_group', [ + 'group' => $group + ])) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Config\Config $config */ + $config = $this->ci->config; + + // Check that we are not deleting the default group + // Need to use loose comparison for now, because some DBs return `id` as a string + if ($group->slug == $config['site.registration.user_defaults.group']) { + $e = new BadRequestException(); + $e->addUserMessage('GROUP.DELETE_DEFAULT', $group->toArray()); + throw $e; + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // Check if there are any users in this group + $countGroupUsers = $classMapper->staticMethod('user', 'where', 'group_id', $group->id)->count(); + if ($countGroupUsers > 0) { + $e = new BadRequestException(); + $e->addUserMessage('GROUP.NOT_EMPTY', $group->toArray()); + throw $e; + } + + $groupName = $group->name; + + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction( function() use ($group, $groupName, $currentUser) { + $group->delete(); + unset($group); + + // Create activity record + $this->ci->userActivityLogger->info("User {$currentUser->user_name} deleted group {$groupName}.", [ + 'type' => 'group_delete', + 'user_id' => $currentUser->id + ]); + }); + + /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */ + $ms = $this->ci->alerts; + + $ms->addMessageTranslated('success', 'GROUP.DELETION_SUCCESSFUL', [ + 'name' => $groupName + ]); + + return $response->withStatus(200); + } + + /** + * Returns info for a single group. + * + * This page requires authentication. + * Request type: GET + */ + public function getInfo($request, $response, $args) + { + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_groups')) { + throw new ForbiddenException(); + } + + $slug = $args['slug']; + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + $group = $classMapper->staticMethod('group', 'where', 'slug', $slug)->first(); + + // If the group doesn't exist, return 404 + if (!$group) { + throw new NotFoundException($request, $response); + } + + // Get group + $result = $group->toArray(); + + // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. + // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). + return $response->withJson($result, 200, JSON_PRETTY_PRINT); + } + + /** + * Returns a list of Groups + * + * Generates a list of groups, optionally paginated, sorted and/or filtered. + * This page requires authentication. + * Request type: GET + */ + public function getList($request, $response, $args) + { + // GET parameters + $params = $request->getQueryParams(); + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_groups')) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + $sprunje = $classMapper->createInstance('group_sprunje', $classMapper, $params); + + // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. + // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). + return $sprunje->toResponse($response); + } + + public function getModalConfirmDelete($request, $response, $args) + { + // GET parameters + $params = $request->getQueryParams(); + + $group = $this->getGroupFromParams($params); + + // If the group no longer exists, forward to main group listing page + if (!$group) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'delete_group', [ + 'group' => $group + ])) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // Check if there are any users in this group + $countGroupUsers = $classMapper->staticMethod('user', 'where', 'group_id', $group->id)->count(); + if ($countGroupUsers > 0) { + $e = new BadRequestException(); + $e->addUserMessage('GROUP.NOT_EMPTY', $group->toArray()); + throw $e; + } + + return $this->ci->view->render($response, 'modals/confirm-delete-group.html.twig', [ + 'group' => $group, + 'form' => [ + 'action' => "api/groups/g/{$group->slug}", + ] + ]); + } + + /** + * Renders the modal form for creating a new group. + * + * This does NOT render a complete page. Instead, it renders the HTML for the modal, which can be embedded in other pages. + * This page requires authentication. + * Request type: GET + */ + public function getModalCreate($request, $response, $args) + { + // GET parameters + $params = $request->getQueryParams(); + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + /** @var UserFrosting\I18n\MessageTranslator $translator */ + $translator = $this->ci->translator; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'create_group')) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // Create a dummy group to prepopulate fields + $group = $classMapper->createInstance('group', []); + + $group->icon = 'fa fa-user'; + + $fieldNames = ['name', 'slug', 'icon', 'description']; + $fields = [ + 'hidden' => [], + 'disabled' => [] + ]; + + // Load validation rules + $schema = new RequestSchema('schema://requests/group/create.yaml'); + $validator = new JqueryValidationAdapter($schema, $this->ci->translator); + + return $this->ci->view->render($response, 'modals/group.html.twig', [ + 'group' => $group, + 'form' => [ + 'action' => 'api/groups', + 'method' => 'POST', + 'fields' => $fields, + 'submit_text' => $translator->translate('CREATE') + ], + 'page' => [ + 'validators' => $validator->rules('json', false) + ] + ]); + } + + /** + * Renders the modal form for editing an existing group. + * + * This does NOT render a complete page. Instead, it renders the HTML for the modal, which can be embedded in other pages. + * This page requires authentication. + * Request type: GET + */ + public function getModalEdit($request, $response, $args) + { + // GET parameters + $params = $request->getQueryParams(); + + $group = $this->getGroupFromParams($params); + + // If the group doesn't exist, return 404 + if (!$group) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + /** @var UserFrosting\I18n\MessageTranslator $translator */ + $translator = $this->ci->translator; + + // Access-controlled resource - check that currentUser has permission to edit basic fields "name", "slug", "icon", "description" for this group + $fieldNames = ['name', 'slug', 'icon', 'description']; + if (!$authorizer->checkAccess($currentUser, 'update_group_field', [ + 'group' => $group, + 'fields' => $fieldNames + ])) { + throw new ForbiddenException(); + } + + // Generate form + $fields = [ + 'hidden' => [], + 'disabled' => [] + ]; + + // Load validation rules + $schema = new RequestSchema('schema://requests/group/edit-info.yaml'); + $validator = new JqueryValidationAdapter($schema, $translator); + + return $this->ci->view->render($response, 'modals/group.html.twig', [ + 'group' => $group, + 'form' => [ + 'action' => "api/groups/g/{$group->slug}", + 'method' => 'PUT', + 'fields' => $fields, + 'submit_text' => $translator->translate('UPDATE') + ], + 'page' => [ + 'validators' => $validator->rules('json', false) + ] + ]); + } + + public function getUsers($request, $response, $args) + { + $group = $this->getGroupFromParams($args); + + // If the group no longer exists, forward to main group listing page + if (!$group) { + throw new NotFoundException($request, $response); + } + + // GET parameters + $params = $request->getQueryParams(); + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'view_group_field', [ + 'group' => $group, + 'property' => 'users' + ])) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + $sprunje = $classMapper->createInstance('user_sprunje', $classMapper, $params); + $sprunje->extendQuery(function ($query) use ($group) { + return $query->where('group_id', $group->id); + }); + + // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. + // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). + return $sprunje->toResponse($response); + } + + /** + * Renders a page displaying a group's information, in read-only mode. + * + * This checks that the currently logged-in user has permission to view the requested group's info. + * It checks each field individually, showing only those that you have permission to view. + * This will also try to show buttons for deleting, and editing the group. + * This page requires authentication. + * Request type: GET + */ + public function pageInfo($request, $response, $args) + { + $group = $this->getGroupFromParams($args); + + // If the group no longer exists, forward to main group listing page + if (!$group) { + $redirectPage = $this->ci->router->pathFor('uri_groups'); + return $response->withRedirect($redirectPage, 404); + } + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_group', [ + 'group' => $group + ])) { + throw new ForbiddenException(); + } + + // Determine fields that currentUser is authorized to view + $fieldNames = ['name', 'slug', 'icon', 'description']; + + // Generate form + $fields = [ + 'hidden' => [] + ]; + + foreach ($fieldNames as $field) { + if (!$authorizer->checkAccess($currentUser, 'view_group_field', [ + 'group' => $group, + 'property' => $field + ])) { + $fields['hidden'][] = $field; + } + } + + // Determine buttons to display + $editButtons = [ + 'hidden' => [] + ]; + + if (!$authorizer->checkAccess($currentUser, 'update_group_field', [ + 'group' => $group, + 'fields' => ['name', 'slug', 'icon', 'description'] + ])) { + $editButtons['hidden'][] = 'edit'; + } + + if (!$authorizer->checkAccess($currentUser, 'delete_group', [ + 'group' => $group + ])) { + $editButtons['hidden'][] = 'delete'; + } + + return $this->ci->view->render($response, 'pages/group.html.twig', [ + 'group' => $group, + 'fields' => $fields, + 'tools' => $editButtons + ]); + } + + /** + * Renders the group listing page. + * + * This page renders a table of groups, with dropdown menus for admin actions for each group. + * Actions typically include: edit group, delete group. + * This page requires authentication. + * Request type: GET + */ + public function pageList($request, $response, $args) + { + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_groups')) { + throw new ForbiddenException(); + } + + return $this->ci->view->render($response, 'pages/groups.html.twig'); + } + + /** + * Processes the request to update an existing group's details. + * + * Processes the request from the group update form, checking that: + * 1. The group name/slug are not already in use; + * 2. The user has the necessary permissions to update the posted field(s); + * 3. The submitted data is valid. + * This route requires authentication (and should generally be limited to admins or the root user). + * Request type: PUT + * @see getModalGroupEdit + */ + public function updateInfo($request, $response, $args) + { + // Get the group based on slug in URL + $group = $this->getGroupFromParams($args); + + if (!$group) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Config\Config $config */ + $config = $this->ci->config; + + // Get PUT parameters: (name, slug, icon, description) + $params = $request->getParsedBody(); + + /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */ + $ms = $this->ci->alerts; + + // Load the request schema + $schema = new RequestSchema('schema://requests/group/edit-info.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + $error = false; + + // Validate request data + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + $error = true; + } + + // Determine targeted fields + $fieldNames = []; + foreach ($data as $name => $value) { + $fieldNames[] = $name; + } + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled resource - check that currentUser has permission to edit submitted fields for this group + if (!$authorizer->checkAccess($currentUser, 'update_group_field', [ + 'group' => $group, + 'fields' => array_values(array_unique($fieldNames)) + ])) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // Check if name or slug already exists + if ( + isset($data['name']) && + $data['name'] != $group->name && + $classMapper->staticMethod('group', 'where', 'name', $data['name'])->first() + ) { + $ms->addMessageTranslated('danger', 'GROUP.NAME.IN_USE', $data); + $error = true; + } + + if ( + isset($data['slug']) && + $data['slug'] != $group->slug && + $classMapper->staticMethod('group', 'where', 'slug', $data['slug'])->first() + ) { + $ms->addMessageTranslated('danger', 'GROUP.SLUG.IN_USE', $data); + $error = true; + } + + if ($error) { + return $response->withStatus(400); + } + + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction( function() use ($data, $group, $currentUser) { + // Update the group and generate success messages + foreach ($data as $name => $value) { + if ($value != $group->$name) { + $group->$name = $value; + } + } + + $group->save(); + + // Create activity record + $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated details for group {$group->name}.", [ + 'type' => 'group_update_info', + 'user_id' => $currentUser->id + ]); + }); + + $ms->addMessageTranslated('success', 'GROUP.UPDATE', [ + 'name' => $group->name + ]); + + return $response->withStatus(200); + } + + protected function getGroupFromParams($params) + { + // Load the request schema + $schema = new RequestSchema('schema://requests/group/get-by-slug.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and throw exception on validation errors. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + // TODO: encapsulate the communication of error messages from ServerSideValidator to the BadRequestException + $e = new BadRequestException(); + foreach ($validator->errors() as $idx => $field) { + foreach($field as $eidx => $error) { + $e->addUserMessage($error); + } + } + throw $e; + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // Get the group + $group = $classMapper->staticMethod('group', 'where', 'slug', $data['slug']) + ->first(); + + return $group; + } +} diff --git a/login/app/sprinkles/admin/src/Controller/PermissionController.php b/login/app/sprinkles/admin/src/Controller/PermissionController.php new file mode 100755 index 0000000..660e296 --- /dev/null +++ b/login/app/sprinkles/admin/src/Controller/PermissionController.php @@ -0,0 +1,206 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Admin\Controller; + +use Carbon\Carbon; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Capsule\Manager as Capsule; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; +use Slim\Exception\NotFoundException; +use UserFrosting\Fortress\RequestDataTransformer; +use UserFrosting\Fortress\RequestSchema; +use UserFrosting\Fortress\ServerSideValidator; +use UserFrosting\Fortress\Adapter\JqueryValidationAdapter; +use UserFrosting\Sprinkle\Account\Database\Models\Permission; +use UserFrosting\Sprinkle\Account\Database\Models\Role; +use UserFrosting\Sprinkle\Core\Controller\SimpleController; +use UserFrosting\Sprinkle\Core\Facades\Debug; +use UserFrosting\Support\Exception\BadRequestException; +use UserFrosting\Support\Exception\ForbiddenException; +use UserFrosting\Support\Exception\HttpException; + +/** + * Controller class for permission-related requests, including listing permissions, CRUD for permissions, etc. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class PermissionController extends SimpleController +{ + /** + * Returns info for a single permission. + * + * This page requires authentication. + * Request type: GET + */ + public function getInfo($request, $response, $args) + { + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_permissions')) { + throw new ForbiddenException(); + } + + $permissionId = $args['id']; + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + $permission = $classMapper->staticMethod('permission', 'find', $permissionId); + + // If the permission doesn't exist, return 404 + if (!$permission) { + throw new NotFoundException($request, $response); + } + + // Get permission + $result = $permission->load('users')->toArray(); + + // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. + // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). + return $response->withJson($result, 200, JSON_PRETTY_PRINT); + } + + /** + * Returns a list of Permissions + * + * Generates a list of permissions, optionally paginated, sorted and/or filtered. + * This page requires authentication. + * Request type: GET + */ + public function getList($request, $response, $args) + { + // GET parameters + $params = $request->getQueryParams(); + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_permissions')) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + $sprunje = $classMapper->createInstance('permission_sprunje', $classMapper, $params); + + // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. + // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). + return $sprunje->toResponse($response); + } + + /** + * Returns a list of Users for a specified Permission. + * + * Generates a list of users, optionally paginated, sorted and/or filtered. + * This page requires authentication. + * Request type: GET + */ + public function getUsers($request, $response, $args) + { + // GET parameters + $params = $request->getQueryParams(); + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_permissions')) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + $params['permission_id'] = $args['id']; + + $sprunje = $classMapper->createInstance('permission_user_sprunje', $classMapper, $params); + + $response = $sprunje->toResponse($response); + + // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. + // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). + return $response; + } + + /** + * Renders a page displaying a permission's information, in read-only mode. + * + * This checks that the currently logged-in user has permission to view permissions. + * Note that permissions cannot be modified through the interface. This is because + * permissions are tighly coupled to the code and should only be modified by developers. + * This page requires authentication. + * Request type: GET + */ + public function pageInfo($request, $response, $args) + { + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_permissions')) { + throw new ForbiddenException(); + } + + $permissionId = $args['id']; + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + $permission = $classMapper->staticMethod('permission', 'find', $permissionId); + + // If the permission doesn't exist, return 404 + if (!$permission) { + throw new NotFoundException($request, $response); + } + + return $this->ci->view->render($response, 'pages/permission.html.twig', [ + 'permission' => $permission + ]); + } + + /** + * Renders the permission listing page. + * + * This page renders a table of permissions, with dropdown menus for admin actions for each permission. + * Actions typically include: edit permission, delete permission. + * This page requires authentication. + * Request type: GET + */ + public function pageList($request, $response, $args) + { + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_permissions')) { + throw new ForbiddenException(); + } + + return $this->ci->view->render($response, 'pages/permissions.html.twig'); + } +} diff --git a/login/app/sprinkles/admin/src/Controller/RoleController.php b/login/app/sprinkles/admin/src/Controller/RoleController.php new file mode 100755 index 0000000..ab86c88 --- /dev/null +++ b/login/app/sprinkles/admin/src/Controller/RoleController.php @@ -0,0 +1,930 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Admin\Controller; + +use Carbon\Carbon; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Capsule\Manager as Capsule; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; +use Slim\Exception\NotFoundException; +use UserFrosting\Fortress\RequestDataTransformer; +use UserFrosting\Fortress\RequestSchema; +use UserFrosting\Fortress\ServerSideValidator; +use UserFrosting\Fortress\Adapter\JqueryValidationAdapter; +use UserFrosting\Sprinkle\Account\Database\Models\Role; +use UserFrosting\Sprinkle\Account\Database\Models\User; +use UserFrosting\Sprinkle\Core\Controller\SimpleController; +use UserFrosting\Sprinkle\Core\Facades\Debug; +use UserFrosting\Support\Exception\BadRequestException; +use UserFrosting\Support\Exception\ForbiddenException; +use UserFrosting\Support\Exception\HttpException; + +/** + * Controller class for role-related requests, including listing roles, CRUD for roles, etc. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class RoleController extends SimpleController +{ + /** + * Processes the request to create a new role. + * + * Processes the request from the role creation form, checking that: + * 1. The role name and slug are not already in use; + * 2. The user has permission to create a new role; + * 3. The submitted data is valid. + * This route requires authentication (and should generally be limited to admins or the root user). + * Request type: POST + * @see getModalCreateRole + */ + public function create($request, $response, $args) + { + // Get POST parameters: name, slug, description + $params = $request->getParsedBody(); + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'create_role')) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */ + $ms = $this->ci->alerts; + + // Load the request schema + $schema = new RequestSchema('schema://requests/role/create.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + $error = false; + + // Validate request data + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + $error = true; + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // Check if name or slug already exists + if ($classMapper->staticMethod('role', 'where', 'name', $data['name'])->first()) { + $ms->addMessageTranslated('danger', 'ROLE.NAME_IN_USE', $data); + $error = true; + } + + if ($classMapper->staticMethod('role', 'where', 'slug', $data['slug'])->first()) { + $ms->addMessageTranslated('danger', 'SLUG_IN_USE', $data); + $error = true; + } + + if ($error) { + return $response->withStatus(400); + } + + /** @var UserFrosting\Config\Config $config */ + $config = $this->ci->config; + + // All checks passed! log events/activities and create role + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction( function() use ($classMapper, $data, $ms, $config, $currentUser) { + // Create the role + $role = $classMapper->createInstance('role', $data); + + // Store new role to database + $role->save(); + + // Create activity record + $this->ci->userActivityLogger->info("User {$currentUser->user_name} created role {$role->name}.", [ + 'type' => 'role_create', + 'user_id' => $currentUser->id + ]); + + $ms->addMessageTranslated('success', 'ROLE.CREATION_SUCCESSFUL', $data); + }); + + return $response->withStatus(200); + } + + /** + * Processes the request to delete an existing role. + * + * Deletes the specified role. + * Before doing so, checks that: + * 1. The user has permission to delete this role; + * 2. The role is not a default for new users; + * 3. The role does not have any associated users; + * 4. The submitted data is valid. + * This route requires authentication (and should generally be limited to admins or the root user). + * Request type: DELETE + */ + public function delete($request, $response, $args) + { + $role = $this->getRoleFromParams($args); + + // If the role doesn't exist, return 404 + if (!$role) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'delete_role', [ + 'role' => $role + ])) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // Check that we are not deleting a default role + $defaultRoleSlugs = $classMapper->staticMethod('role', 'getDefaultSlugs'); + + // Need to use loose comparison for now, because some DBs return `id` as a string + if (in_array($role->slug, $defaultRoleSlugs)) { + $e = new BadRequestException(); + $e->addUserMessage('ROLE.DELETE_DEFAULT'); + throw $e; + } + + // Check if there are any users associated with this role + $countUsers = $role->users()->count(); + if ($countUsers > 0) { + $e = new BadRequestException(); + $e->addUserMessage('ROLE.HAS_USERS'); + throw $e; + } + + $roleName = $role->name; + + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction( function() use ($role, $roleName, $currentUser) { + $role->delete(); + unset($role); + + // Create activity record + $this->ci->userActivityLogger->info("User {$currentUser->user_name} deleted role {$roleName}.", [ + 'type' => 'role_delete', + 'user_id' => $currentUser->id + ]); + }); + + /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */ + $ms = $this->ci->alerts; + + $ms->addMessageTranslated('success', 'ROLE.DELETION_SUCCESSFUL', [ + 'name' => $roleName + ]); + + return $response->withStatus(200); + } + + /** + * Returns info for a single role, along with associated permissions. + * + * This page requires authentication. + * Request type: GET + */ + public function getInfo($request, $response, $args) + { + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_roles')) { + throw new ForbiddenException(); + } + + $slug = $args['slug']; + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + $role = $classMapper->staticMethod('role', 'where', 'slug', $slug)->first(); + + // If the role doesn't exist, return 404 + if (!$role) { + throw new NotFoundException($request, $response); + } + + // Get role + $result = $role->load('permissions')->toArray(); + + // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. + // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). + return $response->withJson($result, 200, JSON_PRETTY_PRINT); + } + + /** + * Returns a list of Roles + * + * Generates a list of roles, optionally paginated, sorted and/or filtered. + * This page requires authentication. + * Request type: GET + */ + public function getList($request, $response, $args) + { + // GET parameters + $params = $request->getQueryParams(); + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_roles')) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + $sprunje = $classMapper->createInstance('role_sprunje', $classMapper, $params); + + // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. + // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). + return $sprunje->toResponse($response); + } + + public function getModalConfirmDelete($request, $response, $args) + { + // GET parameters + $params = $request->getQueryParams(); + + $role = $this->getRoleFromParams($params); + + // If the role no longer exists, forward to main role listing page + if (!$role) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'delete_role', [ + 'role' => $role + ])) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // Check that we are not deleting a default role + $defaultRoleSlugs = $classMapper->staticMethod('role', 'getDefaultSlugs'); + + // Need to use loose comparison for now, because some DBs return `id` as a string + if (in_array($role->slug, $defaultRoleSlugs)) { + $e = new BadRequestException(); + $e->addUserMessage('ROLE.DELETE_DEFAULT', $role->toArray()); + throw $e; + } + + // Check if there are any users associated with this role + $countUsers = $role->users()->count(); + if ($countUsers > 0) { + $e = new BadRequestException(); + $e->addUserMessage('ROLE.HAS_USERS', $role->toArray()); + throw $e; + } + + return $this->ci->view->render($response, 'modals/confirm-delete-role.html.twig', [ + 'role' => $role, + 'form' => [ + 'action' => "api/roles/r/{$role->slug}", + ] + ]); + } + + /** + * Renders the modal form for creating a new role. + * + * This does NOT render a complete page. Instead, it renders the HTML for the modal, which can be embedded in other pages. + * This page requires authentication. + * Request type: GET + */ + public function getModalCreate($request, $response, $args) + { + // GET parameters + $params = $request->getQueryParams(); + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + /** @var UserFrosting\I18n\MessageTranslator $translator */ + $translator = $this->ci->translator; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'create_role')) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // Create a dummy role to prepopulate fields + $role = $classMapper->createInstance('role', []); + + $fieldNames = ['name', 'slug', 'description']; + $fields = [ + 'hidden' => [], + 'disabled' => [] + ]; + + // Load validation rules + $schema = new RequestSchema('schema://requests/role/create.yaml'); + $validator = new JqueryValidationAdapter($schema, $this->ci->translator); + + return $this->ci->view->render($response, 'modals/role.html.twig', [ + 'role' => $role, + 'form' => [ + 'action' => 'api/roles', + 'method' => 'POST', + 'fields' => $fields, + 'submit_text' => $translator->translate('CREATE') + ], + 'page' => [ + 'validators' => $validator->rules('json', false) + ] + ]); + } + + /** + * Renders the modal form for editing an existing role. + * + * This does NOT render a complete page. Instead, it renders the HTML for the modal, which can be embedded in other pages. + * This page requires authentication. + * Request type: GET + */ + public function getModalEdit($request, $response, $args) + { + // GET parameters + $params = $request->getQueryParams(); + + $role = $this->getRoleFromParams($params); + + // If the role doesn't exist, return 404 + if (!$role) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + /** @var UserFrosting\I18n\MessageTranslator $translator */ + $translator = $this->ci->translator; + + // Access-controlled resource - check that currentUser has permission to edit basic fields "name", "slug", "description" for this role + $fieldNames = ['name', 'slug', 'description']; + if (!$authorizer->checkAccess($currentUser, 'update_role_field', [ + 'role' => $role, + 'fields' => $fieldNames + ])) { + throw new ForbiddenException(); + } + + // Generate form + $fields = [ + 'hidden' => [], + 'disabled' => [] + ]; + + // Load validation rules + $schema = new RequestSchema('schema://requests/role/edit-info.yaml'); + $validator = new JqueryValidationAdapter($schema, $translator); + + return $this->ci->view->render($response, 'modals/role.html.twig', [ + 'role' => $role, + 'form' => [ + 'action' => "api/roles/r/{$role->slug}", + 'method' => 'PUT', + 'fields' => $fields, + 'submit_text' => $translator->translate('UPDATE') + ], + 'page' => [ + 'validators' => $validator->rules('json', false) + ] + ]); + } + + /** + * Renders the modal form for editing a role's permissions. + * + * This does NOT render a complete page. Instead, it renders the HTML for the form, which can be embedded in other pages. + * This page requires authentication. + * Request type: GET + */ + public function getModalEditPermissions($request, $response, $args) + { + // GET parameters + $params = $request->getQueryParams(); + + $role = $this->getRoleFromParams($params); + + // If the role doesn't exist, return 404 + if (!$role) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled resource - check that currentUser has permission to edit "permissions" field for this role + if (!$authorizer->checkAccess($currentUser, 'update_role_field', [ + 'role' => $role, + 'fields' => ['permissions'] + ])) { + throw new ForbiddenException(); + } + + return $this->ci->view->render($response, 'modals/role-manage-permissions.html.twig', [ + 'role' => $role + ]); + } + + /** + * Returns a list of Permissions for a specified Role. + * + * Generates a list of permissions, optionally paginated, sorted and/or filtered. + * This page requires authentication. + * Request type: GET + */ + public function getPermissions($request, $response, $args) + { + $role = $this->getRoleFromParams($args); + + // If the role no longer exists, forward to main role listing page + if (!$role) { + throw new NotFoundException($request, $response); + } + + // GET parameters + $params = $request->getQueryParams(); + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'view_role_field', [ + 'role' => $role, + 'property' => 'permissions' + ])) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + $sprunje = $classMapper->createInstance('permission_sprunje', $classMapper, $params); + $sprunje->extendQuery(function ($query) use ($role) { + return $query->forRole($role->id); + }); + + // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. + // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). + return $sprunje->toResponse($response); + } + + /** + * Returns users associated with a single role. + * + * This page requires authentication. + * Request type: GET + */ + public function getUsers($request, $response, $args) + { + $role = $this->getRoleFromParams($args); + + // If the role doesn't exist, return 404 + if (!$role) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // GET parameters + $params = $request->getQueryParams(); + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'view_role_field', [ + 'role' => $role, + 'property' => 'users' + ])) { + throw new ForbiddenException(); + } + + $sprunje = $classMapper->createInstance('user_sprunje', $classMapper, $params); + $sprunje->extendQuery(function ($query) use ($role) { + return $query->forRole($role->id); + }); + + // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. + // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). + return $sprunje->toResponse($response); + } + + /** + * Renders a page displaying a role's information, in read-only mode. + * + * This checks that the currently logged-in user has permission to view the requested role's info. + * It checks each field individually, showing only those that you have permission to view. + * This will also try to show buttons for deleting and editing the role. + * This page requires authentication. + * Request type: GET + */ + public function pageInfo($request, $response, $args) + { + $role = $this->getRoleFromParams($args); + + // If the role no longer exists, forward to main role listing page + if (!$role) { + $redirectPage = $this->ci->router->pathFor('uri_roles'); + return $response->withRedirect($redirectPage, 404); + } + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_role', [ + 'role' => $role + ])) { + throw new ForbiddenException(); + } + + // Determine fields that currentUser is authorized to view + $fieldNames = ['name', 'slug', 'description']; + + // Generate form + $fields = [ + 'hidden' => [] + ]; + + foreach ($fieldNames as $field) { + if (!$authorizer->checkAccess($currentUser, 'view_role_field', [ + 'role' => $role, + 'property' => $field + ])) { + $fields['hidden'][] = $field; + } + } + + // Determine buttons to display + $editButtons = [ + 'hidden' => [] + ]; + + if (!$authorizer->checkAccess($currentUser, 'update_role_field', [ + 'role' => $role, + 'fields' => ['name', 'slug', 'description'] + ])) { + $editButtons['hidden'][] = 'edit'; + } + + if (!$authorizer->checkAccess($currentUser, 'delete_role', [ + 'role' => $role + ])) { + $editButtons['hidden'][] = 'delete'; + } + + return $this->ci->view->render($response, 'pages/role.html.twig', [ + 'role' => $role, + 'fields' => $fields, + 'tools' => $editButtons + ]); + } + + /** + * Renders the role listing page. + * + * This page renders a table of roles, with dropdown menus for admin actions for each role. + * Actions typically include: edit role, delete role. + * This page requires authentication. + * Request type: GET + */ + public function pageList($request, $response, $args) + { + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_roles')) { + throw new ForbiddenException(); + } + + return $this->ci->view->render($response, 'pages/roles.html.twig'); + } + + /** + * Processes the request to update an existing role's details. + * + * Processes the request from the role update form, checking that: + * 1. The role name/slug are not already in use; + * 2. The user has the necessary permissions to update the posted field(s); + * 3. The submitted data is valid. + * This route requires authentication (and should generally be limited to admins or the root user). + * Request type: PUT + * @see getModalRoleEdit + */ + public function updateInfo($request, $response, $args) + { + // Get the role based on slug in the URL + $role = $this->getRoleFromParams($args); + + if (!$role) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Config\Config $config */ + $config = $this->ci->config; + + // Get PUT parameters: (name, slug, description) + $params = $request->getParsedBody(); + + /** @var UserFrosting\I18n\MessageTranslator $translator */ + $ms = $this->ci->alerts; + + // Load the request schema + $schema = new RequestSchema('schema://requests/role/edit-info.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + $error = false; + + // Validate request data + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + $error = true; + } + + // Determine targeted fields + $fieldNames = []; + foreach ($data as $name => $value) { + $fieldNames[] = $name; + } + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled resource - check that currentUser has permission to edit submitted fields for this role + if (!$authorizer->checkAccess($currentUser, 'update_role_field', [ + 'role' => $role, + 'fields' => array_values(array_unique($fieldNames)) + ])) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // Check if name or slug already exists + if ( + isset($data['name']) && + $data['name'] != $role->name && + $classMapper->staticMethod('role', 'where', 'name', $data['name'])->first() + ) { + $ms->addMessageTranslated('danger', 'ROLE.NAME_IN_USE', $data); + $error = true; + } + + if ( + isset($data['slug']) && + $data['slug'] != $role->slug && + $classMapper->staticMethod('role', 'where', 'slug', $data['slug'])->first() + ) { + $ms->addMessageTranslated('danger', 'SLUG_IN_USE', $data); + $error = true; + } + + if ($error) { + return $response->withStatus(400); + } + + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction( function() use ($data, $role, $currentUser) { + // Update the role and generate success messages + foreach ($data as $name => $value) { + if ($value != $role->$name){ + $role->$name = $value; + } + } + + $role->save(); + + // Create activity record + $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated details for role {$role->name}.", [ + 'type' => 'role_update_info', + 'user_id' => $currentUser->id + ]); + }); + + $ms->addMessageTranslated('success', 'ROLE.UPDATED', [ + 'name' => $role->name + ]); + + return $response->withStatus(200); + } + + /** + * Processes the request to update a specific field for an existing role, including permissions. + * + * Processes the request from the role update form, checking that: + * 1. The logged-in user has the necessary permissions to update the putted field(s); + * 2. The submitted data is valid. + * This route requires authentication. + * Request type: PUT + */ + public function updateField($request, $response, $args) + { + // Get the username from the URL + $role = $this->getRoleFromParams($args); + + if (!$role) { + throw new NotFoundException($request, $response); + } + + // Get key->value pair from URL and request body + $fieldName = $args['field']; + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled resource - check that currentUser has permission to edit the specified field for this user + if (!$authorizer->checkAccess($currentUser, 'update_role_field', [ + 'role' => $role, + 'fields' => [$fieldName] + ])) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Config\Config $config */ + $config = $this->ci->config; + + // Get PUT parameters: value + $put = $request->getParsedBody(); + + if (!isset($put['value'])) { + throw new BadRequestException(); + } + + $params = [ + $fieldName => $put['value'] + ]; + + // Validate key -> value pair + + // Load the request schema + $schema = new RequestSchema('schema://requests/role/edit-field.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and throw exception on validation errors. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + // TODO: encapsulate the communication of error messages from ServerSideValidator to the BadRequestException + $e = new BadRequestException(); + foreach ($validator->errors() as $idx => $field) { + foreach($field as $eidx => $error) { + $e->addUserMessage($error); + } + } + throw $e; + } + + // Get validated and transformed value + $fieldValue = $data[$fieldName]; + + /** @var UserFrosting\I18n\MessageTranslator $translator */ + $ms = $this->ci->alerts; + + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction( function() use ($fieldName, $fieldValue, $role, $currentUser) { + if ($fieldName == 'permissions') { + $newPermissions = collect($fieldValue)->pluck('permission_id')->all(); + $role->permissions()->sync($newPermissions); + } else { + $role->$fieldName = $fieldValue; + $role->save(); + } + + // Create activity record + $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated property '$fieldName' for role {$role->name}.", [ + 'type' => 'role_update_field', + 'user_id' => $currentUser->id + ]); + }); + + // Add success messages + if ($fieldName == 'permissions') { + $ms->addMessageTranslated('success', 'ROLE.PERMISSIONS_UPDATED', [ + 'name' => $role->name + ]); + } else { + $ms->addMessageTranslated('success', 'ROLE.UPDATED', [ + 'name' => $role->name + ]); + } + + return $response->withStatus(200); + } + + protected function getRoleFromParams($params) + { + // Load the request schema + $schema = new RequestSchema('schema://requests/role/get-by-slug.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and throw exception on validation errors. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + // TODO: encapsulate the communication of error messages from ServerSideValidator to the BadRequestException + $e = new BadRequestException(); + foreach ($validator->errors() as $idx => $field) { + foreach($field as $eidx => $error) { + $e->addUserMessage($error); + } + } + throw $e; + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // Get the role + $role = $classMapper->staticMethod('role', 'where', 'slug', $data['slug']) + ->first(); + + return $role; + } +} diff --git a/login/app/sprinkles/admin/src/Controller/UserController.php b/login/app/sprinkles/admin/src/Controller/UserController.php new file mode 100755 index 0000000..ff41009 --- /dev/null +++ b/login/app/sprinkles/admin/src/Controller/UserController.php @@ -0,0 +1,1278 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Admin\Controller; + +use Carbon\Carbon; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Capsule\Manager as Capsule; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; +use Slim\Exception\NotFoundException; +use UserFrosting\Fortress\RequestDataTransformer; +use UserFrosting\Fortress\RequestSchema; +use UserFrosting\Fortress\ServerSideValidator; +use UserFrosting\Fortress\Adapter\JqueryValidationAdapter; +use UserFrosting\Sprinkle\Account\Database\Models\Group; +use UserFrosting\Sprinkle\Account\Database\Models\User; +use UserFrosting\Sprinkle\Account\Facades\Password; +use UserFrosting\Sprinkle\Core\Controller\SimpleController; +use UserFrosting\Sprinkle\Core\Facades\Debug; +use UserFrosting\Sprinkle\Core\Mail\EmailRecipient; +use UserFrosting\Sprinkle\Core\Mail\TwigMailMessage; +use UserFrosting\Support\Exception\BadRequestException; +use UserFrosting\Support\Exception\ForbiddenException; +use UserFrosting\Support\Exception\HttpException; + +/** + * Controller class for user-related requests, including listing users, CRUD for users, etc. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class UserController extends SimpleController +{ + /** + * Processes the request to create a new user (from the admin controls). + * + * Processes the request from the user creation form, checking that: + * 1. The username and email are not already in use; + * 2. The logged-in user has the necessary permissions to update the posted field(s); + * 3. The submitted data is valid. + * This route requires authentication. + * Request type: POST + * @see getModalCreate + */ + public function create($request, $response, $args) + { + // Get POST parameters: user_name, first_name, last_name, email, locale, (group) + $params = $request->getParsedBody(); + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'create_user')) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */ + $ms = $this->ci->alerts; + + // Load the request schema + $schema = new RequestSchema('schema://requests/user/create.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + $error = false; + + // Validate request data + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + $error = true; + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // Check if username or email already exists + if ($classMapper->staticMethod('user', 'findUnique', $data['user_name'], 'user_name')) { + $ms->addMessageTranslated('danger', 'USERNAME.IN_USE', $data); + $error = true; + } + + if ($classMapper->staticMethod('user', 'findUnique', $data['email'], 'email')) { + $ms->addMessageTranslated('danger', 'EMAIL.IN_USE', $data); + $error = true; + } + + if ($error) { + return $response->withStatus(400); + } + + /** @var UserFrosting\Config\Config $config */ + $config = $this->ci->config; + + // If currentUser does not have permission to set the group, but they try to set it to something other than their own group, + // throw an exception. + if (!$authorizer->checkAccess($currentUser, 'create_user_field', [ + 'fields' => ['group'] + ])) { + if (isset($data['group_id']) && $data['group_id'] != $currentUser->group_id) { + throw new ForbiddenException(); + } + } + + // In any case, set the group id if not otherwise set + if (!isset($data['group_id'])) { + $data['group_id'] = $currentUser->group_id; + } + + $data['flag_verified'] = 1; + // Set password as empty on initial creation. We will then send email so new user can set it themselves via a verification token + $data['password'] = ''; + + // All checks passed! log events/activities, create user, and send verification email (if required) + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction( function() use ($classMapper, $data, $ms, $config, $currentUser) { + // Create the user + $user = $classMapper->createInstance('user', $data); + + // Store new user to database + $user->save(); + + // Create activity record + $this->ci->userActivityLogger->info("User {$currentUser->user_name} created a new account for {$user->user_name}.", [ + 'type' => 'account_create', + 'user_id' => $currentUser->id + ]); + + // Load default roles + $defaultRoleSlugs = $classMapper->staticMethod('role', 'getDefaultSlugs'); + $defaultRoles = $classMapper->staticMethod('role', 'whereIn', 'slug', $defaultRoleSlugs)->get(); + $defaultRoleIds = $defaultRoles->pluck('id')->all(); + + // Attach default roles + $user->roles()->attach($defaultRoleIds); + + // Try to generate a new password request + $passwordRequest = $this->ci->repoPasswordReset->create($user, $config['password_reset.timeouts.create']); + + // Create and send welcome email with password set link + $message = new TwigMailMessage($this->ci->view, 'mail/password-create.html.twig'); + + $message->from($config['address_book.admin']) + ->addEmailRecipient(new EmailRecipient($user->email, $user->full_name)) + ->addParams([ + 'user' => $user, + 'create_password_expiration' => $config['password_reset.timeouts.create'] / 3600 . ' hours', + 'token' => $passwordRequest->getToken() + ]); + + $this->ci->mailer->send($message); + + $ms->addMessageTranslated('success', 'USER.CREATED', $data); + }); + + return $response->withStatus(200); + } + + /** + * Processes the request to send a user a password reset email. + * + * Processes the request from the user update form, checking that: + * 1. The target user's new email address, if specified, is not already in use; + * 2. The logged-in user has the necessary permissions to update the posted field(s); + * 3. We're not trying to disable the master account; + * 4. The submitted data is valid. + * This route requires authentication. + * Request type: POST + */ + public function createPasswordReset($request, $response, $args) + { + // Get the username from the URL + $user = $this->getUserFromParams($args); + + if (!$user) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled resource - check that currentUser has permission to edit "password" for this user + if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ + 'user' => $user, + 'fields' => ['password'] + ])) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Config\Config $config */ + $config = $this->ci->config; + + /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */ + $ms = $this->ci->alerts; + + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction( function() use ($user, $config) { + + // Create a password reset and shoot off an email + $passwordReset = $this->ci->repoPasswordReset->create($user, $config['password_reset.timeouts.reset']); + + // Create and send welcome email with password set link + $message = new TwigMailMessage($this->ci->view, 'mail/password-reset.html.twig'); + + $message->from($config['address_book.admin']) + ->addEmailRecipient(new EmailRecipient($user->email, $user->full_name)) + ->addParams([ + 'user' => $user, + 'token' => $passwordReset->getToken(), + 'request_date' => Carbon::now()->format('Y-m-d H:i:s') + ]); + + $this->ci->mailer->send($message); + }); + + $ms->addMessageTranslated('success', 'PASSWORD.FORGET.REQUEST_SENT', [ + 'email' => $user->email + ]); + return $response->withStatus(200); + } + + /** + * Processes the request to delete an existing user. + * + * Deletes the specified user, removing any existing associations. + * Before doing so, checks that: + * 1. You are not trying to delete the master account; + * 2. You have permission to delete the target user's account. + * This route requires authentication (and should generally be limited to admins or the root user). + * Request type: DELETE + */ + public function delete($request, $response, $args) + { + $user = $this->getUserFromParams($args); + + // If the user doesn't exist, return 404 + if (!$user) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'delete_user', [ + 'user' => $user + ])) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Config\Config $config */ + $config = $this->ci->config; + + // Check that we are not deleting the master account + // Need to use loose comparison for now, because some DBs return `id` as a string + if ($user->id == $config['reserved_user_ids.master']) { + $e = new BadRequestException(); + $e->addUserMessage('DELETE_MASTER'); + throw $e; + } + + $userName = $user->user_name; + + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction( function() use ($user, $userName, $currentUser) { + $user->delete(); + unset($user); + + // Create activity record + $this->ci->userActivityLogger->info("User {$currentUser->user_name} deleted the account for {$userName}.", [ + 'type' => 'account_delete', + 'user_id' => $currentUser->id + ]); + }); + + /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */ + $ms = $this->ci->alerts; + + $ms->addMessageTranslated('success', 'DELETION_SUCCESSFUL', [ + 'user_name' => $userName + ]); + + return $response->withStatus(200); + } + + /** + * Returns activity history for a single user. + * + * This page requires authentication. + * Request type: GET + */ + public function getActivities($request, $response, $args) + { + $user = $this->getUserFromParams($args); + + // If the user doesn't exist, return 404 + if (!$user) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // GET parameters + $params = $request->getQueryParams(); + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'view_user_field', [ + 'user' => $user, + 'property' => 'activities' + ])) { + throw new ForbiddenException(); + } + + $sprunje = $classMapper->createInstance('activity_sprunje', $classMapper, $params); + + $sprunje->extendQuery(function ($query) use ($user) { + return $query->where('user_id', $user->id); + }); + + // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. + // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). + return $sprunje->toResponse($response); + } + + /** + * Returns info for a single user. + * + * This page requires authentication. + * Request type: GET + */ + public function getInfo($request, $response, $args) + { + $user = $this->getUserFromParams($args); + + // If the user doesn't exist, return 404 + if (!$user) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // Join user's most recent activity + $user = $classMapper->createInstance('user') + ->where('user_name', $user->user_name) + ->joinLastActivity() + ->with('lastActivity', 'group') + ->first(); + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_user', [ + 'user' => $user + ])) { + throw new ForbiddenException(); + } + + $result = $user->toArray(); + + // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. + // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). + return $response->withJson($result, 200, JSON_PRETTY_PRINT); + } + + /** + * Returns a list of Users + * + * Generates a list of users, optionally paginated, sorted and/or filtered. + * This page requires authentication. + * Request type: GET + */ + public function getList($request, $response, $args) + { + // GET parameters + $params = $request->getQueryParams(); + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_users')) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + $sprunje = $classMapper->createInstance('user_sprunje', $classMapper, $params); + + // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. + // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). + return $sprunje->toResponse($response); + } + + /** + * Renders the modal form to confirm user deletion. + * + * This does NOT render a complete page. Instead, it renders the HTML for the modal, which can be embedded in other pages. + * This page requires authentication. + * Request type: GET + */ + public function getModalConfirmDelete($request, $response, $args) + { + // GET parameters + $params = $request->getQueryParams(); + + $user = $this->getUserFromParams($params); + + // If the user doesn't exist, return 404 + if (!$user) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'delete_user', [ + 'user' => $user + ])) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Config\Config $config */ + $config = $this->ci->config; + + // Check that we are not deleting the master account + // Need to use loose comparison for now, because some DBs return `id` as a string + if ($user->id == $config['reserved_user_ids.master']) { + $e = new BadRequestException(); + $e->addUserMessage('DELETE_MASTER'); + throw $e; + } + + return $this->ci->view->render($response, 'modals/confirm-delete-user.html.twig', [ + 'user' => $user, + 'form' => [ + 'action' => "api/users/u/{$user->user_name}", + ] + ]); + } + + /** + * Renders the modal form for creating a new user. + * + * This does NOT render a complete page. Instead, it renders the HTML for the modal, which can be embedded in other pages. + * If the currently logged-in user has permission to modify user group membership, then the group toggle will be displayed. + * Otherwise, the user will be added to the default group and receive the default roles automatically. + * This page requires authentication. + * Request type: GET + */ + public function getModalCreate($request, $response, $args) + { + // GET parameters + $params = $request->getQueryParams(); + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + /** @var UserFrosting\I18n\MessageTranslator $translator */ + $translator = $this->ci->translator; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'create_user')) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + /** @var UserFrosting\Config\Config $config */ + $config = $this->ci->config; + + // Determine form fields to hide/disable + // TODO: come back to this when we finish implementing theming + $fields = [ + 'hidden' => ['theme'], + 'disabled' => [] + ]; + + // Get a list of all locales + $locales = $config->getDefined('site.locales.available'); + + // Determine if currentUser has permission to modify the group. If so, show the 'group' dropdown. + // Otherwise, set to the currentUser's group and disable the dropdown. + if ($authorizer->checkAccess($currentUser, 'create_user_field', [ + 'fields' => ['group'] + ])) { + // Get a list of all groups + $groups = $classMapper->staticMethod('group', 'all'); + } else { + // Get the current user's group + $groups = $currentUser->group()->get(); + $fields['disabled'][] = 'group'; + } + + // Create a dummy user to prepopulate fields + $data = [ + 'group_id' => $currentUser->group_id, + 'locale' => $config['site.registration.user_defaults.locale'], + 'theme' => '' + ]; + + $user = $classMapper->createInstance('user', $data); + + // Load validation rules + $schema = new RequestSchema('schema://requests/user/create.yaml'); + $validator = new JqueryValidationAdapter($schema, $this->ci->translator); + + return $this->ci->view->render($response, 'modals/user.html.twig', [ + 'user' => $user, + 'groups' => $groups, + 'locales' => $locales, + 'form' => [ + 'action' => 'api/users', + 'method' => 'POST', + 'fields' => $fields, + 'submit_text' => $translator->translate('CREATE') + ], + 'page' => [ + 'validators' => $validator->rules('json', false) + ] + ]); + } + + /** + * Renders the modal form for editing an existing user. + * + * This does NOT render a complete page. Instead, it renders the HTML for the modal, which can be embedded in other pages. + * This page requires authentication. + * Request type: GET + */ + public function getModalEdit($request, $response, $args) + { + // GET parameters + $params = $request->getQueryParams(); + + $user = $this->getUserFromParams($params); + + // If the user doesn't exist, return 404 + if (!$user) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // Get the user to edit + $user = $classMapper->staticMethod('user', 'where', 'user_name', $user->user_name) + ->with('group') + ->first(); + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled resource - check that currentUser has permission to edit basic fields "name", "email", "locale" for this user + $fieldNames = ['name', 'email', 'locale']; + if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ + 'user' => $user, + 'fields' => $fieldNames + ])) { + throw new ForbiddenException(); + } + + // Get a list of all groups + $groups = $classMapper->staticMethod('group', 'all'); + + /** @var UserFrosting\Config\Config $config */ + $config = $this->ci->config; + + // Get a list of all locales + $locales = $config->getDefined('site.locales.available'); + + // Generate form + $fields = [ + 'hidden' => ['theme'], + 'disabled' => ['user_name'] + ]; + + // Disable group field if currentUser doesn't have permission to modify group + if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ + 'user' => $user, + 'fields' => ['group'] + ])) { + $fields['disabled'][] = 'group'; + } + + // Load validation rules + $schema = new RequestSchema('schema://requests/user/edit-info.yaml'); + $validator = new JqueryValidationAdapter($schema, $this->ci->translator); + + $translator = $this->ci->translator; + + return $this->ci->view->render($response, 'modals/user.html.twig', [ + 'user' => $user, + 'groups' => $groups, + 'locales' => $locales, + 'form' => [ + 'action' => "api/users/u/{$user->user_name}", + 'method' => 'PUT', + 'fields' => $fields, + 'submit_text' => $translator->translate('UPDATE') + ], + 'page' => [ + 'validators' => $validator->rules('json', false) + ] + ]); + } + + /** + * Renders the modal form for editing a user's password. + * + * This does NOT render a complete page. Instead, it renders the HTML for the form, which can be embedded in other pages. + * This page requires authentication. + * Request type: GET + */ + public function getModalEditPassword($request, $response, $args) + { + // GET parameters + $params = $request->getQueryParams(); + + $user = $this->getUserFromParams($params); + + // If the user doesn't exist, return 404 + if (!$user) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled resource - check that currentUser has permission to edit "password" field for this user + if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ + 'user' => $user, + 'fields' => ['password'] + ])) { + throw new ForbiddenException(); + } + + // Load validation rules + $schema = new RequestSchema('schema://requests/user/edit-password.yaml'); + $validator = new JqueryValidationAdapter($schema, $this->ci->translator); + + return $this->ci->view->render($response, 'modals/user-set-password.html.twig', [ + 'user' => $user, + 'page' => [ + 'validators' => $validator->rules('json', false) + ] + ]); + } + + /** + * Renders the modal form for editing a user's roles. + * + * This does NOT render a complete page. Instead, it renders the HTML for the form, which can be embedded in other pages. + * This page requires authentication. + * Request type: GET + */ + public function getModalEditRoles($request, $response, $args) + { + // GET parameters + $params = $request->getQueryParams(); + + $user = $this->getUserFromParams($params); + + // If the user doesn't exist, return 404 + if (!$user) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled resource - check that currentUser has permission to edit "roles" field for this user + if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ + 'user' => $user, + 'fields' => ['roles'] + ])) { + throw new ForbiddenException(); + } + + return $this->ci->view->render($response, 'modals/user-manage-roles.html.twig', [ + 'user' => $user + ]); + } + + /** + * Returns a list of effective Permissions for a specified User. + * + * Generates a list of permissions, optionally paginated, sorted and/or filtered. + * This page requires authentication. + * Request type: GET + */ + public function getPermissions($request, $response, $args) + { + $user = $this->getUserFromParams($args); + + // If the user doesn't exist, return 404 + if (!$user) { + throw new NotFoundException($request, $response); + } + + // GET parameters + $params = $request->getQueryParams(); + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'view_user_field', [ + 'user' => $user, + 'property' => 'permissions' + ])) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + $params['user_id'] = $user->id; + $sprunje = $classMapper->createInstance('user_permission_sprunje', $classMapper, $params); + + $response = $sprunje->toResponse($response); + + // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. + // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). + return $response; + } + + /** + * Returns roles associated with a single user. + * + * This page requires authentication. + * Request type: GET + */ + public function getRoles($request, $response, $args) + { + $user = $this->getUserFromParams($args); + + // If the user doesn't exist, return 404 + if (!$user) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // GET parameters + $params = $request->getQueryParams(); + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'view_user_field', [ + 'user' => $user, + 'property' => 'roles' + ])) { + throw new ForbiddenException(); + } + + $sprunje = $classMapper->createInstance('role_sprunje', $classMapper, $params); + $sprunje->extendQuery(function ($query) use ($user) { + return $query->forUser($user->id); + }); + + // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. + // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). + return $sprunje->toResponse($response); + } + + /** + * Renders a page displaying a user's information, in read-only mode. + * + * This checks that the currently logged-in user has permission to view the requested user's info. + * It checks each field individually, showing only those that you have permission to view. + * This will also try to show buttons for activating, disabling/enabling, deleting, and editing the user. + * This page requires authentication. + * Request type: GET + */ + public function pageInfo($request, $response, $args) + { + $user = $this->getUserFromParams($args); + + // If the user no longer exists, forward to main user listing page + if (!$user) { + $usersPage = $this->ci->router->pathFor('uri_users'); + return $response->withRedirect($usersPage, 404); + } + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_user', [ + 'user' => $user + ])) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Config\Config $config */ + $config = $this->ci->config; + + // Get a list of all locales + $locales = $config->getDefined('site.locales.available'); + + // Determine fields that currentUser is authorized to view + $fieldNames = ['user_name', 'name', 'email', 'locale', 'group', 'roles']; + + // Generate form + $fields = [ + // Always hide these + 'hidden' => ['theme'] + ]; + + // Determine which fields should be hidden + foreach ($fieldNames as $field) { + if (!$authorizer->checkAccess($currentUser, 'view_user_field', [ + 'user' => $user, + 'property' => $field + ])) { + $fields['hidden'][] = $field; + } + } + + // Determine buttons to display + $editButtons = [ + 'hidden' => [] + ]; + + if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ + 'user' => $user, + 'fields' => ['name', 'email', 'locale'] + ])) { + $editButtons['hidden'][] = 'edit'; + } + + if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ + 'user' => $user, + 'fields' => ['flag_enabled'] + ])) { + $editButtons['hidden'][] = 'enable'; + } + + if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ + 'user' => $user, + 'fields' => ['flag_verified'] + ])) { + $editButtons['hidden'][] = 'activate'; + } + + if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ + 'user' => $user, + 'fields' => ['password'] + ])) { + $editButtons['hidden'][] = 'password'; + } + + if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ + 'user' => $user, + 'fields' => ['roles'] + ])) { + $editButtons['hidden'][] = 'roles'; + } + + if (!$authorizer->checkAccess($currentUser, 'delete_user', [ + 'user' => $user + ])) { + $editButtons['hidden'][] = 'delete'; + } + + // Determine widgets to display + $widgets = [ + 'hidden' => [] + ]; + + if (!$authorizer->checkAccess($currentUser, 'view_user_field', [ + 'user' => $user, + 'property' => 'permissions' + ])) { + $widgets['hidden'][] = 'permissions'; + } + + if (!$authorizer->checkAccess($currentUser, 'view_user_field', [ + 'user' => $user, + 'property' => 'activities' + ])) { + $widgets['hidden'][] = 'activities'; + } + + return $this->ci->view->render($response, 'pages/user.html.twig', [ + 'user' => $user, + 'locales' => $locales, + 'fields' => $fields, + 'tools' => $editButtons, + 'widgets' => $widgets + ]); + } + + /** + * Renders the user listing page. + * + * This page renders a table of users, with dropdown menus for admin actions for each user. + * Actions typically include: edit user details, activate user, enable/disable user, delete user. + * This page requires authentication. + * Request type: GET + */ + public function pageList($request, $response, $args) + { + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_users')) { + throw new ForbiddenException(); + } + + return $this->ci->view->render($response, 'pages/users.html.twig'); + } + + /** + * Processes the request to update an existing user's basic details (first_name, last_name, email, locale, group_id) + * + * Processes the request from the user update form, checking that: + * 1. The target user's new email address, if specified, is not already in use; + * 2. The logged-in user has the necessary permissions to update the putted field(s); + * 3. The submitted data is valid. + * This route requires authentication. + * Request type: PUT + */ + public function updateInfo($request, $response, $args) + { + // Get the username from the URL + $user = $this->getUserFromParams($args); + + if (!$user) { + throw new NotFoundException($request, $response); + } + + /** @var UserFrosting\Config\Config $config */ + $config = $this->ci->config; + + // Get PUT parameters + $params = $request->getParsedBody(); + + /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */ + $ms = $this->ci->alerts; + + // Load the request schema + $schema = new RequestSchema('schema://requests/user/edit-info.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + $error = false; + + // Validate request data + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + $ms->addValidationErrors($validator); + $error = true; + } + + // Determine targeted fields + $fieldNames = []; + foreach ($data as $name => $value) { + if ($name == 'first_name' || $name == 'last_name') { + $fieldNames[] = 'name'; + } elseif ($name == 'group_id') { + $fieldNames[] = 'group'; + } else { + $fieldNames[] = $name; + } + } + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled resource - check that currentUser has permission to edit submitted fields for this user + if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ + 'user' => $user, + 'fields' => array_values(array_unique($fieldNames)) + ])) { + throw new ForbiddenException(); + } + + // Only the master account can edit the master account! + if ( + ($user->id == $config['reserved_user_ids.master']) && + ($currentUser->id != $config['reserved_user_ids.master']) + ) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // Check if email already exists + if ( + isset($data['email']) && + $data['email'] != $user->email && + $classMapper->staticMethod('user', 'findUnique', $data['email'], 'email') + ) { + $ms->addMessageTranslated('danger', 'EMAIL.IN_USE', $data); + $error = true; + } + + if ($error) { + return $response->withStatus(400); + } + + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction( function() use ($data, $user, $currentUser) { + // Update the user and generate success messages + foreach ($data as $name => $value) { + if ($value != $user->$name) { + $user->$name = $value; + } + } + + $user->save(); + + // Create activity record + $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated basic account info for user {$user->user_name}.", [ + 'type' => 'account_update_info', + 'user_id' => $currentUser->id + ]); + }); + + $ms->addMessageTranslated('success', 'DETAILS_UPDATED', [ + 'user_name' => $user->user_name + ]); + return $response->withStatus(200); + } + + /** + * Processes the request to update a specific field for an existing user. + * + * Supports editing all user fields, including password, enabled/disabled status and verification status. + * Processes the request from the user update form, checking that: + * 1. The logged-in user has the necessary permissions to update the putted field(s); + * 2. We're not trying to disable the master account; + * 3. The submitted data is valid. + * This route requires authentication. + * Request type: PUT + */ + public function updateField($request, $response, $args) + { + // Get the username from the URL + $user = $this->getUserFromParams($args); + + if (!$user) { + throw new NotFoundException($request, $response); + } + + // Get key->value pair from URL and request body + $fieldName = $args['field']; + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */ + $authorizer = $this->ci->authorizer; + + /** @var UserFrosting\Sprinkle\Account\Database\Models\User $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled resource - check that currentUser has permission to edit the specified field for this user + if (!$authorizer->checkAccess($currentUser, 'update_user_field', [ + 'user' => $user, + 'fields' => [$fieldName] + ])) { + throw new ForbiddenException(); + } + + /** @var UserFrosting\Config\Config $config */ + $config = $this->ci->config; + + // Only the master account can edit the master account! + if ( + ($user->id == $config['reserved_user_ids.master']) && + ($currentUser->id != $config['reserved_user_ids.master']) + ) { + throw new ForbiddenException(); + } + + // Get PUT parameters: value + $put = $request->getParsedBody(); + + if (!isset($put['value'])) { + throw new BadRequestException(); + } + + // Create and validate key -> value pair + $params = [ + $fieldName => $put['value'] + ]; + + // Load the request schema + $schema = new RequestSchema('schema://requests/user/edit-field.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and throw exception on validation errors. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + // TODO: encapsulate the communication of error messages from ServerSideValidator to the BadRequestException + $e = new BadRequestException(); + foreach ($validator->errors() as $idx => $field) { + foreach($field as $eidx => $error) { + $e->addUserMessage($error); + } + } + throw $e; + } + + // Get validated and transformed value + $fieldValue = $data[$fieldName]; + + /** @var UserFrosting\Sprinkle\Core\MessageStream $ms */ + $ms = $this->ci->alerts; + + // Special checks and transformations for certain fields + if ($fieldName == 'flag_enabled') { + // Check that we are not disabling the master account + if ( + ($user->id == $config['reserved_user_ids.master']) && + ($fieldValue == '0') + ) { + $e = new BadRequestException(); + $e->addUserMessage('DISABLE_MASTER'); + throw $e; + } elseif ( + ($user->id == $currentUser->id) && + ($fieldValue == '0') + ) { + $e = new BadRequestException(); + $e->addUserMessage('DISABLE_SELF'); + throw $e; + } + } elseif ($fieldName == 'password') { + $fieldValue = Password::hash($fieldValue); + } + + // Begin transaction - DB will be rolled back if an exception occurs + Capsule::transaction( function() use ($fieldName, $fieldValue, $user, $currentUser) { + if ($fieldName == 'roles') { + $newRoles = collect($fieldValue)->pluck('role_id')->all(); + $user->roles()->sync($newRoles); + } else { + $user->$fieldName = $fieldValue; + $user->save(); + } + + // Create activity record + $this->ci->userActivityLogger->info("User {$currentUser->user_name} updated property '$fieldName' for user {$user->user_name}.", [ + 'type' => 'account_update_field', + 'user_id' => $currentUser->id + ]); + }); + + // Add success messages + if ($fieldName == 'flag_enabled') { + if ($fieldValue == '1') { + $ms->addMessageTranslated('success', 'ENABLE_SUCCESSFUL', [ + 'user_name' => $user->user_name + ]); + } else { + $ms->addMessageTranslated('success', 'DISABLE_SUCCESSFUL', [ + 'user_name' => $user->user_name + ]); + } + } elseif ($fieldName == 'flag_verified') { + $ms->addMessageTranslated('success', 'MANUALLY_ACTIVATED', [ + 'user_name' => $user->user_name + ]); + } else { + $ms->addMessageTranslated('success', 'DETAILS_UPDATED', [ + 'user_name' => $user->user_name + ]); + } + + return $response->withStatus(200); + } + + protected function getUserFromParams($params) + { + // Load the request schema + $schema = new RequestSchema('schema://requests/user/get-by-username.yaml'); + + // Whitelist and set parameter defaults + $transformer = new RequestDataTransformer($schema); + $data = $transformer->transform($params); + + // Validate, and throw exception on validation errors. + $validator = new ServerSideValidator($schema, $this->ci->translator); + if (!$validator->validate($data)) { + // TODO: encapsulate the communication of error messages from ServerSideValidator to the BadRequestException + $e = new BadRequestException(); + foreach ($validator->errors() as $idx => $field) { + foreach($field as $eidx => $error) { + $e->addUserMessage($error); + } + } + throw $e; + } + + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = $this->ci->classMapper; + + // Get the user to delete + $user = $classMapper->staticMethod('user', 'where', 'user_name', $data['user_name']) + ->first(); + + return $user; + } +} diff --git a/login/app/sprinkles/admin/src/ServicesProvider/ServicesProvider.php b/login/app/sprinkles/admin/src/ServicesProvider/ServicesProvider.php new file mode 100755 index 0000000..061d90c --- /dev/null +++ b/login/app/sprinkles/admin/src/ServicesProvider/ServicesProvider.php @@ -0,0 +1,84 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Admin\ServicesProvider; + +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; +use UserFrosting\Sprinkle\Account\Authenticate\Authenticator; +use UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager; +use UserFrosting\Sprinkle\Core\Facades\Debug; + +/** + * Registers services for the admin sprinkle. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class ServicesProvider +{ + /** + * Register UserFrosting's admin services. + * + * @param Container $container A DI container implementing ArrayAccess and container-interop. + */ + public function register($container) + { + /** + * Extend the 'classMapper' service to register sprunje classes. + * + * Mappings added: 'activity_sprunje', 'group_sprunje', 'permission_sprunje', 'role_sprunje', 'user_sprunje' + */ + $container->extend('classMapper', function ($classMapper, $c) { + $classMapper->setClassMapping('activity_sprunje', 'UserFrosting\Sprinkle\Admin\Sprunje\ActivitySprunje'); + $classMapper->setClassMapping('group_sprunje', 'UserFrosting\Sprinkle\Admin\Sprunje\GroupSprunje'); + $classMapper->setClassMapping('permission_sprunje', 'UserFrosting\Sprinkle\Admin\Sprunje\PermissionSprunje'); + $classMapper->setClassMapping('permission_user_sprunje', 'UserFrosting\Sprinkle\Admin\Sprunje\PermissionUserSprunje'); + $classMapper->setClassMapping('role_sprunje', 'UserFrosting\Sprinkle\Admin\Sprunje\RoleSprunje'); + $classMapper->setClassMapping('user_sprunje', 'UserFrosting\Sprinkle\Admin\Sprunje\UserSprunje'); + $classMapper->setClassMapping('user_permission_sprunje', 'UserFrosting\Sprinkle\Admin\Sprunje\UserPermissionSprunje'); + return $classMapper; + }); + + /** + * Returns a callback that handles setting the `UF-Redirect` header after a successful login. + * + * Overrides the service definition in the account Sprinkle. + */ + $container['redirect.onLogin'] = function ($c) { + /** + * This method is invoked when a user completes the login process. + * + * Returns a callback that handles setting the `UF-Redirect` header after a successful login. + * @param \Psr\Http\Message\ServerRequestInterface $request + * @param \Psr\Http\Message\ResponseInterface $response + * @param array $args + * @return \Psr\Http\Message\ResponseInterface + */ + return function (Request $request, Response $response, array $args) use ($c) { + // Backwards compatibility for the deprecated determineRedirectOnLogin service + if ($c->has('determineRedirectOnLogin')) { + $determineRedirectOnLogin = $c->determineRedirectOnLogin; + + return $determineRedirectOnLogin($response)->withStatus(200); + } + + /** @var UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ + $authorizer = $c->authorizer; + + $currentUser = $c->authenticator->user(); + + if ($authorizer->checkAccess($currentUser, 'uri_dashboard')) { + return $response->withHeader('UF-Redirect', $c->router->pathFor('dashboard')); + } elseif ($authorizer->checkAccess($currentUser, 'uri_account_settings')) { + return $response->withHeader('UF-Redirect', $c->router->pathFor('settings')); + } else { + return $response->withHeader('UF-Redirect', $c->router->pathFor('index')); + } + }; + }; + } +} diff --git a/login/app/sprinkles/admin/src/Sprunje/ActivitySprunje.php b/login/app/sprinkles/admin/src/Sprunje/ActivitySprunje.php new file mode 100755 index 0000000..da4f0e3 --- /dev/null +++ b/login/app/sprinkles/admin/src/Sprunje/ActivitySprunje.php @@ -0,0 +1,80 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Admin\Sprunje; + +use Illuminate\Database\Capsule\Manager as Capsule; +use UserFrosting\Sprinkle\Core\Facades\Debug; +use UserFrosting\Sprinkle\Core\Sprunje\Sprunje; + +/** + * ActivitySprunje + * + * Implements Sprunje for the activities API. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class ActivitySprunje extends Sprunje +{ + protected $sortable = [ + 'occurred_at', + 'user', + 'description' + ]; + + protected $filterable = [ + 'occurred_at', + 'user', + 'description' + ]; + + protected $name = 'activities'; + + /** + * Set the initial query used by your Sprunje. + */ + protected function baseQuery() + { + $query = $this->classMapper->createInstance('activity'); + + return $query->joinUser(); + } + + /** + * Filter LIKE the user info. + * + * @param Builder $query + * @param mixed $value + * @return $this + */ + protected function filterUser($query, $value) + { + // Split value on separator for OR queries + $values = explode($this->orSeparator, $value); + $query->where(function ($query) use ($values) { + foreach ($values as $value) { + $query->orLike('users.first_name', $value) + ->orLike('users.last_name', $value) + ->orLike('users.email', $value); + } + }); + return $this; + } + + /** + * Sort based on user last name. + * + * @param Builder $query + * @param string $direction + * @return $this + */ + protected function sortUser($query, $direction) + { + $query->orderBy('users.last_name', $direction); + return $this; + } +} diff --git a/login/app/sprinkles/admin/src/Sprunje/GroupSprunje.php b/login/app/sprinkles/admin/src/Sprunje/GroupSprunje.php new file mode 100755 index 0000000..7c75691 --- /dev/null +++ b/login/app/sprinkles/admin/src/Sprunje/GroupSprunje.php @@ -0,0 +1,42 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Admin\Sprunje; + +use Illuminate\Database\Capsule\Manager as Capsule; +use UserFrosting\Sprinkle\Core\Facades\Debug; +use UserFrosting\Sprinkle\Core\Sprunje\Sprunje; + +/** + * GroupSprunje + * + * Implements Sprunje for the groups API. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class GroupSprunje extends Sprunje +{ + protected $name = 'groups'; + + protected $sortable = [ + 'name', + 'description' + ]; + + protected $filterable = [ + 'name', + 'description' + ]; + + /** + * {@inheritDoc} + */ + protected function baseQuery() + { + return $this->classMapper->createInstance('group')->newQuery(); + } +} diff --git a/login/app/sprinkles/admin/src/Sprunje/PermissionSprunje.php b/login/app/sprinkles/admin/src/Sprunje/PermissionSprunje.php new file mode 100755 index 0000000..c1803f1 --- /dev/null +++ b/login/app/sprinkles/admin/src/Sprunje/PermissionSprunje.php @@ -0,0 +1,93 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Admin\Sprunje; + +use Illuminate\Database\Capsule\Manager as Capsule; +use UserFrosting\Sprinkle\Core\Facades\Debug; +use UserFrosting\Sprinkle\Core\Sprunje\Sprunje; + +/** + * PermissionSprunje + * + * Implements Sprunje for the permissions API. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class PermissionSprunje extends Sprunje +{ + protected $name = 'permissions'; + + protected $sortable = [ + 'name', + 'properties' + ]; + + protected $filterable = [ + 'name', + 'properties', + 'info' + ]; + + protected $excludeForAll = [ + 'info' + ]; + + /** + * {@inheritDoc} + */ + protected function baseQuery() + { + return $this->classMapper->createInstance('permission')->newQuery(); + } + + /** + * Filter LIKE the slug, conditions, or description. + * + * @param Builder $query + * @param mixed $value + * @return $this + */ + protected function filterInfo($query, $value) + { + return $this->filterProperties($query, $value); + } + + /** + * Filter LIKE the slug, conditions, or description. + * + * @param Builder $query + * @param mixed $value + * @return $this + */ + protected function filterProperties($query, $value) + { + // Split value on separator for OR queries + $values = explode($this->orSeparator, $value); + $query->where(function ($query) use ($values) { + foreach ($values as $value) { + $query->orLike('slug', $value) + ->orLike('conditions', $value) + ->orLike('description', $value); + } + }); + return $this; + } + + /** + * Sort based on slug. + * + * @param Builder $query + * @param string $direction + * @return $this + */ + protected function sortProperties($query, $direction) + { + $query->orderBy('slug', $direction); + return $this; + } +} diff --git a/login/app/sprinkles/admin/src/Sprunje/PermissionUserSprunje.php b/login/app/sprinkles/admin/src/Sprunje/PermissionUserSprunje.php new file mode 100755 index 0000000..242681d --- /dev/null +++ b/login/app/sprinkles/admin/src/Sprunje/PermissionUserSprunje.php @@ -0,0 +1,48 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Admin\Sprunje; + +use Illuminate\Database\Capsule\Manager as Capsule; +use UserFrosting\Sprinkle\Core\Facades\Debug; +use UserFrosting\Support\Exception\BadRequestException; +use UserFrosting\Support\Exception\NotFoundException; + +/** + * PermissionUserSprunje + * + * Implements Sprunje for retrieving a list of users for a specified permission. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class PermissionUserSprunje extends UserSprunje +{ + protected $name = 'permission_users'; + + /** + * {@inheritDoc} + */ + protected function baseQuery() + { + // Requires a permission id + if (!isset($this->options['permission_id'])) { + throw new BadRequestException(); + } + + $permission = $this->classMapper->staticMethod('permission', 'find', $this->options['permission_id']); + + // If the permission doesn't exist, return 404 + if (!$permission) { + throw new NotFoundException; + } + + // Get permission users + $query = $permission->users()->withVia('roles_via'); + + return $query; + } +} diff --git a/login/app/sprinkles/admin/src/Sprunje/RoleSprunje.php b/login/app/sprinkles/admin/src/Sprunje/RoleSprunje.php new file mode 100755 index 0000000..c5e0f8b --- /dev/null +++ b/login/app/sprinkles/admin/src/Sprunje/RoleSprunje.php @@ -0,0 +1,67 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Admin\Sprunje; + +use Illuminate\Database\Capsule\Manager as Capsule; +use UserFrosting\Sprinkle\Core\Facades\Debug; +use UserFrosting\Sprinkle\Core\Sprunje\Sprunje; + +/** + * RoleSprunje + * + * Implements Sprunje for the roles API. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class RoleSprunje extends Sprunje +{ + protected $name = 'roles'; + + protected $sortable = [ + 'name', + 'description' + ]; + + protected $filterable = [ + 'name', + 'description', + 'info' + ]; + + protected $excludeForAll = [ + 'info' + ]; + + /** + * {@inheritDoc} + */ + protected function baseQuery() + { + return $this->classMapper->createInstance('role')->newQuery(); + } + + /** + * Filter LIKE name OR description. + * + * @param Builder $query + * @param mixed $value + * @return $this + */ + protected function filterInfo($query, $value) + { + // Split value on separator for OR queries + $values = explode($this->orSeparator, $value); + $query->where(function ($query) use ($values) { + foreach ($values as $value) { + $query->orLike('name', $value) + ->orLike('description', $value); + } + }); + return $this; + } +} diff --git a/login/app/sprinkles/admin/src/Sprunje/UserPermissionSprunje.php b/login/app/sprinkles/admin/src/Sprunje/UserPermissionSprunje.php new file mode 100755 index 0000000..6142e74 --- /dev/null +++ b/login/app/sprinkles/admin/src/Sprunje/UserPermissionSprunje.php @@ -0,0 +1,48 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Admin\Sprunje; + +use Illuminate\Database\Capsule\Manager as Capsule; +use UserFrosting\Sprinkle\Core\Facades\Debug; +use UserFrosting\Support\Exception\BadRequestException; +use UserFrosting\Support\Exception\NotFoundException; + +/** + * UserPermissionSprunje + * + * Implements Sprunje for retrieving a list of permissions for a specified user. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class UserPermissionSprunje extends PermissionSprunje +{ + protected $name = 'user_permissions'; + + /** + * {@inheritDoc} + */ + protected function baseQuery() + { + // Requires a user id + if (!isset($this->options['user_id'])) { + throw new BadRequestException(); + } + + $user = $this->classMapper->staticMethod('user', 'find', $this->options['user_id']); + + // If the user doesn't exist, return 404 + if (!$user) { + throw new NotFoundException; + } + + // Get user permissions + $query = $user->permissions()->withVia('roles_via'); + + return $query; + } +} diff --git a/login/app/sprinkles/admin/src/Sprunje/UserSprunje.php b/login/app/sprinkles/admin/src/Sprunje/UserSprunje.php new file mode 100755 index 0000000..12378f9 --- /dev/null +++ b/login/app/sprinkles/admin/src/Sprunje/UserSprunje.php @@ -0,0 +1,185 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Admin\Sprunje; + +use Illuminate\Database\Capsule\Manager as Capsule; +use UserFrosting\Sprinkle\Core\Facades\Debug; +use UserFrosting\Sprinkle\Core\Facades\Translator; +use UserFrosting\Sprinkle\Core\Sprunje\Sprunje; + +/** + * UserSprunje + * + * Implements Sprunje for the users API. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class UserSprunje extends Sprunje +{ + protected $name = 'users'; + + protected $listable = [ + 'status' + ]; + + protected $sortable = [ + 'name', + 'last_activity', + 'status' + ]; + + protected $filterable = [ + 'name', + 'last_activity', + 'status' + ]; + + protected $excludeForAll = [ + 'last_activity' + ]; + + /** + * {@inheritDoc} + */ + protected function baseQuery() + { + $query = $this->classMapper->createInstance('user'); + + // Join user's most recent activity + return $query->joinLastActivity()->with('lastActivity'); + } + + /** + * Filter LIKE the last activity description. + * + * @param Builder $query + * @param mixed $value + * @return $this + */ + protected function filterLastActivity($query, $value) + { + // Split value on separator for OR queries + $values = explode($this->orSeparator, $value); + $query->where(function ($query) use ($values) { + foreach ($values as $value) { + $query->orLike('activities.description', $value); + } + }); + return $this; + } + + /** + * Filter LIKE the first name, last name, or email. + * + * @param Builder $query + * @param mixed $value + * @return $this + */ + protected function filterName($query, $value) + { + // Split value on separator for OR queries + $values = explode($this->orSeparator, $value); + $query->where(function ($query) use ($values) { + foreach ($values as $value) { + $query->orLike('first_name', $value) + ->orLike('last_name', $value) + ->orLike('email', $value); + } + }); + return $this; + } + + /** + * Filter by status (active, disabled, unactivated) + * + * @param Builder $query + * @param mixed $value + * @return $this + */ + protected function filterStatus($query, $value) + { + // Split value on separator for OR queries + $values = explode($this->orSeparator, $value); + $query->where(function ($query) use ($values) { + foreach ($values as $value) { + if ($value == 'disabled') { + $query->orWhere('flag_enabled', 0); + } elseif ($value == 'unactivated') { + $query->orWhere('flag_verified', 0); + } elseif ($value == 'active') { + $query->orWhere(function ($query) { + $query->where('flag_enabled', 1)->where('flag_verified', 1); + }); + } + } + }); + return $this; + } + + /** + * Return a list of possible user statuses. + * + * @return array + */ + protected function listStatus() + { + return [ + [ + 'value' => 'active', + 'text' => Translator::translate('ACTIVE') + ], + [ + 'value' => 'unactivated', + 'text' => Translator::translate('UNACTIVATED') + ], + [ + 'value' => 'disabled', + 'text' => Translator::translate('DISABLED') + ] + ]; + } + + /** + * Sort based on last activity time. + * + * @param Builder $query + * @param string $direction + * @return $this + */ + protected function sortLastActivity($query, $direction) + { + $query->orderBy('activities.occurred_at', $direction); + return $this; + } + + /** + * Sort based on last name. + * + * @param Builder $query + * @param string $direction + * @return $this + */ + protected function sortName($query, $direction) + { + $query->orderBy('last_name', $direction); + return $this; + } + + /** + * Sort active, unactivated, disabled + * + * @param Builder $query + * @param string $direction + * @return $this + */ + protected function sortStatus($query, $direction) + { + $query->orderBy('flag_enabled', $direction)->orderBy('flag_verified', $direction); + return $this; + } +} diff --git a/login/app/sprinkles/admin/templates/forms/group.html.twig b/login/app/sprinkles/admin/templates/forms/group.html.twig new file mode 100755 index 0000000..36d6632 --- /dev/null +++ b/login/app/sprinkles/admin/templates/forms/group.html.twig @@ -0,0 +1,69 @@ +<form class="js-form" method="{{form.method | default('POST')}}" action="{{site.uri.public}}/{{form.action}}"> + {% include "forms/csrf.html.twig" %} + <div class="js-form-alerts"> + </div> + <div class="row"> + {% block group_form %} + {% if 'name' not in form.fields.hidden %} + <div class="col-sm-12"> + <div class="form-group"> + <label>{{translate("GROUP.NAME")}}</label> + <div class="input-group"> + <span class="input-group-addon"><i class="fa fa-edit fa-fw"></i></span> + <input type="text" class="form-control" name="name" autocomplete="off" value="{{group.name}}" placeholder="{{translate("GROUP.NAME_EXPLAIN")}}" {% if 'name' in form.fields.disabled %}disabled{% endif %}> + </div> + </div> + </div> + {% endif %} + {% if 'slug' not in form.fields.hidden %} + <div class="col-sm-12"> + <div class="form-group"> + <label>{{translate("SLUG")}}</label> + <div class="input-group"> + <span class="input-group-addon"><i class="fa fa-tag fa-fw"></i></span> + <input type="text" class="form-control" name="slug" autocomplete="off" value="{{group.slug}}" placeholder="{{translate("SLUG")}}" {% if 'slug' in form.fields.disabled %}disabled{% endif %} readonly> + {% if 'slug' not in form.fields.disabled %} + <span class="input-group-btn" data-toggle="buttons"> + <label class="btn btn-primary"> + <input type="checkbox" id="form-group-slug-override" autocomplete="off"> {{translate("OVERRIDE")}} + </label> + </span> + {% endif %} + </div> + </div> + </div> + {% endif %} + {% if 'icon' not in fields.hidden %} + <div class="col-sm-12"> + <div class="form-group"> + <label>{{translate("GROUP.ICON")}}</label> + <div class="input-group"> + <span class="input-group-addon icon-preview"><i class="{{group.icon}} fa-fw"></i></span> + <input type="text" class="form-control" name="icon" autocomplete="off" value="{{group.icon}}" placeholder="{{translate("GROUP.ICON_EXPLAIN")}}" {% if 'icon' in form.fields.disabled %}disabled{% endif %}> + </div> + </div> + </div> + {% endif %} + {% if 'description' not in fields.hidden %} + <div class="col-sm-12"> + <div class="form-group"> + <label for="input_description">{{translate("DESCRIPTION")}}</label> + <textarea id="input_description" class="form-control" type="text" name="description" {% if 'description' in form.fields.disabled %}disabled{% endif %} rows=6>{{group.description}}</textarea> + </div> + </div> + {% endif %} + {% endblock %} + </div><br> + <div class="row"> + <div class="col-xs-6 col-sm-4"> + <button type="submit" class="btn btn-block btn-lg btn-success">{{form.submit_text}}</button> + </div> + <div class="col-xs-4 col-sm-3 pull-right"> + <button type="button" class="btn btn-block btn-lg btn-link" data-dismiss="modal">{{translate("CANCEL")}}</button> + </div> + </div> +</form> +<!-- Include validation rules --> +<script> +{% include "pages/partials/page.js.twig" %} +</script> diff --git a/login/app/sprinkles/admin/templates/forms/role.html.twig b/login/app/sprinkles/admin/templates/forms/role.html.twig new file mode 100755 index 0000000..46a4477 --- /dev/null +++ b/login/app/sprinkles/admin/templates/forms/role.html.twig @@ -0,0 +1,56 @@ +<form class="js-form" method="{{form.method | default('POST')}}" action="{{site.uri.public}}/{{form.action}}"> + {% include "forms/csrf.html.twig" %} + <div class="js-form-alerts"> + </div> + <div class="row"> + {% if 'name' not in form.fields.hidden %} + <div class="col-sm-12"> + <div class="form-group"> + <label>{{translate("ROLE.NAME")}}</label> + <div class="input-group"> + <span class="input-group-addon"><i class="fa fa-edit fa-fw"></i></span> + <input type="text" class="form-control" name="name" autocomplete="off" value="{{role.name}}" placeholder="{{translate("ROLE.NAME_EXPLAIN")}}" {% if 'name' in form.fields.disabled %}disabled{% endif %}> + </div> + </div> + </div> + {% endif %} + {% if 'slug' not in form.fields.hidden %} + <div class="col-sm-12"> + <div class="form-group"> + <label>{{translate("SLUG")}}</label> + <div class="input-group"> + <span class="input-group-addon"><i class="fa fa-tag fa-fw"></i></span> + <input type="text" class="form-control" name="slug" autocomplete="off" value="{{role.slug}}" placeholder="{{translate("SLUG")}}" {% if 'slug' in form.fields.disabled %}disabled{% endif %} readonly> + {% if 'slug' not in form.fields.disabled %} + <span class="input-group-btn" data-toggle="buttons"> + <label class="btn btn-primary"> + <input type="checkbox" id="form-role-slug-override" autocomplete="off"> {{translate("OVERRIDE")}} + </label> + </span> + {% endif %} + </div> + </div> + </div> + {% endif %} + {% if 'description' not in fields.hidden %} + <div class="col-sm-12"> + <div class="form-group"> + <label for="input_description">{{translate("DESCRIPTION")}}</label> + <textarea id="input_description" class="form-control" type="text" name="description" {% if 'description' in form.fields.disabled %}disabled{% endif %} rows=6>{{role.description}}</textarea> + </div> + </div> + {% endif %} + </div><br> + <div class="row"> + <div class="col-xs-8 col-sm-4"> + <button type="submit" class="btn btn-block btn-lg btn-success">{{form.submit_text}}</button> + </div> + <div class="col-xs-4 col-sm-4 pull-right"> + <button type="button" class="btn btn-block btn-lg btn-link" data-dismiss="modal">{{translate("CANCEL")}}</button> + </div> + </div> +</form> +<!-- Include validation rules --> +<script> +{% include "pages/partials/page.js.twig" %} +</script> diff --git a/login/app/sprinkles/admin/templates/forms/user.html.twig b/login/app/sprinkles/admin/templates/forms/user.html.twig new file mode 100755 index 0000000..3ee7fc9 --- /dev/null +++ b/login/app/sprinkles/admin/templates/forms/user.html.twig @@ -0,0 +1,125 @@ +<form class="js-form" method="{{form.method | default('POST')}}" action="{{site.uri.public}}/{{form.action}}"> + {% include "forms/csrf.html.twig" %} + <div class="js-form-alerts"> + </div> + <div class="row"> + {% block user_form %} + {% if 'user_name' not in form.fields.hidden %} + <div class="col-sm-6"> + <div class="form-group"> + <label>{{translate('USERNAME')}}</label> + <div class="input-group"> + <span class="input-group-addon"><i class="fa fa-edit fa-fw"></i></span> + <input type="text" class="form-control" name="user_name" autocomplete="off" value="{{user.user_name}}" placeholder="{{translate('USERNAME')}}" {% if 'user_name' in form.fields.disabled %}disabled{% endif %}> + </div> + </div> + </div> + {% endif %} + {% if 'group' not in form.fields.hidden %} + <div class="col-sm-6"> + <div class="form-group"> + <label for="input-group">{{translate('GROUP')}}</label> + <div class="input-group"> + <span class="input-group-addon"><i class="fa fa-users fa-fw"></i></span> + {% if 'group' in form.fields.disabled %} + <input type="text" class="form-control" name="theme" value="{{user.group.name}}" disabled> + {% else %} + <select id="input-group" class="form-control js-select2" name="group_id"> + {% for group in groups %} + <option value="{{group.id}}" {% if (group.id == user.group_id) %}selected{% endif %}>{{group.name}}</option> + {% endfor %} + </select> + {% endif %} + </div> + </div> + </div> + {% endif %} + {% if 'name' not in form.fields.hidden %} + <div class="col-sm-6"> + <div class="form-group"> + <label>{{translate('FIRST_NAME')}}</label> + <div class="input-group"> + <span class="input-group-addon"><i class="fa fa-edit fa-fw"></i></span> + <input type="text" class="form-control" name="first_name" autocomplete="off" value="{{user.first_name}}" placeholder="{{translate('FIRST_NAME')}}" {% if 'name' in form.fields.disabled %}disabled{% endif %}> + </div> + </div> + </div> + <div class="col-sm-6"> + <div class="form-group"> + <label>{{translate('LAST_NAME')}}</label> + <div class="input-group"> + <span class="input-group-addon"><i class="fa fa-edit fa-fw"></i></span> + <input type="text" class="form-control" name="last_name" autocomplete="off" value="{{user.last_name}}" placeholder="{{translate('LAST_NAME')}}" {% if 'name' in form.fields.disabled %}disabled{% endif %}> + </div> + </div> + </div> + {% endif %} + {% if 'email' not in form.fields.hidden %} + <div class="col-sm-6"> + <div class="form-group"> + <label>{{translate('EMAIL')}}</label> + <div class="input-group js-copy-container"> + <span class="input-group-addon"><i class="fa fa-envelope fa-fw"></i></span> + <input type="text" class="form-control js-copy-target" name="email" autocomplete="off" value="{{user.email}}" placeholder="{{translate('EMAIL')}}" {% if 'email' in form.fields.disabled %}disabled{% endif %}> + {% if 'email' in form.fields.disabled %} + <span class="input-group-btn"> + <button class="btn btn-default uf-copy-trigger js-copy-trigger" type="button"><i class="fa fa-clipboard"></i></button> + </span> + {% endif %} + </div> + </div> + </div> + {% endif %} + {% if 'theme' not in form.fields.hidden %} + <div class="col-sm-6"> + <div class="form-group"> + <label for="input-theme">{{translate('THEME')}}</label> + <div class="input-group"> + <span class="input-group-addon"><i class="fa fa-puzzle-piece fa-fw"></i></span> + {% if 'theme' in form.fields.disabled %} + <input type="text" class="form-control" name="theme" value="{{themes[user.theme]}}" disabled> + {% else %} + <select id="input-theme" class="form-control js-select2" name="theme"> + {% for option, label in theme %} + <option value="{{option}}" {% if (option == user.theme) %}selected{% endif %}>{{label}}</option> + {% endfor %} + </select> + {% endif %} + </div> + </div> + </div> + {% endif %} + {% if 'locale' not in form.fields.hidden %} + <div class="col-sm-6"> + <div class="form-group"> + <label for="input-locale">{{translate('LOCALE')}}</label> + <div class="input-group"> + <span class="input-group-addon"><i class="fa fa-language fa-fw"></i></span> + {% if 'locale' in form.fields.disabled %} + <input type="text" class="form-control" name="theme" value="{{locales[user.locale]}}" disabled> + {% else %} + <select id="input-locale" class="form-control js-select2" name="locale"> + {% for option, label in locales %} + <option value="{{option}}" {% if (option == user.locale) %}selected{% endif %}>{{label}}</option> + {% endfor %} + </select> + {% endif %} + </div> + </div> + </div> + {% endif %} + {% endblock %} + </div><br> + <div class="row"> + <div class="col-xs-8 col-sm-4"> + <button type="submit" class="btn btn-block btn-lg btn-success">{{form.submit_text}}</button> + </div> + <div class="col-xs-4 col-sm-3 pull-right"> + <button type="button" class="btn btn-block btn-lg btn-link" data-dismiss="modal">{{translate('CANCEL')}}</button> + </div> + </div> +</form> +<!-- Include validation rules --> +<script> +{% include "pages/partials/page.js.twig" %} +</script> diff --git a/login/app/sprinkles/admin/templates/mail/password-create.html.twig b/login/app/sprinkles/admin/templates/mail/password-create.html.twig new file mode 100755 index 0000000..854eb77 --- /dev/null +++ b/login/app/sprinkles/admin/templates/mail/password-create.html.twig @@ -0,0 +1,19 @@ +{% block subject %} + {{site.title}} - please set a password for your new account +{% endblock %} + +{% block body %} +<p> + Dear {{user.first_name}}, +</p> +<p> + Someone has created an account for you with {{site.title}} ({{site.uri.public}}). Your username is <b>{{user.user_name}}</b>. +</p> +<p> + To access your account, you must first create a password by visiting: <a href="{{site.uri.public}}/account/set-password/confirm?token={{token}}">{{site.uri.public}}/account/set-password/confirm?token={{token}}</a>. This link has been generated especially for you, and will expire in {{create_password_expiration}}. Do not share it with anyone! +</p> +<p> + With regards,<br> + The {{site.title}} Team +</p> +{% endblock %}
\ No newline at end of file diff --git a/login/app/sprinkles/admin/templates/modals/confirm-clear-cache.html.twig b/login/app/sprinkles/admin/templates/modals/confirm-clear-cache.html.twig new file mode 100755 index 0000000..e5457d3 --- /dev/null +++ b/login/app/sprinkles/admin/templates/modals/confirm-clear-cache.html.twig @@ -0,0 +1,17 @@ +{% extends "modals/modal.html.twig" %} + +{% block modal_title %}{{translate("CACHE.CLEAR")}}{% endblock %} + +{% block modal_body %} +<form class="js-form" method="post" action="{{site.uri.public}}/{{form.action}}"> + {% include "forms/csrf.html.twig" %} + <div class="js-form-alerts"> + </div> + <h4>{{translate("CACHE.CLEAR_CONFIRM")}}<br><small>{{translate("DELETE_CANNOT_UNDONE")}}</small></h4> + <br> + <div class="btn-group-action"> + <button type="submit" class="btn btn-danger btn-lg btn-block">{{translate("CACHE.CLEAR_CONFIRM_YES")}}</button> + <button type="button" class="btn btn-default btn-lg btn-block" data-dismiss="modal">{{translate("CANCEL")}}</button> + </div> +</form> +{% endblock %} diff --git a/login/app/sprinkles/admin/templates/modals/confirm-delete-group.html.twig b/login/app/sprinkles/admin/templates/modals/confirm-delete-group.html.twig new file mode 100755 index 0000000..7889a1e --- /dev/null +++ b/login/app/sprinkles/admin/templates/modals/confirm-delete-group.html.twig @@ -0,0 +1,17 @@ +{% extends "modals/modal.html.twig" %} + +{% block modal_title %}{{translate("GROUP.DELETE")}}{% endblock %} + +{% block modal_body %} +<form class="js-form" method="delete" action="{{site.uri.public}}/{{form.action}}"> + {% include "forms/csrf.html.twig" %} + <div class="js-form-alerts"> + </div> + <h4>{{translate("GROUP.DELETE_CONFIRM", {name: group.name})}}<br><small>{{translate("DELETE_CANNOT_UNDONE")}}</small></h4> + <br> + <div class="btn-group-action"> + <button type="submit" class="btn btn-danger btn-lg btn-block">{{translate("GROUP.DELETE_YES")}}</button> + <button type="button" class="btn btn-default btn-lg btn-block" data-dismiss="modal">{{translate("CANCEL")}}</button> + </div> +</form> +{% endblock %} diff --git a/login/app/sprinkles/admin/templates/modals/confirm-delete-role.html.twig b/login/app/sprinkles/admin/templates/modals/confirm-delete-role.html.twig new file mode 100755 index 0000000..618039b --- /dev/null +++ b/login/app/sprinkles/admin/templates/modals/confirm-delete-role.html.twig @@ -0,0 +1,17 @@ +{% extends "modals/modal.html.twig" %} + +{% block modal_title %}{{translate("ROLE.DELETE")}}{% endblock %} + +{% block modal_body %} +<form class="js-form" method="delete" action="{{site.uri.public}}/{{form.action}}"> + {% include "forms/csrf.html.twig" %} + <div class="js-form-alerts"> + </div> + <h4>{{translate("ROLE.DELETE_CONFIRM", {name: role.name})}}<br><small>{{translate("DELETE_CANNOT_UNDONE")}}</small></h4> + <br> + <div class="btn-group-action"> + <button type="submit" class="btn btn-danger btn-lg btn-block">{{translate("ROLE.DELETE_YES")}}</button> + <button type="button" class="btn btn-default btn-lg btn-block" data-dismiss="modal">{{translate("CANCEL")}}</button> + </div> +</form> +{% endblock %} diff --git a/login/app/sprinkles/admin/templates/modals/confirm-delete-user.html.twig b/login/app/sprinkles/admin/templates/modals/confirm-delete-user.html.twig new file mode 100755 index 0000000..ce86301 --- /dev/null +++ b/login/app/sprinkles/admin/templates/modals/confirm-delete-user.html.twig @@ -0,0 +1,17 @@ +{% extends "modals/modal.html.twig" %} + +{% block modal_title %}{{translate("USER.DELETE")}}{% endblock %} + +{% block modal_body %} +<form class="js-form" method="delete" action="{{site.uri.public}}/{{form.action}}"> + {% include "forms/csrf.html.twig" %} + <div class="js-form-alerts"> + </div> + <h4>{{translate("USER.DELETE_CONFIRM", {name: user.user_name})}}<br><small>{{translate("DELETE_CANNOT_UNDONE")}}</small></h4> + <br> + <div class="btn-group-action"> + <button type="submit" class="btn btn-danger btn-lg btn-block">{{translate("USER.DELETE_YES")}}</button> + <button type="button" class="btn btn-default btn-lg btn-block" data-dismiss="modal">{{translate("CANCEL")}}</button> + </div> +</form> +{% endblock %} diff --git a/login/app/sprinkles/admin/templates/modals/group.html.twig b/login/app/sprinkles/admin/templates/modals/group.html.twig new file mode 100755 index 0000000..be2d98c --- /dev/null +++ b/login/app/sprinkles/admin/templates/modals/group.html.twig @@ -0,0 +1,7 @@ +{% extends "modals/modal.html.twig" %} + +{% block modal_title %}{{translate('GROUP')}}{% endblock %} + +{% block modal_body %} + {% include "forms/group.html.twig" %} +{% endblock %} diff --git a/login/app/sprinkles/admin/templates/modals/role-manage-permissions.html.twig b/login/app/sprinkles/admin/templates/modals/role-manage-permissions.html.twig new file mode 100755 index 0000000..3914d2e --- /dev/null +++ b/login/app/sprinkles/admin/templates/modals/role-manage-permissions.html.twig @@ -0,0 +1,94 @@ +{% extends "modals/modal.html.twig" %} + +{% block modal_title %}{{translate("PERMISSION.MANAGE")}}{% endblock %} + +{% block modal_size %}modal-lg{% endblock %} + +{% block modal_body %} +<form class="js-form" method="PUT" action="{{site.uri.public}}/api/roles/r/{{role.slug}}/permissions"> + {% include "forms/csrf.html.twig" %} + <div class="js-form-alerts"> + </div> + <div class="js-form-permissions"> + <table class="table table-striped"> + <thead> + <tr> + <th>{{translate("NAME")}}</th> + <th>{{translate("DESCRIPTION")}}</th> + <th>{{translate("PERMISSION.HOOK_CONDITION")}}</th> + <th>{{translate("REMOVE")}}</th> + </tr> + </thead> + <tbody> + </tbody> + </table> + <div class="padding-bottom"> + <label>{{translate("PERMISSION.ASSIGN_NEW")}}:</label> + <select class="form-control js-select-new" type="text"> + <option></option> + </select> + </div> + </div> + <br> + <div class="row"> + <div class="col-xs-12 col-sm-6"> + <button type="submit" class="btn btn-block btn-lg btn-success">{{translate("PERMISSION.UPDATE")}}</button> + </div> + <div class="col-xs-12 col-sm-3 pull-right"> + <button type="button" class="btn btn-block btn-lg btn-link" data-dismiss="modal">{{translate('CANCEL')}}</button> + </div> + </div> +</form> + +{# This contains a series of <script> blocks, each of which is a client-side Handlebars template. + # Note that these are NOT Twig templates, although the syntax is similar. We wrap them in the `verbatim` tag, + # so that Twig will output them directly into the DOM instead of trying to treat them like Twig templates. + # + # These templates require handlebars-helpers.js, moment.js +#} +{% verbatim %} +<script id="role-permissions-select-option" type="text/x-handlebars-template"> + <div> + <strong> + {{name}} + </strong> + <br> + {{description}} + <div> + <code>{{this.slug}}</code> + </div> + <div> + ↳ <code>{{conditions}}</code> + </div> + </div> +</script> + +<script id="role-permissions-row" type="text/x-handlebars-template"> + <tr class="uf-collection-row"> + <td> + {{name}} + <input type="hidden" name="value[{{ rownum }}][permission_id]" value="{{id}}"> + </td> + <td> + {{description}} + </td> + <td class="uf-collection-col-wrap"> + <div> + <code>{{this.slug}}</code> + </div> + <div> + ↳ <code>{{conditions}}</code> + </div> + </td> + <td> + <button type="button" class="btn btn-link btn-trash js-delete-row pull-right" title="Delete"> <i class="fa fa-trash"></i> </button> + </td> + </tr> +</script> +{% endverbatim %} + +<!-- Include validation rules --> +<script> + {% include "pages/partials/page.js.twig" %} +</script> +{% endblock %} diff --git a/login/app/sprinkles/admin/templates/modals/role.html.twig b/login/app/sprinkles/admin/templates/modals/role.html.twig new file mode 100755 index 0000000..6346461 --- /dev/null +++ b/login/app/sprinkles/admin/templates/modals/role.html.twig @@ -0,0 +1,7 @@ +{% extends "modals/modal.html.twig" %} + +{% block modal_title %}{{translate('ROLE')}}{% endblock %} + +{% block modal_body %} + {% include "forms/role.html.twig" %} +{% endblock %} diff --git a/login/app/sprinkles/admin/templates/modals/user-manage-roles.html.twig b/login/app/sprinkles/admin/templates/modals/user-manage-roles.html.twig new file mode 100755 index 0000000..b41c60b --- /dev/null +++ b/login/app/sprinkles/admin/templates/modals/user-manage-roles.html.twig @@ -0,0 +1,77 @@ +{% extends "modals/modal.html.twig" %} + +{% block modal_title %}{{translate("ROLE.MANAGE")}}{% endblock %} + +{% block modal_body %} +<form class="js-form" method="PUT" action="{{site.uri.public}}/api/users/u/{{user.user_name}}/roles"> + {% include "forms/csrf.html.twig" %} + <div class="js-form-alerts"> + </div> + <div class="js-form-roles"> + <table class="table table-striped"> + <thead> + <tr> + <th>{{translate("NAME")}}</th> + <th>{{translate("DESCRIPTION")}}</th> + <th>{{translate("REMOVE")}}</th> + </tr> + </thead> + <tbody> + </tbody> + </table> + <div class="padding-bottom"> + <label>{{translate("ROLE.ASSIGN_NEW")}}:</label> + <select class="form-control js-select-new" type="text"> + <option></option> + </select> + </div> + </div> + <br> + <div class="row"> + <div class="col-xs-8 col-sm-4"> + <button type="submit" class="btn btn-block btn-lg btn-success">{{translate("UPDATE")}}</button> + </div> + <div class="col-xs-4 col-sm-3 pull-right"> + <button type="button" class="btn btn-block btn-lg btn-link" data-dismiss="modal">{{translate('CANCEL')}}</button> + </div> + </div> +</form> + +{# This contains a series of <script> blocks, each of which is a client-side Handlebars template. + # Note that these are NOT Twig templates, although the syntax is similar. We wrap them in the `verbatim` tag, + # so that Twig will output them directly into the DOM instead of trying to treat them like Twig templates. + # + # These templates require handlebars-helpers.js, moment.js +#} +{% verbatim %} +<script id="user-roles-select-option" type="text/x-handlebars-template"> + <div> + <strong> + {{name}} + </strong> + <br> + {{description}} + </div> +</script> + +<script id="user-roles-row" type="text/x-handlebars-template"> + <tr class="uf-collection-row"> + <td> + {{name}} + <input type="hidden" name="value[{{ rownum }}][role_id]" value="{{id}}"> + </td> + <td> + {{description}} + </td> + <td> + <button type="button" class="btn btn-link btn-trash js-delete-row pull-right" title="Delete"> <i class="fa fa-trash"></i> </button> + </td> + </tr> +</script> +{% endverbatim %} + +<!-- Include validation rules --> +<script> + {% include "pages/partials/page.js.twig" %} +</script> +{% endblock %} diff --git a/login/app/sprinkles/admin/templates/modals/user-set-password.html.twig b/login/app/sprinkles/admin/templates/modals/user-set-password.html.twig new file mode 100755 index 0000000..922d4e2 --- /dev/null +++ b/login/app/sprinkles/admin/templates/modals/user-set-password.html.twig @@ -0,0 +1,62 @@ +{% extends "modals/modal.html.twig" %} + +{% block modal_title %}{{translate("USER.ADMIN.CHANGE_PASSWORD")}}{% endblock %} + +{% block modal_body %} +<form class="js-form" method="PUT" action="{{site.uri.public}}/api/users/u/{{user.user_name}}"> + {% include "forms/csrf.html.twig" %} + <!-- Prevent browsers from trying to autofill the password field. See http://stackoverflow.com/a/23234498/2970321 --> + <input type="text" style="display:none"> + <input type="password" style="display:none"> + <div class="js-form-alerts"> + </div> + <div class="row"> + <div class="col-sm-12"> + <div class="radio"> + <label for="change_password_mode_link"> + <input type="radio" name="change_password_mode" id="change_password_mode_link" value="link" checked> + {{translate("USER.ADMIN.SEND_PASSWORD_LINK")}} + </label> + </div> + </div> + <div class="col-sm-12"> + <div class="radio"> + <label for="change_password_mode_manual"> + <input type="radio" name="change_password_mode" id="change_password_mode_manual" value="manual"> + {{translate("USER.ADMIN.SET_PASSWORD")}}: + </label> + </div> + <div class="row controls-password"> + <div class="col-sm-11 col-sm-offset-1"> + <div class="form-group"> + <label>{{translate('PASSWORD')}}</label> + <div class="input-group"> + <span class="input-group-addon"><i class="fa fa-key"></i></span> + <input type="password" class="form-control" name="value" autocomplete="off" value="" placeholder="{{translate("PASSWORD.BETWEEN", {min: 12, max: 50})}}"> + </div> + </div> + <div class="form-group"> + <label>{{translate('PASSWORD.CONFIRM')}}</label> + <div class="input-group"> + <span class="input-group-addon"><i class="fa fa-key"></i></span> + <input type="password" class="form-control" name="passwordc" autocomplete="off" value="" placeholder="{{translate('PASSWORD.CONFIRM')}}"> + </div> + </div> + </div> + </div> + </div> + </div><br> + <div class="row"> + <div class="col-xs-8 col-sm-4"> + <button type="submit" class="btn btn-block btn-lg btn-success">{{translate('SUBMIT')}}</button> + </div> + <div class="col-xs-4 col-sm-3 pull-right"> + <button type="button" class="btn btn-block btn-lg btn-link" data-dismiss="modal">{{translate('CANCEL')}}</button> + </div> + </div> +</form> +<!-- Include validation rules --> +<script> + {% include "pages/partials/page.js.twig" %} +</script> +{% endblock %} diff --git a/login/app/sprinkles/admin/templates/modals/user.html.twig b/login/app/sprinkles/admin/templates/modals/user.html.twig new file mode 100755 index 0000000..892fe4f --- /dev/null +++ b/login/app/sprinkles/admin/templates/modals/user.html.twig @@ -0,0 +1,7 @@ +{% extends "modals/modal.html.twig" %} + +{% block modal_title %}{{translate('USER')}}{% endblock %} + +{% block modal_body %} + {% include "forms/user.html.twig" %} +{% endblock %} diff --git a/login/app/sprinkles/admin/templates/navigation/navbar.html.twig b/login/app/sprinkles/admin/templates/navigation/navbar.html.twig new file mode 100755 index 0000000..b2cf699 --- /dev/null +++ b/login/app/sprinkles/admin/templates/navigation/navbar.html.twig @@ -0,0 +1,15 @@ +{% block dashboard_navbar %} + <div class="navbar-custom-menu"> + <ul class="nav navbar-nav"> + {% if current_user.isMaster() %} + <li class="hidden-xs hidden-sm"><a href="#">{{ translate("HEADER_MESSAGE_ROOT") | upper }}</a></li> + {% endif %} + + {# additional elements (e.g., menus, messages) before the user account drop down #} + {% block dashboard_navbar_extra %} {% endblock %} + + {# User Account: style can be found in dropdown.less #} + {% include "navigation/user-card.html.twig" %} + </ul> + </div> +{% endblock %} diff --git a/login/app/sprinkles/admin/templates/navigation/sidebar-menu.html.twig b/login/app/sprinkles/admin/templates/navigation/sidebar-menu.html.twig new file mode 100755 index 0000000..e6d4a7c --- /dev/null +++ b/login/app/sprinkles/admin/templates/navigation/sidebar-menu.html.twig @@ -0,0 +1,38 @@ +{% block navigation %} + {% if checkAccess('uri_dashboard') %} + <li> + <a href="{{site.uri.public}}/dashboard"><i class="fa fa-dashboard fa-fw"></i> <span>{{ translate("DASHBOARD") }}</span></a> + </li> + {% endif %} + {% if checkAccess('uri_users') %} + <li> + <a href="{{site.uri.public}}/users"><i class="fa fa-user fa-fw"></i> <span>{{ translate("USER", 2) }}</span></a> + </li> + {% elseif checkAccess('uri_group', { + 'group': current_user.group + }) %} + <li> + <a href="{{site.uri.public}}/groups/g/{{current_user.group.slug}}"><i class="{{current_user.group.icon}} fa-fw"></i> <span>{{ translate("GROUP.MANAGE") }}</span></a> + </li> + {% endif %} + {% if checkAccess('uri_activities') %} + <li> + <a href="{{site.uri.public}}/activities"><i class="fa fa-tasks fa-fw"></i> <span>{{ translate("ACTIVITY", 2) }}</span></a> + </li> + {% endif %} + {% if checkAccess('uri_roles') %} + <li> + <a href="{{site.uri.public}}/roles"><i class="fa fa-drivers-license fa-fw"></i> <span>{{ translate("ROLE", 2) }}</span></a> + </li> + {% endif %} + {% if checkAccess('uri_permissions') %} + <li> + <a href="{{site.uri.public}}/permissions"><i class="fa fa-key fa-fw"></i> <span>{{ translate("PERMISSION", 2) }}</span></a> + </li> + {% endif %} + {% if checkAccess('uri_groups') %} + <li> + <a href="{{site.uri.public}}/groups"><i class="fa fa-users fa-fw"></i> <span>{{ translate("GROUP", 2) }}</span></a> + </li> + {% endif %} +{% endblock %}
\ No newline at end of file diff --git a/login/app/sprinkles/admin/templates/navigation/sidebar-user.html.twig b/login/app/sprinkles/admin/templates/navigation/sidebar-user.html.twig new file mode 100755 index 0000000..018e644 --- /dev/null +++ b/login/app/sprinkles/admin/templates/navigation/sidebar-user.html.twig @@ -0,0 +1,10 @@ +<!-- Sidebar user panel --> +<div class="user-panel"> + <div class="pull-left image"> + <img src="{{ current_user.avatar }}" class="img-circle" alt="User Image"> + </div> + <div class="pull-left info"> + <p>{{current_user.first_name}} {{current_user.last_name}}</p> + <i class="{{current_user.group.icon}}"></i> {{current_user.group.name}} + </div> +</div>
\ No newline at end of file diff --git a/login/app/sprinkles/admin/templates/navigation/sidebar.html.twig b/login/app/sprinkles/admin/templates/navigation/sidebar.html.twig new file mode 100755 index 0000000..1b2939e --- /dev/null +++ b/login/app/sprinkles/admin/templates/navigation/sidebar.html.twig @@ -0,0 +1,10 @@ +{% block sidebar_user %} + {% include 'navigation/sidebar-user.html.twig' %} +{% endblock %} + +{% block sidebar_menu %} +<ul class="sidebar-menu"> + <li class="header" style="text-transform: uppercase">{{translate('NAVIGATION')}}</li> + {% include 'navigation/sidebar-menu.html.twig' %} +</ul> +{% endblock %}
\ No newline at end of file diff --git a/login/app/sprinkles/admin/templates/navigation/user-card.html.twig b/login/app/sprinkles/admin/templates/navigation/user-card.html.twig new file mode 100755 index 0000000..36fdb4b --- /dev/null +++ b/login/app/sprinkles/admin/templates/navigation/user-card.html.twig @@ -0,0 +1,8 @@ +{% extends "@account/navigation/user-card.html.twig" %} + +{% block userCard_menu %} + {% if checkAccess('uri_dashboard') %} + <a href="{{site.uri.public}}/dashboard" class="btn btn-default btn-flat btn-block">{{translate("DASHBOARD")}}</a> + {% endif %} + {{ parent() }} +{% endblock %}
\ No newline at end of file diff --git a/login/app/sprinkles/admin/templates/pages/abstract/dashboard.html.twig b/login/app/sprinkles/admin/templates/pages/abstract/dashboard.html.twig new file mode 100755 index 0000000..2a53de4 --- /dev/null +++ b/login/app/sprinkles/admin/templates/pages/abstract/dashboard.html.twig @@ -0,0 +1,87 @@ +{% extends "pages/abstract/base.html.twig" %} + +{% block stylesheets_page_group %} + <!-- Page-group-specific CSS asset bundle --> + {{ assets.css('css/admin') | raw }} +{% endblock %} + +{% block body_attributes %} + {% if current_user.isMaster() %} + class="hold-transition skin-red sidebar-mini" + {% else %} + class="hold-transition skin-{{site.AdminLTE.skin}} sidebar-mini" + {% endif %} +{% endblock %} + +{% block content %} + {# This needs to be here (early in the body) to make sure the animation doesn't fire #} + <script> + (function () { + if (Boolean(sessionStorage.getItem('sidebar-toggle-collapsed'))) { + var body = document.getElementsByTagName('body')[0]; + body.className = body.className + ' sidebar-collapse'; + } + })(); + </script> + + <div class="wrapper"> + + <header class="main-header"> + <!-- Logo --> + {% block navbar_logo %} + <a href="{{site.uri.public}}" class="logo"> + <i class="fa fa-home"></i> + {{site.title}} + </a> + {% endblock %} + <!-- Header Navbar: style can be found in header.less --> + <nav class="navbar navbar-static-top"> + <!-- Sidebar toggle button--> + <a href="#" class="sidebar-toggle" data-toggle="offcanvas" role="button"> + <span class="sr-only">Toggle navigation</span> + </a> + <!-- Main nav buttons --> + {% include "navigation/navbar.html.twig" %} + </nav> + </header> + <!-- Left side column. contains the logo and sidebar --> + <aside class="main-sidebar"> + <!-- sidebar: style can be found in sidebar.less --> + <section class="sidebar"> + {% include 'navigation/sidebar.html.twig' %} + </section> + <!-- /.sidebar --> + </aside> + + <!-- Content Wrapper. Contains page content --> + <div class="content-wrapper"> + <!-- Content Header (Page header) --> + {% block content_header %} + <section class="content-header"> + <h1>{% block header_title %}{{ block('page_title') }}{% endblock %}</h1> + {% if block('page_description') is not empty %}<h1><small>{% block header_description %}{{ block('page_description') }}{% endblock %}</small></h1>{% endif %} + {% block breadcrumb %} + {% include 'navigation/breadcrumb.html.twig' with {page_title: block('page_title')} %} + {% endblock %} + <div id="alerts-page"></div> + </section> + {% endblock %} + <section class="content"> + {% block body_matter %}{% endblock %} + </section> + </div> + <!-- /.content-wrapper --> + + <!-- Footer --> + {% block footer %} + {% include "pages/partials/footer.html.twig" %} + {% endblock %} + + </div> + <!-- ./wrapper --> + +{% endblock %} + +{% block scripts_page_group %} + {{ assets.js('js/admin') | raw }} +{% endblock %}
\ No newline at end of file diff --git a/login/app/sprinkles/admin/templates/pages/activities.html.twig b/login/app/sprinkles/admin/templates/pages/activities.html.twig new file mode 100755 index 0000000..bcbd9c6 --- /dev/null +++ b/login/app/sprinkles/admin/templates/pages/activities.html.twig @@ -0,0 +1,46 @@ +{% extends "pages/abstract/dashboard.html.twig" %} + +{% block stylesheets_page %} + <!-- Page-specific CSS asset bundle --> + {{ assets.css('css/form-widgets') | raw }} +{% endblock %} + +{# Overrides blocks in head of base template #} +{% block page_title %}{{ translate("ACTIVITY", 2) }}{% endblock %} + +{% block page_description %}{{ translate("ACTIVITY.PAGE") }}.{% endblock %} + +{% block body_matter %} + <div class="row"> + <div class="col-md-12"> + <div id="widget-activities" class="box box-primary"> + <div class="box-header"> + <h3 class="box-title"><i class="fa fa-tasks fa-fw"></i> {{translate('ACTIVITY', 2)}}</h3> + {% include "tables/table-tool-menu.html.twig" %} + </div> + <div class="box-body"> + {% include "tables/activities.html.twig" with { + "table" : { + "id" : "table-activities", + "columns" : ["user"] + } + } + %} + </div> + </div> + </div> + </div> +{% endblock %} +{% block scripts_page %} + <!-- Include validation rules --> + <script> + {% include "pages/partials/page.js.twig" %} + </script> + + <!-- Include form widgets JS --> + {{ assets.js('js/form-widgets') | raw }} + + <!-- Include page-specific JS --> + {{ assets.js('js/pages/activities') | raw }} + +{% endblock %} diff --git a/login/app/sprinkles/admin/templates/pages/dashboard.html.twig b/login/app/sprinkles/admin/templates/pages/dashboard.html.twig new file mode 100755 index 0000000..f9c85a3 --- /dev/null +++ b/login/app/sprinkles/admin/templates/pages/dashboard.html.twig @@ -0,0 +1,282 @@ +{% extends "pages/abstract/dashboard.html.twig" %} + +{# Overrides blocks in head of base template #} +{% block page_title %}{{ translate("DASHBOARD") }}{% endblock %} +{% block page_description %}{% endblock %} + +{% block body_matter %} + <!-- Info boxes --> + {% if checkAccess('uri_users') %} + <div class="row"> + <div class="col-md-4 col-sm-6 col-xs-12"> + <a href="{{site.uri.public}}/users"> + <div class="info-box"> + <span class="info-box-icon bg-aqua"><i class="fa fa-user fa-fw"></i></span> + <div class="info-box-content"> + <span class="info-box-text">{{ translate("USER", 2) }}</span> + <span class="info-box-number">{{counter.users}}</span> + </div> + <!-- /.info-box-content --> + </div> + <!-- /.info-box --> + </a> + </div> + <!-- /.col --> + + <div class="col-md-4 col-sm-6 col-xs-12"> + <a href="{{site.uri.public}}/roles"> + <div class="info-box"> + <span class="info-box-icon bg-red"><i class="fa fa-drivers-license"></i></span> + <div class="info-box-content"> + <span class="info-box-text">{{ translate("ROLE", 2) }}</span> + <span class="info-box-number">{{counter.roles}}</span> + </div> + <!-- /.info-box-content --> + </div> + <!-- /.info-box --> + </a> + </div> + <!-- /.col --> + + <div class="col-md-4 col-sm-6 col-xs-12"> + <a href="{{site.uri.public}}/groups"> + <div class="info-box"> + <span class="info-box-icon bg-green"><i class="fa fa-users"></i></span> + <div class="info-box-content"> + <span class="info-box-text">{{ translate("GROUP", 2) }}</span> + <span class="info-box-number">{{counter.groups}}</span> + </div> + <!-- /.info-box-content --> + </div> + <!-- /.info-box --> + </a> + </div> + <!-- /.col --> + </div> + <!-- /.row --> + + {% elseif checkAccess('uri_group', { + 'group': current_user.group + }) %} + <div class="row"> + <div class="col-sm-6 col-xs-12"> + <div class="info-box"> + <span class="info-box-icon bg-aqua"><i class="{{current_user.group.icon}}"></i></span> + <div class="info-box-content"> + <h1>{{current_user.group.name}}</h1> + </div> + <!-- /.info-box-content --> + </div> + <!-- /.info-box --> + </div> + <!-- /.col --> + <div class="col-sm-6 col-xs-12"> + <div class="info-box"> + <span class="info-box-icon bg-aqua"><i class="fa fa-user fa-fw"></i></span> + <div class="info-box-content"> + <span class="info-box-text">{{ translate("USER", 2) }}</span> + <span class="info-box-number">{{current_user.group.users.count}}</span> + </div> + <!-- /.info-box-content --> + </div> + <!-- /.info-box --> + </div> + <!-- /.col --> + </div> + <!-- /.row --> + {% endif %} + + <!-- Main panels --> + <div class="row"> + {% if checkAccess('uri_users') or checkAccess('view_system_info') %} + <div class="col-md-6 col-sm-12 col-xs-12"> + {% if checkAccess('uri_users') %} + <div class="row"> + <div class="col-sm-12"> + <!-- USERS LIST --> + <div class="box box-info"> + <div class="box-header with-border"> + <h3 class="box-title">{{translate("USER.LATEST")}}</h3> + </div> + <!-- /.box-header --> + <div class="box-body no-padding"> + <ul class="users-list clearfix"> + {% for user in users %} + <li> + <img src="{{ user.avatar }}" alt="User Image"> + <a class="users-list-name" href="{{site.uri.public}}/users/u/{{user.user_name}}">{{user.first_name}} {{user.last_name}}</a> + <span class="users-list-date">{{ user.registered }}</span> + </li> + {% endfor %} + </ul> + <!-- /.users-list --> + </div> + <!-- /.box-body --> + <div class="box-footer text-center"> + <a href="{{site.uri.public}}/users" class="uppercase">{{translate("USER.VIEW_ALL")}}</a> + </div> + <!-- /.box-footer --> + </div> + <!--/.box --> + </div> + <!-- /.col --> + </div> + <!-- /.row --> + {% endif %} + + {% if checkAccess('view_system_info') %} + <div class="row"> + <div class="col-sm-12"> + <div class="box box-primary"> + <div class="box-header with-border"> + <h3 class="box-title">{{translate("SYSTEM_INFO")}}</h3> + </div> + <!-- /.box-header --> + <div class="box-body"> + <dl class="dl-horizontal"> + <dt>{{translate("SYSTEM_INFO.UF_VERSION")}}</dt> + <dd>{{info.version.UF}}</dd> + + <dt>{{translate("SYSTEM_INFO.PHP_VERSION")}}</dt> + <dd>{{info.version.php}}</dd> + + <dt>{{translate("SYSTEM_INFO.SERVER")}}</dt> + <dd>{{info.environment.SERVER_SOFTWARE}}</dd> + + <dt>{{translate("SYSTEM_INFO.DB_VERSION")}}</dt> + <dd>{{info.version.database.type}} {{info.version.database.version}}</dd> + + <dt>{{translate("SYSTEM_INFO.DB_NAME")}}</dt> + <dd>{{info.database.name}}</dd> + + <dt>{{translate("SYSTEM_INFO.DIRECTORY")}}</dt> + <dd>{{info.path.project}}</dd> + + <dt>{{translate("SYSTEM_INFO.URL")}}</dt> + <dd>{{site.uri.public}}</dd> + + <dt>{{translate("SYSTEM_INFO.SPRINKLES")}}</dt> + <dd> + <ul class="list-unstyled"> + {% for sprinkle in sprinkles %} + <li> + {{sprinkle}} + </li> + {% endfor %} + </ul> + </dd> + </dl> + </div> + <!-- /.box-body --> + <div class="box-footer text-center"> + <a href="javascript:void(0)" class="js-clear-cache uppercase">{{ translate("CACHE.CLEAR") }}</a> + </div> + <!-- /.box-footer --> + </div> + <!--/.box --> + </div> + <!-- /.col --> + </div> + <!-- /.row --> + {% endif %} + </div> + <!-- /.col --> + {% endif %} + + {% if checkAccess('uri_activities') %} + <div class="col-md-6 col-sm-12 col-xs-12"> + <div id="widget-activities" class="box box-primary"> + <div class="box-header"> + <h3 class="box-title"><i class="fa fa-tasks fa-fw"></i> {{translate('ACTIVITY', 2)}}</h3> + {% include "tables/table-tool-menu.html.twig" %} + </div> + <div class="box-body"> + {% include "tables/activities.html.twig" with { + "table" : { + "id" : "table-activities", + "columns" : ["user"] + } + } + %} + </div> + </div> + </div> + {% elseif checkAccess('view_group_field', { + 'group': current_user.group, + 'property': 'users' + }) %} + <div class="col-md-6 col-sm-12 col-xs-12"> + <div id="widget-group-users" class="box box-primary"> + <div class="box-header"> + <h3 class="box-title"><i class="fa fa-fw fa-user"></i> {{translate('USER', 2)}}</h3> + {% include "tables/table-tool-menu.html.twig" %} + </div> + <div class="box-body"> + {% include "tables/users.html.twig" with { + "table" : { + "id" : "table-group-users" + } + } + %} + </div> + <div class="box-footer"> + <button type="button" class="btn btn-success js-user-create"> + <i class="fa fa-plus-square"></i> {{translate("USER.CREATE")}} + </button> + </div> + </div> + </div> + {% else %} + <div class="col-sm-4 col-sm-offset-4 col-xs-12"> + <div class="box box-widget widget-user"> + <!-- Add the bg color to the header using any of the bg-* classes --> + <div class="widget-user-header bg-black-active"> + <h3 class="widget-user-username"> + {{translate("WELCOME", { + 'first_name': current_user.first_name + })}} + </h3> + </div> + <div class="widget-user-image"> + <img class="img-circle" src="{{assets.url('assets://userfrosting/images/cupcake.png')}}" alt="User Avatar"> + </div> + <div class="box-footer"> + <h4> + {{translate("WELCOME_TO", { + 'title': site.title + })}} + </h4> + <p> + {{translate("NO_FEATURES_YET")}} + </p> + </div> + </div> + <!-- /.widget-user --> + + + </div> + <!-- /.row --> + {% endif %} + </div> + <!-- /.row --> +{% endblock %} + +{% block scripts_page %} + <!-- Include page variables --> + <script> + {% include "pages/partials/page.js.twig" %} + + // Add user name + page = $.extend( + true, // deep extend + { + "group_slug": "{{current_user.group.slug}}" + }, + page + ); + </script> + + <!-- Include page-specific JS --> + {{ assets.js('js/pages/dashboard') | raw }} + +{% endblock %}
\ No newline at end of file diff --git a/login/app/sprinkles/admin/templates/pages/group.html.twig b/login/app/sprinkles/admin/templates/pages/group.html.twig new file mode 100755 index 0000000..bf4d275 --- /dev/null +++ b/login/app/sprinkles/admin/templates/pages/group.html.twig @@ -0,0 +1,106 @@ +{% extends "pages/abstract/dashboard.html.twig" %} + +{% block stylesheets_page %} + <!-- Page-specific CSS asset bundle --> + {{ assets.css('css/form-widgets') | raw }} +{% endblock %} + +{# Overrides blocks in head of base template #} +{% block page_title %}{{ translate("GROUP", 2) }} | {{group.name}}{% endblock %} + +{% block page_description %}{{ translate("GROUP.INFO_PAGE", {name: group.name}) }}{% endblock %} + +{% block body_matter %} + <div class="row"> + <div class="col-lg-4"> + <div id="view-group" class="box box-primary"> + <div class="box-header with-border"> + <h3 class="box-title">{{translate('GROUP.SUMMARY')}}</h3> + {% if 'tools' not in tools.hidden %} + <div class="box-tools pull-right"> + <div class="btn-group"> + <button type="button" class="btn btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> + <i class="fa fa-gear"></i> <span class="caret"></span> + </button> + <ul class="dropdown-menu box-tool-menu"> + {% block tools %} + <li> + <a href="#" class="js-group-edit" data-slug="{{group.slug}}"> + <i class="fa fa-edit fa-fw"></i> {{translate('EDIT')}} + </a> + </li> + {% if 'delete' not in tools.hidden %} + <li> + <a href="#" class="js-group-delete" data-slug="{{group.slug}}"> + <i class="fa fa-trash-o fa-fw"></i> {{translate('DELETE')}} + </a> + </li> + {% endif %} + {% endblock %} + </ul> + </div> + </div> + {% endif %} + </div> + <div class="box-body box-profile"> + <div class="text-center"> + <i class="{{group.icon}} fa-5x"></i> + </div> + + <h3 class="profile-username text-center">{{group.name}}</h3> + + {% if 'description' not in fields.hidden %} + <p class="text-muted"> + {{group.description}} + </p> + {% endif %} + {% if 'users' not in fields.hidden %} + <hr> + <strong><i class="fa fa-users margin-r-5"></i> {{ translate('USER', 2)}}</strong> + <p class="badge bg-blue box-profile-property"> + {{group.users.count}} + </p> + {% endif %} + {% block group_profile %}{% endblock %} + </div> + </div> + </div> + <div class="col-lg-8"> + <div id="widget-group-users" class="box box-primary"> + <div class="box-header"> + <h3 class="box-title"><i class="fa fa-fw fa-user"></i> {{translate('USER', 2)}}</h3> + {% include "tables/table-tool-menu.html.twig" %} + </div> + <div class="box-body"> + {% include "tables/users.html.twig" with { + "table" : { + "id" : "table-group-users" + } + } + %} + </div> + </div> + </div> + </div> +{% endblock %} +{% block scripts_page %} + <!-- Include page variables --> + <script> + {% include "pages/partials/page.js.twig" %} + + // Add user name + page = $.extend( + true, // deep extend + { + "group_slug": "{{group.slug}}" + }, + page + ); + </script> + + <!-- Include form widgets JS --> + {{ assets.js('js/form-widgets') | raw }} + + <!-- Include page-specific JS --> + {{ assets.js('js/pages/group') | raw }} +{% endblock %} diff --git a/login/app/sprinkles/admin/templates/pages/groups.html.twig b/login/app/sprinkles/admin/templates/pages/groups.html.twig new file mode 100755 index 0000000..35e9a88 --- /dev/null +++ b/login/app/sprinkles/admin/templates/pages/groups.html.twig @@ -0,0 +1,52 @@ +{% extends "pages/abstract/dashboard.html.twig" %} + +{% block stylesheets_page %} + <!-- Page-specific CSS asset bundle --> + {{ assets.css('css/form-widgets') | raw }} +{% endblock %} + +{# Overrides blocks in head of base template #} +{% block page_title %}{{ translate("GROUP", 2) }}{% endblock %} + +{% block page_description %}{{ translate("GROUP.PAGE_DESCRIPTION") }}{% endblock %} + +{% block body_matter %} + <div class="row"> + <div class="col-md-12"> + <div id="widget-groups" class="box box-primary"> + <div class="box-header"> + <h3 class="box-title"><i class="fa fa-fw fa-users"></i> {{translate('GROUP', 2)}}</h3> + {% include "tables/table-tool-menu.html.twig" %} + </div> + <div class="box-body"> + {% include "tables/groups.html.twig" with { + "table" : { + "id" : "table-groups" + } + } + %} + </div> + {% if checkAccess('create_group') %} + <div class="box-footer"> + <button type="button" class="btn btn-success js-group-create"> + <i class="fa fa-plus-square"></i> {{translate("GROUP.CREATE")}} + </button> + </div> + {% endif %} + </div> + </div> + </div> +{% endblock %} +{% block scripts_page %} + <!-- Include validation rules --> + <script> + {% include "pages/partials/page.js.twig" %} + </script> + + <!-- Include form widgets JS --> + {{ assets.js('js/form-widgets') | raw }} + + <!-- Include page-specific JS --> + {{ assets.js('js/pages/groups') | raw }} + +{% endblock %} diff --git a/login/app/sprinkles/admin/templates/pages/permission.html.twig b/login/app/sprinkles/admin/templates/pages/permission.html.twig new file mode 100755 index 0000000..6adc014 --- /dev/null +++ b/login/app/sprinkles/admin/templates/pages/permission.html.twig @@ -0,0 +1,91 @@ +{% extends "pages/abstract/dashboard.html.twig" %} + +{% block stylesheets_page %} + <!-- Page-group-specific CSS asset bundle --> + {{ assets.css('css/form-widgets') | raw }} +{% endblock %} + +{# Overrides blocks in head of base template #} +{% block page_title %}{{translate("PERMISSION", 2)}} | {{permission.name}}{% endblock %} + +{% block page_description %}{{translate("PERMISSION.INFO_PAGE", {name: permission.name})}}{% endblock %} + +{% block body_matter %} + <div class="row"> + <div class="col-lg-4"> + <div id="view-permission" class="box box-primary"> + <div class="box-header with-border"> + <h3 class="box-title">{{translate("PERMISSION.SUMMARY")}}</h3> + </div> + <div class="box-body box-profile"> + <div class="text-center"> + <i class="fa fa-key fa-5x"></i> + </div> + + <h3 class="profile-username text-center">{{permission.name}}</h3> + + <p class="text-muted"> + {{permission.description}} + </p> + <hr> + <strong>{{translate("PERMISSION.ID")}}: </strong> + <span class="js-copy-container"> + <span class="js-copy-target" style="margin: 0 5px;">{{permission.id}}</span> + <i class="fa fa-copy text-blue uf-copy-trigger js-copy-trigger"></i> + </span> + <hr> + <strong>{{translate("SLUG_CONDITION")}}</strong> + <br> + <br> + <p> + <code>{{permission.slug}}</code> + </p> + <p> + ↳ <code>{{permission.conditions}}</code> + </p> + </div> + <div class="box-footer"> + {{translate("PERMISSION.NOTE_READ_ONLY")}} + </div> + </div> + </div> + <div class="col-lg-8"> + <div id="widget-permission-users" class="box box-primary"> + <div class="box-header"> + <h3 class="box-title"><i class="fa fa-fw fa-key"></i> {{translate("USER.WITH_PERMISSION")}}</h3> + {% include "tables/table-tool-menu.html.twig" %} + </div> + <div class="box-body"> + {% include "tables/users.html.twig" with { + "table" : { + "id" : "table-permission-users", + "columns" : ["via_roles"] + } + } + %} + </div> + </div> + </div> + </div> +{% endblock %} +{% block scripts_page %} + <!-- Include page variables --> + <script> + {% include "pages/partials/page.js.twig" %} + + // Add user name + page = $.extend( + true, // deep extend + { + "permission_id": "{{permission.id}}" + }, + page + ); + </script> + + <!-- Include form widgets JS --> + {{ assets.js('js/form-widgets') | raw }} + + <!-- Include page-specific JS --> + {{ assets.js('js/pages/permission') | raw }} +{% endblock %} diff --git a/login/app/sprinkles/admin/templates/pages/permissions.html.twig b/login/app/sprinkles/admin/templates/pages/permissions.html.twig new file mode 100755 index 0000000..2696209 --- /dev/null +++ b/login/app/sprinkles/admin/templates/pages/permissions.html.twig @@ -0,0 +1,45 @@ +{% extends "pages/abstract/dashboard.html.twig" %} + +{% block stylesheets_page %} + <!-- Page-specific CSS asset bundle --> + {{ assets.css('css/form-widgets') | raw }} +{% endblock %} + +{# Overrides blocks in head of base template #} +{% block page_title %}{{ translate("PERMISSION", 2)}}{% endblock %} + +{% block page_description %}{{ translate("PERMISSION.PAGE_DESCRIPTION")}}{% endblock %} + +{% block body_matter %} + <div class="row"> + <div class="col-md-12"> + <div id="widget-permissions" class="box box-primary"> + <div class="box-header"> + <h3 class="box-title pull-left"><i class="fa fa-key fa-fw"></i> {{translate('PERMISSION', 2)}}</h3> + {% include "tables/table-tool-menu.html.twig" %} + </div> + <div class="box-body"> + {% include "tables/permissions.html.twig" with { + "table" : { + "id" : "table-permissions" + } + } + %} + </div> + </div> + </div> + </div> +{% endblock %} +{% block scripts_page %} + <!-- Include validation rules --> + <script> + {% include "pages/partials/page.js.twig" %} + </script> + + <!-- Include form widgets JS --> + {{ assets.js('js/form-widgets') | raw }} + + <!-- Include page-specific JS --> + {{ assets.js('js/pages/permissions') | raw }} + +{% endblock %} diff --git a/login/app/sprinkles/admin/templates/pages/role.html.twig b/login/app/sprinkles/admin/templates/pages/role.html.twig new file mode 100755 index 0000000..daf1004 --- /dev/null +++ b/login/app/sprinkles/admin/templates/pages/role.html.twig @@ -0,0 +1,129 @@ +{% extends "pages/abstract/dashboard.html.twig" %} + +{% block stylesheets_page %} + <!-- Page-group-specific CSS asset bundle --> + {{ assets.css('css/form-widgets') | raw }} +{% endblock %} + +{# Overrides blocks in head of base template #} +{% block page_title %}{{translate("ROLE", 2)}} | {{role.name}}{% endblock %} + +{% block page_description %}{{translate("ROLE.INFO_PAGE", {name: role.name})}}{% endblock %} + +{% block body_matter %} + <div class="row"> + <div class="col-lg-4"> + <div id="view-role" class="box box-primary"> + <div class="box-header with-border"> + <h3 class="box-title">{{translate('ROLE.SUMMARY')}}</h3> + {% if 'tools' not in tools.hidden %} + <div class="box-tools pull-right"> + <div class="btn-group"> + <button type="button" class="btn btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> + <i class="fa fa-gear"></i> <span class="caret"></span> + </button> + <ul class="dropdown-menu box-tool-menu"> + {% block tools %} + <li> + <a href="#" class="js-role-edit" data-slug="{{role.slug}}"> + <i class="fa fa-edit fa-fw"></i> {{translate('EDIT')}} + </a> + </li> + {% if 'permissions' not in tools.hidden %} + <li> + <a href="#" class="js-role-permissions" data-slug="{{role.slug}}"> + <i class="fa fa-key"></i> {{translate("PERMISSION.MANAGE")}} + </a> + </li> + {% endif %} + {% if 'delete' not in tools.hidden %} + <li> + <a href="#" class="js-role-delete" data-slug="{{role.slug}}"> + <i class="fa fa-trash-o fa-fw"></i> {{translate('DELETE')}} + </a> + </li> + {% endif %} + {% endblock %} + </ul> + </div> + </div> + {% endif %} + </div> + <div class="box-body box-profile"> + <div class="text-center"> + <i class="fa fa-drivers-license fa-5x"></i> + </div> + + <h3 class="profile-username text-center">{{role.name}}</h3> + + {% if 'description' not in fields.hidden %} + <p class="text-muted"> + {{role.description}} + </p> + {% endif %} + {% if 'users' not in fields.hidden %} + <hr> + <strong><i class="fa fa-users margin-r-5"></i> {{ translate('USER', 2)}}</strong> + <p class="badge bg-blue box-profile-property"> + {{role.users.count}} + </p> + {% endif %} + </div> + </div> + </div> + <div class="col-lg-8"> + <div id="widget-role-permissions" class="box box-primary"> + <div class="box-header"> + <h3 class="box-title"><i class="fa fa-fw fa-key"></i> {{translate('PERMISSION', 2)}}</h3> + {% include "tables/table-tool-menu.html.twig" %} + </div> + <div class="box-body"> + {% include "tables/permissions.html.twig" with { + "table" : { + "id" : "table-role-permissions" + } + } + %} + </div> + </div> + </div> + <div class="col-lg-12"> + <div id="widget-role-users" class="box box-primary"> + <div class="box-header"> + <h3 class="box-title"><i class="fa fa-fw fa-key"></i> {{translate('USER', 2)}}</h3> + {% include "tables/table-tool-menu.html.twig" %} + </div> + <div class="box-body"> + {% include "tables/users.html.twig" with { + "table" : { + "id" : "table-role-users", + "columns" : ["last_activity"] + } + } + %} + </div> + </div> + </div> + </div> +{% endblock %} +{% block scripts_page %} + <!-- Include page variables --> + <script> + {% include "pages/partials/page.js.twig" %} + + // Add user name + page = $.extend( + true, // deep extend + { + "role_slug": "{{role.slug}}" + }, + page + ); + </script> + + <!-- Include form widgets JS --> + {{ assets.js('js/form-widgets') | raw }} + + <!-- Include page-specific JS --> + {{ assets.js('js/pages/role') | raw }} +{% endblock %} diff --git a/login/app/sprinkles/admin/templates/pages/roles.html.twig b/login/app/sprinkles/admin/templates/pages/roles.html.twig new file mode 100755 index 0000000..c5b3995 --- /dev/null +++ b/login/app/sprinkles/admin/templates/pages/roles.html.twig @@ -0,0 +1,50 @@ +{% extends "pages/abstract/dashboard.html.twig" %} + +{% block stylesheets_page %} + <!-- Page-specific CSS asset bundle --> + {{ assets.css('css/form-widgets') | raw }} +{% endblock %} + +{# Overrides blocks in head of base template #} +{% block page_title %}{{ translate("ROLE", 2)}}{% endblock %} + +{% block page_description %}{{ translate("ROLE.PAGE_DESCRIPTION")}}{% endblock %} + +{% block body_matter %} + <div class="row"> + <div class="col-md-12"> + <div id="widget-roles" class="box box-primary"> + <div class="box-header"> + <h3 class="box-title"><i class="fa fa-fw fa-drivers-license"></i> {{translate('ROLE', 2)}}</h3> + {% include "tables/table-tool-menu.html.twig" %} + </div> + <div class="panel-body"> + {% include "tables/roles.html.twig" with { + "table" : { + "id" : "table-roles" + } + } + %} + </div> + <div class="box-footer"> + <button type="button" class="btn btn-success js-role-create"> + <i class="fa fa-plus-square"></i> {{translate("ROLE.CREATE")}} + </button> + </div> + </div> + </div> + </div> +{% endblock %} +{% block scripts_page %} + <!-- Include validation rules --> + <script> + {% include "pages/partials/page.js.twig" %} + </script> + + <!-- Include form widgets JS --> + {{ assets.js('js/form-widgets') | raw }} + + <!-- Include page-specific JS --> + {{ assets.js('js/pages/roles') | raw }} + +{% endblock %} diff --git a/login/app/sprinkles/admin/templates/pages/user.html.twig b/login/app/sprinkles/admin/templates/pages/user.html.twig new file mode 100755 index 0000000..d9c9ab2 --- /dev/null +++ b/login/app/sprinkles/admin/templates/pages/user.html.twig @@ -0,0 +1,195 @@ +{% extends "pages/abstract/dashboard.html.twig" %} + +{% block stylesheets_page %} + <!-- Page-specific CSS asset bundle --> + {{ assets.css('css/form-widgets') | raw }} +{% endblock %} + +{# Overrides blocks in head of base template #} +{% block page_title %}{{ translate("USER", 2)}} | {{user.full_name}}{% endblock %} + +{% block page_description %}{{ translate("USER.INFO_PAGE", {name: user.user_name}) }}{% endblock %} + +{% block body_matter %} + {% block group_box %} + {% endblock %} + <div class="row"> + <div class="col-lg-4"> + <div id="view-user"> + {% block user_box %} + <div class="box box-primary"> + <div class="box-header with-border"> + <h3 class="box-title">{{translate('USER.SUMMARY')}}</h3> + {% if 'tools' not in tools.hidden %} + <div class="box-tools pull-right"> + <div class="btn-group"> + <button type="button" class="btn btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> + <i class="fa fa-gear"></i> <span class="caret"></span> + </button> + <ul class="dropdown-menu box-tool-menu"> + {% block tools %} + <li> + <a href="#" class="js-user-edit" data-user_name="{{user.user_name}}"> + <i class="fa fa-edit fa-fw"></i> {{translate('EDIT')}} + </a> + </li> + {% if 'password' not in tools.hidden %} + <li> + <a href="#" class="js-user-password" data-user_name="{{user.user_name}}"> + <i class="fa fa-lock fa-fw"></i> {{translate('PASSWORD')}} + </a> + </li> + {% endif %} + {% if 'roles' not in tools.hidden %} + <li> + <a href="#" class="js-user-roles" data-user_name="{{user.user_name}}"> + <i class="fa fa-drivers-license fa-fw"></i> {{translate('ROLE', 2)}} + </a> + </li> + {% endif %} + {% if 'activate' not in tools.hidden and user.flag_verified == "0" %} + <li> + <a href="#" class="js-user-activate" data-user_name="{{user.user_name}}"> + <i class="fa fa-bolt fa-fw"></i> {{translate('ACTIVATE')}} + </a> + </li> + {% endif %} + {% if 'enable' not in tools.hidden %} + {% if user.flag_enabled == "1" %} + <li> + <a href="#" class="js-user-disable" data-user_name="{{user.user_name}}"> + <i class="fa fa-minus-circle fa-fw"></i> {{translate('DISABLE')}} + </a> + </li> + {% else %} + <li> + <a href="#" class="js-user-enable" data-user_name="{{user.user_name}}"> + <i class="fa fa-plus-circle fa-fw"></i> {{translate('ENABLE')}} + </a> + </li> + {% endif %} + {% endif %} + {% if 'delete' not in tools.hidden %} + <li> + <a href="#" class="js-user-delete" data-user_name="{{user.user_name}}"> + <i class="fa fa-trash-o fa-fw"></i> {{translate('DELETE')}} + </a> + </li> + {% endif %} + {% endblock %} + </ul> + </div> + </div> + {% endif %} + </div> + <div class="box-body box-profile"> + <img class="profile-user-img img-responsive img-circle" src="{{user.avatar}}" alt="{{user.user_name}}"> + + <h3 class="profile-username text-center">{{user.full_name}}</h3> + <div class="text-center"> + {% if user.flag_enabled == 0 %} + <i class="fa fa-fw fa-minus-circle fa-lg text-red" title="{{translate('DISABLED')}}"></i> + {% endif %} + {% if user.flag_verified == 0 %} + <i class="fa fa-fw fa-bolt fa-lg text-yellow" title="{{translate('UNACTIVATED')}}"></i> + {% endif %} + </div> + <h4 class="text-muted text-center">{{user.user_name}}{% if 'group' not in fields.hidden %} • {{user.group.name}}{% endif %}</h4> + + {% if 'email' not in fields.hidden %} + <hr> + <strong><i class="fa fa-envelope margin-r-5"></i> {{translate("EMAIL")}}</strong> + <p class="text-muted box-profile-property js-copy-container"> + <i class="fa fa-copy uf-copy-trigger js-copy-trigger"></i> + <span class="js-copy-target">{{user.email}}</span> + </p> + {% endif %} + + {% if 'locale' not in fields.hidden %} + <hr> + <strong><i class="fa fa-language margin-r-5"></i> {{translate("LOCALE")}}</strong> + <p class="text-muted box-profile-property"> + {{locales[user.locale]}} + </p> + {% endif %} + + {% block user_profile %}{% endblock %} + + {% if 'roles' not in fields.hidden %} + <hr> + <strong><i class="fa fa-drivers-license margin-r-5"></i> {{translate("ROLE", 2)}}</strong> + <p class="box-profile-property"> + {% for role in user.roles %} + <span class="label label-primary" title="{{role.description}}">{{role.name}}</span> + {% endfor %} + </p> + {% endif %} + </div> + </div> + {% endblock %} + </div> + </div> + {% if 'activities' not in widgets.hidden %} + <div class="col-lg-8"> + {% block activity_box %} + <div id="widget-user-activities" class="box box-primary"> + <div class="box-header"> + <h3 class="box-title"><i class="fa fa-fw fa-tasks"></i> {{translate('ACTIVITY', 2)}}</h3> + {% include "tables/table-tool-menu.html.twig" %} + </div> + <div class="box-body"> + {% include "tables/activities.html.twig" with { + "table" : { + "id" : "table-user-activities" + } + } + %} + </div> + </div> + {% endblock %} + </div> + {% endif %} + </div> + {% if 'permissions' not in widgets.hidden %} + <div class="row"> + <div class="col-md-12"> + <div id="widget-permissions" class="box box-primary"> + <div class="box-header"> + <h3 class="box-title pull-left"><i class="fa fa-key fa-fw"></i> {{translate('PERMISSION', 2)}}</h3> + {% include "tables/table-tool-menu.html.twig" %} + </div> + <div class="box-body"> + {% include "tables/permissions.html.twig" with { + "table" : { + "id" : "table-permissions", + "columns" : ["via_roles"] + } + } + %} + </div> + </div> + </div> + </div> + {% endif %} +{% endblock %} +{% block scripts_page %} + <!-- Include page variables --> + <script> + {% include "pages/partials/page.js.twig" %} + + // Add user name + page = $.extend( + true, // deep extend + { + "user_name": "{{user.user_name}}" + }, + page + ); + </script> + + <!-- Include form widgets JS --> + {{ assets.js('js/form-widgets') | raw }} + + <!-- Include page-specific JS --> + {{ assets.js('js/pages/user') | raw }} +{% endblock %} diff --git a/login/app/sprinkles/admin/templates/pages/users.html.twig b/login/app/sprinkles/admin/templates/pages/users.html.twig new file mode 100755 index 0000000..3e4642d --- /dev/null +++ b/login/app/sprinkles/admin/templates/pages/users.html.twig @@ -0,0 +1,53 @@ +{% extends "pages/abstract/dashboard.html.twig" %} + +{% block stylesheets_page %} + <!-- Page-specific CSS asset bundle --> + {{ assets.css('css/form-widgets') | raw }} +{% endblock %} + +{# Overrides blocks in head of base template #} +{% block page_title %}{{ translate("USER", 2)}}{% endblock %} + +{% block page_description %}{{ translate("USER.PAGE_DESCRIPTION")}}{% endblock %} + +{% block body_matter %} + <div class="row"> + <div class="col-md-12"> + <div id="widget-users" class="box box-primary"> + <div class="box-header"> + <h3 class="box-title pull-left"><i class="fa fa-fw fa-user"></i> {{translate('USER', 2)}}</h3> + {% include "tables/table-tool-menu.html.twig" %} + </div> + <div class="box-body"> + {% include "tables/users.html.twig" with { + "table" : { + "id" : "table-users", + "columns" : ["last_activity"] + } + } + %} + </div> + {% if checkAccess('create_user') %} + <div class="box-footer"> + <button type="button" class="btn btn-success js-user-create"> + <i class="fa fa-plus-square"></i> {{ translate("USER.CREATE")}} + </button> + </div> + {% endif %} + </div> + </div> + </div> +{% endblock %} +{% block scripts_page %} + <!-- Include validation rules --> + <script> + {% include "pages/partials/page.js.twig" %} + </script> + + <!-- Include form widgets JS --> + {{ assets.js('js/form-widgets') | raw }} + + <!-- Include page-specific JS --> + {{ assets.js('js/pages/users') | raw }} + +{% endblock %} diff --git a/login/app/sprinkles/admin/templates/tables/activities.html.twig b/login/app/sprinkles/admin/templates/tables/activities.html.twig new file mode 100755 index 0000000..d70541b --- /dev/null +++ b/login/app/sprinkles/admin/templates/tables/activities.html.twig @@ -0,0 +1,73 @@ +{# This partial template renders a table of user activities, to be populated with rows via an AJAX request. + # This extends a generic template for paginated tables. + # + # Note that this template contains a "skeleton" table with an empty table body, and then a block of Handlebars templates which are used + # to render the table cells with the data from the AJAX request. +#} + +{% extends "tables/table-paginated.html.twig" %} + +{% block table %} + <table id="{{table.id}}" class="tablesorter table table-bordered table-hover table-striped" data-sortlist="[[0, 1]]"> + <thead> + <tr> + <th class="sorter-metanum" data-column-name="occurred_at" data-column-template="#activity-table-column-occurred-at" data-priority="1">{{translate('ACTIVITY.TIME')}} <i class="fa fa-sort"></i></th> + {% if 'user' in table.columns %} + <th class="sorter-metatext" data-column-name="user" data-column-template="#activity-table-column-user" data-priority="1">{{translate('USER')}} <i class="fa fa-sort"></i></th> + {% endif %} + <th class="sorter-metatext" data-column-name="description" data-column-template="#activity-table-column-description" data-priority="1">{{translate("DESCRIPTION")}} <i class="fa fa-sort"></i></th> + </tr> + </thead> + <tbody> + </tbody> + </table> +{% endblock %} + +{% block table_cell_templates %} + {# This contains a series of <script> blocks, each of which is a client-side Handlebars template. + # Note that these are NOT Twig templates, although the syntax is similar. We wrap them in the `verbatim` tag, + # so that Twig will output them directly into the DOM instead of trying to treat them like Twig templates. + # + # These templates require handlebars-helpers.js, moment.js + #} + {% verbatim %} + <script id="activity-table-column-occurred-at" type="text/x-handlebars-template"> + {{#if row.occurred_at }} + <td data-num="{{dateFormat row.occurred_at format='x'}}"> + {{dateFormat row.occurred_at format="dddd"}}<br>{{dateFormat row.occurred_at format="MMM Do, YYYY h:mm a"}} + </td> + {{ else }} + <td data-num="0"> + <i>{% endverbatim %}{{translate("UNKNOWN")}}{% verbatim %}</i> + </td> + {{/if }} + </script> + + <script id="activity-table-column-user" type="text/x-handlebars-template"> + <td data-text="{{row.user.last_name}}"> + {{#if row.user }} + <strong> + <a href="{{site.uri.public}}/users/u/{{row.user.user_name}}">{{row.user.first_name}} {{row.user.last_name}} ({{row.user.user_name}})</a> + </strong> + <div class="js-copy-container"> + <span class="js-copy-target">{{row.user.email}}</span> + <button class="btn btn-xs uf-copy-trigger js-copy-trigger"><i class="fa fa-copy"></i></button> + </div> + {{ else }} + <i>{% endverbatim %}{{translate("USER.DELETED")}}{% verbatim %}</i> + {{/if }} + </td> + </script> + + <script id="activity-table-column-description" type="text/x-handlebars-template"> + <td> + <div> + {{row.ip_address}} + </div> + <div> + <i>{{row.description}}</i> + </div> + </td> + </script> + {% endverbatim %} +{% endblock %} diff --git a/login/app/sprinkles/admin/templates/tables/groups.html.twig b/login/app/sprinkles/admin/templates/tables/groups.html.twig new file mode 100755 index 0000000..2c5a84a --- /dev/null +++ b/login/app/sprinkles/admin/templates/tables/groups.html.twig @@ -0,0 +1,69 @@ +{# This partial template renders a table of groups, to be populated with rows via an AJAX request. + # This extends a generic template for paginated tables. + # + # Note that this template contains a "skeleton" table with an empty table body, and then a block of Handlebars templates which are used + # to render the table cells with the data from the AJAX request. +#} + +{% extends "tables/table-paginated.html.twig" %} + +{% block table %} + <table id="{{table.id}}" class="tablesorter table table-bordered table-hover table-striped" data-sortlist="{{table.sortlist}}"> + <thead> + <tr> + <th class="sorter-metatext" data-column-name="name" data-column-template="#group-table-column-info" data-priority="1">{{translate('GROUP')}} <i class="fa fa-sort"></i></th> + <th class="sorter-metatext" data-column-name="description" data-column-template="#group-table-column-description" data-priority="2">{{translate("DESCRIPTION")}} <i class="fa fa-sort"></i></th> + <th data-column-template="#group-table-column-actions" data-sorter="false" data-filter="false" data-priority="1">{{translate("ACTIONS")}}</th> + </tr> + </thead> + <tbody> + </tbody> + </table> +{% endblock %} + +{% block table_cell_templates %} + {# This contains a series of <script> blocks, each of which is a client-side Handlebars template. + # Note that these are NOT Twig templates, although the syntax is similar. We wrap them in the `verbatim` tag, + # so that Twig will output them directly into the DOM instead of trying to treat them like Twig templates. + # + # These templates require handlebars-helpers.js, moment.js + #} + {% verbatim %} + <script id="group-table-column-info" type="text/x-handlebars-template"> + <td data-text="{{row.name}}"> + <strong> + <i class="{{row.icon}} fa-fw"></i> <a href="{{site.uri.public}}/groups/g/{{row.slug}}">{{row.name}}</a> + </strong> + </td> + </script> + + <script id="group-table-column-description" type="text/x-handlebars-template"> + <td> + {{row.description}} + </td> + </script> + + <script id="group-table-column-actions" type="text/x-handlebars-template"> + <td> + <div class="btn-group"> + <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown"> + {% endverbatim %}{{translate("ACTIONS")}}{% verbatim %} + <span class="caret"></span> + </button> + <ul class="dropdown-menu dropdown-menu-right" role="menu"> + <li> + <a href="#" data-slug="{{row.slug}}" class="js-group-edit"> + <i class="fa fa-edit"></i> {% endverbatim %}{{translate("GROUP.EDIT")}}{% verbatim %} + </a> + </li> + <li> + <a href="#" data-slug="{{row.slug}}" class="js-group-delete"> + <i class="fa fa-trash-o"></i> {% endverbatim %}{{translate("GROUP.DELETE")}}{% verbatim %} + </a> + </li> + </ul> + </div> + </td> + </script> + {% endverbatim %} +{% endblock %} diff --git a/login/app/sprinkles/admin/templates/tables/permissions.html.twig b/login/app/sprinkles/admin/templates/tables/permissions.html.twig new file mode 100755 index 0000000..92e236a --- /dev/null +++ b/login/app/sprinkles/admin/templates/tables/permissions.html.twig @@ -0,0 +1,66 @@ +{# This partial template renders a table of permissions, to be populated with rows via an AJAX request. + # This extends a generic template for paginated tables. + # + # Note that this template contains a "skeleton" table with an empty table body, and then a block of Handlebars templates which are used + # to render the table cells with the data from the AJAX request. +#} + +{% extends "tables/table-paginated.html.twig" %} + +{% block table %} + <table id="{{table.id}}" class="tablesorter table table-bordered table-hover table-striped" data-sortlist="{{table.sortlist}}"> + <thead> + <tr> + <th class="sorter-metatext" data-column-name="name" data-column-template="#permission-table-column-name" data-priority="1">{{translate('PERMISSION')}} <i class="fa fa-sort"></i></th> + <th class="sorter-metatext" data-column-name="properties" data-column-template="#permission-table-column-properties" data-priority="1">{{translate('SLUG_CONDITION')}} <i class="fa fa-sort"></i></th> + {% if 'via_roles' in table.columns %} + <th data-column-template="#permission-table-column-via-roles" data-sorter="false" data-filter="false" data-priority="2">{{translate('PERMISSION.VIA_ROLES')}}</th> + {% endif %} + </tr> + </thead> + <tbody> + </tbody> + </table> +{% endblock %} + +{% block table_cell_templates %} + {# This contains a series of <script> blocks, each of which is a client-side Handlebars template. + # Note that these are NOT Twig templates, although the syntax is similar. We wrap them in the `verbatim` tag, + # so that Twig will output them directly into the DOM instead of trying to treat them like Twig templates. + # + # These templates require handlebars-helpers.js, moment.js + #} + {% verbatim %} + <script id="permission-table-column-name" type="text/x-handlebars-template"> + <td data-text="{{row.name}}"> + <div> + <strong> + <a href="{{site.uri.public}}/permissions/p/{{row.id}}">{{row.name}}</a> + </strong> + </div> + </td> + </script> + + <script id="permission-table-column-properties" type="text/x-handlebars-template"> + <td> + <div> + <code>{{row.slug}}</code> + </div> + <div> + ↳ <code>{{row.conditions}}</code> + </div> + <div> + <i>{{row.description}}</i> + </div> + </td> + </script> + + <script id="permission-table-column-via-roles" type="text/x-handlebars-template"> + <td> + {{#each row.roles_via }} + <a href="{% endverbatim %}{# Handlebars can't access variables in the global scope, so we have to use Twig to insert the base url #}{{site.uri.public}}{% verbatim %}/roles/r/{{this.slug}}" class="label label-primary" title="{{this.description}}">{{this.name}}</a> + {{/each}} + </td> + </script> + {% endverbatim %} +{% endblock %} diff --git a/login/app/sprinkles/admin/templates/tables/roles.html.twig b/login/app/sprinkles/admin/templates/tables/roles.html.twig new file mode 100755 index 0000000..dbdb49e --- /dev/null +++ b/login/app/sprinkles/admin/templates/tables/roles.html.twig @@ -0,0 +1,74 @@ +{# This partial template renders a table of roles, to be populated with rows via an AJAX request. + # This extends a generic template for paginated tables. + # + # Note that this template contains a "skeleton" table with an empty table body, and then a block of Handlebars templates which are used + # to render the table cells with the data from the AJAX request. +#} + +{% extends "tables/table-paginated.html.twig" %} + +{% block table %} + <table id="{{table.id}}" class="tablesorter table table-bordered table-hover table-striped" data-sortlist="{{table.sortlist}}"> + <thead> + <tr> + <th class="sorter-metatext" data-column-name="name" data-column-template="#role-table-column-info" data-priority="1">{{translate('ROLE')}} <i class="fa fa-sort"></i></th> + <th class="sorter-metatext" data-column-name="description" data-column-template="#role-table-column-description" data-priority="2">{{translate('DESCRIPTION')}} <i class="fa fa-sort"></i></th> + <th data-column-template="#role-table-column-actions" data-sorter="false" data-filter="false" data-priority="1">{{translate('ACTIONS')}}</th> + </tr> + </thead> + <tbody> + </tbody> + </table> +{% endblock %} + +{% block table_cell_templates %} + {# This contains a series of <script> blocks, each of which is a client-side Handlebars template. + # Note that these are NOT Twig templates, although the syntax is similar. We wrap them in the `verbatim` tag, + # so that Twig will output them directly into the DOM instead of trying to treat them like Twig templates. + # + # These templates require handlebars-helpers.js, moment.js + #} + {% verbatim %} + <script id="role-table-column-info" type="text/x-handlebars-template"> + <td data-text="{{row.name}}"> + <strong> + <a href="{{site.uri.public}}/roles/r/{{row.slug}}">{{row.name}}</a> + </strong> + </td> + </script> + + <script id="role-table-column-description" type="text/x-handlebars-template"> + <td> + {{row.description}} + </td> + </script> + + <script id="role-table-column-actions" type="text/x-handlebars-template"> + <td> + <div class="btn-group"> + <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown"> + {% endverbatim %}{{translate("ACTIONS")}}{% verbatim %} + <span class="caret"></span> + </button> + <ul class="dropdown-menu dropdown-menu-right" role="menu"> + <li> + <a href="#" data-slug="{{row.slug}}" class="js-role-permissions"> + <i class="fa fa-key"></i> {% endverbatim %}{{translate("PERMISSION.MANAGE")}}{% verbatim %} + </a> + </li> + <li> + <a href="#" data-slug="{{row.slug}}" class="js-role-edit"> + <i class="fa fa-edit"></i> {% endverbatim %}{{translate("ROLE.EDIT")}}{% verbatim %} + </a> + </li> + <li> + <a href="#" data-slug="{{row.slug}}" class="js-role-delete"> + <i class="fa fa-trash-o"></i> {% endverbatim %}{{translate("ROLE.DELETE")}}{% verbatim %} + </a> + </li> + </ul> + </div> + </td> + </script> + {% endverbatim %} +{% endblock %} diff --git a/login/app/sprinkles/admin/templates/tables/users.html.twig b/login/app/sprinkles/admin/templates/tables/users.html.twig new file mode 100755 index 0000000..1cebb47 --- /dev/null +++ b/login/app/sprinkles/admin/templates/tables/users.html.twig @@ -0,0 +1,149 @@ +{# This partial template renders a table of users, to be populated with rows via an AJAX request. + # This extends a generic template for paginated tables. + # + # Note that this template contains a "skeleton" table with an empty table body, and then a block of Handlebars templates which are used + # to render the table cells with the data from the AJAX request. +#} + +{% extends "tables/table-paginated.html.twig" %} + +{% block table %} + <table id="{{table.id}}" class="tablesorter table table-bordered table-hover table-striped" data-sortlist="{{table.sortlist}}"> + <thead> + <tr> + <th class="sorter-metatext" data-column-name="name" data-column-template="#user-table-column-info" data-priority="1">{{translate('USER')}} <i class="fa fa-sort"></i></th> + {% if 'last_activity' in table.columns %} + <th class="sorter-metanum" data-column-name="last_activity" data-column-template="#user-table-column-last-activity" data-priority="3">{{translate("ACTIVITY.LAST")}} <i class="fa fa-sort"></i></th> + {% endif %} + {% if 'via_roles' in table.columns %} + <th data-column-template="#user-table-column-via-roles" data-sorter="false" data-filter="false" data-priority="1">{{translate('PERMISSION.VIA_ROLES')}}</th> + {% endif %} + <th class="filter-select filter-metatext" data-column-name="status" data-column-template="#user-table-column-status" data-priority="2">{{translate("STATUS")}} <i class="fa fa-sort"></i></th> + <th data-column-name="actions" data-column-template="#user-table-column-actions" data-sorter="false" data-filter="false" data-priority="1">{{translate("ACTIONS")}}</th> + </tr> + </thead> + <tbody> + </tbody> + </table> +{% endblock %} + +{% block table_cell_templates %} + {# This contains a series of <script> blocks, each of which is a client-side Handlebars template. + # Note that these are NOT Twig templates, although the syntax is similar. We wrap them in the `verbatim` tag, + # so that Twig will output them directly into the DOM instead of trying to treat them like Twig templates. + # + # These templates require handlebars-helpers.js, moment.js + #} + {% verbatim %} + <script id="user-table-column-info" type="text/x-handlebars-template"> + <td data-text="{{row.last_name}}"> + <strong> + <a href="{{site.uri.public}}/users/u/{{row.user_name}}">{{row.first_name}} {{row.last_name}} ({{row.user_name}})</a> + </strong> + <div class="js-copy-container"> + <span class="js-copy-target">{{row.email}}</span> + <button class="btn btn-xs uf-copy-trigger js-copy-trigger"><i class="fa fa-copy"></i></button> + </div> + </td> + </script> + + <script id="user-table-column-last-activity" type="text/x-handlebars-template"> + {{#if row.last_activity }} + <td data-num="{{dateFormat row.last_activity.occurred_at format='x'}}"> + {{dateFormat row.last_activity.occurred_at format="dddd"}}<br>{{dateFormat row.last_activity.occurred_at format="MMM Do, YYYY h:mm a"}} + <br> + <i>{{row.last_activity.description}}</i> + </td> + {{ else }} + <td data-num="0"> + <i>{% endverbatim %}{{translate("UNKNOWN")}}{% verbatim %}</i> + </td> + {{/if }} + </script> + + <script id="user-table-column-status" type="text/x-handlebars-template"> + <td + {{#ifx row.flag_enabled '==' 0 }} + data-text="disabled" + {{ else }} + {{#ifx row.flag_verified '==' 0 }} + data-text="unactivated" + {{ else }} + data-text="active" + {{/ifx }} + {{/ifx }} + > + {{#ifx row.flag_enabled '==' 0 }} + <span class="text-muted"> + {% endverbatim %}{{translate("DISABLED")}}{% verbatim %} + </span> + {{ else }} + {{#ifx row.flag_verified '==' 0 }} + <span class="text-yellow"> + {% endverbatim %}{{translate("UNACTIVATED")}}{% verbatim %} + </span> + {{ else }} + <span> + {% endverbatim %}{{translate("ACTIVE")}}{% verbatim %} + </span> + {{/ifx }} + {{/ifx }} + </td> + </script> + <script id="user-table-column-actions" type="text/x-handlebars-template"> + <td class="uf-table-fit-width"> + <div class="btn-group"> + <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">{% endverbatim %}{{translate("ACTIONS")}}{% verbatim %}<span class="caret"></span></button> + <ul class="dropdown-menu dropdown-menu-right-responsive" role="menu"> + {{#ifx row.flag_verified '==' 0 }} + <li> + <a href="#" data-user_name="{{row.user_name}}" class="js-user-activate"> + <i class="fa fa-bolt"></i> {% endverbatim %}{{translate("USER.ACTIVATE")}}{% verbatim %} + </a> + </li> + {{/ifx }} + <li> + <a href="#" data-user_name="{{row.user_name}}" class="js-user-edit"> + <i class="fa fa-edit"></i> {% endverbatim %}{{translate("USER.EDIT")}}{% verbatim %} + </a> + </li> + <li> + <a href="#" data-user_name="{{row.user_name}}" class="js-user-roles"> + <i class="fa fa-drivers-license"></i> {% endverbatim %}{{translate("ROLE.MANAGE")}}{% verbatim %} + </a> + </li> + <li> + <a href="#" data-user_name="{{row.user_name}}" class="js-user-password"> + <i class="fa fa-key"></i> {% endverbatim %}{{translate("USER.ADMIN.CHANGE_PASSWORD")}}{% verbatim %} + </a> + </li> + <li> + {{#ifx row.flag_enabled '==' 1 }} + <a href="#" data-user_name="{{row.user_name}}" class="js-user-disable"> + <i class="fa fa-minus-circle"></i> {% endverbatim %}{{translate("USER.DISABLE")}}{% verbatim %} + </a> + {{ else }} + <a href="#" data-user_name="{{row.user_name}}" class="js-user-enable"> + <i class="fa fa-plus-circle"></i> {% endverbatim %}{{translate("USER.ENABLE")}}{% verbatim %} + </a> + {{/ifx }} + </li> + <li> + <a href="#" data-user_name="{{row.user_name}}" class="js-user-delete"> + <i class="fa fa-trash-o"></i> {% endverbatim %}{{translate("USER.DELETE")}}{% verbatim %} + </a> + </li> + </ul> + </div> + </td> + </script> + + <script id="user-table-column-via-roles" type="text/x-handlebars-template"> + <td> + {{#each row.roles_via }} + <a href="{% endverbatim %}{# Handlebars can't access variables in the global scope, so we have to use Twig to insert the base url #}{{site.uri.public}}{% verbatim %}/roles/r/{{this.slug}}" class="label label-primary" title="{{this.description}}">{{this.name}}</a> + {{/each}} + </td> + </script> + {% endverbatim %} +{% endblock %} diff --git a/login/app/sprinkles/admin/tests/Integration/SprunjeTests.php b/login/app/sprinkles/admin/tests/Integration/SprunjeTests.php new file mode 100755 index 0000000..9eb4122 --- /dev/null +++ b/login/app/sprinkles/admin/tests/Integration/SprunjeTests.php @@ -0,0 +1,111 @@ +<?php + +namespace UserFrosting\Tests\Integration; + +use Exception; +use Illuminate\Database\Capsule\Manager as DB; +use Symfony\Component\Console\Style\SymfonyStyle; +use UserFrosting\Sprinkle\Admin\Sprunje\UserPermissionSprunje; +use UserFrosting\Sprinkle\Core\Util\ClassMapper; +use UserFrosting\Tests\DatabaseTransactions; +use UserFrosting\Tests\TestCase; + +/** + * Integration tests for the built-in Sprunje classes. + */ +class SprunjeTests extends TestCase +{ + use DatabaseTransactions; + + protected $classMapper; + + /** + * Setup the database schema. + * + * @return void + */ + public function setUp() + { + parent::setUp(); + + $this->classMapper = new ClassMapper(); + } + + /** + * Tests... + */ + public function testUserPermissionSprunje() + { + $fm = $this->ci->factory; + + // Generate some test models + $users = $fm->seed(3, 'UserFrosting\Sprinkle\Account\Database\Models\User'); + $roles = $fm->seed(3, 'UserFrosting\Sprinkle\Account\Database\Models\Role'); + $permissions = $fm->seed(3, 'UserFrosting\Sprinkle\Account\Database\Models\Permission'); + + // Create some relationships + $roles[0]->permissions()->attach($permissions[1]); + $roles[0]->permissions()->attach($permissions[2]); + $roles[1]->permissions()->attach($permissions[2]); + $roles[2]->permissions()->attach($permissions[0]); + $roles[2]->permissions()->attach($permissions[1]); + + $users[0]->roles()->attach($roles[1]); + $users[0]->roles()->attach($roles[2]); + $users[1]->roles()->attach($roles[0]); + $users[1]->roles()->attach($roles[1]); + $users[2]->roles()->attach($roles[1]); + + $this->classMapper->setClassMapping('user', 'UserFrosting\Sprinkle\Account\Database\Models\User'); + + // Test user 0 + $sprunje = new UserPermissionSprunje($this->classMapper, [ + 'user_id' => $users[0]->id + ]); + + list($count, $countFiltered, $models) = $sprunje->getModels(); + + // Check that counts are correct + $this->assertEquals(count($models), $count); + $this->assertEquals(count($models), $countFiltered); + + // Ignore pivot and roles_via. These are covered by the tests for the relationships themselves. + static::ignoreRelations($models); + $this->assertCollectionsSame(collect($permissions), $models); + + // Test user 1 + $sprunje = new UserPermissionSprunje($this->classMapper, [ + 'user_id' => $users[1]->id + ]); + + list($count, $countFiltered, $models) = $sprunje->getModels(); + + // Check that counts are correct + $this->assertEquals(count($models), $count); + $this->assertEquals(count($models), $countFiltered); + + // Ignore pivot and roles_via. These are covered by the tests for the relationships themselves. + static::ignoreRelations($models); + $this->assertCollectionsSame(collect([ + $permissions[1], + $permissions[2] + ]), $models); + + // Test user 2 + $sprunje = new UserPermissionSprunje($this->classMapper, [ + 'user_id' => $users[2]->id + ]); + + list($count, $countFiltered, $models) = $sprunje->getModels(); + + // Check that counts are correct + $this->assertEquals(count($models), $count); + $this->assertEquals(count($models), $countFiltered); + + // Ignore pivot and roles_via. These are covered by the tests for the relationships themselves. + static::ignoreRelations($models); + $this->assertCollectionsSame(collect([ + $permissions[2] + ]), $models); + } +} diff --git a/login/app/sprinkles/core/asset-bundles.json b/login/app/sprinkles/core/asset-bundles.json new file mode 100755 index 0000000..31b8995 --- /dev/null +++ b/login/app/sprinkles/core/asset-bundles.json @@ -0,0 +1,80 @@ +{ + "bundle": { + "js/main": { + "scripts": [ + "vendor/bootstrap/dist/js/bootstrap.js", + "vendor/handlebars/handlebars.js", + "vendor/jquery-validation/dist/jquery.validate.js", + "vendor/jquery-validation/dist/additional-methods.js", + "vendor/jquery-slimscroll/jquery.slimscroll.js", + "vendor/icheck/icheck.min.js", + "vendor/fastclick/lib/fastclick.js", + "vendor/select2/dist/js/select2.full.js", + "vendor/clipboard/dist/clipboard.js", + "userfrosting/js/attrchange.js", + "userfrosting/js/AdminLTE.js", + "userfrosting/js/AdminLTE-custom.js", + "userfrosting/js/fortress-jqueryvalidation-methods.js", + "userfrosting/js/uf-jqueryvalidation-config.js", + "userfrosting/js/uf-alerts.js", + "userfrosting/js/uf-form.js", + "userfrosting/js/uf-modal.js", + "userfrosting/js/uf-copy.js", + "userfrosting/js/uf-init.js" + ], + "options": { + "result": { + "type": { + "scripts": "plain" + } + } + } + }, + "js/form-widgets": { + "scripts": [ + "vendor/speakingurl/speakingurl.min.js", + "userfrosting/js/uf-collection.js" + ], + "options": { + "result": { + "type": { + "scripts": "plain" + } + } + } + }, + "css/main": { + "styles": [ + "vendor/font-awesome/css/font-awesome.css", + "vendor/bootstrap/dist/css/bootstrap.css", + "vendor/select2/dist/css/select2.css", + "vendor/icheck/skins/square/_all.css", + "vendor/ionicons/css/ionicons.css", + "userfrosting/css/uf-jqueryvalidation.css", + "userfrosting/css/uf-alerts.css", + "userfrosting/css/AdminLTE.css", + "userfrosting/css/AdminLTE-skins-all.css", + "userfrosting/css/userfrosting.css" + ], + "options": { + "result": { + "type": { + "styles": "plain" + } + } + } + }, + "css/form-widgets": { + "styles": [ + "userfrosting/css/uf-collection.css" + ], + "options": { + "result": { + "type": { + "styles": "plain" + } + } + } + } + } +} diff --git a/login/app/sprinkles/core/assets/font-starcraft/css/font-starcraft.css b/login/app/sprinkles/core/assets/font-starcraft/css/font-starcraft.css new file mode 100755 index 0000000..d5d38ef --- /dev/null +++ b/login/app/sprinkles/core/assets/font-starcraft/css/font-starcraft.css @@ -0,0 +1,61 @@ +@font-face { + font-family: 'starcraft'; + src:url('../fonts/font-starcraft.eot?h9vi58'); + src:url('../fonts/font-starcraft.eot?#iefixh9vi58') format('embedded-opentype'), + url('../fonts/font-starcraft.woff?h9vi58') format('woff'), + url('../fonts/font-starcraft.ttf?h9vi58') format('truetype'), + url('../fonts/font-starcraft.svg?h9vi58#starcraft') format('svg'); + font-weight: normal; + font-style: normal; +} + +[class^="sc-"], [class*=" sc-"] { + font-family: 'starcraft'; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.sc-terran:before { + content: "\e609"; +} +.sc-marine:before { + content: "\e600"; +} +.sc-goliath:before { + content: "\e605"; +} +.sc-siege-tank:before { + content: "\e604"; +} +.sc-zerg:before { + content: "\e60a"; +} +.sc-zergling:before { + content: "\e602"; +} +.sc-hydralisk:before { + content: "\e603"; +} +.sc-ultralisk:before { + content: "\e601"; +} +.sc-protoss:before { + content: "\e60b"; +} +.sc-zealot:before { + content: "\e606"; +} +.sc-dragoon:before { + content: "\e607"; +} +.sc-reaver:before { + content: "\e608"; +} diff --git a/login/app/sprinkles/core/assets/font-starcraft/fonts/font-starcraft.eot b/login/app/sprinkles/core/assets/font-starcraft/fonts/font-starcraft.eot Binary files differnew file mode 100755 index 0000000..0a2208d --- /dev/null +++ b/login/app/sprinkles/core/assets/font-starcraft/fonts/font-starcraft.eot diff --git a/login/app/sprinkles/core/assets/font-starcraft/fonts/font-starcraft.svg b/login/app/sprinkles/core/assets/font-starcraft/fonts/font-starcraft.svg new file mode 100755 index 0000000..c4e289d --- /dev/null +++ b/login/app/sprinkles/core/assets/font-starcraft/fonts/font-starcraft.svg @@ -0,0 +1,22 @@ +<?xml version="1.0" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" > +<svg xmlns="http://www.w3.org/2000/svg"> +<metadata>Generated by IcoMoon</metadata> +<defs> +<font id="starcraft" horiz-adv-x="512"> +<font-face units-per-em="512" ascent="480" descent="-32" /> +<missing-glyph horiz-adv-x="512" /> +<glyph unicode=" " d="" horiz-adv-x="256" /> +<glyph unicode="" d="M217.538-38.462c-1.581 0.595-7.090 1.289-12.244 1.542-7.667 0.377-10.15 0.991-13.67 3.379-2.365 1.603-4.875 3.673-5.577 4.593s-2.703 1.921-4.446 2.224c-1.744 0.302-3.978 1.358-4.969 2.347s-2.201 1.8-2.693 1.8c-1.318 0-5.069 8.832-5.273 12.421-0.226 3.949 0.535 6.139 1.853 5.324 1.765-1.091 1.125 1.438-0.761 3.002-0.983 0.817-1.7 1.009-1.589 0.428s0.131-1.834 0.047-2.782c-0.131-1.457-0.313-1.474-1.179-0.104-0.564 0.892-0.774 2.272-0.468 3.072s0.006 2.473-0.657 3.719c-0.93 1.738-0.799 2.613 0.559 3.738 0.993 0.824 1.69 3.022 1.583 5.008-0.314 5.928-0.006 7.573 1.339 7.274 0.71-0.158 1.53 0.359 1.82 1.149s-0.020 1.437-0.689 1.437c-0.833 0-0.86 0.935-0.083 2.978 0.964 2.536 0.79 3.070-1.179 3.583-1.273 0.332-2.311 0.937-2.311 1.343s-1.089 1.42-2.419 2.25c-1.773 1.106-2.185 1.133-1.547 0.099 0.574-0.925 0.395-1.118-0.509-0.558-0.761 0.47-1.298 1.714-1.195 2.764 0.334 3.413-0.089 4.244-1.271 2.484-0.867-1.293-1.018-0.892-0.602 1.601 0.304 1.83 1.681 4.524 3.059 5.995 2.339 2.491 2.386 2.851 0.698 5.428-1.547 2.363-1.618 3.872-0.49 10.459 1.084 6.325 1.667 7.608 3.266 7.19 1.547-0.405 2.044 0.466 2.424 4.241 0.471 4.665-0.742 6.906-3.242 5.995-0.689-0.251-1.982 0.062-2.875 0.697-1.234 0.877-1.073 0.988 0.679 0.466 2.266-0.676 2.267-0.659 0.069 1.040-1.227 0.951-1.666 1.727-0.97 1.727s1.046 1.163 0.781 2.587c-0.265 1.424-0.172 1.94 0.204 1.149 1.215-2.555 2.755-1.54 4.79 3.162 1.095 2.529 2.418 4.598 2.94 4.598s0.626 0.517 0.236 1.149c-0.392 0.634-0.25 1.149 0.314 1.149 2.045 0 6.333 15.565 5.581 20.255-0.394 2.454-0.792 5.238-0.883 6.189-0.245 2.529-4.362 8.049-5.773 7.739-1.466-0.322-4.302 7.19-5.111 13.53-0.323 2.529-0.894 5.375-1.273 6.324s-0.767 3.276-0.864 5.173c-0.099 1.897-0.338 3.965-0.531 4.598-1.15 3.754 0.185 9.723 2.813 12.556 1.613 1.738 2.532 3.724 2.079 4.478-0.565 0.943-0.274 0.921 0.957-0.075 1.447-1.17 2.118-1.050 3.673 0.668 1.046 1.156 1.642 2.446 1.324 2.87s-0.246 0.524 0.159 0.225c1.167-0.865 7.381 3.223 6.892 4.536-0.243 0.651-1.155 1.017-2.022 0.814-1.293-0.302-1.312-0.095-0.095 1.14 0.941 0.957 1.782 1.091 2.299 0.369 1.209-1.694 5.988-1.556 5.988 0.17 0 0.821-0.652 1.243-1.447 0.938-1.069-0.409-1.337 0.837-1.023 4.783 0.294 3.707 0.034 5.097-0.852 4.546-2.126-1.314-1.433 0.26 1.186 2.702l2.463 2.294-3.070 1.401c-2.283 1.039-2.694 1.666-1.601 2.44 0.976 0.693 0.421 0.873-1.656 0.541-1.72-0.274-2.901-0.132-2.625 0.314s-1.421 1.392-3.772 2.101c-2.349 0.708-6.117 2.813-8.37 4.676s-8.72 6.49-14.37 10.279c-8.299 5.565-10.826 7.959-13.147 12.452-1.581 3.059-3.185 5.82-3.562 6.135s-0.595 1.739-0.48 3.162c0.115 1.424-0.182 2.587-0.659 2.587s-1.494 1.94-2.258 4.31c-1.863 5.78-2.374 6.525-3.371 4.913-0.605-0.979-1.052-0.833-1.568 0.512-0.46 1.2-0.266 1.599 0.539 1.103 0.816-0.505 1.020-0.019 0.581 1.377-0.372 1.186-1.213 4.983-1.865 8.438-1.293 6.843-4.894 15.116-7.885 18.105-1.523 1.521-1.932 4.64-2.102 16.055-0.117 7.791 0.213 14.167 0.734 14.167s1.3 1.891 1.733 4.2c0.681 3.633 0.359 4.603-2.377 7.185-4.835 4.559-8.829 6.992-11.485 7.002-4.621 0.017-6.621 3.849-5.844 11.202 0.826 7.808 5.121 20.954 10.177 31.15 5.21 10.506 19.334 21.62 30.726 24.181 3.072 0.691 4.975 0.659 5.438-0.090 0.466-0.752 0.975-0.726 1.472 0.079 0.418 0.676 2.554 1.471 4.742 1.763 5.672 0.761 11.982 3.325 12.316 5.010 0.158 0.791-0.286 1.436-0.986 1.436-0.708 0-0.952 0.839-0.547 1.891 0.406 1.060-0.114 2.818-1.186 4.002-1.647 1.818-1.69 2.385-0.315 4.070 1.387 1.707 1.357 1.871-0.248 1.273-1.46-0.546-1.746-0.055-1.376 2.372 0.257 1.684 0.525 3.684 0.596 4.448s0.561 1.12 1.088 0.795c0.526-0.325 0.958-0.045 0.958 0.625 0 1.524 4.215 5.815 5.713 5.815 0.613 0 0.827 0.746 0.477 1.655-0.434 1.13 1.238 2.605 5.263 4.644 3.244 1.644 6.19 3.745 6.542 4.668s2.013 1.695 3.682 1.719c1.671 0.022 4.332 0.779 5.913 1.684s4.556 1.662 6.61 1.684c2.156 0.022 3.771 0.649 3.817 1.477 0.045 0.791 0.525 0.403 1.069-0.862s1.024-1.653 1.069-0.862c0.166 2.968 4.426 1.274 8.009-3.185 2.042-2.542 4.194-4.327 4.778-3.963s0.788-0.063 0.449-0.943c-0.737-1.916 2.343-4.77 4.025-3.731 0.709 0.438 0.91 0.007 0.498-1.063-0.507-1.319-0.183-1.602 1.213-1.068 1.046 0.402 1.647 0.987 1.333 1.299-0.777 0.777 2.299 3.745 3.885 3.745 2.643 0 14.982 7.197 14.982 8.739 0 0.885 0.905 1.599 2.013 1.59 1.686-0.017 1.74-0.19 0.321-1.087-0.93-0.588-1.381-1.381-1-1.762s1.618 0.234 2.748 1.364c1.469 1.469 2.194 1.648 2.532 0.631 0.262-0.785 1.006-1.426 1.652-1.426s0.879 0.775 0.514 1.724c-0.583 1.519-0.198 1.531 3.233 0.098 2.143-0.894 4.13-1.388 4.421-1.1 1.293 1.293 6.661 1.478 9.628 0.334 4.261-1.642 10.896-2.265 10.079-0.943-0.353 0.569 1.459 1.036 4.026 1.036s4.666 0.548 4.666 1.217c0 0.672 0.481 0.921 1.068 0.559 0.599-0.37 0.877 0.848 0.634 2.775-0.24 1.89 0.702 6.683 2.094 10.653l2.529 7.215 8.335 0.146c4.584 0.080 8.335-0.308 8.335-0.862 0-1.55 3.183-1.195 3.837 0.429 0.346 0.858 0.617 0.626 0.673-0.576 0.054-1.234 1.005-2.013 2.457-2.013 1.301 0 2.079-0.469 1.724-1.041s0.392-0.772 1.655-0.442c1.411 0.368 2.301 0.036 2.301-0.86 0-0.965 0.796-0.743 2.347 0.663 1.832 1.657 2.197 1.729 1.658 0.329-0.642-1.673 2.877-3.781 5.575-3.341 0.529 0.086 2.598-0.854 4.598-2.092s4.155-2.416 4.788-2.62c1.318-0.426 1.598-17.041 0.288-17.041-0.475 0-0.862-0.536-0.862-1.192 0-0.8 0.764-0.781 2.332 0.057 1.282 0.686 2.027 1.74 1.655 2.34s-0.127 1.091 0.543 1.091c1.383 0 1.669 3.004 0.356 3.74-0.475 0.265-0.231 0.331 0.541 0.144s2.295 0.546 3.384 1.625c1.87 1.852 1.938 1.851 1.226-0.029-0.51-1.347 0.007-2.404 1.613-3.26 2.101-1.123 2.281-0.996 1.603 1.133-0.991 3.12 0.265 3.046 3.173-0.189 1.418-1.574 2.074-1.872 1.678-0.757-0.356 1.006-0.13 2.557 0.505 3.45 0.945 1.332 1.048 1.293 0.576-0.208-0.694-2.196 0.123-3.468 3.375-5.274 1.707-0.948 2.803-0.992 3.649-0.144 1.545 1.545 16.543-1.25 15.529-2.892-0.421-0.683 0.493-0.821 2.356-0.353 1.899 0.477 3.046 0.292 3.046-0.49 0-0.689 1.163-1.103 2.587-0.921s3.175-0.033 3.899-0.483c0.723-0.449 3.236-1.063 5.582-1.363 6.271-0.802 13.161-4.288 13.195-6.674 0.019-1.174 1.729-2.841 4.052-3.949 2.214-1.056 4.023-2.323 4.023-2.818s0.775-0.602 1.724-0.24c0.95 0.365 1.724 0.154 1.724-0.468s2.574-2.875 5.718-5.010c5.581-3.786 5.719-4.019 5.748-9.685 0.018-3.193 0.651-6.427 1.409-7.185 1.072-1.072 1.072-1.38 0-1.38-0.757 0-1.38-0.775-1.38-1.724s0.646-1.731 1.437-1.742c0.991-0.007 1.046-0.281 0.177-0.862-0.695-0.464-2.404-8.606-3.803-18.089s-2.766-18.281-3.039-19.545c-0.274-1.264-0.2-1.652 0.163-0.862s1.522 1.398 2.576 1.347c1.204-0.056 1.381-0.306 0.473-0.675-0.795-0.32-1.103-1.137-0.683-1.812 0.429-0.695 0.221-0.897-0.476-0.466-0.679 0.419-2.091-0.093-3.137-1.14-1.362-1.362-1.524-2.133-0.571-2.724 0.948-0.585 0.849-1.007-0.345-1.467-1.719-0.659-3.546-7.199-3.447-12.323 0.029-1.58-0.329-3.135-0.8-3.449s-1.245-1.815-1.719-3.329c-0.75-2.406-0.553-2.675 1.57-2.119 1.337 0.349 2.43 0.166 2.43-0.406s-0.603-1.041-1.341-1.041c-0.74 0-1.007-0.334-0.601-0.74s1.625-0.019 2.711 0.862c1.086 0.88 1.61 0.993 1.164 0.253-0.474-0.791 1.291-3.409 4.258-6.324 4.592-4.511 5.142-5.647 5.874-12.161 0.445-3.954 1.356-7.525 2.022-7.938 0.711-0.441 0.875-0.202 0.394 0.577-0.452 0.728-0.274 1.327 0.397 1.327 1.574 0 1.555-1.5-0.039-3.095-1.113-1.113-0.543-5.788 1.903-15.587 0.118-0.475 0.279-2.674 0.359-4.887 0.089-2.515 0.72-4.023 1.676-4.023 0.844 0 1.533-0.752 1.533-1.673s-0.432-1.407-0.957-1.082c-1.125 0.697-1.313-4.555-0.202-5.666 0.416-0.416 1.55-0.114 2.521 0.673 1.406 1.139 1.589 1.138 0.899-0.006-0.615-1.025 0.146-1.437 2.654-1.437 3.445 0 4.647 1.466 2.366 2.875-0.634 0.392-1.149 1.296-1.149 2.010s1.056 0.166 2.344-1.217c2.195-2.356 3.366-2.515 18.3-2.515 15.862 0 20.116 0.524 18.993 2.34-0.323 0.524-0.060 1.274 0.581 1.671s1.167-0.135 1.167-1.186c0-1.755 2.769-1.84 34.203-1.078 22.443 0.546 35.663 0.426 38.455-0.349 3.109-0.864 4.398-0.823 4.794 0.147 0.312 0.764 0.582 0.577 0.635-0.435 0.096-1.855 5.673-5.697 8.317-5.726 4.62-0.052 4.975-1.546 5.668-23.797 0.37-11.881 1.083-22.786 1.586-24.235s0.457-2.554-0.098-2.45c-0.557 0.101-1.399 0.058-1.871-0.095s-1.251-0.363-1.724-0.461c-1.976-0.417-10.979-9.883-9.433-9.916 1.199-0.026 0.991-0.611-0.734-2.051-1.324-1.107-2.626-2.514-2.893-3.122s-1.691-3.050-3.167-5.421c-2.506-4.031-3.049-4.31-8.318-4.31-3.1 0-5.634-0.483-5.634-1.073s-1.424-0.8-3.162-0.469c-1.739 0.334-22.995 0.236-47.236-0.212l-44.074-0.818 0.386-6.238c0.353-5.712 0.161-6.294-2.269-6.904-1.779-0.445-2.435-1.24-1.986-2.412 0.57-1.488 0.394-1.517-1.194-0.199-2.862 2.375-6.372 2.476-8.089 0.233-1.272-1.657-1.741-1.742-2.586-0.46-0.813 1.233-2.844 1.371-9.507 0.652-4.661-0.505-9.222-0.774-10.137-0.602-1.45 0.274-1.45 0.051 0-1.733 1.087-1.339 1.156-1.742 0.197-1.166-0.805 0.484-4.635 1.091-8.508 1.349-4.387 0.291-7.835 1.186-9.148 2.372-1.591 1.442-2.482 1.591-3.659 0.615-1.183-0.982-1.553-0.896-1.553 0.366 0 0.911 0.432 1.388 0.957 1.063 1.193-0.739 1.272 1.147 0.096 2.322-0.475 0.475-0.862 1.458-0.862 2.188s-0.839 1.489-1.863 1.685c-1.94 0.374-3.711-3.279-3.359-6.938 0.105-1.106-0.356-2.014-1.028-2.014s-1.255 0.646-1.295 1.437c-0.068 1.334-1.615-1.701-2.197-4.31-0.142-0.634-0.724-2.024-1.297-3.093-0.774-1.447-0.386-2.401 1.519-3.738 1.407-0.986 2.090-1.794 1.515-1.794s-2.175 1.063-3.558 2.363c-2.212 2.079-2.599 2.137-3.221 0.514-0.411-1.072-0.233-1.551 0.428-1.143 0.625 0.386 1.138 0.189 1.138-0.438s-1.007-1.459-2.241-1.853c-1.317-0.418-1.986-1.377-1.62-2.324 0.488-1.271-0.445-1.522-4.371-1.186-2.745 0.236-4.058 0.169-2.914-0.149s1.86-1.179 1.596-1.916c-0.718-1.998 2.567-7.853 3.931-7.010 0.649 0.401 0.911 1.426 0.583 2.277-0.37 0.969 0 1.322 0.982 0.945 0.868-0.334 1.882-1.401 2.253-2.368 0.461-1.202 0.154-1.564-0.965-1.133-1.898 0.727-3.453-2.246-1.731-3.313 0.634-0.392 2.032-0.026 3.113 0.805 1.493 1.156 1.439 0.904-0.228-1.051-1.344-1.575-1.652-2.57-0.795-2.57 0.769 0 1.103-0.474 0.747-1.053s-0.134-1.225 0.497-1.437c0.632-0.212 1.328-1.95 1.55-3.858 0.486-4.189 2.704-8.599 4.325-8.599 0.647 0 0.857-0.517 0.464-1.152s0.613-1.654 2.231-2.27c3.099-1.178 3.13-1.232 3.368-5.775 0.083-1.58 0.469-3.135 0.858-3.45s1.832-2.791 3.205-5.502c1.375-2.709 2.959-4.642 3.523-4.293s1.401 0.026 1.858-0.711c0.558-0.902 0.331-1.037-0.691-0.405-1.12 0.694-1.319 0.409-0.751-1.067 0.425-1.105 0.777-2.351 0.789-2.776s0.561 0.005 1.225 0.957c0.945 1.35 1.050 1.163 0.479-0.862-0.401-1.424-0.286-2.587 0.255-2.587s1.105-1.682 1.254-3.738c0.375-5.176 4.916-15.807 6.753-15.807 0.831 0 1.514-0.517 1.514-1.149s-0.575-1.149-1.274-1.149c-0.7 0-0.996-0.721-0.659-1.598s-0.353-2.637-1.531-3.901c-1.18-1.267-1.832-2.618-1.449-2.999s0.095-0.697-0.644-0.697c-0.74 0-1.341-1.035-1.341-2.3s-0.675-2.3-1.498-2.3c-0.824 0-3.216-1.772-5.313-3.935-2.387-2.462-4.476-3.678-5.588-3.253-1.094 0.42-1.482 0.207-1.009-0.558 0.495-0.8-0.111-1.007-1.717-0.589-1.849 0.483-2.243 0.265-1.548-0.859 0.596-0.964 0.483-1.232-0.313-0.74-0.684 0.425-3.495 0.168-6.243-0.565-5.861-1.568-15.029-0.514-17.563 2.018-1.916 1.916-4.534 1.058-3.305-1.084 0.447-0.779 0.334-0.951-0.268-0.4-0.577 0.526-1.046 1.563-1.046 2.3s-0.517 1.387-1.149 1.446c-0.634 0.057-3.073 0.962-5.423 2.014s-4.938 1.913-5.748 1.918c-1.961 0.017-11.592 7.715-10.279 8.218 0.563 0.216 0.613 1.427 0.108 2.693-0.848 2.131-0.921 2.144-0.995 0.147-0.065-1.715-0.409-1.88-1.694-0.815-1.238 1.027-1.755 0.993-2.221-0.147-0.389-0.945-0.896-0.543-1.397 1.103-0.432 1.424-1.228 2.587-1.765 2.587s-0.681-0.775-0.317-1.724c1.055-2.748-0.512-2.024-4.719 2.183-2.149 2.149-4.245 3.574-4.657 3.162s-0.649-0.231-0.527 0.402c0.489 2.548-1.541 6.431-3.054 5.849-0.866-0.332-1.639-0.103-1.719 0.509-0.466 3.613-2.288 6.916-3.426 6.212-0.565-0.349-1.029-0.154-1.029 0.434s-2.069 1.402-4.598 1.805c-2.529 0.405-4.608 1.377-4.615 2.159-0.007 1.029-0.292 0.991-1.006-0.135-0.7-1.106-1.22-1.183-1.786-0.268-0.44 0.709-1.359 0.945-2.046 0.522-0.769-0.475-0.915-0.233-0.382 0.63 1.002 1.62-2.602 6.117-4.142 5.168-0.563-0.348-0.882-0.207-0.708 0.315 0.377 1.127-4.416 6.12-5.873 6.12-0.559 0-1.298 1.087-1.647 2.415s-1.158 2.090-1.8 1.692c-0.642-0.397-1.774 0.405-2.511 1.783-1.166 2.18-1.084 2.406 0.644 1.744 1.433-0.549 1.807-0.3 1.348 0.902-0.351 0.915 0.406 2.993 1.683 4.615 1.84 2.341 1.993 3.075 0.733 3.56-1.738 0.667-1.696 4.661 0.108 10.579 0.623 2.046 0.697 3.993 0.159 4.325s-1.317 2.154-1.733 4.048c-0.447 2.038-1.26 3.135-1.993 2.683-0.694-0.428-0.903-0.228-0.481 0.457 0.414 0.673 1.729 0.964 2.923 0.654s2.465-0.087 2.829 0.498c0.363 0.585-1.13 1.132-3.32 1.216s-3.976-0.224-3.976-0.677c0-0.454-5.861-0.668-13.029-0.474-10.081 0.274-13.072 0.017-13.205-1.149-0.095-0.826-0.509-3.259-0.922-5.404-0.531-2.765-0.369-3.666 0.558-3.093 0.719 0.444 1.303 0.241 1.303-0.447s-0.724-1.531-1.613-1.872c-1.964-0.752-3.284-5.014-1.421-4.586 0.72 0.166 0.914 0.081 0.433-0.185s-1.958-3.009-3.286-6.093c-1.327-3.086-3.003-5.495-3.725-5.363s-1.184-0.401-1.025-1.192c0.158-0.791-0.101-1.356-0.576-1.255s-1.774-0.214-2.886-0.702c-3.127-1.366-2.596-14.572 0.733-18.258 3.422-3.784 6.542-9.502 5.666-10.377-0.405-0.405-0.101-0.736 0.674-0.736s1.083-0.529 0.683-1.175c-0.4-0.646 0.172-3.88 1.271-7.185s1.87-6.166 1.718-6.357c-0.154-0.192 0.531-1.587 1.523-3.101 1.108-1.692 1.366-3.024 0.667-3.455-0.625-0.387-1.138-0.324-1.138 0.14s-1.357-0.411-3.014-1.942c-3.014-2.786-3.014-2.788-2.369-16.872 0.387-8.466 0.213-14.084-0.435-14.084-0.595 0-1.079 0.58-1.079 1.291 0 0.95-0.407 0.952-1.543 0.006-0.848-0.703-1.915-0.908-2.369-0.454-1.030 1.030-0.148-3.841 1.433-7.914 0.651-1.673 0.87-3.356 0.489-3.738s-0.117-0.695 0.588-0.695c0.88 0 1.274-4.926 1.257-15.747-0.020-13.098-0.363-16.243-2.038-18.683-1.107-1.615-3.118-5.341-4.469-8.286s-3.898-6.322-5.663-7.512c-4.583-3.089-11.544-6.109-14.072-6.109-1.191 0-2.338-0.455-2.548-1.009-0.216-0.569-1.637-0.54-3.258 0.071zM171.552-8.274c0 0.279-0.517 0.826-1.149 1.217s-1.149 0.163-1.149-0.507c0-0.672 0.517-1.217 1.149-1.217s1.149 0.228 1.149 0.507zM397.464 118.832c-0.392 0.634-1.327 1.142-2.080 1.132-0.822-0.006-0.68-0.462 0.356-1.132 2.209-1.427 2.606-1.427 1.724 0zM293.418 134.421c0 0.986-0.526 1.468-1.167 1.071s-0.87-1.203-0.507-1.794c0.986-1.596 1.675-1.297 1.675 0.723zM164.651 9.615c0 1.264 0.517 2.3 1.149 2.3s1.149-1.035 1.149-2.3c0-1.264-0.517-2.3-1.149-2.3s-1.149 1.035-1.149 2.3zM246.931 26.738c-0.349 0.565 0.111 1.315 1.024 1.666 1.107 0.425 1.446 0.082 1.020-1.029-0.743-1.932-1.162-2.063-2.043-0.637zM160.054 46.473c0 0.672 0.517 0.898 1.149 0.507s1.149-0.938 1.149-1.217c0-0.279-0.517-0.507-1.149-0.507s-1.149 0.548-1.149 1.217zM254.042 59.811c0.791 0.319 2.083 0.319 2.875 0s0.144-0.58-1.436-0.58c-1.581 0-2.227 0.26-1.436 0.58zM256.627 72.272c-0.825 0.993-0.937 1.724-0.264 1.724 0.642 0 1.466-0.775 1.83-1.724s0.483-1.724 0.265-1.724c-0.219 0-1.041 0.775-1.83 1.724zM394.589 169.418c0 0.634 0.548 1.149 1.217 1.149s0.898-0.517 0.507-1.149c-0.392-0.634-0.938-1.149-1.217-1.149s-0.507 0.517-0.507 1.149zM447.857 171.094c0 0.921 0.44 1.402 0.976 1.071s0.679-1.086 0.315-1.673c-0.93-1.503-1.293-1.334-1.293 0.603zM457.248 171.719c-0.392 0.634-0.163 1.149 0.507 1.149s1.217-0.517 1.217-1.149c0-0.634-0.228-1.149-0.507-1.149s-0.826 0.517-1.217 1.149zM588.976 216.079c0.054 1.339 0.327 1.613 0.697 0.697 0.332-0.83 0.292-1.822-0.091-2.202s-0.654 0.297-0.602 1.507zM496.603 275.282c0.406 1.553 0.927 2.077 1.216 1.215 0.276-0.829-0.044-2.055-0.711-2.723-0.853-0.852-1.003-0.404-0.503 1.507zM456.105 275.178c-0.404 0.654 0.058 0.845 1.082 0.452 1.993-0.766 2.356-1.589 0.702-1.589-0.596 0-1.399 0.511-1.783 1.138zM429.077 332.674c-1.037 0.672-1.178 1.122-0.356 1.133 0.752 0.005 1.69-0.5 2.080-1.133 0.882-1.427 0.483-1.427-1.724 0zM158.476 410.004c-1.036 3.865-0.899 4.308 1.292 4.207 1.186-0.053 1.427-0.327 0.583-0.667-1.020-0.411-1.102-1.188-0.285-2.713 0.655-1.22 0.704-2.409 0.123-2.769-0.562-0.346-1.333 0.526-1.711 1.942zM341.704 447.642c0 0.634 0.548 1.149 1.217 1.149s0.898-0.517 0.507-1.149c-0.392-0.633-0.938-1.149-1.217-1.149s-0.507 0.517-0.507 1.149z" horiz-adv-x="684" /> +<glyph unicode="" d="M160.077-6.438c-122.454 3.416-142.66 8.515-136.292 34.394 6.29 25.573 19.828 42.57 28.303 35.536 5.31-4.408 9.871-3.683 16.114 2.558 7.713 7.712 9.115 6.668 12.281-9.164 1.966-9.828 1.954-20.493-0.026-23.697-2.341-3.788 0.222-3.755 7.332 0.098 15.666 8.49 39.073 35.721 51.362 59.756 5.817 11.377 20.138 27.012 31.824 34.746l21.247 14.060-23.375 4.956c-12.857 2.726-36.523 8.63-52.594 13.123s-46.311 12.447-67.204 17.679c-50.724 12.702-87.656 29.511-87.656 39.892 0 13.32 16.646 16.449 23.123 4.346 3.174-5.93 10.446-12.613 16.16-14.849 21.423-8.387 105.231-20.667 141.095-20.675 36.309-0.007 37.138 0.336 34.475 14.274l-2.731 14.284 23.964-11.579c13.18-6.37 27.908-13.543 32.73-15.943s17.97-2.752 29.218-0.781c20.329 3.562 19.164 5.375-9.2 14.332-7.044 2.224-8.837 9.723-7.655 32.005 0.849 16.004-0.269 30.915-2.488 33.132-5.114 5.114-5.29 39.786-0.227 44.848 2.094 2.094 6.695 0.022 10.226-4.601 5.572-7.293 7.967-7.248 18.109 0.345 9.757 7.305 11.44 14.5 10.183 43.559-1.243 28.764 0.133 34.809 7.93 34.809 5.696 0 8.188 3.249 6.289 8.197-1.729 4.508 0.618 13.055 5.219 18.992 8.123 10.484 8.61 10.476 16.918-0.26 4.705-6.081 9.462-13.777 10.572-17.105s5.158-4.108 8.998-1.736c9.301 5.749 2.764 32.207-11.971 48.455-16.086 17.736-8.425 27.617 12.872 16.603 18.435-9.533 52.788-48.341 52.788-59.635 0-3.959 10.181-10.25 22.626-13.978 39.76-11.913 85.911 12.691 78.313 41.75-2.461 9.411 0.125 11.31 15.399 11.31 22.422 0 30.399-16.129 25.617-51.794-3.267-24.353 1.678-30.142 14.659-17.163 5.454 5.454 7.012 4.461 7.012-4.471 0-6.644-14.795-24.768-35.107-43.005-19.31-17.335-34.076-32.551-32.815-33.811 3.83-3.83 52.123 28.989 79.199 53.823 15.741 14.437 32.837 39.017 44.708 64.281 19.019 40.474 19.226 40.675 19.602 18.979 0.548-31.561-30.659-85.808-74.127-128.857-20.087-19.893-36.523-38.883-36.523-42.199 0-6.501-15.089-20.798-32.816-31.094-10.572-6.14-10.58-6.404-0.226-6.575 14.798-0.243 45.168-17.874 58.189-33.779 5.92-7.232 14.060-13.149 18.087-13.149 9.072 0 32.736-24.562 32.736-33.978 0-3.811 2.629-6.928 5.844-6.928s5.844-4.226 5.844-9.392c0-5.658 3.259-8.14 8.197-6.245 13.076 5.018 24.657-8.535 32.559-38.103 7.226-27.034 15.097-38.238 38.134-54.282 11.136-7.757 11.205-8.629 1.461-18.514-7.256-7.362-10.226-8.106-10.226-2.563 0 5.829-1.968 6.177-7.755 1.375-5.092-4.226-29.664-5.457-71.587-3.589-84.825 3.783-100.165 3.733-170.784-0.553-62.161-3.773-91.882-0.545-85.786 9.318 1.928 3.12 0.063 6.007-4.144 6.418s-13.164 1.347-19.904 2.081c-6.74 0.734-30.834 6.979-53.543 13.876s-45.594 10.89-50.853 8.87c-14.496-5.563-8.095-22.59 8.668-23.053 7.965-0.221 18.692-2.324 23.837-4.673s11.719-3.725 14.61-3.055c2.89 0.671 9.2-0.305 14.021-2.168s12.711-4.474 17.531-5.805c20.139-5.555-26.591-7.103-128.563-4.258zM582.29 27.447c19.372 18.030 21.497 33.084 4.026 28.516-7.438-1.946-13.286 0.086-15.292 5.312-2.37 6.175-5.276 6.776-10.761 2.224-5.434-4.509-8.242-4.146-10.139 1.312-1.443 4.151-2.357-5.818-2.029-22.156 0.697-34.75 9.157-38.513 34.197-15.207zM372.481 39.443c9.8 3.856 9.935 4.959 1.461 11.993-5.134 4.26-9.332 10.735-9.332 14.389 0 12.265-23.47 22.048-42.858 17.865-24.867-5.365-64.423-25.777-59.554-30.731 14.676-14.93 84.841-23.529 110.284-13.516zM267.842 78.389c11.229 8.493 11.229 8.578 0 8.5-19.060-0.134-37.691-6.798-34.308-12.272 4.792-7.755 21.496-5.918 34.308 3.772zM443.169 82.425c13.928 19.885 10.823 25.733-5.512 10.387-8.034-7.55-14.61-15.896-14.61-18.55 0-9.236 11.084-4.74 20.122 8.163z" horiz-adv-x="672" /> +<glyph unicode="" d="M379.013-3.925c0 4.118-2.505 5.507-9.143 5.070-15.715-1.036-24.104-0.007-24.104 2.959 0 6.010 26.902 33.070 34.865 35.069 4.596 1.154 8.356 3.443 8.356 5.087s5.245 6.232 11.657 10.195c6.411 3.962 10.767 8.646 9.678 10.406-1.257 2.034-3.447 1.983-6.004-0.139-6.579-5.46-30.79-8.206-33.512-3.801-3.863 6.25 0.918 19.157 11.604 31.327 8.55 9.738 9.173 11.63 4.432 13.45-3.035 1.164-10.75 0.391-17.144-1.719-6.618-2.185-19.645-3.024-30.246-1.949-13.418 1.361-20.942 0.494-26.932-3.104l-8.312-4.993 10.805 1.781c5.943 0.979 10.75 0.696 10.684-0.63-0.256-5.094-24.613-20.681-36.070-23.083l-12.017-2.519 33.124-3.406-14.899-7.195c-33.041-15.957-85.311-18.48-97.765-4.719-7.519 8.308-2.996 26.614 10.127 40.989 8.935 9.786 13.768 11.928 34.738 15.391 28.253 4.666 34.392 8.253 29.563 17.277-7.182 13.419-59.205 18.656-79.755 8.029-6.185-3.199-20.291-6.494-31.345-7.324-18.409-1.381-20.998-0.715-30.797 7.918-5.884 5.184-11.639 13.469-12.788 18.411-2.029 8.724-1.74 8.929 9.868 7.046 13.002-2.11 14.028-0.971 7.928 8.798-2.216 3.549-3.339 6.988-2.494 7.642 40.532 31.372 59.719 47.331 59.719 49.673 0 1.618 8.844 6.987 19.652 11.931 22.67 10.37 43.517 32.758 43.517 46.733 0 11.309-34.108 50.36-43.986 50.36-5.887 0-6.507-1.524-4.831-11.856 2.742-16.894-4.724-13.176-11.028 5.494-6.954 20.594-9.042 22.986-20.067 22.986-11.77 0-18.935-9.608-22.614-30.322l-2.586-14.561-5.78 10.938c-4.356 8.242-5.551 17.264-4.853 36.611 0.867 24.011 0.302 26.457-8.714 37.757-15.983 20.031-35.291 19.138-77.225-3.575-10.028-5.431-17.73-7.942-17.713-5.774 0.088 10.808 20.64 38.5 35.503 47.839 14.648 9.202 19.227 10.223 54.237 12.083 48.133 2.557 53.252 0.022 74.326-36.816 22.295-38.97 44.893-66.808 57.647-71.017 5.979-1.973 14.043-8.043 17.92-13.488 7.096-9.965 7.163-12.436 1.154-41.985-1.844-9.064-1.2-10.846 3.998-11.059 3.408-0.14 8.715-0.741 11.794-1.336s8.173 1.764 11.32 5.241c3.147 3.478 9.747 6.323 14.666 6.323s11.651 2.992 14.961 6.649c7.137 7.887 14.423 8.595 17.084 1.662 1.151-2.998 6.080-4.953 12.363-4.903 7.637 0.061 20.742 7.344 48.683 27.053 40.978 28.906 53.404 36.998 66.493 43.309 4.572 2.203 15.044 8.766 23.272 14.583s29.174 16.61 46.545 23.983c17.371 7.374 42.144 17.965 55.049 23.536s30.111 11.043 38.234 12.16l14.769 2.031-9.26-7.24c-5.093-3.981-12.17-12.943-15.727-19.916-9.215-18.063-42.167-52.584-55.519-58.162-27.826-11.627-57.502-44.285-57.477-63.251 0.006-3.748-1.861-8.705-4.146-11.014-3.346-3.381 3.293-3.783 34.078-2.064 21.029 1.175 40.182 2.748 42.563 3.496 7.938 2.496 0.27-5.375-13.751-14.117-7.617-4.749-16.095-10.742-18.837-13.317s-16.956-12.468-31.585-21.984c-14.629-9.516-26.362-18.798-26.074-20.626s7.471-5.824 15.964-8.878c8.492-3.055 17.702-7.43 20.466-9.724 4.475-3.714 4.456-4.489-0.172-7.079-9.34-5.226-5.75-17.868 10.072-35.465 8.398-9.341 13.635-16.983 11.636-16.983-6.144 0-4.055-9.478 5.158-23.4 8.689-13.13 8.848-13.202 13.754-6.197 4.121 5.883 7.375 6.815 19.168 5.486 7.813-0.88 20.797-4.793 28.854-8.693s19.132-7.091 24.611-7.091c13.249 0 24.723-15.279 25.559-34.034 0.572-12.817-0.303-14.293-12.685-21.396-23.365-13.404-74.641-10.055-107.684 7.033-10.266 5.309-37.481 31.513-55.255 53.204-14.151 17.269-22.566 18.934-49.43 9.781-14.35-4.889-15.577-6.134-13.935-14.13 0.99-4.822 2.195-23.291 2.675-41.041 1.147-42.342 2.114-41.347-49.827-51.305-8.778-1.683-10.805-1.007-10.805 3.601zM601.766 70.55c0 5.736-12.448 13.931-17.952 11.82-5.213-2-5.188-2.587 0.351-8.194 6.157-6.233 17.602-8.591 17.602-3.625z" horiz-adv-x="712" /> +<glyph unicode="" d="M347.842-102.861c-0.158 0.255 0.051 0.462 0.462 0.462s0.62-0.208 0.462-0.462c-0.158-0.255-0.366-0.462-0.462-0.462s-0.305 0.208-0.462 0.462zM469.684-102.544c0.334 0.134 0.733 0.116 0.887-0.037s-0.12-0.263-0.607-0.243c-0.54 0.021-0.649 0.131-0.279 0.279zM46.266-47.759c-0.699 0.766-0.711 0.886-0.089 0.886 0.398 0 0.595 0.206 0.438 0.459-0.306 0.497 0.349 1.963 0.849 1.895 0.812-0.11 0.946 0.005 0.406 0.349-0.327 0.207-0.461 0.723-0.298 1.146s0.503 0.641 0.755 0.486c0.254-0.157 0.197 0.21-0.124 0.813-0.535 1.005-0.519 1.043 0.195 0.453 0.681-0.562 0.743-0.547 0.489 0.115-0.2 0.524-0.042 0.76 0.503 0.76 0.592 0 0.728 0.268 0.541 1.040-0.139 0.574 0.060 1.563 0.444 2.198s0.581 0.782 0.441 0.323c-0.163-0.525-0.074-0.72 0.24-0.527 0.274 0.169 0.389 0.721 0.258 1.227-0.303 1.156 0.597 3.211 1.233 2.818 0.286-0.177 0.357 0.007 0.178 0.477-0.163 0.423-0.081 0.77 0.18 0.77s0.358 0.189 0.215 0.421c-0.349 0.565 0.206 1.947 0.667 1.661 0.201-0.125 0.489 0.096 0.64 0.49 0.156 0.405 0.079 0.595-0.177 0.436s-0.333 0.031-0.176 0.439c0.151 0.395 0.474 0.719 0.717 0.719s0.324 0.189 0.18 0.421c-0.367 0.595 0.218 1.939 0.711 1.634 0.225-0.139 0.285 0.070 0.133 0.465-0.188 0.49 0 0.721 0.583 0.725 0.724 0.004 0.75 0.079 0.166 0.457-0.505 0.326-0.528 0.45-0.089 0.457 0.334 0.003 0.489 0.197 0.346 0.428-0.401 0.651 0.24 1.925 0.795 1.582 0.309-0.192 0.383-0.018 0.197 0.465-0.163 0.423-0.081 0.77 0.18 0.77s0.358 0.189 0.215 0.421c-0.367 0.595 0.218 1.939 0.711 1.634 0.225-0.139 0.285 0.070 0.133 0.465-0.188 0.49 0 0.721 0.583 0.725 0.724 0.004 0.75 0.079 0.166 0.457-0.505 0.326-0.528 0.45-0.089 0.457 0.334 0.003 0.477 0.215 0.321 0.47-0.255 0.413 0.338 0.659 1.255 0.524 0.181-0.026 0.205 0.277 0.051 0.678-0.227 0.592-0.359 0.613-0.707 0.11-0.674-0.97 0.086 1.443 1.063 3.382 0.467 0.927 1.742 2.466 2.832 3.423s1.67 1.57 1.289 1.366c-0.381-0.204-0.118 0.249 0.584 1.006s1.121 1.551 0.925 1.765c-0.194 0.214-0.125 0.259 0.154 0.101s1.633 0.767 3.007 2.059c1.376 1.292 2.153 2.108 1.73 1.813-0.457-0.317-0.877-0.36-1.037-0.103-0.263 0.428 0.442 1.238 1.342 1.539 0.254 0.085 0.413 0.303 0.353 0.486s0.2 0.212 0.579 0.065c0.419-0.161 0.688 0.003 0.688 0.425 0 0.623 1.25 1.892 4.754 4.824 0.775 0.649 1.152 1.359 1.037 1.957-0.1 0.515-0.037 0.91 0.138 0.875 0.701-0.14 1.938 0.683 1.938 1.291 0 0.354 0.221 0.644 0.49 0.644s0.362-0.208 0.204-0.462c-0.644-1.045 0.706-0.366 1.701 0.855 0.593 0.724 2.012 1.938 3.156 2.698 3.204 2.125 3.43 2.235 3.038 1.493-0.189-0.357 0.087-0.129 0.614 0.507s0.96 1.010 0.965 0.831c0.004-0.18 0.498 0.163 1.094 0.76 0.805 0.805 1.421 1.021 2.382 0.837 0.913-0.175 1.199-0.089 0.965 0.288-0.183 0.295-0.578 0.414-0.879 0.262s-0.067 0.147 0.517 0.663c0.584 0.514 1.344 0.83 1.688 0.698s0.782 0.016 0.974 0.326c0.224 0.362 0.157 0.447-0.184 0.236-0.293-0.182-0.534-0.221-0.534-0.087s0.412 0.462 0.915 0.732c0.772 0.413 0.876 0.363 0.662-0.332-0.139-0.451 0.057-0.301 0.435 0.336s0.829 1.091 0.999 1.010c0.171-0.081 0.619 0.233 0.998 0.695 0.406 0.498 0.561 0.558 0.38 0.147-0.168-0.381 0.147-0.174 0.7 0.462s0.865 0.844 0.695 0.462c-0.171-0.381 0.141-0.174 0.695 0.462s0.871 0.896 0.706 0.579c-0.165-0.32-0.026-0.58 0.306-0.585 0.445-0.003 0.421-0.137-0.088-0.498-0.617-0.436-0.617-0.469 0-0.297 0.397 0.111 0.635 0.603 0.558 1.151-0.076 0.526 0.112 1.111 0.418 1.3 0.368 0.228 0.435 0.147 0.198-0.236-0.284-0.459-0.147-0.467 0.644-0.043 0.748 0.4 0.924 0.788 0.689 1.53-0.296 0.933-0.269 0.945 0.474 0.204 0.89-0.89 1.877-1.072 1.371-0.252-0.227 0.368-0.017 0.454 0.665 0.274 0.548-0.144 0.998-0.058 0.998 0.19s0.441 0.311 0.984 0.138c0.706-0.224 0.885-0.154 0.636 0.25-0.252 0.409-0.063 0.471 0.695 0.231 0.775-0.246 0.95-0.182 0.68 0.255-0.263 0.426-0.091 0.514 0.636 0.324 0.548-0.144 0.998-0.058 0.998 0.19s0.441 0.311 0.984 0.138c0.706-0.224 0.885-0.154 0.636 0.25s-0.070 0.474 0.636 0.25c0.912-0.288 1.425 0.351 0.637 0.793-0.453 0.253 0.594-0.029 1.581-0.426 0.522-0.211 0.661-0.135 0.435 0.231-0.203 0.327-0.039 0.542 0.414 0.542 0.448 0 0.617-0.214 0.418-0.533-0.206-0.334-0.127-0.409 0.212-0.199 0.297 0.185 0.431 0.514 0.295 0.733s-0.049 0.522 0.192 0.67c0.241 0.149 0.569 0.060 0.728-0.199s0.499-0.47 0.754-0.47c0.254 0 0.321 0.231 0.147 0.512-0.213 0.345 0.004 0.411 0.665 0.2 0.609-0.193 0.984-0.112 0.984 0.212s0.375 0.406 0.984 0.212c0.728-0.231 0.888-0.159 0.618 0.279-0.257 0.416-0.146 0.507 0.375 0.307 0.407-0.156 0.823-0.036 0.925 0.269 0.138 0.411 0.382 0.39 0.955-0.083 0.665-0.549 0.769-0.527 0.769 0.166 0 0.522 0.242 0.707 0.695 0.533 0.381-0.147 0.695-0.050 0.695 0.212 0 0.271 0.407 0.351 0.938 0.182 0.514-0.163 0.907-0.125 0.87 0.084-0.162 0.933 0.095 1.146 0.738 0.614 0.545-0.452 0.695-0.454 0.695-0.005 0 0.313 0.299 0.454 0.663 0.314s0.748-0.005 0.85 0.298c0.138 0.411 0.382 0.39 0.955-0.083 0.716-0.592 0.983-0.399 0.805 0.577-0.040 0.221 0.248 0.281 0.639 0.13s0.714-0.050 0.714 0.224c0 0.274 0.323 0.374 0.719 0.221 0.469-0.18 0.602-0.089 0.387 0.26-0.224 0.362-0.071 0.437 0.463 0.231 0.437-0.168 0.681-0.123 0.544 0.101-0.335 0.543 0.773 1.415 2.37 1.868 0.716 0.202 1.251 0.603 1.186 0.894s0.078 0.474 0.314 0.411c0.237-0.063 0.773 0.301 1.192 0.81s0.611 0.634 0.424 0.276c-0.226-0.436-0.155-0.538 0.215-0.308 0.331 0.204 0.425 0.745 0.236 1.341-0.284 0.892-0.234 0.93 0.459 0.356 0.707-0.586 0.733-0.533 0.298 0.613-0.602 1.587-0.599 1.788 0.036 1.397 0.349-0.215 0.407 0.017 0.181 0.724-0.208 0.654-0.158 0.935 0.133 0.755 0.659-0.407 1.077 0.954 0.724 2.358-0.186 0.743-0.125 1.082 0.163 0.903 0.255-0.158 0.697 0.228 0.983 0.857 0.444 0.974 0.403 1.144-0.279 1.158-0.756 0.015-0.756 0.047 0.004 0.623 0.442 0.335 0.656 0.757 0.476 0.937s-0.035 0.508 0.325 0.728c0.559 0.345 0.558 0.518-0.015 1.217-0.595 0.73-0.577 0.783 0.176 0.5 0.694-0.26 0.786-0.144 0.529 0.665-0.224 0.703-0.153 0.882 0.251 0.634s0.474-0.070 0.25 0.637c-0.172 0.543-0.111 0.983 0.138 0.983s0.333 0.448 0.19 0.998c-0.19 0.726-0.102 0.899 0.324 0.637 0.436-0.269 0.5-0.096 0.255 0.68-0.24 0.757-0.177 0.945 0.232 0.695 0.401-0.248 0.475-0.072 0.256 0.618-0.448 1.413 0.514 2.963 1.623 2.611 0.541-0.171 0.898-0.068 0.898 0.259 0 0.3 0.313 0.423 0.695 0.277s0.695-0.042 0.695 0.231c0 0.274 0.323 0.374 0.719 0.221 0.465-0.178 0.601-0.089 0.391 0.255-0.222 0.361-0.006 0.447 0.669 0.269 0.548-0.144 0.998-0.058 0.998 0.19s0.44 0.311 0.983 0.138c0.728-0.231 0.888-0.159 0.618 0.279-0.26 0.42-0.143 0.507 0.405 0.295 0.508-0.195 0.77-0.059 0.77 0.4 0 0.565 0.183 0.598 0.982 0.171 0.689-0.369 0.887-0.374 0.666-0.017-0.173 0.279-0.095 0.509 0.176 0.509s0.496-0.26 0.506-0.579c0.006-0.337 0.285-0.222 0.662 0.277 0.357 0.471 0.87 0.719 1.142 0.55s0.493-0.063 0.493 0.235c0 0.297 0.313 0.42 0.695 0.274s0.695-0.055 0.695 0.202c0 0.257 0.26 0.416 0.578 0.351s1.031 0.134 1.585 0.442c0.556 0.307 1.369 0.42 1.811 0.25 0.593-0.226 0.703-0.147 0.424 0.305-0.285 0.461-0.131 0.548 0.618 0.353 0.548-0.144 0.998-0.079 0.998 0.144s2.551 0.305 5.668 0.182c16.876-0.663 38.625-0.901 39.799-0.433 0.448 0.178 1.75 0.185 2.892 0.015 1.407-0.212 2.077-0.144 2.077 0.206 0 0.316 0.945 0.515 2.43 0.514 1.337 0 3.782 0.103 5.437 0.233s4.212 0.233 5.683 0.228c1.864-0.003 2.577 0.151 2.355 0.514-0.211 0.34-0.055 0.418 0.447 0.225 0.423-0.163 0.77-0.082 0.77 0.18s0.208 0.346 0.463 0.189c0.255-0.158 0.463-0.053 0.463 0.231s0.208 0.389 0.463 0.231c0.255-0.158 0.463-0.053 0.463 0.231s0.208 0.389 0.463 0.231c0.255-0.158 0.465-0.015 0.47 0.32 0.004 0.446 0.138 0.423 0.503-0.089 0.403-0.563 0.456-0.507 0.28 0.301-0.177 0.817-0.022 0.973 0.864 0.875 0.595-0.065 1.037 0.112 0.986 0.394s0.387 0.461 0.974 0.397c0.748-0.081 0.964 0.058 0.721 0.462-0.276 0.461-0.204 0.462 0.363 0.003 0.595-0.48 0.799-0.378 1.256 0.623 0.349 0.766 0.792 1.104 1.231 0.937 0.375-0.144 0.683-0.048 0.683 0.214s0.186 0.36 0.414 0.219c0.227-0.142 0.542 0.077 0.698 0.483s0.582 0.623 0.951 0.483c0.472-0.182 0.583-0.026 0.386 0.53-0.154 0.433-0.116 0.637 0.084 0.455s0.807-0.118 1.346 0.142c0.953 0.459 0.955 0.452 0.082-0.248-0.494-0.396-0.697-0.72-0.45-0.72s0.995 0.623 1.666 1.387c0.67 0.764 1.498 1.381 1.838 1.371s0.146-0.279-0.435-0.601c-0.58-0.323-0.921-0.721-0.755-0.887 0.356-0.356 1.801 0.988 1.832 1.705 0.014 0.274 0.252 0.45 0.533 0.395s0.793 0.244 1.137 0.666c0.438 0.541 0.452 0.664 0.044 0.42-0.983-0.592-0.639 0.286 0.633 1.614 0.746 0.779 1.5 1.161 1.966 0.998 0.611-0.214 0.646-0.146 0.188 0.351-0.457 0.492-0.345 0.91 0.559 2.093 0.62 0.813 0.981 1.642 0.801 1.841s-0.119 0.25 0.135 0.111c0.659-0.358 2.545 0.433 2.545 1.070 0 0.386-0.193 0.374-0.695-0.041-0.399-0.332-0.695-0.377-0.695-0.106 0 0.259 0.325 0.596 0.723 0.747s0.596 0.485 0.439 0.738c-0.156 0.253 0 0.352 0.345 0.219 0.349-0.134 1.295 0.489 2.117 1.393 0.818 0.899 1.342 1.635 1.163 1.635s-0.928-0.676-1.668-1.503l-1.342-1.503 1.007 1.522c0.555 0.837 1.515 1.856 2.137 2.264 1.009 0.661 1.098 0.959 0.828 2.762-0.193 1.293-0.107 2.257 0.24 2.676 0.332 0.399 0.426 1.278 0.243 2.253-0.2 1.068-0.055 2.070 0.435 3.019 0.847 1.637 1.019 3.051 0.236 1.931-0.357-0.51-0.409-0.204-0.197 1.158 0.159 1.019 0.267 2.943 0.24 4.281-0.031 1.513 0.147 2.43 0.468 2.43 0.358 0 0.42 0.818 0.2 2.66-0.228 1.921-0.657 3.041-1.537 4.031-0.672 0.753-0.866 1.103-0.436 0.774 0.675-0.512 0.774-0.469 0.716 0.314-0.038 0.5-0.3 0.821-0.58 0.712-0.739-0.284-2.233 1.519-1.817 2.192 0.208 0.337 0.156 0.444-0.133 0.266-0.577-0.356-1.647 0.791-1.175 1.262 0.18 0.18 0.022 0.219-0.351 0.086-0.981-0.349-2.474 0.923-2.131 1.815 0.156 0.407 0.102 0.627-0.12 0.49-0.524-0.324-1.532 0.877-1.098 1.312 0.18 0.18 0.007 0.206-0.38 0.057-0.887-0.341-2.491 1.344-2.014 2.115 0.208 0.337 0.156 0.444-0.133 0.266-0.577-0.356-1.647 0.791-1.175 1.262 0.18 0.18 0.022 0.219-0.351 0.086-0.911-0.325-2.478 0.911-2.17 1.714 0.139 0.363-0.044 0.639-0.425 0.639-0.399 0-0.564-0.274-0.408-0.682 0.215-0.561 0.137-0.579-0.438-0.102-0.385 0.319-0.565 0.798-0.401 1.061s0.087 0.352-0.17 0.193c-0.507-0.313-2.62 1.675-2.214 2.082 0.135 0.135-0.11 0.538-0.545 0.892-0.703 0.574-0.755 0.546-0.464-0.228 0.307-0.817 0.26-0.827-0.723-0.153-0.578 0.396-1.006 0.925-0.95 1.175s-0.215 0.578-0.601 0.726c-0.387 0.148-0.597 0.099-0.468-0.111s-0.057-0.38-0.414-0.38c-0.507 0-0.542 0.211-0.154 0.964 0.411 0.802 0.389 0.865-0.13 0.374-0.511-0.483-0.781-0.399-1.485 0.462-0.474 0.579-0.723 0.741-0.556 0.359 0.271-0.617 0.238-0.617-0.308 0-0.338 0.381-0.896 0.753-1.239 0.827-1.126 0.238-6.109 3.841-5.863 4.24 0.132 0.214-0.082 0.512-0.476 0.663-0.462 0.177-0.599 0.087-0.388-0.255 0.208-0.336 0.12-0.401-0.239-0.18-0.311 0.192-0.453 0.64-0.317 0.996s0.047 0.644-0.2 0.644-0.447-0.212-0.447-0.471c0-0.271-0.49-0.349-1.157-0.182-0.747 0.188-1.157 0.096-1.157-0.26 0-0.375-0.46-0.448-1.446-0.233-1.092 0.24-1.359 0.176-1.090-0.259 0.271-0.44-0.044-0.502-1.331-0.26-1.148 0.215-1.686 0.147-1.686-0.218 0-0.334-0.42-0.428-1.127-0.251-0.62 0.156-1.257 0.074-1.415-0.182-0.159-0.259-1.15-0.358-2.227-0.226-1.429 0.177-1.695 0.125-1.013-0.198 0.756-0.358 0.784-0.444 0.149-0.47-0.428-0.018-1.054 0.244-1.392 0.582s-0.875 0.529-1.19 0.425c-0.315-0.105-0.576 0.026-0.576 0.295s-0.313 0.368-0.695 0.221c-0.381-0.147-0.695-0.053-0.695 0.207s0.26 0.579 0.578 0.706c0.417 0.168 0.425 0.243 0.026 0.27-0.303 0.019-0.679-0.171-0.838-0.426s-0.494-0.462-0.749-0.462c-0.255 0-0.321 0.231-0.147 0.512 0.215 0.349-0.017 0.407-0.724 0.182-0.704-0.224-0.94-0.166-0.727 0.176 0.196 0.317 0.045 0.404-0.405 0.231-0.395-0.151-0.719-0.051-0.719 0.221s-0.286 0.389-0.635 0.255c-0.349-0.134-0.764 0.094-0.923 0.507s-0.655 0.714-1.102 0.666c-1.73-0.176-2.938 0.043-2.663 0.486 0.325 0.527 0.362 0.518-2.849 0.62-1.314 0.041-2.27-0.11-2.131-0.336 0.144-0.233-0.526-0.276-1.548-0.099-1.382 0.24-1.641 0.192-1.107-0.205 0.549-0.409 0.428-0.46-0.578-0.24-0.799 0.175-1.274 0.084-1.274-0.243 0-0.325-0.435-0.413-1.157-0.231-0.691 0.174-1.157 0.092-1.157-0.202 0-0.313-0.637-0.387-1.736-0.205-1.369 0.226-1.589 0.174-1.040-0.25 0.556-0.428 0.391-0.484-0.81-0.277-0.861 0.148-1.504 0.063-1.504-0.201 0-0.26-0.551-0.339-1.274-0.182-0.981 0.214-1.113 0.164-0.578-0.215 0.599-0.425 0.583-0.494-0.115-0.5-0.446-0.003-0.81 0.171-0.81 0.389 0 0.475 3.173 4.693 3.531 4.693 0.137 0-0.43-0.858-1.261-1.906-1.161-1.466-1.277-1.761-0.501-1.277 0.556 0.346 1.007 0.866 1.007 1.156s0.292 0.767 0.645 1.063c0.356 0.295 0.547 0.916 0.425 1.381-0.135 0.519 0.086 0.962 0.577 1.15 0.703 0.27 0.721 0.209 0.153-0.514-0.585-0.747-0.571-0.763 0.172-0.18 0.634 0.496 0.697 0.785 0.282 1.283-0.419 0.507-0.389 0.587 0.139 0.385 0.37-0.142 1.071 0.18 1.555 0.718s0.68 0.908 0.433 0.826c-0.248-0.082-0.488 0.058-0.535 0.313s0.18 0.409 0.505 0.344c0.324-0.065 0.987 0.351 1.473 0.925 0.877 1.036 0.877 1.038-0.047 0.351-0.512-0.381 0.216 0.39 1.619 1.715s2.782 2.332 3.065 2.236c0.284-0.095 0.492 0.175 0.463 0.598s0.156 0.771 0.411 0.771c0.255 0 0.395-0.202 0.313-0.449s0.246-0.091 0.732 0.348c0.485 0.439 0.987 0.694 1.113 0.565s0.233 0.081 0.233 0.464c0 0.582-0.157 0.563-0.964-0.115l-0.964-0.812 0.896 1.057c0.493 0.58 1.18 0.949 1.529 0.815s0.524-0.067 0.391 0.147c-0.132 0.214 0.323 0.889 1.013 1.498l1.255 1.108-1.844-2.313 1.040 0.882c0.575 0.485 1.053 1.109 1.068 1.387s0.69 1.014 1.504 1.634l1.477 1.127-1.355-1.47c-0.745-0.808-1.259-1.565-1.142-1.683s1.222 1.038 2.46 2.564c1.502 1.856 2.478 2.688 2.943 2.511 0.528-0.203 0.576-0.123 0.194 0.336-0.369 0.445-0.254 0.993 0.442 2.093 0.519 0.82 1.039 1.49 1.157 1.49s0.094-0.193-0.053-0.432c-0.147-0.236-0.073-0.55 0.163-0.697s0.372 0.26 0.302 0.904c-0.071 0.643 0.083 1.301 0.344 1.463 0.298 0.183 0.327 0.051 0.081-0.358-0.333-0.555-0.276-0.557 0.392-0.016 0.859 0.698 1.090 1.69 0.274 1.187-0.368-0.226-0.371-0.058-0.017 0.605 0.27 0.507 0.375 1.038 0.232 1.18s0.136 0.156 0.621 0.029c0.628-0.164 1.042 0.12 1.437 0.986 0.38 0.832 0.393 1.118 0.041 0.901-0.365-0.226-0.362-0.035 0.014 0.663 0.288 0.541 0.635 0.981 0.768 0.981s0.094-0.24-0.088-0.533c-0.209-0.338-0.126-0.409 0.226-0.19 0.426 0.264 0.43 0.497 0.016 0.996-0.298 0.358-0.388 0.654-0.201 0.654s-0.017 0.546-0.453 1.214c-0.544 0.831-0.597 1.111-0.169 0.892 0.345-0.177 0 0.236-0.762 0.921-0.763 0.683-1.076 1.099-0.695 0.925s0.301 0.003-0.181 0.395c-0.482 0.392-1.048 0.771-1.259 0.844s-0.446 0.548-0.524 1.058c-0.077 0.509-0.298 0.971-0.493 1.024-0.454 0.127-1.708 1.378-1.708 1.705 0 0.137 0.346 0.116 0.77-0.046 0.518-0.199 0.66-0.118 0.435 0.246-0.185 0.298-0.53 0.542-0.77 0.542s-1.727 1.301-3.306 2.892c-1.581 1.59-2.464 2.583-1.966 2.205 0.887-0.673 0.887-0.665 0.055 0.313-0.487 0.574-1.234 0.925-1.747 0.826-0.493-0.095-0.774 0.023-0.625 0.266s0.050 0.439-0.219 0.439c-0.269 0-0.439 0.161-0.375 0.356s-0.353 0.953-0.925 1.683c-0.575 0.729-1.040 1.156-1.040 0.945s0.433-0.839 0.96-1.402c0.619-0.659 0.785-1.13 0.463-1.328-0.274-0.169-0.498-0.067-0.498 0.226 0 0.577-1.69 2.898-1.946 2.67-0.198-0.178-3.942 2.81-5.041 4.026-0.461 0.509-1.315 1.456-1.899 2.105s-0.751 0.752-0.371 0.231c0.558-0.765 0.447-0.733-0.564 0.161-0.689 0.611-1.123 1.321-0.962 1.582 0.179 0.291-0.016 0.356-0.501 0.17-0.437-0.168-0.684-0.127-0.549 0.091s-0.215 0.788-0.778 1.267c-1.010 0.86-1.013 0.86-0.345-0.054s0.665-0.914-0.345-0.054c-0.562 0.479-0.911 1.053-0.774 1.274s0.026 0.426-0.243 0.459c-0.269 0.033-0.687 0.084-0.93 0.115s-0.71 0.36-1.040 0.729c-0.524 0.583-0.482 0.588 0.324 0.021 0.751-0.527 0.82-0.53 0.359-0.015-0.597 0.673-2.905 0.982-2.905 0.391 0-0.192-0.346-0.214-0.77-0.053-0.518 0.198-0.66 0.119-0.435-0.246 0.443-0.717-0.306-0.697-1.030 0.029-0.452 0.452-0.622 0.368-0.826-0.405-0.302-1.15-2.29-1.358-2.703-0.284-0.19 0.495-0.659 0.614-1.648 0.417-0.761-0.152-1.381-0.080-1.381 0.161s-0.313 0.316-0.695 0.171c-0.381-0.147-0.695-0.038-0.695 0.24s-0.364 0.476-0.81 0.439c-0.446-0.037-1.214-0.036-1.708 0.004-0.888 0.071-0.888 0.081 0 0.77 0.838 0.651 0.811 0.665-0.404 0.232-1.617-0.578-2.302-0.595-1.963-0.046 0.143 0.231-0.19 0.303-0.739 0.159-0.727-0.19-0.901-0.102-0.637 0.324 0.267 0.431 0.102 0.503-0.621 0.274-0.75-0.238-0.992-0.113-1.020 0.529-0.020 0.486-0.135 0.597-0.27 0.262-0.129-0.317-0.446-0.578-0.706-0.578s-0.353 0.313-0.207 0.695c0.147 0.381 0.065 0.695-0.181 0.695s-0.447-0.208-0.447-0.463c0-0.255-0.325-0.463-0.721-0.463s-0.601 0.194-0.457 0.43c0.349 0.565-1.258 1.060-2.233 0.686-0.567-0.218-0.691-0.101-0.478 0.452 0.157 0.411 0.073 0.746-0.188 0.746s-0.56-0.259-0.665-0.576c-0.137-0.412-0.418-0.348-0.996 0.232-0.882 0.882-1.422 1.073-0.971 0.344 0.157-0.255 0.048-0.463-0.241-0.463-0.341 0-0.423 0.475-0.232 1.344 0.175 0.796 0.086 1.473-0.218 1.661-0.327 0.203-0.401 1.050-0.206 2.357 0.221 1.482 0.149 2.042-0.268 2.042-0.421 0-0.494 0.612-0.272 2.281 0.198 1.49 0.123 2.39-0.216 2.599-0.334 0.207-0.411 1.040-0.214 2.348 0.201 1.343 0.133 2.028-0.201 2.028-0.298 0-0.495 0.902-0.481 2.198 0.016 1.209-0.032 2.781-0.101 3.494l-0.126 1.297-11.894-0.279 0.298 2.621c0.199 1.76 0.125 2.729-0.231 2.947s-0.425 1.142-0.216 2.817c0.243 1.939 0.166 2.547-0.348 2.744-0.392 0.15-0.579 0.672-0.463 1.279 0.137 0.719-0.236 1.411-1.24 2.308-0.793 0.706-1.295 1.513-1.12 1.797 0.181 0.293 0.112 0.387-0.163 0.219-0.599-0.37-1.631 0.795-1.161 1.313 0.189 0.208 0.089 0.234-0.219 0.057-0.714-0.411-2.588 1.422-2.161 2.113 0.182 0.294 0.112 0.389-0.162 0.219-0.599-0.37-1.631 0.795-1.161 1.314 0.189 0.208 0.101 0.24-0.195 0.070-0.478-0.274-11.262 10.028-11.261 10.757 0 0.151-1.805 0.24-4.011 0.198-3.115-0.061-4.242 0.095-5.041 0.694-0.566 0.425-0.935 0.928-0.82 1.12s-0.004 0.346-0.265 0.346c-0.261 0-0.343 0.346-0.18 0.77 0.216 0.563 0.129 0.666-0.325 0.387s-0.541-0.176-0.325 0.387c0.162 0.423 0.1 0.77-0.138 0.77s-0.323 0.287-0.189 0.639c0.135 0.352-0.287 1.215-0.941 1.918-1.355 1.46-1.402 1.655-0.606 2.496 0.478 0.507 0.379 0.512-0.579 0.029-0.636-0.32-1.003-0.417-0.814-0.214s0.107 0.653-0.181 0.998c-0.288 0.348-0.392 0.765-0.231 0.926s-0.173 0.539-0.743 0.837c-0.571 0.298-0.97 0.838-0.885 1.199s-0.004 0.495-0.204 0.298c-0.419-0.419-4.41 3.354-4.091 3.868 0.321 0.518-2.136 2.309-2.695 1.964-0.279-0.173-0.34-0.071-0.146 0.245 0.184 0.298 0.561 0.401 0.838 0.231 0.302-0.187 0.368-0.095 0.168 0.231-0.184 0.298-0.495 0.443-0.69 0.322s-1.152 0.596-2.126 1.593c-0.974 0.996-1.996 1.685-2.272 1.526s-0.346-0.112-0.151 0.101c0.193 0.212-0.114 0.923-0.683 1.579s0.227-0.017 1.77-1.494c1.623-1.555 2.804-2.393 2.804-1.986 0 0.384-0.14 0.613-0.313 0.506-0.489-0.302-3.296 2.796-2.896 3.195 0.195 0.195 0.123 0.286-0.162 0.2-0.694-0.206-4.428 2.804-4.112 3.314 0.426 0.689-0.499 0.919-1.984 0.493-1.144-0.327-1.376-0.281-1.174 0.243 0.194 0.505-0.063 0.588-1.13 0.375-0.784-0.156-1.513-0.063-1.686 0.218-0.172 0.279-0.902 0.375-1.675 0.22-0.901-0.18-1.37-0.086-1.37 0.274 0 0.302 0.239 0.4 0.53 0.22 0.374-0.231 0.439 0.036 0.222 0.898-0.169 0.675-0.118 1.226 0.114 1.226s0.553 0.342 0.713 0.76c0.225 0.585 0.153 0.644-0.314 0.258-0.474-0.393-0.601-0.232-0.59 0.744 0.007 0.796-0.206 1.2-0.597 1.12-0.392-0.082-0.48 0.085-0.244 0.463 0.575 0.917 0.599 1.784 0.043 1.441-0.329-0.204-0.386-0.013-0.166 0.562 0.183 0.479 0.192 0.728 0.018 0.556s-0.757-0.088-1.297 0.191c-0.761 0.393-0.677 0.182 0.379-0.942 0.747-0.796 1.163-1.444 0.925-1.437-0.511 0.014-3.004 3.281-2.936 3.851 0.025 0.214-0.497 0.957-1.162 1.652s-1.050 1.437-0.855 1.651c0.194 0.214 0.083 0.233-0.248 0.042-0.49-0.283-0.491-0.49-0.005-1.114 0.418-0.542 0.287-0.509-0.441 0.112-1.065 0.909-1.448 1.975-0.499 1.388 0.334-0.206 0.409-0.127 0.199 0.212-0.184 0.298-0.505 0.437-0.713 0.307-0.456-0.282-7.501 7.378-7.319 7.957 0.147 0.468-2.186 3.368-2.709 3.368-0.194 0 0.261-0.686 1.010-1.524s0.466-0.631-0.631 0.464c-1.095 1.093-1.888 2.16-1.758 2.368 0.251 0.407-0.763 1.503-3.524 3.808-0.948 0.791-1.675 1.672-1.62 1.958s-0.183 0.53-0.534 0.542c-0.781 0.024-2.264 2.104-1.501 2.104 0.285 0 0.519-0.221 0.519-0.49s0.206-0.364 0.457-0.207c0.271 0.168 0.338-0.056 0.163-0.55-0.24-0.681-0.18-0.727 0.329-0.255 0.741 0.687 1.538 0.773 1.137 0.121-0.168-0.271 0.039-0.334 0.511-0.152 0.534 0.205 0.686 0.13 0.463-0.232-0.21-0.339-0.090-0.444 0.326-0.284 0.362 0.139 0.973-0.061 1.358-0.446s0.828-0.57 0.986-0.412c0.157 0.157 0.286 0.031 0.286-0.281s0.208-0.438 0.463-0.282c0.254 0.157 0.463 0.053 0.463-0.232s0.194-0.398 0.43-0.251c0.236 0.147 0.563-0.158 0.727-0.674s0.513-0.806 0.777-0.642c0.309 0.192 0.26 0.431-0.135 0.676-0.338 0.209-0.497 0.497-0.353 0.64s0.787-0.209 1.427-0.786c0.641-0.576 1.456-1.103 1.807-1.173 1.117-0.221 4.129-1.631 3.924-1.834-0.108-0.108-0.632 0.036-1.162 0.321s-0.965 0.411-0.965 0.281c0-0.255 2.548-1.648 3.012-1.648 0.156 0 0.154 0.205 0 0.458-0.162 0.261 0.022 0.34 0.433 0.183 0.394-0.151 0.615-0.439 0.49-0.64s0.096-0.49 0.49-0.64c0.405-0.156 0.595-0.079 0.436 0.177s0.031 0.333 0.439 0.176c0.395-0.151 0.719-0.474 0.719-0.716s0.208-0.313 0.463-0.154c0.254 0.158 0.463 0.053 0.463-0.231s0.243-0.367 0.542-0.183c0.357 0.221 0.447 0.087 0.263-0.392-0.212-0.553-0.007-0.697 0.847-0.601 0.618 0.067 1.125-0.067 1.125-0.303s0.201-0.302 0.449-0.149c0.567 0.352 3.253-1.010 3.253-1.651 0-0.262 0.186-0.363 0.415-0.221 0.584 0.362 3.38-0.881 2.948-1.313-0.194-0.194 0.219-0.211 0.917-0.036 0.823 0.207 1.272 0.135 1.272-0.203 0-0.325 0.435-0.412 1.157-0.231s1.157 0.095 1.157-0.231c0-0.327 0.474-0.418 1.273-0.244 1.025 0.224 1.138 0.173 0.579-0.257-0.546-0.421-0.325-0.474 1.040-0.248 1.098 0.182 1.736 0.106 1.736-0.206 0-0.294 0.466-0.375 1.157-0.202l1.157 0.292v-3.7c0-2.881 0.18-3.897 0.811-4.583 0.445-0.486 0.705-0.655 0.579-0.376s0.26-0.023 0.86-0.673c0.6-0.647 0.922-0.869 0.716-0.49s0.529 0.020 1.638-0.794c1.108-0.815 2.224-1.355 2.481-1.196s0.329 0.067 0.163-0.199c-0.165-0.267 0.131-0.976 0.658-1.575s0.829-0.832 0.669-0.521c-0.174 0.343 0.27 0.251 1.12-0.232 0.773-0.44 1.407-1.036 1.41-1.322 0-0.286 0.368-0.807 0.813-1.157 0.749-0.588 0.757-0.567 0.115 0.263-1.010 1.309 4.615-2.81 6.192-4.531 0.733-0.802 1.212-1.232 1.061-0.956s0.031 0.502 0.401 0.502c0.37 0 0.586-0.086 0.481-0.193s0.208-0.595 0.699-1.085c0.708-0.708 1.273-0.831 2.735-0.597 1.14 0.182 2.090 0.090 2.489-0.24 0.404-0.335 1.355-0.421 2.545-0.231 1.219 0.195 2.141 0.105 2.571-0.251 0.368-0.305 1.149-0.489 1.738-0.406 0.826 0.115 1.105-0.099 1.222-0.943 0.109-0.774-0.014-0.988-0.418-0.738s-0.456 0.165-0.166-0.295c0.225-0.357 0.344-1.037 0.266-1.511-0.086-0.524 0.054-0.739 0.358-0.55 0.665 0.411 0.654-1.258-0.016-1.926-0.583-0.583-0.113-1.677 0.547-1.271 0.628 0.389 0.527-0.421-0.148-1.178-0.492-0.551-0.458-0.558 0.232-0.036 0.647 0.491 0.948 0.498 1.504 0.037 0.381-0.316 0.695-0.863 0.695-1.213s0.22-0.638 0.49-0.638c0.269 0 0.356 0.26 0.192 0.579s0.154 0.059 0.706-0.579c0.553-0.636 0.855-0.868 0.667-0.516s0.957-0.239 2.539-1.314c2.144-1.456 2.801-2.149 2.582-2.723-0.238-0.624-0.181-0.659 0.303-0.178s0.756 0.392 1.456-0.463c0.474-0.579 0.723-0.741 0.556-0.359s0.021 0.29 0.423-0.205c0.4-0.494 0.727-0.747 0.727-0.562s0.423 0.11 0.94-0.166c0.516-0.277 0.796-0.732 0.621-1.013s-0.109-0.383 0.142-0.228c0.542 0.334 3.851-1.812 3.851-2.497 0-0.26 0.198-0.351 0.441-0.2s0.776-0.306 1.186-1.013c0.411-0.707 0.638-0.932 0.508-0.5-0.166 0.546-0.035 0.708 0.43 0.529 0.368-0.142 0.525-0.495 0.351-0.786-0.186-0.31-0.013-0.283 0.42 0.067 0.602 0.489 0.774 0.489 0.94 0 0.162-0.484 0.425-0.478 1.379 0.030 0.721 0.384 1.72 0.508 2.576 0.32 0.831-0.182 1.524-0.105 1.707 0.192 0.169 0.274 1.14 0.442 2.154 0.375l1.846-0.125-0.144-4.281c-0.144-4.294 0.057-4.975 1.024-3.469 0.439 0.683 0.483 0.689 0.286 0.044-0.129-0.42 0.079-1.149 0.462-1.62s0.565-0.595 0.404-0.276c-0.577 1.138 0.209 0.553 0.933-0.695 0.406-0.7 0.626-0.908 0.492-0.462-0.345 1.136 0.039 1.012 1.442-0.463 0.665-0.7 1.050-0.96 0.852-0.579s0.151 0.088 0.773-0.654c0.621-0.741 1.359-1.205 1.638-1.034s0.658 0.069 0.843-0.228c0.209-0.338 0.133-0.418-0.199-0.212-0.294 0.181-0.534 0.221-0.534 0.088s0.442-0.479 0.982-0.768c0.77-0.412 0.904-0.4 0.621 0.055-0.24 0.387-0.171 0.464 0.207 0.231 0.311-0.192 0.442-0.672 0.292-1.061-0.22-0.577-0.144-0.605 0.393-0.159 0.532 0.442 0.766 0.385 1.155-0.283 0.268-0.46 0.492-0.673 0.5-0.472 0.024 0.668 1.864-0.618 1.864-1.305 0-0.373 0.22-0.678 0.49-0.678s0.357 0.26 0.197 0.579c-0.161 0.317 0.004 0.218 0.366-0.224s0.788-0.673 0.948-0.512 0.421-0.050 0.581-0.467c0.161-0.417 0.469-0.646 0.688-0.512s0.835-0.192 1.37-0.727c0.782-0.782 0.864-1.106 0.414-1.647-0.464-0.56-0.411-0.62 0.316-0.358 0.482 0.174 0.741 0.166 0.577-0.018s0.2-0.873 0.81-1.531c0.612-0.658 1.107-1.49 1.107-1.851 0-0.37 0.25-0.527 0.578-0.361 0.317 0.161 0.161-0.061-0.346-0.493-0.923-0.784-0.923-0.786 0.038-0.561 0.597 0.139 0.993-0.015 1.040-0.405 0.042-0.346 0.112-0.943 0.154-1.324s0.244-0.694 0.447-0.694c0.204 0 0.26-0.292 0.123-0.646s0.004-0.805 0.317-0.996c0.349-0.215 0.446-0.157 0.256 0.153-0.171 0.276-0.142 0.605 0.064 0.733s0.425-0.428 0.486-1.233c0.061-0.805 0.344-1.675 0.626-1.935 0.359-0.329 0.417-0.296 0.189 0.108-0.179 0.317-0.215 0.578-0.081 0.578s0.581-0.646 0.992-1.439c0.552-1.068 0.583-1.357 0.127-1.123-0.338 0.174-0.286 0.040 0.119-0.296s0.655-0.854 0.555-1.148c-0.1-0.294 0.071-0.691 0.378-0.881 0.379-0.234 0.439-0.149 0.185 0.26-0.256 0.412-0.194 0.494 0.192 0.256 0.311-0.192 0.456-0.638 0.321-0.992s-0.019-0.594 0.257-0.531c0.277 0.060 0.595-0.133 0.704-0.432 0.115-0.313-0.154-0.488-0.637-0.413-0.46 0.071-0.577 0.038-0.258-0.070s0.578-0.496 0.578-0.859c0-0.363 0.22-0.66 0.49-0.66s0.37 0.194 0.225 0.431c-0.147 0.237-0.004 0.582 0.321 0.768s0.437 0.173 0.256-0.028c-0.395-0.438 0.214-1.672 2.060-4.178l1.362-1.852-1.43 1.602c-0.786 0.881-1.43 1.388-1.43 1.127 0-0.831 2.647-3.081 2.984-2.536 0.171 0.275 0.53 0.5 0.802 0.5 0.311 0 0.279-0.256-0.083-0.695-0.498-0.599-0.364-0.657 0.986-0.424 0.888 0.154 1.327 0.102 1.023-0.119-0.323-0.234 0.322-0.417 1.619-0.46 3.588-0.118 10.365-0.552 12.442-0.798 1.166-0.137 2.083-0.024 2.269 0.277 0.195 0.316 0.101 0.375-0.255 0.156-0.431-0.267-0.426-0.124 0.020 0.594 0.324 0.519 0.732 0.856 0.907 0.747s0.308 0.061 0.297 0.377c-0.056 1.556 0.189 2.194 0.847 2.194 0.398 0 0.594 0.208 0.435 0.463s-0.053 0.463 0.232 0.463c0.285 0 0.388 0.208 0.232 0.463s-0.030 0.463 0.282 0.463c0.313 0 0.438 0.13 0.279 0.288s0.171 0.673 0.732 1.142c0.561 0.468 0.762 0.859 0.443 0.868s-0.578-0.227-0.578-0.525c0-0.297-0.109-0.431-0.243-0.298-0.452 0.452 0.335 1.76 0.869 1.44 0.345-0.207 0.332-0.071-0.038 0.386-0.399 0.494-0.443 1.142-0.146 2.18 0.301 1.045 0.276 1.386-0.083 1.163-0.332-0.205-0.417 0.031-0.244 0.685 0.166 0.635 0.083 0.887-0.226 0.698-0.315-0.195-0.388 0.093-0.208 0.813 0.154 0.614 0.066 1.425-0.194 1.804-0.355 0.517-0.393 0.399-0.154-0.466 0.176-0.637 0.239-1.053 0.142-0.925s-0.594 0.736-1.1 1.353c-0.611 0.742-0.685 1.005-0.226 0.775 0.381-0.191 0.284-0.014-0.219 0.393s-1.142 0.652-1.422 0.543c-0.28-0.108-0.769 0.22-1.087 0.729-0.369 0.593-0.899 0.841-1.47 0.691-0.492-0.129-0.893-0.034-0.893 0.214s-0.208 0.317-0.463 0.161c-0.255-0.158-0.463-0.053-0.463 0.231s-0.244 0.367-0.542 0.183c-0.362-0.223-0.446-0.084-0.251 0.42 0.159 0.416 0.059 0.898-0.225 1.072-0.301 0.185-0.384 0.108-0.204-0.185 0.171-0.276 0.12-0.619-0.112-0.763s-0.549 0.072-0.705 0.479c-0.156 0.407-0.558 0.635-0.892 0.507s-0.863 0.176-1.175 0.676c-0.313 0.5-0.922 0.837-1.356 0.746s-1.063 0.158-1.401 0.548c-0.416 0.482-0.505 0.501-0.276 0.061 0.505-0.971-3.696 2.038-4.577 3.279-0.477 0.673-0.659 0.769-0.489 0.26 0.213-0.635-0.018-0.564-1.052 0.321-2.572 2.202-3.21 2.799-2.5 2.342 0.631-0.407 0.637-0.363 0.063 0.368-0.358 0.457-0.924 0.726-1.257 0.598s-0.748 0-0.923 0.283c-0.206 0.333-0.105 0.382 0.287 0.139 0.412-0.256 0.494-0.194 0.256 0.192-0.192 0.311-0.625 0.46-0.963 0.331-0.688-0.265-1.766 0.782-1.294 1.257 0.167 0.167 0.061 0.304-0.236 0.304s-0.541 0.325-0.541 0.721c0 0.398-0.212 0.592-0.47 0.432s-0.331-0.517-0.159-0.793c0.188-0.303 0.095-0.369-0.232-0.168-0.298 0.185-0.435 0.508-0.305 0.717s-0.085 0.507-0.48 0.657c-0.462 0.177-0.599 0.087-0.388-0.255 0.577-0.931-3.796 2.287-4.619 3.401-0.436 0.589-0.661 0.76-0.499 0.378s-0.159-0.174-0.713 0.463c-0.553 0.637-0.844 0.844-0.644 0.463 0.307-0.594 0.249-0.602-0.405-0.074-0.42 0.341-0.892 1.018-1.046 1.503s-0.5 0.883-0.768 0.883c-0.285 0-0.225-0.327 0.147-0.788 0.349-0.433-0.719 0.178-2.374 1.36-2.422 1.731-2.997 2.38-2.958 3.337 0.040 0.951-0.076 1.085-0.578 0.665-0.767-0.637-0.832-1.052-0.095-0.596 0.332 0.205 0.409 0.127 0.203-0.205-0.232-0.374-0.791-0.186-1.847 0.62-0.833 0.636-1.666 1.007-1.846 0.826s-0.201 0.005-0.042 0.418c0.158 0.412 0.061 0.887-0.214 1.059s-0.391 0.49-0.256 0.708c0.135 0.219 0.053 0.518-0.183 0.663-0.518 0.321-0.417-0.626 0.169-1.589 0.352-0.577 0.303-0.597-0.286-0.119-0.391 0.316-0.803 0.481-0.916 0.368s-0.492 0.252-0.841 0.814c-0.599 0.962-0.585 0.981 0.221 0.326 0.487-0.394 0.707-0.445 0.511-0.118-0.19 0.316-0.775 0.806-1.301 1.088-0.756 0.405-0.879 0.386-0.583-0.089 0.204-0.329-0.294-0.127-1.106 0.452s-1.371 1.221-1.243 1.431c0.129 0.208-0.088 0.503-0.482 0.654-0.47 0.18-0.617 0.078-0.427-0.298 0.159-0.315-0.269 0.052-0.952 0.815s-1.077 1.088-0.876 0.72c0.201-0.367-0.488-0.054-1.531 0.695s-1.86 1.466-1.818 1.593c0.042 0.127-0.074 0.231-0.259 0.231s-1.965 1.62-3.958 3.601c-1.993 1.981-3.799 3.438-4.018 3.239s-0.239-0.087-0.045 0.247c0.279 0.483 0.558 0.434 1.345-0.231l0.995-0.839-0.865 1.024c-0.476 0.562-1.101 0.979-1.387 0.925s-0.471 0.163-0.407 0.481c0.123 0.615-0.856 0.598-1.314-0.021-0.149-0.203-0.544-0.041-0.875 0.357-0.508 0.613-0.49 0.683 0.109 0.453 0.393-0.151 0.87-0.019 1.061 0.292 0.214 0.346 0.157 0.447-0.148 0.259-0.645-0.399-2.061 1.014-1.661 1.661 0.169 0.274 0.108 0.375-0.133 0.225-0.589-0.364-1.785 0.409-1.785 1.155 0 0.327-0.468 0.596-1.040 0.596s-1.051 0.208-1.060 0.463c-0.034 0.788-3.343 3.911-3.83 3.612-0.252-0.156-0.332-0.078-0.175 0.175 0.371 0.601-3.088 3.976-3.599 3.511-0.215-0.197-0.251-0.111-0.079 0.19s0.098 0.679-0.164 0.841c-0.262 0.163-0.338 0.522-0.167 0.798s0.119 0.384-0.113 0.24c-0.705-0.436-2.113 0.906-1.953 1.86 0.083 0.492-0.057 0.911-0.311 0.934s-0.398-0.279-0.316-0.668c0.084-0.409-0.063-0.58-0.346-0.405-0.271 0.168-0.493 0.575-0.493 0.901 0 0.642-1.802 2.462-2.175 2.196-0.393-0.279-5.715 5.115-6.452 6.54-0.387 0.747-0.48 1.221-0.207 1.053 0.304-0.189 0.39-0.031 0.221 0.409-0.151 0.394-0.425 0.621-0.607 0.51-0.493-0.304-2.416 3.889-2.051 4.477 0.172 0.277 0.101 0.375-0.155 0.215s-0.706 0.239-1.001 0.883c-0.304 0.667-0.335 1.050-0.073 0.886 0.266-0.164 0.345 0.018 0.186 0.431-0.151 0.393-0.423 0.623-0.604 0.511s-0.894 0.945-1.583 2.351c-0.689 1.405-1.041 2.423-0.784 2.265 0.275-0.171 0.356 0.004 0.195 0.425-0.151 0.393-0.439 0.614-0.64 0.489s-0.489 0.096-0.64 0.49c-0.155 0.405-0.079 0.595 0.177 0.436s0.333 0.031 0.177 0.436c-0.151 0.393-0.425 0.622-0.607 0.51-0.493-0.305-2.415 3.889-2.051 4.477 0.171 0.277 0.102 0.374-0.156 0.215s-0.706 0.239-1.001 0.883c-0.304 0.667-0.335 1.050-0.073 0.887 0.266-0.164 0.345 0.018 0.186 0.43-0.151 0.394-0.424 0.623-0.604 0.511s-0.894 0.945-1.583 2.351c-0.689 1.405-1.041 2.423-0.784 2.265 0.275-0.17 0.356 0.004 0.195 0.426-0.151 0.393-0.439 0.614-0.64 0.489s-0.49 0.096-0.64 0.49c-0.156 0.405-0.079 0.595 0.177 0.436s0.334 0.032 0.177 0.436c-0.151 0.394-0.425 0.621-0.607 0.51-0.493-0.304-2.415 3.889-2.051 4.477 0.171 0.277 0.106 0.377-0.144 0.222-0.539-0.333-3.689 5.976-3.473 6.958 0.081 0.372-0.044 0.558-0.279 0.412s-0.665 0.263-0.96 0.908c-0.304 0.667-0.335 1.050-0.073 0.887 0.266-0.164 0.345 0.018 0.186 0.43-0.151 0.394-0.425 0.622-0.607 0.51-0.493-0.305-2.415 3.889-2.051 4.477 0.171 0.277 0.109 0.379-0.138 0.226s-1.013 0.872-1.703 2.276c-0.689 1.405-1.041 2.423-0.784 2.265 0.275-0.17 0.356 0.004 0.195 0.426-0.151 0.393-0.461 0.599-0.688 0.46s-0.442-0.087-0.474 0.118c-0.505 3.197-0.644 3.778-1.007 4.14-0.507 0.507-0.376-0.717 0.188-1.77 0.346-0.644 0.351-0.853 0.017-0.645-1.077 0.665-1.212 4.148-0.989 25.791 0.127 12.383 0.076 22.417-0.115 22.299s-0.347-0.035-0.347 0.188c0 0.221-0.365 0.814-0.811 1.318-0.822 0.929-1.102 1.634-1.212 3.058-0.035 0.434-0.478 1.111-0.987 1.505-0.594 0.458-0.675 0.658-0.231 0.559 0.381-0.086 0.538-0.069 0.344 0.038s-0.219 0.406-0.059 0.663c0.159 0.258-0.204 0.214-0.806-0.097-0.865-0.446-0.984-0.445-0.564 0 0.405 0.43 0.314 0.785-0.382 1.481-0.504 0.505-0.803 1.208-0.665 1.567s-0.133 0.858-0.603 1.108c-0.469 0.251-0.981 0.786-1.135 1.187s-0.094 0.616 0.133 0.476c0.228-0.142 0.305 0.161 0.172 0.67s-0.523 0.925-0.865 0.925c-0.896 0-1.745 1.707-0.999 2.008 0.346 0.139 0.258 0.247-0.219 0.269-0.876 0.039-1.944 1.664-1.534 2.327 0.144 0.233 0.017 0.574-0.282 0.755-0.359 0.221-0.428 0.115-0.205-0.315 0.186-0.357 0.022-0.265-0.359 0.206-2.287 2.809-3.182 11.842-1.436 14.507 0.766 1.169 0.831 1.186 1.433 0.362 0.703-0.962 0.861-0.38 0.188 0.687-0.487 0.769 0.195 1.865 1.159 1.865 0.332 0 1.243-0.54 2.025-1.199s1.424-1.028 1.424-0.822c0 0.206 0.325-0.087 0.721-0.654s0.615-1.202 0.483-1.413c-0.131-0.212 0.056-0.385 0.415-0.385s0.518 0.225 0.353 0.501c-0.165 0.276 0.019 0.243 0.41-0.073s0.879-0.42 1.088-0.231c0.207 0.188 0.26 0.015 0.117-0.39s-0.016-0.732 0.287-0.732c0.901 0 3.078-1.611 2.796-2.067-0.144-0.233 0.016-0.367 0.351-0.296s0.592-0.203 0.561-0.604c-0.028-0.403 0.222-0.732 0.558-0.732s0.477 0.214 0.314 0.476c-0.496 0.804 2.811-1.603 3.974-2.891 0.6-0.665 0.974-0.986 0.831-0.709s0.021 0.501 0.368 0.501c0.346 0 0.532-0.251 0.415-0.558-0.233-0.602 1.007-2.064 1.75-2.064 0.251 0 0.308-0.241 0.127-0.533-0.215-0.348-0.126-0.408 0.254-0.173 0.426 0.263 0.227 0.669-0.739 1.502-0.727 0.627-0.21 0.361 1.15-0.594 1.995-1.398 2.554-1.608 2.885-1.085 0.226 0.357 0.415 0.432 0.419 0.164s-0.254-0.751-0.573-1.074c-0.463-0.471-0.442-0.559 0.115-0.44 0.381 0.082 0.648-0.091 0.594-0.386s0.215-0.656 0.602-0.805c0.461-0.177 0.6-0.072 0.407 0.303-0.162 0.315 0.126 0.054 0.641-0.579s1.314-1.156 1.776-1.157c0.462-0.002 0.787-0.264 0.723-0.581s0.040-0.531 0.231-0.476c0.618 0.183 3.334-1.231 3.576-1.861 0.138-0.36-0.317-0.219-1.106 0.341-0.738 0.525-1.516 0.951-1.731 0.945-0.504-0.007 2.656-2.039 3.293-2.115 1.012-0.121 2.563-0.774 2.563-1.081 0-0.176 0.327-0.724 0.727-1.218s0.585-0.585 0.412-0.204c-0.174 0.381 0.175 0.163 0.775-0.484s0.883-0.857 0.631-0.463c-0.404 0.623-0.358 0.635 0.346 0.081 0.446-0.349 0.81-0.922 0.81-1.274s0.22-0.637 0.49-0.637c0.269 0 0.335 0.249 0.148 0.555s0.321 0.082 1.129-0.494c0.81-0.577 1.471-1.163 1.471-1.304s-0.293-0.014-0.654 0.285c-0.5 0.414-0.733 0.411-0.996-0.016-0.195-0.315-0.149-0.436 0.106-0.279 0.248 0.153 1.007-0.24 1.69-0.873 1.242-1.152 1.752-0.943 0.662 0.272-0.317 0.356 0.255 0.051 1.274-0.675s2.233-1.755 2.701-2.287c0.467-0.533 0.691-0.669 0.498-0.303s0.62-0.035 1.811-0.888c2.351-1.688 2.593-1.967 1.355-1.569-0.666 0.214-0.689 0.175-0.131-0.219 0.477-0.337 0.88-0.316 1.359 0.072 0.562 0.457 0.625 0.407 0.368-0.283-0.172-0.459-0.127-0.719 0.101-0.579s0.846-0.422 1.375-1.25c0.53-0.828 0.966-1.284 0.971-1.018s-0.252 0.763-0.571 1.102c-0.317 0.338 0.004 0.246 0.714-0.205s1.609-1.363 1.996-2.026c0.465-0.798 0.614-0.893 0.437-0.281-0.226 0.781-0.115 0.745 0.704-0.231 0.534-0.637 0.832-0.844 0.66-0.462s0.142 0.174 0.695-0.463c0.553-0.636 0.874-0.898 0.71-0.582-0.195 0.379-0.052 0.48 0.423 0.298 0.395-0.151 0.719-0.541 0.719-0.864s0.364-0.874 0.81-1.222c0.75-0.588 0.759-0.567 0.115 0.272-0.601 0.786-0.493 0.767 0.81-0.147 0.828-0.579 1.504-1.341 1.504-1.69s0.244-0.786 0.542-0.971c0.363-0.224 0.445-0.083 0.249 0.429-0.249 0.647-0.161 0.68 0.585 0.214 0.482-0.303 0.756-0.747 0.605-0.991s-0.066-0.314 0.186-0.158c0.543 0.335 2.571-0.976 2.267-1.468-0.115-0.186 0.246-0.621 0.802-0.967 0.955-0.596 0.973-0.581 0.316 0.236-0.55 0.687-0.383 0.647 0.81-0.188 0.828-0.58 1.504-1.17 1.504-1.312s-0.364 0.029-0.81 0.379c-0.716 0.562-0.744 0.549-0.236-0.102 0.316-0.405 0.482-0.829 0.369-0.942s0.186-0.205 0.663-0.205c0.478 0 0.765 0.215 0.638 0.479s0.096 0.075 0.497-0.419c0.4-0.494 1.060-0.947 1.465-1.006s0.874-0.26 1.040-0.447c0.167-0.188-0.060-0.209-0.506-0.048s-0.81 0.089-0.81-0.157c0-0.247 0.53-0.448 1.178-0.448s1.694-0.515 2.324-1.146c0.631-0.632 1.245-1.15 1.367-1.158s0.404-0.007 0.626-0.007c0.225 0 1.296-0.862 2.381-1.916s1.839-1.698 1.674-1.431c-0.189 0.306 0.125 0.288 0.855-0.051 0.635-0.294 1.156-0.703 1.157-0.91 0-0.205 0.212-0.245 0.466-0.089s0.488-0.194 0.516-0.782c0.041-0.837 0.101-0.886 0.271-0.225 0.135 0.522 0.482 0.743 0.91 0.579 0.38-0.147 0.791-0.265 0.913-0.265s0.070-0.244-0.113-0.542c-0.225-0.364-0.083-0.445 0.435-0.246 0.423 0.162 0.77 0.101 0.77-0.138s0.346-0.301 0.77-0.138c0.513 0.197 0.66 0.118 0.439-0.24-0.216-0.351-0.083-0.44 0.387-0.26 0.418 0.161 0.719 0.029 0.719-0.314 0-0.324 0.13-0.46 0.288-0.302s0.673-0.171 1.142-0.732c0.468-0.562 0.859-0.762 0.868-0.443s-0.227 0.578-0.525 0.578c-0.297 0-0.431 0.109-0.298 0.243 0.426 0.426 1.768-0.321 1.465-0.813-0.161-0.26-0.017-0.382 0.321-0.269s1.010-0.192 1.495-0.677c0.507-0.507 1.257-0.784 1.763-0.652 0.486 0.127 0.882 0.029 0.882-0.216s0.208-0.317 0.463-0.161c0.255 0.158 0.463 0.072 0.463-0.189s0.346-0.343 0.77-0.18c0.563 0.216 0.667 0.128 0.387-0.325s-0.176-0.541 0.387-0.325c0.423 0.162 0.77 0.101 0.77-0.138s0.346-0.301 0.77-0.138c0.513 0.197 0.66 0.118 0.439-0.24-0.21-0.339-0.089-0.444 0.327-0.284 0.363 0.139 0.973-0.061 1.357-0.446s0.828-0.57 0.986-0.412c0.157 0.157 0.286 0.036 0.286-0.269 0-0.433 0.232-0.433 1.040-0.001 0.948 0.507 0.984 0.486 0.398-0.226-0.562-0.681-0.547-0.743 0.115-0.489 0.437 0.167 0.761 0.063 0.761-0.249 0-0.297 0.208-0.411 0.463-0.254s0.463 0.072 0.463-0.189c0-0.262 0.346-0.343 0.77-0.18 0.513 0.197 0.66 0.118 0.439-0.24-0.216-0.351-0.083-0.44 0.387-0.26 0.489 0.188 0.72 0.001 0.725-0.582 0.004-0.724 0.078-0.75 0.457-0.165 0.326 0.505 0.45 0.528 0.457 0.088 0.004-0.333 0.196-0.489 0.428-0.346 0.574 0.354 1.944-0.209 1.654-0.678-0.127-0.207 0.5-0.317 1.393-0.246 1.41 0.112 1.549 0.036 1.045-0.577-0.512-0.62-0.452-0.637 0.499-0.13 0.595 0.317 1.201 0.456 1.35 0.307s0.024-0.269-0.27-0.269c-0.297 0-0.541-0.221-0.541-0.49s0.208-0.362 0.463-0.204c0.255 0.157 0.48-0.091 0.499-0.55 0.020-0.486 0.135-0.595 0.27-0.259 0.129 0.317 0.437 0.579 0.684 0.579s0.303-0.24 0.121-0.533c-0.194-0.315-0.127-0.409 0.162-0.229 0.702 0.434 2.018-0.163 1.654-0.751-0.192-0.311-0.017-0.382 0.471-0.195 0.979 0.376 3.149-0.578 2.756-1.213-0.177-0.286 0.007-0.357 0.477-0.178 0.423 0.163 0.77 0.101 0.77-0.138s0.346-0.302 0.77-0.138c0.513 0.198 0.66 0.118 0.439-0.24-0.216-0.35-0.083-0.44 0.387-0.26 0.395 0.151 0.719 0.045 0.719-0.237s0.156-0.488 0.346-0.457c1.040 0.164 1.983-0.114 1.739-0.511-0.162-0.262 0.022-0.34 0.435-0.182 0.397 0.151 0.719 0.034 0.719-0.264s0.208-0.411 0.463-0.254 0.463 0.076 0.463-0.18c0-0.279 0.813-0.366 2.029-0.217 1.232 0.151 2.151 0.049 2.342-0.258s1.002-0.395 2.078-0.223c1.069 0.171 1.888 0.084 2.075-0.219 0.192-0.311 1.092-0.393 2.372-0.218 1.327 0.182 2.063 0.107 2.063-0.211 0-0.279 0.951-0.493 2.198-0.494 1.209-0 3.238-0.123 4.512-0.269s3.199-0.272 4.28-0.276c1.082-0.003 1.966-0.217 1.966-0.474 0-0.291 0.868-0.351 2.313-0.158 1.626 0.215 2.313 0.147 2.313-0.231s0.687-0.447 2.313-0.231c1.652 0.219 2.313 0.149 2.313-0.243 0-0.389 0.598-0.461 2.027-0.247 1.195 0.18 2.145 0.114 2.313-0.159 0.161-0.261 1.141-0.349 2.252-0.2 1.794 0.239 1.911 0.194 1.332-0.507-0.738-0.894-0.078-1.043 0.841-0.192 0.502 0.465 0.557 0.459 0.277-0.031-0.217-0.382-0.113-0.577 0.279-0.524 1.173 0.161 1.837-0.022 1.581-0.439-0.142-0.227 0.065-0.539 0.46-0.688 0.416-0.159 0.596-0.080 0.428 0.192-0.158 0.257 0.447 0.115 1.344-0.313s1.525-0.951 1.397-1.159c-0.13-0.211 0.123-0.244 0.559-0.077 0.58 0.224 0.693 0.14 0.416-0.308-0.25-0.404-0.2-0.502 0.147-0.288 0.822 0.509 2.51-0.601 2.19-1.439-0.231-0.601-0.159-0.624 0.426-0.139 0.569 0.472 0.704 0.47 0.704-0.014 0-0.327 0.137-0.459 0.305-0.291s0.613-0.13 0.986-0.663c0.539-0.767 0.762-0.839 1.075-0.344 0.317 0.5 0.398 0.505 0.404 0.019 0.003-0.334 0.215-0.477 0.47-0.321s0.462 0.053 0.462-0.231c0-0.284 0.208-0.388 0.462-0.231s0.462-0.038 0.462-0.435c0-0.463 0.419-0.721 1.17-0.721 1.403 0 6.698-2.588 6.698-3.274 0-0.266 0.208-0.354 0.462-0.197s0.477-0.194 0.495-0.782c0.031-1.010 0.055-1.018 0.43-0.144 0.219 0.509 0.413 0.654 0.43 0.32s0.241-0.477 0.495-0.32c0.255 0.157 0.462 0.053 0.462-0.231s0.208-0.389 0.462-0.232c0.255 0.158 0.462 0.065 0.462-0.204s0.156-0.465 0.346-0.435c0.971 0.153 1.523-0.112 1.23-0.589-0.194-0.315-0.127-0.409 0.163-0.229 0.699 0.432 2.019-0.161 1.658-0.743-0.172-0.279-0.079-0.349 0.219-0.166 0.664 0.411 4.25-1.402 4.259-2.153 0.003-0.407 0.12-0.392 0.404 0.057 0.314 0.495 0.538 0.424 1.075-0.344 0.374-0.533 0.817-0.832 0.986-0.663s0.305 0.050 0.305-0.262c0-0.313 0.208-0.439 0.462-0.283s0.462 0.072 0.462-0.189c0-0.262 0.311-0.357 0.689-0.211 0.394 0.151 0.813-0.053 0.974-0.475 0.156-0.407 0.47-0.623 0.698-0.483s0.414 0.022 0.414-0.261c0-0.284 0.187-0.401 0.414-0.262s0.532-0.051 0.676-0.428c0.163-0.422 0.563-0.567 1.051-0.381 0.494 0.19 0.675 0.119 0.485-0.188-0.166-0.269-0.044-0.48 0.271-0.468 1.538 0.055 2.193-0.185 2.193-0.805 0-0.392 0.274-0.575 0.646-0.432 0.356 0.136 1.3-0.391 2.098-1.171s1.705-1.415 2.014-1.411c0.353 0.003 0.305 0.174-0.132 0.457-0.381 0.246-0.459 0.452-0.169 0.457s0.879-0.533 1.313-1.194c0.534-0.815 1.071-1.125 1.673-0.969 0.488 0.127 0.886 0.031 0.886-0.214s0.208-0.317 0.462-0.161c0.255 0.157 0.462 0.053 0.462-0.232s0.185-0.403 0.411-0.263c0.226 0.139 0.509-0.127 0.632-0.592s0.551-0.843 0.957-0.843c0.406 0 0.619 0.192 0.476 0.424s0.386 0.089 1.178-0.321c0.791-0.409 1.439-0.626 1.439-0.483s0.292-0.005 0.647-0.335c0.493-0.454 0.524-0.763 0.123-1.291-0.307-0.408-0.123-0.353 0.451 0.135 0.538 0.457 1.158 0.796 1.38 0.755s0.561-0.022 0.752 0.038c0.192 0.061 0.607 0.148 0.925 0.192 0.603 0.083 0.618 0.353 0.106 1.889-0.245 0.736-0.209 0.785 0.176 0.238 0.399-0.565 0.474-0.513 0.412 0.296-0.042 0.565-0.314 0.892-0.638 0.767-0.659-0.253-1.733 1.514-1.323 2.179 0.154 0.249 0.047 0.453-0.238 0.453s-0.389 0.208-0.231 0.463c0.158 0.255 0.053 0.462-0.231 0.462s-0.367 0.244-0.183 0.542c0.233 0.378 0.067 0.439-0.55 0.204-0.71-0.271-0.771-0.222-0.307 0.245 0.749 0.755 0.749 1.481 0 1.030-0.337-0.202-0.288 0.017 0.115 0.523 0.633 0.791 0.619 0.841-0.139 0.558-0.5-0.188-0.721-0.13-0.55 0.144 0.156 0.252 0.007 0.461-0.324 0.464-0.44 0.004-0.416 0.13 0.089 0.457 0.588 0.38 0.565 0.449-0.144 0.457-0.461 0.002-0.708 0.215-0.55 0.47s0.015 0.466-0.32 0.47c-0.44 0.004-0.416 0.13 0.089 0.457 0.584 0.378 0.559 0.449-0.166 0.457-0.569 0.004-0.77 0.24-0.596 0.697 0.152 0.398 0.026 0.642-0.298 0.579-0.334-0.065-0.514 0.322-0.447 0.957 0.063 0.587-0.053 0.964-0.259 0.837-0.486-0.301-4.357 7.5-4.004 8.070 0.148 0.24 0.036 0.435-0.248 0.435s-0.389 0.208-0.231 0.463c0.158 0.255 0.072 0.463-0.189 0.463s-0.348 0.332-0.193 0.736c0.178 0.464-0.199 1.182-1.017 1.94-0.761 0.704-1.075 1.279-0.761 1.385 0.297 0.1 0.541 0.393 0.541 0.655s-0.324 0.349-0.719 0.198c-0.412-0.158-0.598-0.081-0.435 0.181 0.156 0.252-0.067 0.458-0.495 0.458-0.668 0-0.69 0.114-0.158 0.81 0.341 0.445 0.438 0.656 0.214 0.467-0.45-0.381-4.193 6.511-3.767 6.938 0.148 0.148 0.269 0.024 0.269-0.271s0.201-0.54 0.447-0.54c0.247 0 0.327 0.313 0.182 0.695s-0.036 0.695 0.246 0.695c0.283 0 0.512-0.29 0.512-0.644s0.313-0.764 0.695-0.911c0.381-0.147 0.695-0.065 0.695 0.181s0.221 0.448 0.49 0.448c0.269 0 0.363-0.206 0.207-0.458-0.156-0.253 0.014-0.345 0.375-0.205s0.973-0.061 1.358-0.446 0.828-0.57 0.986-0.412c0.158 0.158 0.286 0.050 0.286-0.239 0-0.301 0.331-0.399 0.77-0.229 0.563 0.216 0.666 0.128 0.387-0.325s-0.176-0.541 0.387-0.324c0.423 0.162 0.77 0.129 0.77-0.074s0.468-0.353 1.040-0.333c0.582 0.020 0.993-0.211 0.931-0.525-0.067-0.346 0.183-0.449 0.655-0.269 0.512 0.197 0.655 0.115 0.43-0.248-0.188-0.303-0.067-0.574 0.271-0.612 0.334-0.037 0.918-0.111 1.3-0.163s0.966-0.125 1.3-0.163c0.334-0.038 0.491-0.256 0.349-0.483-0.337-0.544 0.935-1.019 1.82-0.678 0.381 0.147 0.695 0.022 0.695-0.274s0.208-0.411 0.462-0.254c0.255 0.158 0.462 0.053 0.462-0.231s0.208-0.389 0.462-0.231c0.255 0.157 0.462 0.053 0.462-0.231s0.194-0.398 0.433-0.251c0.64 0.396 4.181-1.139 4.065-1.76-0.055-0.293 0.368-0.517 0.94-0.497s1.040-0.13 1.040-0.333c0-0.203 0.346-0.236 0.77-0.074 0.563 0.216 0.666 0.129 0.387-0.325s-0.176-0.541 0.387-0.325c0.529 0.203 0.781 0.046 0.807-0.499 0.021-0.483 0.128-0.567 0.27-0.218 0.128 0.317 0.437 0.579 0.684 0.579s0.324-0.206 0.169-0.457c-0.163-0.262 0.022-0.34 0.435-0.182 0.397 0.151 0.719 0.034 0.719-0.264s0.208-0.411 0.462-0.254 0.462 0.053 0.462-0.232c0-0.284 0.208-0.388 0.462-0.231s0.462 0.072 0.462-0.189c0-0.262 0.346-0.343 0.77-0.18 0.563 0.216 0.666 0.128 0.387-0.325s-0.176-0.541 0.387-0.325c0.423 0.162 0.77 0.129 0.77-0.074s0.468-0.352 1.040-0.333c0.582 0.020 0.993-0.211 0.931-0.525-0.067-0.346 0.183-0.449 0.655-0.268 0.512 0.197 0.655 0.115 0.43-0.249-0.188-0.303-0.067-0.573 0.271-0.611 0.334-0.038 0.918-0.112 1.3-0.163s0.966-0.125 1.3-0.163c0.334-0.038 0.491-0.255 0.349-0.483-0.337-0.544 0.935-1.018 1.82-0.678 0.381 0.147 0.695 0.022 0.695-0.274s0.208-0.411 0.462-0.254c0.255 0.157 0.462 0.053 0.462-0.232s0.208-0.388 0.462-0.231c0.255 0.157 0.462 0.053 0.462-0.231s0.194-0.397 0.433-0.251c0.64 0.396 4.182-1.139 4.065-1.76-0.055-0.293 0.368-0.517 0.94-0.497s1.040-0.13 1.040-0.333c0-0.203 0.346-0.236 0.77-0.074 0.563 0.216 0.666 0.129 0.387-0.324s-0.176-0.541 0.387-0.325c0.529 0.203 0.781 0.046 0.806-0.499 0.021-0.483 0.128-0.567 0.27-0.218 0.128 0.317 0.437 0.579 0.684 0.579s0.324-0.205 0.169-0.457c-0.163-0.262 0.022-0.339 0.435-0.182 0.396 0.151 0.719 0.035 0.719-0.263s0.208-0.411 0.462-0.254 0.462 0.053 0.462-0.231c0-0.284 0.208-0.389 0.462-0.231s0.462 0.071 0.462-0.189c0-0.262 0.346-0.343 0.77-0.18 0.563 0.216 0.666 0.129 0.387-0.325s-0.176-0.541 0.387-0.325c0.423 0.163 0.77 0.082 0.77-0.18s0.214-0.344 0.476-0.18c0.599 0.37 4.413-1.837 5.934-3.433 0.631-0.661 1.015-0.975 0.858-0.701s-0.003 0.502 0.344 0.502c0.346 0 0.548-0.226 0.451-0.502s0.204-0.998 0.673-1.604c0.639-0.83 0.943-0.956 1.22-0.507 0.214 0.347 0.185 0.482-0.072 0.324-0.243-0.15-0.687 0.121-0.988 0.601-0.405 0.647-0.411 0.957-0.021 1.199 0.288 0.178 0.399 0.119 0.243-0.131-0.276-0.444 5.118-4.181 5.747-3.982 0.176 0.055 0.44-0.215 0.589-0.601 0.18-0.468 0.079-0.596-0.303-0.38-0.393 0.221-0.428 0.166-0.111-0.175 0.255-0.274 0.774-0.564 1.158-0.646s0.969-0.564 1.303-1.073c0.612-0.925 0.612-0.925 0.341 0-0.226 0.781-0.116 0.743 0.702-0.231 0.534-0.637 0.858-0.931 0.721-0.656s0.034 0.501 0.379 0.501c0.346 0 0.531-0.257 0.411-0.57s0.036-0.727 0.346-0.921c0.327-0.203 0.447-0.157 0.284 0.108-0.156 0.252 0 0.457 0.348 0.457 0.346 0 0.521-0.286 0.386-0.636-0.144-0.375 0.017-0.58 0.392-0.502 0.349 0.074 0.592-0.106 0.533-0.399s0.215-0.656 0.601-0.805c0.435-0.168 0.584-0.079 0.392 0.234-0.219 0.355-0.024 0.351 0.651-0.013 0.529-0.284 1.173-0.876 1.43-1.319s0.476-0.608 0.483-0.368c0.005 0.24 0.342 0.031 0.743-0.462s0.582-0.617 0.406-0.271c-0.226 0.44 0.024 0.398 0.852-0.144 1.058-0.694 1.93-1.928 1.822-2.58-0.021-0.136 0.346-0.249 0.818-0.249s0.755 0.215 0.626 0.48c-0.127 0.263 0.043 0.148 0.38-0.256s0.902-0.637 1.255-0.518c0.432 0.146 0.522 0.019 0.271-0.385-0.267-0.432-0.154-0.518 0.399-0.305 0.423 0.163 0.77 0.101 0.77-0.138s0.346-0.301 0.77-0.138c0.514 0.197 0.659 0.117 0.439-0.24-0.216-0.351-0.082-0.441 0.387-0.26 0.396 0.151 0.719 0.035 0.719-0.263s0.208-0.411 0.462-0.254c0.255 0.158 0.462 0.039 0.462-0.26 0-0.408 0.176-0.399 0.695 0.029 0.57 0.473 0.695 0.437 0.695-0.202 0-0.428 0.208-0.651 0.462-0.492s0.462 0.071 0.462-0.189c0-0.261 0.346-0.342 0.77-0.18 0.563 0.216 0.666 0.128 0.387-0.325s-0.176-0.541 0.381-0.327c0.42 0.162 0.743 0.144 0.716-0.037-0.135-0.91 0.125-1.18 0.54-0.561 0.404 0.602 0.527 0.585 0.953-0.136 0.307-0.524 0.8-0.731 1.332-0.563 0.462 0.147 1.126-0.017 1.474-0.364s0.919-0.535 1.271-0.418c0.425 0.142 0.514 0.015 0.271-0.381-0.226-0.366-0.183-0.48 0.113-0.297 0.717 0.443 3.416-0.62 3.056-1.203-0.193-0.313-0.021-0.387 0.462-0.2 0.529 0.203 0.781 0.045 0.806-0.5 0.021-0.483 0.128-0.567 0.27-0.218 0.128 0.317 0.437 0.579 0.684 0.579s0.303-0.24 0.122-0.533c-0.211-0.339-0.127-0.409 0.229-0.188 0.623 0.387 5.598-1.473 5.598-2.093 0-0.218 0.286-0.285 0.635-0.151s0.923-0.045 1.277-0.399c0.353-0.353 0.93-0.546 1.281-0.43 0.414 0.138 0.514 0.013 0.285-0.359-0.26-0.421-0.076-0.484 0.7-0.238 0.848 0.268 0.998 0.189 0.769-0.406-0.243-0.637-0.147-0.659 0.684-0.169 0.714 0.42 1.019 0.433 1.162 0.041 0.106-0.292 0.599-0.423 1.095-0.293s0.903 0.007 0.903-0.266c0-0.276 0.313-0.382 0.695-0.236s0.695 0.078 0.695-0.154c0-0.231 0.774-0.442 1.721-0.469s1.789-0.255 1.873-0.507c0.084-0.251 0.559-0.351 1.056-0.22s0.903 0.022 0.903-0.238c0-0.26 0.448-0.356 0.998-0.212 0.726 0.19 0.899 0.102 0.637-0.324-0.269-0.436-0.096-0.5 0.68-0.255s0.95 0.182 0.68-0.255c-0.267-0.431-0.082-0.511 0.697-0.308 0.644 0.169 1.327 0.005 1.749-0.414 0.467-0.467 1.159-0.598 2.137-0.407 1.084 0.213 1.274 0.162 0.752-0.2-0.595-0.414-0.575-0.485 0.144-0.491 0.514-0.004 0.709-0.214 0.507-0.542-0.206-0.334-0.127-0.409 0.212-0.199 0.298 0.185 0.409 0.549 0.247 0.812-0.791 1.279 1.865-0.84 5.402-4.314 2.146-2.106 3.588-3.406 3.205-2.889s0.086 0.093 1.039-0.942c0.954-1.035 1.734-2.060 1.736-2.277s0.366-0.254 0.812-0.080c0.601 0.236 1.465-0.38 3.356-2.394 1.401-1.491 2.144-2.236 1.654-1.656s-0.728 1.055-0.529 1.055c0.46 0 2.346-2.379 2.346-2.958 0-0.24 0.288-0.531 0.642-0.651 0.445-0.148 0.524-0.018 0.255 0.425-0.214 0.351 0.368 0.016 1.291-0.749 1.641-1.358 2.317-2.427 1.166-1.844-0.317 0.161-0.248 0.018 0.156-0.32s0.637-0.902 0.518-1.256c-0.146-0.432-0.019-0.522 0.385-0.27 0.432 0.268 0.518 0.154 0.305-0.399-0.163-0.423-0.082-0.77 0.18-0.77s0.356-0.194 0.209-0.432c-0.147-0.236-0.063-0.557 0.183-0.708s0.32-0.618 0.161-1.034c-0.18-0.471-0.104-0.64 0.203-0.45 0.557 0.344 1.197-0.933 0.796-1.582-0.144-0.232-0.017-0.421 0.279-0.421s0.42-0.313 0.274-0.695c-0.147-0.381-0.070-0.695 0.168-0.695s0.301-0.346 0.138-0.77c-0.216-0.563-0.128-0.666 0.325-0.387s0.541 0.176 0.325-0.387c-0.163-0.423-0.080-0.77 0.183-0.77s0.353-0.401 0.197-0.892c-0.183-0.577-0.004-1.046 0.503-1.332 0.493-0.276 0.675-0.731 0.486-1.22-0.171-0.445-0.531-0.638-0.837-0.448-0.313 0.193-0.413 0.131-0.241-0.147 0.363-0.586-1.952-2.99-2.533-2.631-0.229 0.142-0.361 0.019-0.292-0.274 0.091-0.382-2.019-0.546-7.508-0.583-9.519-0.065-25.454 0.359-28.226 0.756-1.145 0.163-1.801 0.167-1.458 0.006 0.399-0.185-0.077-0.601-1.323-1.159-2.276-1.019-2.475-1.053-2.034-0.337 0.206 0.334 0.127 0.409-0.212 0.199-0.298-0.184-0.435-0.507-0.305-0.717 0.243-0.393-0.803-0.947-1.762-0.935-0.31 0.003-0.244 0.199 0.154 0.457 0.381 0.247 0.466 0.452 0.188 0.457s-0.903-0.459-1.387-1.026c-0.578-0.678-0.635-0.877-0.166-0.578 0.394 0.25 0.317 0.089-0.166-0.356s-0.703-0.817-0.484-0.826c0.219-0.006-0.123-0.335-0.759-0.726s-0.791-0.597-0.346-0.46c0.762 0.235 0.765 0.18 0.047-0.916-0.44-0.672-1.039-1.060-1.413-0.916-0.43 0.166-0.529 0.056-0.292-0.324 0.619-0.992 0.55-1.675-0.144-1.409-0.368 0.141-0.635 0.074-0.594-0.148 0.18-0.981-0.13-1.584-1.54-3.004-0.828-0.832-1.193-1.355-0.81-1.158s0.29 0.029-0.204-0.371c-0.494-0.4-0.728-0.727-0.523-0.727s0.108-0.324-0.221-0.72c-0.5-0.601-0.425-0.883 0.47-1.736 0.586-0.559 0.922-0.755 0.745-0.438-0.425 0.762-0.26 0.738 2.066-0.304 1.853-0.83 3.488-0.855 2.988-0.044-0.123 0.199 0.539 0.944 1.47 1.654s1.541 1.046 1.357 0.746c-0.186-0.302-0.166-0.547 0.042-0.547s0.585 0.468 0.837 1.040c0.251 0.575 0.829 1.040 1.281 1.040s1.006 0.292 1.226 0.646c0.305 0.493 1.019 0.576 2.993 0.344 1.817-0.214 2.591-0.147 2.591 0.226 0 0.37 0.762 0.441 2.517 0.235 1.385-0.163 2.632-0.112 2.769 0.113s0.784 0.51 1.432 0.635c0.846 0.161 1.375-0.052 1.868-0.755 0.378-0.54 0.839-0.928 1.024-0.865s0.337-0.075 0.337-0.308c0-0.233 0.156-0.58 0.346-0.771s0.346-0.111 0.346 0.178c0 0.865 1.363 0.071 2.126-1.236 0.466-0.8 0.619-0.901 0.447-0.293-0.259 0.912-0.252 0.913 0.433 0.027 0.382-0.494 0.697-0.716 0.697-0.49s0.327 0.003 0.728-0.49c0.4-0.494 0.618-0.674 0.483-0.398s0.046 0.501 0.404 0.501c0.477 0 0.532-0.217 0.212-0.818-0.549-1.027-0.551-1.341-0.006-1.341 0.236 0 0.515 0.26 0.622 0.578s0.099-0.020-0.017-0.752c-0.156-0.988-0.059-1.237 0.375-0.969 0.454 0.281 0.521-0.116 0.293-1.774-0.185-1.345-0.106-2.25 0.212-2.445 0.295-0.183 0.405-1.053 0.264-2.102-0.132-0.986-0.051-1.907 0.178-2.049s0.416-0.819 0.412-1.504c-0.004-0.974-0.105-1.093-0.457-0.55-0.7 1.085-0.541 0.102 0.226-1.379 0.425-0.821 0.579-1.877 0.405-2.752-0.197-0.986-0.105-1.421 0.3-1.421 0.425 0 0.507-0.564 0.298-2.082-0.192-1.395-0.115-2.083 0.231-2.083s0.423-0.686 0.231-2.083c-0.176-1.291-0.106-2.083 0.185-2.083 0.631 0 0.243-4.769-0.404-4.985-0.272-0.090-0.367-0.498-0.211-0.905 0.205-0.534 0.112-0.635-0.336-0.357-0.453 0.28-0.541 0.176-0.325-0.387 0.163-0.423 0.101-0.77-0.138-0.77s-0.301-0.346-0.138-0.77c0.216-0.563 0.128-0.667-0.325-0.387-0.461 0.285-0.54 0.172-0.305-0.439 0.221-0.578 0.131-0.75-0.307-0.582-0.732 0.281-0.866-1.018-0.194-1.871 0.327-0.414 0.236-0.578-0.322-0.578-0.428 0-0.661-0.189-0.517-0.421 0.399-0.643-0.238-1.928-0.788-1.587-0.293 0.18-0.348 0.077-0.138-0.262 0.464-0.751-0.641-3.354-1.261-2.969-0.303 0.188-0.375 0.007-0.192-0.468 0.387-1.007-0.098-2.237-0.728-1.85-0.309 0.191-0.382 0.018-0.197-0.465 0.163-0.423 0.101-0.77-0.138-0.77s-0.322-0.292-0.185-0.646c0.137-0.356 0.027-0.784-0.24-0.95-0.307-0.19-0.375-0.006-0.185 0.492 0.224 0.581 0.14 0.694-0.308 0.416-0.408-0.251-0.503-0.2-0.284 0.155 0.182 0.293 0.129 0.533-0.116 0.533s-0.344 0.393-0.219 0.874c0.125 0.48 0.003 1.012-0.269 1.18-0.313 0.194-0.401 0-0.236-0.526 0.247-0.787 0.233-0.788-0.262-0.020-0.286 0.447-0.395 1.020-0.239 1.274s0.051 0.459-0.234 0.459c-0.284 0-0.389 0.208-0.231 0.463s0.043 0.463-0.254 0.463c-0.297 0-0.42 0.313-0.274 0.695 0.173 0.45-0.015 0.697-0.529 0.701-0.684 0.004-0.699 0.071-0.101 0.471 0.602 0.404 0.581 0.549-0.161 1.12-0.502 0.383-0.711 0.887-0.507 1.219 0.224 0.361 0.156 0.446-0.185 0.235-0.474-0.293-0.769 0.556-0.589 1.69 0.029 0.191-0.147 0.346-0.394 0.346s-0.315 0.346-0.152 0.77c0.185 0.482 0.112 0.657-0.197 0.465-0.557-0.344-1.197 0.934-0.796 1.583 0.144 0.232 0.026 0.421-0.257 0.421s-0.389 0.208-0.231 0.463c0.158 0.255 0.051 0.463-0.236 0.463-0.319 0-0.408 0.404-0.228 1.040 0.228 0.815 0.188 0.892-0.193 0.346-0.416-0.595-0.488-0.576-0.493 0.143-0.003 0.46-0.185 0.727-0.401 0.595s-0.789 0.078-1.274 0.47c-0.483 0.393-0.585 0.561-0.228 0.376 0.442-0.229 0.539-0.156 0.301 0.228-0.192 0.311-0.64 0.453-0.996 0.317s-0.646-0.014-0.646 0.277c0 0.288-0.135 0.39-0.302 0.225s-0.598 0.060-0.961 0.501c-0.44 0.535-0.559 0.573-0.358 0.108s0.081-0.427-0.358 0.108c-0.363 0.442-0.794 0.667-0.961 0.501s-0.302-0.046-0.302 0.266c0 0.313-0.205 0.44-0.457 0.285-0.262-0.162-0.339 0.022-0.182 0.435 0.18 0.472 0.014 0.719-0.495 0.719-0.425 0-0.642-0.208-0.485-0.463 0.562-0.908-0.296-0.441-1.486 0.81-0.665 0.7-1.060 1.015-0.88 0.701 0.248-0.429 0.065-0.469-0.733-0.161-0.585 0.226-1.007 0.656-0.938 0.953s-0.086 0.411-0.346 0.249c-0.26-0.161-0.475-0.061-0.475 0.224s-0.2 0.393-0.446 0.242c-0.245-0.152-0.831 0.109-1.301 0.579s-1.103 0.773-1.405 0.673c-0.303-0.101-0.549 0.022-0.549 0.276s-0.448 0.34-0.998 0.197c-0.726-0.19-0.899-0.102-0.637 0.324 0.267 0.432 0.103 0.503-0.621 0.274-0.608-0.193-0.983-0.112-0.983 0.212s-0.375 0.406-0.983 0.213c-0.705-0.224-0.885-0.154-0.637 0.25 0.248 0.399 0.075 0.476-0.597 0.262-0.521-0.166-1.072-0.092-1.231 0.161s-0.623 0.332-1.039 0.171c-0.533-0.205-0.647-0.116-0.39 0.302 0.269 0.433 0.115 0.511-0.579 0.292-0.521-0.166-1.067-0.103-1.216 0.138s-0.72 0.321-1.269 0.177c-0.639-0.167-0.887-0.083-0.693 0.233 0.192 0.31-0.038 0.402-0.621 0.25-0.513-0.133-0.925-0.016-0.925 0.268s-0.365 0.481-0.81 0.443c-1.608-0.135-2.478 0.088-2.186 0.56 0.195 0.315-0.139 0.371-0.967 0.163-0.978-0.245-1.18-0.179-0.887 0.292 0.277 0.45 0.158 0.525-0.461 0.287-0.909-0.349-2.71 0.925-2.284 1.614 0.135 0.221 0.016 0.4-0.269 0.4-0.291 0-0.365 0.253-0.171 0.579 0.274 0.455 0.2 0.46-0.341 0.021-0.74-0.598-1.842 0.268-4.375 3.438-0.657 0.821-1.336 1.473-1.507 1.45-0.945-0.132-1.488 0.127-1.211 0.578 0.215 0.348-0.017 0.407-0.724 0.181-0.757-0.24-0.945-0.176-0.695 0.232 0.26 0.419-0.034 0.477-1.158 0.231s-1.415-0.188-1.158 0.231c0.252 0.408 0.063 0.471-0.695 0.232-0.709-0.226-0.94-0.167-0.724 0.181 0.175 0.283 0.108 0.512-0.147 0.512s-0.585-0.199-0.736-0.442c-0.151-0.243-0.578-0.325-0.95-0.183s-0.547 0.468-0.39 0.723c0.18 0.292-0.101 0.341-0.755 0.133-0.757-0.24-0.945-0.177-0.695 0.231 0.26 0.42-0.034 0.478-1.158 0.232-1.159-0.255-1.421-0.195-1.139 0.26 0.265 0.428 0.137 0.505-0.464 0.274-0.531-0.204-1.070-0.023-1.498 0.5-0.368 0.449-0.539 0.558-0.377 0.24 0.175-0.345-0.006-0.579-0.457-0.579-0.445 0-0.615 0.215-0.421 0.53 0.231 0.373-0.036 0.439-0.898 0.222-0.763-0.192-1.226-0.118-1.226 0.194 0 0.304-0.394 0.378-0.992 0.188-0.953-0.303-0.979-0.219-0.674 2.094 0.214 1.609 0.138 2.626-0.228 3.069-0.375 0.451-0.46 1.79-0.27 4.238 0.193 2.489 0.139 3.296-0.176 2.655s-0.384 0.458-0.226 3.585c0.156 3.094 0.067 4.512-0.279 4.512-0.337 0-0.428 0.964-0.27 2.892 0.279 3.414-0.368 6.671-1.324 6.671-0.498 0-0.596-0.374-0.386-1.49 0.156-0.832 0.070-1.618-0.194-1.782-0.269-0.166-0.356-1.019-0.2-1.976 0.199-1.221 0.125-1.59-0.269-1.348-0.672 0.414-0.271-4.315 0.544-6.438 0.695-1.812 0.613-8.555-0.112-9.003-0.455-0.282-0.755-1.381-0.926-3.402-0.018-0.206-0.229-0.252-0.473-0.102s-0.692-0.569-1.002-1.598c-0.308-1.028-0.86-2.119-1.23-2.424-1.187-0.986-4.394-2.695-5.493-2.927-0.809-0.172-3.215-2.838-4.828-5.355-0.796-1.242-1.621-2.257-1.832-2.257s-0.858-0.677-1.438-1.504c-0.613-0.875-0.74-1.252-0.304-0.903 0.579 0.463 0.83 0.474 1.094 0.043 0.212-0.344 0.149-0.436-0.166-0.241-0.281 0.174-0.743-0.118-1.026-0.645-0.322-0.601-0.878-0.911-1.485-0.822-0.558 0.081-1.052-0.154-1.163-0.556-0.172-0.617-0.139-0.617 0.297 0 0.361 0.51 0.493 0.533 0.498 0.089 0.003-0.334 0.266-0.473 0.585-0.306 0.317 0.164 0.058-0.154-0.579-0.706s-0.844-0.868-0.462-0.7c0.459 0.202 0.421 0.083-0.108-0.353-0.442-0.362-0.673-0.788-0.512-0.948s-0.050-0.421-0.467-0.581c-0.417-0.16-0.661-0.446-0.544-0.637s-0.107-0.741-0.5-1.223c-0.393-0.482-0.562-0.585-0.377-0.228 0.228 0.442 0.156 0.539-0.228 0.302-0.312-0.193-0.457-0.635-0.324-0.982s0.014-0.775-0.271-0.951c-0.312-0.192-0.383-0.107-0.183 0.216 0.182 0.293 0.11 0.533-0.159 0.533s-0.49-0.429-0.49-0.953c0-0.524 0.187-0.838 0.414-0.697s0.541-0.071 0.695-0.474c0.417-1.087 0.325-1.442-0.17-0.659-0.341 0.541-0.418 0.462-0.356-0.358 0.051-0.688 0.301-0.962 0.725-0.799 0.433 0.166 0.558 0.018 0.378-0.452-0.326-0.85 0.12-2.169 0.628-1.856 0.201 0.125 0.498-0.12 0.661-0.544s0.027-0.94-0.298-1.147c-0.433-0.274-0.367-0.379 0.243-0.384 0.461-0.004 0.708-0.215 0.55-0.47s-0.065-0.463 0.204-0.463c0.269 0 0.49-0.22 0.49-0.49s-0.26-0.357-0.579-0.197c-0.317 0.161-0.217-0.004 0.224-0.365s0.666-0.794 0.502-0.96-0.055-0.302 0.248-0.302c0.354 0 0.446-0.411 0.259-1.157-0.226-0.897-0.112-1.157 0.505-1.157 0.527 0 0.701-0.241 0.518-0.719-0.158-0.412-0.080-0.597 0.182-0.435 0.632 0.39 0.577-0.393-0.072-1.041-0.397-0.398-0.253-0.458 0.579-0.244 1.017 0.262 1.053 0.218 0.432-0.54-0.599-0.732-0.581-0.893 0.163-1.438 0.491-0.358 0.695-0.847 0.49-1.179-0.234-0.378-0.156-0.446 0.235-0.204 0.434 0.268 0.498 0.095 0.253-0.68-0.226-0.709-0.168-0.94 0.182-0.724 0.485 0.301 0.588 0.115 0.698-1.242 0.024-0.317 0.317-0.865 0.653-1.216s0.495-0.923 0.362-1.274c-0.345-0.898 0.192-2.076 0.78-1.712 0.309 0.191 0.382 0.018 0.197-0.465-0.163-0.423-0.101-0.77 0.138-0.77s0.301-0.346 0.138-0.77c-0.216-0.563-0.128-0.667 0.325-0.387s0.541 0.176 0.325-0.387c-0.163-0.423-0.099-0.77 0.142-0.77 0.274 0 0.274-0.431 0-1.152-0.241-0.635-0.288-1.129-0.104-1.103 1.025 0.154 1.433-0.135 1.166-0.829-0.185-0.483-0.112-0.657 0.197-0.465 0.82 0.507 1.145-1.184 0.375-1.954-0.414-0.414-0.711-0.494-0.711-0.19 0 0.288-0.544 0.462-1.214 0.387-0.7-0.078-1.565 0.218-2.045 0.697-0.457 0.457-0.951 0.708-1.101 0.56s-0.27-0.016-0.27 0.297c0 0.313-0.208 0.438-0.462 0.283s-0.462-0.053-0.462 0.231c0 0.284-0.208 0.389-0.462 0.231s-0.462-0.053-0.462 0.231c0 0.284-0.2 0.394-0.446 0.241s-0.852 0.131-1.349 0.628c-0.497 0.497-1.051 0.765-1.232 0.596s-0.176 0.212 0.007 0.848c0.313 1.073 0.288 1.095-0.32 0.298-0.63-0.823-0.688-0.81-1.43 0.323-0.557 0.848-0.624 1.275-0.245 1.51 0.348 0.215 0.401 0.119 0.154-0.279-0.253-0.411-0.194-0.494 0.182-0.262 0.426 0.263 0.43 0.497 0.016 0.996-0.67 0.807-1.538 0.868-1.060 0.074 0.236-0.393 0.096-0.375-0.435 0.057-0.913 0.74-1.046 1.195-0.204 0.697 0.317-0.188-0.114 0.314-0.961 1.117s-1.421 1.65-1.276 1.883c0.144 0.234-0.308 0.802-1.006 1.26-1.457 0.96-3.827 0.91-5.826-0.124-0.731-0.378-1.212-0.496-1.068-0.263s-0.029 0.424-0.387 0.424c-0.509 0-0.541-0.208-0.148-0.964 0.276-0.53 0.334-0.807 0.128-0.617s-0.802-0.017-1.327-0.463c-0.716-0.607-0.813-0.625-0.389-0.074 0.657 0.855 0.317 1.397-0.641 1.027-0.375-0.144-0.556-0.596-0.413-1.040 0.156-0.493-0.226-1.204-1.027-1.912l-1.278-1.125 2.313 2.776-1.274-1.122c-0.7-0.617-1.274-1.376-1.274-1.686s-0.325-0.689-0.723-0.841c-0.398-0.153-0.604-0.468-0.462-0.701s-0.159-0.314-0.673-0.179c-0.513 0.133-1.067 0.46-1.23 0.722-0.171 0.274-0.070 0.338 0.233 0.151 0.344-0.213 0.432-0.071 0.249 0.404-0.204 0.532-0.44 0.597-0.87 0.241-0.442-0.367-1.024 0.005-2.324 1.492-0.954 1.090-1.445 1.574-1.091 1.078s-1.094 0.438-3.219 2.077l-3.863 2.979 0.293 2.202c0.205 1.547 0.13 2.202-0.252 2.202-0.389 0-0.462 0.791-0.255 2.776 0.159 1.526 0.105 2.776-0.12 2.776s-0.483 1.019-0.574 2.266l-0.161 2.267 1.235-0.638c1.050-0.543 1.42-0.543 2.462-0.004 0.849 0.441 1.039 0.724 0.616 0.923-0.336 0.157-0.144 0.199 0.43 0.093s1.163 0.004 1.313 0.245c0.149 0.241 0.67 0.311 1.158 0.156 0.53-0.168 0.886-0.063 0.886 0.262 0 0.349 0.414 0.44 1.158 0.254 0.725-0.182 1.158-0.095 1.158 0.236 0 0.298 0.401 0.42 0.925 0.284 0.509-0.133 0.925-0.029 0.925 0.232s0.416 0.364 0.925 0.232c0.509-0.133 0.925-0.030 0.925 0.227 0 0.266 0.486 0.347 1.12 0.189 0.695-0.174 1.192-0.064 1.31 0.292 0.151 0.451 0.469 0.446 1.504-0.024 1.221-0.558 1.267-0.542 0.658 0.198-0.601 0.729-0.545 0.768 0.672 0.465 0.954-0.238 1.572-0.109 2.198 0.458 0.949 0.858 1.2 1.829 0.337 1.295-0.342-0.212-0.409-0.126-0.185 0.236 0.192 0.311 0.693 0.435 1.111 0.276 0.584-0.222 0.637-0.163 0.221 0.256-0.619 0.624-0.294 1.816 0.7 2.561 0.432 0.323 0.534 0.745 0.286 1.158-0.32 0.53-0.262 0.544 0.315 0.078 0.56-0.453 0.705-0.452 0.705 0.004 0 0.574 4.281 7.275 4.644 7.275 0.105 0 0.041-0.24-0.139-0.533-0.2-0.323-0.127-0.409 0.183-0.216 0.284 0.175 0.405 0.601 0.271 0.951s0.014 0.789 0.324 0.982c0.386 0.239 0.447 0.157 0.192-0.256-0.253-0.411-0.194-0.494 0.182-0.262 0.391 0.242 0.413 0.517 0.076 0.923-0.488 0.588-0.214 1.189 0.541 1.189 0.226 0 0.293-0.194 0.147-0.432s-0.067-0.555 0.175-0.703c0.255-0.158 0.349 0.234 0.224 0.93-0.12 0.661-0.031 1.088 0.199 0.945 0.488-0.302 2.204 1.45 1.809 1.845-0.147 0.147-0.496-0.158-0.775-0.68-0.295-0.55-0.788-0.839-1.178-0.69-0.399 0.153-1.366-0.462-2.393-1.521-0.945-0.978-1.318-1.257-0.826-0.621l0.896 1.158-1.030-0.871c-0.565-0.48-0.917-1.055-0.78-1.277s-0.387-0.767-1.164-1.212c-0.779-0.443-1.418-0.987-1.419-1.209 0-0.221 0.362-0.122 0.807 0.223 0.675 0.523 0.663 0.456-0.062-0.395-0.479-0.562-1.085-0.892-1.344-0.73s-0.335 0.070-0.168-0.2c0.168-0.271-0.57-1.137-1.639-1.921s-1.762-1.429-1.541-1.429c0.221 0 0.834 0.363 1.363 0.806 0.822 0.689 0.81 0.635-0.084-0.381-0.576-0.654-0.887-1.375-0.695-1.601s-0.023-0.125-0.485 0.228c-0.46 0.353-0.8 0.45-0.755 0.216 0.186-0.979-0.067-1.583-0.666-1.583-0.351 0-0.923-0.364-1.274-0.81-0.588-0.75-0.565-0.76 0.291-0.124 0.914 0.678 0.914 0.675 0.054-0.335-0.479-0.562-1.010-0.937-1.179-0.832s-0.611 0-0.979-0.236c-0.581-0.369-0.579-0.43 0.021-0.462 0.381-0.019 0.195-0.236-0.414-0.48s-1.494-0.321-1.966-0.171c-0.492 0.156-0.858 0.048-0.858-0.252 0-0.325-0.375-0.406-0.983-0.213-0.724 0.229-0.887 0.158-0.621-0.274 0.262-0.426 0.089-0.513-0.637-0.324-0.548 0.143-0.998 0.059-0.998-0.189s-0.416-0.317-0.925-0.156c-0.57 0.18-0.925 0.083-0.925-0.254 0-0.321-0.385-0.447-0.925-0.305-0.509 0.133-0.925 0.038-0.925-0.208s-0.416-0.317-0.925-0.156c-0.565 0.18-0.925 0.083-0.925-0.25 0-0.299-0.305-0.426-0.679-0.284s-0.812 0.047-0.973-0.214c-0.161-0.261-0.583-0.363-0.94-0.226s-0.646 0.047-0.646-0.2c0-0.246-0.325-0.447-0.721-0.447s-0.594 0.208-0.435 0.463c0.158 0.255 0.089 0.463-0.151 0.463s-0.558-0.465-0.705-1.034c-0.214-0.819-0.389-0.915-0.841-0.463-0.699 0.699-1.774 0.748-1.774 0.081 0-0.269 0.219-0.355 0.486-0.189s0.127-0.499-0.315-1.476c-0.567-1.259-0.877-3.553-1.065-7.865-0.146-3.348-0.159-6.024-0.034-5.947s0.948-0.827 1.823-2.010c0.875-1.183 2.229-2.858 3.007-3.724s1.103-1.399 0.721-1.184c-0.598 0.335-0.605 0.282-0.059-0.393 0.736-0.906 1.645-1.060 1.204-0.204-0.163 0.317 0.363-0.15 1.168-1.040s1.303-1.306 1.106-0.925c-0.312 0.601-0.252 0.607 0.434 0.049 0.437-0.355 0.695-0.744 0.574-0.866s0.218-0.579 0.757-1.017c0.714-0.578 1.618-0.749 3.337-0.634 1.655 0.113 2.082 0.038 1.43-0.252-0.644-0.286-0.084-0.38 1.852-0.303 2.36 0.092 2.981 0.311 4.144 1.45 0.751 0.738 1.207 1.079 1.014 0.762-0.195-0.322-0.135-0.579 0.137-0.579 0.269 0 0.49 0.288 0.49 0.641s0.575 1.013 1.274 1.469c1.234 0.804 1.252 0.802 0.579-0.026s-0.656-0.831 0.579-0.026c1.137 0.739 1.909 1.632 0.841 0.973-0.236-0.147-0.541-0.089-0.675 0.127s0.006 0.401 0.315 0.411c0.307 0.007 0.767 0.069 1.021 0.131 1.327 0.326 2.169-0.028 1.876-0.792-0.168-0.438-0.158-0.646 0.022-0.466s0.661 0.070 1.067-0.245c0.665-0.519 0.67-0.488 0.042 0.335-0.381 0.5 0.295 0.091 1.503-0.907s2.198-1.919 2.198-2.044c0-0.125-0.25 0.022-0.557 0.329s-0.762 0.557-1.014 0.557c-0.252 0-0.060-0.292 0.426-0.646s1.252-0.577 1.7-0.491c0.447 0.086 0.765-0.004 0.706-0.204s0.571-0.933 1.399-1.634c0.829-0.7 1.368-1.495 1.2-1.765-0.193-0.313-0.080-0.353 0.308-0.113 0.449 0.277 0.531 0.166 0.306-0.42-0.233-0.602-0.094-0.763 0.563-0.655 0.747 0.125 3.27-1.637 6.087-4.252 0.411-0.381 1.043-0.757 1.404-0.835s0.885-0.439 1.163-0.803c0.279-0.364 0.643-0.526 0.81-0.36s0.302 0.046 0.302-0.266c0-0.313 0.208-0.438 0.462-0.283s0.462 0.065 0.462-0.204c0-0.269 0.156-0.465 0.346-0.435 0.981 0.156 1.658-0.171 2.76-1.327 0.665-0.7 1.077-1.039 0.914-0.755-0.197 0.344 0.163 0.462 1.077 0.356 0.755-0.087 1.324 0.026 1.26 0.257-0.107 0.397 3.213 3.731 3.714 3.731 0.132 0 1.196 0.781 2.363 1.736l2.122 1.736 5.793 0.089c3.186 0.050 11.935 0.25 19.442 0.446s17.891 0.335 23.077 0.31c9.846-0.048 10.565-0.198 9.979-2.079-0.132-0.425-0.024-0.903 0.236-1.065 0.288-0.178 0.374 0.035 0.215 0.541-0.247 0.787-0.233 0.788 0.262 0.020 0.286-0.447 0.395-1.020 0.239-1.274s-0.039-0.459 0.257-0.459c0.298 0 0.416-0.322 0.264-0.719-0.151-0.395-0.091-0.603 0.133-0.465 0.493 0.305 1.079-1.039 0.711-1.634-0.144-0.233-0.026-0.421 0.257-0.421s0.389-0.208 0.231-0.462c-0.158-0.255-0.072-0.462 0.189-0.462s0.342-0.346 0.18-0.77c-0.199-0.517-0.118-0.659 0.246-0.435 0.546 0.337 0.632 0.101 0.723-2.034 0.026-0.637 0.298-1.108 0.599-1.051 0.632 0.123 1.264-1.414 1.286-3.115 0.007-0.732 0.248-1.107 0.651-1.023 0.425 0.089 0.536-0.125 0.337-0.643-0.262-0.683-0.201-0.699 0.495-0.135 0.5 0.406 0.666 0.433 0.452 0.075-0.188-0.313-0.079-1.041 0.241-1.62s0.592-1.24 0.599-1.474c0.006-0.233 0.329-0.51 0.71-0.615 0.617-0.168 0.617-0.135 0 0.302-0.418 0.295-0.476 0.493-0.149 0.498 0.301 0.003 0.848-0.295 1.217-0.664s0.801-0.543 0.959-0.386c0.158 0.158 0.286 0.031 0.286-0.281s0.208-0.438 0.462-0.283c0.255 0.158 0.466-0.091 0.47-0.55 0.004-0.709 0.075-0.731 0.457-0.144 0.326 0.505 0.45 0.528 0.457 0.089 0.003-0.334 0.215-0.477 0.47-0.32s0.462 0.053 0.462-0.231c0-0.284 0.189-0.4 0.421-0.257 0.628 0.389 1.932-0.233 1.603-0.763-0.156-0.253 0.209-0.197 0.813 0.125 1.036 0.551 1.058 0.536 0.402-0.271-0.627-0.774-0.618-0.829 0.102-0.558 0.442 0.166 1.087 0.005 1.449-0.353 0.358-0.358 0.781-0.524 0.94-0.366s0.286 0.050 0.286-0.239c0-0.288 0.311-0.406 0.689-0.26 0.394 0.151 0.812-0.053 0.974-0.475 0.156-0.407 0.452-0.637 0.657-0.509 0.608 0.375 1.863-0.292 1.538-0.818-0.172-0.279-0.079-0.349 0.221-0.163 0.286 0.177 1.203-0.041 2.038-0.486s1.673-0.765 1.865-0.714c0.514 0.142 0.42-0.73-0.115-1.060-0.255-0.158-0.462-0.031-0.462 0.283s-0.123 0.444-0.274 0.293c-0.151-0.151-0.053-0.654 0.217-1.12s0.498-0.583 0.505-0.266c0.005 0.317 0.257 0.579 0.555 0.579s0.413-0.127 0.257-0.283c-0.156-0.156 0.189-1.039 0.765-1.966 1.87-3.004 1.871-3.010 1.169-2.659-0.357 0.178-0.182-0.022 0.392-0.447s1.040-1.255 1.040-1.844c0-0.592 0.24-1.221 0.533-1.403 0.332-0.204 0.409-0.127 0.204 0.204-0.532 0.861-0.087 0.615 0.834-0.462 0.83-0.971 0.83-0.988-0.014-0.693-0.812 0.284-0.809 0.26 0.033-0.392 0.494-0.382 0.716-0.697 0.49-0.697s-0.003-0.327 0.49-0.728c0.494-0.4 0.638-0.596 0.32-0.435-0.974 0.493-0.644-0.206 0.666-1.423 0.978-0.907 1.053-1.094 0.346-0.875-0.494 0.154-0.322-0.054 0.383-0.462 1.387-0.805 1.284-0.13 1.301-8.686 0.003-1.577 0.206-2.66 0.5-2.66 0.325 0 0.399-0.87 0.219-2.524-0.176-1.621-0.092-2.637 0.234-2.838 0.327-0.203 0.407-1.185 0.221-2.769-0.178-1.517-0.104-2.57 0.194-2.752 0.265-0.163 0.48-1.375 0.476-2.688-0.003-1.315 0.11-3.743 0.252-5.399s0.25-4.204 0.24-5.668c-0.007-1.612 0.18-2.66 0.483-2.66 0.337 0 0.399-0.892 0.192-2.738-0.202-1.785-0.135-2.846 0.192-3.046 0.276-0.171 0.5-1.385 0.5-2.699 0-6.473 0.135-8.637 0.546-8.637 0.274 0 0.322-1.007 0.123-2.596-0.252-2.017-0.195-2.517 0.252-2.241s0.51-0.277 0.281-2.471c-0.18-1.724-0.114-2.94 0.171-3.115 0.257-0.158 0.459-1.26 0.45-2.452-0.026-3.802 0.281-7.69 0.681-8.596 0.219-0.494 0.188-1.86-0.067-3.113-0.407-1.986-0.379-2.169 0.26-1.665 0.395 0.312-0.286-0.683-1.515-2.211s-2.577-3.348-2.998-4.046c-0.421-0.699-0.947-1.272-1.169-1.274-0.221 0-0.14 0.315 0.182 0.702s0.468 0.822 0.325 0.964c-0.144 0.144-0.492-0.175-0.776-0.705s-0.4-0.964-0.259-0.964c0.142 0-0.060-0.485-0.447-1.078s-0.549-1.252-0.357-1.464c0.192-0.212 0.128-0.26-0.142-0.106s-1.103-0.245-1.852-0.885c-0.748-0.639-1.050-1.028-0.666-0.865s0.069-0.26-0.695-0.943c-0.764-0.683-1.091-1.078-0.726-0.88s0.080-0.404-0.634-1.337c-1.216-1.595-2.103-2.346-1.373-1.163 0.206 0.334 0.127 0.409-0.212 0.199-0.298-0.185-0.437-0.507-0.307-0.714s-1.221-2.198-2.999-4.421c-1.779-2.223-3.235-4.224-3.235-4.448s-0.25-0.405-0.557-0.405c-0.432 0-0.432 0.123 0 0.557 0.305 0.305 0.548 0.774 0.541 1.040s-0.314 0.091-0.679-0.392c-0.366-0.483-0.557-1.055-0.421-1.272s-0.29-0.635-0.94-0.932c-0.652-0.296-1.183-0.74-1.183-0.985s0.365-0.185 0.81 0.132c0.445 0.317 0.226 0.031-0.489-0.637s-1.13-1.219-0.925-1.225c0.205-0.004-0.147-0.44-0.785-0.965s-0.896-0.82-0.578-0.656c0.635 0.327-1.103-2.834-1.79-3.259-0.233-0.144-0.305-0.453-0.161-0.687s-0.038-0.31-0.409-0.168c-0.369 0.142-0.829 0.003-1.021-0.308-0.238-0.385-0.142-0.459 0.301-0.228 0.357 0.186 0.070-0.116-0.638-0.673-1.089-0.854-1.196-1.123-0.695-1.74 0.498-0.615 0.476-0.687-0.139-0.457-0.407 0.152-0.732 0.034-0.732-0.267 0-0.615-3.33-5.385-3.761-5.385-0.154 0-0.031 0.25 0.276 0.557 0.686 0.686 0.718 1.296 0.065 1.296-0.269 0-0.37-0.193-0.224-0.432s0.039-0.57-0.236-0.741c-0.276-0.171-0.387-0.611-0.247-0.974s-0.053-0.783-0.428-0.927c-0.375-0.144-0.577-0.434-0.447-0.644s0.021-0.514-0.241-0.678c-0.262-0.163-0.375-0.461-0.252-0.661s-0.29-0.309-0.921-0.24c-0.632 0.069-1.026-0.067-0.88-0.304s0.425-0.332 0.618-0.212c0.194 0.12 0.353-0.075 0.353-0.434s-0.187-0.539-0.414-0.397c-0.47 0.291-2.207-1.133-2.207-1.807 0-0.243 0.365-0.044 0.81 0.44s0.617 0.561 0.38 0.166c-0.236-0.394-0.964-1.207-1.62-1.807s-0.964-0.974-0.688-0.83c0.276 0.144 0.502-0.021 0.502-0.368s-0.257-0.531-0.57-0.411c-0.313 0.12-0.727-0.036-0.921-0.346-0.244-0.396-0.154-0.447 0.301-0.175 0.885 0.532-0.212-0.959-1.882-2.562-0.662-0.637-0.901-0.949-0.528-0.695 0.483 0.331 0.44 0.099-0.151-0.81-0.455-0.7-1.063-1.271-1.354-1.265-0.288 0.003-0.212 0.209 0.169 0.457 1.095 0.708 0.096 0.536-1.411-0.243-0.75-0.389-1.365-0.921-1.365-1.183s-0.208-0.349-0.462-0.192c-0.255 0.158-0.462 0.053-0.462-0.231s-0.243-0.367-0.542-0.183c-0.372 0.231-0.442 0.074-0.221-0.5 0.271-0.705 0.212-0.745-0.384-0.25-0.522 0.432-0.704 0.442-0.704 0.038 0-0.301-0.187-0.432-0.414-0.291s-0.54-0.069-0.692-0.467c-0.18-0.467-0.594-0.622-1.166-0.442-0.489 0.156-1.043 0.034-1.232-0.271-0.216-0.349-0.146-0.432 0.192-0.222 0.293 0.182 0.533 0.221 0.533 0.087s-0.442-0.478-0.981-0.767c-0.675-0.362-0.885-0.368-0.67-0.021 0.195 0.317 0.038 0.399-0.426 0.221-0.406-0.156-0.637-0.383-0.514-0.507s-0.023-0.494-0.327-0.825c-0.449-0.491-0.72-0.461-1.468 0.166-0.864 0.721-0.88 0.719-0.291-0.043 0.651-0.839 0.346-1.050-1.228-0.843-0.322 0.041-0.716-0.262-0.875-0.678-0.233-0.603-0.368-0.633-0.68-0.137-0.297 0.467-0.393 0.416-0.399-0.219-0.003-0.461-0.215-0.708-0.47-0.55s-0.462 0.053-0.462-0.231c0-0.284-0.208-0.389-0.462-0.231s-0.462 0.053-0.462-0.231c0-0.284-0.208-0.389-0.462-0.231s-0.462 0.072-0.462-0.189c0-0.262-0.296-0.362-0.659-0.222-0.689 0.265-1.842-1.327-1.33-1.84 0.161-0.161 0.168-0.293 0.017-0.293s-0.601 0.327-1.003 0.727c-0.654 0.654-0.727 0.654-0.727 0.007 0-0.858-3.512-2.76-4.252-2.303-0.295 0.182-0.39 0.113-0.221-0.161 0.163-0.265-0.325-0.738-1.086-1.053s-1.382-0.755-1.382-0.979c0-0.224-0.365-0.4-0.81-0.393-0.771 0.015-0.771 0.037 0 0.486 0.893 0.522 1.090 1.351 0.32 1.351-0.269 0-0.407-0.156-0.305-0.346 0.243-0.454-1.361-1.972-1.711-1.62-0.148 0.148-0.27 0-0.27-0.327 0-0.459-0.151-0.473-0.644-0.061-0.51 0.423-0.632 0.373-0.579-0.24 0.037-0.426-0.193-0.868-0.511-0.981s-0.579 0.111-0.579 0.497c0 0.823 2.502 2.735 2.969 2.269 0.176-0.176 0.182 0.038 0.016 0.474s-0.524 0.658-0.789 0.493c-0.276-0.171-0.349-0.080-0.168 0.214 0.175 0.284 0.676 0.389 1.111 0.235 0.656-0.231 0.692-0.168 0.204 0.356-0.484 0.522-0.401 0.791 0.462 1.497 0.579 0.474 0.741 0.721 0.358 0.549s-0.174 0.142 0.462 0.695c0.637 0.553 0.844 0.868 0.462 0.7s-0.29 0.021 0.204 0.423c0.657 0.532 0.719 0.732 0.231 0.743-0.491 0.007-0.452 0.139 0.147 0.488 0.447 0.26 1.072 0.323 1.387 0.137s0.171 0.125-0.325 0.688c-0.699 0.796-0.734 0.962-0.163 0.743 0.406-0.156 0.894-0.027 1.087 0.284 0.203 0.327 0.158 0.447-0.108 0.284-0.252-0.156-0.457 0-0.457 0.348 0 0.346 0.239 0.539 0.53 0.426s0.914 0.221 1.382 0.738c0.647 0.716 0.697 0.944 0.202 0.952-0.4 0.005-0.205 0.377 0.507 0.965 0.637 0.526 0.844 0.817 0.462 0.651s-0.29 0.021 0.204 0.423c0.494 0.4 0.716 0.728 0.49 0.728s-0.003 0.327 0.49 0.728c0.494 0.4 0.616 0.582 0.271 0.406-0.44-0.226-0.398 0.024 0.144 0.852 0.694 1.058 1.928 1.93 2.58 1.822 0.135-0.021 0.248 0.346 0.248 0.818s-0.185 0.745-0.411 0.604c-1.015-0.627 0.703 1.666 1.775 2.366 0.661 0.433 1.115 1.036 1.007 1.338-0.301 0.832 0.596 2.27 1.255 2.017 0.32-0.123 0.58 0.060 0.58 0.406s-0.205 0.503-0.457 0.348c-0.257-0.158-0.312-0.046-0.127 0.252 0.182 0.293 0.682 0.538 1.111 0.541 0.662 0.004 0.678 0.073 0.101 0.438-0.589 0.372-0.579 0.47 0.071 0.72 0.466 0.18 0.705 0.718 0.63 1.418-0.106 0.967-0.021 1.046 0.596 0.546 0.528-0.428 0.632-0.433 0.382-0.021-0.189 0.315 0.060 0.957 0.565 1.466 0.497 0.497 0.782 1.025 0.635 1.174s-0.046 0.27 0.226 0.27c0.274 0 0.394 0.395 0.267 0.878s0.125 1.42 0.559 2.082c0.602 0.921 0.714 1.704 0.468 3.337-0.211 1.405-0.159 2.034 0.151 1.841 0.303-0.188 0.377 0.29 0.206 1.339-0.169 1.041-0.080 1.631 0.248 1.631s0.406 0.526 0.219 1.459c-0.217 1.085-0.147 1.37 0.272 1.111 0.425-0.262 0.484 0.077 0.24 1.383-0.243 1.294-0.185 1.645 0.231 1.387s0.475 0.095 0.231 1.387c-0.243 1.294-0.185 1.645 0.231 1.387s0.475 0.095 0.231 1.387c-0.253 1.349-0.193 1.651 0.269 1.365 0.452-0.279 0.522-0.072 0.286 0.86-0.2 0.798-0.047 1.625 0.436 2.363 0.546 0.834 0.666 1.703 0.454 3.276-0.204 1.504-0.139 2.046 0.221 1.824 0.356-0.221 0.416 0.188 0.198 1.349-0.203 1.081-0.135 1.666 0.19 1.666 0.317 0 0.395 0.54 0.211 1.459-0.217 1.085-0.147 1.37 0.272 1.111 0.425-0.262 0.486 0.065 0.252 1.316-0.203 1.081-0.135 1.666 0.19 1.666 0.317 0 0.395 0.54 0.211 1.459-0.193 0.967-0.127 1.358 0.194 1.159 0.267-0.166 0.605 0.007 0.751 0.391s0.063 0.691-0.183 0.691c-0.247 0-0.447 0.429-0.447 0.953s0.171 0.847 0.381 0.717c0.211-0.13 0.368 1.212 0.349 2.981s-0.212 3.216-0.432 3.216c-0.219 0-0.272 0.508-0.116 1.127s0.065 1.262-0.2 1.427c-0.266 0.164-0.356 0.698-0.202 1.185 0.166 0.523 0.063 0.886-0.25 0.886-0.353 0-0.425 0.482-0.212 1.447 0.243 1.103 0.178 1.361-0.267 1.086s-0.513-0.005-0.288 1.122c0.211 1.053 0.144 1.387-0.231 1.158s-0.442 0.105-0.231 1.158c0.185 0.926 0.125 1.377-0.159 1.201-0.653-0.402-0.565 1.328 0.099 1.995 0.432 0.432 0.369 0.557-0.283 0.557-0.661 0-0.775 0.243-0.546 1.158 0.178 0.708 0.092 1.158-0.218 1.158-0.327 0-0.397 0.511-0.192 1.447 0.241 1.097 0.178 1.36-0.262 1.087-0.433-0.268-0.502-0.048-0.272 0.868 0.193 0.769 0.118 1.226-0.2 1.226-0.327 0-0.397 0.511-0.192 1.447 0.231 1.048 0.169 1.355-0.224 1.111-0.396-0.244-0.462 0.146-0.246 1.446 0.229 1.385 0.178 1.626-0.229 1.087-0.413-0.545-0.462-0.37-0.228 0.81 0.2 1.019 0.119 1.503-0.254 1.503-0.356 0-0.447 0.409-0.26 1.158 0.18 0.721 0.094 1.158-0.231 1.158s-0.413 0.435-0.231 1.158c0.18 0.721 0.094 1.158-0.231 1.158-0.323 0-0.413 0.433-0.238 1.132 0.2 0.796 0.043 1.284-0.528 1.647-0.508 0.322-0.618 0.598-0.293 0.738 0.31 0.133 0.447 1.007 0.339 2.171-0.099 1.072-0.228 4.949-0.288 8.62-0.085 5.202-0.25 6.719-0.75 6.885-0.389 0.13-0.514 0.006-0.317-0.309 0.178-0.288 0.089-0.524-0.194-0.524s-0.367-0.243-0.183-0.542c0.225-0.364 0.082-0.445-0.435-0.246-0.423 0.163-0.77 0.111-0.77-0.114s0.26-0.514 0.579-0.642c0.439-0.177 0.442-0.243 0.015-0.27-0.31-0.019-0.445-0.348-0.3-0.726 0.149-0.387-0.047-0.811-0.447-0.964-0.392-0.151-0.607-0.547-0.479-0.882 0.24-0.623-1.93-3.144-3.104-3.611-0.498-0.197-0.467-0.269 0.129-0.296 0.433-0.019 0.998 0.327 1.258 0.773s0.476 0.601 0.478 0.346c0.014-0.724-0.595-1.426-1.525-1.772-0.618-0.228-0.919-0.062-1.094 0.606-0.132 0.507-0.425 0.808-0.653 0.668s-0.411 0-0.411 0.313c0 0.313-0.129 0.438-0.286 0.279-0.3-0.3-5.729 2.271-5.729 2.711 0 0.139 0.26 0.12 0.579-0.041s0.168 0.038-0.334 0.446c-0.502 0.406-1.142 0.652-1.421 0.543s-0.769 0.221-1.087 0.729c-0.369 0.593-0.899 0.841-1.471 0.691-0.491-0.128-0.894-0.044-0.894 0.186s-0.156 0.395-0.346 0.365c-0.943-0.149-1.526 0.106-1.254 0.548 0.168 0.271 0.022 0.439-0.32 0.372-1.068-0.207-2.686 0.544-3.188 1.478-0.386 0.721-0.326 0.894 0.309 0.894 0.433 0 0.663 0.123 0.514 0.274s-0.733 0.127-1.296-0.050c-0.798-0.253-0.882-0.413-0.382-0.729 0.426-0.27 0.454-0.408 0.084-0.412-0.305-0.003-0.741 0.324-0.967 0.728s-0.548 0.717-0.717 0.695c-1.123-0.152-1.322-0.016-0.832 0.577 0.659 0.793 0.955 0.766-10.577 0.993-5.217 0.103-13.963 0.306-19.435 0.452-17.913 0.478-19.98 0.488-20.221 0.099-0.131-0.212-0.716-0.265-1.297-0.119-0.707 0.178-1.147 0.029-1.332-0.445-0.214-0.559-0.473-0.603-1.204-0.212-0.682 0.365-0.932 0.356-0.932-0.029 0-0.334-0.428-0.421-1.158-0.239-0.708 0.178-1.158 0.092-1.158-0.218 0-0.327-0.511-0.397-1.447-0.192-1.092 0.24-1.359 0.176-1.090-0.259 0.272-0.44-0.051-0.5-1.377-0.252-1.298 0.243-1.647 0.185-1.386-0.235 0.253-0.409 0.017-0.477-0.88-0.252-0.782 0.197-1.225 0.12-1.225-0.214 0-0.325-0.435-0.413-1.158-0.231-0.725 0.182-1.158 0.094-1.158-0.236 0-0.313-0.474-0.434-1.167-0.303-0.642 0.123-1.291 0.024-1.438-0.214s-0.786-0.309-1.415-0.151c-0.822 0.206-1.050 0.131-0.804-0.266 0.25-0.406-0.048-0.476-1.12-0.262-0.899 0.18-1.459 0.105-1.459-0.195 0-0.309-0.547-0.368-1.494-0.159-1.298 0.285-1.42 0.236-0.915-0.369 0.502-0.603 0.387-0.659-0.844-0.407-0.887 0.182-3.88 2.38-7.957 5.844-3.594 3.054-6.699 5.502-6.899 5.442s-0.234 0.234-0.072 0.655c0.197 0.512 0.115 0.655-0.248 0.43-0.359-0.221-0.542 0.063-0.542 0.849 0 0.653 0.212 1.183 0.471 1.183 0.271 0 0.349 0.49 0.182 1.158-0.176 0.7-0.092 1.158 0.212 1.158 0.315 0 0.394 0.541 0.209 1.459-0.221 1.108-0.151 1.373 0.293 1.099 0.447-0.276 0.507-0.007 0.254 1.142-0.247 1.123-0.188 1.415 0.231 1.158 0.418-0.259 0.48 0.023 0.243 1.099-0.19 0.865-0.132 1.447 0.144 1.447 0.259 0 0.396 0.868 0.313 1.966-0.127 1.668-0.297 1.958-1.125 1.911-0.697-0.039-0.848 0.101-0.529 0.484 0.31 0.374-0.131 1.175-1.431 2.601l-1.879 2.061 1.701-2.198c0.935-1.209 1.531-2.19 1.323-2.18-0.575 0.026-3.616 4.281-3.628 5.072-0.005 0.4-0.351 0.637-0.816 0.558-0.442-0.076-0.928 0.183-1.078 0.577s-0.461 0.598-0.687 0.457c-0.228-0.142-0.414 0-0.414 0.312s-0.137 0.43-0.305 0.262-0.613 0.131-0.986 0.663c-0.539 0.767-0.762 0.839-1.075 0.344-0.317-0.5-0.398-0.505-0.404-0.019-0.003 0.334-0.215 0.477-0.47 0.32s-0.462-0.053-0.462 0.231c0 0.284-0.208 0.389-0.462 0.231s-0.462 0.063-0.462 0.49c0 0.549-0.271 0.704-0.933 0.531-0.513-0.134-1.242 0.065-1.62 0.442s-0.686 0.522-0.686 0.317c0-0.202-0.338 0.116-0.753 0.708-0.592 0.843-0.839 0.94-1.149 0.45-0.317-0.5-0.398-0.505-0.404-0.019-0.003 0.334-0.215 0.477-0.47 0.32s-0.462-0.053-0.462 0.231c0 0.284-0.208 0.389-0.462 0.231s-0.462 0.063-0.462 0.49c0 0.549-0.271 0.704-0.933 0.531-0.513-0.134-1.242 0.065-1.62 0.442s-0.686 0.522-0.686 0.317c0-0.202-0.338 0.116-0.753 0.708-0.592 0.843-0.839 0.94-1.149 0.45-0.317-0.5-0.398-0.505-0.404-0.019-0.003 0.334-0.215 0.477-0.47 0.32s-0.462-0.053-0.462 0.231c0 0.284-0.208 0.389-0.462 0.231s-0.462 0.063-0.462 0.49c0 0.547-0.271 0.704-0.925 0.533-0.509-0.133-0.925-0.041-0.925 0.205s-0.208 0.317-0.462 0.161c-0.255-0.158-0.462-0.031-0.462 0.283s-0.13 0.438-0.288 0.279c-0.159-0.159-0.673 0.171-1.142 0.732s-0.859 0.779-0.868 0.481c-0.006-0.297 0.244-0.644 0.563-0.773 0.453-0.183 0.45-0.241-0.015-0.27-0.326-0.019-0.712 0.274-0.858 0.656s-0.452 0.577-0.68 0.435c-0.228-0.142-0.414-0.036-0.414 0.234s-0.156 0.465-0.346 0.435c-0.977-0.154-1.524 0.111-1.223 0.596 0.225 0.364 0.082 0.445-0.435 0.246-0.423-0.163-0.77-0.082-0.77 0.18s-0.208 0.346-0.462 0.189c-0.255-0.158-0.462-0.031-0.462 0.283s-0.13 0.438-0.288 0.279c-0.159-0.159-0.673 0.171-1.142 0.732s-0.859 0.779-0.868 0.481c-0.006-0.297 0.244-0.644 0.563-0.773 0.453-0.183 0.45-0.241-0.015-0.27-0.326-0.019-0.712 0.274-0.858 0.656s-0.452 0.577-0.68 0.435c-0.228-0.142-0.414-0.036-0.414 0.234s-0.156 0.465-0.346 0.435c-0.977-0.154-1.524 0.111-1.223 0.596 0.226 0.368 0.080 0.443-0.46 0.236-0.471-0.18-0.679-0.119-0.511 0.152 0.156 0.252 0.068 0.457-0.193 0.457s-0.342 0.346-0.18 0.77c0.216 0.563 0.128 0.666-0.325 0.387s-0.541-0.176-0.325 0.387c0.164 0.428 0.071 0.77-0.209 0.77-0.325 0-0.293 0.257 0.092 0.721 0.494 0.596 0.484 0.697-0.059 0.579-0.418-0.089-0.602 0.171-0.51 0.716 0.101 0.581-0.037 0.743-0.425 0.503-0.393-0.243-0.484-0.125-0.292 0.378 0.161 0.421 0 0.839-0.384 0.987-0.366 0.14-0.651 0.625-0.634 1.079 0.027 0.721 0.082 0.709 0.43-0.101 0.262-0.612 0.406-0.688 0.423-0.231 0.015 0.381-0.176 1.007-0.423 1.387s-0.459 0.486-0.469 0.231c-0.014-0.255-0.248-0.074-0.524 0.401-0.38 0.656-0.349 0.962 0.13 1.265 0.488 0.309 0.436 0.401-0.226 0.408-0.584 0.003-0.771 0.236-0.582 0.725 0.178 0.466 0.089 0.601-0.255 0.391-0.361-0.222-0.447-0.006-0.269 0.672 0.169 0.644 0.085 0.89-0.236 0.69-0.315-0.195-0.39-0.023-0.203 0.462 0.163 0.425 0.071 0.77-0.202 0.77s-0.378 0.313-0.231 0.695c0.147 0.381 0.070 0.695-0.168 0.695s-0.301 0.346-0.138 0.77c0.216 0.563 0.128 0.666-0.325 0.387s-0.541-0.176-0.325 0.387c0.163 0.425 0.071 0.77-0.202 0.77s-0.374 0.324-0.221 0.719c0.173 0.45 0.086 0.601-0.231 0.405-0.342-0.212-0.399 0.023-0.176 0.727 0.248 0.78 0.182 0.951-0.26 0.677-0.42-0.26-0.507-0.143-0.295 0.405 0.199 0.521 0.057 0.77-0.442 0.77-0.634 0-0.646 0.109-0.101 0.782 0.432 0.531 0.449 0.673 0.057 0.435-0.865-0.521-0.723 1.018 0.178 1.917 0.495 0.495 0.576 0.816 0.231 0.931-0.659 0.22-0.697 1.949-0.041 1.949 0.303 0 0.369 0.607 0.18 1.619-0.205 1.092-0.13 1.619 0.231 1.619s0.436 0.527 0.231 1.619c-0.189 1.007-0.123 1.619 0.177 1.619 0.265 0 0.481 0.781 0.482 1.736 0 3.656 0.069 4.228 0.538 4.518 0.283 0.174 0.358 0.955 0.185 1.881-0.166 0.888-0.106 1.583 0.137 1.583 0.239 0 0.38 0.958 0.314 2.129s0.003 2.005 0.153 1.855c0.151-0.15 1.177 0.466 2.281 1.37s2.221 1.512 2.483 1.35c0.262-0.162 0.42-0.028 0.349 0.297-0.116 0.545 0.067 0.641 1.546 0.807 0.284 0.033 0.408 0.475 0.274 0.985s-0.053 0.81 0.177 0.665c0.231-0.142 0.42-0.061 0.42 0.182s0.365 0.579 0.81 0.749c0.651 0.247 0.695 0.193 0.231-0.274-0.317-0.321-0.579-0.781-0.579-1.023s0.625 0.425 1.391 1.481c0.765 1.057 1.519 1.92 1.675 1.92s0-0.364-0.351-0.81c-0.574-0.729-0.558-0.749 0.156-0.189 0.435 0.341 0.918 1.018 1.072 1.503s0.488 0.883 0.74 0.883c0.252 0 0.317-0.228 0.144-0.509-0.207-0.334 0.043-0.317 0.729 0.050 0.733 0.393 0.876 0.661 0.48 0.907-0.406 0.251-0.326 0.452 0.284 0.718 0.466 0.203 1.108 0.327 1.427 0.276s0.579 0.195 0.579 0.549c0 0.354 0.465 1.039 1.034 1.524 1.024 0.874 1.026 0.873 0.346-0.043-0.639-0.863-0.631-0.881 0.144-0.274 0.475 0.374 0.711 0.962 0.555 1.375-0.152 0.398-0.127 0.574 0.057 0.388s0.68-0.020 1.104 0.363c0.665 0.601 0.77 0.605 0.77 0.021 0-0.371-0.205-0.548-0.457-0.393-0.262 0.162-0.311 0.045-0.113-0.274 0.556-0.898 1.541 0.247 1.391 1.615-0.072 0.657-0.007 1.073 0.137 0.925s0.599-0.015 1.005 0.298c0.42 0.323 0.337 0.086-0.188-0.55l-0.925-1.119 1.225 0.894c0.675 0.491 1.147 1.022 1.053 1.179s0.291 0.673 0.858 1.144c0.565 0.474 0.77 0.563 0.452 0.201-0.847-0.964-0.707-1.595 0.161-0.726 0.406 0.406 0.617 1.057 0.467 1.445-0.154 0.402-0.074 0.584 0.187 0.423 0.252-0.156 0.457-0.084 0.457 0.158s0.324 0.564 0.719 0.716c0.48 0.185 0.614 0.086 0.401-0.298-0.174-0.315 0.358 0.156 1.183 1.046s1.32 1.298 1.103 0.904c-0.219-0.394-0.036-0.288 0.406 0.231s1.114 1.274 1.493 1.669c0.379 0.398 0.88 1.542 1.115 2.545 0.257 1.098 0.445 1.407 0.474 0.781 0.024-0.575 0.248-1.040 0.495-1.040 0.464 0 0.163 1.237-0.515 2.125-0.212 0.277-0.283 0.612-0.152 0.739s-1.278 0.274-3.127 0.322c-1.851 0.048-3.778 0.182-4.288 0.298s-0.655 0.086-0.323-0.064c0.459-0.208 0.402-0.435-0.231-0.945-0.771-0.622-0.759-0.637 0.176-0.185 1.26 0.611 6.935 0.644 7.305 0.046 0.731-1.184-5.601-3.34-8.918-3.037-1.502 0.137-2.311-0.101-3.853-1.138-1.072-0.72-2.212-1.212-2.531-1.087s-0.935-0.317-1.367-0.978c-0.661-1.010-0.902-1.107-1.512-0.615-0.565 0.459-0.649 0.46-0.377 0.006 0.527-0.879-0.12-1.658-0.865-1.040-0.447 0.371-0.704 0.358-0.955-0.048-0.219-0.353-0.148-0.436 0.19-0.226 1.219 0.753 0.417-0.628-0.971-1.669-1.957-1.47-3.23-2.115-1.736-0.882 0.637 0.525 0.907 0.96 0.601 0.965s-1.084-0.681-1.727-1.526c-0.644-0.846-1.69-1.814-2.322-2.151s-1.019-0.844-0.859-1.123c0.159-0.28 0.116-0.352-0.094-0.161s-0.863 0.036-1.449-0.348c-0.977-0.64-1.041-0.633-0.783 0.114 0.154 0.446-0.562-0.098-1.593-1.207-1.152-1.242-2.087-1.887-2.431-1.675-0.305 0.188-0.738 0.057-0.959-0.294-0.677-1.070-0.464-1.569 0.26-0.612 0.374 0.494 0.686 0.739 0.695 0.545 0.021-0.508-0.975-2.211-2.183-3.73-0.575-0.721-1.045-1.658-1.048-2.083-0.004-0.668-0.067-0.68-0.457-0.081-0.257 0.399-0.452 0.464-0.457 0.154-0.003-0.297-0.131-0.922-0.283-1.387-0.228-0.704-0.192-0.731 0.224-0.154 0.639 0.887 0.666 0 0.051-1.618-0.254-0.667-0.666-1.088-0.918-0.932-0.284 0.175-0.349-0.252-0.175-1.127 0.173-0.864 0.092-1.411-0.209-1.411s-0.393-0.611-0.238-1.561c0.144-0.893 0.039-1.691-0.245-1.87-0.284-0.175-0.38-0.899-0.225-1.678 0.183-0.916 0.087-1.37-0.286-1.37-0.399 0-0.471-0.538-0.249-1.852 0.218-1.286 0.147-1.852-0.231-1.852s-0.448-0.563-0.231-1.852c0.212-1.255 0.144-1.852-0.212-1.852-0.346 0-0.425-0.535-0.231-1.567 0.161-0.861 0.101-1.686-0.135-1.832s-0.352-1.471-0.259-2.945c0.168-2.644 0.161-2.662-0.521-1.293l-0.689 1.387 0.325-1.387c0.178-0.763 0.313-2.262 0.296-3.33-0.017-1.15 0.156-1.829 0.423-1.664 0.267 0.164 0.345-0.129 0.192-0.719-0.144-0.548-0.072-1.114 0.159-1.256s0.416-0.507 0.412-0.811c-0.003-0.332-0.186-0.274-0.457 0.144-0.383 0.594-0.449 0.577-0.457-0.115-0.003-0.445 0.327-0.81 0.738-0.81 0.569 0 0.675-0.281 0.444-1.195-0.215-0.858-0.146-1.101 0.249-0.857 0.398 0.246 0.462-0.006 0.231-0.925s-0.166-1.171 0.231-0.925c0.396 0.244 0.462-0.003 0.241-0.887-0.192-0.763-0.118-1.226 0.195-1.226 0.303 0 0.378-0.393 0.192-0.983-0.229-0.724-0.158-0.887 0.274-0.621 0.426 0.262 0.514 0.091 0.325-0.637-0.144-0.548-0.063-1.12 0.177-1.269s0.303-0.697 0.138-1.216c-0.203-0.639-0.131-0.839 0.224-0.62 0.372 0.231 0.44-0.2 0.233-1.493-0.226-1.418-0.128-1.878 0.444-2.099 0.44-0.169 0.621-0.576 0.454-1.012-0.173-0.45-0.059-0.678 0.297-0.596 0.317 0.074 0.683-0.154 0.812-0.509 0.142-0.391 0.031-0.517-0.284-0.324-0.329 0.204-0.418 0.060-0.243-0.396 0.151-0.396 0.511-0.567 0.805-0.384 0.814 0.509 1.723 0.559 1.425 0.079-0.147-0.236-0.46-0.31-0.697-0.163-0.77 0.476-0.455-0.023 0.565-0.899 0.887-0.761 0.964-0.767 0.695-0.065-0.2 0.524-0.108 0.718 0.276 0.577 0.317-0.116 0.531-0.432 0.474-0.7-0.062-0.286 0.398-0.397 1.116-0.267 0.995 0.18 1.123 0.103 0.695-0.414-0.396-0.476-0.392-0.771 0.017-1.18 0.801-0.801 1.3-0.661 0.952 0.265-0.265 0.707-0.205 0.729 0.473 0.178 0.426-0.346 0.641-0.848 0.478-1.111-0.173-0.279-0.074-0.344 0.236-0.151 0.293 0.182 0.533 0.11 0.533-0.159s0.156-0.465 0.346-0.435c0.993 0.156 1.542-0.173 2.213-1.327 0.448-0.772 0.635-0.909 0.476-0.346-0.259 0.911-0.252 0.913 0.433 0.026 0.382-0.494 0.697-0.716 0.697-0.49s0.327 0.003 0.728-0.49c0.4-0.494 0.576-0.585 0.387-0.204s0.204 0.122 0.87-0.579c1.377-1.447 1.818-1.613 1.401-0.523-0.24 0.626-0.123 0.611 0.708-0.103 1.038-0.89 1.333-1.373 0.533-0.879-0.255 0.158-0.462 0.065-0.462-0.204s0.365-0.514 0.81-0.543c1.722-0.113 1.947-0.205 2.675-1.103 0.413-0.509 0.598-0.634 0.413-0.276-0.365 0.702-0.358 0.699 2.646-1.169 0.926-0.577 1.812-0.921 1.966-0.765s0.283 0.080 0.283-0.17c0-0.248 0.327-0.856 0.728-1.349s0.582-0.617 0.407-0.274c-0.259 0.505-0.131 0.524 0.659 0.099 0.541-0.288 0.981-0.726 0.981-0.971s0.156-0.421 0.346-0.392c0.993 0.156 1.542-0.173 2.213-1.327 0.448-0.772 0.635-0.909 0.476-0.346-0.259 0.911-0.252 0.913 0.433 0.026 0.382-0.494 0.697-0.716 0.697-0.49s0.327 0.003 0.728-0.49c0.4-0.494 0.596-0.638 0.435-0.32-0.493 0.974 0.204 0.644 1.478-0.695 0.665-0.7 1.057-0.961 0.87-0.579s-0.015 0.29 0.387-0.204c0.4-0.494 0.728-0.716 0.728-0.49s0.313 0.003 0.697-0.49c0.684-0.885 0.691-0.885 0.428 0.026-0.243 0.839-0.219 0.85 0.255 0.112 0.286-0.447 0.368-1.072 0.177-1.387-0.262-0.438-0.171-0.433 0.383 0.017 0.598 0.484 0.775 0.468 0.988-0.085 0.142-0.371 0.712-0.918 1.267-1.215 0.772-0.413 0.928-0.412 0.668 0.005-0.475 0.767 2.384-0.762 3.49-1.868 0.495-0.495 0.901-0.791 0.901-0.658 0 0.767 2.586-1.25 2.646-2.064 0.043-0.576-0.125-0.853-0.411-0.676-0.315 0.195-0.369-0.063-0.154-0.743 0.248-0.78 0.182-0.95-0.26-0.676-0.42 0.26-0.507 0.142-0.295-0.405 0.163-0.423 0.101-0.77-0.138-0.77s-0.314-0.313-0.168-0.695c0.147-0.381 0.042-0.695-0.231-0.695s-0.365-0.346-0.202-0.77c0.216-0.563 0.128-0.666-0.325-0.387s-0.541 0.176-0.325-0.387c0.203-0.529 0.046-0.781-0.5-0.807-0.483-0.021-0.567-0.128-0.217-0.27 0.317-0.128 0.579-0.541 0.579-0.916s-0.205-0.557-0.457-0.399c-0.262 0.163-0.339-0.022-0.182-0.435 0.163-0.423 0.026-0.719-0.329-0.719-0.47 0-0.481-0.123-0.050-0.557 0.305-0.305 0.557-1.031 0.557-1.613 0-0.62 0.858-1.952 2.082-3.231 2.326-2.428 2.752-3.222 0.81-1.51l-1.274 1.122 1.122-1.274c0.617-0.7 1.39-1.274 1.719-1.274 0.738 0 1.644-1.596 1.193-2.096-0.182-0.201-0.102-0.236 0.176-0.078s1.091-0.321 1.805-1.067c0.714-0.745 1.526-1.225 1.805-1.067s0.349 0.115 0.156-0.098c-0.193-0.212 0.119-0.887 0.695-1.5s0.993-1.2 0.93-1.306c-0.334-0.561-0.029-1.12 0.613-1.12 0.862 0 2.13-1.154 1.719-1.565-0.158-0.158-0.031-0.286 0.281-0.286s0.438-0.208 0.283-0.462c-0.158-0.255-0.043-0.462 0.254-0.462 0.303 0 0.416 0.323 0.259 0.732-0.18 0.469-0.073 0.657 0.298 0.522 0.317-0.116 0.526-0.451 0.461-0.743-0.078-0.346 0.284-0.429 1.040-0.239 0.666 0.168 1.159 0.091 1.159-0.178 0-0.259 0.468-0.367 1.040-0.241 0.837 0.183 0.904 0.13 0.346-0.267-0.599-0.428-0.582-0.497 0.115-0.503 0.445-0.003 0.81 0.173 0.81 0.394 0 0.222 0.875 0.231 1.966 0.019 2.639-0.51 2.767-0.505 2.384 0.115-0.226 0.365-0.020 0.454 0.644 0.281 0.538-0.14 1.077-0.017 1.2 0.274 0.144 0.337 0.508 0.193 1.005-0.397 0.771-0.914 0.778-0.911 0.524 0.161-0.214 0.901-0.105 1.047 0.637 0.853 0.492-0.128 0.894-0.022 0.894 0.236s0.313 0.349 0.695 0.203c0.381-0.147 0.695-0.061 0.695 0.192 0 0.262 0.521 0.327 1.226 0.149 0.944-0.236 1.14-0.168 0.849 0.302-0.272 0.44-0.163 0.527 0.394 0.313 0.425-0.163 0.77-0.071 0.77 0.202s0.313 0.378 0.695 0.231c0.446-0.171 0.695 0.007 0.695 0.507 0 0.599 0.123 0.649 0.557 0.217 0.305-0.305 0.877-0.551 1.274-0.548 0.633 0.005 0.635 0.060 0.022 0.457-0.559 0.361-0.485 0.45 0.375 0.457 0.638 0.003 0.945-0.192 0.764-0.485-0.207-0.334 0.249-0.389 1.428-0.168 1.344 0.252 1.651 0.193 1.366-0.266s0.021-0.518 1.366-0.266c1.271 0.238 1.642 0.18 1.398-0.217-0.243-0.393 0.063-0.454 1.111-0.224 0.971 0.214 1.447 0.142 1.447-0.219 0-0.356 0.468-0.433 1.385-0.233 0.966 0.212 1.605 0.084 2.115-0.426 0.401-0.401 0.975-0.726 1.276-0.723 0.344 0.003 0.305 0.159-0.104 0.418-0.467 0.296-0.319 0.392 0.533 0.339 0.652-0.038 1.103-0.204 1.002-0.366s0.127-0.55 0.502-0.862c0.855-0.709 0.401-1.552-0.624-1.161-0.578 0.219-0.635 0.161-0.228-0.248 0.37-0.374 0.787-0.397 1.363-0.074 0.639 0.358 1.18 0.197 2.358-0.701 0.839-0.64 1.529-1.43 1.529-1.756s0.342-0.721 0.76-0.882c0.664-0.255 0.679-0.192 0.115 0.512-0.353 0.442 0.658-0.192 2.249-1.404s2.892-2.509 2.892-2.875c0-0.367 0.221-0.666 0.49-0.666s0.358 0.26 0.197 0.579c-0.161 0.317 0.137 0.087 0.663-0.511s0.865-1.24 0.749-1.426c-0.115-0.186 0.091-0.337 0.459-0.337 0.54 0 0.558 0.137 0.095 0.712-0.314 0.392 0.212 0.123 1.171-0.599 1.313-0.988 1.62-1.459 1.251-1.906-0.352-0.423-0.334-0.691 0.065-0.937 0.337-0.208 0.436-0.149 0.251 0.151-0.168 0.272-0.044 0.584 0.276 0.695 0.778 0.267 5.997-3.762 5.632-4.349-0.156-0.254-0.003-0.423 0.339-0.375 1.33 0.183 1.781-0.058 1.781-0.95 0-0.509 0.221-0.925 0.49-0.925s0.353 0.26 0.185 0.579c-0.168 0.317 0.46-0.255 1.394-1.274s1.582-1.613 1.437-1.32c-0.144 0.292 0.036 0.643 0.398 0.783 0.459 0.176 0.607-0.018 0.489-0.637-0.095-0.493 0.063-0.842 0.358-0.783 0.291 0.058 0.539-0.188 0.55-0.549s0.311-0.752 0.663-0.87c0.445-0.148 0.524-0.018 0.259 0.425-0.315 0.524-0.239 0.522 0.421-0.017 0.686-0.558 0.795-1.397 0.727-5.623-0.043-2.731 0.059-5.192 0.228-5.466 0.421-0.681 3.099-0.312 3.706 0.512 0.271 0.368 0.783 0.574 1.137 0.454 0.358-0.12 0.513-0.005 0.349 0.259-0.305 0.493 1.038 1.238 1.466 0.813 0.133-0.133 0-0.243-0.298-0.243s-0.533-0.26-0.525-0.579c0.006-0.317 0.399-0.119 0.868 0.443s0.983 0.892 1.142 0.732c0.159-0.159 0.288-0.035 0.288 0.279s0.208 0.438 0.462 0.283c0.255-0.158 0.462-0.065 0.462 0.204 0 0.948 1.221 0.475 2.694-1.045 0.818-0.844 1.361-1.659 1.207-1.814-0.259-0.259 1.498-1.457 2.192-1.494 0.17-0.006 1.679-1.242 3.356-2.741l3.046-2.724v-18.88c0-16.933-0.229-20.933-1.079-18.822-0.147 0.362-0.248 0.285-0.271-0.201-0.019-0.43-0.277-0.931-0.571-1.111-0.362-0.224-0.409-0.12-0.148 0.32 0.212 0.357-0.255-0.026-1.039-0.854s-1.602-1.5-1.818-1.493c-0.216 0.004 0.127 0.414 0.764 0.907 1.079 0.837 1.090 0.879 0.147 0.638-1.007-0.259-1.728-1.334-1.325-1.983 0.116-0.189-0.091-0.379-0.462-0.421s-0.995-0.112-1.387-0.154c-0.393-0.042-0.62-0.32-0.507-0.617s-0.065-0.541-0.399-0.541c-0.337 0-0.508 0.324-0.383 0.732 0.168 0.55 0.092 0.53-0.301-0.081-0.286-0.447-0.416-0.986-0.286-1.195s-0.047-0.382-0.394-0.382c-0.357 0-0.536 0.317-0.413 0.732 0.119 0.402-0.115 0.161-0.523-0.541-0.838-1.445-1.262-1.666-0.894-0.462 0.137 0.445-0.038 0.332-0.389-0.252s-1.875-1.918-3.387-2.966c-1.513-1.048-2.459-1.555-2.105-1.125 0.562 0.68 0.547 0.743-0.112 0.49-0.502-0.193-0.673-0.624-0.507-1.286 0.175-0.697-0.32-1.76-1.649-3.538-1.666-2.229-1.881-2.798-1.757-4.624 0.079-1.145 0.326-2.216 0.551-2.38s0.434-0.944 0.462-1.734c0.027-0.789 0.207-1.437 0.399-1.437s0.346-0.221 0.346-0.491c0-0.269-0.243-0.339-0.542-0.156-0.37 0.228-0.442 0.076-0.226-0.488 0.221-0.578 0.131-0.75-0.307-0.582-0.342 0.131-0.622-0.044-0.622-0.391s0.226-0.517 0.502-0.379c0.276 0.138 0.071-0.131-0.454-0.599-0.998-0.887-2.515-3.118-2.515-3.698 0-0.185 0.323 0.067 0.717 0.562 0.579 0.726 0.567 0.587-0.063-0.721-0.428-0.892-1.428-2.223-2.22-2.961-0.822-0.764-1.091-1.217-0.625-1.053 0.761 0.268 0.767 0.194 0.103-1.089-0.392-0.757-0.822-1.376-0.957-1.376s-0.098 0.24 0.084 0.533c0.212 0.342 0.127 0.409-0.236 0.185-0.312-0.192-0.454-0.64-0.317-0.996s0.035-0.646-0.226-0.646c-0.262 0-0.346-0.208-0.189-0.462s0.031-0.462-0.283-0.462c-0.313 0-0.432-0.135-0.266-0.302s-0.060-0.598-0.502-0.961c-0.442-0.363-0.541-0.526-0.221-0.363 0.46 0.233 0.469 0.048 0.048-0.876-0.293-0.644-0.764-1.040-1.045-0.88s-0.364 0.128-0.185-0.070c0.18-0.199-0.194-1.388-0.83-2.644-0.938-1.853-1.327-2.228-2.063-1.995-0.519 0.164-0.906 0.073-0.906-0.215 0-0.313-0.462-0.387-1.226-0.195-0.94 0.236-1.139 0.166-0.854-0.295 0.288-0.467 0.038-0.536-1.111-0.305-1.053 0.211-1.387 0.144-1.157-0.233 0.221-0.358 0.004-0.445-0.673-0.267-0.549 0.144-0.901 0.103-0.781-0.091 0.24-0.389-1.564-2.238-2.3-2.356-0.255-0.039-0.411-0.262-0.346-0.491 0.344-1.236 0.293-1.563-0.231-1.457-0.317 0.063-0.579-0.092-0.579-0.346s0.212-0.462 0.471-0.462c0.271 0 0.349-0.49 0.182-1.158-0.192-0.762-0.098-1.158 0.276-1.158 0.338 0 0.469-0.372 0.325-0.925-0.133-0.509-0.053-0.925 0.177-0.925s0.42-0.399 0.42-0.885c0-1.037-3.508-3.787-4.396-3.447-0.331 0.127-0.757-0.022-0.95-0.335-0.215-0.349-0.158-0.447 0.152-0.255 0.276 0.171 0.654 0.067 0.837-0.231 0.188-0.305 0.135-0.419-0.123-0.26-0.251 0.156-1.251-0.257-2.219-0.914s-1.596-0.91-1.389-0.558c0.205 0.352-0.171 0.068-0.834-0.63s-1.498-1.157-1.853-1.020c-0.356 0.135-0.94-0.113-1.298-0.557-0.567-0.7-0.562-0.753 0.041-0.416 0.909 0.509 0.172-0.212-1.51-1.474-0.738-0.556-1.667-0.858-2.198-0.72-0.533 0.139-0.921 0.014-0.921-0.303 0-0.3 0.193-0.425 0.432-0.277s0.582 0.003 0.769-0.32c0.186-0.323 0.164-0.429-0.046-0.238s-0.894 0.015-1.519-0.395c-0.623-0.409-0.993-0.514-0.82-0.233 0.637 1.029-0.249 0.464-2.129-1.361-1.060-1.028-2.199-1.766-2.531-1.639s-0.746 0-0.921-0.284c-0.212-0.342-0.099-0.382 0.333-0.123 0.558 0.335 0.558 0.277 0.003-0.402-0.579-0.716-0.723-0.724-1.425-0.087-0.43 0.389-0.954 0.55-1.164 0.358s-0.228-0.079-0.037 0.253c0.284 0.493 0.48 0.493 1.089 0 0.454-0.368 0.616-0.392 0.418-0.062-0.178 0.296-0.062 0.652 0.259 0.788s0.033 0.173-0.639 0.080c-0.909-0.127-1.606 0.228-2.733 1.392-0.832 0.858-1.74 1.432-2.019 1.274s-0.345-0.111-0.149 0.104c0.464 0.511-2.909 3.971-3.51 3.599-0.252-0.156-0.332-0.078-0.175 0.175 0.371 0.601-3.088 3.976-3.599 3.51-0.215-0.195-0.257-0.12-0.092 0.168 0.363 0.635-3.532 4.334-4.563 4.334-0.402 0-0.604-0.205-0.448-0.457 0.161-0.262-0.022-0.34-0.433-0.183-0.394 0.151-0.575 0.513-0.398 0.805 0.203 0.337 0.060 0.322-0.392-0.044-0.391-0.316-0.795-0.524-0.898-0.462s-0.134 0.021-0.067-0.087c0.067-0.11-0.453-0.818-1.158-1.575s-1.019-1.228-0.7-1.046c0.861 0.492 0.699-0.438-0.204-1.171-0.531-0.432-0.673-0.449-0.435-0.057 0.479 0.796-0.087 0.726-0.93-0.115-0.534-0.534-0.555-0.697-0.083-0.701 0.452-0.003 0.43-0.125-0.083-0.457-0.381-0.247-0.486-0.459-0.231-0.469s0.099-0.233-0.346-0.493c-0.445-0.26-0.807-0.721-0.803-1.027 0.003-0.339 0.182-0.286 0.457 0.139 0.313 0.486 0.451 0.514 0.457 0.101 0.003-0.326-0.293-0.707-0.658-0.848s-0.546-0.563-0.402-0.94c0.17-0.442-0.099-0.778-0.762-0.952-0.664-0.174-0.833-0.386-0.482-0.602 0.389-0.24 0.195-0.977-0.68-2.596-0.674-1.243-1.127-1.932-1.010-1.53 0.118 0.406-0.059 0.732-0.397 0.732-0.448 0-0.5-0.286-0.195-1.090 0.336-0.883 0.197-1.279-0.738-2.092-0.769-0.667-0.976-1.11-0.624-1.327 0.335-0.207 0.166-0.44-0.466-0.64-0.546-0.174-0.86-0.531-0.698-0.794 0.19-0.307 0.438-0.255 0.694 0.149 0.305 0.483 0.399 0.491 0.405 0.036 0.003-0.326-0.304-0.712-0.683-0.858s-0.592-0.43-0.465-0.632c0.125-0.201-0.085-0.485-0.465-0.632s-0.688-0.532-0.683-0.858c0.004-0.457 0.099-0.447 0.405 0.036 0.277 0.438 0.493 0.474 0.714 0.115 0.175-0.283 0.132-0.512-0.094-0.512s-0.894-1.034-1.486-2.298c-0.594-1.262-1.274-2.175-1.514-2.026s-0.175-0.224 0.147-0.827c0.514-0.967 0.502-1.027-0.123-0.514-0.603 0.498-0.688 0.414-0.579-0.579 0.070-0.639-0.084-1.161-0.344-1.161s-0.352 0.313-0.205 0.695c0.147 0.381 0.039 0.695-0.236 0.695-0.284 0-0.391-0.425-0.248-0.974 0.319-1.221-1.063-4.217-1.815-3.931-0.358 0.137-0.513-0.188-0.433-0.911 0.085-0.775-0.108-1.127-0.622-1.132-0.652-0.004-0.658-0.065-0.051-0.457 0.617-0.399 0.617-0.45 0-0.462s-0.617-0.063 0-0.462c0.54-0.348 0.404-0.45-0.604-0.456-0.75-0.004-1.179 0.188-1.014 0.457 0.158 0.255 0.053 0.462-0.231 0.462s-0.389 0.208-0.231 0.462c0.158 0.255 0.053 0.462-0.231 0.462s-0.389 0.208-0.231 0.462c0.158 0.255 0.085 0.462-0.161 0.462s-0.344 0.392-0.219 0.872c0.142 0.544-0.133 1.125-0.734 1.546-0.529 0.37-0.807 0.829-0.618 1.018s-0.060 0.639-0.556 1c-0.809 0.593-0.817 0.657-0.087 0.657 0.445 0 0.687 0.199 0.536 0.442s-0.614 0.313-1.028 0.152c-0.505-0.193-0.644-0.111-0.42 0.252 0.185 0.298 0.102 0.542-0.183 0.542s-0.389 0.208-0.231 0.462c0.158 0.255 0.085 0.462-0.161 0.462s-0.344 0.392-0.219 0.872c0.142 0.544-0.133 1.125-0.734 1.546-0.529 0.37-0.807 0.829-0.618 1.018s-0.060 0.639-0.556 1c-0.809 0.593-0.817 0.657-0.087 0.657 0.445 0 0.687 0.199 0.536 0.442s-0.614 0.313-1.028 0.152c-0.505-0.193-0.644-0.111-0.42 0.252 0.185 0.298 0.102 0.542-0.183 0.542s-0.389 0.208-0.231 0.462c0.158 0.255 0.085 0.462-0.161 0.462s-0.342 0.401-0.214 0.894c0.149 0.571-0.101 1.103-0.691 1.471-0.536 0.334-0.813 0.866-0.661 1.264 0.192 0.498 0.083 0.579-0.394 0.292-0.557-0.334-0.562-0.277-0.038 0.367 0.757 0.935 0.322 1.795-0.709 1.399-0.505-0.193-0.644-0.111-0.42 0.252 0.183 0.298 0.102 0.542-0.183 0.542s-0.389 0.208-0.231 0.462c0.158 0.255 0.085 0.462-0.161 0.462s-0.342 0.401-0.214 0.894c0.149 0.571-0.101 1.103-0.691 1.471-0.536 0.334-0.813 0.866-0.661 1.264 0.192 0.498 0.083 0.579-0.394 0.292-0.557-0.334-0.562-0.277-0.038 0.367 0.757 0.935 0.322 1.795-0.709 1.399-0.505-0.193-0.644-0.111-0.42 0.252 0.183 0.298 0.102 0.542-0.183 0.542s-0.389 0.208-0.231 0.462c0.158 0.255 0.085 0.462-0.161 0.462s-0.344 0.392-0.219 0.872c0.135 0.521-0.12 1.126-0.634 1.503-0.474 0.348-0.832 1.084-0.799 1.634 0.044 0.72-0.137 0.925-0.642 0.731-0.461-0.176-0.589-0.083-0.37 0.271 0.186 0.301 0.083 0.542-0.233 0.542-0.313 0-0.428 0.139-0.257 0.309 0.382 0.382-1.035 2.010-1.73 1.99-0.279-0.005-0.103-0.321 0.392-0.695 0.719-0.544 0.753-0.679 0.172-0.679-0.399 0-0.959 0.279-1.243 0.622s-1.344 0.995-2.358 1.452c-1.012 0.457-1.762 0.957-1.666 1.111s-2.704 0.283-6.22 0.283c-4.777 0-6.283-0.135-5.952-0.533 0.32-0.386 0.060-0.507-0.937-0.438-0.757 0.053-1.278-0.067-1.156-0.266 0.348-0.563-2.701-3.68-3.355-3.429-0.331 0.127-0.486-0.027-0.358-0.358 0.293-0.766-2.914-3.671-3.593-3.254-0.334 0.206-0.395 0.115-0.169-0.25 0.442-0.714-2.483-3.849-3.296-3.536-0.324 0.125-0.471-0.038-0.34-0.378 0.127-0.327-0.579-1.306-1.567-2.175s-1.709-1.838-1.602-2.158c0.107-0.317-0.023-0.579-0.293-0.579s-0.353-0.346-0.192-0.77c0.203-0.527 0.12-0.661-0.262-0.425-0.398 0.246-0.494-0.003-0.336-0.875 0.14-0.774-0.014-1.307-0.418-1.464-0.351-0.135-0.526-0.541-0.387-0.901s0.026-0.656-0.246-0.656c-0.274 0-0.378-0.313-0.231-0.695s0.042-0.695-0.231-0.695c-0.274 0-0.365-0.346-0.202-0.77 0.216-0.563 0.128-0.666-0.325-0.387s-0.541 0.176-0.325-0.387c0.199-0.521 0.057-0.77-0.442-0.77-0.576 0-0.676-0.311-0.455-1.411 0.192-0.961 0.122-1.313-0.224-1.099-0.341 0.211-0.398-0.026-0.175-0.728 0.224-0.704 0.166-0.94-0.176-0.728-0.317 0.197-0.404 0.045-0.231-0.405 0.151-0.395 0.051-0.719-0.221-0.719s-0.378-0.313-0.231-0.695c0.147-0.381 0.042-0.695-0.231-0.695s-0.378-0.313-0.231-0.695c0.147-0.381 0.042-0.695-0.231-0.695s-0.393-0.274-0.264-0.611c0.259-0.675-0.904-3.222-1.293-2.834-0.135 0.135-0.371-0.087-0.528-0.493-0.163-0.428-0.090-0.618 0.174-0.455 0.251 0.156 0.457 0.063 0.457-0.207s-0.201-0.49-0.447-0.49c-0.246 0-0.315-0.346-0.153-0.77 0.19-0.494 0.113-0.658-0.213-0.457-0.286 0.177-0.643-0.116-0.822-0.676-0.198-0.623-0.083-1.138 0.314-1.387 0.492-0.312 0.456-0.399-0.167-0.405-0.49-0.004-0.7-0.256-0.547-0.655 0.137-0.356-0.004-0.805-0.317-0.996-0.342-0.212-0.446-0.158-0.266 0.135 0.166 0.268-0.034 0.614-0.44 0.769-0.412 0.158-0.624 0.581-0.48 0.96 0.143 0.371 0.026 0.818-0.255 0.993-0.337 0.208-0.387 0.094-0.143-0.333 0.258-0.452 0.219-0.509-0.127-0.188-0.274 0.255-0.564 0.774-0.645 1.156s-0.461 0.97-0.843 1.306c-0.617 0.546-0.617 0.58 0 0.314 0.557-0.24 0.548-0.178-0.040 0.313-0.404 0.337-0.643 0.88-0.535 1.204s0.023 0.534-0.191 0.462c-0.214-0.071-0.429 0.044-0.476 0.259s-0.463 0.698-0.925 1.075c-0.506 0.412-0.563 0.565-0.147 0.387 0.557-0.24 0.548-0.178-0.040 0.313-0.404 0.337-0.644 0.88-0.535 1.204s0.023 0.534-0.191 0.462c-0.214-0.071-0.428 0.044-0.476 0.259s-0.463 0.698-0.925 1.075c-0.462 0.377-0.528 0.532-0.147 0.345s0.102 0.221-0.62 0.908c-1.079 1.026-1.214 1.371-0.744 1.937 0.468 0.563 0.442 0.638-0.142 0.414-0.393-0.151-0.87-0.019-1.061 0.293-0.238 0.385-0.142 0.459 0.302 0.228 0.357-0.186 0.246-0.007-0.249 0.392s-0.715 0.728-0.49 0.728c0.225 0 0.004 0.305-0.49 0.679s-0.702 0.686-0.463 0.695c0.24 0.005 0.108 0.276-0.292 0.594-1.107 0.88-2.637 3.42-1.762 2.928 0.381-0.214-0.036 0.254-0.925 1.039-1.32 1.166-1.456 1.433-0.732 1.45 0.923 0.019 0.601 0.697-0.346 0.733-0.297 0.014-0.488 0.271-0.424 0.579s-0.069 0.558-0.293 0.558c-0.522 0-2.059 2.324-2.059 3.109 0 0.326-0.149 0.594-0.333 0.594-0.454 0-1.981 2.38-1.981 3.090 0 0.315-0.274 0.678-0.611 0.807-0.358 0.137-0.485 0.558-0.306 1.021 0.195 0.51 0.123 0.676-0.208 0.473-0.28-0.173-0.803 0.103-1.161 0.615s-0.788 0.793-0.957 0.623c-0.168-0.168-0.305-0.026-0.305 0.312s-0.126 0.491-0.28 0.337c-0.154-0.154-0.761 0.238-1.347 0.872s-1.192 0.95-1.342 0.704c-0.151-0.245-0.061-0.551 0.201-0.682 0.296-0.147 0.285-0.201-0.032-0.144-0.28 0.051-0.508 0.295-0.507 0.544 0.004 0.663-1.244 1.246-1.858 0.866-0.315-0.195-0.405-0.125-0.22 0.175 0.171 0.276 0.099 0.634-0.159 0.793s-0.474 0.018-0.477-0.315c-0.004-0.37-0.139-0.425-0.346-0.144-0.188 0.255-0.654 0.531-1.035 0.616s-0.865 0.459-1.076 0.83c-0.243 0.433-0.724 0.567-1.331 0.375-0.523-0.166-0.839-0.123-0.705 0.096s-0.471 0.413-1.343 0.432c-0.874 0.019-1.387-0.087-1.143-0.238s-0.378-0.723-1.381-1.273c-1.003-0.549-1.685-0.774-1.515-0.5 0.189 0.306 0.089 0.364-0.257 0.149-0.311-0.192-0.453-0.643-0.315-1.003 0.34-0.887-0.668-2.998-1.259-2.635-0.283 0.175-0.359-0.003-0.193-0.439 0.161-0.421 0-0.839-0.384-0.987-0.366-0.14-0.649-0.523-0.628-0.848 0.028-0.464 0.087-0.467 0.27-0.015 0.129 0.317 0.415 0.579 0.637 0.579s0.054-0.608-0.374-1.353c-0.427-0.743-0.973-1.231-1.214-1.081-0.248 0.152-0.309-0.065-0.143-0.498 0.204-0.533 0.051-0.772-0.5-0.777-0.683-0.004-0.697-0.070-0.101-0.457 0.505-0.326 0.528-0.45 0.089-0.457-0.333-0.003-0.491-0.193-0.349-0.421s-0.055-0.533-0.435-0.679c-0.38-0.147-0.691-0.029-0.691 0.26s-0.109 0.416-0.244 0.281c-0.135-0.135-0.036-0.794 0.22-1.466 0.316-0.832 0.313-1.221-0.016-1.221-0.262 0-0.351-0.208-0.193-0.462s0.071-0.462-0.189-0.462c-0.262 0-0.343-0.346-0.18-0.77 0.197-0.514 0.118-0.659-0.24-0.439-0.351 0.216-0.44 0.082-0.26-0.387 0.151-0.396 0.035-0.719-0.263-0.719-0.297 0-0.411-0.208-0.254-0.462s0.053-0.462-0.232-0.462c-0.285 0-0.388-0.208-0.232-0.462s0.053-0.462-0.232-0.462c-0.285 0-0.388-0.208-0.232-0.462s-0.090-0.477-0.55-0.495c-0.747-0.029-0.738-0.075 0.089-0.43 0.772-0.332 0.793-0.404 0.131-0.43-0.511-0.020-0.7-0.281-0.529-0.726 0.147-0.381 0.022-0.695-0.274-0.695s-0.411-0.208-0.254-0.462c0.157-0.255 0.053-0.462-0.232-0.462s-0.388-0.208-0.232-0.462c0.157-0.255-0.024-0.462-0.409-0.462-0.923 0-1.366-0.774-0.684-1.196 0.338-0.209 0.418-0.134 0.212 0.199-0.181 0.293-0.109 0.533 0.159 0.533 0.787 0 0.566-0.817-0.485-1.797-0.536-0.498-0.837-1.132-0.666-1.406 0.176-0.285-0.050-0.502-0.529-0.507-0.652-0.004-0.697-0.096-0.205-0.408 0.501-0.317 0.518-0.579 0.081-1.265-0.502-0.787-0.528-0.789-0.292-0.031 0.151 0.485 0.070 0.716-0.195 0.551-0.25-0.154-0.404-0.945-0.342-1.76 0.079-1.024-0.074-1.438-0.499-1.35-0.337 0.070-0.493-0.067-0.345-0.305 0.406-0.658-0.227-1.94-0.788-1.595-0.309 0.192-0.382 0.018-0.197-0.465 0.162-0.423 0.082-0.77-0.18-0.77s-0.346-0.208-0.189-0.462c0.157-0.255 0.053-0.462-0.232-0.462s-0.4-0.189-0.257-0.421c0.349-0.565-0.206-1.947-0.667-1.661-0.201 0.125-0.489-0.096-0.64-0.49-0.183-0.479-0.085-0.611 0.298-0.392 0.349 0.197 0.325 0.071-0.063-0.323-0.495-0.503-0.683-0.516-0.85-0.065-0.117 0.321-0.456 0.436-0.752 0.259s-0.254 0.029 0.096 0.461c0.447 0.551 0.475 0.783 0.089 0.783-0.302 0-0.418 0.208-0.261 0.462s0.053 0.462-0.232 0.462c-0.285 0-0.388 0.208-0.232 0.462s0.071 0.462-0.192 0.462c-0.262 0-0.802 0.623-1.196 1.387s-0.597 1.387-0.451 1.387c0.147 0-0.274 0.502-0.937 1.115-0.727 0.675-1.011 1.235-0.719 1.414 0.728 0.451 0.583 1.171-0.236 1.171-0.398 0-0.594 0.208-0.435 0.462s0.030 0.462-0.282 0.462c-0.313 0-0.432 0.135-0.266 0.302s-0.060 0.598-0.501 0.961c-0.442 0.363-0.523 0.514-0.178 0.337 0.505-0.259 0.524-0.131 0.1 0.659-0.288 0.541-0.751 0.981-1.028 0.981-0.311 0-0.26 0.301 0.131 0.783 0.349 0.43 0.393 0.638 0.099 0.461-0.78-0.467-1.854 0.065-1.854 0.921 0 0.618 0.099 0.599 0.617-0.108 0.339-0.464 0.617-0.607 0.617-0.319s-0.416 0.815-0.923 1.171c-0.508 0.356-0.786 0.784-0.617 0.952s0.007 0.308-0.351 0.313c-0.541 0.004-0.532 0.086 0.038 0.457 0.596 0.385 0.581 0.449-0.1 0.457-0.871 0.005-1.739 1.316-1.433 2.163 0.106 0.295-0.059 0.691-0.365 0.882-0.383 0.236-0.442 0.142-0.188-0.305 0.265-0.462 0.221-0.514-0.147-0.177-0.285 0.26-0.595 0.977-0.688 1.596s-0.414 1.203-0.71 1.301c-0.296 0.099-0.421 0.37-0.277 0.601s0.005 0.423-0.305 0.423c-0.313 0-0.446 0.122-0.297 0.27s-0.143 0.683-0.647 1.187c-0.737 0.736-0.806 1.052-0.358 1.591 0.462 0.558 0.415 0.618-0.276 0.353-0.593-0.226-0.733-0.156-0.487 0.244 0.211 0.341-0.137 1.015-0.875 1.7-0.674 0.623-1.097 1.342-0.94 1.596s0.052 0.461-0.233 0.461c-0.285 0-0.388 0.208-0.232 0.462s0.073 0.462-0.188 0.462c-0.261 0-0.695 0.412-0.964 0.915-0.416 0.775-0.368 0.868 0.315 0.605 0.903-0.346 0.704-0.055-1.836 2.695-0.705 0.764-1.162 1.586-1.013 1.825s0.037 0.436-0.249 0.436c-0.285 0-0.409 0.176-0.274 0.393 0.259 0.418-1.199 1.277-2.732 1.613-0.577 0.125-0.372 0.221 0.541 0.255 0.806 0.029 1.571-0.159 1.701-0.42s0.181-0.209 0.115 0.115c-0.169 0.835-4.256 0.904-4.938 0.084-0.292-0.351-0.41-0.954-0.261-1.339s0.054-0.7-0.207-0.7c-0.262 0-0.349-0.204-0.196-0.452s-0.035-1.029-0.417-1.736c-0.79-1.456-0.991-1.060-0.244 0.482 0.48 0.993 0.476 0.995-0.254 0.085-0.407-0.509-0.646-1.291-0.531-1.736s0.006-0.81-0.237-0.81c-0.246 0-0.317-0.208-0.161-0.462s0.101-0.462-0.127-0.462c-0.226 0-0.412-0.288-0.412-0.643 0-1.061-1.16-3.525-1.548-3.286-0.453 0.279-1.012-1.101-0.669-1.653 0.139-0.226-0.060-0.988-0.442-1.694s-0.704-1.113-0.715-0.906c-0.007 0.207 0.194 0.775 0.456 1.261 0.374 0.697 0.334 0.831-0.183 0.634-0.394-0.151-0.619-0.793-0.562-1.61 0.052-0.747-0.086-1.358-0.306-1.358s-0.284-0.189-0.14-0.421c0.399-0.644-0.238-1.928-0.788-1.587-0.268 0.166-0.351 0.080-0.181-0.192 0.167-0.271 0.175-0.83 0.017-1.242-0.26-0.676-0.351-0.668-0.93 0.074-0.57 0.731-0.593 0.679-0.182-0.464l0.46-1.286-3.349 0.086c-1.842 0.047-5.045-0.079-7.116-0.279-2.356-0.228-4.276-0.188-5.125 0.108-0.962 0.335-1.618 0.32-2.249-0.053-0.501-0.296-3.319-0.571-6.444-0.63-8.041-0.148-13.224-0.471-13.575-0.844-0.169-0.18 0.214-0.186 0.85-0.016 0.968 0.26 1.024 0.228 0.344-0.199-0.447-0.279-1.977-0.699-3.4-0.931s-3.008-0.557-3.525-0.719c-0.705-0.224-0.85-0.142-0.581 0.331 0.287 0.507 0.237 0.515-0.266 0.050-0.343-0.317-0.945-0.576-1.341-0.574-0.579 0.004-0.597 0.084-0.089 0.405 0.438 0.277 0.474 0.493 0.115 0.714-0.282 0.175-0.511 0.096-0.511-0.174s-0.781-0.517-1.736-0.55l-1.736-0.061 3.239-0.682-1.404-0.061c-0.772-0.034-2.906-0.553-4.743-1.156-2.112-0.692-3.341-0.904-3.345-0.579-0.004 0.344-0.125 0.329-0.361-0.041-0.194-0.306-1.284-0.666-2.421-0.803-1.537-0.183-1.773-0.142-0.912 0.163 0.965 0.341 1.004 0.414 0.231 0.442-0.509 0.017-1.342-0.303-1.852-0.71-0.925-0.742-0.925-0.742 0-0.42 0.578 0.2 0.751 0.161 0.463-0.108-0.598-0.556-4.806-1.95-5.751-1.907-0.528 0.022-0.475 0.142 0.198 0.43 1.045 0.449 0.242 0.615-0.885 0.183-0.401-0.154-0.6-0.489-0.442-0.743 0.176-0.284-0.079-0.348-0.657-0.164-0.669 0.212-1.15 0.003-1.657-0.72-0.393-0.561-0.618-1.175-0.499-1.366s-0.288-0.292-0.903-0.224c-0.852 0.094-1.056-0.054-0.859-0.618 0.211-0.601 0.142-0.634-0.365-0.163-0.344 0.317-0.843 0.576-1.109 0.574-0.277-0.003-0.246-0.159 0.073-0.363 0.401-0.255 0.288-0.525-0.395-0.952-0.525-0.327-0.817-0.373-0.647-0.101 0.498 0.807-0.503 0.567-1.195-0.286-0.432-0.531-0.449-0.673-0.057-0.435 0.317 0.192 0.579 0.259 0.579 0.151 0-0.3-3.578-2.603-4.258-2.743-0.328-0.067-0.874-0.434-1.212-0.817-0.546-0.617-0.58-0.617-0.308 0 0.168 0.381-0.076 0.259-0.544-0.272-1.492-1.696-3.755-3.041-4.123-2.45-0.218 0.351-0.531 0.156-0.917-0.564-0.323-0.602-0.462-1.308-0.307-1.567s-0.112-0.156-0.594 0.226c-0.48 0.381-0.739 0.472-0.577 0.201s-0.349-0.875-1.138-1.34c-1.010-0.597-1.332-0.657-1.091-0.201 0.188 0.355-0.112 0.125-0.665-0.511s-0.865-0.845-0.695-0.462c0.171 0.381-0.141 0.174-0.695-0.462s-0.894-0.932-0.755-0.658c0.146 0.292-0.176 0.467-0.769 0.425-0.561-0.040-0.915-0.266-0.788-0.5s0.403-0.367 0.614-0.297c0.21 0.069 0.248-0.089 0.084-0.355s-0.49-0.364-0.724-0.218c-0.235 0.146-0.794 0.028-1.239-0.259-0.769-0.494-0.769-0.509-0.004-0.27 0.446 0.138 0.81 0.182 0.81 0.096 0-0.269-3.633-2.573-4.258-2.702-0.328-0.067-0.851-0.434-1.162-0.817s-0.574-0.498-0.58-0.259c-0.005 0.24-0.327 0.032-0.709-0.462-0.684-0.885-0.691-0.885-0.413 0.027 0.156 0.509-0.147 0.26-0.669-0.551-0.541-0.838-1.313-1.451-1.785-1.414-0.459 0.035-1.405-0.314-2.106-0.774-1.286-0.846-1.76-1.494-0.694-0.953 0.318 0.162 0.081-0.144-0.528-0.678-0.716-0.628-1.43-0.869-2.025-0.679-0.611 0.193-0.812 0.119-0.601-0.221 0.512-0.83-0.362-0.511-1.242 0.452zM49.342-47.411c0.131 0.212-0.056 0.386-0.414 0.386s-0.545-0.174-0.414-0.386c0.131-0.212 0.317-0.386 0.414-0.386s0.285 0.174 0.414 0.386zM54.479-44.096c0.157 0.255-0.051 0.462-0.462 0.462s-0.619-0.208-0.462-0.462c0.157-0.255 0.366-0.462 0.462-0.462s0.305 0.208 0.462 0.462zM73.593-30.896c-0.142 0.142-0.401-0.114-0.576-0.571-0.251-0.659-0.198-0.713 0.259-0.259 0.316 0.314 0.459 0.687 0.316 0.83zM56.561-30.935c0 0.112-0.208 0.333-0.462 0.49s-0.462 0.065-0.462-0.204c0-0.269 0.208-0.49 0.462-0.49s0.462 0.091 0.462 0.204zM193.381-27.911c-0.077 0.503-0.268 1.042-0.425 1.199s-0.214-0.098-0.125-0.565c0.089-0.468 0.177-1.007 0.196-1.199s0.138-0.346 0.263-0.346c0.125 0 0.166 0.411 0.089 0.913zM57.544-22.687c-0.458 0.454-0.51 0.4-0.259-0.259 0.174-0.457 0.432-0.714 0.576-0.571s0 0.515-0.316 0.83zM105.572-16.498c0.106 0.091-0.35 0.166-1.013 0.166s-1.341-0.216-1.504-0.481c-0.269-0.435 1.973-0.156 2.517 0.315zM119.616-13.715c-0.314 0.127-0.938 0.135-1.388 0.017s-0.194-0.221 0.569-0.231c0.763-0.007 1.13 0.086 0.819 0.214zM63.965-9.161c0.597 0.637 0.983 1.158 0.856 1.158s-0.721-0.522-1.318-1.158c-0.597-0.637-0.983-1.158-0.855-1.158s0.721 0.522 1.318 1.158zM65.661-9.181c0 0.37-0.177 0.564-0.393 0.432s-0.274-0.437-0.127-0.675c0.374-0.604 0.522-0.538 0.522 0.243zM356.683-7.425c0.183 0.317 0.161 0.421-0.051 0.231s-0.817 0.017-1.346 0.462c-0.962 0.811-0.962 0.81-0.082-0.231 1.028-1.215 1.043-1.219 1.478-0.462zM67.436-5.734c0 0.49 1.468 1.892 1.719 1.642 0.115-0.115 0.21 0.11 0.21 0.5 0 0.483-0.489 0.238-1.524-0.767-0.838-0.813-1.407-1.667-1.264-1.897 0.269-0.435 0.861-0.077 0.861 0.524zM157.772-6.335c-0.154 0.154-0.553 0.171-0.887 0.037-0.369-0.147-0.26-0.257 0.279-0.279 0.488-0.019 0.762 0.089 0.607 0.243zM354.091-5.227c0 0.255-0.221 0.462-0.49 0.462s-0.362-0.208-0.204-0.462c0.158-0.255 0.378-0.462 0.49-0.462s0.204 0.208 0.204 0.462zM352.702-4.142c0 0.087-0.468 0.558-1.040 1.040l-1.040 0.882 0.882-1.040c0.829-0.978 1.2-1.252 1.2-0.882zM367.74-3.839c0.158 0.255 0.065 0.462-0.204 0.462s-0.49-0.208-0.49-0.462c0-0.255 0.091-0.462 0.204-0.462s0.333 0.208 0.49 0.462zM234.052-2.093c-0.797 0.763-0.806 0.753-0.221-0.221 0.337-0.561 0.713-0.921 0.833-0.799s-0.154 0.58-0.613 1.020zM369.081-2.99c0.131 0.212-0.057 0.386-0.414 0.386s-0.546-0.174-0.414-0.386c0.131-0.212 0.317-0.386 0.414-0.386s0.284 0.174 0.414 0.386zM350.389-1.525c0 0.255-0.221 0.462-0.49 0.462s-0.362-0.208-0.204-0.462c0.158-0.255 0.378-0.462 0.49-0.462s0.204 0.208 0.204 0.462zM349-0.439c0 0.087-0.468 0.558-1.040 1.040l-1.040 0.882 0.882-1.040c0.829-0.978 1.2-1.252 1.2-0.882zM231.47 2.178c0 0.255-0.22 0.462-0.49 0.462s-0.362-0.208-0.204-0.462c0.157-0.255 0.378-0.462 0.49-0.462s0.204 0.208 0.204 0.462zM346.687 2.178c0 0.255-0.221 0.462-0.49 0.462s-0.362-0.208-0.204-0.462c0.158-0.255 0.378-0.462 0.49-0.462s0.204 0.208 0.204 0.462zM76.257 3.721c0.398-0.152 0.585 0.103 0.54 0.73-0.039 0.531-0.097 0.964-0.125 0.964s-0.555-0.575-1.166-1.274c-0.614-0.7-0.832-1.028-0.487-0.73s0.903 0.437 1.239 0.307zM344.072 4.689c-0.724 0.873-1.575 1.601-1.89 1.62-0.477 0.026 2.558-3.155 3.046-3.193 0.089-0.005-0.432 0.701-1.158 1.573zM380.118 4.663c-0.163 0.411-0.834 0.133-0.695-0.288 0.063-0.192-0.197-0.346-0.579-0.346s-0.695-0.234-0.695-0.519c0-0.333 0.374-0.271 1.040 0.172 0.575 0.38 0.988 0.822 0.925 0.982zM212.373 4.769c-0.154 0.154-0.553 0.171-0.887 0.037-0.369-0.147-0.26-0.257 0.279-0.279 0.488-0.019 0.762 0.089 0.607 0.243zM381.603 5.382c0.147 0.236 0.089 0.541-0.127 0.675s-0.394-0.060-0.394-0.432c0-0.78 0.147-0.849 0.522-0.243zM215.012 7.681c0.142 0.228-0.020 0.522-0.357 0.651s-0.614-0.057-0.614-0.414c0-0.728 0.58-0.87 0.973-0.236zM296.249 8.407c0 0.118-0.468 0.442-1.040 0.72-1 0.484-1.005 0.476-0.144-0.214 0.944-0.757 1.183-0.858 1.183-0.505zM88.7 14.638c0.147 0.236 0.022 0.58-0.276 0.766-0.323 0.199-0.418 0.135-0.236-0.159 0.168-0.272 0.043-0.599-0.276-0.728-0.416-0.168-0.425-0.244-0.029-0.271 0.303-0.019 0.672 0.158 0.817 0.394zM382.932 16.786c-0.139 0.363-0.357 0.659-0.485 0.659s-0.122-0.286 0.015-0.637c0.135-0.351 0-0.913-0.298-1.251-0.447-0.505-0.405-0.509 0.24-0.022 0.43 0.325 0.667 0.887 0.529 1.251zM433.678 18.653c0 0.101-0.313 0.182-0.695 0.182s-0.695-0.201-0.695-0.447c0-0.247 0.313-0.327 0.695-0.182s0.695 0.348 0.695 0.447zM94.514 19.077c0.49 0.589 0.46 0.658-0.174 0.413-0.418-0.161-0.76-0.477-0.76-0.704 0-0.596 0.268-0.511 0.934 0.292zM391.966 19.645c0.471 1.101 0.46 1.163-0.087 0.47-0.348-0.442-0.732-0.702-0.856-0.579s-0.224-0.087-0.224-0.47c0-1.094 0.574-0.812 1.166 0.579zM97.743 21.149c0 0.255-0.091 0.462-0.204 0.462s-0.333-0.208-0.49-0.462c-0.157-0.255-0.065-0.462 0.204-0.462s0.49 0.208 0.49 0.462zM459.356 33.636c0.163 0.262-0.022 0.339-0.435 0.182-0.802-0.307-0.948-0.639-0.284-0.639 0.24 0 0.563 0.205 0.719 0.457zM146.792 41.046c0.272 0.509 0.417 0.925 0.319 0.925s-0.553-0.416-1.013-0.925c-0.461-0.509-0.602-0.925-0.319-0.925s0.741 0.416 1.013 0.925zM465.817 43.39c-0.147 0.236-0.514 0.414-0.817 0.394-0.394-0.024-0.386-0.103 0.029-0.271 0.32-0.129 0.444-0.457 0.276-0.728-0.182-0.295-0.087-0.359 0.236-0.159 0.298 0.185 0.421 0.529 0.276 0.766zM402.834 43.611c-0.151 0.393-0.373 0.615-0.495 0.493s-0.083-0.442 0.084-0.714c0.457-0.738 0.723-0.596 0.411 0.221zM466.994 44.466c0 0.101-0.324 0.305-0.719 0.457-0.412 0.158-0.598 0.080-0.435-0.182 0.271-0.44 1.154-0.651 1.154-0.276zM401.605 48.536c-0.147 0.369-0.257 0.26-0.279-0.279-0.019-0.488 0.089-0.762 0.243-0.607s0.171 0.553 0.037 0.887zM470.695 49.892c0 0.255-0.208 0.334-0.462 0.176s-0.462-0.494-0.462-0.748c0-0.255 0.208-0.334 0.462-0.176s0.462 0.494 0.462 0.748zM403.587 52.382c0 0.255-0.202 0.151-0.448-0.231s-0.448-0.902-0.448-1.158c0-0.255 0.202-0.151 0.448 0.231s0.448 0.902 0.448 1.158zM404.99 53.538c0.272 0.509 0.392 0.925 0.264 0.925s-0.454-0.416-0.726-0.925c-0.272-0.509-0.392-0.925-0.264-0.925s0.454 0.416 0.726 0.925zM474.625 55.384c0.163 0.262-0.022 0.339-0.435 0.182-0.802-0.307-0.948-0.639-0.284-0.639 0.24 0 0.563 0.205 0.719 0.457zM475.572 56.808c0.498 0.806-0.069 0.976-0.704 0.212-0.365-0.44-0.399-0.705-0.094-0.705 0.271 0 0.631 0.221 0.796 0.493zM408.228 57.423c0 0.101-0.324 0.305-0.719 0.457-0.412 0.158-0.598 0.080-0.435-0.182 0.271-0.44 1.154-0.651 1.154-0.276zM155.111 64.423c-0.322 0.387-0.705 0.584-0.852 0.438s0.038-0.462 0.412-0.699c0.981-0.619 1.107-0.544 0.44 0.26zM483.125 66.649c0.382 0.461 0.358 0.617-0.098 0.617-0.335 0-0.611-0.277-0.611-0.617s0.043-0.617 0.098-0.617c0.054 0 0.327 0.277 0.611 0.617zM485.067 70.308c-0.505 0.468-0.56 0.465-0.286-0.017 0.187-0.329 0.072-0.954-0.254-1.387-0.533-0.711-0.507-0.709 0.286 0.017 0.736 0.675 0.777 0.902 0.254 1.387zM486.95 71.469c0.577 0.733 0.56 0.75-0.175 0.175-0.445-0.349-0.81-0.714-0.81-0.81 0-0.38 0.38-0.135 0.986 0.635zM494.063 80.377c0.158 0.255 0.065 0.462-0.204 0.462s-0.49-0.208-0.49-0.462c0-0.255 0.091-0.462 0.204-0.462s0.333 0.208 0.49 0.462zM498.422 86.101c-0.022 0.325-0.125 0.303-0.271-0.060-0.129-0.32-0.39-0.485-0.58-0.368s-0.346-0.086-0.346-0.452c0-0.503 0.151-0.541 0.617-0.154 0.339 0.283 0.599 0.747 0.58 1.034zM264.642 87.108c0.394 0.394 0.62 0.812 0.505 0.928s-0.592-0.205-1.053-0.716c-0.981-1.085-0.507-1.267 0.548-0.212zM500.264 88.442c-0.123 0.123-0.497-0.238-0.833-0.799-0.585-0.974-0.577-0.986 0.221-0.221 0.459 0.44 0.734 0.898 0.613 1.020zM267.793 90.959c0 0.306 0.313 0.755 0.695 0.995s0.46 0.442 0.172 0.446c-0.286 0.003-0.781-0.305-1.098-0.687s-0.394-0.697-0.172-0.701c0.221-0.003 0.091-0.228-0.291-0.498-0.617-0.436-0.617-0.47 0-0.302 0.381 0.104 0.695 0.44 0.695 0.746zM421.876 105.828c-0.462 0.51-0.948 0.821-1.079 0.688s0.163-0.543 0.654-0.914c1.252-0.945 1.41-0.863 0.426 0.224zM419.798 107.358c0 0.080-0.675 0.757-1.497 1.503-1.251 1.137-1.344 1.168-0.579 0.202 1.084-1.365 2.075-2.18 2.075-1.706zM414.243 112.455c0 0.083-0.498 0.618-1.108 1.193-0.814 0.766-1.2 0.892-1.459 0.473-0.209-0.337-0.161-0.451 0.12-0.277 0.26 0.161 0.846-0.12 1.3-0.622 0.781-0.863 1.147-1.106 1.147-0.764zM277.985 125.171c-0.142 0.142-0.515 0-0.83-0.316-0.454-0.457-0.4-0.51 0.259-0.259 0.457 0.174 0.714 0.433 0.571 0.576zM276.525 126.087c0.123 0.2-0.161 0.364-0.634 0.364s-0.757-0.163-0.634-0.364c0.123-0.2 0.409-0.364 0.634-0.364s0.509 0.163 0.634 0.364zM393.885 127.315c0 0.112-0.208 0.333-0.462 0.49s-0.462 0.065-0.462-0.204c0-0.269 0.208-0.49 0.462-0.49s0.462 0.091 0.462 0.204zM385.757 133.637c-0.392 0.635-0.973 0.493-0.973-0.236 0-0.357 0.276-0.544 0.615-0.414s0.498 0.421 0.358 0.651zM380.296 136.944c-0.142 0.94-0.77 1.737-1.361 1.728-0.329-0.003-0.271-0.183 0.144-0.446 0.381-0.241 0.721-0.655 0.752-0.918 0.092-0.764 0.161-0.942 0.364-0.942 0.105 0 0.151 0.26 0.103 0.579zM374.45 142.123c0 0.112-0.208 0.333-0.462 0.49s-0.462 0.065-0.462-0.204c0-0.269 0.208-0.49 0.462-0.49s0.462 0.091 0.462 0.204zM370.749 144.87c0 0.096-0.365 0.461-0.81 0.81-0.733 0.577-0.75 0.56-0.175-0.175 0.604-0.771 0.986-1.017 0.986-0.635zM440.78 146.762c0.144 0.374 0.105 0.775-0.086 0.894s-0.377-0.188-0.414-0.679c-0.080-1.063 0.138-1.157 0.5-0.214zM329.905 147.75c-0.154 0.154-0.553 0.171-0.887 0.037-0.369-0.147-0.26-0.257 0.279-0.279 0.488-0.019 0.762 0.089 0.607 0.243zM441.88 149.139c-0.154 0.154-0.553 0.171-0.887 0.037-0.369-0.147-0.26-0.257 0.279-0.279 0.488-0.019 0.762 0.089 0.607 0.243zM248.494 150.433c-0.327 0.356-0.668 0.578-0.759 0.493-0.274-0.26 0.421-1.141 0.902-1.141 0.25 0 0.185 0.292-0.143 0.646zM326.327 151.866c-1.123 1.145-2.146 2.082-2.269 2.082s0.443-0.69 1.26-1.534c0.818-0.844 1.327-1.71 1.13-1.925s-0.109-0.25 0.192-0.079c0.301 0.172 0.677 0.101 0.837-0.158s0.426-0.47 0.593-0.47c0.166 0-0.618 0.938-1.743 2.082zM247.048 151.384c0 0.37-0.156 0.577-0.348 0.459s-0.677 0.114-1.079 0.515c-0.401 0.401-0.732 0.533-0.732 0.293 0-0.34 0.88-1.137 2.121-1.916 0.020-0.015 0.038 0.279 0.038 0.651zM336.182 154.135c-0.457 0.613-0.916 1.028-1.022 0.928-0.19-0.183 1.332-2.041 1.671-2.041 0.101 0-0.193 0.5-0.651 1.111zM444.483 154.166c0.144 0.374 0.105 0.775-0.086 0.894s-0.377-0.188-0.414-0.679c-0.080-1.063 0.138-1.157 0.5-0.214zM322.374 155.306c0.147 0.236 0.089 0.541-0.127 0.675s-0.393-0.060-0.393-0.432c0-0.78 0.147-0.849 0.522-0.243zM334.425 156.263c-0.158 0.255-0.545 0.462-0.86 0.462-1.103 0-1.717 1.019-1.094 1.817 0.502 0.643 0.473 0.654-0.248 0.089-0.658-0.515-0.705-0.771-0.248-1.324 0.313-0.377 0.992-0.857 1.51-1.067 1.257-0.51 1.272-0.51 0.942 0.021zM330.49 158.738c0 0.087-0.468 0.558-1.040 1.040l-1.040 0.882 0.882-1.040c0.829-0.978 1.2-1.252 1.2-0.882zM319.387 159.199c0 0.087-0.4 0.524-0.892 0.965-0.849 0.767-0.882 0.766-0.729-0.043 0.089-0.467 0.176-0.902 0.197-0.965 0.055-0.178 1.424-0.137 1.424 0.043zM328.022 161.485c0 0.327-0.37 0.596-0.822 0.596-0.618 0-0.7-0.148-0.329-0.596 0.271-0.327 0.641-0.596 0.822-0.596s0.329 0.268 0.329 0.596zM317.207 162.273c-0.132 0.507-0.435 0.8-0.675 0.654-0.255-0.158-0.211-0.534 0.109-0.921 0.73-0.879 0.849-0.823 0.563 0.267zM205.767 166.476c-0.169 0.274 0.182 1.055 0.781 1.736 0.978 1.113 0.937 1.102-0.416-0.123-1.088-0.985-1.343-1.463-0.923-1.728 0.787-0.5 0.922-0.473 0.559 0.115zM315.685 177.962c0 0.228-0.342 0.545-0.76 0.704-0.635 0.243-0.663 0.176-0.175-0.413 0.666-0.804 0.935-0.886 0.935-0.292zM219.901 179.399c0 0.255-0.091 0.462-0.204 0.462s-0.333-0.208-0.49-0.462c-0.157-0.255-0.065-0.462 0.204-0.462s0.49 0.208 0.49 0.462zM487.586 182.637c-0.158 0.255-0.494 0.462-0.748 0.462s-0.334-0.208-0.176-0.462c0.158-0.255 0.494-0.462 0.748-0.462s0.334 0.208 0.176 0.462zM486.588 184.277c-0.151 0.393-0.373 0.615-0.495 0.493s-0.083-0.442 0.084-0.714c0.457-0.738 0.723-0.596 0.411 0.221zM484.070 186.979c0.409 0.492 0.375 0.7-0.144 0.899-0.46 0.176-0.654-0.027-0.607-0.64 0.081-1.072 0.079-1.071 0.751-0.259zM297.080 189.305c-0.074 0.039-0.551 0.38-1.060 0.752-0.833 0.613-0.868 0.605-0.351-0.058 0.316-0.405 0.426-0.885 0.245-1.067s0.038-0.188 0.488-0.017c0.448 0.172 0.755 0.346 0.68 0.387zM293.687 190.935c0.147 0.236 0.055 0.562-0.204 0.721s-0.47-0.035-0.47-0.432c0-0.813 0.277-0.932 0.675-0.291zM227.583 193.095c-0.164 0.429-0.404 0.676-0.53 0.548s-0.096-0.582 0.067-1.012c0.164-0.429 0.404-0.676 0.53-0.548s0.096 0.582-0.067 1.012zM464.72 196.726c0.233-0.144 0.423-0.041 0.423 0.228s-0.221 0.49-0.49 0.49c-0.269 0-0.34 0.26-0.158 0.579s0.156 0.417-0.062 0.219c-0.217-0.197-0.445-0.822-0.508-1.387s-0.003-0.767 0.128-0.447c0.132 0.32 0.432 0.464 0.664 0.32zM223.688 198.47c-0.332 0.332-0.547 0.359-0.547 0.069 0-0.618 0.547-1.164 0.856-0.856 0.131 0.131-0.005 0.486-0.308 0.786zM220.827 202.716c0 0.101-0.323 0.305-0.719 0.457-0.412 0.158-0.597 0.080-0.435-0.182 0.271-0.44 1.155-0.651 1.155-0.276zM219.357 204.688c-0.083 0.421-0.531 0.822-1 0.892-0.81 0.118-0.812 0.082-0.043-0.766 0.988-1.091 1.238-1.122 1.043-0.125zM389.719 208.089c0 0.255-0.091 0.462-0.204 0.462s-0.333-0.208-0.49-0.462c-0.158-0.255-0.065-0.462 0.204-0.462s0.49 0.208 0.49 0.462zM394.577 211.789c0.158 0.255 0.079 0.462-0.176 0.462s-0.593-0.208-0.748-0.462c-0.158-0.255-0.079-0.462 0.176-0.462s0.593 0.208 0.748 0.462zM209.641 213.492c-0.083 0.186-0.514 0.365-0.962 0.397-0.769 0.057-0.77 0.015-0.014-0.824 0.704-0.779 1.38-0.483 0.974 0.426zM378.152 214.277c0 0.096-0.365 0.461-0.81 0.81-0.733 0.577-0.75 0.56-0.175-0.175 0.604-0.771 0.986-1.017 0.986-0.635zM398.974 214.846c0 0.382-0.898 0.599-1.105 0.266-0.133-0.216 0.060-0.393 0.432-0.393s0.675 0.058 0.675 0.127zM205.911 215.954c-0.317 0.509-0.768 0.925-1.002 0.925s-0.046-0.416 0.414-0.925c0.461-0.509 0.911-0.925 1.002-0.925s-0.096 0.416-0.414 0.925zM374.936 217.008l-1.082 1.027 0.876-1.228c0.482-0.675 0.97-1.139 1.082-1.027s-0.283 0.661-0.876 1.228zM371.015 220.477c-0.796 0.763-0.807 0.753-0.221-0.221 0.337-0.561 0.711-0.921 0.833-0.799s-0.154 0.58-0.613 1.020zM353.039 221.323c-0.154 0.154-0.553 0.171-0.887 0.037-0.369-0.147-0.26-0.257 0.279-0.279 0.488-0.019 0.762 0.089 0.607 0.243zM194.22 225.67c-0.157 0.255-0.533 0.46-0.838 0.457-0.332-0.004-0.274-0.186 0.143-0.457 0.888-0.576 1.050-0.576 0.695 0zM352.702 226.364c0.332 0.399 0.377 0.695 0.106 0.695-0.259 0-0.592-0.313-0.736-0.695s-0.194-0.695-0.106-0.695c0.087 0 0.42 0.313 0.736 0.695zM348.594 228.571c-0.457 0.453-0.51 0.4-0.259-0.259 0.174-0.457 0.433-0.714 0.576-0.571s0 0.516-0.316 0.83zM337.839 229.834c-0.818 1.018-1.582 1.852-1.698 1.852-0.236 0 0.562-1.102 1.928-2.66 1.519-1.733 1.358-1.166-0.229 0.81zM368.594 229.625c-0.151 0.393-0.373 0.615-0.495 0.493s-0.083-0.442 0.084-0.714c0.457-0.738 0.723-0.595 0.411 0.22zM344.557 231.599c-0.738 0.716-1.603 1.305-1.923 1.312-0.644 0.007 2.183-2.563 2.851-2.594 0.228-0.007-0.188 0.565-0.925 1.282zM336.045 232.324c0 0.096-0.293 0.418-0.654 0.717-0.5 0.415-0.733 0.411-0.996-0.016-0.211-0.34-0.149-0.436 0.158-0.246 0.276 0.171 0.634 0.099 0.793-0.159 0.286-0.463 0.699-0.637 0.699-0.295zM363.933 233.377c-0.313 0.127-0.938 0.135-1.387 0.018s-0.193-0.221 0.569-0.231c0.764-0.006 1.13 0.086 0.819 0.214zM341.599 234c0 0.255-0.365 0.46-0.81 0.457-0.692-0.004-0.709-0.071-0.115-0.457 0.874-0.564 0.925-0.564 0.925 0zM471.496 237.982c-0.154 0.154-0.553 0.171-0.887 0.037-0.369-0.148-0.26-0.257 0.279-0.279 0.488-0.019 0.762 0.089 0.607 0.243zM207.409 243.921c0 0.112-0.208 0.333-0.463 0.49s-0.463 0.065-0.463-0.204c0-0.269 0.208-0.49 0.463-0.49s0.463 0.091 0.463 0.204zM335.171 247.949c0.267 0.522 0.137 0.96-0.411 1.387-0.699 0.547-0.73 0.538-0.248-0.083 0.357-0.459 0.4-0.95 0.123-1.387-0.236-0.374-0.416-0.992-0.399-1.373 0.024-0.617 0.058-0.617 0.286 0 0.142 0.381 0.433 1.037 0.647 1.456zM277.133 250.281c-0.147 0.369-0.257 0.26-0.279-0.279-0.019-0.488 0.089-0.762 0.243-0.607s0.171 0.553 0.037 0.887zM234.089 252.624c-0.129 0.317-0.233 0.059-0.233-0.578s0.105-0.896 0.233-0.578c0.129 0.317 0.129 0.839 0 1.157zM340.209 252.508c0 0.255-0.195 0.463-0.435 0.463s-0.564-0.208-0.721-0.463c-0.158-0.255 0.038-0.463 0.435-0.463s0.721 0.208 0.721 0.463zM278.962 253.576c0.177 0.177-0.014 0.322-0.421 0.322s-0.861-0.19-1.005-0.422c-0.296-0.48 0.933-0.393 1.426 0.101zM280.363 255.124c0 0.457-0.156 0.48-0.617 0.098-0.339-0.282-0.617-0.557-0.617-0.612s0.277-0.098 0.617-0.098c0.339 0 0.617 0.274 0.617 0.612zM363.808 256.808c0.461 0.169 0.349 0.268-0.332 0.292-0.618 0.021-1.242-0.377-1.57-1.005-0.522-0.992-0.505-1.006 0.332-0.292 0.482 0.412 1.19 0.865 1.57 1.005zM365.287 259.387c0.349-0.498 0.408-0.502 0.279-0.018-0.197 0.741-1.005 0.815-1.135 0.104-0.060-0.329-0.003-0.323 0.161 0.018 0.178 0.371 0.382 0.34 0.695-0.104zM272.637 261.415l1.086 1.504-1.231-1.122c-0.676-0.617-1.231-1.295-1.231-1.503 0-0.642 0.245-0.442 1.375 1.122zM367.522 263.639c0.703 1.315 0.224 1.101-0.702-0.314-0.438-0.667-0.565-1.072-0.286-0.899s0.724 0.719 0.99 1.214zM429.051 263.15c0 0.255-0.221 0.463-0.49 0.463s-0.362-0.208-0.204-0.463c0.158-0.255 0.378-0.462 0.49-0.462s0.204 0.208 0.204 0.462zM424.357 265.053c-0.998 0.634-1.488 0.476-0.864-0.277 0.315-0.38 0.779-0.561 1.031-0.405 0.279 0.172 0.214 0.441-0.168 0.682zM194.452 265.668c0 0.112-0.208 0.333-0.463 0.49s-0.463 0.065-0.463-0.204c0-0.269 0.208-0.49 0.463-0.49s0.463 0.091 0.463 0.204zM375.896 266.514c-0.457 0.453-0.51 0.4-0.259-0.259 0.174-0.457 0.433-0.714 0.576-0.571s0 0.516-0.316 0.83zM152.113 266.854c0.157 0.255 0.118 0.478-0.089 0.498s-0.75 0.107-1.212 0.194c-0.675 0.128-0.73 0.034-0.292-0.498 0.64-0.771 1.191-0.839 1.589-0.194zM420.859 267.533c-0.457 0.628-1.024 1.147-1.259 1.157s0.062-0.507 0.659-1.142c1.387-1.476 1.669-1.483 0.599-0.015zM194.619 268.010c-0.29 0.756-1.092 0.954-1.092 0.27 0-0.351 0.324-0.592 1.257-0.927 0.055-0.019-0.018 0.276-0.166 0.657zM214.613 268.697c0 0.543-0.171 0.749-0.416 0.503-0.228-0.228-0.276-0.642-0.105-0.919 0.435-0.704 0.522-0.636 0.522 0.416zM211.276 270.785c-0.147 0.381-0.477 0.695-0.737 0.695-0.27 0-0.226-0.294 0.106-0.695 0.316-0.381 0.647-0.694 0.737-0.694s0.040 0.313-0.106 0.694zM283.757 271.015c0 0.224-0.406 0.375-0.902 0.338-0.678-0.051-0.78-0.214-0.411-0.659 0.505-0.605 1.313-0.409 1.313 0.322zM370.981 271.942c0.993 1.019 1.692 1.852 1.553 1.852-0.322 0-1.747-1.313-2.897-2.663-1.43-1.684-0.608-1.186 1.344 0.814zM381.843 272.554c0.017 0.615-1.169-0.76-1.525-1.767-0.26-0.738-0.139-0.689 0.595 0.231 0.507 0.637 0.925 1.327 0.932 1.537zM146.328 271.942c0 0.255-0.22 0.462-0.49 0.462s-0.362-0.208-0.204-0.462c0.157-0.255 0.378-0.462 0.49-0.462s0.204 0.208 0.204 0.462zM382.938 274c0.171 0.447 0.036 0.583-0.411 0.414-0.368-0.142-0.551-0.442-0.412-0.672 0.354-0.575 0.524-0.522 0.822 0.257zM206.327 274.467c0 0.37-0.177 0.564-0.393 0.432s-0.274-0.437-0.127-0.674c0.375-0.605 0.522-0.538 0.522 0.243zM375.49 276.302c0.154-0.154 0.637 0.415 1.072 1.265l0.791 1.547-1.082-0.925c-0.596-0.509-1.58-1.498-2.188-2.199s-1.327-1.138-1.599-0.971c-0.302 0.187-0.356 0.082-0.138-0.268 0.259-0.418 0.693-0.246 1.606 0.631 0.689 0.661 1.38 1.075 1.534 0.921zM384.967 274.998c-0.154 0.154-0.553 0.171-0.887 0.037-0.369-0.147-0.26-0.257 0.279-0.279 0.488-0.019 0.762 0.089 0.607 0.243zM142.626 275.645c0 0.255-0.22 0.463-0.49 0.463s-0.362-0.208-0.204-0.463c0.157-0.255 0.378-0.462 0.49-0.462s0.204 0.208 0.204 0.462zM389.456 276.833c1.014 1.014 0.914 1.437-0.119 0.502-0.974-0.882-1.678-1.018-1.193-0.233 0.206 0.334 0.127 0.409-0.212 0.199-0.571-0.353-0.293-1.196 0.393-1.196 0.221 0 0.729 0.327 1.129 0.727zM141.239 276.73c0 0.087-0.468 0.558-1.040 1.040l-1.040 0.881 0.881-1.040c0.828-0.978 1.201-1.252 1.201-0.881zM309.17 279.053c-0.023 0.673-0.118 0.767-0.29 0.293-0.139-0.381-0.697-1.193-1.236-1.802l-0.982-1.107 1.274 0.813c0.77 0.493 1.257 1.204 1.236 1.802zM201.393 277.671c0 0.096-0.364 0.461-0.81 0.81-0.733 0.577-0.75 0.56-0.174-0.174 0.604-0.771 0.986-1.017 0.986-0.636zM295.061 278.839c-0.123 0.123-0.58-0.154-1.020-0.613-0.763-0.797-0.752-0.807 0.221-0.221 0.561 0.337 0.921 0.711 0.799 0.833zM138.925 279.346c0 0.255-0.22 0.462-0.49 0.462s-0.362-0.208-0.204-0.462c0.157-0.255 0.378-0.463 0.49-0.463s0.204 0.208 0.204 0.463zM199.238 279.597c-0.151 0.393-0.374 0.615-0.494 0.493s-0.083-0.442 0.084-0.715c0.457-0.738 0.723-0.595 0.411 0.221zM441.544 279.065c0 0.101-0.313 0.303-0.695 0.447s-0.695 0.065-0.695-0.181c0-0.247 0.313-0.447 0.695-0.447s0.695 0.082 0.695 0.181zM296.914 279.76c0.142 0.227-0.020 0.522-0.358 0.651s-0.615-0.057-0.615-0.414c0-0.728 0.58-0.869 0.973-0.236zM445.71 280.067c0 0.398-0.212 0.592-0.47 0.432s-0.351-0.484-0.204-0.721c0.397-0.641 0.675-0.523 0.675 0.291zM137.536 280.432c0 0.087-0.468 0.558-1.040 1.040l-1.040 0.881 0.881-1.040c0.828-0.978 1.201-1.252 1.201-0.881zM299.548 281.545c0.562 0.716 0.55 0.744-0.101 0.236-0.405-0.316-0.907-0.419-1.115-0.231s-0.228 0.083-0.046-0.236c0.43-0.747 0.507-0.733 1.262 0.231zM449.124 282.841c0.158 0.412 0.080 0.597-0.182 0.435-0.252-0.156-0.457-0.478-0.457-0.719 0-0.665 0.332-0.519 0.639 0.284zM134.631 283.573c-0.448 0.251-0.674 0.704-0.5 1.007s0.144 0.396-0.063 0.207c-0.208-0.189-0.71-0.092-1.114 0.214s-0.127-0.11 0.618-0.924c0.745-0.814 1.471-1.364 1.616-1.22s-0.106 0.466-0.556 0.718zM390.962 284.524c-0.147 0.369-0.257 0.26-0.279-0.279-0.019-0.488 0.089-0.762 0.243-0.607s0.171 0.553 0.037 0.886zM456.225 283.789c-0.154 0.154-0.553 0.171-0.887 0.037-0.369-0.148-0.26-0.257 0.279-0.279 0.488-0.019 0.762 0.089 0.607 0.243zM189.825 286.287c0 0.255-0.22 0.462-0.49 0.462s-0.362-0.208-0.204-0.462c0.157-0.255 0.378-0.463 0.49-0.463s0.204 0.208 0.204 0.463zM391.423 288.225c-0.147 0.369-0.257 0.26-0.279-0.279-0.019-0.489 0.089-0.762 0.243-0.607s0.171 0.553 0.037 0.887zM187.048 288.37c-0.316 0.381-0.798 0.694-1.071 0.694s-0.089-0.305 0.404-0.679c1.161-0.877 1.387-0.882 0.667-0.016zM160.731 290.576c-0.458 0.453-0.51 0.4-0.259-0.259 0.174-0.457 0.433-0.714 0.576-0.571s0 0.516-0.316 0.83zM391.892 291.715c-0.118 0.45-0.221 0.194-0.231-0.569s0.086-1.131 0.214-0.819c0.127 0.314 0.135 0.938 0.017 1.388zM159.746 291.195c0 0.382-0.898 0.599-1.105 0.266-0.133-0.216 0.060-0.393 0.431-0.393s0.674 0.058 0.674 0.127zM120.417 293.41c0 0.101-0.323 0.305-0.719 0.458-0.412 0.158-0.597 0.080-0.435-0.181 0.271-0.44 1.154-0.652 1.154-0.276zM392.349 295.164c-0.147 0.369-0.257 0.26-0.279-0.279-0.019-0.488 0.089-0.761 0.243-0.607s0.171 0.553 0.037 0.887zM152.113 296.467c-0.157 0.255-0.533 0.46-0.838 0.457-0.332-0.003-0.274-0.186 0.143-0.457 0.888-0.575 1.050-0.575 0.695 0zM149.163 298.442c-0.458 0.453-0.51 0.4-0.259-0.259 0.174-0.457 0.433-0.714 0.576-0.571s0 0.516-0.316 0.83zM171.366 300.137c-0.339 0.21-0.438 0.476-0.218 0.594s0.164 0.158-0.124 0.092c-0.286-0.065-1.214 0.594-2.059 1.465s-1.363 1.583-1.157 1.583c0.208 0 0.093 0.345-0.256 0.765s-0.6 0.889-0.559 1.040c0.042 0.151-0.042 0.38-0.19 0.507s-0.126-0.015 0.046-0.315c0.172-0.301 0.081-0.689-0.204-0.865-0.321-0.199 0.495-1.34 2.163-3.027 1.474-1.49 2.791-2.598 2.924-2.463s-0.035 0.416-0.374 0.625zM114.473 300.447c-0.677 0.475-0.956 1.012-0.772 1.49 0.158 0.411 0.144 0.604-0.029 0.431s-0.757-0.089-1.297 0.188c-0.662 0.342-0.465-0.037 0.615-1.174 0.877-0.923 1.809-1.677 2.069-1.677s0 0.334-0.583 0.743zM395.147 306.926c-0.154 0.154-0.553 0.171-0.887 0.037-0.369-0.147-0.26-0.257 0.279-0.279 0.488-0.019 0.762 0.089 0.607 0.243zM162.987 309.212c0 0.054-0.313 0.356-0.695 0.674-0.563 0.467-0.695 0.448-0.695-0.098 0-0.37 0.313-0.675 0.695-0.675s0.695 0.043 0.695 0.098zM157.837 312.991c-0.349 0.435-1.313 1.394-2.138 2.131-1.549 1.38-2.138 2.781-0.695 1.647 0.717-0.562 0.744-0.549 0.236 0.101-0.316 0.405-0.419 0.919-0.232 1.142s-0.028 0.119-0.483-0.229c-0.726-0.558-1.006-0.472-2.314 0.7-0.818 0.733 0.174-0.38 2.202-2.475 3.475-3.585 4.856-4.803 3.423-3.017zM102.57 313.536c0.139 0.226-0.039 0.655-0.398 0.952-0.525 0.434-0.62 0.377-0.493-0.295 0.087-0.46 0.175-0.887 0.194-0.952 0.078-0.254 0.457-0.092 0.698 0.295zM125.935 313.923c0.643-0.501 0.654-0.472 0.088 0.249-0.492 0.625-0.787 0.694-1.273 0.291-0.348-0.288-0.633-0.644-0.633-0.792s0.244-0.021 0.544 0.277c0.417 0.417 0.715 0.412 1.273-0.022zM123.887 315.9c-0.157 0.255-0.494 0.463-0.749 0.463s-0.334-0.208-0.177-0.463c0.157-0.255 0.494-0.462 0.749-0.462s0.334 0.208 0.177 0.462zM486.891 317c0 0.096-0.365 0.461-0.81 0.81-0.733 0.577-0.75 0.56-0.175-0.175 0.604-0.77 0.986-1.016 0.986-0.635zM118.624 319.726c-0.457 0.453-0.51 0.4-0.259-0.259 0.174-0.457 0.433-0.713 0.576-0.571s0 0.516-0.316 0.83zM150.494 320.529c0 0.255-0.22 0.462-0.49 0.462s-0.362-0.208-0.204-0.462c0.157-0.255 0.378-0.462 0.49-0.462s0.204 0.208 0.204 0.462zM115.094 322.378c-0.157 0.255-0.533 0.46-0.838 0.457-0.332-0.003-0.274-0.186 0.144-0.457 0.888-0.575 1.050-0.575 0.695 0zM480.413 322.538c0 0.087-0.381 0.505-0.848 0.927-0.702 0.635-0.849 0.652-0.849 0.092 0-0.371 0.159-0.577 0.353-0.457s0.484 0.005 0.644-0.252c0.279-0.454 0.699-0.639 0.699-0.311zM113.475 323.331c0 0.269-0.208 0.362-0.463 0.204s-0.463-0.065-0.463 0.205c0 0.269-0.201 0.49-0.448 0.49s-0.328-0.313-0.181-0.695c0.302-0.787 1.555-0.952 1.555-0.204zM143.228 327.166c-0.204 0.343-1.141 1.293-2.083 2.111s-1.831 1.856-1.974 2.307c-0.143 0.451-0.581 0.82-0.974 0.82-0.412 0 0.513-1.238 2.192-2.93 2.948-2.976 3.502-3.426 2.838-2.307zM108.386 327.211c0 0.489-0.887 0.694-1.152 0.267-0.159-0.259 0.035-0.47 0.431-0.47s0.721 0.092 0.721 0.204zM84.325 331.345c0 0.096-0.365 0.461-0.811 0.81-0.734 0.577-0.75 0.56-0.174-0.174 0.604-0.77 0.986-1.017 0.986-0.635zM85.309 332.906c0.576 0.733 0.559 0.75-0.174 0.175-0.77-0.604-1.016-0.986-0.635-0.986 0.097 0 0.461 0.364 0.811 0.81zM467.373 333.815c-0.271 0.327-0.55 0.595-0.618 0.595s-0.228-0.268-0.353-0.595c-0.135-0.351 0.118-0.595 0.618-0.595 0.638 0 0.725 0.147 0.353 0.595zM82.704 333.947c-0.157 0.255-0.494 0.462-0.749 0.462s-0.334-0.208-0.177-0.462c0.157-0.255 0.494-0.463 0.749-0.463s0.334 0.208 0.177 0.463zM81.072 335.22c-0.005 0.191-0.58 0.746-1.273 1.235l-1.257 0.887 1.122-1.235c1.179-1.298 1.431-1.458 1.409-0.887zM95.891 336.906c0 0.101-0.323 0.305-0.719 0.458-0.412 0.158-0.597 0.080-0.435-0.182 0.271-0.44 1.154-0.651 1.154-0.276zM439.231 342.919c0 0.101-0.313 0.303-0.695 0.447s-0.695 0.065-0.695-0.181c0-0.247 0.313-0.447 0.695-0.447s0.695 0.082 0.695 0.182zM341.301 356.386c-0.147 0.38-0.475 0.562-0.728 0.405-0.285-0.176-0.221-0.44 0.164-0.683 0.842-0.533 0.872-0.521 0.565 0.279zM337.623 358.867c0.135 0.218-0.269 0.297-0.898 0.177s-1.142-0.298-1.142-0.395c0-0.358 1.803-0.166 2.041 0.219zM334.193 359.579c0 0.1-0.324 0.305-0.719 0.457-0.412 0.158-0.598 0.080-0.435-0.181 0.271-0.44 1.154-0.652 1.154-0.276zM337.49 364.611c-0.457 0.454-0.51 0.4-0.259-0.259 0.174-0.457 0.433-0.713 0.576-0.571s0 0.515-0.316 0.83zM393.478 366.461c-0.457 0.453-0.51 0.4-0.259-0.259 0.174-0.457 0.433-0.714 0.576-0.571s0 0.516-0.316 0.83zM305.967 374.203c-0.381 0.247-0.902 0.448-1.158 0.448s-0.151-0.202 0.231-0.448c0.381-0.246 0.902-0.448 1.158-0.448s0.151 0.202-0.231 0.448zM368.251 379.409c0.183 0.317 0.163 0.425-0.046 0.236s-0.71-0.084-1.115 0.231c-0.653 0.509-0.663 0.482-0.101-0.236 0.757-0.964 0.832-0.979 1.262-0.231zM329.271 382.302c-0.147 0.381-0.446 0.695-0.664 0.695s-0.279-0.313-0.132-0.695c0.147-0.381 0.446-0.695 0.664-0.695s0.279 0.313 0.132 0.695zM342.337 390.515c0.183 0.317 0.163 0.425-0.046 0.236s-0.71-0.084-1.115 0.231c-0.653 0.509-0.663 0.482-0.101-0.236 0.757-0.964 0.832-0.979 1.262-0.231zM323.089 395.488c0 0.255-0.091 0.463-0.204 0.463s-0.333-0.208-0.49-0.463c-0.158-0.254-0.065-0.462 0.204-0.462s0.49 0.208 0.49 0.462zM175.48 420.013c0 0.255-0.208 0.463-0.463 0.463s-0.463-0.208-0.463-0.463c0-0.254 0.208-0.462 0.463-0.462s0.463 0.208 0.463 0.462zM159.285 431.861c0 0.382-0.898 0.599-1.105 0.266-0.133-0.217 0.060-0.394 0.431-0.394s0.674 0.058 0.674 0.127zM151.013 437.721c-0.458 0.453-0.51 0.4-0.259-0.259 0.174-0.457 0.433-0.714 0.576-0.571s0 0.516-0.316 0.83zM106.073 441.937c0 0.096-0.317 0.438-0.704 0.76-0.59 0.49-0.657 0.461-0.413-0.175 0.249-0.648 1.12-1.103 1.12-0.583zM141.548 443.798c0 0.387-0.28 0.543-0.694 0.384-0.488-0.188-0.58-0.084-0.314 0.346 0.268 0.433 0.201 0.505-0.226 0.24-0.472-0.292-0.418-0.51 0.249-0.996 1.040-0.762 0.984-0.763 0.984 0.024zM130.335 452.452c-0.392 0.635-0.973 0.493-0.973-0.236 0-0.357 0.276-0.544 0.614-0.414s0.499 0.421 0.357 0.651zM94.358 462.207c-0.147 0.369-0.257 0.26-0.279-0.279-0.019-0.488 0.089-0.762 0.243-0.607s0.171 0.553 0.037 0.887zM94.504 463.25c0 0.112-0.208 0.333-0.463 0.49s-0.463 0.065-0.463-0.204c0-0.269 0.208-0.49 0.463-0.49s0.463 0.092 0.463 0.204zM111.347 465.9c-0.131 0.212-0.433 0.386-0.674 0.386s-0.435-0.174-0.435-0.386c0-0.212 0.303-0.386 0.674-0.386s0.566 0.174 0.435 0.386zM92.975 468.012c-0.117 0.45-0.221 0.193-0.231-0.569s0.086-1.13 0.213-0.819c0.127 0.314 0.135 0.938 0.017 1.388zM93.444 472.416c-0.111 0.575-0.201 0.105-0.201-1.040s0.090-1.614 0.201-1.040c0.111 0.575 0.111 1.51 0 2.082zM104.915 470.449c-0.157 0.255-0.494 0.462-0.749 0.462s-0.334-0.208-0.177-0.462c0.157-0.255 0.494-0.463 0.749-0.463s0.334 0.208 0.177 0.463zM94.504 474.153c0 0.255-0.091 0.462-0.204 0.462s-0.333-0.208-0.49-0.462c-0.157-0.255-0.065-0.463 0.204-0.463s0.49 0.208 0.49 0.463zM98.229 475.205l-1.082 1.028 0.876-1.228c0.482-0.675 0.97-1.139 1.082-1.027s-0.282 0.661-0.876 1.227zM562.314-46.697c0 0.096 0.365 0.461 0.811 0.811 0.734 0.577 0.75 0.559 0.174-0.174-0.604-0.77-0.986-1.016-0.986-0.635zM51.471-46.154c0 0.096 0.441 0.41 0.979 0.698 0.763 0.408 0.937 0.395 0.785-0.059-0.171-0.512-1.764-1.090-1.764-0.639zM-40.111-44.288c0.021 0.54 0.132 0.649 0.279 0.279 0.133-0.334 0.117-0.733-0.037-0.886s-0.263 0.12-0.243 0.607zM259.695-16.767c0 0.269 0.208 0.362 0.462 0.204s0.462-0.378 0.462-0.49c0-0.112-0.208-0.204-0.462-0.204s-0.462 0.221-0.462 0.49zM629.182-11.234c0.159 0.259 0.484 0.351 0.721 0.204 0.641-0.397 0.523-0.675-0.292-0.675-0.398 0-0.592 0.212-0.432 0.47zM306.661-9.39c0.158 0.255 0.378 0.462 0.49 0.462s0.204-0.208 0.204-0.462c0-0.255-0.221-0.462-0.49-0.462s-0.362 0.208-0.204 0.462zM-106.279-8.659c0.021 0.54 0.132 0.649 0.279 0.279 0.133-0.334 0.117-0.733-0.037-0.887s-0.263 0.12-0.243 0.607zM302.96-1.988c0.158 0.255 0.378 0.462 0.49 0.462s0.204-0.208 0.204-0.462c0-0.255-0.221-0.462-0.49-0.462s-0.362 0.208-0.204 0.462zM70.212-1.062c0.461 0.509 0.942 0.925 1.070 0.925s-0.146-0.416-0.605-0.925c-0.461-0.509-0.942-0.925-1.070-0.925s0.146 0.416 0.605 0.925zM337.859 4.195c0.616 0.67 1.211 1.127 1.318 1.019 0.332-0.332-0.349-1.15-1.424-1.709-0.967-0.505-0.964-0.471 0.105 0.689zM174.306 4.058c-0.168 0.271-0.206 0.594-0.084 0.714s0.345-0.101 0.494-0.493c0.313-0.815 0.046-0.959-0.411-0.221zM78.12 7.962c0.417 0.637 0.978 1.158 1.249 1.158s0-0.522-0.597-1.158c-0.597-0.637-1.159-1.158-1.248-1.158s0.18 0.522 0.597 1.158zM167.151 9.072c0 0.481 0.88 1.177 1.141 0.902 0.084-0.089-0.137-0.43-0.493-0.759s-0.646-0.393-0.646-0.144zM430.13 25.987c0 0.37 0.177 0.564 0.394 0.432s0.274-0.437 0.127-0.675c-0.375-0.604-0.522-0.538-0.522 0.243zM401.548 37.017c0.174 0.457 0.433 0.714 0.576 0.571s0-0.515-0.316-0.83c-0.457-0.454-0.51-0.4-0.259 0.259zM445.283 52.42c0.021 0.54 0.132 0.649 0.279 0.279 0.134-0.334 0.116-0.733-0.037-0.887s-0.262 0.12-0.243 0.607zM446.208 63.526c0.021 0.54 0.132 0.649 0.279 0.279 0.134-0.334 0.116-0.733-0.037-0.887s-0.262 0.12-0.243 0.607zM448.984 74.632c0.021 0.54 0.132 0.649 0.279 0.279 0.134-0.334 0.116-0.733-0.037-0.887s-0.262 0.12-0.243 0.607zM449.911 85.736c0.021 0.54 0.132 0.649 0.279 0.279 0.134-0.334 0.116-0.733-0.037-0.887s-0.262 0.12-0.243 0.607zM629.177 109.99c-0.158 0.255 0.051 0.462 0.462 0.462s0.619-0.208 0.462-0.462c-0.158-0.255-0.366-0.462-0.462-0.462s-0.305 0.208-0.462 0.462zM401.983 111.841c-0.158 0.255-0.079 0.462 0.176 0.462s0.593-0.208 0.748-0.462c0.158-0.255 0.079-0.462-0.176-0.462s-0.593 0.208-0.748 0.462zM404.065 111.841c0.381 0.247 0.902 0.448 1.158 0.448s0.151-0.202-0.231-0.448c-0.381-0.247-0.902-0.448-1.158-0.448s-0.151 0.202 0.231 0.448zM-106.316 112.767c0 0.255 0.221 0.462 0.49 0.462s0.362-0.208 0.204-0.462c-0.157-0.255-0.378-0.462-0.49-0.462s-0.204 0.208-0.204 0.462zM270.57 130.351c-0.461 0.509-0.646 0.925-0.414 0.925s0.683-0.416 1.002-0.925c0.317-0.509 0.505-0.925 0.414-0.925s-0.541 0.416-1.002 0.925zM266.868 134.050c-0.461 0.509-0.646 0.925-0.414 0.925s0.683-0.416 1.002-0.925c0.317-0.509 0.505-0.925 0.414-0.925s-0.541 0.416-1.002 0.925zM263.166 137.755c-0.461 0.509-0.646 0.925-0.414 0.925s0.683-0.416 1.002-0.925c0.317-0.509 0.505-0.925 0.414-0.925s-0.541 0.416-1.002 0.925zM-104.892 140.338c0.021 0.54 0.132 0.649 0.279 0.279 0.133-0.334 0.117-0.733-0.037-0.887s-0.263 0.12-0.243 0.607zM426.736 149.398c0 0.212 0.195 0.386 0.435 0.386s0.543-0.174 0.675-0.386c0.131-0.212-0.065-0.386-0.435-0.386s-0.675 0.174-0.675 0.386zM220.827 198.546c0 0.096 0.364 0.461 0.81 0.81 0.733 0.577 0.75 0.56 0.174-0.175-0.604-0.771-0.986-1.017-0.986-0.635zM381.485 218.438c-0.757 0.855-0.748 0.87 0.225 0.358 0.663-0.348 1.111-0.371 1.3-0.067 0.158 0.255 0.509 0.464 0.779 0.464 0.292 0 0.233-0.319-0.144-0.781-0.557-0.683-0.534-0.743 0.175-0.477 0.445 0.166 0.81 0.104 0.81-0.139 0-0.764-2.318-0.291-3.144 0.642zM353.819 222.496c-0.199 0.323-0.135 0.418 0.159 0.236 0.272-0.168 0.599-0.043 0.728 0.276 0.168 0.416 0.244 0.425 0.271 0.029 0.053-0.796-0.767-1.179-1.159-0.542zM346.039 222.818c-0.118 0.192 0.188 0.377 0.679 0.414 1.063 0.080 1.157-0.138 0.214-0.5-0.374-0.144-0.775-0.105-0.894 0.086zM365.384 231.801l-0.882 1.040 1.158-0.921c0.637-0.507 1.243-0.975 1.349-1.040s-0.018-0.12-0.276-0.12c-0.257 0-0.865 0.468-1.349 1.040zM338.858 235.195c0.021 0.54 0.132 0.649 0.279 0.279 0.133-0.334 0.116-0.733-0.037-0.887s-0.262 0.119-0.243 0.607zM340.671 248.344c0 0.255 0.325 0.463 0.721 0.463s0.594-0.208 0.435-0.463c-0.158-0.255-0.482-0.463-0.721-0.463s-0.435 0.208-0.435 0.463zM-104.892 262.033c0.021 0.54 0.132 0.649 0.279 0.279 0.133-0.334 0.117-0.733-0.037-0.887s-0.263 0.12-0.243 0.607zM628.735 262.193c-0.146 0.237-0.054 0.562 0.205 0.721s0.47-0.035 0.47-0.431c0-0.813-0.278-0.932-0.675-0.291zM416.745 270.207l-0.882 1.040 1.040-0.881c0.978-0.828 1.252-1.201 0.882-1.201-0.087 0-0.558 0.468-1.040 1.040zM438.518 281.228c-0.168 0.271-0.205 0.594-0.084 0.714s0.344-0.101 0.495-0.493c0.313-0.815 0.046-0.959-0.411-0.22zM122.576 285.207c-0.169 0.169-0.308 0.524-0.308 0.786 0 0.29 0.215 0.262 0.547-0.069 0.302-0.302 0.441-0.655 0.308-0.786s-0.378-0.101-0.547 0.069zM394.846 285.632c0.021 0.54 0.132 0.649 0.279 0.279 0.134-0.334 0.116-0.733-0.037-0.887s-0.262 0.12-0.243 0.607zM-39.685 288.774c0 0.096 0.364 0.461 0.81 0.81 0.733 0.577 0.75 0.56 0.175-0.175-0.604-0.77-0.986-1.016-0.986-0.635zM99.47 314.571c0.314 0.316 0.686 0.459 0.83 0.316s-0.114-0.401-0.571-0.576c-0.659-0.251-0.713-0.198-0.259 0.259zM476.437 326.197c-0.88 1.039-0.88 1.039 0.082 0.231 0.529-0.446 1.137-0.655 1.346-0.463s0.234 0.088 0.051-0.231c-0.435-0.757-0.45-0.752-1.478 0.463zM86.379 327.052c-0.4 0.646 0.611 0.88 1.125 0.258 0.383-0.462 0.335-0.613-0.199-0.613-0.389 0-0.805 0.159-0.925 0.353zM72.52 343.672c-0.159 0.259-0.449 0.372-0.644 0.251s-0.353 0.086-0.353 0.457c0 0.559 0.147 0.543 0.848-0.092 0.843-0.763 1.035-1.087 0.644-1.087-0.112 0-0.335 0.212-0.495 0.47zM563.702 411.221c0 0.255 0.221 0.463 0.49 0.463s0.362-0.208 0.205-0.463c-0.158-0.255-0.378-0.462-0.49-0.462s-0.205 0.208-0.205 0.462zM629.177 411.221c-0.158 0.255-0.065 0.463 0.205 0.463s0.49-0.208 0.49-0.463c0-0.255-0.091-0.462-0.205-0.462s-0.333 0.208-0.49 0.462zM193.131 411.634c-0.345 0.219-0.449 0.507-0.232 0.638s0.665-0.050 0.993-0.405c0.661-0.716 0.214-0.852-0.762-0.233zM562.718 444.885c-0.577 0.733-0.559 0.75 0.174 0.175 0.446-0.349 0.811-0.715 0.811-0.81 0-0.381-0.38-0.135-0.986 0.635zM96.356 477.39c-0.417 0.269-0.475 0.452-0.144 0.457 0.303 0.002 0.68-0.201 0.838-0.457 0.356-0.575 0.195-0.575-0.695 0zM18.39 503.31c0.159 0.259 0.484 0.351 0.721 0.204 0.641-0.397 0.523-0.675-0.291-0.675-0.398 0-0.593 0.212-0.431 0.47zM175.017 503.476c0 0.096 0.364 0.461 0.81 0.81 0.733 0.577 0.75 0.56 0.174-0.174-0.604-0.771-0.986-1.017-0.986-0.636zM349 568.082c0 0.255 0.221 0.462 0.49 0.462s0.362-0.208 0.204-0.462c-0.158-0.255-0.378-0.462-0.49-0.462s-0.204 0.208-0.204 0.462zM470.464 568.082c-0.158 0.255-0.065 0.462 0.204 0.462s0.49-0.208 0.49-0.462c0-0.255-0.091-0.462-0.204-0.462s-0.333 0.208-0.49 0.462z" horiz-adv-x="535" /> +<glyph unicode="" d="M182.109-13.426c-4.915 0.64-9.078 2.565-9.078 4.197 0 0.81-1.802 3.286-4.006 5.505l-4.006 4.034-9.078-0.882c-10.539-1.024-19.059 0.235-27.541 4.067-4.842 2.188-6.059 2.368-8.543 1.262-1.594-0.709-3.773-2.373-4.844-3.697-1.701-2.103-2.939-2.408-9.79-2.408h-7.844v4.724c0 4.644 0.097 4.773 5.784 7.651 6.222 3.149 6.484 3.56 4.45 7.004-1.028 1.741-1.040 2.368-0.055 2.726 0.963 0.351 0.987 1.107 0.096 3.065-0.651 1.43-1.55 2.599-1.996 2.599-1.623 0-2.338 6.633-0.829 7.691 0.934 0.654 1.041 1.186 0.303 1.495-0.63 0.262-0.937 1.909-0.68 3.656 0.272 1.854-0.015 3.178-0.689 3.178-0.635 0-0.884 0.708-0.551 1.572 0.449 1.17-0.512 1.75-3.751 2.269-5.246 0.839-6.522 2.163-5.784 6.002 0.602 3.138 1.859 3.808 8.368 4.464 2.329 0.234 4.716 0.55 5.303 0.705s1.915-0.092 2.95-0.545c1.035-0.452 3.319-0.613 5.073-0.354s3.191 0.070 3.191-0.415c0-1.761 2.213-0.811 6.138 2.637 2.204 1.936 5.346 4.361 6.979 5.387s2.705 2.3 2.379 2.827c-0.326 0.527 2.263 1.76 5.754 2.74 7.394 2.075 8.319 3.792 6.016 11.183-0.82 2.63-1.522 3.942-1.562 2.913-0.047-1.218-0.928-1.87-2.531-1.87-2.449 0-13.575 4.98-14.665 6.563-0.314 0.457-3.438 2.765-6.942 5.132-6.013 4.058-12.767 9.029-16.646 12.245-0.953 0.789-3.356 1.972-5.342 2.627s-6.115 3.236-9.175 5.735c-3.061 2.499-8.948 5.958-13.083 7.685-7.609 3.181-14.709 8.683-17.068 13.229-2.038 3.927-4.383 6.373-11.538 12.029-3.671 2.902-6.674 5.99-6.674 6.861s-0.721 1.584-1.602 1.584c-0.882 0-1.602 0.731-1.602 1.626s-1.92 3.71-4.269 6.257c-2.348 2.546-4.571 6.14-4.94 7.986s-1.251 3.356-1.96 3.356c-1.003 0-0.992 2.444 0.050 11.023 0.736 6.063 1.457 14.113 1.602 17.889 0.314 8.163 2.555 13.259 6.578 14.962 1.615 0.683 2.938 1.666 2.938 2.181s0.532 0.938 1.185 0.938c2.031 0 10.563 9.397 10.563 11.633 0 2.891-0.508 3.085-10.209 3.929-9.983 0.869-11.45 1.668-10.010 5.454 1.248 3.281 10.377 12.357 11.644 11.574 0.498-0.309 3.355 0.668 6.343 2.169 4.94 2.481 5.436 3.063 5.436 6.373 0 4.011 3.57 13.968 5.962 16.624 2.102 2.337 0.908 3.115-4.803 3.133-7.895 0.027-8.483 0.955-5.047 7.971l2.938 6 10.502-0.181c11.721-0.202 14.621-1.084 11.376-3.454-3.391-2.479-0.251-3.006 6.939-1.164 4.217 1.079 8.526 1.474 11.627 1.063 4.864-0.645 4.958-0.599 7.549 3.701 3.949 6.558 5.067 7.409 9.713 7.409 3.603 0 4.174 0.296 3.665 1.897-0.784 2.47 5.235 9.851 8.034 9.851 1.927 0 2.646-1.050 4.766-6.941 0.719-1.999 0.82-1.965 1.582 0.533 1.457 4.775 1.088 6.014-3.058 10.246-2.248 2.294-5.966 6.575-8.265 9.511-11.151 14.253-23.254 27.236-25.424 27.272-3.101 0.051-6.46 2.067-9.139 5.488-1.231 1.57-3.662 3.168-5.403 3.549-1.826 0.401-4.478 2.355-6.273 4.621-1.71 2.16-5.087 6.089-7.503 8.732s-4.394 5.187-4.394 5.652c0 0.466-2.099 3.109-4.666 5.875-5.131 5.528-8.151 10.713-8.151 13.99 0 1.169-0.422 2.387-0.939 2.707s-0.698 2.716-0.404 5.329c0.294 2.611 0.002 6.277-0.649 8.144-1.006 2.886-0.869 3.796 0.909 6.057 1.151 1.465 2.413 4.415 2.803 6.559 0.586 3.219 3.893 7.12 18.985 22.404 10.052 10.178 19.138 18.978 20.194 19.555 2.892 1.58 33.619 1.222 35.856-0.416 1.001-0.733 11.185-10.957 22.631-22.718 18.233-18.735 20.809-21.804 20.786-24.768-0.020-2.862 2.089-5.688 13.676-18.323l13.702-14.94 7.125-0.76c6.881-0.733 16.923-4.934 33.292-13.925 2.643-1.452 8.891-4.645 13.884-7.096 7.75-3.804 10.852-6.23 21.198-16.584 13.193-13.2 12.452-11.666 10.671-22.129-0.228-1.338 0.164-2.027 1.051-1.843 0.778 0.161 1.116-0.189 0.752-0.78-1.401-2.267 4.445-9.661 7.637-9.661 0.987 0 4.951-1.007 8.812-2.237 16.53-5.27 21.435-8.181 21.435-12.72 0-1.947 1.072-4.095 2.938-5.889 2.772-2.666 15.599-7.797 25.632-10.255 2.496-0.612 4.539-1.471 4.539-1.913s2.043-2.283 4.539-4.093c2.496-1.811 6.401-5.016 8.678-7.123s4.594-3.831 5.149-3.831c1.583 0 4.062-2.878 4.062-4.718 0-1.118 1.406-1.89 4.272-2.348 2.349-0.375 4.272-1.103 4.272-1.615 0-1.135 8.185-1.257 8.814-0.131 0.246 0.44 0.332 0.28 0.19-0.357-0.339-1.526 6.253-3.874 8.479-3.020 1.163 0.447 1.74 0.099 1.74-1.050 0-0.944 0.721-1.716 1.601-1.716s1.601-0.48 1.601-1.069c0-1.517 11.060-1.33 12.603 0.214 0.997 0.996 1.286 0.938 1.298-0.267 0.014-1.235 0.195-1.265 0.905-0.147 1.625 2.562 23.526 0.953 29.462-2.164 3.074-1.615 3.233-1.568 6.656 1.958 3.447 3.548 3.668 3.612 12.638 3.612 10.947 0 11.725-0.88 7.303-8.264-2.874-4.799-3.899-9.105-5.313-22.348-0.485-4.53-0.221-5.882 1.476-7.579 1.137-1.137 3.22-2.019 4.632-1.963 2.276 0.091 2.605-0.412 2.896-4.437 0.181-2.496 0.781-4.539 1.334-4.539 2.185 0 0.901-5.268-1.803-7.394-3.457-2.72-10.161-4.625-12.829-3.647-3.204 1.175-18.532 13.591-18.534 15.013-0.001 0.716-0.675 1.043-1.497 0.728s-1.812-0.060-2.199 0.567c-0.387 0.627-1.674 1.141-2.859 1.141-2.084 0-2.075-0.101 0.33-3.118 1.366-1.714 4.747-4.377 7.514-5.917s6.029-4.070 7.251-5.625c1.221-1.553 3.628-3.68 5.346-4.724s4.349-3.839 5.846-6.211c1.928-3.054 3.457-4.31 5.248-4.31 1.389 0 3.125-0.721 3.855-1.601s1.875-1.601 2.543-1.601c1.73 0 10.846-4.955 17.909-9.734 6.925-4.687 9.971-9.063 11.063-15.898 0.423-2.644 1.31-5.476 1.973-6.295 0.745-0.919 1.163-5.827 1.092-12.815-0.218-21.923 0.072-20.44-5.704-29.115-5.57-8.364-9.633-11.791-21.8-18.387-8.276-4.488-15.255-6.211-24.86-6.141-6.271 0.046-6.602-0.084-7.293-2.848-0.397-1.593-1.37-3.617-2.161-4.498s-2.454-4.324-3.696-7.648c-2.517-6.736-5.92-9.907-15.132-14.101-3.154-1.437-7.177-3.995-8.938-5.687-4.502-4.324-10.182-5.671-23.887-5.671-9.99 0-12.19-0.303-13.76-1.901-2.929-2.98-4.354-6.719-4.832-12.678-0.357-4.452-1.236-6.475-4.613-10.613-9.13-11.188-14.775-15.844-26.435-21.8l-11.499-5.874-15.103 0.284c-11.322 0.212-16.12 0.732-19.164 2.075-2.233 0.986-4.061 2.18-4.061 2.654s-1.497 0.863-3.325 0.863c-1.829 0-5.073 0.932-7.209 2.070-4.018 2.142-19.498 8.613-27.914 11.669-2.644 0.96-4.966 2.106-5.162 2.546s-1.277 0.801-2.403 0.801c-1.125 0-2.047 0.509-2.047 1.13s-0.426 0.868-0.946 0.546c-0.522-0.322-1.57 0.166-2.331 1.084s-3.309 2.65-5.661 3.85c-2.351 1.2-4.946 2.851-5.764 3.67s-1.93 1.215-2.47 0.882c-0.541-0.334-0.982-0.204-0.982 0.288s-2.46 1.842-5.47 3c-3.007 1.158-5.702 2.705-5.988 3.438s-1.203 1.334-2.041 1.334c-0.837 0-1.798 0.721-2.136 1.601-0.351 0.913-1.943 1.601-3.705 1.601-2.547 0-3.090 0.449-3.090 2.556 0 1.406-0.775 2.853-1.724 3.217-1.092 0.419-1.52 1.438-1.167 2.783 0.305 1.166 0.119 2.392-0.412 2.721s-0.969 0.121-0.969-0.462c0-2.179-10.892-14.603-13.93-15.89-3.524-1.493-3.908-2.584-1.585-4.512 2.58-2.142 3.205-8.334 1.255-12.425-0.949-1.991-2.148-3.619-2.663-3.619s-2.655-1.363-4.75-3.029c-3.714-2.951-3.759-3.074-1.749-4.796 1.898-1.625 2.516-1.452 2.19 0.615-0.199 1.26 1.801 0.935 2.643-0.429 0.418-0.676-0.154-1.928-1.273-2.78-1.953-1.49-1.942-1.524 0.28-0.863 1.273 0.378 2.623 0.188 3-0.423s0.058-1.113-0.711-1.119c-0.769-0.002-0.002-0.733 1.702-1.622s4.107-1.334 5.34-0.991c1.79 0.498 1.919 0.399 0.639-0.497-1.381-0.967-1.344-1.124 0.267-1.139 2.499-0.021 2.363-0.928-0.839-5.644-2.827-4.16-3.827-4.414-12.778-3.249z" horiz-adv-x="525" /> +<glyph unicode="" d="M7.335-30.005c-3.834 3.834-9.199 4.803-18.425 3.327-16.322-2.611-28.842 0.502-28.842 7.168 0 7.9 19.328 34.134 33.478 45.44 7 5.594 21.622 18.295 32.494 28.224 19.739 18.027 19.759 18.069 13.822 27.13-8.27 12.622-7.514 22.928 1.966 26.765 11.088 4.488 73.938 50.518 75.539 55.325 0.725 2.177-3.68 5.055-9.789 6.396l-11.106 2.44 2.492 23.377c1.37 12.857 3.852 25.929 5.511 29.049 1.984 3.728 0.655 10.314-3.877 19.205-10.303 20.209-9.052 25.831 7.167 32.204 7.737 3.039 14.067 7.659 14.067 10.267 0 3.294 4.963 4.741 16.255 4.741 14.407 0 15.732 0.631 11.649 5.55-3.165 3.813-9.627 5.322-20.65 4.82-35.683-1.62-38.388-0.828-44.132 12.917-2.927 7.006-5.322 16.5-5.322 21.1 0 5.268-2.050 8.361-5.543 8.361-4.343 0-5.311 3.221-4.471 14.864l1.073 14.864-31.575 8.241c-17.366 4.534-33.002 9.637-34.747 11.343-1.861 1.82 9.010 16.445 26.337 35.424l29.509 32.323 48.801-2.087c26.841-1.147 49.638-2.923 50.661-3.945s-0.595-10.484-3.594-21.023c-5.851-20.567-5.232-23.184 5.491-23.184 3.809 0 10.512-3.871 14.895-8.604 7.248-7.82 9.196-8.265 21.439-4.884 7.852 2.167 31.822 3.025 57.488 2.058 49.327-1.861 75.721-9.221 81.967-22.857 4.123-9 13.045-10.683 13.045-2.458 0 6.23 20.035 15.645 33.295 15.645 4.897 0 8.904 0.662 8.904 1.471s-5.485 12.917-12.188 26.905c-6.703 13.988-11.575 26.423-10.826 27.635 1.421 2.301 75.829-1.815 88.073-4.873 4.135-1.033 13.219-12.483 22.048-27.796 17.139-29.728 16.798-34.763-3.699-54.391-8.269-7.92-16.814-12.322-26.416-13.61-9.462-1.271-14.726-3.941-15.791-8.017-0.879-3.363-6.158-7.228-11.728-8.591s-15.933-6.457-23.025-11.323c-11.618-7.97-15.695-8.732-41.133-7.691l-28.236 1.156-3.863-15.824c-2.125-8.704-4.688-18.595-5.699-21.979-2.219-7.44 8.435-8.689 11.264-1.32 3.308 8.625 27.924 17.358 41.658 14.781 6.892-1.294 12.531-3.861 12.531-5.709s4.555-3.357 10.123-3.357c7.786 0 11.070-2.286 14.226-9.904 2.38-5.745 11.202-14.050 21.007-19.778 13.044-7.618 19.048-14.242 26.286-29 5.159-10.519 8.464-20.61 7.344-22.422s0.39-3.295 3.356-3.295c8.61 0 16.382-18.486 12.889-30.661-2.8-9.764-1.077-12.353 28.259-42.451 30.852-31.655 36.828-42.974 27.572-52.228-2.545-2.545 4.978-15.297 24.598-41.685 21.986-29.573 27.394-39.070 24.337-42.752-2.161-2.603-6.788-4.734-10.279-4.734s-8.86-2.51-11.927-5.575c-4.246-4.246-9.125-4.991-20.471-3.118-8.193 1.351-16.798 3.991-19.124 5.866s-7.413 2.399-11.308 1.162c-3.894-1.236-12.662-0.706-19.483 1.177-7.474 2.064-13.195 2.144-14.395 0.199-3.154-5.102-31.411-3.753-42.497 2.027-5.539 2.889-12.661 5.264-15.822 5.275-7.197 0.027-18.238 16.318-14.977 22.099 1.333 2.363 8.731 12.626 16.438 22.809l14.013 18.512-11.517 27.204c-12.596 29.746-12.784 30.623-9.072 42.149 2.18 6.772 1.283 7.913-6.229 7.913-9.031 0-19.829 8.103-19.829 14.879 0 2.104-7.346 18.212-16.323 35.793-13.985 27.387-19.079 33.873-35.551 45.269-21.137 14.623-32.524 14.957-32.524 0.954 0-5.738-3.14-9.429-10.683-12.553-14.365-5.949-11.382-12.012 6.56-13.334 8.070-0.596 14.672-2.574 14.672-4.396s2.479-3.315 5.512-3.315c3.743 0 4.856-2.067 3.467-6.444-1.265-3.99 0.652-8.884 5.038-12.853 12.017-10.875 9.077-21.649-8.974-32.884-13.798-8.589-19.928-10.063-43.594-10.476-20.454-0.358-28.647 0.854-31.849 4.711-2.37 2.858-6.492 5.195-9.155 5.195-3.462 0-4.844 4.957-4.844 17.346 0 14.451-1.215 17.732-7.276 19.655-4.001 1.271-10.078 6.916-13.501 12.546l-6.226 10.238-3.716-8.155c-4.436-9.736-2.063-25.214 4.734-30.853 5.248-4.355 6.404-16.493 2.195-23.029-1.421-2.206-9.139-7.421-17.154-11.589-12.866-6.69-14.843-9.389-16.893-23.061-2.721-18.144-28.902-54.571-42.564-59.217-10.782-3.668-10.749-6.241 0.324-25.137 9.245-15.774 9.242-29.199-0.007-36.876-7.162-5.943-45.398-12.029-46.919-7.468-1.541 4.625-15.533 5.099-20.209 0.683-1.837-1.736-14.039-3.979-27.116-4.988-18.517-1.428-24.971-0.637-29.183 3.577zM199.792 270.833c3 2.418 6.458 7.268 7.683 10.78 1.703 4.882 0.785 5.979-3.904 4.658-3.373-0.95-11.275-2.761-17.561-4.026-12.54-2.524-12.341-2.306-9.295-10.245 2.554-6.652 15.458-7.305 23.077-1.167z" /> +<glyph unicode="" d="M299.882-26.635c-6.921 5.454-12.582 13.088-12.582 16.964 0 10.227-10.716 12.917-15.542 3.901-8.036-15.017-45.638-26.014-52.268-15.288-4.941 7.995 2.9 27.007 17.228 41.769 13.306 13.711 19.563 28.703 19.985 47.89 0.079 3.584 2.64 8.589 5.691 11.12 8.371 6.947 6.988 19.142-2.171 19.142-10.346 0-30.996 28.907-30.996 43.389 0 11.491-5.036 36.876-12.127 61.142-3.469 11.866-5.505 13.543-16.382 13.497-7.151-0.032-19.981-4.958-30.243-11.614-9.803-6.358-20.965-13.43-24.809-15.716-6.638-3.95-6.564-4.747 1.513-16.091 5.691-7.991 10.823-11.326 15.538-10.094 3.873 1.012 9.358-0.95 12.19-4.363 4.325-5.21 3.771-11.82-3.45-41.26-8.097-33.014-8.208-35.341-1.923-39.936 8.075-5.905 4.179-13.798-6.809-13.798-6.944 0-14.44-5.002-41.965-28.001-17.457-14.587-27.178-18.653-34.82-14.564-4.45 2.382-7.704 8.101-7.704 13.543 0 5.914-1.802 8.752-4.839 7.626-7.031-2.609-27.306 3.623-29.078 8.938-2.693 8.079 18.617 24.074 32.072 24.074 6.882 0 14.68 2.613 17.331 5.808s7.219 5.808 10.153 5.808c2.933 0 5.333 1.43 5.333 3.179s7.94 11.465 17.645 21.594c9.703 10.128 16.971 20.457 16.149 22.954s-11.81 5.453-24.419 6.57c-12.608 1.116-26.828 4.217-31.596 6.889-11.719 6.567-20.365 24.381-20.365 41.957 0 13.078-0.505 13.792-5.206 7.363-4.954-6.774-5.319-6.69-7.523 1.743-1.435 5.487 2.715 22.296 10.896 44.126 13.308 35.516 23.471 48.828 54.022 70.769l11.537 8.286-10.115-1.934c-5.564-1.063-11.294-0.027-12.734 2.303s-6.974 4.236-12.297 4.236c-5.468 0-8.638 1.685-7.286 3.871 1.316 2.13 5.494 3.871 9.285 3.871 8.395 0 16.139 15.59 13.244 26.663-1.296 4.957-0.072 8.709 3.165 9.697 19.785 6.039 20.623 6.596 15.092 10.014-7.701 4.759-34.242-9.553-39.626-21.367-5.179-11.367-10.838-12.452-10.838-2.079 0 4.093 6.733 13.24 14.961 20.326s16.186 16.804 17.683 21.596c1.498 4.79 5.218 8.711 8.269 8.711 4.046 0 5.546 5.039 5.546 18.618 0 10.377 2.516 21.945 5.684 26.133 3.126 4.133 5.74 6.163 5.808 4.512s5.263-0.345 11.546 2.903c12.186 6.302 21.16 7.78 18.22 3.003-0.983-1.598 4.576-3.356 12.354-3.908s15.889-3.347 18.026-6.212c10.095-13.527 32.055-10.126 52.124 8.070l9.552 8.661 10.017-9.597c8.246-7.899 13.885-9.598 31.877-9.598 24.002 0 60.817-6.623 66.505-11.964 7.644-7.179-2.133-7.925-25.865-1.974-30.411 7.627-55.91 7.952-63.058 0.805-4.604-4.604-2.95-5.755 11.361-7.9 9.212-1.382 28.986-8.376 43.94-15.543s40.889-16.049 57.635-19.74c58.57-12.905 99.844-29.611 113.341-45.873 5.505-6.633 2.181-21.737-4.761-21.635-2.518 0.036-8.756 4.469-13.861 9.851s-16.438 13.977-25.185 19.103c-15.625 9.156-64.514 26.214-67.26 23.466-1.591-1.591 40.020-17.644 45.737-17.644 5.445 0 4.55-6.111-1.649-11.255-6.869-5.701-18.39-0.961-54.119 22.267-19.090 12.411-35.346 19.324-57.659 24.52-17.090 3.979-31.659 6.65-32.375 5.934s3.454-6.656 9.265-13.202c10.493-11.818 25.457-17.298 61.843-22.65 9.039-1.33 17.385-3.957 18.548-5.838 3.748-6.066-17.215-8.526-34.839-4.087-21.663 5.454-20.169 5.701-22.637-3.734-1.455-5.564 1.817-12.431 10.606-22.261 6.99-7.819 16.49-24.669 21.109-37.445s10.305-26.989 12.635-31.584c6.785-13.382-1.17-109.38-9.572-115.524-3.901-2.853-6.476-10.080-6.476-18.181 0-17.123-4.106-21.279-10.047-10.176-4.449 8.313-4.846 7.995-9.525-7.621-2.686-8.963-6.349-16.296-8.142-16.296-4.377 0-4.159 22.055 0.3 30.386 2.219 4.147 1.998 12.494-0.592 22.193-3.779 14.159-2.935 18.221 9.468 45.574 13.298 29.326 13.507 30.546 8.921 52.079-2.583 12.127-8.642 30.337-13.464 40.464-10.45 21.948-29.7 39.851-36.647 34.084-2.563-2.127-14.886-5.070-27.384-6.54-16.003-1.881-20.468-3.505-15.096-5.491 14.14-5.231 18.988-14.629 14.152-27.438-3.692-9.78-3.072-13.675 4.108-25.822 6.659-11.266 8.421-20.27 8.296-42.379-0.142-24.923 0.595-27.834 6.56-25.94 10.359 3.288 19.834-14.768 21.058-40.132 1.346-27.881 14.695-68.178 26.972-81.418 10.997-11.86 11.043-12.121 4.358-25.005-3.906-7.529-4.192-15.252-1.229-33.164 6.729-40.688-8.060-54.546-35.298-33.079zM99.457 210.171c4.122 7.985 10.722 19.292 14.668 25.127l7.175 10.609-8.875-3.284c-5.724-2.117-9.772-7.673-11.403-15.645-3.294-16.108-4.269-16.801-16.88-12.005-9.127 3.47-10.316 3.162-8.281-2.144 1.308-3.409 2.378-8.666 2.378-11.686 0-11.169 13.831-5.284 21.219 9.027zM155.668 272.062c0 3.507-4.562 1.807-13.55-5.048-7.452-5.684-13.55-10.971-13.55-11.749s6.097 1.494 13.55 5.048c7.452 3.554 13.55 8.841 13.55 11.749z" /> +<glyph unicode="" d="M146.201-29.308c-3.404 5.983-9.050 10.887-12.547 10.899-7.228 0.026-8.513 11.006-2.587 22.082 2.66 4.971 2.432 13.533-0.773 29.052-3.95 19.122-3.745 24.259 1.569 39.216 3.792 10.678 5.308 22.176 3.992 30.288-1.264 7.793 0.209 20.2 3.647 30.705 6.206 18.964 7.040 32.663 3.729 61.228-2.522 21.752-11.058 31.998-39.395 47.284-19.703 10.629-20.113 11.174-20.126 26.854-0.011 15.282-0.313 15.733-6.666 9.983-3.66-3.311-7.605-5.069-8.768-3.906s-6.428-0.711-11.7-4.165c-6.419-4.207-8.379-7.486-5.935-9.93s2.361-6.058-0.251-10.939c-3.756-7.020-2.356-30.863 2.9-49.375l2.477-8.726-14.193 7.341c-7.806 4.037-18.816 9.534-24.465 12.217-13.735 6.519-18.39 15.718-13.026 25.74 3.172 5.928 3.186 12.423 0.053 26.416-3.853 17.219-3.451 19.991 5.569 38.465 5.348 10.949 14.058 31.066 19.356 44.703 10.537 27.12 17.025 31.486 15.532 10.454-0.909-12.804-0.683-13.101 4.703-6.183 4.645 5.966 9.691 7.2 28.204 6.893 12.401-0.205 31.431-3.546 42.29-7.423 18.090-6.459 20.782-8.944 32.154-29.667 12.668-23.087 26.22-35.617 26.22-24.244 0 5.914 18.655 15.547 38.15 19.7l10.899 2.322 1.061 32.489c0.845 25.868 2.462 33.985 7.93 39.826 3.778 4.035 10.365 15.512 14.639 25.504 9.907 23.162 17.497 31.58 26.028 28.873 5.439-1.726 6.656-0.129 6.656 8.734 0 17.812 2.928 21.903 13.632 19.040 7.123-1.905 10.844-6.543 14.819-18.467 2.919-8.758 9.79-19.695 15.268-24.305 9.745-8.2 9.94-9.148 9.040-43.828-1.186-45.692-0.721-43.376-9.159-45.584-4.147-1.085-7.267-4.957-7.267-9.017 0-3.914-3.392-11.428-7.538-16.701-6.383-8.115-6.699-9.905-2.053-11.688 3.017-1.157 11.647-11.053 19.18-21.989l13.695-19.885 22.546 26.856 27.043-3.815c14.874-2.099 33.583-5.722 41.577-8.052s17.262-4.225 20.596-4.21c3.748 0.016 8.361-5.778 12.080-15.168 3.309-8.357 10.746-20.12 16.525-26.142l10.507-10.949-5.582-21.38c-4.128-15.808-4.498-22.051-1.416-23.955 3.077-1.902 3.143-6.513 0.253-17.63-2.152-8.279-5.612-21.594-7.687-29.587-3.764-14.496-3.82-14.533-21.8-14.533-19.45 0-19.964 0.625-14.18 17.219 3.457 9.918 2.277 13.658-11.629 36.875-8.512 14.212-17.401 25.839-19.753 25.839s-6.311 2.452-8.799 5.45c-5.503 6.63-10.857 7.072-11.021 0.909-0.271-10.171-13.163-19.733-29.651-21.993-24.566-3.366-46.46-10.886-46.983-16.134-1.728-17.375-7.577-26.587-20.237-31.877-15.517-6.483-14.042-6.425-19.923-0.781-2.64 2.534-8.865 6.366-13.831 8.519-9.549 4.138-11.923 1.979-21.7-19.727-2.726-6.051-5.744-7.798-10.736-6.215-5.607 1.779-7.433-0.098-9.484-9.748-1.398-6.574-4.724-14.436-7.393-17.474-3.561-4.053-4.004-9.28-1.666-19.651 2.819-12.497 1.738-16.764-9.352-36.947-10.804-19.662-11.757-23.257-6.882-25.984 4.907-2.747 5.035-5.063 0.957-17.481-3.017-9.188-6.918-14.33-10.893-14.357-3.406-0.021-8.218-2.067-10.692-4.542-8.215-8.216-21.125-5.248-27.74 6.377z" /> +<glyph unicode="" d="M553.036 83.129c-9.881 4.495-49.014 4.687-72.13 0.353-5.188-0.973-9.435-0.847-9.435 0.279s-10.777 1.481-23.949 0.787c-39.513-2.080-77.653-0.64-103.567 3.911-13.454 2.363-25.425 3.701-26.604 2.973s-2.637 0.158-3.239 1.969c-0.604 1.811-10.186 4.138-21.294 5.169s-21.829 2.901-23.825 4.154c-2.103 1.32-3.629 0.902-3.629-0.993 0-2.062-2.393-2.51-6.474-1.216-3.561 1.13-13.685 2.433-22.498 2.896s-26.954 2.221-40.314 3.908c-13.359 1.687-25.644 2.229-27.3 1.206s-2.281-0.68-1.388 0.764c0.898 1.453-6.357 2.676-16.243 2.738-9.827 0.062-30.276 1.154-45.445 2.427s-29.864 2.482-32.657 2.687c-2.794 0.204-5.081 1.476-5.081 2.825s-1.364 1.609-3.030 0.579c-1.667-1.029-5.913-0.010-9.434 2.269s-15.874 7.001-27.45 10.494l-21.046 6.352v11.384c0 7.745 1.595 12.276 4.989 14.175 3.721 2.082 4.769 5.844 4.122 14.806-1.023 14.158 1.806 18.224 18.471 26.546 10.531 5.259 12.697 7.872 14.171 17.085 2.047 12.799 4.968 15.626 21.091 20.411 9.825 2.916 12.82 5.554 16.267 14.329 3.975 10.119 5.598 11.151 27.477 17.473 23.046 6.66 23.303 6.84 29.041 20.425l5.791 13.705 20.006 2.133c24.244 2.584 27.843 4.347 36.077 17.671l6.43 10.404 41.511-0.741c41.796-0.746 53.507 1.68 48.702 10.091-1.037 1.815-0.86 2.375 0.392 1.246s4.589 0.802 7.416 4.292l5.138 6.346 25.708-6.822c58.24-15.454 54.591-15.19 65.666-4.745 14.354 13.539 35.764 27.658 41.942 27.658 2.959 0 8.601-2.527 12.541-5.615s15.652-11.866 26.031-19.505c10.378-7.639 19.522-14.714 20.32-15.721s4.355-3.361 7.905-5.23c3.549-1.869 14.269-9.903 23.822-17.852s18.732-14.454 20.399-14.454c1.667 0 3.039-0.979 3.051-2.178 0.012-1.197 4.909-5.431 10.886-9.409s11.519-7.982 12.317-8.9c0.798-0.918 7.748-5.953 15.444-11.192s13.436-10.079 12.754-10.761c-0.682-0.682-7.631-5.86-15.444-11.508s-12.092-9.863-9.51-9.367c2.582 0.495 11.538-5.63 19.899-13.613 25.37-24.221 40.721-39.656 40.721-40.944 0-0.671-7.345-5.919-16.321-11.661l-16.321-10.44 7.486-6.941c4.116-3.817 10.764-6.941 14.772-6.941 13.052 0 45.218-33.075 45.218-46.493 0-1.255-4.485-4.928-9.966-8.163l-9.966-5.88-4.615 7.045c-5.789 8.835-8.524 8.868-12.496 0.154-3.723-8.173-10.457-9.674-52.947-11.802-23.829-1.193-32.636-0.546-40.357 2.966zM621.649 160.176c1.97 1.983 4.312 2.876 5.205 1.982s-0.717-2.517-3.581-3.607c-4.138-1.575-4.471-1.242-1.625 1.625z" horiz-adv-x="684" /> +<glyph unicode="" d="M28.938 9.567c8.203 15.957 22.343 43.452 31.423 61.099s23.414 45.75 31.854 62.45c8.44 16.7 15.798 30.37 16.35 30.378 1.917 0.026 31.070-18.424 31.075-19.668 0.002-0.688-14.124-29.103-31.393-63.147l-31.398-61.897h30.715c16.894 0 30.715-0.295 30.715-0.655 0-0.739-10.597-24.617-14.403-32.454l-2.487-5.12h-107.367l14.914 29.014zM403.265-13.643c-1.387 3.192-5.053 11.514-8.144 18.493s-5.62 12.97-5.62 13.312c0 0.342 14.489 0.622 32.199 0.622h32.199l-31.515 62.194c-17.334 34.207-31.493 62.623-31.466 63.147 0.050 0.965 28.126 18.409 30.661 19.050 0.757 0.192 6.454-9.639 12.659-21.846 15.375-30.239 53.731-105.456 69.281-135.857l12.745-24.918h-110.474l-2.523 5.803zM150.983-15.009c0.388 0.938 5.746 13.073 11.907 26.966s14.132 32.017 17.715 40.278c3.583 8.26 9.922 22.699 14.087 32.086s9.696 21.863 12.291 27.724c2.596 5.861 5.096 11.033 5.555 11.491s0.834 1.746 0.834 2.861c0 1.114-1.847-0.524-4.105-3.641-4.517-6.235-4.543-6.249-8.155-4.315-2.22 1.188-2.638 0.656-3.482-4.438-0.528-3.183-1.52-6.856-2.206-8.163-1.506-2.867-8.185-6.499-11.952-6.499-2.584 0-2.624 0.251-0.75 4.735 2.725 6.521 2.503 8.518-1.373 12.394-5.101 5.101-13.025 4.654-19.255-1.086-4.202-3.872-4.939-5.536-5.782-13.055l-0.966-8.617-4.546 4.697c-5.664 5.854-7.718 13.87-5.596 21.846 3.369 12.664 13.338 19.961 27.346 20.016 5.544 0.022 9.487 1 14.336 3.558 9.016 4.754 9.489 5.341 7.626 9.43-1.401 3.075-1.043 4.224 3.121 10.022 14.167 19.719 15.399 36.096 2.95 39.221-10.481 2.63-29.222-19.428-33.769-39.75-0.924-4.129-2.143-7.337-2.709-7.128s-8.409 6.488-17.428 13.954l-16.398 13.575 7.633 8.245c17.323 18.711 20.011 39.793 6.030 47.292-12.584 6.749-39.636-11.006-52.521-34.47-2.388-4.349-4.802-7.913-5.365-7.922s-1.024 14.423-1.024 32.070v32.086h7.925c9.581 0 18.306 4.846 24.755 13.749 5.613 7.748 6.46 13.362 3.319 21.992-3.777 10.376-10.344 15.174-21.767 15.901-11.377 0.725-18.361-1.291-30.062-8.678-5.186-3.273-9.627-5.391-9.869-4.705s5.555 12.94 12.883 27.232l13.323 25.986h69.493c40.161 0 69.299-0.514 69.037-1.218-0.25-0.67-6.095-4.982-12.989-9.581-17.667-11.788-29.109-25.851-29.109-35.781 0-5.87 2.626-6.999 7.244-3.113 3.622 3.048 18.823 11.463 20.706 11.463 1.702 0 0.475-3.478-2.691-7.631-4.73-6.202-4.421-7.701 1.365-6.615 5.496 1.031 5.989-0.231 2.048-5.241-3.995-5.079-3.618-12.385 0.821-15.876 11.327-8.91 30.635-0.088 25.323 11.57-2.363 5.186-5.072 5.366-10.715 0.714-2.821-2.325-5.533-4.227-6.028-4.227-1.842 0-2.672 11.945-1.074 15.453 1.839 4.038 34.133 22.527 41.407 23.707 5.476 0.888 14.081-1.421 20.033-5.377 6.183-4.109 9.913-11.911 12.8-26.771 2.851-14.682 5.698-18.308 14.373-18.308 10.765 0 16.175 11.028 9.558 19.487-3.894 4.979-3.385 5.812 2.904 4.75 6.192-1.046 6.197-0.924 0.265 6.985-2.974 3.965-4.099 7.38-2.432 7.38 1.689 0 12.69-6.113 18.722-10.403 3.568-2.538 6.735-4.615 7.034-4.615s1.177 1.81 1.949 4.022c3.163 9.074-11.219 27.413-31.651 40.356-5.745 3.639-10.445 7.123-10.445 7.743s31.183 1.127 69.294 1.127h69.294l13.65-26.086c7.507-14.347 13.855-26.635 14.105-27.307 0.922-2.473-2.668-1.097-8.993 3.447-10.674 7.668-19.945 10.677-31.183 10.119-8.064-0.401-10.519-1.119-14.679-4.291-6.587-5.024-9.451-10.891-9.577-19.622-0.084-5.835 0.649-8.219 3.986-12.968 6.823-9.708 14.12-13.913 25.235-14.541l9.498-0.536v-31.851c0-17.518-0.534-32.181-1.188-32.585s-4.009 4.014-7.457 9.818c-12.779 21.51-30.158 34.446-46.275 34.446-4.286 0-6.24-0.874-9.312-4.161-9.798-10.488-5.091-29.025 11.775-46.383l5.997-6.171-16.975-13.967c-9.336-7.682-17.216-13.967-17.512-13.967s-1.325 3.533-2.285 7.851c-3.423 15.382-12.238 28.486-24.724 36.747-6.017 3.983-10.399 3.635-14.887-1.182-5.486-5.888-2.264-20.597 7.392-33.751 4.006-5.457 4.913-7.811 4.584-11.904-0.396-4.938-0.166-5.226 7.101-8.918 5.373-2.731 9.56-3.822 14.721-3.839 13.563-0.043 23.415-6.76 27.111-18.486 2.735-8.675 1.38-15.515-4.443-22.435-4.675-5.557-5.986-5.241-5.986 1.439 0 16-15.923 25.111-26.101 14.935l-4.045-4.045 2.88-5.645c1.584-3.104 2.526-5.999 2.093-6.432s-3.675 0.11-7.207 1.207c-6.336 1.967-6.447 2.103-8.524 10.392-1.929 7.696-2.327 8.273-4.761 6.901-2.281-1.286-3.403-0.647-7.943 4.522l-5.287 6.019 2.002-4.779c3.648-8.709 26.736-60.12 43.226-96.256 15.408-33.765 19.197-43.008 17.63-43.008-0.738 0-4.53 2.147-8.426 4.771-13.086 8.813-22.009 10.93-46.055 10.93-19.519 0-21.623-0.246-27.098-3.168-3.264-1.742-6.075-2.971-6.247-2.731s-5.181 12.257-11.132 26.706c-5.951 14.448-11.127 26.272-11.502 26.274s-4.019-7.211-8.096-16.029c-4.077-8.819-9.59-20.642-12.25-26.274s-4.885-10.373-4.941-10.534c-0.057-0.161-4.067 1.325-8.912 3.305-7.841 3.203-10.544 3.587-24.623 3.5-19.891-0.123-29.65-2.731-43.113-11.519-10.198-6.658-13.633-8.019-12.338-4.89zM214.739 373.18c0 0.728 13.025 26.293 38.727 76.010 6.11 11.818 11.416 21.491 11.791 21.496s6.298-10.894 13.16-24.219c6.863-13.325 18.075-35.088 24.918-48.363s12.44-24.488 12.44-24.917c0-0.43-9.53-0.781-21.178-0.781h-21.178l-3.683 7.51c-2.026 4.13-4.041 7.51-4.479 7.51s-2.454-3.379-4.479-7.51l-3.683-7.51h-21.178c-11.648 0-21.178 0.349-21.178 0.775z" /> +<glyph unicode="" d="M143.669-29.846c-1.073 0.121-5.025 0.547-8.782 0.947-7.403 0.788-19.823 3.435-28.625 6.101-23.677 7.17-37.576 17.552-49.066 36.653-1.358 2.257-2.364 4.209-2.236 4.336s1.843-1.225 3.811-3.005c5.773-5.221 13.625-10.93 18.194-13.229 15.59-7.844 35.456-10.461 50.572-6.659 20.344 5.116 32.672 19.028 32.698 36.899 0.014 9.656-2.584 17.005-8.823 24.954-7.871 10.029-23.465 16.315-35.137 14.164-7.558-1.392-11.272-3.18-15.6-7.508-4.728-4.728-6.728-9.325-7.243-16.647-0.42-5.972 0.828-11.476 4.043-17.839 1.251-2.476 2.18-4.599 2.063-4.716-0.623-0.623-6.803 4.399-10.413 8.46-12.346 13.886-20.596 32.982-20.596 47.671 0 5.762 0.787 11.393 1.824 13.063 0.35 0.562 1.545 0.259 4.37-1.108 8.098-3.917 15.762-3.987 21.284-0.195 4.844 3.326 8.954 11.783 8.954 18.421 0 5.322-1.734 8.999-6.452 13.68-12.419 12.324-28.798 10.286-38.304-4.765-8.904-14.099-12.593-26.54-13.737-46.326l-0.452-7.807-1.684 3.599c-2.203 4.708-5.433 16.431-7.139 25.916-2.516 13.983-1.87 48.056 1.142 60.229 0.526 2.127 1.083 3.994 1.236 4.147s1.828-0.749 3.721-2.005c1.894-1.256 4.614-2.608 6.045-3.005 6.148-1.704 18.279 0.048 23.605 3.411 9.606 6.064 12.614 20.374 6.364 30.275-6.656 10.545-26.481 13.135-38.467 5.026-10.007-6.771-20.759-22.079-27.93-39.766-1.127-2.779-1.568 3.995-0.932 14.313 1.111 18.026 4.366 30.702 11.728 45.675 4.239 8.62 12.669 21.741 17.146 26.686l2.806 3.1 0.962-2.304c1.539-3.684 7.927-10.1 12.084-12.137 3.325-1.629 4.377-1.81 10.525-1.814 8.071-0.005 10.375 0.859 15.073 5.655 6.593 6.731 8.647 15.804 5.174 22.857-2.531 5.141-8.004 10.51-13.465 13.209-4.204 2.077-5.534 2.405-10.828 2.673-10.154 0.512-20.056-2.736-32.215-10.57-4.273-2.753-8.613-6.618-17.341-15.447-13.352-13.506-12.908-13.609-7.826 1.833 5.259 15.98 10.331 25.26 20.779 38.013 11.511 14.051 23.279 23.763 35.923 29.648 8.186 3.81 21.715 8.358 21.629 7.272-0.007-0.090-0.436-2.505-0.953-5.367-1.817-10.055-0.927-17.434 2.516-20.866 6.993-6.971 23.779-6.688 31.99 0.541 5.417 4.768 7.1 14.66 4.131 24.273-3.16 10.233-14.235 16.779-37.672 22.269-9.648 2.26-10.605 2.363-22.12 2.382-7.458 0.013-13.767-0.311-16.59-0.852-8.772-1.682-16.284-3.545-19.027-4.721-1.522-0.653-2.767-0.932-2.767-0.621 0 0.784 12.304 11.791 18.216 16.295 7.178 5.468 13.59 9.432 22.341 13.808 14.263 7.134 23.822 10.203 36.159 11.612 7.944 0.907 33.738 0.445 38.76-0.694 4.602-1.044 9.434-2.632 9.434-3.099 0-0.219-1.385-3.187-3.077-6.597-3.952-7.964-4.483-12.961-1.954-18.386 1.861-3.99 6.145-8.524 9.911-10.484 4.021-2.093 13.030-2.33 18.488-0.486 10.958 3.704 15.102 10.043 15.125 23.134 0.013 7.799-1.537 14.975-5.336 24.69-5.82 14.885-14.717 24.067-30.879 31.868-11.564 5.582-20.82 8.103-38.55 10.498-4.743 0.641-8.403 1.364-8.132 1.606 1.511 1.355 30.884 5.141 44.572 5.746 22.565 0.996 47.472-2.5 61.68-8.657 11.188-4.848 23.372-13.498 29.255-20.768 1.778-2.197 3.232-4.179 3.232-4.404s-1.976-0.561-4.392-0.746c-11.626-0.89-20.196-7.785-22.346-17.979-1.346-6.384 0.827-14.555 5.066-19.045 2.504-2.653 8.923-5.248 13.851-5.601 9.565-0.685 18.28 3.011 26.363 11.178 2.863 2.892 6.19 6.939 7.394 8.993 4.503 7.68 7.91 21.411 7.87 31.722-0.054 14.003-4.412 28.556-11.133 37.175-5.495 7.047-20.374 22.588-26.773 27.964-2.919 2.452-3.465 3.171-2.411 3.171 4.399 0 29.851-11.736 41.642-19.201 19.281-12.208 37.308-27.6 46.154-39.406 6.57-8.768 12.172-21.046 14.961-32.788 2-8.42 2.188-8.013-3.377-7.315-6.643 0.832-15.039 0.204-19.478-1.457-4.305-1.611-7.077-4.437-9.57-9.749-1.546-3.295-1.69-4.313-1.67-11.74 0.021-7.333 0.187-8.483 1.689-11.71 5.188-11.141 20.875-15.512 34.553-9.628 7.88 3.39 17.987 19.090 22.533 35.001 3.048 10.668 3.595 14.552 3.577 25.373-0.014 8.687-0.267 11.165-1.826 17.891-0.995 4.294-2.95 11.003-4.343 14.909s-2.395 7.24-2.226 7.409c0.169 0.169 1.959-0.685 3.979-1.897 7.784-4.673 17.477-18.125 27.544-38.225 8.728-17.426 13.569-32.22 15.635-47.788 1.233-9.289 0.657-29.085-1.002-34.409-0.317-1.018-0.656-0.978-3.921 0.466-7.885 3.487-13.464 4.41-19.597 3.242-9.568-1.823-14.784-6.358-17.462-15.184-3.044-10.030 1.413-19.697 11.206-24.301 7.927-3.727 15.85-3.008 23.428 2.129 7.036 4.769 17.647 17.235 21.837 25.654 3.337 6.706 7.106 20.32 9.276 33.505 1.429 8.685 1.547 9.093 2.262 7.807 4.428-7.964 7.166-33.294 5.899-54.574-1.245-20.928-3.614-32.884-9.195-46.418-2.955-7.167-3.036-7.215-8.344-4.985-6.015 2.528-12.777 4.027-15.988 3.546-6.236-0.935-13.354-5.788-15.996-10.905-1.557-3.016-1.661-9.472-0.221-13.698 1.247-3.658 5.417-8.543 8.74-10.239 6.315-3.221 17.040-2.916 24.373 0.693 11.897 5.857 24.002 21.269 37.036 47.159l4.241 8.425 0.009-3.578c0.035-14.21-5.017-40.697-11.231-58.876-7.377-21.584-19.080-45.055-32.336-64.851-14.781-22.075-29.457-38.678-53.663-60.709-10.606-9.654-34.177-29.938-34.681-29.846-1.928 0.354-36.026 11.644-45.327 15.010-20.81 7.527-32.358 12.747-49.729 22.474-16.131 9.034-26.177 16.728-37.764 28.928-19.839 20.886-27.002 36.059-28.541 60.456-1.514 24.019 2.965 42.267 14.036 57.186 12.637 17.028 30.833 26.442 51.024 26.4 14.649-0.030 24.448-4.375 32.501-14.409 8.415-10.485 12.087-25.72 9.406-39.023-1.837-9.117-4.070-12.873-12.855-21.62-4.223-4.204-7.955-7.644-8.294-7.644s-0.002 1.683 0.749 3.741c2.015 5.514 2.084 18.721 0.12 22.966-3.367 7.276-9.343 11.091-18.077 11.541-20.171 1.040-31.976-11.283-30.721-32.067 0.672-11.125 3.441-17.791 9.812-23.614 10.015-9.153 23.478-14.029 38.73-14.027 10.343 0.002 15.608 1.246 24.972 5.901 14.132 7.026 27.512 22.467 33.57 38.74 3.231 8.68 4.17 14.599 4.204 26.503 0.036 12.492-0.747 17.694-4.24 28.18-8.618 25.865-28.298 46.664-52.001 54.957-6.801 2.38-16.16 4.039-22.77 4.039-23.346 0-54.407-15.071-75.865-36.811-9.071-9.19-13.325-15.342-19.757-28.572-8.491-17.465-11.411-25.857-13.073-37.579-1.091-7.694-0.917-27.449 0.308-35.013 4.791-29.57 25.784-69.702 47.672-91.132 10.654-10.43 33.916-29.582 44.451-36.594 9.964-6.634 30.013-15.88 44.564-20.551 2.505-0.804 4.821-1.707 5.147-2.007 1.493-1.372-12.557-14.322-23.363-21.536-13.94-9.306-30.944-16.639-49.736-21.45-15.696-4.018-24.597-5.111-40.694-5-7.514 0.052-14.541 0.193-15.613 0.314z" horiz-adv-x="461" /> +<glyph unicode="" d="M28.122-21.585c-7.453 8.935-15.794 27.236-20.636 45.281-3.804 14.176-4.274 18.933-4.372 44.238-0.101 26.147 0.236 29.596 4.335 44.303 4.602 16.513 16.762 42.263 22.345 47.315 3.071 2.779 3.331 2.763 6.822-0.435 5.197-4.762 15.525-9.605 23.203-10.883 25.311-4.212 36.898 26.827 23.398 62.682-7.027 18.665-26.559 34.538-44.816 36.423l-8.689 0.897v118.322c0 65.077 0.325 117.997 0.723 117.599s2.609-8.721 4.914-18.498c11.109-47.117 24.948-72.777 60.584-112.333 12.539-13.919 16.388-19.396 16.403-23.339 0.010-2.531-0.684-2.404-6.066 1.116-8.475 5.545-13.537 6.442-20.332 3.603-9.406-3.93-13.019-12.901-12.123-30.106 0.871-16.714 6.829-31.315 17.502-42.896 11.91-12.921 27.577-13.852 35.934-2.135 5.787 8.114 6.094 26.013 0.7 40.812-2.262 6.206-3.878 11.517-3.591 11.804s4.574-0.995 9.528-2.848c10.985-4.11 25.597-18.906 32.322-32.73 5.065-10.41 9.267-26.65 9.302-35.948l0.024-6.313-3.974 4.287c-7.338 7.917-17.403 3.992-19.584-7.637-1.278-6.81 1.522-13.2 7.828-17.868 6.369-4.714 25.081-4.555 34.544 0.293 7.578 3.882 19.351 16.311 25.68 27.109 15.133 25.822 13.055 66.546-4.948 96.982-3.1 5.241-5.274 9.892-4.83 10.336 1.704 1.704 27.962-27.015 33.788-36.957 3.369-5.749 8.102-16.65 10.519-24.224 4.031-12.633 4.385-15.85 4.272-38.886-0.111-22.899-0.574-26.781-5.238-44.007-2.814-10.391-5.834-20.781-6.712-23.089l-1.595-4.195h-69.431l-3.4-5.764c-1.87-3.17-3.401-5.904-3.402-6.076s16.040-0.086 35.645 0.192c19.606 0.277 35.647 0.184 35.647-0.207s-1.103-3.352-2.452-6.58l-2.452-5.869-72.058-0.040-7.321-11.301h35.66c19.613 0 35.661-0.589 35.661-1.31s-2.607-5.642-5.792-10.937c-16.993-28.241-43.651-55.305-77.651-78.827-11.67-8.074-35.612-22.343-37.489-22.343-0.409 0 1.379 3.099 3.974 6.886 20.517 29.952 22.041 61.447 3.434 70.94-8.457 4.315-12.266 4.033-23.874-1.769-32.751-16.366-59.94-75.686-55.839-121.83 0.456-5.124 0.426-9.317-0.065-9.317s-3.174 2.734-5.962 6.076zM45.106 175.538c-23.159 10.293-16.294 43.080 9.020 43.080 12.978 0 22.559-9.748 22.57-22.964 0.013-15.772-16.995-26.602-31.589-20.115z" horiz-adv-x="258" /> +</font></defs></svg>
\ No newline at end of file diff --git a/login/app/sprinkles/core/assets/font-starcraft/fonts/font-starcraft.ttf b/login/app/sprinkles/core/assets/font-starcraft/fonts/font-starcraft.ttf Binary files differnew file mode 100755 index 0000000..559deba --- /dev/null +++ b/login/app/sprinkles/core/assets/font-starcraft/fonts/font-starcraft.ttf diff --git a/login/app/sprinkles/core/assets/font-starcraft/fonts/font-starcraft.woff b/login/app/sprinkles/core/assets/font-starcraft/fonts/font-starcraft.woff Binary files differnew file mode 100755 index 0000000..a6b329f --- /dev/null +++ b/login/app/sprinkles/core/assets/font-starcraft/fonts/font-starcraft.woff diff --git a/login/app/sprinkles/core/assets/userfrosting/css/AdminLTE-skins-all.css b/login/app/sprinkles/core/assets/userfrosting/css/AdminLTE-skins-all.css new file mode 100755 index 0000000..53577c4 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/css/AdminLTE-skins-all.css @@ -0,0 +1,1770 @@ +/* + * Skin: Blue + * ---------- + */ +.skin-blue .main-header .navbar { + background-color: #3c8dbc; +} +.skin-blue .main-header .navbar .nav > li > a { + color: #ffffff; +} +.skin-blue .main-header .navbar .nav > li > a:hover, +.skin-blue .main-header .navbar .nav > li > a:active, +.skin-blue .main-header .navbar .nav > li > a:focus, +.skin-blue .main-header .navbar .nav .open > a, +.skin-blue .main-header .navbar .nav .open > a:hover, +.skin-blue .main-header .navbar .nav .open > a:focus, +.skin-blue .main-header .navbar .nav > .active > a { + background: rgba(0, 0, 0, 0.1); + color: #f6f6f6; +} +.skin-blue .main-header .navbar .sidebar-toggle { + color: #ffffff; +} +.skin-blue .main-header .navbar .sidebar-toggle:hover { + color: #f6f6f6; + background: rgba(0, 0, 0, 0.1); +} +.skin-blue .main-header .navbar .sidebar-toggle { + color: #fff; +} +.skin-blue .main-header .navbar .sidebar-toggle:hover { + background-color: #367fa9; +} +@media (max-width: 767px) { + .skin-blue .main-header .navbar .dropdown-menu li.divider { + background-color: rgba(255, 255, 255, 0.1); + } + .skin-blue .main-header .navbar .dropdown-menu li a { + color: #fff; + } + .skin-blue .main-header .navbar .dropdown-menu li a:hover { + background: #367fa9; + } +} +.skin-blue .main-header .logo { + background-color: #367fa9; + color: #ffffff; + border-bottom: 0 solid transparent; +} +.skin-blue .main-header .logo:hover { + background-color: #357ca5; +} +.skin-blue .main-header li.user-header { + background-color: #3c8dbc; +} +.skin-blue .content-header { + background: transparent; +} +.skin-blue .wrapper, +.skin-blue .main-sidebar, +.skin-blue .left-side { + background-color: #222d32; +} +.skin-blue .user-panel > .info, +.skin-blue .user-panel > .info > a { + color: #fff; +} +.skin-blue .sidebar-menu > li.header { + color: #4b646f; + background: #1a2226; +} +.skin-blue .sidebar-menu > li > a { + border-left: 3px solid transparent; +} +.skin-blue .sidebar-menu > li:hover > a, +.skin-blue .sidebar-menu > li.active > a { + color: #ffffff; + background: #1e282c; + border-left-color: #3c8dbc; +} +.skin-blue .sidebar-menu > li > .treeview-menu { + margin: 0 1px; + background: #2c3b41; +} +.skin-blue .sidebar a { + color: #b8c7ce; +} +.skin-blue .sidebar a:hover { + text-decoration: none; +} +.skin-blue .treeview-menu > li > a { + color: #8aa4af; +} +.skin-blue .treeview-menu > li.active > a, +.skin-blue .treeview-menu > li > a:hover { + color: #ffffff; +} +.skin-blue .sidebar-form { + border-radius: 3px; + border: 1px solid #374850; + margin: 10px 10px; +} +.skin-blue .sidebar-form input[type="text"], +.skin-blue .sidebar-form .btn { + box-shadow: none; + background-color: #374850; + border: 1px solid transparent; + height: 35px; +} +.skin-blue .sidebar-form input[type="text"] { + color: #666; + border-top-left-radius: 2px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 2px; +} +.skin-blue .sidebar-form input[type="text"]:focus, +.skin-blue .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + background-color: #fff; + color: #666; +} +.skin-blue .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + border-left-color: #fff; +} +.skin-blue .sidebar-form .btn { + color: #999; + border-top-left-radius: 0; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-bottom-left-radius: 0; +} +.skin-blue.layout-top-nav .main-header > .logo { + background-color: #3c8dbc; + color: #ffffff; + border-bottom: 0 solid transparent; +} +.skin-blue.layout-top-nav .main-header > .logo:hover { + background-color: #3b8ab8; +} +/* + * Skin: Blue + * ---------- + */ +.skin-blue-light .main-header .navbar { + background-color: #3c8dbc; +} +.skin-blue-light .main-header .navbar .nav > li > a { + color: #ffffff; +} +.skin-blue-light .main-header .navbar .nav > li > a:hover, +.skin-blue-light .main-header .navbar .nav > li > a:active, +.skin-blue-light .main-header .navbar .nav > li > a:focus, +.skin-blue-light .main-header .navbar .nav .open > a, +.skin-blue-light .main-header .navbar .nav .open > a:hover, +.skin-blue-light .main-header .navbar .nav .open > a:focus, +.skin-blue-light .main-header .navbar .nav > .active > a { + background: rgba(0, 0, 0, 0.1); + color: #f6f6f6; +} +.skin-blue-light .main-header .navbar .sidebar-toggle { + color: #ffffff; +} +.skin-blue-light .main-header .navbar .sidebar-toggle:hover { + color: #f6f6f6; + background: rgba(0, 0, 0, 0.1); +} +.skin-blue-light .main-header .navbar .sidebar-toggle { + color: #fff; +} +.skin-blue-light .main-header .navbar .sidebar-toggle:hover { + background-color: #367fa9; +} +@media (max-width: 767px) { + .skin-blue-light .main-header .navbar .dropdown-menu li.divider { + background-color: rgba(255, 255, 255, 0.1); + } + .skin-blue-light .main-header .navbar .dropdown-menu li a { + color: #fff; + } + .skin-blue-light .main-header .navbar .dropdown-menu li a:hover { + background: #367fa9; + } +} +.skin-blue-light .main-header .logo { + background-color: #3c8dbc; + color: #ffffff; + border-bottom: 0 solid transparent; +} +.skin-blue-light .main-header .logo:hover { + background-color: #3b8ab8; +} +.skin-blue-light .main-header li.user-header { + background-color: #3c8dbc; +} +.skin-blue-light .content-header { + background: transparent; +} +.skin-blue-light .wrapper, +.skin-blue-light .main-sidebar, +.skin-blue-light .left-side { + background-color: #f9fafc; +} +.skin-blue-light .content-wrapper, +.skin-blue-light .main-footer { + border-left: 1px solid #d2d6de; +} +.skin-blue-light .user-panel > .info, +.skin-blue-light .user-panel > .info > a { + color: #444444; +} +.skin-blue-light .sidebar-menu > li { + -webkit-transition: border-left-color 0.3s ease; + -o-transition: border-left-color 0.3s ease; + transition: border-left-color 0.3s ease; +} +.skin-blue-light .sidebar-menu > li.header { + color: #848484; + background: #f9fafc; +} +.skin-blue-light .sidebar-menu > li > a { + border-left: 3px solid transparent; + font-weight: 600; +} +.skin-blue-light .sidebar-menu > li:hover > a, +.skin-blue-light .sidebar-menu > li.active > a { + color: #000000; + background: #f4f4f5; +} +.skin-blue-light .sidebar-menu > li.active { + border-left-color: #3c8dbc; +} +.skin-blue-light .sidebar-menu > li.active > a { + font-weight: 600; +} +.skin-blue-light .sidebar-menu > li > .treeview-menu { + background: #f4f4f5; +} +.skin-blue-light .sidebar a { + color: #444444; +} +.skin-blue-light .sidebar a:hover { + text-decoration: none; +} +.skin-blue-light .treeview-menu > li > a { + color: #777777; +} +.skin-blue-light .treeview-menu > li.active > a, +.skin-blue-light .treeview-menu > li > a:hover { + color: #000000; +} +.skin-blue-light .treeview-menu > li.active > a { + font-weight: 600; +} +.skin-blue-light .sidebar-form { + border-radius: 3px; + border: 1px solid #d2d6de; + margin: 10px 10px; +} +.skin-blue-light .sidebar-form input[type="text"], +.skin-blue-light .sidebar-form .btn { + box-shadow: none; + background-color: #fff; + border: 1px solid transparent; + height: 35px; +} +.skin-blue-light .sidebar-form input[type="text"] { + color: #666; + border-top-left-radius: 2px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 2px; +} +.skin-blue-light .sidebar-form input[type="text"]:focus, +.skin-blue-light .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + background-color: #fff; + color: #666; +} +.skin-blue-light .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + border-left-color: #fff; +} +.skin-blue-light .sidebar-form .btn { + color: #999; + border-top-left-radius: 0; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-bottom-left-radius: 0; +} +@media (min-width: 768px) { + .skin-blue-light.sidebar-mini.sidebar-collapse .sidebar-menu > li > .treeview-menu { + border-left: 1px solid #d2d6de; + } +} +.skin-blue-light .main-footer { + border-top-color: #d2d6de; +} +.skin-blue.layout-top-nav .main-header > .logo { + background-color: #3c8dbc; + color: #ffffff; + border-bottom: 0 solid transparent; +} +.skin-blue.layout-top-nav .main-header > .logo:hover { + background-color: #3b8ab8; +} +/* + * Skin: Black + * ----------- + */ +/* skin-black navbar */ +.skin-black .main-header { + -webkit-box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.05); +} +.skin-black .main-header .navbar-toggle { + color: #333; +} +.skin-black .main-header .navbar-brand { + color: #333; + border-right: 1px solid #eee; +} +.skin-black .main-header .navbar { + background-color: #ffffff; +} +.skin-black .main-header .navbar .nav > li > a { + color: #333333; +} +.skin-black .main-header .navbar .nav > li > a:hover, +.skin-black .main-header .navbar .nav > li > a:active, +.skin-black .main-header .navbar .nav > li > a:focus, +.skin-black .main-header .navbar .nav .open > a, +.skin-black .main-header .navbar .nav .open > a:hover, +.skin-black .main-header .navbar .nav .open > a:focus, +.skin-black .main-header .navbar .nav > .active > a { + background: #ffffff; + color: #999999; +} +.skin-black .main-header .navbar .sidebar-toggle { + color: #333333; +} +.skin-black .main-header .navbar .sidebar-toggle:hover { + color: #999999; + background: #ffffff; +} +.skin-black .main-header .navbar > .sidebar-toggle { + color: #333; + border-right: 1px solid #eee; +} +.skin-black .main-header .navbar .navbar-nav > li > a { + border-right: 1px solid #eee; +} +.skin-black .main-header .navbar .navbar-custom-menu .navbar-nav > li > a, +.skin-black .main-header .navbar .navbar-right > li > a { + border-left: 1px solid #eee; + border-right-width: 0; +} +.skin-black .main-header > .logo { + background-color: #ffffff; + color: #333333; + border-bottom: 0 solid transparent; + border-right: 1px solid #eee; +} +.skin-black .main-header > .logo:hover { + background-color: #fcfcfc; +} +@media (max-width: 767px) { + .skin-black .main-header > .logo { + background-color: #222222; + color: #ffffff; + border-bottom: 0 solid transparent; + border-right: none; + } + .skin-black .main-header > .logo:hover { + background-color: #1f1f1f; + } +} +.skin-black .main-header li.user-header { + background-color: #222; +} +.skin-black .content-header { + background: transparent; + box-shadow: none; +} +.skin-black .wrapper, +.skin-black .main-sidebar, +.skin-black .left-side { + background-color: #222d32; +} +.skin-black .user-panel > .info, +.skin-black .user-panel > .info > a { + color: #fff; +} +.skin-black .sidebar-menu > li.header { + color: #4b646f; + background: #1a2226; +} +.skin-black .sidebar-menu > li > a { + border-left: 3px solid transparent; +} +.skin-black .sidebar-menu > li:hover > a, +.skin-black .sidebar-menu > li.active > a { + color: #ffffff; + background: #1e282c; + border-left-color: #ffffff; +} +.skin-black .sidebar-menu > li > .treeview-menu { + margin: 0 1px; + background: #2c3b41; +} +.skin-black .sidebar a { + color: #b8c7ce; +} +.skin-black .sidebar a:hover { + text-decoration: none; +} +.skin-black .treeview-menu > li > a { + color: #8aa4af; +} +.skin-black .treeview-menu > li.active > a, +.skin-black .treeview-menu > li > a:hover { + color: #ffffff; +} +.skin-black .sidebar-form { + border-radius: 3px; + border: 1px solid #374850; + margin: 10px 10px; +} +.skin-black .sidebar-form input[type="text"], +.skin-black .sidebar-form .btn { + box-shadow: none; + background-color: #374850; + border: 1px solid transparent; + height: 35px; +} +.skin-black .sidebar-form input[type="text"] { + color: #666; + border-top-left-radius: 2px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 2px; +} +.skin-black .sidebar-form input[type="text"]:focus, +.skin-black .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + background-color: #fff; + color: #666; +} +.skin-black .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + border-left-color: #fff; +} +.skin-black .sidebar-form .btn { + color: #999; + border-top-left-radius: 0; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-bottom-left-radius: 0; +} +.skin-black .pace .pace-progress { + background: #222; +} +.skin-black .pace .pace-activity { + border-top-color: #222; + border-left-color: #222; +} +/* + * Skin: Black + * ----------- + */ +/* skin-black navbar */ +.skin-black-light .main-header { + -webkit-box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.05); +} +.skin-black-light .main-header .navbar-toggle { + color: #333; +} +.skin-black-light .main-header .navbar-brand { + color: #333; + border-right: 1px solid #eee; +} +.skin-black-light .main-header .navbar { + background-color: #ffffff; +} +.skin-black-light .main-header .navbar .nav > li > a { + color: #333333; +} +.skin-black-light .main-header .navbar .nav > li > a:hover, +.skin-black-light .main-header .navbar .nav > li > a:active, +.skin-black-light .main-header .navbar .nav > li > a:focus, +.skin-black-light .main-header .navbar .nav .open > a, +.skin-black-light .main-header .navbar .nav .open > a:hover, +.skin-black-light .main-header .navbar .nav .open > a:focus, +.skin-black-light .main-header .navbar .nav > .active > a { + background: #ffffff; + color: #999999; +} +.skin-black-light .main-header .navbar .sidebar-toggle { + color: #333333; +} +.skin-black-light .main-header .navbar .sidebar-toggle:hover { + color: #999999; + background: #ffffff; +} +.skin-black-light .main-header .navbar > .sidebar-toggle { + color: #333; + border-right: 1px solid #eee; +} +.skin-black-light .main-header .navbar .navbar-nav > li > a { + border-right: 1px solid #eee; +} +.skin-black-light .main-header .navbar .navbar-custom-menu .navbar-nav > li > a, +.skin-black-light .main-header .navbar .navbar-right > li > a { + border-left: 1px solid #eee; + border-right-width: 0; +} +.skin-black-light .main-header > .logo { + background-color: #ffffff; + color: #333333; + border-bottom: 0 solid transparent; + border-right: 1px solid #eee; +} +.skin-black-light .main-header > .logo:hover { + background-color: #fcfcfc; +} +@media (max-width: 767px) { + .skin-black-light .main-header > .logo { + background-color: #222222; + color: #ffffff; + border-bottom: 0 solid transparent; + border-right: none; + } + .skin-black-light .main-header > .logo:hover { + background-color: #1f1f1f; + } +} +.skin-black-light .main-header li.user-header { + background-color: #222; +} +.skin-black-light .content-header { + background: transparent; + box-shadow: none; +} +.skin-black-light .wrapper, +.skin-black-light .main-sidebar, +.skin-black-light .left-side { + background-color: #f9fafc; +} +.skin-black-light .content-wrapper, +.skin-black-light .main-footer { + border-left: 1px solid #d2d6de; +} +.skin-black-light .user-panel > .info, +.skin-black-light .user-panel > .info > a { + color: #444444; +} +.skin-black-light .sidebar-menu > li { + -webkit-transition: border-left-color 0.3s ease; + -o-transition: border-left-color 0.3s ease; + transition: border-left-color 0.3s ease; +} +.skin-black-light .sidebar-menu > li.header { + color: #848484; + background: #f9fafc; +} +.skin-black-light .sidebar-menu > li > a { + border-left: 3px solid transparent; + font-weight: 600; +} +.skin-black-light .sidebar-menu > li:hover > a, +.skin-black-light .sidebar-menu > li.active > a { + color: #000000; + background: #f4f4f5; +} +.skin-black-light .sidebar-menu > li.active { + border-left-color: #ffffff; +} +.skin-black-light .sidebar-menu > li.active > a { + font-weight: 600; +} +.skin-black-light .sidebar-menu > li > .treeview-menu { + background: #f4f4f5; +} +.skin-black-light .sidebar a { + color: #444444; +} +.skin-black-light .sidebar a:hover { + text-decoration: none; +} +.skin-black-light .treeview-menu > li > a { + color: #777777; +} +.skin-black-light .treeview-menu > li.active > a, +.skin-black-light .treeview-menu > li > a:hover { + color: #000000; +} +.skin-black-light .treeview-menu > li.active > a { + font-weight: 600; +} +.skin-black-light .sidebar-form { + border-radius: 3px; + border: 1px solid #d2d6de; + margin: 10px 10px; +} +.skin-black-light .sidebar-form input[type="text"], +.skin-black-light .sidebar-form .btn { + box-shadow: none; + background-color: #fff; + border: 1px solid transparent; + height: 35px; +} +.skin-black-light .sidebar-form input[type="text"] { + color: #666; + border-top-left-radius: 2px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 2px; +} +.skin-black-light .sidebar-form input[type="text"]:focus, +.skin-black-light .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + background-color: #fff; + color: #666; +} +.skin-black-light .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + border-left-color: #fff; +} +.skin-black-light .sidebar-form .btn { + color: #999; + border-top-left-radius: 0; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-bottom-left-radius: 0; +} +@media (min-width: 768px) { + .skin-black-light.sidebar-mini.sidebar-collapse .sidebar-menu > li > .treeview-menu { + border-left: 1px solid #d2d6de; + } +} +/* + * Skin: Green + * ----------- + */ +.skin-green .main-header .navbar { + background-color: #00a65a; +} +.skin-green .main-header .navbar .nav > li > a { + color: #ffffff; +} +.skin-green .main-header .navbar .nav > li > a:hover, +.skin-green .main-header .navbar .nav > li > a:active, +.skin-green .main-header .navbar .nav > li > a:focus, +.skin-green .main-header .navbar .nav .open > a, +.skin-green .main-header .navbar .nav .open > a:hover, +.skin-green .main-header .navbar .nav .open > a:focus, +.skin-green .main-header .navbar .nav > .active > a { + background: rgba(0, 0, 0, 0.1); + color: #f6f6f6; +} +.skin-green .main-header .navbar .sidebar-toggle { + color: #ffffff; +} +.skin-green .main-header .navbar .sidebar-toggle:hover { + color: #f6f6f6; + background: rgba(0, 0, 0, 0.1); +} +.skin-green .main-header .navbar .sidebar-toggle { + color: #fff; +} +.skin-green .main-header .navbar .sidebar-toggle:hover { + background-color: #008d4c; +} +@media (max-width: 767px) { + .skin-green .main-header .navbar .dropdown-menu li.divider { + background-color: rgba(255, 255, 255, 0.1); + } + .skin-green .main-header .navbar .dropdown-menu li a { + color: #fff; + } + .skin-green .main-header .navbar .dropdown-menu li a:hover { + background: #008d4c; + } +} +.skin-green .main-header .logo { + background-color: #008d4c; + color: #ffffff; + border-bottom: 0 solid transparent; +} +.skin-green .main-header .logo:hover { + background-color: #008749; +} +.skin-green .main-header li.user-header { + background-color: #00a65a; +} +.skin-green .content-header { + background: transparent; +} +.skin-green .wrapper, +.skin-green .main-sidebar, +.skin-green .left-side { + background-color: #222d32; +} +.skin-green .user-panel > .info, +.skin-green .user-panel > .info > a { + color: #fff; +} +.skin-green .sidebar-menu > li.header { + color: #4b646f; + background: #1a2226; +} +.skin-green .sidebar-menu > li > a { + border-left: 3px solid transparent; +} +.skin-green .sidebar-menu > li:hover > a, +.skin-green .sidebar-menu > li.active > a { + color: #ffffff; + background: #1e282c; + border-left-color: #00a65a; +} +.skin-green .sidebar-menu > li > .treeview-menu { + margin: 0 1px; + background: #2c3b41; +} +.skin-green .sidebar a { + color: #b8c7ce; +} +.skin-green .sidebar a:hover { + text-decoration: none; +} +.skin-green .treeview-menu > li > a { + color: #8aa4af; +} +.skin-green .treeview-menu > li.active > a, +.skin-green .treeview-menu > li > a:hover { + color: #ffffff; +} +.skin-green .sidebar-form { + border-radius: 3px; + border: 1px solid #374850; + margin: 10px 10px; +} +.skin-green .sidebar-form input[type="text"], +.skin-green .sidebar-form .btn { + box-shadow: none; + background-color: #374850; + border: 1px solid transparent; + height: 35px; +} +.skin-green .sidebar-form input[type="text"] { + color: #666; + border-top-left-radius: 2px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 2px; +} +.skin-green .sidebar-form input[type="text"]:focus, +.skin-green .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + background-color: #fff; + color: #666; +} +.skin-green .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + border-left-color: #fff; +} +.skin-green .sidebar-form .btn { + color: #999; + border-top-left-radius: 0; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-bottom-left-radius: 0; +} +/* + * Skin: Green + * ----------- + */ +.skin-green-light .main-header .navbar { + background-color: #00a65a; +} +.skin-green-light .main-header .navbar .nav > li > a { + color: #ffffff; +} +.skin-green-light .main-header .navbar .nav > li > a:hover, +.skin-green-light .main-header .navbar .nav > li > a:active, +.skin-green-light .main-header .navbar .nav > li > a:focus, +.skin-green-light .main-header .navbar .nav .open > a, +.skin-green-light .main-header .navbar .nav .open > a:hover, +.skin-green-light .main-header .navbar .nav .open > a:focus, +.skin-green-light .main-header .navbar .nav > .active > a { + background: rgba(0, 0, 0, 0.1); + color: #f6f6f6; +} +.skin-green-light .main-header .navbar .sidebar-toggle { + color: #ffffff; +} +.skin-green-light .main-header .navbar .sidebar-toggle:hover { + color: #f6f6f6; + background: rgba(0, 0, 0, 0.1); +} +.skin-green-light .main-header .navbar .sidebar-toggle { + color: #fff; +} +.skin-green-light .main-header .navbar .sidebar-toggle:hover { + background-color: #008d4c; +} +@media (max-width: 767px) { + .skin-green-light .main-header .navbar .dropdown-menu li.divider { + background-color: rgba(255, 255, 255, 0.1); + } + .skin-green-light .main-header .navbar .dropdown-menu li a { + color: #fff; + } + .skin-green-light .main-header .navbar .dropdown-menu li a:hover { + background: #008d4c; + } +} +.skin-green-light .main-header .logo { + background-color: #00a65a; + color: #ffffff; + border-bottom: 0 solid transparent; +} +.skin-green-light .main-header .logo:hover { + background-color: #00a157; +} +.skin-green-light .main-header li.user-header { + background-color: #00a65a; +} +.skin-green-light .content-header { + background: transparent; +} +.skin-green-light .wrapper, +.skin-green-light .main-sidebar, +.skin-green-light .left-side { + background-color: #f9fafc; +} +.skin-green-light .content-wrapper, +.skin-green-light .main-footer { + border-left: 1px solid #d2d6de; +} +.skin-green-light .user-panel > .info, +.skin-green-light .user-panel > .info > a { + color: #444444; +} +.skin-green-light .sidebar-menu > li { + -webkit-transition: border-left-color 0.3s ease; + -o-transition: border-left-color 0.3s ease; + transition: border-left-color 0.3s ease; +} +.skin-green-light .sidebar-menu > li.header { + color: #848484; + background: #f9fafc; +} +.skin-green-light .sidebar-menu > li > a { + border-left: 3px solid transparent; + font-weight: 600; +} +.skin-green-light .sidebar-menu > li:hover > a, +.skin-green-light .sidebar-menu > li.active > a { + color: #000000; + background: #f4f4f5; +} +.skin-green-light .sidebar-menu > li.active { + border-left-color: #00a65a; +} +.skin-green-light .sidebar-menu > li.active > a { + font-weight: 600; +} +.skin-green-light .sidebar-menu > li > .treeview-menu { + background: #f4f4f5; +} +.skin-green-light .sidebar a { + color: #444444; +} +.skin-green-light .sidebar a:hover { + text-decoration: none; +} +.skin-green-light .treeview-menu > li > a { + color: #777777; +} +.skin-green-light .treeview-menu > li.active > a, +.skin-green-light .treeview-menu > li > a:hover { + color: #000000; +} +.skin-green-light .treeview-menu > li.active > a { + font-weight: 600; +} +.skin-green-light .sidebar-form { + border-radius: 3px; + border: 1px solid #d2d6de; + margin: 10px 10px; +} +.skin-green-light .sidebar-form input[type="text"], +.skin-green-light .sidebar-form .btn { + box-shadow: none; + background-color: #fff; + border: 1px solid transparent; + height: 35px; +} +.skin-green-light .sidebar-form input[type="text"] { + color: #666; + border-top-left-radius: 2px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 2px; +} +.skin-green-light .sidebar-form input[type="text"]:focus, +.skin-green-light .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + background-color: #fff; + color: #666; +} +.skin-green-light .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + border-left-color: #fff; +} +.skin-green-light .sidebar-form .btn { + color: #999; + border-top-left-radius: 0; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-bottom-left-radius: 0; +} +@media (min-width: 768px) { + .skin-green-light.sidebar-mini.sidebar-collapse .sidebar-menu > li > .treeview-menu { + border-left: 1px solid #d2d6de; + } +} +/* + * Skin: Red + * --------- + */ +.skin-red .main-header .navbar { + background-color: #dd4b39; +} +.skin-red .main-header .navbar .nav > li > a { + color: #ffffff; +} +.skin-red .main-header .navbar .nav > li > a:hover, +.skin-red .main-header .navbar .nav > li > a:active, +.skin-red .main-header .navbar .nav > li > a:focus, +.skin-red .main-header .navbar .nav .open > a, +.skin-red .main-header .navbar .nav .open > a:hover, +.skin-red .main-header .navbar .nav .open > a:focus, +.skin-red .main-header .navbar .nav > .active > a { + background: rgba(0, 0, 0, 0.1); + color: #f6f6f6; +} +.skin-red .main-header .navbar .sidebar-toggle { + color: #ffffff; +} +.skin-red .main-header .navbar .sidebar-toggle:hover { + color: #f6f6f6; + background: rgba(0, 0, 0, 0.1); +} +.skin-red .main-header .navbar .sidebar-toggle { + color: #fff; +} +.skin-red .main-header .navbar .sidebar-toggle:hover { + background-color: #d73925; +} +@media (max-width: 767px) { + .skin-red .main-header .navbar .dropdown-menu li.divider { + background-color: rgba(255, 255, 255, 0.1); + } + .skin-red .main-header .navbar .dropdown-menu li a { + color: #fff; + } + .skin-red .main-header .navbar .dropdown-menu li a:hover { + background: #d73925; + } +} +.skin-red .main-header .logo { + background-color: #d73925; + color: #ffffff; + border-bottom: 0 solid transparent; +} +.skin-red .main-header .logo:hover { + background-color: #d33724; +} +.skin-red .main-header li.user-header { + background-color: #dd4b39; +} +.skin-red .content-header { + background: transparent; +} +.skin-red .wrapper, +.skin-red .main-sidebar, +.skin-red .left-side { + background-color: #222d32; +} +.skin-red .user-panel > .info, +.skin-red .user-panel > .info > a { + color: #fff; +} +.skin-red .sidebar-menu > li.header { + color: #4b646f; + background: #1a2226; +} +.skin-red .sidebar-menu > li > a { + border-left: 3px solid transparent; +} +.skin-red .sidebar-menu > li:hover > a, +.skin-red .sidebar-menu > li.active > a { + color: #ffffff; + background: #1e282c; + border-left-color: #dd4b39; +} +.skin-red .sidebar-menu > li > .treeview-menu { + margin: 0 1px; + background: #2c3b41; +} +.skin-red .sidebar a { + color: #b8c7ce; +} +.skin-red .sidebar a:hover { + text-decoration: none; +} +.skin-red .treeview-menu > li > a { + color: #8aa4af; +} +.skin-red .treeview-menu > li.active > a, +.skin-red .treeview-menu > li > a:hover { + color: #ffffff; +} +.skin-red .sidebar-form { + border-radius: 3px; + border: 1px solid #374850; + margin: 10px 10px; +} +.skin-red .sidebar-form input[type="text"], +.skin-red .sidebar-form .btn { + box-shadow: none; + background-color: #374850; + border: 1px solid transparent; + height: 35px; +} +.skin-red .sidebar-form input[type="text"] { + color: #666; + border-top-left-radius: 2px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 2px; +} +.skin-red .sidebar-form input[type="text"]:focus, +.skin-red .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + background-color: #fff; + color: #666; +} +.skin-red .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + border-left-color: #fff; +} +.skin-red .sidebar-form .btn { + color: #999; + border-top-left-radius: 0; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-bottom-left-radius: 0; +} +/* + * Skin: Red + * --------- + */ +.skin-red-light .main-header .navbar { + background-color: #dd4b39; +} +.skin-red-light .main-header .navbar .nav > li > a { + color: #ffffff; +} +.skin-red-light .main-header .navbar .nav > li > a:hover, +.skin-red-light .main-header .navbar .nav > li > a:active, +.skin-red-light .main-header .navbar .nav > li > a:focus, +.skin-red-light .main-header .navbar .nav .open > a, +.skin-red-light .main-header .navbar .nav .open > a:hover, +.skin-red-light .main-header .navbar .nav .open > a:focus, +.skin-red-light .main-header .navbar .nav > .active > a { + background: rgba(0, 0, 0, 0.1); + color: #f6f6f6; +} +.skin-red-light .main-header .navbar .sidebar-toggle { + color: #ffffff; +} +.skin-red-light .main-header .navbar .sidebar-toggle:hover { + color: #f6f6f6; + background: rgba(0, 0, 0, 0.1); +} +.skin-red-light .main-header .navbar .sidebar-toggle { + color: #fff; +} +.skin-red-light .main-header .navbar .sidebar-toggle:hover { + background-color: #d73925; +} +@media (max-width: 767px) { + .skin-red-light .main-header .navbar .dropdown-menu li.divider { + background-color: rgba(255, 255, 255, 0.1); + } + .skin-red-light .main-header .navbar .dropdown-menu li a { + color: #fff; + } + .skin-red-light .main-header .navbar .dropdown-menu li a:hover { + background: #d73925; + } +} +.skin-red-light .main-header .logo { + background-color: #dd4b39; + color: #ffffff; + border-bottom: 0 solid transparent; +} +.skin-red-light .main-header .logo:hover { + background-color: #dc4735; +} +.skin-red-light .main-header li.user-header { + background-color: #dd4b39; +} +.skin-red-light .content-header { + background: transparent; +} +.skin-red-light .wrapper, +.skin-red-light .main-sidebar, +.skin-red-light .left-side { + background-color: #f9fafc; +} +.skin-red-light .content-wrapper, +.skin-red-light .main-footer { + border-left: 1px solid #d2d6de; +} +.skin-red-light .user-panel > .info, +.skin-red-light .user-panel > .info > a { + color: #444444; +} +.skin-red-light .sidebar-menu > li { + -webkit-transition: border-left-color 0.3s ease; + -o-transition: border-left-color 0.3s ease; + transition: border-left-color 0.3s ease; +} +.skin-red-light .sidebar-menu > li.header { + color: #848484; + background: #f9fafc; +} +.skin-red-light .sidebar-menu > li > a { + border-left: 3px solid transparent; + font-weight: 600; +} +.skin-red-light .sidebar-menu > li:hover > a, +.skin-red-light .sidebar-menu > li.active > a { + color: #000000; + background: #f4f4f5; +} +.skin-red-light .sidebar-menu > li.active { + border-left-color: #dd4b39; +} +.skin-red-light .sidebar-menu > li.active > a { + font-weight: 600; +} +.skin-red-light .sidebar-menu > li > .treeview-menu { + background: #f4f4f5; +} +.skin-red-light .sidebar a { + color: #444444; +} +.skin-red-light .sidebar a:hover { + text-decoration: none; +} +.skin-red-light .treeview-menu > li > a { + color: #777777; +} +.skin-red-light .treeview-menu > li.active > a, +.skin-red-light .treeview-menu > li > a:hover { + color: #000000; +} +.skin-red-light .treeview-menu > li.active > a { + font-weight: 600; +} +.skin-red-light .sidebar-form { + border-radius: 3px; + border: 1px solid #d2d6de; + margin: 10px 10px; +} +.skin-red-light .sidebar-form input[type="text"], +.skin-red-light .sidebar-form .btn { + box-shadow: none; + background-color: #fff; + border: 1px solid transparent; + height: 35px; +} +.skin-red-light .sidebar-form input[type="text"] { + color: #666; + border-top-left-radius: 2px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 2px; +} +.skin-red-light .sidebar-form input[type="text"]:focus, +.skin-red-light .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + background-color: #fff; + color: #666; +} +.skin-red-light .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + border-left-color: #fff; +} +.skin-red-light .sidebar-form .btn { + color: #999; + border-top-left-radius: 0; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-bottom-left-radius: 0; +} +@media (min-width: 768px) { + .skin-red-light.sidebar-mini.sidebar-collapse .sidebar-menu > li > .treeview-menu { + border-left: 1px solid #d2d6de; + } +} +/* + * Skin: Yellow + * ------------ + */ +.skin-yellow .main-header .navbar { + background-color: #f39c12; +} +.skin-yellow .main-header .navbar .nav > li > a { + color: #ffffff; +} +.skin-yellow .main-header .navbar .nav > li > a:hover, +.skin-yellow .main-header .navbar .nav > li > a:active, +.skin-yellow .main-header .navbar .nav > li > a:focus, +.skin-yellow .main-header .navbar .nav .open > a, +.skin-yellow .main-header .navbar .nav .open > a:hover, +.skin-yellow .main-header .navbar .nav .open > a:focus, +.skin-yellow .main-header .navbar .nav > .active > a { + background: rgba(0, 0, 0, 0.1); + color: #f6f6f6; +} +.skin-yellow .main-header .navbar .sidebar-toggle { + color: #ffffff; +} +.skin-yellow .main-header .navbar .sidebar-toggle:hover { + color: #f6f6f6; + background: rgba(0, 0, 0, 0.1); +} +.skin-yellow .main-header .navbar .sidebar-toggle { + color: #fff; +} +.skin-yellow .main-header .navbar .sidebar-toggle:hover { + background-color: #e08e0b; +} +@media (max-width: 767px) { + .skin-yellow .main-header .navbar .dropdown-menu li.divider { + background-color: rgba(255, 255, 255, 0.1); + } + .skin-yellow .main-header .navbar .dropdown-menu li a { + color: #fff; + } + .skin-yellow .main-header .navbar .dropdown-menu li a:hover { + background: #e08e0b; + } +} +.skin-yellow .main-header .logo { + background-color: #e08e0b; + color: #ffffff; + border-bottom: 0 solid transparent; +} +.skin-yellow .main-header .logo:hover { + background-color: #db8b0b; +} +.skin-yellow .main-header li.user-header { + background-color: #f39c12; +} +.skin-yellow .content-header { + background: transparent; +} +.skin-yellow .wrapper, +.skin-yellow .main-sidebar, +.skin-yellow .left-side { + background-color: #222d32; +} +.skin-yellow .user-panel > .info, +.skin-yellow .user-panel > .info > a { + color: #fff; +} +.skin-yellow .sidebar-menu > li.header { + color: #4b646f; + background: #1a2226; +} +.skin-yellow .sidebar-menu > li > a { + border-left: 3px solid transparent; +} +.skin-yellow .sidebar-menu > li:hover > a, +.skin-yellow .sidebar-menu > li.active > a { + color: #ffffff; + background: #1e282c; + border-left-color: #f39c12; +} +.skin-yellow .sidebar-menu > li > .treeview-menu { + margin: 0 1px; + background: #2c3b41; +} +.skin-yellow .sidebar a { + color: #b8c7ce; +} +.skin-yellow .sidebar a:hover { + text-decoration: none; +} +.skin-yellow .treeview-menu > li > a { + color: #8aa4af; +} +.skin-yellow .treeview-menu > li.active > a, +.skin-yellow .treeview-menu > li > a:hover { + color: #ffffff; +} +.skin-yellow .sidebar-form { + border-radius: 3px; + border: 1px solid #374850; + margin: 10px 10px; +} +.skin-yellow .sidebar-form input[type="text"], +.skin-yellow .sidebar-form .btn { + box-shadow: none; + background-color: #374850; + border: 1px solid transparent; + height: 35px; +} +.skin-yellow .sidebar-form input[type="text"] { + color: #666; + border-top-left-radius: 2px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 2px; +} +.skin-yellow .sidebar-form input[type="text"]:focus, +.skin-yellow .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + background-color: #fff; + color: #666; +} +.skin-yellow .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + border-left-color: #fff; +} +.skin-yellow .sidebar-form .btn { + color: #999; + border-top-left-radius: 0; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-bottom-left-radius: 0; +} +/* + * Skin: Yellow + * ------------ + */ +.skin-yellow-light .main-header .navbar { + background-color: #f39c12; +} +.skin-yellow-light .main-header .navbar .nav > li > a { + color: #ffffff; +} +.skin-yellow-light .main-header .navbar .nav > li > a:hover, +.skin-yellow-light .main-header .navbar .nav > li > a:active, +.skin-yellow-light .main-header .navbar .nav > li > a:focus, +.skin-yellow-light .main-header .navbar .nav .open > a, +.skin-yellow-light .main-header .navbar .nav .open > a:hover, +.skin-yellow-light .main-header .navbar .nav .open > a:focus, +.skin-yellow-light .main-header .navbar .nav > .active > a { + background: rgba(0, 0, 0, 0.1); + color: #f6f6f6; +} +.skin-yellow-light .main-header .navbar .sidebar-toggle { + color: #ffffff; +} +.skin-yellow-light .main-header .navbar .sidebar-toggle:hover { + color: #f6f6f6; + background: rgba(0, 0, 0, 0.1); +} +.skin-yellow-light .main-header .navbar .sidebar-toggle { + color: #fff; +} +.skin-yellow-light .main-header .navbar .sidebar-toggle:hover { + background-color: #e08e0b; +} +@media (max-width: 767px) { + .skin-yellow-light .main-header .navbar .dropdown-menu li.divider { + background-color: rgba(255, 255, 255, 0.1); + } + .skin-yellow-light .main-header .navbar .dropdown-menu li a { + color: #fff; + } + .skin-yellow-light .main-header .navbar .dropdown-menu li a:hover { + background: #e08e0b; + } +} +.skin-yellow-light .main-header .logo { + background-color: #f39c12; + color: #ffffff; + border-bottom: 0 solid transparent; +} +.skin-yellow-light .main-header .logo:hover { + background-color: #f39a0d; +} +.skin-yellow-light .main-header li.user-header { + background-color: #f39c12; +} +.skin-yellow-light .content-header { + background: transparent; +} +.skin-yellow-light .wrapper, +.skin-yellow-light .main-sidebar, +.skin-yellow-light .left-side { + background-color: #f9fafc; +} +.skin-yellow-light .content-wrapper, +.skin-yellow-light .main-footer { + border-left: 1px solid #d2d6de; +} +.skin-yellow-light .user-panel > .info, +.skin-yellow-light .user-panel > .info > a { + color: #444444; +} +.skin-yellow-light .sidebar-menu > li { + -webkit-transition: border-left-color 0.3s ease; + -o-transition: border-left-color 0.3s ease; + transition: border-left-color 0.3s ease; +} +.skin-yellow-light .sidebar-menu > li.header { + color: #848484; + background: #f9fafc; +} +.skin-yellow-light .sidebar-menu > li > a { + border-left: 3px solid transparent; + font-weight: 600; +} +.skin-yellow-light .sidebar-menu > li:hover > a, +.skin-yellow-light .sidebar-menu > li.active > a { + color: #000000; + background: #f4f4f5; +} +.skin-yellow-light .sidebar-menu > li.active { + border-left-color: #f39c12; +} +.skin-yellow-light .sidebar-menu > li.active > a { + font-weight: 600; +} +.skin-yellow-light .sidebar-menu > li > .treeview-menu { + background: #f4f4f5; +} +.skin-yellow-light .sidebar a { + color: #444444; +} +.skin-yellow-light .sidebar a:hover { + text-decoration: none; +} +.skin-yellow-light .treeview-menu > li > a { + color: #777777; +} +.skin-yellow-light .treeview-menu > li.active > a, +.skin-yellow-light .treeview-menu > li > a:hover { + color: #000000; +} +.skin-yellow-light .treeview-menu > li.active > a { + font-weight: 600; +} +.skin-yellow-light .sidebar-form { + border-radius: 3px; + border: 1px solid #d2d6de; + margin: 10px 10px; +} +.skin-yellow-light .sidebar-form input[type="text"], +.skin-yellow-light .sidebar-form .btn { + box-shadow: none; + background-color: #fff; + border: 1px solid transparent; + height: 35px; +} +.skin-yellow-light .sidebar-form input[type="text"] { + color: #666; + border-top-left-radius: 2px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 2px; +} +.skin-yellow-light .sidebar-form input[type="text"]:focus, +.skin-yellow-light .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + background-color: #fff; + color: #666; +} +.skin-yellow-light .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + border-left-color: #fff; +} +.skin-yellow-light .sidebar-form .btn { + color: #999; + border-top-left-radius: 0; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-bottom-left-radius: 0; +} +@media (min-width: 768px) { + .skin-yellow-light.sidebar-mini.sidebar-collapse .sidebar-menu > li > .treeview-menu { + border-left: 1px solid #d2d6de; + } +} +/* + * Skin: Purple + * ------------ + */ +.skin-purple .main-header .navbar { + background-color: #605ca8; +} +.skin-purple .main-header .navbar .nav > li > a { + color: #ffffff; +} +.skin-purple .main-header .navbar .nav > li > a:hover, +.skin-purple .main-header .navbar .nav > li > a:active, +.skin-purple .main-header .navbar .nav > li > a:focus, +.skin-purple .main-header .navbar .nav .open > a, +.skin-purple .main-header .navbar .nav .open > a:hover, +.skin-purple .main-header .navbar .nav .open > a:focus, +.skin-purple .main-header .navbar .nav > .active > a { + background: rgba(0, 0, 0, 0.1); + color: #f6f6f6; +} +.skin-purple .main-header .navbar .sidebar-toggle { + color: #ffffff; +} +.skin-purple .main-header .navbar .sidebar-toggle:hover { + color: #f6f6f6; + background: rgba(0, 0, 0, 0.1); +} +.skin-purple .main-header .navbar .sidebar-toggle { + color: #fff; +} +.skin-purple .main-header .navbar .sidebar-toggle:hover { + background-color: #555299; +} +@media (max-width: 767px) { + .skin-purple .main-header .navbar .dropdown-menu li.divider { + background-color: rgba(255, 255, 255, 0.1); + } + .skin-purple .main-header .navbar .dropdown-menu li a { + color: #fff; + } + .skin-purple .main-header .navbar .dropdown-menu li a:hover { + background: #555299; + } +} +.skin-purple .main-header .logo { + background-color: #555299; + color: #ffffff; + border-bottom: 0 solid transparent; +} +.skin-purple .main-header .logo:hover { + background-color: #545096; +} +.skin-purple .main-header li.user-header { + background-color: #605ca8; +} +.skin-purple .content-header { + background: transparent; +} +.skin-purple .wrapper, +.skin-purple .main-sidebar, +.skin-purple .left-side { + background-color: #222d32; +} +.skin-purple .user-panel > .info, +.skin-purple .user-panel > .info > a { + color: #fff; +} +.skin-purple .sidebar-menu > li.header { + color: #4b646f; + background: #1a2226; +} +.skin-purple .sidebar-menu > li > a { + border-left: 3px solid transparent; +} +.skin-purple .sidebar-menu > li:hover > a, +.skin-purple .sidebar-menu > li.active > a { + color: #ffffff; + background: #1e282c; + border-left-color: #605ca8; +} +.skin-purple .sidebar-menu > li > .treeview-menu { + margin: 0 1px; + background: #2c3b41; +} +.skin-purple .sidebar a { + color: #b8c7ce; +} +.skin-purple .sidebar a:hover { + text-decoration: none; +} +.skin-purple .treeview-menu > li > a { + color: #8aa4af; +} +.skin-purple .treeview-menu > li.active > a, +.skin-purple .treeview-menu > li > a:hover { + color: #ffffff; +} +.skin-purple .sidebar-form { + border-radius: 3px; + border: 1px solid #374850; + margin: 10px 10px; +} +.skin-purple .sidebar-form input[type="text"], +.skin-purple .sidebar-form .btn { + box-shadow: none; + background-color: #374850; + border: 1px solid transparent; + height: 35px; +} +.skin-purple .sidebar-form input[type="text"] { + color: #666; + border-top-left-radius: 2px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 2px; +} +.skin-purple .sidebar-form input[type="text"]:focus, +.skin-purple .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + background-color: #fff; + color: #666; +} +.skin-purple .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + border-left-color: #fff; +} +.skin-purple .sidebar-form .btn { + color: #999; + border-top-left-radius: 0; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-bottom-left-radius: 0; +} +/* + * Skin: Purple + * ------------ + */ +.skin-purple-light .main-header .navbar { + background-color: #605ca8; +} +.skin-purple-light .main-header .navbar .nav > li > a { + color: #ffffff; +} +.skin-purple-light .main-header .navbar .nav > li > a:hover, +.skin-purple-light .main-header .navbar .nav > li > a:active, +.skin-purple-light .main-header .navbar .nav > li > a:focus, +.skin-purple-light .main-header .navbar .nav .open > a, +.skin-purple-light .main-header .navbar .nav .open > a:hover, +.skin-purple-light .main-header .navbar .nav .open > a:focus, +.skin-purple-light .main-header .navbar .nav > .active > a { + background: rgba(0, 0, 0, 0.1); + color: #f6f6f6; +} +.skin-purple-light .main-header .navbar .sidebar-toggle { + color: #ffffff; +} +.skin-purple-light .main-header .navbar .sidebar-toggle:hover { + color: #f6f6f6; + background: rgba(0, 0, 0, 0.1); +} +.skin-purple-light .main-header .navbar .sidebar-toggle { + color: #fff; +} +.skin-purple-light .main-header .navbar .sidebar-toggle:hover { + background-color: #555299; +} +@media (max-width: 767px) { + .skin-purple-light .main-header .navbar .dropdown-menu li.divider { + background-color: rgba(255, 255, 255, 0.1); + } + .skin-purple-light .main-header .navbar .dropdown-menu li a { + color: #fff; + } + .skin-purple-light .main-header .navbar .dropdown-menu li a:hover { + background: #555299; + } +} +.skin-purple-light .main-header .logo { + background-color: #605ca8; + color: #ffffff; + border-bottom: 0 solid transparent; +} +.skin-purple-light .main-header .logo:hover { + background-color: #5d59a6; +} +.skin-purple-light .main-header li.user-header { + background-color: #605ca8; +} +.skin-purple-light .content-header { + background: transparent; +} +.skin-purple-light .wrapper, +.skin-purple-light .main-sidebar, +.skin-purple-light .left-side { + background-color: #f9fafc; +} +.skin-purple-light .content-wrapper, +.skin-purple-light .main-footer { + border-left: 1px solid #d2d6de; +} +.skin-purple-light .user-panel > .info, +.skin-purple-light .user-panel > .info > a { + color: #444444; +} +.skin-purple-light .sidebar-menu > li { + -webkit-transition: border-left-color 0.3s ease; + -o-transition: border-left-color 0.3s ease; + transition: border-left-color 0.3s ease; +} +.skin-purple-light .sidebar-menu > li.header { + color: #848484; + background: #f9fafc; +} +.skin-purple-light .sidebar-menu > li > a { + border-left: 3px solid transparent; + font-weight: 600; +} +.skin-purple-light .sidebar-menu > li:hover > a, +.skin-purple-light .sidebar-menu > li.active > a { + color: #000000; + background: #f4f4f5; +} +.skin-purple-light .sidebar-menu > li.active { + border-left-color: #605ca8; +} +.skin-purple-light .sidebar-menu > li.active > a { + font-weight: 600; +} +.skin-purple-light .sidebar-menu > li > .treeview-menu { + background: #f4f4f5; +} +.skin-purple-light .sidebar a { + color: #444444; +} +.skin-purple-light .sidebar a:hover { + text-decoration: none; +} +.skin-purple-light .treeview-menu > li > a { + color: #777777; +} +.skin-purple-light .treeview-menu > li.active > a, +.skin-purple-light .treeview-menu > li > a:hover { + color: #000000; +} +.skin-purple-light .treeview-menu > li.active > a { + font-weight: 600; +} +.skin-purple-light .sidebar-form { + border-radius: 3px; + border: 1px solid #d2d6de; + margin: 10px 10px; +} +.skin-purple-light .sidebar-form input[type="text"], +.skin-purple-light .sidebar-form .btn { + box-shadow: none; + background-color: #fff; + border: 1px solid transparent; + height: 35px; +} +.skin-purple-light .sidebar-form input[type="text"] { + color: #666; + border-top-left-radius: 2px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 2px; +} +.skin-purple-light .sidebar-form input[type="text"]:focus, +.skin-purple-light .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + background-color: #fff; + color: #666; +} +.skin-purple-light .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + border-left-color: #fff; +} +.skin-purple-light .sidebar-form .btn { + color: #999; + border-top-left-radius: 0; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-bottom-left-radius: 0; +} +@media (min-width: 768px) { + .skin-purple-light.sidebar-mini.sidebar-collapse .sidebar-menu > li > .treeview-menu { + border-left: 1px solid #d2d6de; + } +} diff --git a/login/app/sprinkles/core/assets/userfrosting/css/AdminLTE.css b/login/app/sprinkles/core/assets/userfrosting/css/AdminLTE.css new file mode 100755 index 0000000..103689d --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/css/AdminLTE.css @@ -0,0 +1,4932 @@ +@import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic); +/*! + * AdminLTE v2.3.6 + * Author: Almsaeed Studio + * Website: Almsaeed Studio <http://almsaeedstudio.com> + * License: Open source - MIT + * Please visit http://opensource.org/licenses/MIT for more information +!*/ +/* + * Core: General Layout Style + * ------------------------- + */ +html, +body { + min-height: 100%; +} +.layout-boxed html, +.layout-boxed body { + height: 100%; +} +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-family: 'Source Sans Pro', 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-weight: 400; + overflow-x: hidden; + overflow-y: auto; +} +/* Layout */ +.wrapper { + min-height: 100%; + position: relative; + overflow: hidden; +} +.wrapper:before, +.wrapper:after { + content: " "; + display: table; +} +.wrapper:after { + clear: both; +} +.layout-boxed .wrapper { + max-width: 1250px; + margin: 0 auto; + min-height: 100%; + box-shadow: 0 0 8px rgba(0, 0, 0, 0.5); + position: relative; +} +.layout-boxed { + background: url('../img/boxed-bg.jpg') repeat fixed; +} +/* + * Content Wrapper - contains the main content + * ```.right-side has been deprecated as of v2.0.0 in favor of .content-wrapper ``` + */ +.content-wrapper, +.right-side, +.main-footer { + -webkit-transition: -webkit-transform 0.3s ease-in-out, margin 0.3s ease-in-out; + -moz-transition: -moz-transform 0.3s ease-in-out, margin 0.3s ease-in-out; + -o-transition: -o-transform 0.3s ease-in-out, margin 0.3s ease-in-out; + transition: transform 0.3s ease-in-out, margin 0.3s ease-in-out; + margin-left: 230px; + z-index: 820; +} +.layout-top-nav .content-wrapper, +.layout-top-nav .right-side, +.layout-top-nav .main-footer { + margin-left: 0; +} +@media (max-width: 767px) { + .content-wrapper, + .right-side, + .main-footer { + margin-left: 0; + } +} +@media (min-width: 768px) { + .sidebar-collapse .content-wrapper, + .sidebar-collapse .right-side, + .sidebar-collapse .main-footer { + margin-left: 0; + } +} +@media (max-width: 767px) { + .sidebar-open .content-wrapper, + .sidebar-open .right-side, + .sidebar-open .main-footer { + -webkit-transform: translate(230px, 0); + -ms-transform: translate(230px, 0); + -o-transform: translate(230px, 0); + transform: translate(230px, 0); + } +} +.content-wrapper, +.right-side { + min-height: 100%; + background-color: #ecf0f5; + z-index: 800; +} +.main-footer { + background: #fff; + padding: 15px; + color: #444; + border-top: 1px solid #d2d6de; +} +/* Fixed layout */ +.fixed .main-header, +.fixed .main-sidebar, +.fixed .left-side { + position: fixed; +} +.fixed .main-header { + top: 0; + right: 0; + left: 0; +} +.fixed .content-wrapper, +.fixed .right-side { + padding-top: 50px; +} +@media (max-width: 767px) { + .fixed .content-wrapper, + .fixed .right-side { + padding-top: 100px; + } +} +.fixed.layout-boxed .wrapper { + max-width: 100%; +} +body.hold-transition .content-wrapper, +body.hold-transition .right-side, +body.hold-transition .main-footer, +body.hold-transition .main-sidebar, +body.hold-transition .left-side, +body.hold-transition .main-header .navbar, +body.hold-transition .main-header .logo { + /* Fix for IE */ + -webkit-transition: none; + -o-transition: none; + transition: none; +} +/* Content */ +.content { + min-height: 250px; + padding: 15px; + margin-right: auto; + margin-left: auto; + padding-left: 15px; + padding-right: 15px; +} +/* H1 - H6 font */ +h1, +h2, +h3, +h4, +h5, +h6, +.h1, +.h2, +.h3, +.h4, +.h5, +.h6 { + font-family: 'Source Sans Pro', sans-serif; +} +/* General Links */ +a { + color: #3c8dbc; +} +a:hover, +a:active, +a:focus { + outline: none; + text-decoration: none; + color: #72afd2; +} +/* Page Header */ +.page-header { + margin: 10px 0 20px 0; + font-size: 22px; +} +.page-header > small { + color: #666; + display: block; + margin-top: 5px; +} +/* + * Component: Main Header + * ---------------------- + */ +.main-header { + position: relative; + max-height: 100px; + z-index: 1030; +} +.main-header .navbar { + -webkit-transition: margin-left 0.3s ease-in-out; + -o-transition: margin-left 0.3s ease-in-out; + transition: margin-left 0.3s ease-in-out; + margin-bottom: 0; + margin-left: 230px; + border: none; + min-height: 50px; + border-radius: 0; +} +.layout-top-nav .main-header .navbar { + margin-left: 0; +} +.main-header #navbar-search-input.form-control { + background: rgba(255, 255, 255, 0.2); + border-color: transparent; +} +.main-header #navbar-search-input.form-control:focus, +.main-header #navbar-search-input.form-control:active { + border-color: rgba(0, 0, 0, 0.1); + background: rgba(255, 255, 255, 0.9); +} +.main-header #navbar-search-input.form-control::-moz-placeholder { + color: #ccc; + opacity: 1; +} +.main-header #navbar-search-input.form-control:-ms-input-placeholder { + color: #ccc; +} +.main-header #navbar-search-input.form-control::-webkit-input-placeholder { + color: #ccc; +} +.main-header .navbar-custom-menu, +.main-header .navbar-right { + float: right; +} +@media (max-width: 991px) { + .main-header .navbar-custom-menu a, + .main-header .navbar-right a { + color: inherit; + background: transparent; + } +} +@media (max-width: 767px) { + .main-header .navbar-right { + float: none; + } + .navbar-collapse .main-header .navbar-right { + margin: 7.5px -15px; + } + .main-header .navbar-right > li { + color: inherit; + border: 0; + } +} +.main-header .sidebar-toggle { + float: left; + background-color: transparent; + background-image: none; + padding: 15px 15px; + font-family: fontAwesome; +} +.main-header .sidebar-toggle:before { + content: "\f0c9"; +} +.main-header .sidebar-toggle:hover { + color: #fff; +} +.main-header .sidebar-toggle:focus, +.main-header .sidebar-toggle:active { + background: transparent; +} +.main-header .sidebar-toggle .icon-bar { + display: none; +} +.main-header .navbar .nav > li.user > a > .fa, +.main-header .navbar .nav > li.user > a > .glyphicon, +.main-header .navbar .nav > li.user > a > .ion { + margin-right: 5px; +} +.main-header .navbar .nav > li > a > .label { + position: absolute; + top: 9px; + right: 7px; + text-align: center; + font-size: 9px; + padding: 2px 3px; + line-height: .9; +} +.main-header .logo { + -webkit-transition: width 0.3s ease-in-out; + -o-transition: width 0.3s ease-in-out; + transition: width 0.3s ease-in-out; + display: block; + float: left; + height: 50px; + font-size: 20px; + line-height: 50px; + text-align: center; + width: 230px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + padding: 0 15px; + font-weight: 300; + overflow: hidden; +} +.main-header .logo .logo-lg { + display: block; +} +.main-header .logo .logo-mini { + display: none; +} +.main-header .navbar-brand { + color: #fff; +} +.content-header { + position: relative; + padding: 15px 15px 0 15px; +} +.content-header > h1 { + margin: 0; + font-size: 24px; +} +.content-header > h1 > small { + font-size: 15px; + display: inline-block; + padding-left: 4px; + font-weight: 300; +} +.content-header > .breadcrumb { + float: right; + background: transparent; + margin-top: 0; + margin-bottom: 0; + font-size: 12px; + padding: 7px 5px; + position: absolute; + top: 15px; + right: 10px; + border-radius: 2px; +} +.content-header > .breadcrumb > li > a { + color: #444; + text-decoration: none; + display: inline-block; +} +.content-header > .breadcrumb > li > a > .fa, +.content-header > .breadcrumb > li > a > .glyphicon, +.content-header > .breadcrumb > li > a > .ion { + margin-right: 5px; +} +.content-header > .breadcrumb > li + li:before { + content: '>\00a0'; +} +@media (max-width: 991px) { + .content-header > .breadcrumb { + position: relative; + margin-top: 5px; + top: 0; + right: 0; + float: none; + background: #d2d6de; + padding-left: 10px; + } + .content-header > .breadcrumb li:before { + color: #97a0b3; + } +} +.navbar-toggle { + color: #fff; + border: 0; + margin: 0; + padding: 15px 15px; +} +@media (max-width: 991px) { + .navbar-custom-menu .navbar-nav > li { + float: left; + } + .navbar-custom-menu .navbar-nav { + margin: 0; + float: left; + } + .navbar-custom-menu .navbar-nav > li > a { + padding-top: 15px; + padding-bottom: 15px; + line-height: 20px; + } +} +@media (max-width: 767px) { + .main-header { + position: relative; + } + .main-header .logo, + .main-header .navbar { + width: 100%; + float: none; + } + .main-header .navbar { + margin: 0; + } + .main-header .navbar-custom-menu { + float: right; + } +} +@media (max-width: 991px) { + .navbar-collapse.pull-left { + float: none !important; + } + .navbar-collapse.pull-left + .navbar-custom-menu { + display: block; + position: absolute; + top: 0; + right: 40px; + } +} +/* + * Component: Sidebar + * ------------------ + */ +.main-sidebar, +.left-side { + position: absolute; + top: 0; + left: 0; + padding-top: 50px; + min-height: 100%; + width: 230px; + z-index: 810; + -webkit-transition: -webkit-transform 0.3s ease-in-out, width 0.3s ease-in-out; + -moz-transition: -moz-transform 0.3s ease-in-out, width 0.3s ease-in-out; + -o-transition: -o-transform 0.3s ease-in-out, width 0.3s ease-in-out; + transition: transform 0.3s ease-in-out, width 0.3s ease-in-out; +} +@media (max-width: 767px) { + .main-sidebar, + .left-side { + padding-top: 100px; + } +} +@media (max-width: 767px) { + .main-sidebar, + .left-side { + -webkit-transform: translate(-230px, 0); + -ms-transform: translate(-230px, 0); + -o-transform: translate(-230px, 0); + transform: translate(-230px, 0); + } +} +@media (min-width: 768px) { + .sidebar-collapse .main-sidebar, + .sidebar-collapse .left-side { + -webkit-transform: translate(-230px, 0); + -ms-transform: translate(-230px, 0); + -o-transform: translate(-230px, 0); + transform: translate(-230px, 0); + } +} +@media (max-width: 767px) { + .sidebar-open .main-sidebar, + .sidebar-open .left-side { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + -o-transform: translate(0, 0); + transform: translate(0, 0); + } +} +.sidebar { + padding-bottom: 10px; +} +.sidebar-form input:focus { + border-color: transparent; +} +.user-panel { + position: relative; + width: 100%; + padding: 10px; + overflow: hidden; +} +.user-panel:before, +.user-panel:after { + content: " "; + display: table; +} +.user-panel:after { + clear: both; +} +.user-panel > .image > img { + width: 100%; + max-width: 45px; + height: auto; +} +.user-panel > .info { + padding: 5px 5px 5px 15px; + line-height: 1; + position: absolute; + left: 55px; +} +.user-panel > .info > p { + font-weight: 600; + margin-bottom: 9px; +} +.user-panel > .info > a { + text-decoration: none; + padding-right: 5px; + margin-top: 3px; + font-size: 11px; +} +.user-panel > .info > a > .fa, +.user-panel > .info > a > .ion, +.user-panel > .info > a > .glyphicon { + margin-right: 3px; +} +.sidebar-menu { + list-style: none; + margin: 0; + padding: 0; +} +.sidebar-menu > li { + position: relative; + margin: 0; + padding: 0; +} +.sidebar-menu > li > a { + padding: 12px 5px 12px 15px; + display: block; +} +.sidebar-menu > li > a > .fa, +.sidebar-menu > li > a > .glyphicon, +.sidebar-menu > li > a > .ion { + width: 20px; +} +.sidebar-menu > li .label, +.sidebar-menu > li .badge { + margin-right: 5px; +} +.sidebar-menu > li .badge { + margin-top: 3px; +} +.sidebar-menu li.header { + padding: 10px 25px 10px 15px; + font-size: 12px; +} +.sidebar-menu li > a > .fa-angle-left, +.sidebar-menu li > a > .pull-right-container > .fa-angle-left { + width: auto; + height: auto; + padding: 0; + margin-right: 10px; +} +.sidebar-menu li.active > a > .fa-angle-left, +.sidebar-menu li.active > a > .pull-right-container > .fa-angle-left { + -webkit-transform: rotate(-90deg); + -ms-transform: rotate(-90deg); + -o-transform: rotate(-90deg); + transform: rotate(-90deg); +} +.sidebar-menu li.active > .treeview-menu { + display: block; +} +.sidebar-menu .treeview-menu { + display: none; + list-style: none; + padding: 0; + margin: 0; + padding-left: 5px; +} +.sidebar-menu .treeview-menu .treeview-menu { + padding-left: 20px; +} +.sidebar-menu .treeview-menu > li { + margin: 0; +} +.sidebar-menu .treeview-menu > li > a { + padding: 5px 5px 5px 15px; + display: block; + font-size: 14px; +} +.sidebar-menu .treeview-menu > li > a > .fa, +.sidebar-menu .treeview-menu > li > a > .glyphicon, +.sidebar-menu .treeview-menu > li > a > .ion { + width: 20px; +} +.sidebar-menu .treeview-menu > li > a > .pull-right-container > .fa-angle-left, +.sidebar-menu .treeview-menu > li > a > .pull-right-container > .fa-angle-down, +.sidebar-menu .treeview-menu > li > a > .fa-angle-left, +.sidebar-menu .treeview-menu > li > a > .fa-angle-down { + width: auto; +} +/* + * Component: Sidebar Mini + */ +@media (min-width: 768px) { + .sidebar-mini.sidebar-collapse .content-wrapper, + .sidebar-mini.sidebar-collapse .right-side, + .sidebar-mini.sidebar-collapse .main-footer { + margin-left: 50px !important; + z-index: 840; + } + .sidebar-mini.sidebar-collapse .main-sidebar { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + -o-transform: translate(0, 0); + transform: translate(0, 0); + width: 50px !important; + z-index: 850; + } + .sidebar-mini.sidebar-collapse .sidebar-menu > li { + position: relative; + } + .sidebar-mini.sidebar-collapse .sidebar-menu > li > a { + margin-right: 0; + } + .sidebar-mini.sidebar-collapse .sidebar-menu > li > a > span { + border-top-right-radius: 4px; + } + .sidebar-mini.sidebar-collapse .sidebar-menu > li:not(.treeview) > a > span { + border-bottom-right-radius: 4px; + } + .sidebar-mini.sidebar-collapse .sidebar-menu > li > .treeview-menu { + padding-top: 5px; + padding-bottom: 5px; + border-bottom-right-radius: 4px; + } + .sidebar-mini.sidebar-collapse .sidebar-menu > li:hover > a > span:not(.pull-right), + .sidebar-mini.sidebar-collapse .sidebar-menu > li:hover > .treeview-menu { + display: block !important; + position: absolute; + width: 180px; + left: 50px; + } + .sidebar-mini.sidebar-collapse .sidebar-menu > li:hover > a > span { + top: 0; + margin-left: -3px; + padding: 12px 5px 12px 20px; + background-color: inherit; + } + .sidebar-mini.sidebar-collapse .sidebar-menu > li:hover > a > .pull-right-container { + float: right; + width: auto!important; + left: 200px!important; + top: 10px!important; + } + .sidebar-mini.sidebar-collapse .sidebar-menu > li:hover > a > .pull-right-container > .label:not(:first-of-type) { + display: none; + } + .sidebar-mini.sidebar-collapse .sidebar-menu > li:hover > .treeview-menu { + top: 44px; + margin-left: 0; + } + .sidebar-mini.sidebar-collapse .main-sidebar .user-panel > .info, + .sidebar-mini.sidebar-collapse .sidebar-form, + .sidebar-mini.sidebar-collapse .sidebar-menu > li > a > span, + .sidebar-mini.sidebar-collapse .sidebar-menu > li > .treeview-menu, + .sidebar-mini.sidebar-collapse .sidebar-menu > li > a > .pull-right, + .sidebar-mini.sidebar-collapse .sidebar-menu li.header { + display: none !important; + -webkit-transform: translateZ(0); + } + .sidebar-mini.sidebar-collapse .main-header .logo { + width: 50px; + } + .sidebar-mini.sidebar-collapse .main-header .logo > .logo-mini { + display: block; + margin-left: -15px; + margin-right: -15px; + font-size: 18px; + } + .sidebar-mini.sidebar-collapse .main-header .logo > .logo-lg { + display: none; + } + .sidebar-mini.sidebar-collapse .main-header .navbar { + margin-left: 50px; + } +} +.sidebar-menu, +.main-sidebar .user-panel, +.sidebar-menu > li.header { + white-space: nowrap; + overflow: hidden; +} +.sidebar-menu:hover { + overflow: visible; +} +.sidebar-form, +.sidebar-menu > li.header { + overflow: hidden; + text-overflow: clip; +} +.sidebar-menu li > a { + position: relative; +} +.sidebar-menu li > a > .pull-right-container { + position: absolute; + right: 10px; + top: 50%; + margin-top: -7px; +} +/* + * Component: Control sidebar. By default, this is the right sidebar. + */ +.control-sidebar-bg { + position: fixed; + z-index: 1000; + bottom: 0; +} +.control-sidebar-bg, +.control-sidebar { + top: 0; + right: -230px; + width: 230px; + -webkit-transition: right 0.3s ease-in-out; + -o-transition: right 0.3s ease-in-out; + transition: right 0.3s ease-in-out; +} +.control-sidebar { + position: absolute; + padding-top: 50px; + z-index: 1010; +} +@media (max-width: 768px) { + .control-sidebar { + padding-top: 100px; + } +} +.control-sidebar > .tab-content { + padding: 10px 15px; +} +.control-sidebar.control-sidebar-open, +.control-sidebar.control-sidebar-open + .control-sidebar-bg { + right: 0; +} +.control-sidebar-open .control-sidebar-bg, +.control-sidebar-open .control-sidebar { + right: 0; +} +@media (min-width: 768px) { + .control-sidebar-open .content-wrapper, + .control-sidebar-open .right-side, + .control-sidebar-open .main-footer { + margin-right: 230px; + } +} +.nav-tabs.control-sidebar-tabs > li:first-of-type > a, +.nav-tabs.control-sidebar-tabs > li:first-of-type > a:hover, +.nav-tabs.control-sidebar-tabs > li:first-of-type > a:focus { + border-left-width: 0; +} +.nav-tabs.control-sidebar-tabs > li > a { + border-radius: 0; +} +.nav-tabs.control-sidebar-tabs > li > a, +.nav-tabs.control-sidebar-tabs > li > a:hover { + border-top: none; + border-right: none; + border-left: 1px solid transparent; + border-bottom: 1px solid transparent; +} +.nav-tabs.control-sidebar-tabs > li > a .icon { + font-size: 16px; +} +.nav-tabs.control-sidebar-tabs > li.active > a, +.nav-tabs.control-sidebar-tabs > li.active > a:hover, +.nav-tabs.control-sidebar-tabs > li.active > a:focus, +.nav-tabs.control-sidebar-tabs > li.active > a:active { + border-top: none; + border-right: none; + border-bottom: none; +} +@media (max-width: 768px) { + .nav-tabs.control-sidebar-tabs { + display: table; + } + .nav-tabs.control-sidebar-tabs > li { + display: table-cell; + } +} +.control-sidebar-heading { + font-weight: 400; + font-size: 16px; + padding: 10px 0; + margin-bottom: 10px; +} +.control-sidebar-subheading { + display: block; + font-weight: 400; + font-size: 14px; +} +.control-sidebar-menu { + list-style: none; + padding: 0; + margin: 0 -15px; +} +.control-sidebar-menu > li > a { + display: block; + padding: 10px 15px; +} +.control-sidebar-menu > li > a:before, +.control-sidebar-menu > li > a:after { + content: " "; + display: table; +} +.control-sidebar-menu > li > a:after { + clear: both; +} +.control-sidebar-menu > li > a > .control-sidebar-subheading { + margin-top: 0; +} +.control-sidebar-menu .menu-icon { + float: left; + width: 35px; + height: 35px; + border-radius: 50%; + text-align: center; + line-height: 35px; +} +.control-sidebar-menu .menu-info { + margin-left: 45px; + margin-top: 3px; +} +.control-sidebar-menu .menu-info > .control-sidebar-subheading { + margin: 0; +} +.control-sidebar-menu .menu-info > p { + margin: 0; + font-size: 11px; +} +.control-sidebar-menu .progress { + margin: 0; +} +.control-sidebar-dark { + color: #b8c7ce; +} +.control-sidebar-dark, +.control-sidebar-dark + .control-sidebar-bg { + background: #222d32; +} +.control-sidebar-dark .nav-tabs.control-sidebar-tabs { + border-bottom: #1c2529; +} +.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li > a { + background: #181f23; + color: #b8c7ce; +} +.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li > a, +.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li > a:hover, +.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li > a:focus { + border-left-color: #141a1d; + border-bottom-color: #141a1d; +} +.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li > a:hover, +.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li > a:focus, +.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li > a:active { + background: #1c2529; +} +.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li > a:hover { + color: #fff; +} +.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li.active > a, +.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li.active > a:hover, +.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li.active > a:focus, +.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li.active > a:active { + background: #222d32; + color: #fff; +} +.control-sidebar-dark .control-sidebar-heading, +.control-sidebar-dark .control-sidebar-subheading { + color: #fff; +} +.control-sidebar-dark .control-sidebar-menu > li > a:hover { + background: #1e282c; +} +.control-sidebar-dark .control-sidebar-menu > li > a .menu-info > p { + color: #b8c7ce; +} +.control-sidebar-light { + color: #5e5e5e; +} +.control-sidebar-light, +.control-sidebar-light + .control-sidebar-bg { + background: #f9fafc; + border-left: 1px solid #d2d6de; +} +.control-sidebar-light .nav-tabs.control-sidebar-tabs { + border-bottom: #d2d6de; +} +.control-sidebar-light .nav-tabs.control-sidebar-tabs > li > a { + background: #e8ecf4; + color: #444444; +} +.control-sidebar-light .nav-tabs.control-sidebar-tabs > li > a, +.control-sidebar-light .nav-tabs.control-sidebar-tabs > li > a:hover, +.control-sidebar-light .nav-tabs.control-sidebar-tabs > li > a:focus { + border-left-color: #d2d6de; + border-bottom-color: #d2d6de; +} +.control-sidebar-light .nav-tabs.control-sidebar-tabs > li > a:hover, +.control-sidebar-light .nav-tabs.control-sidebar-tabs > li > a:focus, +.control-sidebar-light .nav-tabs.control-sidebar-tabs > li > a:active { + background: #eff1f7; +} +.control-sidebar-light .nav-tabs.control-sidebar-tabs > li.active > a, +.control-sidebar-light .nav-tabs.control-sidebar-tabs > li.active > a:hover, +.control-sidebar-light .nav-tabs.control-sidebar-tabs > li.active > a:focus, +.control-sidebar-light .nav-tabs.control-sidebar-tabs > li.active > a:active { + background: #f9fafc; + color: #111; +} +.control-sidebar-light .control-sidebar-heading, +.control-sidebar-light .control-sidebar-subheading { + color: #111; +} +.control-sidebar-light .control-sidebar-menu { + margin-left: -14px; +} +.control-sidebar-light .control-sidebar-menu > li > a:hover { + background: #f4f4f5; +} +.control-sidebar-light .control-sidebar-menu > li > a .menu-info > p { + color: #5e5e5e; +} +/* + * Component: Dropdown menus + * ------------------------- + */ +/*Dropdowns in general*/ +.dropdown-menu { + box-shadow: none; + border-color: #eee; +} +.dropdown-menu > li > a { + color: #777; +} +.dropdown-menu > li > a > .glyphicon, +.dropdown-menu > li > a > .fa, +.dropdown-menu > li > a > .ion { + margin-right: 10px; +} +.dropdown-menu > li > a:hover { + background-color: #e1e3e9; + color: #333; +} +.dropdown-menu > .divider { + background-color: #eee; +} +.navbar-nav > .notifications-menu > .dropdown-menu, +.navbar-nav > .messages-menu > .dropdown-menu, +.navbar-nav > .tasks-menu > .dropdown-menu { + width: 280px; + padding: 0 0 0 0; + margin: 0; + top: 100%; +} +.navbar-nav > .notifications-menu > .dropdown-menu > li, +.navbar-nav > .messages-menu > .dropdown-menu > li, +.navbar-nav > .tasks-menu > .dropdown-menu > li { + position: relative; +} +.navbar-nav > .notifications-menu > .dropdown-menu > li.header, +.navbar-nav > .messages-menu > .dropdown-menu > li.header, +.navbar-nav > .tasks-menu > .dropdown-menu > li.header { + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + background-color: #ffffff; + padding: 7px 10px; + border-bottom: 1px solid #f4f4f4; + color: #444444; + font-size: 14px; +} +.navbar-nav > .notifications-menu > .dropdown-menu > li.footer > a, +.navbar-nav > .messages-menu > .dropdown-menu > li.footer > a, +.navbar-nav > .tasks-menu > .dropdown-menu > li.footer > a { + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; + font-size: 12px; + background-color: #fff; + padding: 7px 10px; + border-bottom: 1px solid #eeeeee; + color: #444 !important; + text-align: center; +} +@media (max-width: 991px) { + .navbar-nav > .notifications-menu > .dropdown-menu > li.footer > a, + .navbar-nav > .messages-menu > .dropdown-menu > li.footer > a, + .navbar-nav > .tasks-menu > .dropdown-menu > li.footer > a { + background: #fff !important; + color: #444 !important; + } +} +.navbar-nav > .notifications-menu > .dropdown-menu > li.footer > a:hover, +.navbar-nav > .messages-menu > .dropdown-menu > li.footer > a:hover, +.navbar-nav > .tasks-menu > .dropdown-menu > li.footer > a:hover { + text-decoration: none; + font-weight: normal; +} +.navbar-nav > .notifications-menu > .dropdown-menu > li .menu, +.navbar-nav > .messages-menu > .dropdown-menu > li .menu, +.navbar-nav > .tasks-menu > .dropdown-menu > li .menu { + max-height: 200px; + margin: 0; + padding: 0; + list-style: none; + overflow-x: hidden; +} +.navbar-nav > .notifications-menu > .dropdown-menu > li .menu > li > a, +.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a, +.navbar-nav > .tasks-menu > .dropdown-menu > li .menu > li > a { + display: block; + white-space: nowrap; + /* Prevent text from breaking */ + border-bottom: 1px solid #f4f4f4; +} +.navbar-nav > .notifications-menu > .dropdown-menu > li .menu > li > a:hover, +.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a:hover, +.navbar-nav > .tasks-menu > .dropdown-menu > li .menu > li > a:hover { + background: #f4f4f4; + text-decoration: none; +} +.navbar-nav > .notifications-menu > .dropdown-menu > li .menu > li > a { + color: #444444; + overflow: hidden; + text-overflow: ellipsis; + padding: 10px; +} +.navbar-nav > .notifications-menu > .dropdown-menu > li .menu > li > a > .glyphicon, +.navbar-nav > .notifications-menu > .dropdown-menu > li .menu > li > a > .fa, +.navbar-nav > .notifications-menu > .dropdown-menu > li .menu > li > a > .ion { + width: 20px; +} +.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a { + margin: 0; + padding: 10px 10px; +} +.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a > div > img { + margin: auto 10px auto auto; + width: 40px; + height: 40px; +} +.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a > h4 { + padding: 0; + margin: 0 0 0 45px; + color: #444444; + font-size: 15px; + position: relative; +} +.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a > h4 > small { + color: #999999; + font-size: 10px; + position: absolute; + top: 0; + right: 0; +} +.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a > p { + margin: 0 0 0 45px; + font-size: 12px; + color: #888888; +} +.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a:before, +.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a:after { + content: " "; + display: table; +} +.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a:after { + clear: both; +} +.navbar-nav > .tasks-menu > .dropdown-menu > li .menu > li > a { + padding: 10px; +} +.navbar-nav > .tasks-menu > .dropdown-menu > li .menu > li > a > h3 { + font-size: 14px; + padding: 0; + margin: 0 0 10px 0; + color: #666666; +} +.navbar-nav > .tasks-menu > .dropdown-menu > li .menu > li > a > .progress { + padding: 0; + margin: 0; +} +.navbar-nav > .user-menu > .dropdown-menu { + border-top-right-radius: 0; + border-top-left-radius: 0; + padding: 1px 0 0 0; + border-top-width: 0; + width: 280px; +} +.navbar-nav > .user-menu > .dropdown-menu, +.navbar-nav > .user-menu > .dropdown-menu > .user-body { + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; +} +.navbar-nav > .user-menu > .dropdown-menu > li.user-header { + height: 175px; + padding: 10px; + text-align: center; +} +.navbar-nav > .user-menu > .dropdown-menu > li.user-header > img { + z-index: 5; + height: 90px; + width: 90px; + border: 3px solid; + border-color: transparent; + border-color: rgba(255, 255, 255, 0.2); +} +.navbar-nav > .user-menu > .dropdown-menu > li.user-header > p { + z-index: 5; + color: #fff; + color: rgba(255, 255, 255, 0.8); + font-size: 17px; + margin-top: 10px; +} +.navbar-nav > .user-menu > .dropdown-menu > li.user-header > p > small { + display: block; + font-size: 12px; +} +.navbar-nav > .user-menu > .dropdown-menu > .user-body { + padding: 15px; + border-bottom: 1px solid #f4f4f4; + border-top: 1px solid #dddddd; +} +.navbar-nav > .user-menu > .dropdown-menu > .user-body:before, +.navbar-nav > .user-menu > .dropdown-menu > .user-body:after { + content: " "; + display: table; +} +.navbar-nav > .user-menu > .dropdown-menu > .user-body:after { + clear: both; +} +.navbar-nav > .user-menu > .dropdown-menu > .user-body a { + color: #444 !important; +} +@media (max-width: 991px) { + .navbar-nav > .user-menu > .dropdown-menu > .user-body a { + background: #fff !important; + color: #444 !important; + } +} +.navbar-nav > .user-menu > .dropdown-menu > .user-footer { + background-color: #f9f9f9; + padding: 10px; +} +.navbar-nav > .user-menu > .dropdown-menu > .user-footer:before, +.navbar-nav > .user-menu > .dropdown-menu > .user-footer:after { + content: " "; + display: table; +} +.navbar-nav > .user-menu > .dropdown-menu > .user-footer:after { + clear: both; +} +.navbar-nav > .user-menu > .dropdown-menu > .user-footer .btn-default { + color: #666666; +} +@media (max-width: 991px) { + .navbar-nav > .user-menu > .dropdown-menu > .user-footer .btn-default:hover { + background-color: #f9f9f9; + } +} +.navbar-nav > .user-menu .user-image { + float: left; + width: 25px; + height: 25px; + border-radius: 50%; + margin-right: 10px; + margin-top: -2px; +} +@media (max-width: 767px) { + .navbar-nav > .user-menu .user-image { + float: none; + margin-right: 0; + margin-top: -8px; + line-height: 10px; + } +} +/* Add fade animation to dropdown menus by appending + the class .animated-dropdown-menu to the .dropdown-menu ul (or ol)*/ +.open:not(.dropup) > .animated-dropdown-menu { + backface-visibility: visible !important; + -webkit-animation: flipInX 0.7s both; + -o-animation: flipInX 0.7s both; + animation: flipInX 0.7s both; +} +@keyframes flipInX { + 0% { + transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + transition-timing-function: ease-in; + opacity: 0; + } + 40% { + transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + transition-timing-function: ease-in; + } + 60% { + transform: perspective(400px) rotate3d(1, 0, 0, 10deg); + opacity: 1; + } + 80% { + transform: perspective(400px) rotate3d(1, 0, 0, -5deg); + } + 100% { + transform: perspective(400px); + } +} +@-webkit-keyframes flipInX { + 0% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + -webkit-transition-timing-function: ease-in; + opacity: 0; + } + 40% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + -webkit-transition-timing-function: ease-in; + } + 60% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 10deg); + opacity: 1; + } + 80% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -5deg); + } + 100% { + -webkit-transform: perspective(400px); + } +} +/* Fix dropdown menu in navbars */ +.navbar-custom-menu > .navbar-nav > li { + position: relative; +} +.navbar-custom-menu > .navbar-nav > li > .dropdown-menu { + position: absolute; + right: 0; + left: auto; +} +@media (max-width: 991px) { + .navbar-custom-menu > .navbar-nav { + float: right; + } + .navbar-custom-menu > .navbar-nav > li { + position: static; + } + .navbar-custom-menu > .navbar-nav > li > .dropdown-menu { + position: absolute; + right: 5%; + left: auto; + border: 1px solid #ddd; + background: #fff; + } +} +/* + * Component: Form + * --------------- + */ +.form-control { + border-radius: 0; + box-shadow: none; + border-color: #d2d6de; +} +.form-control:focus { + border-color: #3c8dbc; + box-shadow: none; +} +.form-control::-moz-placeholder, +.form-control:-ms-input-placeholder, +.form-control::-webkit-input-placeholder { + color: #bbb; + opacity: 1; +} +.form-control:not(select) { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} +.form-group.has-success label { + color: #00a65a; +} +.form-group.has-success .form-control, +.form-group.has-success .input-group-addon { + border-color: #00a65a; + box-shadow: none; +} +.form-group.has-success .help-block { + color: #00a65a; +} +.form-group.has-warning label { + color: #f39c12; +} +.form-group.has-warning .form-control, +.form-group.has-warning .input-group-addon { + border-color: #f39c12; + box-shadow: none; +} +.form-group.has-warning .help-block { + color: #f39c12; +} +.form-group.has-error label { + color: #dd4b39; +} +.form-group.has-error .form-control, +.form-group.has-error .input-group-addon { + border-color: #dd4b39; + box-shadow: none; +} +.form-group.has-error .help-block { + color: #dd4b39; +} +/* Input group */ +.input-group .input-group-addon { + border-radius: 0; + border-color: #d2d6de; + background-color: #fff; +} +/* button groups */ +.btn-group-vertical .btn.btn-flat:first-of-type, +.btn-group-vertical .btn.btn-flat:last-of-type { + border-radius: 0; +} +.icheck > label { + padding-left: 0; +} +/* support Font Awesome icons in form-control */ +.form-control-feedback.fa { + line-height: 34px; +} +.input-lg + .form-control-feedback.fa, +.input-group-lg + .form-control-feedback.fa, +.form-group-lg .form-control + .form-control-feedback.fa { + line-height: 46px; +} +.input-sm + .form-control-feedback.fa, +.input-group-sm + .form-control-feedback.fa, +.form-group-sm .form-control + .form-control-feedback.fa { + line-height: 30px; +} +/* + * Component: Progress Bar + * ----------------------- + */ +.progress, +.progress > .progress-bar { + -webkit-box-shadow: none; + box-shadow: none; +} +.progress, +.progress > .progress-bar, +.progress .progress-bar, +.progress > .progress-bar .progress-bar { + border-radius: 1px; +} +/* size variation */ +.progress.sm, +.progress-sm { + height: 10px; +} +.progress.sm, +.progress-sm, +.progress.sm .progress-bar, +.progress-sm .progress-bar { + border-radius: 1px; +} +.progress.xs, +.progress-xs { + height: 7px; +} +.progress.xs, +.progress-xs, +.progress.xs .progress-bar, +.progress-xs .progress-bar { + border-radius: 1px; +} +.progress.xxs, +.progress-xxs { + height: 3px; +} +.progress.xxs, +.progress-xxs, +.progress.xxs .progress-bar, +.progress-xxs .progress-bar { + border-radius: 1px; +} +/* Vertical bars */ +.progress.vertical { + position: relative; + width: 30px; + height: 200px; + display: inline-block; + margin-right: 10px; +} +.progress.vertical > .progress-bar { + width: 100%; + position: absolute; + bottom: 0; +} +.progress.vertical.sm, +.progress.vertical.progress-sm { + width: 20px; +} +.progress.vertical.xs, +.progress.vertical.progress-xs { + width: 10px; +} +.progress.vertical.xxs, +.progress.vertical.progress-xxs { + width: 3px; +} +.progress-group .progress-text { + font-weight: 600; +} +.progress-group .progress-number { + float: right; +} +/* Remove margins from progress bars when put in a table */ +.table tr > td .progress { + margin: 0; +} +.progress-bar-light-blue, +.progress-bar-primary { + background-color: #3c8dbc; +} +.progress-striped .progress-bar-light-blue, +.progress-striped .progress-bar-primary { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.progress-bar-green, +.progress-bar-success { + background-color: #00a65a; +} +.progress-striped .progress-bar-green, +.progress-striped .progress-bar-success { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.progress-bar-aqua, +.progress-bar-info { + background-color: #00c0ef; +} +.progress-striped .progress-bar-aqua, +.progress-striped .progress-bar-info { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.progress-bar-yellow, +.progress-bar-warning { + background-color: #f39c12; +} +.progress-striped .progress-bar-yellow, +.progress-striped .progress-bar-warning { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.progress-bar-red, +.progress-bar-danger { + background-color: #dd4b39; +} +.progress-striped .progress-bar-red, +.progress-striped .progress-bar-danger { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +/* + * Component: Small Box + * -------------------- + */ +.small-box { + border-radius: 2px; + position: relative; + display: block; + margin-bottom: 20px; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); +} +.small-box > .inner { + padding: 10px; +} +.small-box > .small-box-footer { + position: relative; + text-align: center; + padding: 3px 0; + color: #fff; + color: rgba(255, 255, 255, 0.8); + display: block; + z-index: 10; + background: rgba(0, 0, 0, 0.1); + text-decoration: none; +} +.small-box > .small-box-footer:hover { + color: #fff; + background: rgba(0, 0, 0, 0.15); +} +.small-box h3 { + font-size: 38px; + font-weight: bold; + margin: 0 0 10px 0; + white-space: nowrap; + padding: 0; +} +.small-box p { + font-size: 15px; +} +.small-box p > small { + display: block; + color: #f9f9f9; + font-size: 13px; + margin-top: 5px; +} +.small-box h3, +.small-box p { + z-index: 5; +} +.small-box .icon { + -webkit-transition: all 0.3s linear; + -o-transition: all 0.3s linear; + transition: all 0.3s linear; + position: absolute; + top: -10px; + right: 10px; + z-index: 0; + font-size: 90px; + color: rgba(0, 0, 0, 0.15); +} +.small-box:hover { + text-decoration: none; + color: #f9f9f9; +} +.small-box:hover .icon { + font-size: 95px; +} +@media (max-width: 767px) { + .small-box { + text-align: center; + } + .small-box .icon { + display: none; + } + .small-box p { + font-size: 12px; + } +} +/* + * Component: Box + * -------------- + */ +.box { + position: relative; + border-radius: 3px; + background: #ffffff; + border-top: 3px solid #d2d6de; + margin-bottom: 20px; + width: 100%; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); +} +.box.box-primary { + border-top-color: #3c8dbc; +} +.box.box-info { + border-top-color: #00c0ef; +} +.box.box-danger { + border-top-color: #dd4b39; +} +.box.box-warning { + border-top-color: #f39c12; +} +.box.box-success { + border-top-color: #00a65a; +} +.box.box-default { + border-top-color: #d2d6de; +} +.box.collapsed-box .box-body, +.box.collapsed-box .box-footer { + display: none; +} +.box .nav-stacked > li { + border-bottom: 1px solid #f4f4f4; + margin: 0; +} +.box .nav-stacked > li:last-of-type { + border-bottom: none; +} +.box.height-control .box-body { + max-height: 300px; + overflow: auto; +} +.box .border-right { + border-right: 1px solid #f4f4f4; +} +.box .border-left { + border-left: 1px solid #f4f4f4; +} +.box.box-solid { + border-top: 0; +} +.box.box-solid > .box-header .btn.btn-default { + background: transparent; +} +.box.box-solid > .box-header .btn:hover, +.box.box-solid > .box-header a:hover { + background: rgba(0, 0, 0, 0.1); +} +.box.box-solid.box-default { + border: 1px solid #d2d6de; +} +.box.box-solid.box-default > .box-header { + color: #444444; + background: #d2d6de; + background-color: #d2d6de; +} +.box.box-solid.box-default > .box-header a, +.box.box-solid.box-default > .box-header .btn { + color: #444444; +} +.box.box-solid.box-primary { + border: 1px solid #3c8dbc; +} +.box.box-solid.box-primary > .box-header { + color: #ffffff; + background: #3c8dbc; + background-color: #3c8dbc; +} +.box.box-solid.box-primary > .box-header a, +.box.box-solid.box-primary > .box-header .btn { + color: #ffffff; +} +.box.box-solid.box-info { + border: 1px solid #00c0ef; +} +.box.box-solid.box-info > .box-header { + color: #ffffff; + background: #00c0ef; + background-color: #00c0ef; +} +.box.box-solid.box-info > .box-header a, +.box.box-solid.box-info > .box-header .btn { + color: #ffffff; +} +.box.box-solid.box-danger { + border: 1px solid #dd4b39; +} +.box.box-solid.box-danger > .box-header { + color: #ffffff; + background: #dd4b39; + background-color: #dd4b39; +} +.box.box-solid.box-danger > .box-header a, +.box.box-solid.box-danger > .box-header .btn { + color: #ffffff; +} +.box.box-solid.box-warning { + border: 1px solid #f39c12; +} +.box.box-solid.box-warning > .box-header { + color: #ffffff; + background: #f39c12; + background-color: #f39c12; +} +.box.box-solid.box-warning > .box-header a, +.box.box-solid.box-warning > .box-header .btn { + color: #ffffff; +} +.box.box-solid.box-success { + border: 1px solid #00a65a; +} +.box.box-solid.box-success > .box-header { + color: #ffffff; + background: #00a65a; + background-color: #00a65a; +} +.box.box-solid.box-success > .box-header a, +.box.box-solid.box-success > .box-header .btn { + color: #ffffff; +} +.box.box-solid > .box-header > .box-tools .btn { + border: 0; + box-shadow: none; +} +.box.box-solid[class*='bg'] > .box-header { + color: #fff; +} +.box .box-group > .box { + margin-bottom: 5px; +} +.box .knob-label { + text-align: center; + color: #333; + font-weight: 100; + font-size: 12px; + margin-bottom: 0.3em; +} +.box > .overlay, +.overlay-wrapper > .overlay, +.box > .loading-img, +.overlay-wrapper > .loading-img { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +.box .overlay, +.overlay-wrapper .overlay { + z-index: 50; + background: rgba(255, 255, 255, 0.7); + border-radius: 3px; +} +.box .overlay > .fa, +.overlay-wrapper .overlay > .fa { + position: absolute; + top: 50%; + left: 50%; + margin-left: -15px; + margin-top: -15px; + color: #000; + font-size: 30px; +} +.box .overlay.dark, +.overlay-wrapper .overlay.dark { + background: rgba(0, 0, 0, 0.5); +} +.box-header:before, +.box-body:before, +.box-footer:before, +.box-header:after, +.box-body:after, +.box-footer:after { + content: " "; + display: table; +} +.box-header:after, +.box-body:after, +.box-footer:after { + clear: both; +} +.box-header { + color: #444; + display: block; + padding: 10px; + position: relative; +} +.box-header.with-border { + border-bottom: 1px solid #f4f4f4; +} +.collapsed-box .box-header.with-border { + border-bottom: none; +} +.box-header > .fa, +.box-header > .glyphicon, +.box-header > .ion, +.box-header .box-title { + display: inline-block; + font-size: 18px; + margin: 0; + line-height: 1; +} +.box-header > .fa, +.box-header > .glyphicon, +.box-header > .ion { + margin-right: 5px; +} +.box-header > .box-tools { + position: absolute; + right: 10px; + top: 5px; +} +.box-header > .box-tools [data-toggle="tooltip"] { + position: relative; +} +.box-header > .box-tools.pull-right .dropdown-menu { + right: 0; + left: auto; +} +.btn-box-tool { + padding: 5px; + font-size: 12px; + background: transparent; + color: #97a0b3; +} +.open .btn-box-tool, +.btn-box-tool:hover { + color: #606c84; +} +.btn-box-tool.btn:active { + box-shadow: none; +} +.box-body { + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; + padding: 10px; +} +.no-header .box-body { + border-top-right-radius: 3px; + border-top-left-radius: 3px; +} +.box-body > .table { + margin-bottom: 0; +} +.box-body .fc { + margin-top: 5px; +} +.box-body .full-width-chart { + margin: -19px; +} +.box-body.no-padding .full-width-chart { + margin: -9px; +} +.box-body .box-pane { + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 3px; +} +.box-body .box-pane-right { + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 0; +} +.box-footer { + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; + border-top: 1px solid #f4f4f4; + padding: 10px; + background-color: #ffffff; +} +.chart-legend { + margin: 10px 0; +} +@media (max-width: 991px) { + .chart-legend > li { + float: left; + margin-right: 10px; + } +} +.box-comments { + background: #f7f7f7; +} +.box-comments .box-comment { + padding: 8px 0; + border-bottom: 1px solid #eee; +} +.box-comments .box-comment:before, +.box-comments .box-comment:after { + content: " "; + display: table; +} +.box-comments .box-comment:after { + clear: both; +} +.box-comments .box-comment:last-of-type { + border-bottom: 0; +} +.box-comments .box-comment:first-of-type { + padding-top: 0; +} +.box-comments .box-comment img { + float: left; +} +.box-comments .comment-text { + margin-left: 40px; + color: #555; +} +.box-comments .username { + color: #444; + display: block; + font-weight: 600; +} +.box-comments .text-muted { + font-weight: 400; + font-size: 12px; +} +/* Widget: TODO LIST */ +.todo-list { + margin: 0; + padding: 0; + list-style: none; + overflow: auto; +} +.todo-list > li { + border-radius: 2px; + padding: 10px; + background: #f4f4f4; + margin-bottom: 2px; + border-left: 2px solid #e6e7e8; + color: #444; +} +.todo-list > li:last-of-type { + margin-bottom: 0; +} +.todo-list > li > input[type='checkbox'] { + margin: 0 10px 0 5px; +} +.todo-list > li .text { + display: inline-block; + margin-left: 5px; + font-weight: 600; +} +.todo-list > li .label { + margin-left: 10px; + font-size: 9px; +} +.todo-list > li .tools { + display: none; + float: right; + color: #dd4b39; +} +.todo-list > li .tools > .fa, +.todo-list > li .tools > .glyphicon, +.todo-list > li .tools > .ion { + margin-right: 5px; + cursor: pointer; +} +.todo-list > li:hover .tools { + display: inline-block; +} +.todo-list > li.done { + color: #999; +} +.todo-list > li.done .text { + text-decoration: line-through; + font-weight: 500; +} +.todo-list > li.done .label { + background: #d2d6de !important; +} +.todo-list .danger { + border-left-color: #dd4b39; +} +.todo-list .warning { + border-left-color: #f39c12; +} +.todo-list .info { + border-left-color: #00c0ef; +} +.todo-list .success { + border-left-color: #00a65a; +} +.todo-list .primary { + border-left-color: #3c8dbc; +} +.todo-list .handle { + display: inline-block; + cursor: move; + margin: 0 5px; +} +/* Chat widget (DEPRECATED - this will be removed in the next major release. Use Direct Chat instead)*/ +.chat { + padding: 5px 20px 5px 10px; +} +.chat .item { + margin-bottom: 10px; +} +.chat .item:before, +.chat .item:after { + content: " "; + display: table; +} +.chat .item:after { + clear: both; +} +.chat .item > img { + width: 40px; + height: 40px; + border: 2px solid transparent; + border-radius: 50%; +} +.chat .item > .online { + border: 2px solid #00a65a; +} +.chat .item > .offline { + border: 2px solid #dd4b39; +} +.chat .item > .message { + margin-left: 55px; + margin-top: -40px; +} +.chat .item > .message > .name { + display: block; + font-weight: 600; +} +.chat .item > .attachment { + border-radius: 3px; + background: #f4f4f4; + margin-left: 65px; + margin-right: 15px; + padding: 10px; +} +.chat .item > .attachment > h4 { + margin: 0 0 5px 0; + font-weight: 600; + font-size: 14px; +} +.chat .item > .attachment > p, +.chat .item > .attachment > .filename { + font-weight: 600; + font-size: 13px; + font-style: italic; + margin: 0; +} +.chat .item > .attachment:before, +.chat .item > .attachment:after { + content: " "; + display: table; +} +.chat .item > .attachment:after { + clear: both; +} +.box-input { + max-width: 200px; +} +.modal .panel-body { + color: #444; +} +/* + * Component: Info Box + * ------------------- + */ +.info-box { + display: block; + min-height: 90px; + background: #fff; + width: 100%; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + border-radius: 2px; + margin-bottom: 15px; +} +.info-box small { + font-size: 14px; +} +.info-box .progress { + background: rgba(0, 0, 0, 0.2); + margin: 5px -10px 5px -10px; + height: 2px; +} +.info-box .progress, +.info-box .progress .progress-bar { + border-radius: 0; +} +.info-box .progress .progress-bar { + background: #fff; +} +.info-box-icon { + border-top-left-radius: 2px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 2px; + display: block; + float: left; + height: 90px; + width: 90px; + text-align: center; + font-size: 45px; + line-height: 90px; + background: rgba(0, 0, 0, 0.2); +} +.info-box-icon > img { + max-width: 100%; +} +.info-box-content { + padding: 5px 10px; + margin-left: 90px; +} +.info-box-number { + display: block; + font-weight: bold; + font-size: 18px; +} +.progress-description, +.info-box-text { + display: block; + font-size: 14px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.info-box-text { + text-transform: uppercase; +} +.info-box-more { + display: block; +} +.progress-description { + margin: 0; +} +/* + * Component: Timeline + * ------------------- + */ +.timeline { + position: relative; + margin: 0 0 30px 0; + padding: 0; + list-style: none; +} +.timeline:before { + content: ''; + position: absolute; + top: 0; + bottom: 0; + width: 4px; + background: #ddd; + left: 31px; + margin: 0; + border-radius: 2px; +} +.timeline > li { + position: relative; + margin-right: 10px; + margin-bottom: 15px; +} +.timeline > li:before, +.timeline > li:after { + content: " "; + display: table; +} +.timeline > li:after { + clear: both; +} +.timeline > li > .timeline-item { + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + border-radius: 3px; + margin-top: 0; + background: #fff; + color: #444; + margin-left: 60px; + margin-right: 15px; + padding: 0; + position: relative; +} +.timeline > li > .timeline-item > .time { + color: #999; + float: right; + padding: 10px; + font-size: 12px; +} +.timeline > li > .timeline-item > .timeline-header { + margin: 0; + color: #555; + border-bottom: 1px solid #f4f4f4; + padding: 10px; + font-size: 16px; + line-height: 1.1; +} +.timeline > li > .timeline-item > .timeline-header > a { + font-weight: 600; +} +.timeline > li > .timeline-item > .timeline-body, +.timeline > li > .timeline-item > .timeline-footer { + padding: 10px; +} +.timeline > li > .fa, +.timeline > li > .glyphicon, +.timeline > li > .ion { + width: 30px; + height: 30px; + font-size: 15px; + line-height: 30px; + position: absolute; + color: #666; + background: #d2d6de; + border-radius: 50%; + text-align: center; + left: 18px; + top: 0; +} +.timeline > .time-label > span { + font-weight: 600; + padding: 5px; + display: inline-block; + background-color: #fff; + border-radius: 4px; +} +.timeline-inverse > li > .timeline-item { + background: #f0f0f0; + border: 1px solid #ddd; + -webkit-box-shadow: none; + box-shadow: none; +} +.timeline-inverse > li > .timeline-item > .timeline-header { + border-bottom-color: #ddd; +} +/* + * Component: Button + * ----------------- + */ +.btn { + border-radius: 3px; + -webkit-box-shadow: none; + box-shadow: none; + border: 1px solid transparent; +} +.btn.uppercase { + text-transform: uppercase; +} +.btn.btn-flat { + border-radius: 0; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + border-width: 1px; +} +.btn:active { + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + -moz-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} +.btn:focus { + outline: none; +} +.btn.btn-file { + position: relative; + overflow: hidden; +} +.btn.btn-file > input[type='file'] { + position: absolute; + top: 0; + right: 0; + min-width: 100%; + min-height: 100%; + font-size: 100px; + text-align: right; + opacity: 0; + filter: alpha(opacity=0); + outline: none; + background: white; + cursor: inherit; + display: block; +} +.btn-default { + background-color: #f4f4f4; + color: #444; + border-color: #ddd; +} +.btn-default:hover, +.btn-default:active, +.btn-default.hover { + background-color: #e7e7e7; +} +.btn-primary { + background-color: #3c8dbc; + border-color: #367fa9; +} +.btn-primary:hover, +.btn-primary:active, +.btn-primary.hover { + background-color: #367fa9; +} +.btn-success { + background-color: #00a65a; + border-color: #008d4c; +} +.btn-success:hover, +.btn-success:active, +.btn-success.hover { + background-color: #008d4c; +} +.btn-info { + background-color: #00c0ef; + border-color: #00acd6; +} +.btn-info:hover, +.btn-info:active, +.btn-info.hover { + background-color: #00acd6; +} +.btn-danger { + background-color: #dd4b39; + border-color: #d73925; +} +.btn-danger:hover, +.btn-danger:active, +.btn-danger.hover { + background-color: #d73925; +} +.btn-warning { + background-color: #f39c12; + border-color: #e08e0b; +} +.btn-warning:hover, +.btn-warning:active, +.btn-warning.hover { + background-color: #e08e0b; +} +.btn-outline { + border: 1px solid #fff; + background: transparent; + color: #fff; +} +.btn-outline:hover, +.btn-outline:focus, +.btn-outline:active { + color: rgba(255, 255, 255, 0.7); + border-color: rgba(255, 255, 255, 0.7); +} +.btn-link { + -webkit-box-shadow: none; + box-shadow: none; +} +.btn[class*='bg-']:hover { + -webkit-box-shadow: inset 0 0 100px rgba(0, 0, 0, 0.2); + box-shadow: inset 0 0 100px rgba(0, 0, 0, 0.2); +} +.btn-app { + border-radius: 3px; + position: relative; + padding: 15px 5px; + margin: 0 0 10px 10px; + min-width: 80px; + height: 60px; + text-align: center; + color: #666; + border: 1px solid #ddd; + background-color: #f4f4f4; + font-size: 12px; +} +.btn-app > .fa, +.btn-app > .glyphicon, +.btn-app > .ion { + font-size: 20px; + display: block; +} +.btn-app:hover { + background: #f4f4f4; + color: #444; + border-color: #aaa; +} +.btn-app:active, +.btn-app:focus { + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + -moz-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} +.btn-app > .badge { + position: absolute; + top: -3px; + right: -10px; + font-size: 10px; + font-weight: 400; +} +/* + * Component: Callout + * ------------------ + */ +.callout { + border-radius: 3px; + margin: 0 0 20px 0; + padding: 15px 30px 15px 15px; + border-left: 5px solid #eee; +} +.callout a { + color: #fff; + text-decoration: underline; +} +.callout a:hover { + color: #eee; +} +.callout h4 { + margin-top: 0; + font-weight: 600; +} +.callout p:last-child { + margin-bottom: 0; +} +.callout code, +.callout .highlight { + background-color: #fff; +} +.callout.callout-danger { + border-color: #c23321; +} +.callout.callout-warning { + border-color: #c87f0a; +} +.callout.callout-info { + border-color: #0097bc; +} +.callout.callout-success { + border-color: #00733e; +} +/* + * Component: alert + * ---------------- + */ +.alert { + border-radius: 3px; +} +.alert h4 { + font-weight: 600; +} +.alert .icon { + margin-right: 10px; +} +.alert .close { + color: #000; + opacity: 0.2; + filter: alpha(opacity=20); +} +.alert .close:hover { + opacity: 0.5; + filter: alpha(opacity=50); +} +.alert a { + color: #fff; + text-decoration: underline; +} +.alert-success { + border-color: #008d4c; +} +.alert-danger, +.alert-error { + border-color: #d73925; +} +.alert-warning { + border-color: #e08e0b; +} +.alert-info { + border-color: #00acd6; +} +/* + * Component: Nav + * -------------- + */ +.nav > li > a:hover, +.nav > li > a:active, +.nav > li > a:focus { + color: #444; + background: #f7f7f7; +} +/* NAV PILLS */ +.nav-pills > li > a { + border-radius: 0; + border-top: 3px solid transparent; + color: #444; +} +.nav-pills > li > a > .fa, +.nav-pills > li > a > .glyphicon, +.nav-pills > li > a > .ion { + margin-right: 5px; +} +.nav-pills > li.active > a, +.nav-pills > li.active > a:hover, +.nav-pills > li.active > a:focus { + border-top-color: #3c8dbc; +} +.nav-pills > li.active > a { + font-weight: 600; +} +/* NAV STACKED */ +.nav-stacked > li > a { + border-radius: 0; + border-top: 0; + border-left: 3px solid transparent; + color: #444; +} +.nav-stacked > li.active > a, +.nav-stacked > li.active > a:hover { + background: transparent; + color: #444; + border-top: 0; + border-left-color: #3c8dbc; +} +.nav-stacked > li.header { + border-bottom: 1px solid #ddd; + color: #777; + margin-bottom: 10px; + padding: 5px 10px; + text-transform: uppercase; +} +/* NAV TABS */ +.nav-tabs-custom { + margin-bottom: 20px; + background: #fff; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + border-radius: 3px; +} +.nav-tabs-custom > .nav-tabs { + margin: 0; + border-bottom-color: #f4f4f4; + border-top-right-radius: 3px; + border-top-left-radius: 3px; +} +.nav-tabs-custom > .nav-tabs > li { + border-top: 3px solid transparent; + margin-bottom: -2px; + margin-right: 5px; +} +.nav-tabs-custom > .nav-tabs > li > a { + color: #444; + border-radius: 0; +} +.nav-tabs-custom > .nav-tabs > li > a.text-muted { + color: #999; +} +.nav-tabs-custom > .nav-tabs > li > a, +.nav-tabs-custom > .nav-tabs > li > a:hover { + background: transparent; + margin: 0; +} +.nav-tabs-custom > .nav-tabs > li > a:hover { + color: #999; +} +.nav-tabs-custom > .nav-tabs > li:not(.active) > a:hover, +.nav-tabs-custom > .nav-tabs > li:not(.active) > a:focus, +.nav-tabs-custom > .nav-tabs > li:not(.active) > a:active { + border-color: transparent; +} +.nav-tabs-custom > .nav-tabs > li.active { + border-top-color: #3c8dbc; +} +.nav-tabs-custom > .nav-tabs > li.active > a, +.nav-tabs-custom > .nav-tabs > li.active:hover > a { + background-color: #fff; + color: #444; +} +.nav-tabs-custom > .nav-tabs > li.active > a { + border-top-color: transparent; + border-left-color: #f4f4f4; + border-right-color: #f4f4f4; +} +.nav-tabs-custom > .nav-tabs > li:first-of-type { + margin-left: 0; +} +.nav-tabs-custom > .nav-tabs > li:first-of-type.active > a { + border-left-color: transparent; +} +.nav-tabs-custom > .nav-tabs.pull-right { + float: none !important; +} +.nav-tabs-custom > .nav-tabs.pull-right > li { + float: right; +} +.nav-tabs-custom > .nav-tabs.pull-right > li:first-of-type { + margin-right: 0; +} +.nav-tabs-custom > .nav-tabs.pull-right > li:first-of-type > a { + border-left-width: 1px; +} +.nav-tabs-custom > .nav-tabs.pull-right > li:first-of-type.active > a { + border-left-color: #f4f4f4; + border-right-color: transparent; +} +.nav-tabs-custom > .nav-tabs > li.header { + line-height: 35px; + padding: 0 10px; + font-size: 20px; + color: #444; +} +.nav-tabs-custom > .nav-tabs > li.header > .fa, +.nav-tabs-custom > .nav-tabs > li.header > .glyphicon, +.nav-tabs-custom > .nav-tabs > li.header > .ion { + margin-right: 5px; +} +.nav-tabs-custom > .tab-content { + background: #fff; + padding: 10px; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.nav-tabs-custom .dropdown.open > a:active, +.nav-tabs-custom .dropdown.open > a:focus { + background: transparent; + color: #999; +} +.nav-tabs-custom.tab-primary > .nav-tabs > li.active { + border-top-color: #3c8dbc; +} +.nav-tabs-custom.tab-info > .nav-tabs > li.active { + border-top-color: #00c0ef; +} +.nav-tabs-custom.tab-danger > .nav-tabs > li.active { + border-top-color: #dd4b39; +} +.nav-tabs-custom.tab-warning > .nav-tabs > li.active { + border-top-color: #f39c12; +} +.nav-tabs-custom.tab-success > .nav-tabs > li.active { + border-top-color: #00a65a; +} +.nav-tabs-custom.tab-default > .nav-tabs > li.active { + border-top-color: #d2d6de; +} +/* PAGINATION */ +.pagination > li > a { + background: #fafafa; + color: #666; +} +.pagination.pagination-flat > li > a { + border-radius: 0 !important; +} +/* + * Component: Products List + * ------------------------ + */ +.products-list { + list-style: none; + margin: 0; + padding: 0; +} +.products-list > .item { + border-radius: 3px; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + padding: 10px 0; + background: #fff; +} +.products-list > .item:before, +.products-list > .item:after { + content: " "; + display: table; +} +.products-list > .item:after { + clear: both; +} +.products-list .product-img { + float: left; +} +.products-list .product-img img { + width: 50px; + height: 50px; +} +.products-list .product-info { + margin-left: 60px; +} +.products-list .product-title { + font-weight: 600; +} +.products-list .product-description { + display: block; + color: #999; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +.product-list-in-box > .item { + -webkit-box-shadow: none; + box-shadow: none; + border-radius: 0; + border-bottom: 1px solid #f4f4f4; +} +.product-list-in-box > .item:last-of-type { + border-bottom-width: 0; +} +/* + * Component: Table + * ---------------- + */ +.table > thead > tr > th, +.table > tbody > tr > th, +.table > tfoot > tr > th, +.table > thead > tr > td, +.table > tbody > tr > td, +.table > tfoot > tr > td { + border-top: 1px solid #f4f4f4; +} +.table > thead > tr > th { + border-bottom: 2px solid #f4f4f4; +} +.table tr td .progress { + margin-top: 5px; +} +.table-bordered { + border: 1px solid #f4f4f4; +} +.table-bordered > thead > tr > th, +.table-bordered > tbody > tr > th, +.table-bordered > tfoot > tr > th, +.table-bordered > thead > tr > td, +.table-bordered > tbody > tr > td, +.table-bordered > tfoot > tr > td { + border: 1px solid #f4f4f4; +} +.table-bordered > thead > tr > th, +.table-bordered > thead > tr > td { + border-bottom-width: 2px; +} +.table.no-border, +.table.no-border td, +.table.no-border th { + border: 0; +} +/* .text-center in tables */ +table.text-center, +table.text-center td, +table.text-center th { + text-align: center; +} +.table.align th { + text-align: left; +} +.table.align td { + text-align: right; +} +/* + * Component: Label + * ---------------- + */ +.label-default { + background-color: #d2d6de; + color: #444; +} +/* + * Component: Direct Chat + * ---------------------- + */ +.direct-chat .box-body { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + position: relative; + overflow-x: hidden; + padding: 0; +} +.direct-chat.chat-pane-open .direct-chat-contacts { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + -o-transform: translate(0, 0); + transform: translate(0, 0); +} +.direct-chat-messages { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + -o-transform: translate(0, 0); + transform: translate(0, 0); + padding: 10px; + height: 250px; + overflow: auto; +} +.direct-chat-msg, +.direct-chat-text { + display: block; +} +.direct-chat-msg { + margin-bottom: 10px; +} +.direct-chat-msg:before, +.direct-chat-msg:after { + content: " "; + display: table; +} +.direct-chat-msg:after { + clear: both; +} +.direct-chat-messages, +.direct-chat-contacts { + -webkit-transition: -webkit-transform 0.5s ease-in-out; + -moz-transition: -moz-transform 0.5s ease-in-out; + -o-transition: -o-transform 0.5s ease-in-out; + transition: transform 0.5s ease-in-out; +} +.direct-chat-text { + border-radius: 5px; + position: relative; + padding: 5px 10px; + background: #d2d6de; + border: 1px solid #d2d6de; + margin: 5px 0 0 50px; + color: #444444; +} +.direct-chat-text:after, +.direct-chat-text:before { + position: absolute; + right: 100%; + top: 15px; + border: solid transparent; + border-right-color: #d2d6de; + content: ' '; + height: 0; + width: 0; + pointer-events: none; +} +.direct-chat-text:after { + border-width: 5px; + margin-top: -5px; +} +.direct-chat-text:before { + border-width: 6px; + margin-top: -6px; +} +.right .direct-chat-text { + margin-right: 50px; + margin-left: 0; +} +.right .direct-chat-text:after, +.right .direct-chat-text:before { + right: auto; + left: 100%; + border-right-color: transparent; + border-left-color: #d2d6de; +} +.direct-chat-img { + border-radius: 50%; + float: left; + width: 40px; + height: 40px; +} +.right .direct-chat-img { + float: right; +} +.direct-chat-info { + display: block; + margin-bottom: 2px; + font-size: 12px; +} +.direct-chat-name { + font-weight: 600; +} +.direct-chat-timestamp { + color: #999; +} +.direct-chat-contacts-open .direct-chat-contacts { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + -o-transform: translate(0, 0); + transform: translate(0, 0); +} +.direct-chat-contacts { + -webkit-transform: translate(101%, 0); + -ms-transform: translate(101%, 0); + -o-transform: translate(101%, 0); + transform: translate(101%, 0); + position: absolute; + top: 0; + bottom: 0; + height: 250px; + width: 100%; + background: #222d32; + color: #fff; + overflow: auto; +} +.contacts-list > li { + border-bottom: 1px solid rgba(0, 0, 0, 0.2); + padding: 10px; + margin: 0; +} +.contacts-list > li:before, +.contacts-list > li:after { + content: " "; + display: table; +} +.contacts-list > li:after { + clear: both; +} +.contacts-list > li:last-of-type { + border-bottom: none; +} +.contacts-list-img { + border-radius: 50%; + width: 40px; + float: left; +} +.contacts-list-info { + margin-left: 45px; + color: #fff; +} +.contacts-list-name, +.contacts-list-status { + display: block; +} +.contacts-list-name { + font-weight: 600; +} +.contacts-list-status { + font-size: 12px; +} +.contacts-list-date { + color: #aaa; + font-weight: normal; +} +.contacts-list-msg { + color: #999; +} +.direct-chat-danger .right > .direct-chat-text { + background: #dd4b39; + border-color: #dd4b39; + color: #ffffff; +} +.direct-chat-danger .right > .direct-chat-text:after, +.direct-chat-danger .right > .direct-chat-text:before { + border-left-color: #dd4b39; +} +.direct-chat-primary .right > .direct-chat-text { + background: #3c8dbc; + border-color: #3c8dbc; + color: #ffffff; +} +.direct-chat-primary .right > .direct-chat-text:after, +.direct-chat-primary .right > .direct-chat-text:before { + border-left-color: #3c8dbc; +} +.direct-chat-warning .right > .direct-chat-text { + background: #f39c12; + border-color: #f39c12; + color: #ffffff; +} +.direct-chat-warning .right > .direct-chat-text:after, +.direct-chat-warning .right > .direct-chat-text:before { + border-left-color: #f39c12; +} +.direct-chat-info .right > .direct-chat-text { + background: #00c0ef; + border-color: #00c0ef; + color: #ffffff; +} +.direct-chat-info .right > .direct-chat-text:after, +.direct-chat-info .right > .direct-chat-text:before { + border-left-color: #00c0ef; +} +.direct-chat-success .right > .direct-chat-text { + background: #00a65a; + border-color: #00a65a; + color: #ffffff; +} +.direct-chat-success .right > .direct-chat-text:after, +.direct-chat-success .right > .direct-chat-text:before { + border-left-color: #00a65a; +} +/* + * Component: Users List + * --------------------- + */ +.users-list > li { + width: 25%; + float: left; + padding: 10px; + text-align: center; +} +.users-list > li img { + border-radius: 50%; + max-width: 100%; + height: auto; +} +.users-list > li > a:hover, +.users-list > li > a:hover .users-list-name { + color: #999; +} +.users-list-name, +.users-list-date { + display: block; +} +.users-list-name { + font-weight: 600; + color: #444; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +.users-list-date { + color: #999; + font-size: 12px; +} +/* + * Component: Carousel + * ------------------- + */ +.carousel-control.left, +.carousel-control.right { + background-image: none; +} +.carousel-control > .fa { + font-size: 40px; + position: absolute; + top: 50%; + z-index: 5; + display: inline-block; + margin-top: -20px; +} +/* + * Component: modal + * ---------------- + */ +.modal { + background: rgba(0, 0, 0, 0.3); +} +.modal-content { + border-radius: 0; + -webkit-box-shadow: 0 2px 3px rgba(0, 0, 0, 0.125); + box-shadow: 0 2px 3px rgba(0, 0, 0, 0.125); + border: 0; +} +@media (min-width: 768px) { + .modal-content { + -webkit-box-shadow: 0 2px 3px rgba(0, 0, 0, 0.125); + box-shadow: 0 2px 3px rgba(0, 0, 0, 0.125); + } +} +.modal-header { + border-bottom-color: #f4f4f4; +} +.modal-footer { + border-top-color: #f4f4f4; +} +.modal-primary .modal-header, +.modal-primary .modal-footer { + border-color: #307095; +} +.modal-warning .modal-header, +.modal-warning .modal-footer { + border-color: #c87f0a; +} +.modal-info .modal-header, +.modal-info .modal-footer { + border-color: #0097bc; +} +.modal-success .modal-header, +.modal-success .modal-footer { + border-color: #00733e; +} +.modal-danger .modal-header, +.modal-danger .modal-footer { + border-color: #c23321; +} +/* + * Component: Social Widgets + * ------------------------- + */ +.box-widget { + border: none; + position: relative; +} +.widget-user .widget-user-header { + padding: 20px; + height: 120px; + border-top-right-radius: 3px; + border-top-left-radius: 3px; +} +.widget-user .widget-user-username { + margin-top: 0; + margin-bottom: 5px; + font-size: 25px; + font-weight: 300; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); +} +.widget-user .widget-user-desc { + margin-top: 0; +} +.widget-user .widget-user-image { + position: absolute; + top: 65px; + left: 50%; + margin-left: -45px; +} +.widget-user .widget-user-image > img { + width: 90px; + height: auto; + border: 3px solid #fff; +} +.widget-user .box-footer { + padding-top: 30px; +} +.widget-user-2 .widget-user-header { + padding: 20px; + border-top-right-radius: 3px; + border-top-left-radius: 3px; +} +.widget-user-2 .widget-user-username { + margin-top: 5px; + margin-bottom: 5px; + font-size: 25px; + font-weight: 300; +} +.widget-user-2 .widget-user-desc { + margin-top: 0; +} +.widget-user-2 .widget-user-username, +.widget-user-2 .widget-user-desc { + margin-left: 75px; +} +.widget-user-2 .widget-user-image > img { + width: 65px; + height: auto; + float: left; +} +/* + * Page: Mailbox + * ------------- + */ +.mailbox-messages > .table { + margin: 0; +} +.mailbox-controls { + padding: 5px; +} +.mailbox-controls.with-border { + border-bottom: 1px solid #f4f4f4; +} +.mailbox-read-info { + border-bottom: 1px solid #f4f4f4; + padding: 10px; +} +.mailbox-read-info h3 { + font-size: 20px; + margin: 0; +} +.mailbox-read-info h5 { + margin: 0; + padding: 5px 0 0 0; +} +.mailbox-read-time { + color: #999; + font-size: 13px; +} +.mailbox-read-message { + padding: 10px; +} +.mailbox-attachments li { + float: left; + width: 200px; + border: 1px solid #eee; + margin-bottom: 10px; + margin-right: 10px; +} +.mailbox-attachment-name { + font-weight: bold; + color: #666; +} +.mailbox-attachment-icon, +.mailbox-attachment-info, +.mailbox-attachment-size { + display: block; +} +.mailbox-attachment-info { + padding: 10px; + background: #f4f4f4; +} +.mailbox-attachment-size { + color: #999; + font-size: 12px; +} +.mailbox-attachment-icon { + text-align: center; + font-size: 65px; + color: #666; + padding: 20px 10px; +} +.mailbox-attachment-icon.has-img { + padding: 0; +} +.mailbox-attachment-icon.has-img > img { + max-width: 100%; + height: auto; +} +/* + * Page: Lock Screen + * ----------------- + */ +/* ADD THIS CLASS TO THE <BODY> TAG */ +.lockscreen { + background: #d2d6de; +} +.lockscreen-logo { + font-size: 35px; + text-align: center; + margin-bottom: 25px; + font-weight: 300; +} +.lockscreen-logo a { + color: #444; +} +.lockscreen-wrapper { + max-width: 400px; + margin: 0 auto; + margin-top: 10%; +} +/* User name [optional] */ +.lockscreen .lockscreen-name { + text-align: center; + font-weight: 600; +} +/* Will contain the image and the sign in form */ +.lockscreen-item { + border-radius: 4px; + padding: 0; + background: #fff; + position: relative; + margin: 10px auto 30px auto; + width: 290px; +} +/* User image */ +.lockscreen-image { + border-radius: 50%; + position: absolute; + left: -10px; + top: -25px; + background: #fff; + padding: 5px; + z-index: 10; +} +.lockscreen-image > img { + border-radius: 50%; + width: 70px; + height: 70px; +} +/* Contains the password input and the login button */ +.lockscreen-credentials { + margin-left: 70px; +} +.lockscreen-credentials .form-control { + border: 0; +} +.lockscreen-credentials .btn { + background-color: #fff; + border: 0; + padding: 0 10px; +} +.lockscreen-footer { + margin-top: 10px; +} +/* + * Page: Login & Register + * ---------------------- + */ +.login-logo, +.register-logo { + font-size: 35px; + text-align: center; + margin-bottom: 25px; + font-weight: 300; +} +.login-logo a, +.register-logo a { + color: #444; +} +.login-page, +.register-page { + background: #d2d6de; +} +.login-box, +.register-box { + width: 360px; + margin: 7% auto; +} +@media (max-width: 768px) { + .login-box, + .register-box { + width: 90%; + margin-top: 20px; + } +} +.login-box-body, +.register-box-body { + background: #fff; + padding: 20px; + border-top: 0; + color: #666; +} +.login-box-body .form-control-feedback, +.register-box-body .form-control-feedback { + color: #777; +} +.login-box-msg, +.register-box-msg { + margin: 0; + text-align: center; + padding: 0 20px 20px 20px; +} +.social-auth-links { + margin: 10px 0; +} +/* + * Page: 400 and 500 error pages + * ------------------------------ + */ +.error-page { + width: 600px; + margin: 20px auto 0 auto; +} +@media (max-width: 991px) { + .error-page { + width: 100%; + } +} +.error-page > .headline { + float: left; + font-size: 100px; + font-weight: 300; +} +@media (max-width: 991px) { + .error-page > .headline { + float: none; + text-align: center; + } +} +.error-page > .error-content { + margin-left: 190px; + display: block; +} +@media (max-width: 991px) { + .error-page > .error-content { + margin-left: 0; + } +} +.error-page > .error-content > h3 { + font-weight: 300; + font-size: 25px; +} +@media (max-width: 991px) { + .error-page > .error-content > h3 { + text-align: center; + } +} +/* + * Page: Invoice + * ------------- + */ +.invoice { + position: relative; + background: #fff; + border: 1px solid #f4f4f4; + padding: 20px; + margin: 10px 25px; +} +.invoice-title { + margin-top: 0; +} +/* + * Page: Profile + * ------------- + */ +.profile-user-img { + margin: 0 auto; + width: 100px; + padding: 3px; + border: 3px solid #d2d6de; +} +.profile-username { + font-size: 21px; + margin-top: 5px; +} +.post { + border-bottom: 1px solid #d2d6de; + margin-bottom: 15px; + padding-bottom: 15px; + color: #666; +} +.post:last-of-type { + border-bottom: 0; + margin-bottom: 0; + padding-bottom: 0; +} +.post .user-block { + margin-bottom: 15px; +} +/* + * Social Buttons for Bootstrap + * + * Copyright 2013-2015 Panayiotis Lipiridis + * Licensed under the MIT License + * + * https://github.com/lipis/bootstrap-social + */ +.btn-social { + position: relative; + padding-left: 44px; + text-align: left; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.btn-social > :first-child { + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 32px; + line-height: 34px; + font-size: 1.6em; + text-align: center; + border-right: 1px solid rgba(0, 0, 0, 0.2); +} +.btn-social.btn-lg { + padding-left: 61px; +} +.btn-social.btn-lg > :first-child { + line-height: 45px; + width: 45px; + font-size: 1.8em; +} +.btn-social.btn-sm { + padding-left: 38px; +} +.btn-social.btn-sm > :first-child { + line-height: 28px; + width: 28px; + font-size: 1.4em; +} +.btn-social.btn-xs { + padding-left: 30px; +} +.btn-social.btn-xs > :first-child { + line-height: 20px; + width: 20px; + font-size: 1.2em; +} +.btn-social-icon { + position: relative; + padding-left: 44px; + text-align: left; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + height: 34px; + width: 34px; + padding: 0; +} +.btn-social-icon > :first-child { + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 32px; + line-height: 34px; + font-size: 1.6em; + text-align: center; + border-right: 1px solid rgba(0, 0, 0, 0.2); +} +.btn-social-icon.btn-lg { + padding-left: 61px; +} +.btn-social-icon.btn-lg > :first-child { + line-height: 45px; + width: 45px; + font-size: 1.8em; +} +.btn-social-icon.btn-sm { + padding-left: 38px; +} +.btn-social-icon.btn-sm > :first-child { + line-height: 28px; + width: 28px; + font-size: 1.4em; +} +.btn-social-icon.btn-xs { + padding-left: 30px; +} +.btn-social-icon.btn-xs > :first-child { + line-height: 20px; + width: 20px; + font-size: 1.2em; +} +.btn-social-icon > :first-child { + border: none; + text-align: center; + width: 100%; +} +.btn-social-icon.btn-lg { + height: 45px; + width: 45px; + padding-left: 0; + padding-right: 0; +} +.btn-social-icon.btn-sm { + height: 30px; + width: 30px; + padding-left: 0; + padding-right: 0; +} +.btn-social-icon.btn-xs { + height: 22px; + width: 22px; + padding-left: 0; + padding-right: 0; +} +.btn-adn { + color: #ffffff; + background-color: #d87a68; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-adn:focus, +.btn-adn.focus { + color: #ffffff; + background-color: #ce563f; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-adn:hover { + color: #ffffff; + background-color: #ce563f; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-adn:active, +.btn-adn.active, +.open > .dropdown-toggle.btn-adn { + color: #ffffff; + background-color: #ce563f; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-adn:active, +.btn-adn.active, +.open > .dropdown-toggle.btn-adn { + background-image: none; +} +.btn-adn .badge { + color: #d87a68; + background-color: #ffffff; +} +.btn-bitbucket { + color: #ffffff; + background-color: #205081; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-bitbucket:focus, +.btn-bitbucket.focus { + color: #ffffff; + background-color: #163758; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-bitbucket:hover { + color: #ffffff; + background-color: #163758; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-bitbucket:active, +.btn-bitbucket.active, +.open > .dropdown-toggle.btn-bitbucket { + color: #ffffff; + background-color: #163758; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-bitbucket:active, +.btn-bitbucket.active, +.open > .dropdown-toggle.btn-bitbucket { + background-image: none; +} +.btn-bitbucket .badge { + color: #205081; + background-color: #ffffff; +} +.btn-dropbox { + color: #ffffff; + background-color: #1087dd; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-dropbox:focus, +.btn-dropbox.focus { + color: #ffffff; + background-color: #0d6aad; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-dropbox:hover { + color: #ffffff; + background-color: #0d6aad; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-dropbox:active, +.btn-dropbox.active, +.open > .dropdown-toggle.btn-dropbox { + color: #ffffff; + background-color: #0d6aad; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-dropbox:active, +.btn-dropbox.active, +.open > .dropdown-toggle.btn-dropbox { + background-image: none; +} +.btn-dropbox .badge { + color: #1087dd; + background-color: #ffffff; +} +.btn-facebook { + color: #ffffff; + background-color: #3b5998; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-facebook:focus, +.btn-facebook.focus { + color: #ffffff; + background-color: #2d4373; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-facebook:hover { + color: #ffffff; + background-color: #2d4373; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-facebook:active, +.btn-facebook.active, +.open > .dropdown-toggle.btn-facebook { + color: #ffffff; + background-color: #2d4373; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-facebook:active, +.btn-facebook.active, +.open > .dropdown-toggle.btn-facebook { + background-image: none; +} +.btn-facebook .badge { + color: #3b5998; + background-color: #ffffff; +} +.btn-flickr { + color: #ffffff; + background-color: #ff0084; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-flickr:focus, +.btn-flickr.focus { + color: #ffffff; + background-color: #cc006a; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-flickr:hover { + color: #ffffff; + background-color: #cc006a; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-flickr:active, +.btn-flickr.active, +.open > .dropdown-toggle.btn-flickr { + color: #ffffff; + background-color: #cc006a; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-flickr:active, +.btn-flickr.active, +.open > .dropdown-toggle.btn-flickr { + background-image: none; +} +.btn-flickr .badge { + color: #ff0084; + background-color: #ffffff; +} +.btn-foursquare { + color: #ffffff; + background-color: #f94877; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-foursquare:focus, +.btn-foursquare.focus { + color: #ffffff; + background-color: #f71752; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-foursquare:hover { + color: #ffffff; + background-color: #f71752; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-foursquare:active, +.btn-foursquare.active, +.open > .dropdown-toggle.btn-foursquare { + color: #ffffff; + background-color: #f71752; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-foursquare:active, +.btn-foursquare.active, +.open > .dropdown-toggle.btn-foursquare { + background-image: none; +} +.btn-foursquare .badge { + color: #f94877; + background-color: #ffffff; +} +.btn-github { + color: #ffffff; + background-color: #444444; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-github:focus, +.btn-github.focus { + color: #ffffff; + background-color: #2b2b2b; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-github:hover { + color: #ffffff; + background-color: #2b2b2b; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-github:active, +.btn-github.active, +.open > .dropdown-toggle.btn-github { + color: #ffffff; + background-color: #2b2b2b; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-github:active, +.btn-github.active, +.open > .dropdown-toggle.btn-github { + background-image: none; +} +.btn-github .badge { + color: #444444; + background-color: #ffffff; +} +.btn-google { + color: #ffffff; + background-color: #dd4b39; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-google:focus, +.btn-google.focus { + color: #ffffff; + background-color: #c23321; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-google:hover { + color: #ffffff; + background-color: #c23321; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-google:active, +.btn-google.active, +.open > .dropdown-toggle.btn-google { + color: #ffffff; + background-color: #c23321; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-google:active, +.btn-google.active, +.open > .dropdown-toggle.btn-google { + background-image: none; +} +.btn-google .badge { + color: #dd4b39; + background-color: #ffffff; +} +.btn-instagram { + color: #ffffff; + background-color: #3f729b; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-instagram:focus, +.btn-instagram.focus { + color: #ffffff; + background-color: #305777; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-instagram:hover { + color: #ffffff; + background-color: #305777; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-instagram:active, +.btn-instagram.active, +.open > .dropdown-toggle.btn-instagram { + color: #ffffff; + background-color: #305777; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-instagram:active, +.btn-instagram.active, +.open > .dropdown-toggle.btn-instagram { + background-image: none; +} +.btn-instagram .badge { + color: #3f729b; + background-color: #ffffff; +} +.btn-linkedin { + color: #ffffff; + background-color: #007bb6; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-linkedin:focus, +.btn-linkedin.focus { + color: #ffffff; + background-color: #005983; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-linkedin:hover { + color: #ffffff; + background-color: #005983; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-linkedin:active, +.btn-linkedin.active, +.open > .dropdown-toggle.btn-linkedin { + color: #ffffff; + background-color: #005983; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-linkedin:active, +.btn-linkedin.active, +.open > .dropdown-toggle.btn-linkedin { + background-image: none; +} +.btn-linkedin .badge { + color: #007bb6; + background-color: #ffffff; +} +.btn-microsoft { + color: #ffffff; + background-color: #2672ec; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-microsoft:focus, +.btn-microsoft.focus { + color: #ffffff; + background-color: #125acd; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-microsoft:hover { + color: #ffffff; + background-color: #125acd; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-microsoft:active, +.btn-microsoft.active, +.open > .dropdown-toggle.btn-microsoft { + color: #ffffff; + background-color: #125acd; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-microsoft:active, +.btn-microsoft.active, +.open > .dropdown-toggle.btn-microsoft { + background-image: none; +} +.btn-microsoft .badge { + color: #2672ec; + background-color: #ffffff; +} +.btn-openid { + color: #ffffff; + background-color: #f7931e; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-openid:focus, +.btn-openid.focus { + color: #ffffff; + background-color: #da7908; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-openid:hover { + color: #ffffff; + background-color: #da7908; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-openid:active, +.btn-openid.active, +.open > .dropdown-toggle.btn-openid { + color: #ffffff; + background-color: #da7908; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-openid:active, +.btn-openid.active, +.open > .dropdown-toggle.btn-openid { + background-image: none; +} +.btn-openid .badge { + color: #f7931e; + background-color: #ffffff; +} +.btn-pinterest { + color: #ffffff; + background-color: #cb2027; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-pinterest:focus, +.btn-pinterest.focus { + color: #ffffff; + background-color: #9f191f; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-pinterest:hover { + color: #ffffff; + background-color: #9f191f; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-pinterest:active, +.btn-pinterest.active, +.open > .dropdown-toggle.btn-pinterest { + color: #ffffff; + background-color: #9f191f; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-pinterest:active, +.btn-pinterest.active, +.open > .dropdown-toggle.btn-pinterest { + background-image: none; +} +.btn-pinterest .badge { + color: #cb2027; + background-color: #ffffff; +} +.btn-reddit { + color: #000000; + background-color: #eff7ff; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-reddit:focus, +.btn-reddit.focus { + color: #000000; + background-color: #bcddff; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-reddit:hover { + color: #000000; + background-color: #bcddff; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-reddit:active, +.btn-reddit.active, +.open > .dropdown-toggle.btn-reddit { + color: #000000; + background-color: #bcddff; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-reddit:active, +.btn-reddit.active, +.open > .dropdown-toggle.btn-reddit { + background-image: none; +} +.btn-reddit .badge { + color: #eff7ff; + background-color: #000000; +} +.btn-soundcloud { + color: #ffffff; + background-color: #ff5500; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-soundcloud:focus, +.btn-soundcloud.focus { + color: #ffffff; + background-color: #cc4400; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-soundcloud:hover { + color: #ffffff; + background-color: #cc4400; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-soundcloud:active, +.btn-soundcloud.active, +.open > .dropdown-toggle.btn-soundcloud { + color: #ffffff; + background-color: #cc4400; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-soundcloud:active, +.btn-soundcloud.active, +.open > .dropdown-toggle.btn-soundcloud { + background-image: none; +} +.btn-soundcloud .badge { + color: #ff5500; + background-color: #ffffff; +} +.btn-tumblr { + color: #ffffff; + background-color: #2c4762; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-tumblr:focus, +.btn-tumblr.focus { + color: #ffffff; + background-color: #1c2d3f; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-tumblr:hover { + color: #ffffff; + background-color: #1c2d3f; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-tumblr:active, +.btn-tumblr.active, +.open > .dropdown-toggle.btn-tumblr { + color: #ffffff; + background-color: #1c2d3f; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-tumblr:active, +.btn-tumblr.active, +.open > .dropdown-toggle.btn-tumblr { + background-image: none; +} +.btn-tumblr .badge { + color: #2c4762; + background-color: #ffffff; +} +.btn-twitter { + color: #ffffff; + background-color: #55acee; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-twitter:focus, +.btn-twitter.focus { + color: #ffffff; + background-color: #2795e9; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-twitter:hover { + color: #ffffff; + background-color: #2795e9; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-twitter:active, +.btn-twitter.active, +.open > .dropdown-toggle.btn-twitter { + color: #ffffff; + background-color: #2795e9; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-twitter:active, +.btn-twitter.active, +.open > .dropdown-toggle.btn-twitter { + background-image: none; +} +.btn-twitter .badge { + color: #55acee; + background-color: #ffffff; +} +.btn-vimeo { + color: #ffffff; + background-color: #1ab7ea; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-vimeo:focus, +.btn-vimeo.focus { + color: #ffffff; + background-color: #1295bf; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-vimeo:hover { + color: #ffffff; + background-color: #1295bf; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-vimeo:active, +.btn-vimeo.active, +.open > .dropdown-toggle.btn-vimeo { + color: #ffffff; + background-color: #1295bf; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-vimeo:active, +.btn-vimeo.active, +.open > .dropdown-toggle.btn-vimeo { + background-image: none; +} +.btn-vimeo .badge { + color: #1ab7ea; + background-color: #ffffff; +} +.btn-vk { + color: #ffffff; + background-color: #587ea3; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-vk:focus, +.btn-vk.focus { + color: #ffffff; + background-color: #466482; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-vk:hover { + color: #ffffff; + background-color: #466482; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-vk:active, +.btn-vk.active, +.open > .dropdown-toggle.btn-vk { + color: #ffffff; + background-color: #466482; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-vk:active, +.btn-vk.active, +.open > .dropdown-toggle.btn-vk { + background-image: none; +} +.btn-vk .badge { + color: #587ea3; + background-color: #ffffff; +} +.btn-yahoo { + color: #ffffff; + background-color: #720e9e; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-yahoo:focus, +.btn-yahoo.focus { + color: #ffffff; + background-color: #500a6f; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-yahoo:hover { + color: #ffffff; + background-color: #500a6f; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-yahoo:active, +.btn-yahoo.active, +.open > .dropdown-toggle.btn-yahoo { + color: #ffffff; + background-color: #500a6f; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-yahoo:active, +.btn-yahoo.active, +.open > .dropdown-toggle.btn-yahoo { + background-image: none; +} +.btn-yahoo .badge { + color: #720e9e; + background-color: #ffffff; +} +/* + * Plugin: Full Calendar + * --------------------- + */ +.fc-button { + background: #f4f4f4; + background-image: none; + color: #444; + border-color: #ddd; + border-bottom-color: #ddd; +} +.fc-button:hover, +.fc-button:active, +.fc-button.hover { + background-color: #e9e9e9; +} +.fc-header-title h2 { + font-size: 15px; + line-height: 1.6em; + color: #666; + margin-left: 10px; +} +.fc-header-right { + padding-right: 10px; +} +.fc-header-left { + padding-left: 10px; +} +.fc-widget-header { + background: #fafafa; +} +.fc-grid { + width: 100%; + border: 0; +} +.fc-widget-header:first-of-type, +.fc-widget-content:first-of-type { + border-left: 0; + border-right: 0; +} +.fc-widget-header:last-of-type, +.fc-widget-content:last-of-type { + border-right: 0; +} +.fc-toolbar { + padding: 10px; + margin: 0; +} +.fc-day-number { + font-size: 20px; + font-weight: 300; + padding-right: 10px; +} +.fc-color-picker { + list-style: none; + margin: 0; + padding: 0; +} +.fc-color-picker > li { + float: left; + font-size: 30px; + margin-right: 5px; + line-height: 30px; +} +.fc-color-picker > li .fa { + -webkit-transition: -webkit-transform linear 0.3s; + -moz-transition: -moz-transform linear 0.3s; + -o-transition: -o-transform linear 0.3s; + transition: transform linear 0.3s; +} +.fc-color-picker > li .fa:hover { + -webkit-transform: rotate(30deg); + -ms-transform: rotate(30deg); + -o-transform: rotate(30deg); + transform: rotate(30deg); +} +#add-new-event { + -webkit-transition: all linear 0.3s; + -o-transition: all linear 0.3s; + transition: all linear 0.3s; +} +.external-event { + padding: 5px 10px; + font-weight: bold; + margin-bottom: 4px; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + border-radius: 3px; + cursor: move; +} +.external-event:hover { + box-shadow: inset 0 0 90px rgba(0, 0, 0, 0.2); +} +/* + * Plugin: Select2 + * --------------- + */ +.select2-container--default.select2-container--focus, +.select2-selection.select2-container--focus, +.select2-container--default:focus, +.select2-selection:focus, +.select2-container--default:active, +.select2-selection:active { + outline: none; +} +.select2-container--default .select2-selection--single, +.select2-selection .select2-selection--single { + border: 1px solid #d2d6de; + border-radius: 0; + padding: 6px 12px; + height: 34px; +} +.select2-container--default.select2-container--open { + border-color: #3c8dbc; +} +.select2-dropdown { + border: 1px solid #d2d6de; + border-radius: 0; +} +.select2-container--default .select2-results__option--highlighted[aria-selected] { + background-color: #3c8dbc; + color: white; +} +.select2-results__option { + padding: 6px 12px; + user-select: none; + -webkit-user-select: none; +} +.select2-container .select2-selection--single .select2-selection__rendered { + padding-left: 0; + padding-right: 0; + height: auto; + margin-top: -4px; +} +.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered { + padding-right: 6px; + padding-left: 20px; +} +.select2-container--default .select2-selection--single .select2-selection__arrow { + height: 28px; + right: 3px; +} +.select2-container--default .select2-selection--single .select2-selection__arrow b { + margin-top: 0; +} +.select2-dropdown .select2-search__field, +.select2-search--inline .select2-search__field { + border: 1px solid #d2d6de; +} +.select2-dropdown .select2-search__field:focus, +.select2-search--inline .select2-search__field:focus { + outline: none; + border: 1px solid #3c8dbc; +} +.select2-container--default .select2-results__option[aria-disabled=true] { + color: #999; +} +.select2-container--default .select2-results__option[aria-selected=true] { + background-color: #ddd; +} +.select2-container--default .select2-results__option[aria-selected=true], +.select2-container--default .select2-results__option[aria-selected=true]:hover { + color: #444; +} +.select2-container--default .select2-selection--multiple { + border: 1px solid #d2d6de; + border-radius: 0; +} +.select2-container--default .select2-selection--multiple:focus { + border-color: #3c8dbc; +} +.select2-container--default.select2-container--focus .select2-selection--multiple { + border-color: #d2d6de; +} +.select2-container--default .select2-selection--multiple .select2-selection__choice { + background-color: #3c8dbc; + border-color: #367fa9; + padding: 1px 10px; + color: #fff; +} +.select2-container--default .select2-selection--multiple .select2-selection__choice__remove { + margin-right: 5px; + color: rgba(255, 255, 255, 0.7); +} +.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover { + color: #fff; +} +.select2-container .select2-selection--single .select2-selection__rendered { + padding-right: 10px; +} +/* + * General: Miscellaneous + * ---------------------- + */ +.pad { + padding: 10px; +} +.margin { + margin: 10px; +} +.margin-bottom { + margin-bottom: 20px; +} +.margin-bottom-none { + margin-bottom: 0; +} +.margin-r-5 { + margin-right: 5px; +} +.inline { + display: inline; +} +.description-block { + display: block; + margin: 10px 0; + text-align: center; +} +.description-block.margin-bottom { + margin-bottom: 25px; +} +.description-block > .description-header { + margin: 0; + padding: 0; + font-weight: 600; + font-size: 16px; +} +.description-block > .description-text { + text-transform: uppercase; +} +.bg-red, +.bg-yellow, +.bg-aqua, +.bg-blue, +.bg-light-blue, +.bg-green, +.bg-navy, +.bg-teal, +.bg-olive, +.bg-lime, +.bg-orange, +.bg-fuchsia, +.bg-purple, +.bg-maroon, +.bg-black, +.bg-red-active, +.bg-yellow-active, +.bg-aqua-active, +.bg-blue-active, +.bg-light-blue-active, +.bg-green-active, +.bg-navy-active, +.bg-teal-active, +.bg-olive-active, +.bg-lime-active, +.bg-orange-active, +.bg-fuchsia-active, +.bg-purple-active, +.bg-maroon-active, +.bg-black-active, +.callout.callout-danger, +.callout.callout-warning, +.callout.callout-info, +.callout.callout-success, +.alert-success, +.alert-danger, +.alert-error, +.alert-warning, +.alert-info, +.label-danger, +.label-info, +.label-warning, +.label-primary, +.label-success, +.modal-primary .modal-body, +.modal-primary .modal-header, +.modal-primary .modal-footer, +.modal-warning .modal-body, +.modal-warning .modal-header, +.modal-warning .modal-footer, +.modal-info .modal-body, +.modal-info .modal-header, +.modal-info .modal-footer, +.modal-success .modal-body, +.modal-success .modal-header, +.modal-success .modal-footer, +.modal-danger .modal-body, +.modal-danger .modal-header, +.modal-danger .modal-footer { + color: #fff !important; +} +.bg-gray { + color: #000; + background-color: #d2d6de !important; +} +.bg-gray-light { + background-color: #f7f7f7; +} +.bg-black { + background-color: #111111 !important; +} +.bg-red, +.callout.callout-danger, +.alert-danger, +.alert-error, +.label-danger, +.modal-danger .modal-body { + background-color: #dd4b39 !important; +} +.bg-yellow, +.callout.callout-warning, +.alert-warning, +.label-warning, +.modal-warning .modal-body { + background-color: #f39c12 !important; +} +.bg-aqua, +.callout.callout-info, +.alert-info, +.label-info, +.modal-info .modal-body { + background-color: #00c0ef !important; +} +.bg-blue { + background-color: #0073b7 !important; +} +.bg-light-blue, +.label-primary, +.modal-primary .modal-body { + background-color: #3c8dbc !important; +} +.bg-green, +.callout.callout-success, +.alert-success, +.label-success, +.modal-success .modal-body { + background-color: #00a65a !important; +} +.bg-navy { + background-color: #001f3f !important; +} +.bg-teal { + background-color: #39cccc !important; +} +.bg-olive { + background-color: #3d9970 !important; +} +.bg-lime { + background-color: #01ff70 !important; +} +.bg-orange { + background-color: #ff851b !important; +} +.bg-fuchsia { + background-color: #f012be !important; +} +.bg-purple { + background-color: #605ca8 !important; +} +.bg-maroon { + background-color: #d81b60 !important; +} +.bg-gray-active { + color: #000; + background-color: #b5bbc8 !important; +} +.bg-black-active { + background-color: #000000 !important; +} +.bg-red-active, +.modal-danger .modal-header, +.modal-danger .modal-footer { + background-color: #d33724 !important; +} +.bg-yellow-active, +.modal-warning .modal-header, +.modal-warning .modal-footer { + background-color: #db8b0b !important; +} +.bg-aqua-active, +.modal-info .modal-header, +.modal-info .modal-footer { + background-color: #00a7d0 !important; +} +.bg-blue-active { + background-color: #005384 !important; +} +.bg-light-blue-active, +.modal-primary .modal-header, +.modal-primary .modal-footer { + background-color: #357ca5 !important; +} +.bg-green-active, +.modal-success .modal-header, +.modal-success .modal-footer { + background-color: #008d4c !important; +} +.bg-navy-active { + background-color: #001a35 !important; +} +.bg-teal-active { + background-color: #30bbbb !important; +} +.bg-olive-active { + background-color: #368763 !important; +} +.bg-lime-active { + background-color: #00e765 !important; +} +.bg-orange-active { + background-color: #ff7701 !important; +} +.bg-fuchsia-active { + background-color: #db0ead !important; +} +.bg-purple-active { + background-color: #555299 !important; +} +.bg-maroon-active { + background-color: #ca195a !important; +} +[class^="bg-"].disabled { + opacity: 0.65; + filter: alpha(opacity=65); +} +.text-red { + color: #dd4b39 !important; +} +.text-yellow { + color: #f39c12 !important; +} +.text-aqua { + color: #00c0ef !important; +} +.text-blue { + color: #0073b7 !important; +} +.text-black { + color: #111111 !important; +} +.text-light-blue { + color: #3c8dbc !important; +} +.text-green { + color: #00a65a !important; +} +.text-gray { + color: #d2d6de !important; +} +.text-navy { + color: #001f3f !important; +} +.text-teal { + color: #39cccc !important; +} +.text-olive { + color: #3d9970 !important; +} +.text-lime { + color: #01ff70 !important; +} +.text-orange { + color: #ff851b !important; +} +.text-fuchsia { + color: #f012be !important; +} +.text-purple { + color: #605ca8 !important; +} +.text-maroon { + color: #d81b60 !important; +} +.link-muted { + color: #7a869d; +} +.link-muted:hover, +.link-muted:focus { + color: #606c84; +} +.link-black { + color: #666; +} +.link-black:hover, +.link-black:focus { + color: #999; +} +.hide { + display: none !important; +} +.no-border { + border: 0 !important; +} +.no-padding { + padding: 0 !important; +} +.no-margin { + margin: 0 !important; +} +.no-shadow { + box-shadow: none !important; +} +.list-unstyled, +.chart-legend, +.contacts-list, +.users-list, +.mailbox-attachments { + list-style: none; + margin: 0; + padding: 0; +} +.list-group-unbordered > .list-group-item { + border-left: 0; + border-right: 0; + border-radius: 0; + padding-left: 0; + padding-right: 0; +} +.flat { + border-radius: 0 !important; +} +.text-bold, +.text-bold.table td, +.text-bold.table th { + font-weight: 700; +} +.text-sm { + font-size: 12px; +} +.jqstooltip { + padding: 5px !important; + width: auto !important; + height: auto !important; +} +.bg-teal-gradient { + background: #39cccc !important; + background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #39cccc), color-stop(1, #7adddd)) !important; + background: -ms-linear-gradient(bottom, #39cccc, #7adddd) !important; + background: -moz-linear-gradient(center bottom, #39cccc 0%, #7adddd 100%) !important; + background: -o-linear-gradient(#7adddd, #39cccc) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#7adddd', endColorstr='#39cccc', GradientType=0) !important; + color: #fff; +} +.bg-light-blue-gradient { + background: #3c8dbc !important; + background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #3c8dbc), color-stop(1, #67a8ce)) !important; + background: -ms-linear-gradient(bottom, #3c8dbc, #67a8ce) !important; + background: -moz-linear-gradient(center bottom, #3c8dbc 0%, #67a8ce 100%) !important; + background: -o-linear-gradient(#67a8ce, #3c8dbc) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#67a8ce', endColorstr='#3c8dbc', GradientType=0) !important; + color: #fff; +} +.bg-blue-gradient { + background: #0073b7 !important; + background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #0073b7), color-stop(1, #0089db)) !important; + background: -ms-linear-gradient(bottom, #0073b7, #0089db) !important; + background: -moz-linear-gradient(center bottom, #0073b7 0%, #0089db 100%) !important; + background: -o-linear-gradient(#0089db, #0073b7) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0089db', endColorstr='#0073b7', GradientType=0) !important; + color: #fff; +} +.bg-aqua-gradient { + background: #00c0ef !important; + background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #00c0ef), color-stop(1, #14d1ff)) !important; + background: -ms-linear-gradient(bottom, #00c0ef, #14d1ff) !important; + background: -moz-linear-gradient(center bottom, #00c0ef 0%, #14d1ff 100%) !important; + background: -o-linear-gradient(#14d1ff, #00c0ef) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#14d1ff', endColorstr='#00c0ef', GradientType=0) !important; + color: #fff; +} +.bg-yellow-gradient { + background: #f39c12 !important; + background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #f39c12), color-stop(1, #f7bc60)) !important; + background: -ms-linear-gradient(bottom, #f39c12, #f7bc60) !important; + background: -moz-linear-gradient(center bottom, #f39c12 0%, #f7bc60 100%) !important; + background: -o-linear-gradient(#f7bc60, #f39c12) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f7bc60', endColorstr='#f39c12', GradientType=0) !important; + color: #fff; +} +.bg-purple-gradient { + background: #605ca8 !important; + background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #605ca8), color-stop(1, #9491c4)) !important; + background: -ms-linear-gradient(bottom, #605ca8, #9491c4) !important; + background: -moz-linear-gradient(center bottom, #605ca8 0%, #9491c4 100%) !important; + background: -o-linear-gradient(#9491c4, #605ca8) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#9491c4', endColorstr='#605ca8', GradientType=0) !important; + color: #fff; +} +.bg-green-gradient { + background: #00a65a !important; + background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #00a65a), color-stop(1, #00ca6d)) !important; + background: -ms-linear-gradient(bottom, #00a65a, #00ca6d) !important; + background: -moz-linear-gradient(center bottom, #00a65a 0%, #00ca6d 100%) !important; + background: -o-linear-gradient(#00ca6d, #00a65a) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ca6d', endColorstr='#00a65a', GradientType=0) !important; + color: #fff; +} +.bg-red-gradient { + background: #dd4b39 !important; + background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #dd4b39), color-stop(1, #e47365)) !important; + background: -ms-linear-gradient(bottom, #dd4b39, #e47365) !important; + background: -moz-linear-gradient(center bottom, #dd4b39 0%, #e47365 100%) !important; + background: -o-linear-gradient(#e47365, #dd4b39) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#e47365', endColorstr='#dd4b39', GradientType=0) !important; + color: #fff; +} +.bg-black-gradient { + background: #111111 !important; + background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #111111), color-stop(1, #2b2b2b)) !important; + background: -ms-linear-gradient(bottom, #111111, #2b2b2b) !important; + background: -moz-linear-gradient(center bottom, #111111 0%, #2b2b2b 100%) !important; + background: -o-linear-gradient(#2b2b2b, #111111) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#2b2b2b', endColorstr='#111111', GradientType=0) !important; + color: #fff; +} +.bg-maroon-gradient { + background: #d81b60 !important; + background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #d81b60), color-stop(1, #e73f7c)) !important; + background: -ms-linear-gradient(bottom, #d81b60, #e73f7c) !important; + background: -moz-linear-gradient(center bottom, #d81b60 0%, #e73f7c 100%) !important; + background: -o-linear-gradient(#e73f7c, #d81b60) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#e73f7c', endColorstr='#d81b60', GradientType=0) !important; + color: #fff; +} +.description-block .description-icon { + font-size: 16px; +} +.no-pad-top { + padding-top: 0; +} +.position-static { + position: static !important; +} +.list-header { + font-size: 15px; + padding: 10px 4px; + font-weight: bold; + color: #666; +} +.list-seperator { + height: 1px; + background: #f4f4f4; + margin: 15px 0 9px 0; +} +.list-link > a { + padding: 4px; + color: #777; +} +.list-link > a:hover { + color: #222; +} +.font-light { + font-weight: 300; +} +.user-block:before, +.user-block:after { + content: " "; + display: table; +} +.user-block:after { + clear: both; +} +.user-block img { + width: 40px; + height: 40px; + float: left; +} +.user-block .username, +.user-block .description, +.user-block .comment { + display: block; + margin-left: 50px; +} +.user-block .username { + font-size: 16px; + font-weight: 600; +} +.user-block .description { + color: #999; + font-size: 13px; +} +.user-block.user-block-sm .username, +.user-block.user-block-sm .description, +.user-block.user-block-sm .comment { + margin-left: 40px; +} +.user-block.user-block-sm .username { + font-size: 14px; +} +.img-sm, +.img-md, +.img-lg, +.box-comments .box-comment img, +.user-block.user-block-sm img { + float: left; +} +.img-sm, +.box-comments .box-comment img, +.user-block.user-block-sm img { + width: 30px !important; + height: 30px !important; +} +.img-sm + .img-push { + margin-left: 40px; +} +.img-md { + width: 60px; + height: 60px; +} +.img-md + .img-push { + margin-left: 70px; +} +.img-lg { + width: 100px; + height: 100px; +} +.img-lg + .img-push { + margin-left: 110px; +} +.img-bordered { + border: 3px solid #d2d6de; + padding: 3px; +} +.img-bordered-sm { + border: 2px solid #d2d6de; + padding: 2px; +} +.attachment-block { + border: 1px solid #f4f4f4; + padding: 5px; + margin-bottom: 10px; + background: #f7f7f7; +} +.attachment-block .attachment-img { + max-width: 100px; + max-height: 100px; + height: auto; + float: left; +} +.attachment-block .attachment-pushed { + margin-left: 110px; +} +.attachment-block .attachment-heading { + margin: 0; +} +.attachment-block .attachment-text { + color: #555; +} +.connectedSortable { + min-height: 100px; +} +.ui-helper-hidden-accessible { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} +.sort-highlight { + background: #f4f4f4; + border: 1px dashed #ddd; + margin-bottom: 10px; +} +.full-opacity-hover { + opacity: 0.65; + filter: alpha(opacity=65); +} +.full-opacity-hover:hover { + opacity: 1; + filter: alpha(opacity=100); +} +.chart { + position: relative; + overflow: hidden; + width: 100%; +} +.chart svg, +.chart canvas { + width: 100% !important; +} +/* + * Misc: print + * ----------- + */ +@media print { + .no-print, + .main-sidebar, + .left-side, + .main-header, + .content-header { + display: none !important; + } + .content-wrapper, + .right-side, + .main-footer { + margin-left: 0 !important; + min-height: 0 !important; + -webkit-transform: translate(0, 0) !important; + -ms-transform: translate(0, 0) !important; + -o-transform: translate(0, 0) !important; + transform: translate(0, 0) !important; + } + .fixed .content-wrapper, + .fixed .right-side { + padding-top: 0 !important; + } + .invoice { + width: 100%; + border: 0; + margin: 0; + padding: 0; + } + .invoice-col { + float: left; + width: 33.3333333%; + } + .table-responsive { + overflow: auto; + } + .table-responsive > .table tr th, + .table-responsive > .table tr td { + white-space: normal !important; + } +} diff --git a/login/app/sprinkles/core/assets/userfrosting/css/tablesorter-reflow.css b/login/app/sprinkles/core/assets/userfrosting/css/tablesorter-reflow.css new file mode 100755 index 0000000..3a9c14f --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/css/tablesorter-reflow.css @@ -0,0 +1,61 @@ +/* REQUIRED CSS: change your reflow breakpoint here (35em below) */ +@media ( max-width: 35em ) { + + /* uncomment out the line below if you don't want the sortable headers to show */ + table.ui-table-reflow thead { display: none; } + + .uf-table-search { + display: inherit; + } + + /* css for reflow & reflow2 widgets */ + .ui-table-reflow td, + .ui-table-reflow th { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + float: right; + /* if not using the stickyHeaders widget (not the css3 version) + * the "!important" flag, and "height: auto" can be removed */ + width: 100% !important; + height: auto !important; + } + + .tablesorter-bootstrap > thead > tr > th, + .tablesorter-bootstrap > thead > tr > td, + .tablesorter-bootstrap > tfoot > tr > th, + .tablesorter-bootstrap > tfoot > tr > td { + padding: 4px; + margin: 0; + } + + /* reflow widget only */ + .ui-table-reflow tbody td[data-title]:not(:first-child):before { + color: #469; + font-size: 1.5em; + content: attr(data-title); + width: 100%; + white-space: pre; + display: block; + } + + /* reflow2 widget only */ + table.ui-table-reflow .ui-table-cell-label { + display: none; + } + + table.ui-table-reflow .ui-table-cell-label.ui-table-cell-label-top { + display: block; + padding: .4em 0; + margin: .4em 0; + text-transform: uppercase; + font-size: 1.5em; + font-weight: 400; + } + +} /* end media query */ + +/* reflow2 widget */ +.ui-table-reflow .ui-table-cell-label { + display: none; +} diff --git a/login/app/sprinkles/core/assets/userfrosting/css/uf-alerts.css b/login/app/sprinkles/core/assets/userfrosting/css/uf-alerts.css new file mode 100755 index 0000000..7ac5a97 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/css/uf-alerts.css @@ -0,0 +1,23 @@ +.uf-alert +{ + margin: 10px 0px 0px 0px; +} + +/* Invert margin when inside a modal-body */ +.modal-body .uf-alert +{ + margin: 0px 0px 10px 0px; +} + +.uf-alert .icon { + line-height: 20px; +} + +.uf-alert-message-container { + margin-left: 30px; + line-height: 20px; +} + +.uf-alert-message-container > ul { + margin-left: -20px; +}
\ No newline at end of file diff --git a/login/app/sprinkles/core/assets/userfrosting/css/uf-collection.css b/login/app/sprinkles/core/assets/userfrosting/css/uf-collection.css new file mode 100755 index 0000000..432e1a4 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/css/uf-collection.css @@ -0,0 +1,15 @@ +/** + * Custom CSS for the ufCollection widget. + */ + +/** Prevent rows from overflowing container */ +.uf-collection-col-wrap { + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-all; +} + +.uf-collection span.select2-dropdown.select2-dropdown--below { + position:absolute; + top: -32px; +} diff --git a/login/app/sprinkles/core/assets/userfrosting/css/uf-jqueryvalidation.css b/login/app/sprinkles/core/assets/userfrosting/css/uf-jqueryvalidation.css new file mode 100755 index 0000000..6fd2e47 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/css/uf-jqueryvalidation.css @@ -0,0 +1,10 @@ +/* ========================================================================== + Custom styles for jQuery Validation elements + ========================================================================== */ + +.error-block { + margin-top: 5px; + margin-bottom: 10px; + + color: #a94442; +} diff --git a/login/app/sprinkles/core/assets/userfrosting/css/userfrosting.css b/login/app/sprinkles/core/assets/userfrosting/css/userfrosting.css new file mode 100755 index 0000000..38a92e2 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/css/userfrosting.css @@ -0,0 +1,204 @@ +/* Use to collapse empty button columns */ + +.hideable { + min-height: 0px; +} + +/* Custom styles for bootstrap-switch labels */ +label.label-switch { + margin-top: 5px; +} + +form textarea { + resize: vertical; /* user can resize vertically, but width is fixed */ +} + +/* Custom styling for table tool menus */ +.dropdown-toggle .caret { + position: relative; + top: -1px; + right: -3px; +} + +.box-tools .dropdown-toggle { + background-color: #ebeef5; + box-shadow: 1px 1px 15px 0px rgba(155, 155, 155, 0.1); +} + +/* Add some extra padding to tool groups in boxes */ +.box-tool-group { + padding: 10px 0 10px; +} + +.box-tool-group .btn-app { + margin: 0 10px 10px 0; +} + +.box-tool-menu { + background-color: #f4f4f4; +} + +.box-tool-menu > li > a { + color: #444; +} + +.box-profile { + padding: 10px 20px; +} + +.box-profile-property { + float: right !important; +} + +/* Styles for table column selectors */ + +.uf-table-cs-title { + padding: 0px 20px; + font-weight: bold; +} + +.uf-table-cs-options { + padding: 3px 20px; +} + +.uf-table-cs-options label { + width: 100%; + font-weight: normal; + word-wrap: break-word; + word-break: break-all; +} + +.uf-table-cs-options label span { + padding-left: 10px; +} + +/* Table column width, fit to content */ +.uf-table-fit-width { + width: 1%; + white-space: nowrap; +} + +/* Hide global search field on larger devices */ +.uf-table-search { + display: none; +} + +/* Special styling for uf-table messages */ +.uf-table-info { + background-color: #fff !important; + color: #cdcdcd; + font-weight: 700; + text-align: center; +} + +.uf-table-error-row td { + background-color: #f39c12 !important; + color: #fff !important; + text-align: center; +} + +/* Cursor styling for ufCopy */ +.uf-copy-trigger { + cursor: pointer; +} + +/* Special wrapper for alerts on public form pages (sign-in, register, etc) */ +.form-alerts { + margin-bottom: 10px !important; +} + +/* Styling for AdminLTE form control icons, that doesn't interfere with validation plugin */ +.form-control-icon { + position: absolute; + top: 0; + right: 0; + z-index: 2; + display: block; + width: 34px; + height: 34px; + line-height: 34px; + text-align: center; + pointer-events: none; +} + +.input-lg ~ .form-control-icon, +.input-group-lg ~ .form-control-icon, +.form-group-lg .form-control ~ .form-control-icon { + width: 46px; + height: 46px; + line-height: 46px; +} +.input-sm ~ .form-control-icon, +.input-group-sm ~ .form-control-icon, +.form-group-sm .form-control ~ .form-control-icon { + width: 30px; + height: 30px; + line-height: 30px; +} + +@media (min-width: 768px) { + .form-inline .has-feedback .form-control-icon, + .navbar-form .has-feedback .form-control-icon { + top: 0; + } +} + +.form-horizontal .has-feedback .form-control-icon { + right: 15px; +} + +/* Fix for select2 not resizing properly when the page is resized */ +.select2-container--default { + display: block; + width: 100% !important; +} + +/* Fix for links on info-box */ +a .info-box { + color: black; +} + +/* Custom styling for trash buttons */ +.btn-trash { + padding: 0 10px; + color: #777; +} + +.btn-trash:hover { + color: #d73925; +} + +/* Custom styling for nav logo */ +.main-header .logo { + font-weight: 400; + font-size: 18px; +} + +/* Padding for user menu */ +.user-menu .fa { + padding-left: .5em; + font-size: 11px; +} + +/* Custom classes to add responsiveness to bootstrap's dropdown-menu-right and dropdown-menu-left */ + +.dropdown-menu-right-responsive { + right: auto; + left: 0; +} + +.dropdown-menu-left-responsive { + right: 0; + left: auto; +} + +@media(min-width: 561px) { + .dropdown-menu-right-responsive { + right: 0; + left: auto; + } + .dropdown-menu-left-responsive { + right: auto; + left: 0; + } +} diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/README.md b/login/app/sprinkles/core/assets/userfrosting/favicons/README.md new file mode 100755 index 0000000..c756d36 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/README.md @@ -0,0 +1,7 @@ +# Default Favicons for UserFrosting + +There are a bewildering number of device- and vendor-specific favicons these days. And, the specifications are changing constantly as new devices with new screen resolutions are released, and old devices and operating systems are retired. + +We recommend the [Favicon Cheat Sheet](https://github.com/audreyr/favicon-cheat-sheet) as a way to stay up-to-date with current specifications and best practices regarding favicons. + +To automatically generate favicons for a wide variety of devices, platforms, and operating systems, we suggest https://realfavicongenerator.net. The default icons you see in this directory were generated using this site. diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-144x144.png b/login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-144x144.png Binary files differnew file mode 100755 index 0000000..d2b133f --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-144x144.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-192x192.png b/login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-192x192.png Binary files differnew file mode 100755 index 0000000..f6877ef --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-192x192.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-256x256.png b/login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-256x256.png Binary files differnew file mode 100755 index 0000000..f0dcd45 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-256x256.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-36x36.png b/login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-36x36.png Binary files differnew file mode 100755 index 0000000..03b3f7d --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-36x36.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-384x384.png b/login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-384x384.png Binary files differnew file mode 100755 index 0000000..cdb7f6d --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-384x384.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-48x48.png b/login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-48x48.png Binary files differnew file mode 100755 index 0000000..dea6333 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-48x48.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-512x512.png b/login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-512x512.png Binary files differnew file mode 100755 index 0000000..292cc0b --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-512x512.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-72x72.png b/login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-72x72.png Binary files differnew file mode 100755 index 0000000..2a12eae --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-72x72.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-96x96.png b/login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-96x96.png Binary files differnew file mode 100755 index 0000000..be2007b --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/android-chrome-96x96.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-114x114-precomposed.png b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-114x114-precomposed.png Binary files differnew file mode 100755 index 0000000..9cc0516 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-114x114-precomposed.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-114x114.png b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-114x114.png Binary files differnew file mode 100755 index 0000000..ee79201 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-114x114.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-120x120-precomposed.png b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-120x120-precomposed.png Binary files differnew file mode 100755 index 0000000..c99a379 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-120x120-precomposed.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-120x120.png b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-120x120.png Binary files differnew file mode 100755 index 0000000..097a9af --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-120x120.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-144x144-precomposed.png b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-144x144-precomposed.png Binary files differnew file mode 100755 index 0000000..1ca7d71 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-144x144-precomposed.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-144x144.png b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-144x144.png Binary files differnew file mode 100755 index 0000000..af016ec --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-144x144.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-152x152-precomposed.png b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-152x152-precomposed.png Binary files differnew file mode 100755 index 0000000..4ed4ec6 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-152x152-precomposed.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-152x152.png b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-152x152.png Binary files differnew file mode 100755 index 0000000..e08a1c2 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-152x152.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-180x180-precomposed.png b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-180x180-precomposed.png Binary files differnew file mode 100755 index 0000000..1b1d093 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-180x180-precomposed.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-180x180.png b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-180x180.png Binary files differnew file mode 100755 index 0000000..d2e5117 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-180x180.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-57x57-precomposed.png b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-57x57-precomposed.png Binary files differnew file mode 100755 index 0000000..b836367 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-57x57-precomposed.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-57x57.png b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-57x57.png Binary files differnew file mode 100755 index 0000000..d1628ca --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-57x57.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-60x60-precomposed.png b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-60x60-precomposed.png Binary files differnew file mode 100755 index 0000000..7c85a57 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-60x60-precomposed.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-60x60.png b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-60x60.png Binary files differnew file mode 100755 index 0000000..011967e --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-60x60.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-72x72-precomposed.png b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-72x72-precomposed.png Binary files differnew file mode 100755 index 0000000..dcb3f7c --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-72x72-precomposed.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-72x72.png b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-72x72.png Binary files differnew file mode 100755 index 0000000..bd57115 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-72x72.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-76x76-precomposed.png b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-76x76-precomposed.png Binary files differnew file mode 100755 index 0000000..8c919b8 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-76x76-precomposed.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-76x76.png b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-76x76.png Binary files differnew file mode 100755 index 0000000..e96533b --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-76x76.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-precomposed.png b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-precomposed.png Binary files differnew file mode 100755 index 0000000..1b1d093 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon-precomposed.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon.png b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon.png Binary files differnew file mode 100755 index 0000000..d2e5117 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/apple-touch-icon.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/favicon-16x16.png b/login/app/sprinkles/core/assets/userfrosting/favicons/favicon-16x16.png Binary files differnew file mode 100755 index 0000000..a705675 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/favicon-16x16.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/favicon-32x32.png b/login/app/sprinkles/core/assets/userfrosting/favicons/favicon-32x32.png Binary files differnew file mode 100755 index 0000000..0d89d12 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/favicon-32x32.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/favicon.ico b/login/app/sprinkles/core/assets/userfrosting/favicons/favicon.ico Binary files differnew file mode 100755 index 0000000..a787724 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/favicon.ico diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/ieconfig.xml b/login/app/sprinkles/core/assets/userfrosting/favicons/ieconfig.xml new file mode 100755 index 0000000..21fa5ad --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/ieconfig.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?>
+<browserconfig>
+ <msapplication>
+ <tile>
+ <square70x70logo src="/mstile-70x70.png"/>
+ <square150x150logo src="/mstile-150x150.png"/>
+ <square310x310logo src="/mstile-310x310.png"/>
+ <wide310x150logo src="/mstile-310x150.png"/>
+ <TileColor>#603cba</TileColor>
+ </tile>
+ </msapplication>
+</browserconfig>
diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/manifest.json b/login/app/sprinkles/core/assets/userfrosting/favicons/manifest.json new file mode 100755 index 0000000..328cd7a --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/manifest.json @@ -0,0 +1,51 @@ +{ + "name": "UserFrosting", + "icons": [ + { + "src": "\/android-chrome-36x36.png", + "sizes": "36x36", + "type": "image\/png" + }, + { + "src": "\/android-chrome-48x48.png", + "sizes": "48x48", + "type": "image\/png" + }, + { + "src": "\/android-chrome-72x72.png", + "sizes": "72x72", + "type": "image\/png" + }, + { + "src": "\/android-chrome-96x96.png", + "sizes": "96x96", + "type": "image\/png" + }, + { + "src": "\/android-chrome-144x144.png", + "sizes": "144x144", + "type": "image\/png" + }, + { + "src": "\/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image\/png" + }, + { + "src": "\/android-chrome-256x256.png", + "sizes": "256x256", + "type": "image\/png" + }, + { + "src": "\/android-chrome-384x384.png", + "sizes": "384x384", + "type": "image\/png" + }, + { + "src": "\/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image\/png" + } + ], + "theme_color": "#f3f2e4" +} diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/mstile-144x144.png b/login/app/sprinkles/core/assets/userfrosting/favicons/mstile-144x144.png Binary files differnew file mode 100755 index 0000000..3227cbb --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/mstile-144x144.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/mstile-150x150.png b/login/app/sprinkles/core/assets/userfrosting/favicons/mstile-150x150.png Binary files differnew file mode 100755 index 0000000..e8f3a69 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/mstile-150x150.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/mstile-310x150.png b/login/app/sprinkles/core/assets/userfrosting/favicons/mstile-310x150.png Binary files differnew file mode 100755 index 0000000..b14af89 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/mstile-310x150.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/mstile-310x310.png b/login/app/sprinkles/core/assets/userfrosting/favicons/mstile-310x310.png Binary files differnew file mode 100755 index 0000000..cabb0b9 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/mstile-310x310.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/mstile-70x70.png b/login/app/sprinkles/core/assets/userfrosting/favicons/mstile-70x70.png Binary files differnew file mode 100755 index 0000000..7ae9615 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/mstile-70x70.png diff --git a/login/app/sprinkles/core/assets/userfrosting/favicons/safari-pinned-tab.svg b/login/app/sprinkles/core/assets/userfrosting/favicons/safari-pinned-tab.svg new file mode 100755 index 0000000..2435014 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/favicons/safari-pinned-tab.svg @@ -0,0 +1,246 @@ +<?xml version="1.0" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" + "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"> +<svg version="1.0" xmlns="http://www.w3.org/2000/svg" + width="2061.000000pt" height="2061.000000pt" viewBox="0 0 2061.000000 2061.000000" + preserveAspectRatio="xMidYMid meet"> +<metadata> +Created by potrace 1.11, written by Peter Selinger 2001-2013 +</metadata> +<g transform="translate(0.000000,2061.000000) scale(0.100000,-0.100000)" +fill="#000000" stroke="none"> +<path d="M11924 20602 c-23 -15 -154 -225 -166 -265 -6 -23 -24 -56 -38 -73 +-29 -34 -77 -193 -104 -344 -34 -185 -47 -488 -25 -547 13 -34 59 -84 71 -77 +4 3 8 38 8 77 0 40 5 135 10 212 27 360 96 596 235 800 53 77 85 145 85 180 0 +34 -47 57 -76 37z"/> +<path d="M9482 18775 c-113 -29 -217 -78 -303 -142 l-64 -48 56 -8 c48 -8 63 +-6 102 12 25 12 77 28 114 37 126 28 178 67 197 147 8 32 14 32 -102 2z"/> +<path d="M11641 18720 c-26 -43 -26 -66 -1 -132 22 -58 26 -118 10 -148 -15 +-28 -47 -42 -110 -49 l-55 -6 74 -24 c40 -14 146 -60 235 -104 236 -116 321 +-152 442 -187 137 -39 306 -121 410 -197 110 -83 250 -233 334 -360 40 -60 +107 -180 151 -268 43 -88 97 -189 119 -224 38 -62 45 -86 41 -141 -4 -50 -6 +-51 -83 -24 -190 65 -447 128 -711 173 -222 39 -321 52 -786 106 -409 48 -450 +54 -774 115 -115 22 -234 40 -264 40 l-56 0 6 -37 c4 -24 16 -46 33 -59 28 +-23 111 -49 183 -59 24 -3 56 -12 71 -20 30 -15 52 -62 43 -93 -2 -10 0 -37 6 +-60 15 -53 84 -117 269 -247 78 -55 142 -107 142 -115 0 -9 -7 -26 -16 -38 +-13 -18 -24 -22 -72 -22 -31 0 -86 9 -120 20 -35 11 -78 20 -95 20 -35 0 -117 +-34 -117 -48 0 -8 738 -232 1040 -316 333 -93 675 -172 1440 -336 644 -138 +744 -162 803 -191 55 -27 104 -74 129 -123 21 -41 48 -152 48 -200 l0 -28 +-122 6 c-973 50 -1322 53 -1676 16 -122 -13 -152 -25 -152 -57 0 -41 41 -69 +146 -102 390 -119 938 -257 1284 -322 61 -11 120 -42 103 -52 -9 -5 -129 -25 +-345 -56 -138 -21 -299 -66 -305 -88 -2 -6 5 -14 17 -18 12 -4 19 -12 17 -18 +-10 -28 -164 -5 -587 84 -543 115 -765 160 -1025 204 -116 20 -250 44 -300 55 +-141 30 -162 32 -206 20 -33 -9 -43 -18 -56 -47 -13 -33 -13 -38 5 -62 30 -41 +76 -57 321 -116 405 -97 628 -136 1326 -235 237 -34 484 -70 550 -81 66 -10 +123 -19 127 -19 4 0 10 -10 13 -22 12 -40 55 -87 96 -109 23 -11 81 -29 131 +-40 94 -20 152 -41 161 -56 3 -5 -343 -19 -811 -32 -449 -13 -819 -26 -822 +-28 -9 -8 33 -33 64 -37 537 -77 1265 -146 2021 -192 276 -17 736 -15 852 4 +33 5 56 5 63 -2 6 -6 17 -6 32 1 13 6 56 14 96 17 73 6 73 6 109 -29 45 -43 +110 -68 253 -95 74 -14 144 -35 204 -62 50 -22 132 -54 181 -72 71 -26 93 -38 +103 -59 19 -39 -2 -78 -48 -90 -56 -15 -465 -1 -665 23 -99 11 -297 37 -440 +56 -391 52 -509 64 -755 74 -186 8 -267 16 -465 49 -132 22 -314 49 -405 61 +-312 38 -1083 202 -1157 246 -37 21 -53 24 -152 24 -127 0 -136 -8 -78 -64 25 +-26 78 -49 260 -115 273 -98 401 -132 602 -161 176 -25 297 -51 611 -131 259 +-65 460 -105 608 -118 78 -7 102 -13 113 -27 19 -25 17 -32 -15 -48 -16 -8 +-48 -40 -72 -71 -62 -80 -142 -150 -195 -171 -39 -15 -53 -15 -99 -5 -63 14 +-179 80 -261 150 -33 28 -75 59 -93 68 -31 16 -35 16 -67 -1 -40 -22 -60 -63 +-51 -109 9 -49 80 -190 141 -282 29 -44 97 -132 150 -195 53 -63 123 -147 156 +-187 l60 -72 -40 -20 c-72 -37 -104 -36 -286 8 -94 22 -307 63 -475 91 -168 +27 -394 68 -504 90 -288 58 -531 100 -1066 185 -695 110 -993 165 -1355 251 +-162 39 -205 40 -253 6 l-28 -21 -493 185 c-272 102 -496 182 -500 178 -11 +-11 97 -102 166 -140 105 -59 531 -236 993 -414 680 -261 1396 -498 2105 -695 +554 -154 1403 -355 1503 -355 104 0 150 -42 122 -111 -27 -65 -169 -104 -410 +-114 -488 -19 -1114 97 -1990 370 -366 115 -1178 393 -1218 419 -22 13 -58 29 +-80 36 -42 11 -92 5 -92 -11 0 -12 84 -59 134 -74 24 -7 79 -33 122 -58 421 +-238 1013 -477 1619 -652 498 -144 1352 -337 1965 -444 150 -26 1044 -171 +1056 -171 2 0 4 -7 4 -15 0 -26 53 -74 105 -94 163 -62 375 -155 422 -183 29 +-18 70 -49 90 -70 l37 -38 -31 0 c-17 0 -44 6 -60 14 -20 11 -145 22 -447 41 +-230 14 -419 25 -421 23 -1 -2 -5 -13 -9 -24 -6 -20 -1 -23 56 -36 602 -136 +1408 -324 1408 -329 0 -12 -58 -31 -92 -31 -18 0 -80 8 -137 17 -189 32 -381 +8 -561 -68 -72 -31 -79 -32 -160 -25 -47 4 -404 57 -794 118 -1439 225 -1626 +249 -2466 319 -679 57 -687 57 -718 34 -15 -11 -28 -27 -30 -35 -6 -29 253 +-128 333 -128 13 0 28 -6 34 -14 6 -8 31 -17 55 -20 38 -6 44 -10 50 -37 9 +-47 44 -72 146 -108 199 -69 337 -148 445 -254 89 -87 98 -141 31 -192 -81 +-62 -509 -215 -1061 -380 -176 -52 -378 -118 -450 -146 -169 -66 -273 -93 +-339 -86 -44 5 -68 -1 -171 -40 -275 -104 -834 -281 -1145 -363 -184 -48 -208 +-58 -233 -95 -20 -32 -23 -33 -112 -39 -727 -56 -1440 -59 -2045 -11 -510 41 +-1079 132 -1514 241 -191 48 -298 83 -536 176 -169 66 -209 85 -230 111 -49 +63 -123 137 -176 178 -53 41 -224 133 -231 125 -11 -11 46 -96 96 -146 58 -59 +146 -113 249 -155 43 -18 75 -41 114 -81 29 -31 93 -88 142 -126 98 -76 321 +-216 376 -236 336 -123 583 -181 925 -217 231 -24 745 -38 1190 -31 899 14 +1535 56 2435 161 331 39 1117 120 1255 130 50 3 110 13 135 20 25 8 162 30 +305 50 143 20 355 49 470 65 116 16 320 41 455 55 135 14 405 50 600 80 426 +66 573 84 746 92 l130 6 80 -42 c250 -131 504 -389 630 -638 80 -160 41 -273 +-114 -327 -66 -24 -180 -35 -523 -51 -361 -18 -352 -17 -1134 -95 -324 -33 +-705 -68 -845 -80 -868 -70 -1174 -130 -1670 -325 -195 -77 -342 -125 -685 +-223 -324 -93 -764 -212 -784 -212 -10 0 -44 9 -77 20 -53 19 -223 47 -1249 +210 -334 53 -386 63 -570 115 -169 48 -548 171 -584 189 -59 31 -101 16 -101 +-36 0 -33 2 -33 -139 -12 -275 40 -478 117 -649 246 -36 28 -124 77 -195 109 +-361 166 -641 320 -867 478 -267 187 -384 240 -594 267 -124 16 -189 42 -289 +116 l-58 43 30 7 c16 5 45 26 64 48 l34 40 646 -294 c356 -161 659 -304 673 +-317 24 -22 33 -24 121 -23 95 2 97 1 121 -27 19 -23 27 -27 38 -18 22 18 17 +26 -20 39 -23 7 -49 30 -78 67 -49 64 -137 141 -728 633 -234 195 -567 476 +-740 625 -402 346 -730 619 -1362 1133 -278 227 -530 439 -560 470 -66 72 +-122 177 -144 268 -42 179 29 596 151 877 6 13 2 17 -17 17 -39 0 -136 -54 +-218 -120 -41 -34 -100 -82 -131 -106 -91 -73 -100 -116 -43 -210 118 -194 +140 -281 112 -445 -22 -134 21 -260 127 -369 23 -25 46 -59 50 -77 3 -17 12 +-34 19 -36 7 -3 16 -19 19 -36 4 -17 16 -39 27 -48 11 -10 20 -26 20 -37 0 +-11 20 -41 44 -66 24 -25 87 -114 141 -198 83 -131 105 -158 152 -188 29 -20 +67 -56 84 -80 36 -52 74 -151 83 -216 7 -49 38 -98 62 -98 10 0 14 -13 14 -46 +0 -42 -1 -45 -22 -38 -42 13 -73 32 -131 83 -60 53 -106 81 -130 81 -16 0 -49 +-70 -68 -143 -18 -72 -6 -216 25 -312 14 -45 30 -126 35 -186 11 -120 22 -176 +53 -269 65 -201 194 -364 376 -476 160 -98 248 -190 277 -287 12 -41 14 -84 9 +-216 -7 -191 1 -218 72 -255 36 -19 54 -21 229 -19 217 3 323 17 485 62 127 +36 154 34 186 -14 16 -24 15 -27 -12 -66 -63 -89 -76 -120 -82 -184 -11 -141 +91 -292 206 -303 56 -5 99 11 187 68 34 22 75 42 91 46 46 9 86 -31 144 -145 +107 -207 224 -284 386 -251 76 15 219 83 319 150 83 56 165 96 205 102 24 3 +25 0 28 -50 2 -29 -6 -95 -17 -147 -23 -103 -27 -237 -10 -298 6 -21 26 -56 +46 -78 121 -133 421 -74 932 183 285 143 310 150 354 99 18 -21 33 -65 52 +-148 35 -153 75 -260 131 -359 73 -127 117 -164 200 -164 66 0 125 31 152 79 +27 48 38 135 51 391 6 102 16 205 23 230 13 43 15 45 48 42 53 -6 146 -37 276 +-93 71 -31 157 -59 214 -70 152 -31 211 -71 238 -159 6 -19 16 -136 22 -259 +16 -323 36 -422 101 -496 47 -53 114 -69 259 -62 63 4 134 10 158 13 l44 7 +-21 -39 c-35 -66 -28 -124 53 -446 95 -373 162 -611 287 -1023 56 -181 144 +-476 196 -655 130 -448 230 -739 284 -826 14 -23 19 -50 19 -103 0 -73 9 -93 +55 -119 15 -8 27 -78 75 -423 32 -228 72 -531 89 -674 22 -184 41 -299 66 +-392 49 -188 60 -282 45 -370 -11 -66 -16 -76 -56 -112 -34 -32 -51 -41 -79 +-41 -80 0 -133 53 -342 340 -78 107 -97 142 -137 247 -51 138 -61 199 -136 +823 -93 771 -115 903 -271 1580 -197 854 -306 1230 -472 1631 -35 85 -67 166 +-71 181 -14 58 -57 74 -101 35 -21 -18 -31 -48 -60 -172 -85 -364 -86 -417 +-14 -706 113 -458 170 -750 214 -1108 37 -295 52 -359 180 -776 95 -307 129 +-449 160 -661 51 -347 155 -673 322 -1012 65 -132 70 -168 30 -227 -15 -22 +-32 -58 -38 -80 -15 -53 -60 -108 -132 -161 -99 -73 -169 -69 -255 14 -112 +109 -214 298 -302 559 -48 141 -60 188 -60 239 0 121 -498 2064 -730 2849 +-179 606 -275 899 -340 1043 -46 102 -98 199 -110 207 -11 7 -55 -50 -75 -98 +-8 -20 -15 -54 -15 -75 0 -80 87 -558 171 -937 98 -442 336 -1383 459 -1815 +149 -525 190 -716 183 -860 -4 -71 1 -127 17 -220 24 -134 28 -265 11 -370 +-13 -80 -49 -120 -108 -120 -33 0 -46 9 -118 82 -45 46 -96 101 -113 123 l-32 +40 0 -30 c0 -36 30 -151 58 -222 72 -188 228 -370 382 -448 25 -12 101 -35 +170 -50 188 -40 438 -113 930 -270 58 -18 146 -45 196 -59 51 -14 163 -53 250 +-87 396 -153 413 -156 829 -163 464 -7 641 -28 1545 -180 750 -126 927 -145 +1425 -153 566 -9 988 24 1733 137 1120 171 1302 188 1982 194 431 4 527 11 +780 56 400 73 727 191 1739 629 178 77 341 152 363 167 81 57 138 165 138 264 +0 107 -52 187 -179 272 -153 104 -250 221 -309 376 -28 73 -40 120 -78 322 +-31 166 -124 309 -238 365 -28 13 -95 33 -149 44 -104 21 -195 58 -274 112 +-89 61 -367 272 -533 404 -270 216 -360 272 -520 326 -115 38 -270 49 -385 27 +l-71 -14 94 225 c51 124 210 512 352 861 602 1481 843 2031 1216 2780 168 336 +222 434 316 576 127 190 211 331 413 699 76 138 156 277 180 310 59 84 83 135 +99 208 10 50 33 97 104 214 59 96 101 154 117 161 29 14 41 52 25 82 -9 17 -9 +25 4 39 18 20 20 44 6 70 -5 10 -71 90 -146 177 -157 184 -178 221 -178 314 1 +81 25 146 101 265 31 50 85 146 120 213 56 110 63 130 63 182 0 63 -19 94 -76 +123 -16 8 -179 58 -363 112 -184 53 -351 105 -371 115 -59 30 -88 71 -95 137 +-14 121 -49 206 -145 348 -26 39 -59 92 -75 119 -19 34 -38 53 -64 64 l-36 16 +0 90 c0 133 -23 174 -195 351 -149 154 -160 173 -160 283 0 81 31 236 85 429 +25 87 45 179 45 206 0 45 -1 47 -28 47 -39 0 -52 32 -52 125 0 57 -5 82 -20 +107 -16 26 -20 51 -20 116 0 56 -4 82 -12 82 -6 0 -22 10 -35 22 -42 40 -48 +17 -49 -195 -1 -187 -10 -254 -37 -287 -14 -19 -58 -50 -69 -50 -16 0 -8 97 +16 195 46 192 49 212 36 282 -20 117 -97 256 -159 287 -14 7 -55 16 -90 20 +-146 16 -183 56 -189 203 l-4 93 53 -16 c30 -9 78 -30 107 -47 62 -37 230 +-169 253 -199 10 -13 23 -19 33 -15 22 9 20 27 -4 27 -13 0 -22 10 -29 36 -17 +60 -51 101 -151 178 -52 41 -104 84 -115 96 -11 12 -38 31 -60 42 -22 11 -69 +46 -104 77 -304 269 -576 385 -1206 514 l-109 22 -53 70 c-242 318 -456 448 +-773 470 -174 12 -287 50 -550 186 -178 92 -195 106 -186 144 9 36 28 40 110 +26 39 -7 162 -16 273 -21 319 -15 398 -38 583 -175 125 -91 217 -147 309 -185 +71 -29 200 -65 207 -57 3 3 -157 166 -354 363 -453 454 -602 628 -783 921 +-115 186 -209 272 -384 354 -80 38 -247 87 -355 104 l-45 7 65 -59 c64 -59 +153 -137 344 -303 155 -134 303 -310 424 -505 52 -84 150 -280 167 -334 7 -23 +5 -23 -33 10 -22 19 -147 126 -278 239 -471 406 -873 722 -1449 1140 l-210 +153 63 -7 c93 -10 211 -47 437 -136 191 -75 334 -124 341 -117 10 9 -51 78 +-93 105 -78 50 -263 134 -468 211 -263 100 -432 186 -699 356 -289 184 -394 +246 -509 300 -102 47 -245 97 -331 115 -71 15 -110 47 -134 113 l-20 51 -21 +-34z m105 -1815 c193 -33 371 -97 549 -196 134 -74 155 -92 155 -135 0 -50 +-14 -55 -126 -44 -134 13 -312 12 -376 -1 -29 -6 -81 -24 -115 -39 -84 -38 +-118 -44 -169 -30 -63 18 -117 70 -158 150 -46 93 -55 149 -29 191 22 34 139 +119 164 119 9 0 56 -7 105 -15z m5990 -3540 c-15 -23 -52 -42 -62 -32 -3 3 -2 +17 2 32 8 33 67 64 72 39 2 -8 -3 -26 -12 -39z m-15398 -3420 c35 -29 46 -67 +36 -119 l-6 -36 -50 42 c-57 47 -72 77 -58 113 12 32 40 32 78 0z m109 -180 +c2 -6 -3 -20 -12 -30 -14 -20 -15 -20 -35 5 -11 14 -20 29 -20 33 0 12 63 4 +67 -8z m263 -103 c0 -2 -13 -15 -30 -29 -21 -17 -30 -34 -30 -54 0 -34 19 -46 +126 -77 80 -24 92 -35 101 -86 l6 -36 -64 0 c-51 0 -67 -4 -86 -22 -13 -12 +-23 -27 -23 -35 0 -20 -60 -16 -97 7 -33 20 -76 77 -88 118 -7 24 4 29 23 10 +10 -10 15 -10 22 2 18 28 10 56 -29 113 -42 59 -41 66 10 86 21 9 159 11 159 +3z m90 -451 c3 -2 -8 -1 -25 3 -32 8 -55 30 -55 55 0 10 13 3 37 -20 21 -18 +40 -36 43 -38z m3374 -1488 c57 -40 57 -53 2 -53 -53 0 -110 21 -143 52 -31 +29 -13 41 52 34 32 -4 64 -16 89 -33z m158 -77 c22 -16 22 -16 -14 -16 -22 0 +-45 8 -59 21 l-24 20 37 -5 c21 -2 48 -12 60 -20z m263 -110 c26 -8 65 -29 86 +-45 21 -17 45 -31 54 -31 22 0 55 -18 55 -30 0 -19 -59 -11 -70 10 -8 15 -21 +20 -54 20 -47 0 -166 53 -174 78 -5 17 43 16 103 -2z m7062 -337 c24 -11 52 +-33 63 -47 12 -15 85 -111 163 -213 78 -103 190 -245 249 -317 123 -149 166 +-179 244 -168 43 5 44 5 44 -23 0 -15 -9 -70 -20 -122 -11 -52 -20 -116 -20 +-144 0 -27 -9 -72 -19 -100 -10 -27 -67 -232 -126 -455 -308 -1171 -764 -2779 +-798 -2819 -8 -9 -17 -32 -21 -51 -12 -60 -153 -157 -208 -142 -32 8 -66 64 +-118 192 -60 149 -120 421 -120 545 0 78 125 1113 196 1620 40 292 42 402 6 +510 -73 220 -88 245 -137 245 -36 0 -74 -36 -80 -78 -4 -20 -19 -93 -35 -162 +-61 -260 -95 -447 -306 -1700 -19 -113 -72 -470 -119 -795 -124 -857 -172 +-1143 -256 -1519 l-39 -178 -58 -77 c-90 -117 -150 -142 -242 -101 -67 30 +-225 141 -236 164 -5 12 -40 37 -77 56 -38 20 -76 44 -84 55 -20 27 -14 96 36 +430 53 353 86 619 171 1375 93 826 102 947 132 1750 19 508 17 820 -5 897 -24 +86 -159 363 -191 392 -17 16 -41 25 -74 29 -44 4 -53 1 -84 -26 -36 -32 -52 +-72 -63 -157 -4 -27 -9 -59 -12 -69 -5 -18 -6 -18 -19 5 -9 16 -13 62 -14 139 +-1 215 -23 412 -63 549 l-21 74 29 55 c41 78 117 157 170 178 128 48 307 -47 +560 -299 66 -66 147 -140 180 -165 159 -118 438 -216 657 -229 91 -6 100 -4 +152 21 42 21 62 39 81 71 37 62 44 94 55 240 12 154 28 223 80 330 33 68 50 +90 120 150 124 108 188 127 277 84z m-4723 -610 c20 -17 52 -58 72 -92 195 +-325 376 -468 618 -486 120 -9 120 -8 127 -279 3 -119 9 -226 14 -237 25 -61 +101 -124 152 -126 10 0 -3 -7 -29 -15 -92 -28 -107 -91 -68 -282 10 -54 24 +-173 30 -267 21 -327 235 -2034 340 -2705 26 -166 31 -187 54 -206 13 -12 30 +-24 36 -26 7 -2 20 -61 31 -143 48 -351 71 -455 126 -557 25 -46 25 -48 8 -75 +-73 -114 -169 -181 -241 -170 -96 16 -213 109 -320 252 -75 103 -145 165 -185 +165 -22 0 -12 -66 -99 695 -97 852 -125 1105 -159 1460 -47 472 -60 577 -92 +745 -69 353 -209 750 -386 1093 -46 89 -104 152 -151 164 l-24 6 3 496 c3 453 +2 499 -14 535 -9 21 -17 40 -17 42 0 3 15 13 33 24 49 30 99 26 141 -11z +m1526 -69 c129 -219 248 -287 431 -247 l66 14 7 -406 c14 -880 49 -1612 112 +-2361 33 -406 46 -520 55 -520 4 0 22 30 40 66 30 61 33 77 40 198 4 72 25 +514 48 981 23 468 46 860 51 872 5 12 18 27 29 33 19 10 20 8 16 -17 -3 -16 +-12 -197 -20 -403 -25 -627 -47 -976 -75 -1205 -6 -44 -15 -210 -20 -370 -5 +-159 -17 -414 -26 -565 -9 -151 -17 -459 -18 -685 -3 -383 -4 -415 -24 -480 +-18 -62 -28 -77 -84 -130 -35 -33 -87 -72 -115 -88 -71 -38 -189 -67 -305 -74 +l-98 -6 0 99 c0 54 9 430 20 834 34 1295 42 1820 38 2510 -4 642 -6 679 -25 +760 -40 160 -102 299 -186 410 -48 63 -44 22 -38 420 3 172 1 204 -18 273 -19 +72 -19 82 -6 115 17 39 33 62 45 62 4 0 31 -40 60 -90z m-1540 -2529 c50 -398 +89 -726 87 -728 -12 -11 -41 15 -61 56 -21 41 -22 49 -10 76 19 44 17 158 -6 +252 -11 45 -27 144 -35 220 -9 76 -31 242 -50 368 -58 395 -60 415 -40 460 10 +22 19 36 21 30 2 -5 44 -336 94 -734z"/> +<path d="M7997 17171 c-21 -10 -55 -34 -75 -53 -20 -19 -71 -62 -113 -96 -42 +-33 -98 -85 -126 -115 l-50 -55 -7 -94 c-12 -177 22 -403 79 -524 33 -68 111 +-160 157 -185 27 -15 27 -15 -56 183 -37 86 -74 191 -83 232 -15 70 -15 78 1 +121 30 78 84 91 228 54 105 -27 147 -18 173 38 16 33 18 64 16 228 -2 196 -6 +251 -24 273 -14 17 -75 14 -120 -7z"/> +<path d="M6910 17111 c-207 -255 -261 -331 -432 -617 -160 -266 -161 -270 +-124 -544 8 -63 20 -218 26 -345 11 -253 21 -340 51 -448 11 -40 24 -94 30 +-122 14 -64 41 -109 85 -135 27 -17 34 -28 34 -53 0 -34 31 -110 48 -121 16 +-10 35 32 28 60 -4 13 -19 38 -35 56 -23 27 -37 68 -74 213 -55 216 -72 331 +-63 445 11 161 78 331 170 435 50 56 46 59 71 -60 27 -126 73 -262 123 -360 +37 -73 64 -108 139 -183 51 -52 96 -105 99 -118 8 -33 57 -80 91 -89 15 -3 38 +-16 51 -27 12 -12 22 -17 22 -11 0 5 -9 18 -20 28 -11 10 -20 27 -20 37 0 28 +-27 76 -49 88 -11 5 -31 10 -45 10 -23 0 -26 4 -26 38 0 28 -11 56 -40 101 +-52 80 -163 301 -201 400 -58 151 -83 274 -82 406 1 69 8 143 16 175 45 172 +85 286 131 375 50 97 51 102 51 185 0 83 -20 200 -33 200 -4 -1 -14 -9 -22 +-19z"/> +<path d="M4200 15495 c-19 -19 -42 -35 -53 -35 -10 0 -39 -21 -65 -47 -26 -25 +-35 -37 -21 -25 14 12 32 22 40 22 23 1 53 22 98 73 50 55 51 64 1 12z"/> +<path d="M3930 13725 c-7 -8 -21 -15 -33 -15 -28 0 -81 -35 -111 -74 -14 -19 +-41 -44 -60 -56 -19 -11 -107 -98 -195 -191 -137 -145 -166 -182 -201 -252 +-30 -62 -42 -99 -46 -152 l-6 -70 46 -43 c27 -25 55 -42 69 -42 31 0 88 39 +104 70 7 14 32 66 55 115 24 50 87 158 140 240 133 210 219 364 225 405 3 19 +15 45 26 58 11 12 16 22 10 22 -6 0 -16 -7 -23 -15z"/> +<path d="M2674 13567 c-25 -14 -132 -145 -192 -234 -98 -145 -190 -374 -227 +-563 -49 -255 11 -719 131 -1009 111 -269 296 -490 779 -933 371 -340 858 +-778 862 -774 3 2 -1 23 -7 46 -9 34 -49 81 -222 263 -522 549 -1019 1133 +-1175 1382 -53 84 -112 216 -134 300 -39 150 -22 409 44 685 l13 55 18 -80 +c42 -186 147 -439 255 -618 27 -45 52 -100 56 -122 11 -57 22 -75 56 -87 24 +-8 29 -15 29 -43 0 -32 21 -65 40 -65 5 0 17 11 25 24 21 31 6 63 -35 77 -25 +9 -28 16 -32 64 -3 40 -14 70 -41 113 -59 96 -133 376 -156 594 -21 192 -8 +400 38 583 44 177 46 199 27 249 -28 73 -106 121 -152 93z"/> +<path d="M5265 6405 c56 -741 1232 -4262 1540 -4612 13 -16 25 -22 25 -15 0 7 +-22 86 -50 175 -71 234 -180 621 -335 1197 -335 1241 -499 1768 -740 2380 +-128 325 -382 878 -430 935 -15 19 -16 16 -10 -60z"/> +<path d="M5345 4220 c6 -31 41 -162 187 -685 50 -181 91 -381 118 -575 39 +-283 116 -539 255 -840 34 -74 67 -149 74 -167 20 -51 54 -76 90 -68 38 8 71 +48 71 85 0 40 -34 201 -81 380 -76 292 -174 561 -424 1160 -70 168 -165 404 +-212 525 -46 121 -81 204 -78 185z"/> +<path d="M5125 3042 c1 -265 3 -275 126 -456 107 -160 195 -265 253 -306 41 +-28 66 -37 66 -25 0 3 -23 50 -50 105 -28 56 -103 232 -166 393 -100 251 -200 +475 -222 499 -4 4 -7 -91 -7 -210z"/> +</g> +</svg> diff --git a/login/app/sprinkles/core/assets/userfrosting/images/cupcake.png b/login/app/sprinkles/core/assets/userfrosting/images/cupcake.png Binary files differnew file mode 100755 index 0000000..3e1a704 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/images/cupcake.png diff --git a/login/app/sprinkles/core/assets/userfrosting/images/logo.svg b/login/app/sprinkles/core/assets/userfrosting/images/logo.svg new file mode 100755 index 0000000..b11b0b5 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/images/logo.svg @@ -0,0 +1,514 @@ +<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ width="612px" height="537px" viewBox="0 0 612 537" style="enable-background:new 0 0 612 537;" xml:space="preserve">
+<path style="fill:#FFFFFF;" d="M71.3,277.7c-1.9,3.4-2.2,7.6-1.4,12.6c9.1,23.9,33.6,61.6,48.5,100.3c14.8,38.2,18.8,57.5,32.1,75.8
+ s59.7,30.3,59.7,30.3l59.2,11.9V309C269.5,309,77.4,266.9,71.3,277.7z"/>
+<polygon style="fill:#FFFFFF;" points="310.5,35.1 305.1,40.5 299.3,44.3 242.7,84 278.2,108.5 328.5,109.7 310,72.4 310,54 "/>
+<path style="fill:#FFFFFF;" d="M133,147L133,147c-0.4,0.4-0.7,0.7-1,1l0,0c-0.8,0-1.5,0.2-2,1l0,0c-0.3,0.3-0.7,0.6-1,1
+ c-2.6,0.3-3.9,2.4-5.4,4.1c-6,6.8-12.2,13.3-19.2,19.2c-10.4,8.8-19,19.4-27.5,29.8c-3.6,4.3-7.4,8.7-9.8,13.9c-4,0.6-6.4,3.9-9.5,6
+ c-1.8,1.3-1.6,2.9-0.4,4.7c2,2.9,3.4,6.3,2.7,9.8c-0.8,3.8,0.3,6.7,2.8,9.4c0.4,0.3,0.9,0.7,1.3,1l0,0c0,0.9,0,1.7,1,2l0,0
+ c0,0.9,0.1,1.7,1,2l0,0c0,0.9,0.2,1.6,1,2l0,0c0.3,0.4,0.7,0.8,1,1.1c1.7,2.6,3.4,5.2,5,7.8c0,2.7,0,5.4,0,8.1
+ c-2.4,3.8-2,7.8-0.5,11.8c0.5,1.4,0.4,2.9,0.6,4.4c0.8,7.4,4.2,13.4,10.4,17.1c4.8,2.9,7.8,5.8,6.6,11.6c-0.2,0.8,0,1.7,0,2.5
+ c-0.2,3.2,1.3,4.5,4.5,4.5c4.8,0,9.6,0,14.3-1.6c1.5-0.5,3.5-1.3,4.6,0.2c0.9,1.2-0.9,2.3-1.4,3.4c-1.8,3.3-0.7,6.3,1.5,8.7
+ c2.3,2.5,5.1,1.2,7.2-0.4c2.9-2.2,4.1-0.6,5.2,1.8c3.5,7.4,7.5,8.6,14.6,4.7c2.6-1.4,4.9-3.7,8.1-4.2c0.6,2,0,3.5-0.4,5.2
+ c-1.9,8.8,1.3,11.9,10.1,9.7c1.1-0.3,2.2-0.7,3.3-1.1c5.1-1.6,9.7-4.4,14.5-6.7c2.8-1.4,4.3-0.9,4.9,2.3c0.6,3.3,1.5,6.5,3.1,9.4
+ c1.3,2.4,2.6,4.9,5.9,4.3c3.5-0.7,3.3-3.9,3.6-6.3c0.4-3.8-0.3-7.8,1.2-11.5c4.7,0.2,8.4,3.5,13,4.1c3.4,0.4,5.6,2,5.8,5.6
+ c0.1,1.8,0.1,3.6,0.2,5.5c0.6,12.3,2.1,13.6,14,11.6c-1.9,1.6-1.9,3.3-1.3,5.7c2.8,12.1,6.3,24,10,35.8c3.1,9.8,5.3,19.8,9.3,29.3
+ l0,0c0.3,0.7,0.7,1.3,1,2l0,0c0.3,1.7-0.8,3.9,1.9,4.5c1.3,10,2.9,20,3.9,30c0.5,5.4,3.5,10.5,2,16.1c-2.2,3.3-4.6,2.7-6.7,0.2
+ c-2.5-3-4.7-6.2-7-9.4c-0.5-2.2-1.8-4.1-2.2-6.5c-2.3-15.6-3.3-31.4-6.9-46.8c-4-16.8-7.2-33.7-14.4-49.6c-0.4-0.8-0.1-2.2-1.4-2.3
+ c-1.1-0.1-1.7,1-1.9,2c-1,5.3-3.2,10.5-1.7,16.2c2.6,10.3,5.1,20.6,6.1,31.2c1.1,11.3,6.7,21.6,8,32.8c1,8.4,3.8,16.1,7.3,23.7
+ c0.8,1.8,2.2,3.4,1.8,5.5c-0.7,1.2-1.7,2.2-1.7,3.7c-5.1,6-7.8,5.8-12.2-0.9c-2.8-4.3-4.3-9.2-5.8-14c0.2-1.7,0-3.3-0.4-4.9
+ c-6.3-25.2-12.4-50.5-20.2-75.4c-2.1-6.7-3.9-13.5-7.7-19.8c-2,1.9-2.3,3.6-1.9,5.8c2.8,18.5,7.7,36.6,12.3,54.7
+ c2.4,9.6,5.8,19,7.3,28.9c-1,4.5,1.4,8.7,0.8,13.2c-0.3,2,0.1,4.4-1.8,5.3c-2.2,1-3.4-1.3-4.8-2.6c-0.9-0.9-1.7-1.9-2.6-3
+ c0.8,7.9,6.9,16.3,13.4,17.4c7.5,1.3,14.5,4.1,21.8,6.1c1.9,0.6,3.7,1.3,5.7,1.8c7.5,1.9,14.6,6.5,22.2,6.2
+ c19-0.7,37.2,4.1,55.7,6.6c17.9,2.4,35.5,1.8,53.1-0.6c18.7-2.5,37.2-6.5,56.3-5.9c10,0.3,20.3-1.1,29.9-4.2
+ c12.6-4.2,24.7-9.9,37-15.1c6.3-2.6,7.4-10,1.5-13.6c-6.2-3.8-8.5-9-9.5-15.8c-0.7-5.3-3.3-10.3-9.2-10.9c-3.7-0.4-6.1-2.1-8.7-4
+ c-3.5-2.5-6.9-5.1-10.3-7.8c-6.9-5.6-13.7-11.5-24.3-8.6c14.7-35.1,27.7-70.6,45.5-103.9c5.9-8,10.3-16.8,15.1-25.4
+ c1.4-2.5,3.8-4.6,3.6-7.8c1.6-2.5,3.1-5.1,4.7-7.6c1.6-0.6,1.8-1.6,0.8-2.9c1.5-0.9,0.8-1.8,0-2.7c-2.1-2.4-4.1-4.9-6.2-7.3
+ c-2-3.6-0.7-6.7,1.3-9.7c1.5-2.4,2.9-4.8,4.1-7.4c1.6-3.3,0.4-5-2.8-5.9c-4.5-1.3-8.9-2.6-13.4-3.9c-2.3-0.7-4.9-1.5-5-4.1
+ c-0.2-4.2-2.5-7.1-4.7-10.2c-0.6-1.5-1.5-2.8-3.2-3c1.6-6.3-3.2-9.2-6.7-12.8c-2.4-2.5-1.7-5.6-1.2-8.5c0.6-3.6,1.8-7,2.8-10.6
+ c1-1.7,2.4-3.3,0.2-6.4c-0.2,2.3-0.3,3.4-0.3,4.5c-0.3,0-0.7,0-1,0c0-1.4,0.5-2.9-0.8-4.1c0-0.4-0.1-0.7-0.1-1.1
+ c-0.2-1.7,0.6-3.5-0.8-5l-0.1-0.2c-0.2-2.9,0.4-6.2-3-7.9c-0.4,0-0.7,0-1.1,0l0,0c-0.5-4.2-5-5.1-7-8c-0.5-7.2-6.1-10.4-11.3-13.7
+ c-8-5.1-17.8-6.7-25.5-12.4c-1.3-1.4-2.6-2.9-4.1-4.3c-8.6-8.4-17.6-16.7-23.7-27.2c-4.3-7.4-11-9.3-18.5-10.6c-1-0.6-2-1.3-3-1.9
+ c-1.1-2.3-3.3-3.2-5.4-4.2c-5.5-2.7-11.4-4.3-16.9-7.1c-9.6-5-17.8-12.8-28.9-14.9c-1.7-0.3-2.6-1.9-2.8-3.8
+ c-1.3-4.3-1.7-8.7-0.1-13.1c0.5-9.6,0.5-19.2,6.8-27.3c0.4-0.5,0.6-1.2,0.9-1.8c0.4-0.8,0.6-1.7-0.2-2.2s-1.6,0.1-2,0.9
+ c-1.2,2.1-3.1,3.9-3.5,6.5c-0.3,0.3-0.6,0.7-0.9,1l0,0c-1.4,0-1.9,1.1-2.5,2.1c-3,4.9-3.4,10.4-3.3,15.8c0.2,7.9-1.5,15.3-4.2,22.7
+ c-0.7,2-2,2.3-3.8,2.5c-5.2,0.7-10.5,1.2-15.6,2.3c-6.2,1.3-8.3,5.6-5.9,11.5c0.7,1.9,1.1,4.7,3.7,4.8c2.6,0.2,4.6-2.2,5.1-4.1
+ c1.9-7,6.6-8.3,12.9-7.6c1.5,0.2,3,0,4.5,0c9.8,0.2,14.9,5.5,15.3,15.4c0.1,3.4-0.8,4.8-4.4,4.6c-7-0.3-14-0.1-21,0
+ c-1.6,0-3.4-0.5-4.7,1c-0.7,0-1.3,0-2,0c-11.4-0.9-19.9-9.3-30.7-12c-1.1-0.3-1.7-1.9-0.8-2.3c3.9-1.6,1.7-4.3,1.5-6.7
+ c-0.1-1.6-0.3-3,1.7-4.2c6.7-4.2,13.7-7.8,19.5-13.7c5.9-5.8,13.5-9.9,19.2-16.2c1.1-1.2,3.2-2.1,1.8-4.6
+ c-11.6,9.3-23.5,18-37.4,23.5c-4.1,0.7-7.9,2.2-11.1,5.1c-3,0-5.7,1.3-7.8,2.9c-8.4,6.4-17.6,10.9-27.4,14.5
+ c-4.9,1.8-9.9,3.5-14.5,5.8c-7.8,3.9-12.8,10.9-18.2,17.3c-4.3,5.1-8.9,10.4-14.7,13.2c-7.5,3.5-15,7-22.4,10.7
+ c-4.3,2.2-9.1,4-11.8,8.6C133.6,146.2,133.3,146.6,133,147z"/>
+<path style="fill:#FFFFFF;" d="M156,405c-0.1-0.4-0.3-0.9-0.4-1.3c-0.1,0.1-0.4,0.2-0.4,0.3C155.5,404.3,155.7,404.7,156,405
+ c0.1,0.8,0.2,1.5,0.4,2.2c0.1-0.1,0.3-0.1,0.4-0.3C157.1,406.1,156.7,405.4,156,405z"/>
+<path style="fill:#FFFFFF;" d="M158.3,363.3c-0.6,13.8,30.6,108,37.7,113.3C184.1,438.5,177.3,399.2,158.3,363.3z"/>
+<path style="fill:#FFFFFF;" d="M160,416.7c2.4,10.3,6.3,20.3,7.5,30.7c1,8.3,3.7,15.7,7.3,23c0.7,1.4,1,3.9,3,3.2
+ c2.1-0.7,1.2-3.2,0.9-4.9C175.2,450.5,166,434.2,160,416.7z"/>
+<path style="fill:#FFFFFF;" d="M154.9,440.7c1.1,4.6-1.3,9.5,1.6,13.9c4.7,7.2,6.8,9.6,9.3,10.6
+ C161.3,457.3,159.4,448.5,154.9,440.7z"/>
+<path style="fill:#FFFFFF;" d="M153.4,398.4c0.1-0.3,0.1-0.5,0.2-0.7c-0.1,0-0.2,0.1-0.4,0.1C153.3,398,153.3,398.1,153.4,398.4z"/>
+<path style="fill:#FFFFFF;" d="M154.4,401.3c0.4,0,0.5-0.2,0.2-0.5c0,0-0.2,0.1-0.4,0.1C154.3,401,154.3,401.1,154.4,401.3z"/>
+<path style="fill:#FFFFFF;" d="M157.8,409.9c-0.1-0.1-0.2-0.2-0.3-0.3c-0.1,0.1-0.2,0.2-0.3,0.3c0.1,0.1,0.3,0.3,0.4,0.4
+ C157.7,410.2,157.7,410,157.8,409.9z"/>
+<path style="fill:#F7E3E5;" d="M261.9,67.9c13.9-5.5,25.8-14.2,37.4-23.5c1.4,2.5-0.7,3.4-1.8,4.6c-5.7,6.3-13.4,10.4-19.2,16.2
+ c-5.9,5.9-12.8,9.5-19.5,13.7c-2,1.2-1.8,2.6-1.7,4.2c0.2,2.4,2.4,5.1-1.5,6.7c-0.9,0.4-0.3,2,0.8,2.3c10.8,2.7,19.3,11.1,30.7,12
+ c-0.3,2.4,1.2,2.7,3.1,3.3c1.7,0.6,4.5-0.3,5,2.7c-0.5,2.7,1.4,3.9,3.1,5.2c2.2,1.7,4.5,3.3,6.8,4.9c-0.1,2.6-1.6,2.1-3.5,1.9
+ c-1.9-0.2-3.9-2.1-6.7,0.2c7.9,2.4,15.2,4.6,22.5,6.7c17.3,5,35,8.2,52.5,12.1c6,1.4,7.6,3.4,8.1,9.8c-15.4-0.4-30.7-2.2-46.1-0.1
+ c-0.4-0.5-0.7-0.3-0.9,0.2c-0.1,2,1.6,2.4,2.9,2.8c10.9,3.4,21.9,6.1,33.1,8.2c0.5,0.8,1.2,0.9,2.1,0.8h0.1
+ c-5.6,1.7-11.7,1.1-17.1,3.7c1.1,0.5,1.3,0.7,0.9,1.5c-1.6-0.2-3.3-0.2-4.9-0.5c-13-2.7-25.8-5.7-38.9-7.7c-3-0.5-7.4-2.4-8.3,0.4
+ c-0.7,2.2,4.5,2.9,7.3,3.6c16.1,4.2,32.6,5.5,48.9,8.3c1.4,5.3,7.2,3,10,5.8c-13.3,0.4-26.6,0.8-39.8,1.2c0.3-0.9,2.1-2-0.8-2.3
+ c-5.2-0.5-10.5-1.3-15.6-2.6c-20-4.9-39.4-11.4-58.1-20.1c-11.5-5.3-23.2-10.4-31.7-20.3c-1.2-3-2.7-6-3.6-9.1
+ c-1.4-4.7,0.7-6.9,5.2-5.4c3.9,1.3,4.7-0.3,4.8-3.5c1.5,0,3,0.1,4.3,0.9c6.5,4,13,7.9,19.5,11.8c7.4,4.3,14.6,8.8,23.2,10.7
+ c15.3,3.3,30.6,7.2,45.9,10.4c2.6,0.5,5.5,2.5,9.1-0.1c-8.9-3.7-17.6-6-25.9-9.5c-10.5-4.4-20.4-8.9-27.1-18.9
+ c-2.3-3.4-5.8-6.2-10.5-7.8c-8.8-2.9-17.6-5.9-25.5-10.9c-5.2-3.3-9.9-3-14.9,0.7c-1.4,1-2.5,2.2-2.4,4.1c-2.4,2.8-6.1,4.4-8,7.8
+ c-3-2.6-5.5-0.1-8.3,0.6c-4.4,1.3-7.1,5.2-11.2,7c0.3-1.6,0.6-3.2,1.5-4.7c2.4-4.1,2.4-4.9,1.2-10.1c-5,5.4-8.7,11.7-12.4,17.9
+ c-1.3,2.1-2.3,4.6-1.8,7.1c1.7,7.9,0.2,16.2,2.8,24c-1,0.6-1.4,1.9-2.8,1.9c-4.2,0.1-3.5,1.9-1.5,4.2c1,1.1,1.7,2.4,2.8,3.5
+ c9.7,9.7,22.1,15.4,33.7,22.4c1.6,0.9,3.3,1.8,5.3,1.3c-0.1-1.4-1.1-1.6-1.8-1.9c-8.7-4-16.3-9.5-23.5-15.8
+ c-1.2-1.1-2.2-2.6-4.1-2.6c-2.8-0.1-3.5-2.2-4.1-4.3c1.2-1.7-0.3-2.6-1.2-3.7l0.1,0.1c-0.9-3.7-2.1-7.3-2.5-11.1
+ c-0.6-5.8,0.6-11.2,5.3-16.1c1.4,7.4,3.1,13.9,9.2,18.1l0,0c0.1,1.9,1.3,2.8,3,3l0,0c0.3,0.3,0.7,0.7,1,1c3.3,1.6,5.1,4.9,8.2,6.9
+ c10.2,6.5,20,13.5,30.2,19.9c5.5,3.4,11.2,6.8,18.1,7.8c-0.1-2.3-3-1.9-3-4.3c4.4-1,9.2,0.5,13.6-0.9c2.7-0.8,5.1-0.2,7.7,0.9
+ c9.5,4.3,18,10.4,27.9,14c5.9,2.1,11.3,5.4,17.8,5.8c-8-7-16.3-13.5-25.8-18.3c-8.4-4.3-16.2-9.9-24.9-13.4
+ c-9.1-3.7-19.1-5.3-28.5-8.4c-6.9-2.3-11.7-7.5-16.5-13c2.2-1.1,3.1,0.1,4.3,0.6c6.5,2.8,12.5,7.1,19.6,8.2
+ c23.2,3.5,44.9,12.4,67.4,18.2c0,1.4,0.9,2.1,2.1,2.5c6.1,2.1,12,4.8,18.5,5.5c10.9,1.2,21.2,5.8,32.3,6.3c0.4,0,0.7,1,1.1,1.5
+ c-0.8,0.2-1.4,0.6-2,1.3c-5.1,6.5-7.1,6.6-13.9,1.7c-1.3-0.9-2.7-3.3-4.5-1.6c-1.5,1.5-0.1,3.4,0.6,5c2.6,5.6,7.2,9.8,10.9,14.6
+ c-2,1.9-4.6,1.4-6.6,0.9c-7.5-2.1-15.3-2.7-22.8-4.3c-20.6-4.4-41.7-6.2-62.2-11.4c-1.5-0.4-3.3-0.7-4.6,1c-8-3-16-6-24-9
+ c2.5,3,5.9,4.3,9.2,5.7c33.4,14.2,67.9,25.1,103.4,32.6c1.7,0.4,5-0.4,5,2c0,2.7-3.3,2.8-5.4,3.4c-1.1,0.4-2.3,0.2-3.5,0.2
+ c-10.6,0.7-20.8-1.3-31.1-3.7c-15.9-3.8-31.1-9.5-46.6-14.6c-1.4-0.9-2.7-2-4.7-1.4c1.3,1.3,2.8,1.6,4.2,2
+ c24.1,13.9,50.8,19.9,77.6,25.5c11.8,2.4,23.8,4.1,35.7,6.1c0.3,2.3,2.4,2.3,3.9,3.1c4.1,2.1,9,2.8,12,6.8c-0.7,0-1.3,0-2,0
+ c-0.9-1-2-0.8-3.1-0.7c-6.1-0.4-12.2-0.8-18.3-1.2c0,0.3-0.1,0.6-0.1,0.9c11.8,2.7,23.7,5.5,35.5,8.2c-1.7,1.7-3.7,1-5.5,0.7
+ c-5.5-1.1-10.6-0.1-15.5,2.4c-1.6-0.2-3.3-0.2-4.9-0.5c-14.2-2.2-28.4-4.5-42.6-6.6c-15.5-2.3-31.2-3.5-46.8-4.7
+ c-1.4-0.1-3-0.6-4.1,1.2c2.7,1.3,5.4,2.6,8.4,2.7c0.4,0.3,0.7,0.5,1.1,0.8c0.6,0.1,1.2,0.1,1.9,0.2c0,1.7,0.7,2.4,2.5,3
+ c4.2,1.4,8.3,3.2,11.7,6.3c2.9,2.7,2.7,4.4-1,6c-10.2,4.4-20.9,7.2-31.4,10.5c-4.1,1.3-8,4-12.8,3.1c-0.1-2,0.3-4-0.5-5.9
+ c-1-2.3-2.4-3.6-5-2.7c-9.7,3.3-19.3,6.9-29.1,10.2c-13.5,4.5-27.1,7.5-41.6,6.3c0.5-0.4,0.7-0.7,1-0.7c15.6-0.6,29.3-7.1,43.2-13.1
+ c2.9-1.3,4.7-2.9,2.7-6.8c-7.5-15.1-14.8-30.3-25.6-43.3c-7-8.4-13.3-17.3-20.9-25.2c-2.6-2.7-4.7-5.8-7-8.7c0.3-0.3,0.6-0.5,1-0.8
+ c6.6,5.2,13.3,10.3,19.9,15.5c1.4,1.1,2.8,3.1,4.7,1.6c2-1.6,3.6-0.3,5,0.4c4.9,2.5,9.5,5.3,14.4,7.9c3.7,1.9,7.5,3.6,11.2,5.3
+ c-7.7-7.8-17.8-11.9-25.5-19.5c20.4,11.5,40.7,22.9,63.5,28.9c-5.4-3.1-11-5.6-16.6-8.4c-12.5-6.2-25.1-12.1-37.1-19.4
+ c-17.1-10.3-34-20.7-48.8-34.1c-7.8-7.1-15.7-14.4-17.6-25.7c-0.4-2.5-2.1-4.1-4-4.6c-3.9-0.9-7.2-3.2-10.9-4.4
+ c-1.8-0.6-3.3-0.5-4.6,0.7c-1.5,1.4-0.1,2.8,0.4,4.1c3.9,8.8,7.2,17.8,11.9,26.2c6.6,12,13,24.2,23,33.8c5.3,5.1,9.8,10.7,13.8,16.8
+ c8,12.2,14,25.5,21.6,37.9c0.4,0.7,0.8,1.6,0,2.3c-0.8,0.7-1.6,0-2.1-0.6c-8.7-9.2-19-17-24.6-29c-4.4-9.4-9.1-18.7-15.1-27.2
+ c-2.1-3-3-6.6-5.8-9.2c-1.5-1.3-2.5-1.5-3.5,0.3c-0.6,1.1,0.1,3.1-1.9,3c-1.8-0.1-1.8-2.1-2.5-3.4c-5.3-10.1-10.3-20.4-17.6-29.3
+ c-0.9-1.1-1.4-2.6-2.9-2c-8.4,3-19.3,2.1-22.5,13.7c-0.3,1.2-1,2.3-2.4,1.8c-1.3-0.5-2.4-1.5-2.3-3c0.1-0.9,0.8-1.8,1.2-2.6
+ c0.9-1.7,0.7-3.2-0.6-4.6c-1.4-1.5-2.7-1.2-4.5-0.6c-5.3,2-8.5,6.7-13.1,9.6c-1.3,0.8-2.8,2.2-1.8,4.2c1,1.8,2.8,1.7,4.4,1.3
+ c3.7-0.8,7.4-1.8,11.1-2.6c3.4-0.8,5.6,0.1,5.1,4.2c-1.4,1.4-2.7,2.9-4.1,4.3c-3.1,3.2-6.4,6.3-6.1,11.4c-4.4-1.3-8.8-2.8-12.1-6.2
+ c0.4-1.6,0.7-3.2,1.1-4.8c0.5-1.9-0.3-3.3-1.8-4.3c-1.9-1.2-2.7,0.7-3.5,1.7c-5.2,5.9-7.9,12.9-8.8,20.7c-3.1,0.7-2,4-3.5,5.7
+ c-3.3,3.8-2.3,7.7-0.4,12c3.7,8.3,8.9,15.6,14.4,22.5c4.5,5.7,9.7,11,16.1,15.3c5.3,3.5,11,6.6,15.4,11.5c1.7,1.9,2.4,3.3,0.4,5.3
+ c1.9,4.4,5.9,6,9.9,7.6l0.1,0.1c3.6,4.3,8.4,7,13,9.8c1.8,5,3,11,8.7,12.5c7.4,2,14.2,5.4,21.8,6.7c8,1.4,16.1,2,24,3.6
+ c2.7,0.6,5.9,1.9,10.3,0.2c-11.6-2.9-21-7.8-30.8-11.9l0,0c4.7-0.6,8.9,1.3,13.1,2.6c17.2,5.3,34.7,6.4,52.6,4.4
+ c14.3-1.6,28.4-4.1,42.8-5.3c3.3-0.3,7-0.1,10.3-1.4c1.6-0.6,3.4-1.1,3.8-2.8c0.5-2-1.4-2.8-2.8-3.6c-3.9-2-7.9-4-11.8-5.9
+ c0.9-0.9,2.1-0.8,3.3-0.9c9.3-1.3,18.6-2.8,28-3.7c11.7-1.1,23.1-4,34.9-4.2c7.4,3.5,13.1,9,16.9,16.1c2.8,5.2,0.4,8.3-5.4,9
+ c-6.1,0.7-12.2,0.6-18.3,1.1c-13,1.1-25.9,2.7-38.9,3.7c-13.6,1.1-27,2.2-39.7,7.7c-4.7,2-9.6,3.4-14.5,4.8
+ c-7,2.1-14.1,3.9-21.2,5.9c-1.6-0.4-3-1.1-4.7-1.4c-10.5-1.5-20.9-3.4-31.4-4.9c-7.8-1.1-15-3.9-22.5-6.2c-1.1-0.3-3.3-2.4-3.2,1.1
+ c-7.8-0.7-15.1-2.5-21.1-7.9c1.6-0.7,2.2-1.9,1.9-3.6c-0.1-0.8-0.6-1.2-1.4-1c-5.4,1.8-7.8-2.1-10.5-5.4c1.3,0,2.7,0,4,0
+ c0.4,0.4,0.8,0.9,1.2,1.3c0.1-0.2,0.3-0.4,0.4-0.6c-0.5-0.2-1.1-0.5-1.6-0.7c-1-1.1-1.8-2.4-2.9-3.4c-10.9-9.4-22.2-18.4-33.1-27.9
+ c-14.4-12.5-29.3-24.4-44-36.5c-4.4-3.6-6.7-7.6-6.1-13.6c0.6-5.8,1.6-11.4,4.1-16.7c2.4-5.2,6.3-9.6,9.8-13.9
+ c8.6-10.4,17.2-21,27.5-29.8c7-5.9,13.2-12.4,19.2-19.2c1.5-1.7,2.8-3.9,5.4-4.1c0.3-0.3,0.7-0.6,1-1l0,0c0.8,0,1.6-0.1,2-1l0,0
+ c0.3-0.3,0.6-0.7,1-1l0,0c0.3-0.4,0.6-0.8,1-1.2c2.7-4.6,7.4-6.4,11.8-8.6c7.4-3.7,14.9-7.2,22.4-10.7c5.8-2.8,10.4-8.1,14.7-13.2
+ c5.5-6.4,10.5-13.4,18.2-17.3c4.7-2.3,9.6-4,14.5-5.8c9.8-3.5,19-8.1,27.4-14.5c2.1-1.6,4.9-2.9,7.8-2.9c1.1,0,2.6,0.5,3.3,0
+ C256.7,71.2,261.3,72.5,261.9,67.9z"/>
+<path style="fill:#E794A1;" d="M419,270c-11.8-2.7-23.7-5.5-35.5-8.2c0-0.3,0.1-0.6,0.1-0.9c6.1,0.4,12.2,0.8,18.3,1.2
+ c0.9,0.9,1.9,0.9,3.1,0.7c0.7,0,1.3,0,2,0.1c10.9,0.2,21.9,1.6,32.7,0.1c10.9-1.5,19.3-7.7,25.7-16.2c2.4-3.2,1.6-7.6-1.6-9.9
+ c-3.2-2.2-5.5,1.6-8,3.2c-0.9,0.6-1.4,1.8-2,2.8c-5.2,8.4-12.9,11.9-22.5,12.3c-13.5,0.6-26.8-1.5-40.2-2.1
+ c-11.9-2-23.9-3.7-35.7-6.1c-26.8-5.5-53.5-11.6-77.6-25.5c0.2-0.2,0.4-0.4,0.5-0.7c15.4,5.1,30.7,10.8,46.6,14.6
+ c10.2,2.4,20.5,4.4,31.1,3.7c1.2-0.1,2.4,0.1,3.5-0.2c2.1-0.7,5.4-0.8,5.4-3.4c0-2.4-3.3-1.7-5-2c-35.5-7.5-70-18.4-103.4-32.6
+ c-3.3-1.4-6.7-2.7-9.2-5.7c8,3,16,6,24,9c13.7,6.2,28,10.2,42.4,14.1c16.6,4.6,33.4,8.5,50.5,11c1.3,0.2,2.6,1.1,3.2-1
+ c1.1-3.5,2.8-6.9,0-10.3c-3.7-4.9-8.3-9-10.9-14.6c-0.7-1.6-2.2-3.6-0.6-5c1.8-1.8,3.2,0.6,4.5,1.6c6.8,4.9,8.8,4.8,13.9-1.7
+ c0.6-0.7,1.2-1.1,2-1.3c1.8,0.3,3.7,0.3,5.3,1c13.3,4.9,26.7,9.8,39.9,15c13,5,28.3-0.8,34.9-13.5c1.2-0.2,2-0.9,2-2.2v0.1
+ c2.6-0.8,2.8-3.1,2.9-5.2c0.1-2.3-0.3-4.7,0-6.9c0.3-2.9,1.4,0.2,2.1-0.1l0.1,0.2c0.1,1.7-0.7,3.5,0.8,5c0,0.4,0.1,0.7,0.1,1.1
+ c0,1.4-0.5,2.9,0.8,4.1c0.3,0,0.7,0,1,0c0,0.6,0.1,1.3,0.1,1.9c-0.9,3.5-2.1,7-2.8,10.6c-0.5,2.9-1.2,6,1.2,8.5
+ c3.5,3.6,8.3,6.5,6.7,12.8c0.6,1.6,1.5,2.8,3.2,3c2.1,3.1,4.5,6.1,4.7,10.2c0.1,2.7,2.7,3.5,5,4.1c4.5,1.3,8.9,2.6,13.4,3.9
+ c3.2,0.9,4.4,2.6,2.8,5.9c-1.2,2.6-2.5,5-4.1,7.4c-2,3-3.3,6.2-1.3,9.7c-3.6,2-7.1,3.9-10.7,5.9c-0.4,0.3-0.9,0.7-1.3,1
+ c-4,3.1-4.9,7.4-5.1,12.1c-2.1-0.8-4.1-1.6-6.2-2.4c1.4,2.3,3.4,4,4.4,6.4c-5,4.1-10.5,7.7-16.1,11c-0.7,0.3-1.3,0.7-2,1h0.1
+ c-1.7-0.3-3.4-0.6-5.2-0.9c-0.2,1.9,1.9,1.7,2.3,3c-6.5,4.3-13.3,8.3-19.5,13c-3.5,2.7-7.3,3.9-11.4,5.2c-3.3,1-6.6-1.3-9.7,0.6
+ l-0.2-0.2l-0.3,0.1c-3.1-0.1-6,1.3-7.1,3.6c-3.1,6.2-8.4,5.5-13.8,5.5c-2.7,0-5.5,0.8-8.2,1.2c-2.3,0.4-4.5,1.1-6.2,2.8
+ c-4,3.3-8.4,4.9-13.7,4.2c-1.1-0.1-2.3,1.1-3.4,1.6c-4.6-4.8-7.4-0.4-10.5,2.1c-1.2-1.2-2.4,0.8-3.6,0.2c-4.1,0.4-8.4-0.6-12.5,0.6
+ c-6.3-1.6-13.1,0.3-19.3-2.6c7.9-2.3,16.2-0.5,24.2-1.9c10.2-1.8,20.7-1.7,30.7-5.8c10.4-4.3,21-8.2,32-10.8
+ c6.3-1.5,12.6-2.9,18.8-4.7c2.8-0.8,5.6-2.2,5.5-6.1c-0.1-3.6-2.6-4.9-5.4-5.7c-5.3-1.5-10.6-2.7-15.9-4.1c-5-1.3-10.1-2.7-15.1-4.1
+ c9.8-1.9,19.6-2.5,29.2-4c10.3-1.6,20.7-2.5,30.7-5.8c1.2-0.4,2.9-0.6,2.9-2.2c0-1.5-1.3-2.3-2.5-2.8c-1.5-0.7-3.1-1.3-4.7-1.7
+ c-9.6-2.1-19.5-1.9-29.3-2c-9.7-0.1-19.5,1.5-29,3.7c-9.6,2.2-19.3,4.1-29,6.1c-11.8,0.2-23.2,3.1-34.9,4.2
+ c-9.4,0.9-18.7,2.5-28,3.7c-1.1,0.2-2.3,0-3.3,0.9c-1.3,0.1-2.6,0.2-3.9,0.3c-0.3,0-0.7,0-1,0c-9.2,1-18.5,1.9-27.7,3
+ c-25,3-50,4.3-75.2,3.7c-11.7-0.2-23.2-1.5-34.1-6.1c-4.6-2.9-9.4-5.5-13-9.8c5.5,2.3,11,4.6,16.8,6c29.3,7.4,59.1,8.2,89,5.6
+ c10.1,0,20.1-1.7,30.2-2.6c16.3-1.5,32.6-3.9,48.7-6.9c18.6-3.5,37-7.4,55.5-11.3c6.7-1.4,13.3-3.3,21-5.2
+ c-15.5-2.6-29.7-4.9-43.9-7.4c-14.8-2.6-29.6-5.4-44.5-8.1c-0.6-0.1-1.2-0.1-1.9-0.2c-0.4-0.3-0.7-0.5-1.1-0.8
+ c-3.1-0.1-5.7-1.4-8.4-2.7c1-1.8,2.6-1.3,4.1-1.2c15.7,1.2,31.3,2.4,46.8,4.7c14.2,2.1,28.4,4.4,42.6,6.6c1.6,0.3,3.3,0.3,4.9,0.5
+ c0.4,0.3,0.8,0.7,1.2,0.8c12.6,2.1,25.3,3.6,37.7,6.3c14.5,3.1,27.9,0.9,41.1-5c4.3-1.9,5.9-5.5,7.4-9.4c0.7-1.7-0.2-2.6-1.9-2.5
+ c-1.5,0-3,0.1-4.4,0.4C459.3,267.6,439.4,271.3,419,270z"/>
+<path style="fill:#7A7A7A;" d="M378.8,342c1.7-1.7,3.9-2.4,6.2-2.8c4.1,3,9.1,4.3,12.9,7.9c2.3,2.2,4.3,6.8,7.5,5.4
+ c3.1-1.3,2.1-6.1,2.6-9.5c0.8-5.2,0.2-11.1,6.1-14.2l0.3-0.1l0.2,0.2c5.9-0.1,10.6,3.3,15.8,5.4c3,1.2,6.7,3.5,9.1,0.2
+ c2.3-3-0.9-5.8-2.6-8.3c-0.2-0.2-0.1-0.6-0.2-1c7.6,1.4,14.9,3.4,22.5,4.1c5,0.5,7.2-1.1,7.9-5.8c0.8-5.6-0.3-9.8-6.3-11.6
+ c-1-0.3-1.8-1.3-2.7-2c0.1-0.6,0.1-1.2,0.2-1.9h-0.1c0.5,0,1-0.1,1.6-0.1c7.6-0.3,15.3,2,22.9-0.6c0.3,0.3,0.5,0.5,0.8,0.8
+ c-2.4,4.4-5.1,8.6-7.2,13.1c-17.7,39-36.4,77.6-51.8,117.6c-3.2,8.2-8.1,13.9-16.4,17.6c-7.9,3.4-15,8.5-22.4,13
+ c-8.9,5.5-16.7,12.4-26.7,16.4c-22.8,9.2-45.3,19.1-70.3,21c-8.1,0.6-16.1,3.1-24.2,1.1c-6-1.4-11.9-3-17.9-3.9
+ c-1.3-0.2-2.3-0.5-2.5-2c-0.5-4-3-6.9-5.1-10c2.2-0.1,2.2-1.2,1.7-3c-0.6-2.3-1.4-4.8,0.5-7c2.3,3.1,4.5,6.4,7,9.4
+ c2.1,2.5,4.6,3.1,6.7-0.2c1.6-1.1,2-0.3,2.7,1.2c0.7,1.6,2.1,3.3,4.2,2.1c1.7-1,3.7-2.3,3.1-5c-0.2-0.9-0.3-1.9-0.5-2.8
+ c3.7,0.7,4.7,4.4,7,6.6c5.3,5.1,8.2,4.8,12-1.4c0.8,0,1.5,0,2.3-0.1c5.6,0,10.9-0.9,14.4-6c1.8,1.7,2,4,2.2,6.3
+ c0.1,1.2,0.8,1.7,1.9,2c0.9,0.2,1.3-0.6,1.3-1.1c0.4-4.3,3.2-4.7,6.6-4.1c7.7,5.6,8.4,5.5,12.9-1c0.6-0.3,1.3-0.9,1.8-0.7
+ c5.7,1.7,7.8-1.8,9.2-6.3c3.5-11.4,3.6-23.5,6.5-35c1.2,0.5,0.2,3.1,2.3,2.9c0.4,0.9,0.9,1.8,1.2,2.7c2.3,6.3,3.2,6.5,8,2.3
+ c0.3,6.3-3.2,11.9-3.9,19.3c0.9-1.6,1.2-2,1.4-2.4c8.2-22.7,15.6-45.7,22.7-68.8c3.1-9.8,4.6-19.6,3.1-29.8
+ c-0.7-2.9,1.1-5.6,0.7-8.5c1-2.3,0.2-4.6,0.1-6.8c1.5-2.3,1.3-4.6-0.1-6.9C377.7,347.3,377.5,344.5,378.8,342z M392,454
+ c0.4-0.9,0.8-1.8,1.3-2.7c15.3-35.7,30.5-71.4,45.7-107.1c0.5-1.1,1.8-2.7,0.3-3.6c-1.9-1.1-2.5,1.1-3.2,2.3
+ c-4.8,8.5-8.8,17.5-12.7,26.5c-11.4,26.7-23.8,53-30.9,81.3C392.4,451.8,392.2,452.9,392,454c0,0.1-0.1,0.2-0.2,0.3
+ C391.9,454.2,391.9,454.1,392,454z M450,348c-1.5,0.8-1.8,2.3-2.1,3.9c-0.7,1.3-1.5,2.6-2.2,3.9c-13.8,25.5-23,52.9-33.5,79.8
+ c-0.7,1.9-2.4,3.7-1.7,7.1c1.1-2.4,1.9-3.8,2.5-5.3c6.2-14.7,12.5-29.3,18.7-44c5.7-13.6,11.1-27.3,16.7-41
+ C449.3,351,450.1,349.7,450,348c0.3-0.3,0.5-0.7,0.8-1c-0.2-0.1-0.4-0.2-0.5-0.4C450.2,347.1,450.1,347.5,450,348z M403.3,362.9
+ c-8.2,13.7-30.6,81.4-27.7,96.7C385.5,427.2,396.7,395.7,403.3,362.9z M406.6,450.6c-0.1,0-0.3,0-0.3,0.1c-0.1,0.1-0.1,0.3-0.1,0.4
+ c0.2,0,0.4-0.1,0.6-0.1C406.7,450.9,406.7,450.7,406.6,450.6z M451.6,343.8c-0.1,0-0.2,0-0.2,0.1c-0.1,0.1-0.1,0.3-0.1,0.4
+ c0.2,0.4,0.3,0.3,0.4-0.1C451.7,344.1,451.7,344,451.6,343.8z M409.8,444c-0.1-0.3-0.2-0.6-0.3-0.8c-0.1,0.1-0.3,0.2-0.3,0.4
+ c0,0.3,0.1,0.6,0.1,0.9C409.5,444.3,409.7,444.2,409.8,444z M407.9,448.3c-0.1-0.3-0.2-0.5-0.3-0.8c-0.1,0.1-0.4,0.2-0.4,0.3
+ c0.1,0.3,0.2,0.6,0.3,0.8C407.6,448.5,407.8,448.4,407.9,448.3z"/>
+<path style="fill:#272424;" d="M238.8,492c2.1,3.1,4.6,6.1,5.1,10c0.2,1.5,1.2,1.8,2.5,2c6.1,0.9,12,2.5,17.9,3.9
+ c8.1,1.9,16.2-0.5,24.2-1.1c25-1.9,47.5-11.8,70.3-21c9.9-4,17.8-10.9,26.7-16.4c7.4-4.5,14.5-9.6,22.4-13
+ c8.3-3.6,13.2-9.4,16.4-17.6c15.5-40,34.1-78.6,51.8-117.6c2.1-4.5,4.8-8.7,7.2-13.1c0.4-0.3,0.8-0.5,1.2-0.8
+ c0.3,0.3,0.6,0.5,0.8,0.8c-2.3,6.4-6.9,11.9-8.3,18.7c-17.8,33.3-30.8,68.8-45.5,103.9c10.6-2.9,17.4,3,24.3,8.6
+ c3.3,2.7,6.8,5.3,10.3,7.8c2.6,1.9,5.1,3.6,8.7,4c5.9,0.7,8.4,5.6,9.2,10.9c0.9,6.8,3.2,12,9.5,15.8c5.9,3.5,4.8,10.9-1.5,13.6
+ c-12.3,5.2-24.4,10.9-37,15.1c-9.5,3.1-19.8,4.6-29.9,4.2c-19.1-0.6-37.6,3.3-56.3,5.9c-17.6,2.4-35.2,3.1-53.1,0.6
+ c-18.5-2.5-36.7-7.3-55.7-6.6c-7.6,0.3-14.7-4.3-22.2-6.2c-1.9-0.5-3.8-1.2-5.7-1.8c0.4-3.7,3.4-6.9,1.9-10.9
+ c-0.1-1.5,1-2.6,1.7-3.7C237.5,488.8,237.2,491.2,238.8,492z"/>
+<path style="fill:#F5CFD6;" d="M295,110c-0.4-3-3.3-2.1-5-2.7c-1.8-0.7-3.4-0.9-3.1-3.3c0.7,0,1.3,0,2,0c5.6,1,11.2,2.3,16.9,2.9
+ c15.1,1.8,30.3,2.9,44.8,8.1c1.3-3.3-1.5-5.1-2.4-7.3c-4.5-10.6-11.1-19.4-23-22.5c-6.3-1.6-11.4-5.9-18.2-7.5
+ c4.3,0.1,5.8-1.2,4-5.1c-0.5-1.1-0.4-2.7,0.9-3.6c0.2,1.8,1.1,3.4,2.8,3.8c11.1,2.2,19.3,9.9,28.9,14.9c5.5,2.9,11.4,4.4,16.9,7.1
+ c2.1,1,4.3,1.9,5.4,4.2c-6.9-1.6-13-5.8-20.4-6.3c16.6,11.8,32.6,24.2,47.7,37.8c-3.4-9-8.9-16.4-16.2-22.5
+ c-2.8-2.3-5.4-4.8-8.1-7.1c7.5,1.2,14.2,3.2,18.5,10.6c6.1,10.5,15.1,18.7,23.7,27.2c1.4,1.4,2.7,2.9,4.1,4.3
+ c-5.2-0.8-9.7-3.6-13.6-6.8c-6.3-4.9-13.8-2.3-20.5-4.1c-0.6-0.2-1.4,0-1.6,0.8c-0.2,0.8,0.2,1.3,0.8,1.6c5.4,2.7,10.6,6.2,16.9,6.4
+ c9.4,0.3,15,6.1,19.8,13c-9.1-1-17.8-4.1-27.1-3.9c11.2,3.9,22.6,7.5,33.9,10.9c2.4,0.7,4.6,1.6,2.1,4.2c-1.3,1.4-3.5,2.5-5.9,1.3
+ c-4-2.1-8-4-12-6c-9.5-4.7-19.2-8.9-30.1-9.5c-0.5-6.4-2.1-8.4-8.1-9.8c-17.5-4-35.2-7.2-52.5-12.1c-7.3-2.1-14.6-4.3-22.5-6.7
+ c2.8-2.3,4.7-0.3,6.7-0.2c1.8,0.1,3.4,0.6,3.5-1.9c1.5-0.6,0.7-2.6,1.9-3.3c1,5.6,4.7,8.8,8.5,6.7c4.2-2.3,8.4-1.2,12.6-1.3
+ c1,0,2.4,1.3,2.8-0.6c0.3-1.3-0.8-1.9-1.9-2.5c-5.6-3.4-11.5-5.6-18-6.2C305.6,111.9,300.3,111,295,110z M343.1,91.8
+ c0.1-0.1,0.2-0.2,0.3-0.2c-0.1-0.1-0.2-0.2-0.3-0.3c-0.1,0.1-0.3,0.3-0.4,0.4C342.8,91.7,343,91.7,343.1,91.8z"/>
+<path style="fill:#BDBCBC;" d="M154.9,325c6,5.5,13.3,7.2,21.1,7.9l0,0c1.4,1.9,3.3,2.2,5.5,2l0,0c0.8,0.9,1.8,1.1,3,1
+ c1.5,0,2.6,0.4,3,2c1.6,1.3,3.4,2.3,5.5,2l0,0c0.3,1,1.1,1.2,2,1l0,0c-1.4,3.7-0.7,7.7-1.2,11.5c-0.3,2.5-0.1,5.7-3.6,6.3
+ c-3.3,0.6-4.6-1.9-5.9-4.3c-1.6-3-2.5-6.2-3.1-9.4c-0.6-3.3-2.1-3.7-4.9-2.3c-4.8,2.3-9.3,5.2-14.5,6.7c-1.1,0.3-2.2,0.8-3.3,1.1
+ c-8.7,2.1-12-0.9-10.1-9.7c0.3-1.6,1-3.2,0.4-5.2c-3.2,0.4-5.5,2.7-8.1,4.2c-7.2,3.9-11.2,2.7-14.6-4.7c-1.1-2.4-2.3-4-5.2-1.8
+ c-2.2,1.6-4.9,2.9-7.2,0.4c-2.2-2.4-3.2-5.4-1.5-8.7c0.6-1.1,2.4-2.2,1.4-3.4c-1.1-1.5-3.1-0.7-4.6-0.2c-4.7,1.5-9.5,1.5-14.3,1.6
+ c-3.1,0-4.7-1.3-4.5-4.5c0.1-0.8-0.1-1.7,0-2.5c1.1-5.8-1.8-8.8-6.6-11.6c-6.2-3.7-9.6-9.8-10.4-17.1c-0.2-1.5-0.1-3.1-0.6-4.4
+ c-1.5-4.1-1.9-8,0.5-11.8c2.8,0.5,4,3.9,7,4.1c1,1.8,2.1,3.7,3.1,5.5c0.3,0.1,0.6,0.2,0.9,0.4L84,281c0.4,0.6,0.9,1.1,1.3,1.7
+ c1.5,1.9,2.5,4.2,4.8,5.4L90,288c0.7,1.1,1.5,2.3,2.2,3.4c0.6,2.4,1.8,4.4,4.7,4.4c0.1,0.1,0.2,0.3,0.3,0.4c-0.2,1.5,0.7,1.8,2,1.8
+ c3,2,6.2,3.4,9.7,4c2.3,2,4.9,3.8,7.8,3.9c5.8,0.3,10.5,3.3,14.7,6.3C138.7,317.6,146.8,321.3,154.9,325z"/>
+<path style="fill:#110D0E;" d="M315.8,348.1c6.2,2.9,12.9,1,19.3,2.6c4.2,0.4,8.4,1.3,12.5-0.6c1.2,0.6,2.3-1.3,3.6-0.1
+ c-2.1,3.3-3,6.8-3.1,10.7c0,5.6-2.6,7.9-8,7c-6.7-1.1-13.1-3.1-17.9-8.5c-2-2.2-4.3-4.2-6.7-5.9c-5.9-4.3-9.6-3.1-12.6,3.8
+ c-2.3,3.9-4,8.3-7.9,11c-6.4,1.8-9.1,0.2-13.3-7.9c-1,0.7-1.3,1.8-1.6,2.8c-4.2,5-8.8,9.6-15.2,11.8c-8.5,0.2-12.9-5.5-16.7-11.8
+ c-1.7-2.8-3.1-4.1-6-1.8c-4.7,2.2-9.4,4.4-14.1,6.6c-11.9,2-13.4,0.7-14-11.6c-0.1-1.8-0.2-3.6-0.2-5.5c-0.2-4.3,3-4,5.5-3.4
+ c4.1,1.1,8.7,2.1,12,4.5c7.7,5.7,15.9,4.1,24,2.6c7.9-1.5,15.4-5,23.8-4.5c2.1,0.1,4.2-0.8,6.1-1.9
+ C295.3,347,305.6,346.4,315.8,348.1z"/>
+<path style="fill:#F7E3E5;" d="M312,69c-1.3,1-1.4,2.5-0.9,3.6c1.8,3.9,0.3,5.3-4,5.1c6.7,1.7,11.9,5.9,18.2,7.5
+ c11.9,3.1,18.4,11.9,23,22.5c1,2.3,3.7,4.1,2.4,7.3c-14.5-5.2-29.7-6.3-44.8-8.1c-5.7-0.7-11.3-1.9-16.9-2.9c1.3-1.5,3.1-1,4.7-1
+ c7-0.1,14-0.2,21,0c3.6,0.1,4.5-1.3,4.4-4.6c-0.3-10-5.5-15.3-15.3-15.4c-1.5,0-3,0.1-4.5,0c-6.2-0.7-11,0.6-12.9,7.6
+ c-0.5,1.9-2.5,4.3-5.1,4.1c-2.6-0.2-3-2.9-3.7-4.8c-2.3-6-0.2-10.2,5.9-11.5c5.1-1.1,10.4-1.7,15.6-2.3c1.8-0.2,3.1-0.5,3.8-2.5
+ c2.7-7.3,4.3-14.7,4.2-22.7c-0.1-5.4,0.3-10.9,3.3-15.8c0.6-1,1.1-2.1,2.5-2.1c-2.1,5.9-3,12-3,18.3c0,1.8-0.2,3.7,1.9,4.7
+ C310.3,60.3,310.8,64.6,312,69z"/>
+<path style="fill:#F5CFD6;" d="M67,217c-2.6,5.3-3.6,10.8-4.1,16.7c-0.6,6,1.7,10.1,6.1,13.6c14.7,12.1,29.7,23.9,44,36.5
+ c10.9,9.5,22.2,18.5,33.1,27.9c1.1,1,2,2.2,2.9,3.4l0,0c-1.3,0-2.7,0-4,0c-0.3,0-0.6,0-1-0.1c-0.3-0.3-0.7-0.6-1-0.9
+ c-10.4-4.7-20.7-9.4-31.1-14.1c-3.1-3-7.4-3.8-10.9-6c0.3-2.5-1.3-2.8-3.2-3.2c-2-0.5-3.9-1.5-0.4-3.8c-2.1,0.5-4.1-0.7-5.4,1.2
+ c-0.6-0.1-1.2-1.2-1.9-0.2l0.1,0.1c1-0.7-0.1-1.3-0.1-2c1.6-2,0-1.8-1.2-1.8l0.1,0.1c0.2-1.7,0.7-3.6-1.5-4.4
+ c-1.2-0.5-1.2,0.6-1.3,1.3c-0.7,0-1.3-1.2-2-0.2l0.1,0.1c0.9-0.7-0.1-1.2-0.1-1.9c0.9-3.5-1.9-4.6-4-6.2l0,0
+ c0.1-1.4-0.5-2.3-1.8-2.8c-0.9-3-1.9-6-5.2-7.3c-1.7-2.6-3.4-5.2-5-7.8c-0.3-0.4-0.7-0.8-1-1.1l0,0c0-0.8-0.1-1.6-1-2l0,0
+ c0-0.9-0.1-1.6-1-2l0,0c0-0.9-0.2-1.6-1-2l0,0c-0.4-0.3-0.9-0.7-1.3-1c-2.5-2.7-3.6-5.6-2.8-9.4c0.8-3.5-0.7-6.9-2.7-9.8
+ c-1.3-1.8-1.4-3.5,0.4-4.7C60.6,220.9,63,217.6,67,217z"/>
+<path style="fill:#E794A1;" d="M378,151c10.8,0.6,20.5,4.8,30.1,9.5c4,2,8,3.9,12,6c2.4,1.2,4.6,0.1,5.9-1.3
+ c2.5-2.6,0.3-3.4-2.1-4.2c-11.4-3.4-22.7-7-33.9-10.9c9.3-0.3,18.1,2.8,27.1,3.9c11.7,2.3,23.5,4.6,32.4,13.6c0.7,0.7,1.7,1,2.6,1.5
+ c2.1,2.9,6.5,3.7,7,8c-3.3-2.1-5.8-5.4-10-6c-7.8-4.3-16.4-1.4-24.6-2.2c-8.3-0.8-17-0.2-25.5-1.4c-9.9-1.4-19.9-3-29.8-4.5h-0.1
+ c-0.5-0.8-1.2-0.9-2.1-0.8c-11.2-2.1-22.2-4.9-33.1-8.2c-1.3-0.4-3-0.8-2.9-2.8c0.3,0,0.6-0.1,0.9-0.2
+ C347.3,148.8,362.6,150.6,378,151z"/>
+<path style="fill:#5F5F5F;" d="M238.8,492c-1.6-0.8-1.3-3.2-3.1-3.8c0.4-2.2-1-3.8-1.8-5.5c-3.5-7.6-6.3-15.3-7.3-23.7
+ c-1.3-11.2-6.8-21.5-8-32.8c-1.1-10.6-3.5-20.9-6.1-31.2c-1.4-5.6,0.7-10.8,1.7-16.2c0.2-1,0.8-2,1.9-2c1.3,0.1,1,1.5,1.4,2.3
+ c7.2,15.8,10.4,32.8,14.4,49.6c3.6,15.4,4.6,31.2,6.9,46.8c0.4,2.4,1.7,4.3,2.2,6.5c-1.9,2.2-1.1,4.7-0.5,7
+ C241,490.8,241,491.9,238.8,492z"/>
+<path style="fill:#5F5F5F;" d="M264.2,486.7c0.2,0.9,0.3,1.9,0.5,2.8c0.6,2.7-1.4,3.9-3.1,5c-2.1,1.2-3.5-0.5-4.2-2.1
+ c-0.6-1.5-1.1-2.3-2.7-1.2c1.6-5.6-1.5-10.7-2-16.1c-1-10-2.6-20-3.9-30c0.2-1.4,0.5-2.8-0.5-4.1c-1.5-11.9-3-23.9-4.5-35.8
+ c-0.3-6.1-1.5-12.1-0.8-18.2c2.9,0.9,3.8,3.6,4.9,5.9c5.2,10.6,8.6,21.7,9.7,33.6C259.4,446.6,262,466.6,264.2,486.7z"/>
+<path style="fill:#BDBCBC;" d="M205.3,466.4c-1.4-9.9-4.8-19.3-7.3-28.9c-4.6-18.1-9.5-36.2-12.3-54.7c-0.3-2.2,0-3.9,1.9-5.8
+ c3.8,6.3,5.6,13.1,7.7,19.8c7.8,24.8,13.8,50.1,20.2,75.4c0.4,1.6,0.6,3.3,0.4,4.9c-4.3,5.1-4.4,5-6.9-1.1
+ C207.8,472.8,206.6,469.6,205.3,466.4z"/>
+<path style="fill:#BDBCBC;" d="M243,387c-0.7,6.1,0.5,12.1,0.8,18.2c-1.3,1.4-0.9,3.2-0.7,4.8c0.7,6.1,1.9,12.1,2.4,18.1
+ c0.3,3.4,2.4,6.9,0.4,10.4c-4-9.5-6.2-19.5-9.3-29.3c-3.7-11.8-7.1-23.7-10-35.8c-0.6-2.4-0.6-4.1,1.3-5.7c4.7-2.2,9.4-4.4,14.1-6.6
+ c1,1.4,1,3.1,1,4.7C243,372.9,243,379.9,243,387z"/>
+<path style="fill:#BDBCBC;" d="M158.3,363.3c19.1,35.9,25.8,75.2,37.7,113.3C188.9,471.4,157.7,377.2,158.3,363.3z"/>
+<path style="fill:#E794A1;" d="M285.1,348c-1.9,1.1-4,2-6.1,1.9c-8.3-0.5-15.8,3-23.8,4.5c-8.2,1.5-16.3,3.1-24-2.6
+ c-3.3-2.5-7.8-3.4-12-4.5c-2.4-0.6-5.6-0.9-5.5,3.4c-0.2-3.6-2.4-5.2-5.8-5.6c-4.5-0.6-8.3-3.8-13-4.1l0,0c-0.3-1.1-1.2-1.1-2-1l0,0
+ c-1.5-1.7-3.3-2.3-5.5-2c-0.4-1.5-1.5-2-3-2c-0.8-0.9-1.8-1.2-3-1l0,0c-1.4-1.9-3.2-2.6-5.5-2l0,0c-0.1-3.6,2.1-1.5,3.2-1.1
+ c7.4,2.3,14.7,5,22.5,6.2c10.5,1.5,20.9,3.4,31.4,4.9c1.7,0.3,3.2,1,4.7,1.4c2,6,6.4,8.3,13,7.3C262.2,349.9,273.5,347.1,285.1,348z
+ "/>
+<path style="fill:#5F5F5F;" d="M205.3,466.4c1.3,3.2,2.5,6.4,3.8,9.6c2.4,6.1,2.6,6.1,6.9,1.1c1.5,4.8,3,9.7,5.8,14
+ c4.5,6.7,7.1,6.9,12.2,0.9c1.4,4-1.5,7.2-1.9,10.9c-7.3-2-14.3-4.8-21.8-6.1c-6.6-1.1-12.6-9.5-13.4-17.4c0.9,1,1.6,2.1,2.6,3
+ c1.4,1.3,2.6,3.7,4.8,2.6c1.9-0.9,1.6-3.3,1.8-5.3C206.7,475.1,204.3,470.9,205.3,466.4z"/>
+<path style="fill:#F7E3E5;" d="M452,169.1c-0.9-0.5-1.9-0.8-2.6-1.5c-8.9-9-20.6-11.3-32.4-13.6c-4.9-7-10.4-12.7-19.8-13
+ c-6.2-0.2-11.4-3.7-16.9-6.4c-0.6-0.3-1-0.8-0.8-1.6c0.2-0.8,1-1,1.6-0.8c6.7,1.9,14.2-0.8,20.5,4.1c4,3.1,8.4,5.9,13.6,6.8
+ c7.7,5.7,17.5,7.3,25.5,12.4C445.8,158.7,451.4,161.8,452,169.1z"/>
+<path style="fill:#7A7A7A;" d="M484.4,307.4c-0.4,0.3-0.8,0.5-1.2,0.8c-0.3-0.3-0.5-0.5-0.8-0.8c1.2-5.3-3.1-8-5.4-11.6
+ c0.4-1,0.8-1.9,1.2-2.9c2.7,0.5,5.5,1.3,8.2,1.5c4.6,0.2,6.1-2.4,3.6-6.3c-1.9-3-4.1-6-7.1-8.1c0.4-0.3,0.9-0.7,1.3-1
+ c5.6,1.4,11.3,2.8,16.9,4.2c0.9,1.3,0.8,2.3-0.8,2.9c-2,0.6-2.9,2.5-3.9,4.1c-0.6,1-3.4,1.8-0.7,3.6c0.2,3.3-2.2,5.4-3.6,7.8
+ c-4.8,8.6-9.2,17.5-15.1,25.4c1.4-6.8,6-12.3,8.3-18.7c0.7-1.3,1.4-2.6,2.1-3.9c0.2-0.4,0.5-0.9,0.7-1.3c0.9-0.3,1.1-1,1.2-1.9
+ c0.5-0.2,0.8-0.5,0.3-0.9c-0.2,0.2-0.3,0.5-0.5,0.7c-0.9,0.4-1.2,1.1-1,2c-0.8-0.1-0.9,0.4-1.1,0.9C485.5,304.6,485,306,484.4,307.4
+ z"/>
+<path style="fill:#F7E3E5;" d="M369,100.9c2.7,2.4,5.3,4.9,8.1,7.1c7.4,6,12.8,13.5,16.2,22.5c-15.1-13.6-31.1-26.1-47.7-37.8
+ C353,93.2,359,97.4,366,99C367,99.6,368,100.3,369,100.9z"/>
+<path style="fill:#BDBCBC;" d="M160,416.7c5.9,17.5,15.2,33.8,18.7,52.1c0.3,1.7,1.2,4.1-0.9,4.9c-2,0.7-2.4-1.9-3-3.2
+ c-3.6-7.3-6.4-14.7-7.3-23C166.3,436.9,162.4,427,160,416.7z"/>
+<path style="fill:#F7E3E5;" d="M449,171c4.2,0.6,6.8,3.9,10,6l0,0c0.4,0.9,0.7,0.8,1.1,0c3.4,1.7,2.8,4.9,3,7.9
+ c-0.7,0.3-1.8-2.8-2.1,0.1c-0.2,2.3,0.1,4.6,0,6.9c-0.1,2.1-0.3,4.4-2.9,5.2c-0.3-3.2,1-6.2,1.5-9.3c0.5-3.5-2.4-9.6-5.3-9.6
+ C448.6,178.3,448.8,174.9,449,171z"/>
+<path style="fill:#110D0E;" d="M501.1,283.1c-5.6-1.4-11.3-2.8-16.9-4.2c3.6-2,7.1-3.9,10.7-5.9c2.1,2.4,4.1,4.9,6.2,7.3
+ C501.9,281.3,502.6,282.2,501.1,283.1z"/>
+<path style="fill:#BDBCBC;" d="M154.9,440.7c4.5,7.8,6.4,16.6,10.8,24.5c-2.5-1-4.6-3.4-9.3-10.6C153.6,450.2,156,445.2,154.9,440.7
+ z"/>
+<path style="fill:#F5CFD6;" d="M311.9,55.9c-2.1-0.9-1.9-2.8-1.9-4.7c-0.1-6.3,0.8-12.4,3-18.3l0,0c0.3-0.3,0.6-0.7,0.9-1
+ c0.3-2.6,2.3-4.4,3.5-6.5c0.4-0.7,1.2-1.4,2-0.9s0.6,1.4,0.2,2.2c-0.3,0.6-0.5,1.3-0.9,1.8C312.4,36.7,312.4,46.3,311.9,55.9z"/>
+<path style="fill:#F7E3E5;" d="M245.9,438.5c2-3.5-0.1-7-0.4-10.4c-0.5-6.1-1.7-12.1-2.4-18.1c-0.2-1.6-0.6-3.3,0.7-4.8
+ c1.5,11.9,3,23.9,4.5,35.8c-0.4-0.1-0.9-0.3-1.3-0.4l0,0C246.7,439.8,246.3,439.2,245.9,438.5L245.9,438.5z"/>
+<path style="fill:#F7E3E5;" d="M73,263c3.3,1.3,4.2,4.3,5.2,7.3c0.2,1.2,0.3,2.4,1.8,2.8l0,0c0,0.7,0.1,1.5,0.1,2.2
+ c-3-0.2-4.2-3.6-7-4.1C73,268.4,73,265.7,73,263z"/>
+<path style="fill:#F5CFD6;" d="M261.9,67.9c-0.6,4.6-5.2,3.3-7.7,5c-0.8,0.5-2.2,0.1-3.3,0C254,70.1,257.8,68.6,261.9,67.9z"/>
+<path style="fill:#110D0E;" d="M495.6,293.7c-2.7-1.7,0.1-2.6,0.7-3.6c1-1.6,1.9-3.4,3.9-4.1C498.7,288.6,497.2,291.1,495.6,293.7z"
+ />
+<path style="fill:#BDBCBC;" d="M247,440.5c0.4,0.1,0.9,0.3,1.3,0.4c1.1,1.3,0.8,2.7,0.5,4.1C246.2,444.4,247.2,442.2,247,440.5z"/>
+<path style="fill:#F7E3E5;" d="M466.1,197c0-0.6-0.1-1.3-0.1-1.9c0.1-1.1,0.2-2.2,0.3-4.5C468.5,193.7,467,195.3,466.1,197z"/>
+<path style="fill:#7A7A7A;" d="M474.4,231.9c-1.8-0.3-2.7-1.5-3.2-3C473,229.1,473.8,230.4,474.4,231.9z"/>
+<path style="fill:#F7E3E5;" d="M464,189.9c-1.5-1.4-0.7-3.3-0.8-5C464.6,186.4,463.8,188.3,464,189.9z"/>
+<path style="fill:#F7E3E5;" d="M464.9,195.1c-1.3-1.2-0.8-2.7-0.8-4.1C465.4,192.2,464.9,193.7,464.9,195.1z"/>
+<path style="fill:#F7E3E5;" d="M156,405c0.7,0.5,1.1,1.1,0.8,2c0,0.1-0.2,0.2-0.4,0.3C156.3,406.5,156.1,405.8,156,405L156,405z"/>
+<path style="fill:#F7E3E5;" d="M153.4,398.4c-0.1-0.3-0.1-0.4-0.2-0.6c0.1,0,0.2-0.1,0.4-0.1C153.6,397.9,153.5,398.1,153.4,398.4z"
+ />
+<path style="fill:#BDBCBC;" d="M66,252c0.9,0.4,1,1.2,1,2C66.2,253.6,65.9,252.9,66,252z"/>
+<path style="fill:#BDBCBC;" d="M65,250c0.9,0.4,1,1.2,1,2C65,251.7,64.9,250.9,65,250z"/>
+<path style="fill:#BDBCBC;" d="M64,248c0.8,0.4,1,1.2,1,2C64,249.7,64,248.9,64,248z"/>
+<path style="fill:#F7E3E5;" d="M154.4,401.3c0-0.1-0.1-0.3-0.1-0.4c0.1,0,0.3-0.1,0.4-0.1C154.9,401.1,154.8,401.3,154.4,401.3z"/>
+<path style="fill:#F7E3E5;" d="M156,405c-0.2-0.4-0.5-0.7-0.7-1.1c0,0,0.2-0.2,0.4-0.3C155.8,404.1,155.9,404.5,156,405L156,405z"/>
+<path style="fill:#F7E3E5;" d="M157.8,409.9c0,0.1-0.1,0.3-0.1,0.4c-0.1-0.1-0.3-0.3-0.4-0.4c0.1-0.1,0.2-0.2,0.3-0.3
+ C157.6,409.7,157.7,409.8,157.8,409.9z"/>
+<path style="fill:#F5CFD6;" d="M132,148c-0.4,0.8-1.1,1-2,1C130.4,148.2,131.1,147.9,132,148z"/>
+<path style="fill:#BDBCBC;" d="M67,254c0.3,0.4,0.7,0.8,1,1.1C67.6,254.8,67.3,254.4,67,254z"/>
+<path style="fill:#F5CFD6;" d="M130,149c-0.3,0.3-0.7,0.6-1,1C129.3,149.6,129.6,149.3,130,149z"/>
+<path style="fill:#E794A1;" d="M460.1,177c-0.4,0.8-0.7,0.9-1.1,0C459.3,177,459.7,177,460.1,177z"/>
+<path style="fill:#BDBCBC;" d="M62.7,247c0.4,0.3,0.9,0.7,1.3,1C63.6,247.7,63.1,247.3,62.7,247z"/>
+<path style="fill:#F5CFD6;" d="M133,147c-0.3,0.3-0.6,0.7-1,1C132.3,147.6,132.6,147.3,133,147z"/>
+<path style="fill:#BDBCBC;" d="M246,438.5c0.3,0.7,0.7,1.3,1,2C246.7,439.8,246.3,439.2,246,438.5z"/>
+<path style="fill:#F5CFD6;" d="M133.9,145.8c-0.3,0.4-0.6,0.8-1,1.2C133.3,146.6,133.6,146.2,133.9,145.8z"/>
+<path style="fill:#F7E3E5;" d="M313.9,31.9c-0.3,0.3-0.6,0.7-0.9,1C313.3,32.6,313.6,32.3,313.9,31.9z"/>
+<path style="fill:#FFFFFF;" d="M243.9,307.7c-30,2.5-59.7,1.7-89-5.6c-5.8-1.5-11.3-3.8-16.8-6L138,296c-2.5-3.6-5.8-6.1-9.9-7.6
+ c2-1.9,1.3-3.3-0.4-5.3c-4.4-4.9-10.1-8-15.4-11.5c-6.4-4.2-11.6-9.6-16.1-15.3c-5.5-6.9-10.7-14.2-14.4-22.5c-1.9-4.3-3-8.2,0.4-12
+ c1.5-1.8,0.4-5,3.5-5.7c0,6.7,0.8,13.2,3.1,19.5c3.1,8.3,9.2,14.2,15.3,19.9c7.8,7.3,15.8,14.4,24.4,22.2c-0.3-2.7-1.6-3.3-2.5-4.2
+ c-8.6-8.8-16.8-17.9-24.6-27.4c-11.2-13.6-12.3-17.7-8.4-34.4c1.5,7.6,4.2,13.6,7.8,19.1c0.1,1.5,0,3,2,3.2c0.1,0.9-0.2,2,0.9,2.4
+ c0.2,0.1,0.8-0.4,0.9-0.7c0.2-1.3-0.7-1.6-1.7-1.8c0.1-1.9-0.5-3.6-1.9-5c-2.9-8.5-4.4-17.1-2.5-26.1c3.4,3.4,7.7,4.9,12.1,6.2
+ c1.5,2.2,3.1,2.7,5,0.5c0,8.8,5.5,13.4,12.7,17c-0.9-3.6-2.3-6.9-3.7-10.3c-1.4-3.5-1.9-7.5-0.3-10.6c1.7-3.3,6.9-3.3,8.8-7.1
+ c0.1-0.3,1.8,0.3,2.7,0.5c1.6,0.3,3.2,0.7,4.8,1.1c0.1,0.7,0.1,1.6,1.2,1.5c0.1,0,0.3-0.2,0.3-0.3c0.1-1.1-0.8-1.1-1.5-1.2
+ c-2.8-3.3-5.5-6.6-8.3-9.8c-1.6-1.7-3.3-3.6-5.7-1c-0.3,0.3-0.6,0.5-0.9,0.8l0,0c-2.5-0.1-3.1,2.6-5,3.4c0.5-4.1-1.7-4.9-5.1-4.2
+ c-3.7,0.8-7.4,1.8-11.1,2.6c-1.6,0.4-3.5,0.5-4.4-1.3c-1-2,0.5-3.3,1.8-4.2c4.6-2.9,7.8-7.6,13.1-9.6c1.8-0.7,3.1-0.9,4.5,0.6
+ c1.3,1.4,1.5,2.9,0.6,4.6c-0.5,0.9-1.2,1.7-1.2,2.6c-0.1,1.5,1,2.5,2.3,3c1.5,0.5,2.1-0.6,2.4-1.8c3.2-11.6,14-10.7,22.5-13.7
+ c1.5-0.5,2,1,2.9,2c7.3,8.9,12.3,19.2,17.6,29.3c0.7,1.3,0.7,3.2,2.5,3.4c2,0.1,1.3-1.9,1.9-3c1-1.9,2-1.7,3.5-0.3
+ c2.8,2.5,3.7,6.2,5.8,9.2c6,8.5,10.7,17.8,15.1,27.2c5.6,12,15.9,19.8,24.6,29c0.6,0.6,1.3,1.3,2.1,0.6c0.8-0.6,0.5-1.5,0-2.3
+ c-7.6-12.4-13.7-25.7-21.6-37.9c-4-6.1-8.5-11.7-13.8-16.8c-10-9.6-16.4-21.9-23-33.8c-4.7-8.4-8-17.4-11.9-26.2
+ c-0.6-1.3-1.9-2.7-0.4-4.1c1.3-1.2,2.8-1.4,4.6-0.7c3.7,1.3,7,3.5,10.9,4.4c2,0.5,3.6,2.1,4,4.6c1.9,11.3,9.8,18.5,17.6,25.7
+ c14.8,13.4,31.7,23.8,48.8,34.1c12,7.3,24.6,13.2,37.1,19.4c5.5,2.7,11.2,5.3,16.6,8.4c-22.8-6-43.2-17.4-63.5-28.9
+ c7.7,7.6,17.8,11.7,25.5,19.5c-3.8-1.8-7.6-3.4-11.2-5.3c-4.8-2.5-9.5-5.4-14.4-7.9c-1.4-0.7-3-2-5-0.4c-1.9,1.4-3.3-0.5-4.7-1.6
+ c-6.7-5.1-13.3-10.3-19.9-15.5c-0.3,0.3-0.6,0.5-1,0.8c2.3,2.9,4.4,6,7,8.7c7.6,7.9,13.9,16.8,20.9,25.2
+ c10.8,13.1,18.1,28.3,25.6,43.3c2,4,0.2,5.6-2.7,6.8c-13.9,6-27.6,12.6-43.2,13.1c-0.2,0-0.5,0.3-1,0.7c14.5,1.2,28.1-1.8,41.6-6.3
+ c9.7-3.3,19.4-6.9,29.1-10.2c2.6-0.9,4.1,0.4,5,2.7c0.8,1.9,0.4,3.9,0.5,5.9C269.5,299,259,302,248.5,305
+ C247,305.6,244.7,305.4,243.9,307.7z"/>
+<path style="fill:#F5CFD6;" d="M285.1,348c-11.6-0.9-22.9,1.9-34.2,3.6c-6.6,1-11.1-1.2-13-7.3c7.1-1.9,14.2-3.8,21.2-5.9
+ c4.9-1.4,9.8-2.8,14.5-4.8c12.7-5.4,26.2-6.6,39.7-7.7c13-1,26-2.6,38.9-3.7c6.1-0.5,12.2-0.4,18.3-1.1c5.8-0.7,8.2-3.8,5.4-9
+ c-3.9-7.1-9.6-12.5-16.9-16.1c9.7-2,19.4-3.9,29-6.1c9.6-2.2,19.3-3.7,29-3.7c9.7,0.1,19.6-0.1,29.3,2c1.6,0.4,3.2,1,4.7,1.7
+ c1.2,0.5,2.6,1.3,2.5,2.8c0,1.6-1.7,1.8-2.9,2.2c-10,3.2-20.4,4.2-30.7,5.8c-9.6,1.5-19.4,2.1-29.2,4c5,1.4,10.1,2.8,15.1,4.1
+ c5.3,1.4,10.6,2.6,15.9,4.1c2.8,0.8,5.3,2,5.4,5.7c0.1,3.9-2.7,5.3-5.5,6.1c-6.2,1.8-12.5,3.2-18.8,4.7c-11,2.6-21.6,6.5-32,10.8
+ c-9.9,4.1-20.4,3.9-30.7,5.8c-8.1,1.4-16.3-0.4-24.2,1.9C305.6,346.4,295.3,347,285.1,348z"/>
+<path style="fill:#F5CFD6;" d="M449,171c-0.2,3.9-0.4,7.3,5.3,7.2c2.9,0,5.8,6.1,5.3,9.6c-0.5,3.1-1.8,6-1.5,9.3V197
+ c-1.5,0-2.1,0.8-2,2.2c-6.5,12.7-21.8,18.5-34.9,13.5c-13.3-5.1-26.6-10-39.9-15c-1.7-0.6-3.6-0.7-5.3-1c-0.4-0.5-0.7-1.5-1.1-1.5
+ c-11.1-0.5-21.4-5.1-32.3-6.3c-6.5-0.7-12.4-3.4-18.5-5.5c-1.1-0.4-2.1-1.1-2.1-2.5c1.7,0,3.3,0,5,0c1,0.5,2,1.2,3.1,1.4
+ c9,1.9,18,4.4,27.2,5.3c6.2,0.6,12.3,2.4,18.7,2.4c7.7,0,15.4,1.4,23.1,2.4c7.1,0.9,14.1,1.8,21.3,1.4c1-0.1,2.1,0,2.4-1.2
+ s-0.7-1.9-1.6-2.1c-2.6-0.6-4.8-2.1-7.3-2.9c-3.3-1.1-7.3-0.6-9.8-3.8c1.5-1.5,5.3-1.3,4.4-4.3c-0.8-2.7-4-1.5-6.2-1.5
+ c-11.7-0.1-23.5-0.1-35.2-0.1c-2.8-2.8-8.6-0.6-10-5.8c5,0.5,10.1,0.9,15.1,1.4c0-0.2,0.1-0.4,0.1-0.7c-6.4-1.6-12.8-3.2-19.3-4.8
+ c0.4-0.8,0.3-1-0.9-1.5c5.4-2.6,11.5-1.9,17.1-3.7c9.9,1.5,19.8,3.1,29.8,4.5c8.5,1.2,17.1,0.7,25.5,1.4
+ C432.6,169.6,441.2,166.7,449,171z"/>
+<path style="fill:#FFFFFF;" d="M327,181c-1.7,0-3.3,0-5,0c-22.5-5.8-44.2-14.6-67.4-18.2c-7.1-1.1-13-5.4-19.6-8.2
+ c-1.1-0.5-2.1-1.8-4.3-0.6c4.9,5.4,9.7,10.7,16.5,13c9.4,3.1,19.4,4.7,28.5,8.4c8.7,3.5,16.5,9.1,24.9,13.4
+ c9.5,4.9,17.7,11.3,25.8,18.3c-6.5-0.4-12-3.7-17.8-5.8c-9.8-3.6-18.4-9.7-27.9-14c-2.5-1.1-5-1.8-7.7-0.9
+ c-4.4,1.4-9.1-0.1-13.6,0.9c0.1,2.4,2.9,2,3,4.3c-6.9-1-12.6-4.4-18.1-7.8c-10.2-6.4-20-13.5-30.2-19.9c-3.1-2-4.9-5.3-8.2-6.9
+ c-0.3-0.3-0.7-0.7-1-1l0,0c-0.1-1.9-0.9-3.1-3-3l0,0c0.6-1.9-1-3-1.7-4.4c-4.4-8.1-8.3-16.3-4.7-25.8c4-1.8,6.8-5.8,11.2-7
+ c2.7-0.8,5.3-3.2,8.3-0.6c0,1.8-0.2,3.7-0.1,5.5c0.4,5.1,1,10.2,6,13.4c8.6,9.9,20.3,15,31.7,20.3c18.7,8.6,38.1,15.2,58.1,20.1
+ c5.1,1.3,10.3,2.1,15.6,2.6c2.9,0.3,1.1,1.3,0.8,2.3c1,1.2,2.5,0.6,3.7,1C329.8,181.5,328.3,180.8,327,181z"/>
+<path style="fill:#FFFFFF;" d="M289,305.2c1.3-0.1,2.6-0.2,3.9-0.3c3.9,2,7.9,3.9,11.8,5.9c1.5,0.8,3.3,1.6,2.8,3.6
+ c-0.4,1.7-2.2,2.2-3.8,2.8c-3.3,1.4-6.9,1.2-10.3,1.4c-14.3,1.2-28.5,3.7-42.8,5.3c-17.9,2-35.4,0.9-52.6-4.4
+ c-4.2-1.3-8.4-3.3-13.1-2.6c-1-3.2-3.4-3.1-6-2.2c2,0.7,4,1.5,6,2.2c9.8,4,19.2,9,30.8,11.9c-4.4,1.7-7.6,0.4-10.3-0.2
+ c-8-1.7-16-2.2-24-3.6c-7.6-1.3-14.4-4.8-21.8-6.7c-5.7-1.5-6.9-7.5-8.7-12.5c10.9,4.6,22.4,5.9,34.1,6.1
+ c25.1,0.5,50.2-0.7,75.2-3.7c9.2-1.1,18.5-2,27.7-3C288.3,305.7,288.7,305.6,289,305.2z"/>
+<path style="fill:#F5CFD6;" d="M243.9,307.7c0.9-2.3,3.1-2.2,5-2.7c10.5-3,21-6,31.1-10.1c4.7,1,8.6-1.8,12.8-3.1
+ c10.5-3.3,21.3-6,31.4-10.5c3.7-1.6,3.9-3.3,1-6c-3.4-3.1-7.4-5-11.7-6.3c-1.8-0.6-2.5-1.3-2.5-3c14.8,2.7,29.6,5.5,44.5,8.1
+ c14.2,2.5,28.4,4.8,43.9,7.4c-7.8,1.9-14.4,3.8-21,5.2c-18.5,3.9-36.9,7.8-55.5,11.3c-16.1,3-32.3,5.4-48.7,6.9
+ C264,306,254,307.7,243.9,307.7z"/>
+<path style="fill:#FFFFFF;" d="M223,107.4c-0.1-1.9,1-3,2.4-4.1c4.9-3.7,9.6-4,14.9-0.7c7.9,5,16.8,8,25.5,10.9
+ c4.7,1.5,8.2,4.4,10.5,7.8c6.7,10,16.6,14.5,27.1,18.9c8.3,3.4,17,5.7,25.9,9.5c-3.6,2.5-6.5,0.6-9.1,0.1
+ c-15.3-3.3-30.6-7.1-45.9-10.4c-8.6-1.9-15.8-6.3-23.2-10.7c-6.5-3.9-13-7.8-19.5-11.8c-1.4-0.8-2.8-0.9-4.3-0.9c0-2,0-4,0-6
+ c0.9-1.1,1-2.1-0.3-3C225.6,105.6,224.4,107.1,223,107.4z"/>
+<path style="fill:#F5CFD6;" d="M419,270c20.4,1.3,40.3-2.4,60.2-6.5c1.4-0.3,3-0.4,4.4-0.4c1.7,0,2.5,0.8,1.9,2.5
+ c-1.5,3.9-3.1,7.5-7.4,9.4c-13.2,5.9-26.7,8.1-41.1,5c-12.4-2.7-25.1-4.2-37.7-6.3c-0.4-0.1-0.8-0.5-1.2-0.8
+ c4.9-2.5,10-3.5,15.5-2.4C415.4,271,417.3,271.7,419,270z"/>
+<path style="fill:#F5CFD6;" d="M367.1,217.9c2.8,3.4,1,6.9,0,10.3c-0.6,2-1.9,1.2-3.2,1c-17.1-2.5-33.9-6.4-50.5-11
+ c-14.4-4-28.8-8-42.4-14.1c1.2-1.7,3.1-1.3,4.6-1c20.5,5.1,41.5,7,62.2,11.4c7.5,1.6,15.3,2.2,22.8,4.3
+ C362.5,219.4,365.1,219.9,367.1,217.9z"/>
+<path style="fill:#F5CFD6;" d="M391,253c13.4,0.6,26.7,2.7,40.2,2.1c9.7-0.4,17.3-3.9,22.5-12.3c0.6-1,1.1-2.2,2-2.8
+ c2.5-1.6,4.8-5.5,8-3.2c3.2,2.2,4,6.6,1.6,9.9c-6.4,8.6-14.8,14.7-25.7,16.2s-21.8,0.1-32.7-0.1c-3-3.9-7.9-4.7-12-6.8
+ C393.4,255.3,391.3,255.3,391,253z"/>
+<path style="fill:#E794A1;" d="M98.7,202.8c-1.9,9-0.4,17.6,2.5,26.1c-0.1,0.6-0.2,1.3-0.3,1.9c-3.7-5.5-6.4-11.5-7.8-19.1
+ c-3.9,16.7-2.8,20.8,8.4,34.4c7.8,9.5,16,18.6,24.6,27.4c0.9,0.9,2.1,1.5,2.5,4.2c-8.6-7.8-16.6-14.9-24.4-22.2
+ c-6.2-5.8-12.2-11.6-15.3-19.9c-2.4-6.4-3.1-12.9-3.1-19.5c0.9-7.8,3.6-14.8,8.8-20.7c0.9-1,1.7-2.9,3.5-1.7c1.4,1,2.2,2.4,1.8,4.3
+ C99.4,199.6,99.1,201.2,98.7,202.8z"/>
+<path style="fill:#E794A1;" d="M330.9,180.2c-1.2-0.4-2.7,0.2-3.7-1c13.3-0.4,26.6-0.8,39.8-1.2c11.7,0,23.5,0,35.2,0.1
+ c2.2,0,5.3-1.2,6.2,1.5c0.9,3-3,2.8-4.4,4.3c-1.3,0.1-2.6,0.2-4,0.2c-0.3,0-0.7,0-1,0c-5.3,0.7-10.6,0.9-15.8,0.6
+ C365.7,183.9,348.2,182.6,330.9,180.2z"/>
+<path style="fill:#E794A1;" d="M195.7,122.8c-3.7,9.5,0.3,17.7,4.7,25.8c0.8,1.4,2.3,2.5,1.7,4.4c-6.1-4.2-7.8-10.7-9.2-18.1
+ c-4.7,4.9-5.9,10.3-5.3,16.1c0.4,3.7,1.6,7.4,2.5,11.1c-2.2-0.9-3.1-2.7-3.1-5c-2.7-7.8-1.1-16.1-2.8-24c-0.5-2.5,0.5-4.9,1.8-7.1
+ c3.7-6.2,7.4-12.5,12.4-17.9c1.2,5.1,1.2,6-1.2,10.1C196.3,119.6,196,121.2,195.7,122.8z"/>
+<path style="fill:#E794A1;" d="M352.9,168.1c6.4,1.6,12.8,3.2,19.3,4.8c0,0.2-0.1,0.4-0.1,0.7c-5-0.5-10.1-0.9-15.1-1.4
+ c-16.3-2.7-32.8-4.1-48.9-8.3c-2.8-0.7-7.9-1.4-7.3-3.6c0.8-2.8,5.3-0.9,8.3-0.4c13.1,2,26,5,38.9,7.7
+ C349.6,167.9,351.3,167.9,352.9,168.1z"/>
+<path style="fill:#FFFFFF;" d="M186.9,157.1c0,2.3,0.9,4,3.1,5l-0.1-0.1c-0.2,1.4,0.4,2.6,1.2,3.7c0.6,2.1,1.3,4.2,4.1,4.3
+ c1.9,0.1,2.9,1.6,4.1,2.6c7.2,6.2,14.8,11.8,23.5,15.8c0.7,0.3,1.6,0.5,1.8,1.9c-2,0.4-3.8-0.4-5.3-1.3c-11.6-6.9-24-12.7-33.7-22.4
+ c-1-1-1.8-2.3-2.8-3.5c-2-2.3-2.8-4.1,1.5-4.2C185.5,159,185.9,157.6,186.9,157.1z"/>
+<path style="fill:#FFFFFF;" d="M111.9,299.9c10.4,4.7,20.7,9.4,31.1,14.1c0.3,0.3,0.7,0.6,1,0.9c0.3,0,0.6,0,1,0.1
+ c2.7,3.4,5.1,7.2,10.5,5.4c0.8-0.3,1.2,0.2,1.4,1c0.3,1.7-0.3,2.9-1.9,3.6c-8.1-3.8-16.2-7.4-23.6-12.8c-4.2-3.1-8.9-6-14.7-6.3
+ c-2.9-0.2-5.5-1.9-7.8-3.9C110.5,302.3,111,300.8,111.9,299.9z"/>
+<path style="fill:#E794A1;" d="M227.3,110c0,2,0,4,0,6c-0.1,3.2-0.9,4.8-4.8,3.5c-4.5-1.5-6.6,0.7-5.2,5.4c0.9,3.1,2.4,6.1,3.6,9.1
+ c-4.9-3.2-5.5-8.3-6-13.4c-0.2-1.8,0-3.6,0.1-5.5c1.9-3.4,5.6-4.9,8-7.8c1.3-0.2,2.5-1.7,4-0.4C227.1,108,227.2,109,227.3,110z"/>
+<path style="fill:#E794A1;" d="M115.9,209.5c-1.9,2.2-3.5,1.7-5-0.5c-0.4-5.1,2.9-8.2,6.1-11.4c1.4-1.4,2.7-2.8,4.1-4.3
+ c1.9-0.8,2.5-3.5,5-3.4c0.3,1.7-0.8,2.8-1.4,4.1C121.9,199.4,117.9,203.9,115.9,209.5z"/>
+<path style="fill:#E794A1;" d="M295,110c5.3,1,10.5,1.9,15.8,2.9c-1.4,1.3-3.5,1.8-3.9,4c-1.3,0.7-0.5,2.7-1.9,3.3
+ c-2.3-1.6-4.6-3.2-6.8-4.9C296.5,114,294.5,112.7,295,110z"/>
+<path style="fill:#E794A1;" d="M128.1,288.3c4.1,1.5,7.4,4.1,9.9,7.6C134,294.3,130,292.7,128.1,288.3z"/>
+<path style="fill:#E794A1;" d="M202,153c2.1-0.1,2.9,1.2,3,3C203.3,155.7,202.2,154.8,202,153z"/>
+<path style="fill:#E794A1;" d="M277.6,221.4c-1.4-0.4-2.8-0.7-4.2-2c2-0.6,3.4,0.5,4.7,1.4C278,221,277.8,221.2,277.6,221.4z"/>
+<path style="fill:#E794A1;" d="M191.1,165.7c-0.8-1.1-1.4-2.3-1.2-3.7C190.8,163.1,192.3,164,191.1,165.7z"/>
+<path style="fill:#F5CFD6;" d="M149,315c0.5,0.2,1.1,0.5,1.6,0.7c-0.1,0.2-0.3,0.4-0.4,0.6C149.8,315.9,149.4,315.4,149,315L149,315
+ z"/>
+<path style="fill:#F5CFD6;" d="M405,262.8c-1.1,0.2-2.2,0.2-3.1-0.7C403,262,404.1,261.8,405,262.8z"/>
+<path style="fill:#F5CFD6;" d="M367,162.1c0.8-0.1,1.6,0,2.1,0.8C368.2,163,367.5,162.9,367,162.1z"/>
+<path style="fill:#FFFFFF;" d="M331.9,150.8c-0.3,0.1-0.6,0.2-0.9,0.2C331.2,150.5,331.5,150.3,331.9,150.8z"/>
+<path style="fill:#F5CFD6;" d="M308,265.2c0.4,0.3,0.7,0.5,1.1,0.8C308.7,265.7,308.4,265.4,308,265.2z"/>
+<path style="fill:#E794A1;" d="M205,156c0.3,0.3,0.7,0.7,1,1C205.7,156.7,205.3,156.3,205,156z"/>
+<path style="fill:#FFFFFF;" d="M302.9,357c3-6.9,6.7-8,12.6-3.8c2.4,1.7,4.7,3.7,6.7,5.9c4.8,5.4,11.2,7.4,17.9,8.5
+ c5.4,0.9,8-1.5,8-7c0-3.9,1-7.4,3.1-10.7c3.2-2.5,5.9-6.9,10.5-2.1c3.4,4.3,6.6,8.7,10.1,12.9c1.5,1.8,3,4.2,6.2,3.1
+ c0.3,2.9-1.4,5.6-0.7,8.5c-0.4,1.1-0.9,2.2-1.2,3.3c-6.7,25.6-13.6,51.1-21.2,76.4c-0.6,0.3-0.8,0.7-0.6,1.3
+ c-0.1,0.2-0.3,0.5-0.3,0.8c-4.8,4.2-5.7,4-8-2.3c-0.3-0.9-0.8-1.8-1.2-2.7c-0.6-3.9-2.1-7.8-1.6-11.8c1.7-14,3.4-27.9,5.3-41.9
+ c0.5-3.7,0-6.9-1.5-10.1c-0.5-1.1-0.3-3-2.1-3c-2.1,0.1-1.8,2-2.1,3.4c-3,11.9-4.5,24.1-6.7,36.2c-4,22-5.8,44.4-11.1,66.3
+ c-4.5,6.5-5.3,6.6-12.9,1c-0.7-2.4-5.2-0.7-4.4-5.9c2.9-17.4,4.6-34.9,6.6-52.4c1.6-13.9,1.6-27.9,2.2-41.9c0.2-4.9,0.3-9.6-2.3-14
+ c-1.4-2.3-1.9-6.3-5.2-5.8c-2.8,0.5-2.4,4.2-3.1,6.7c-1.5-1.9-0.7-4.1-0.8-6.1C304.7,365.4,304.6,361.1,302.9,357z"/>
+<path style="fill:#110D0E;" d="M457.9,309.9c0.9,0.7,1.7,1.7,2.7,2c6,1.8,7.2,6.1,6.3,11.6c-0.7,4.8-2.9,6.3-7.9,5.8
+ c-7.6-0.7-15-2.7-22.5-4.1c0.1,0.4,0.1,0.8,0.2,1c1.7,2.6,4.9,5.3,2.6,8.3c-2.4,3.3-6.2,1-9.1-0.2c-5.2-2.1-9.8-5.5-15.8-5.4
+ c3.1-2,6.4,0.3,9.7-0.6c4.1-1.2,7.9-2.4,11.4-5.2c6.2-4.7,13-8.7,19.5-13C456.1,310.1,457,310,457.9,309.9z"/>
+<path style="fill:#110D0E;" d="M414.1,328.9c-5.9,3.1-5.3,9-6.1,14.2c-0.5,3.3,0.6,8.1-2.6,9.5c-3.2,1.4-5.2-3.3-7.5-5.4
+ c-3.8-3.6-8.8-4.9-12.9-7.9c2.7-0.4,5.5-1.2,8.2-1.2c5.4,0,10.6,0.7,13.8-5.5C408.2,330.2,411.1,328.8,414.1,328.9z"/>
+<path style="fill:#110D0E;" d="M378,363.7c-3.1,1.2-4.7-1.3-6.2-3.1c-3.5-4.2-6.8-8.6-10.1-12.9c1.1-0.6,2.4-1.8,3.4-1.6
+ c5.3,0.7,9.7-0.9,13.7-4.2c-1.4,2.6-1.1,5.3-0.8,8.1c0,2.3,0.1,4.6,0.1,6.9C378.2,359.2,379,361.5,378,363.7z"/>
+<path style="fill:#110D0E;" d="M477,295.8c2.4,3.6,6.7,6.3,5.4,11.6c-7.6,2.5-15.2,0.3-22.9,0.6c0.1-0.3,0.3-0.6,0.4-0.9
+ c5.6-3.3,11.1-6.8,16.1-11C476.4,296,476.7,295.9,477,295.8z"/>
+<path style="fill:#110D0E;" d="M482.9,280c3,2.1,5.1,5.1,7.1,8.1c2.5,3.9,0.9,6.5-3.6,6.3c-2.8-0.1-5.5-1-8.2-1.5
+ c-0.1-0.3-0.2-0.5-0.3-0.8C478,287.4,479,283.1,482.9,280z"/>
+<path style="fill:#F5CFD6;" d="M477.8,292.1c0.1,0.3,0.2,0.5,0.3,0.8c-0.4,1-0.8,1.9-1.2,2.9c-0.3,0.1-0.6,0.2-0.9,0.3
+ c-1-2.4-3.1-4-4.4-6.4C473.7,290.5,475.8,291.3,477.8,292.1z"/>
+<path style="fill:#F5CFD6;" d="M347.6,350.1c-4.1,1.9-8.3,1.1-12.5,0.6C339.2,349.4,343.5,350.5,347.6,350.1z"/>
+<path style="fill:#F5CFD6;" d="M457.9,309.9c-0.9,0.1-1.9,0.2-2.8,0.3c-0.3-1.3-2.4-1.1-2.3-3c1.8,0.3,3.5,0.6,5.2,0.9
+ C458,308.7,458,309.3,457.9,309.9z"/>
+<path style="fill:#F7E3E5;" d="M456,199.2c0-1.4,0.6-2.2,2-2.2C458,198.4,457.3,199,456,199.2z"/>
+<path style="fill:#F5CFD6;" d="M460,307c-0.1,0.3-0.3,0.6-0.4,0.9c-0.5,0-1,0.1-1.6,0.1C458.6,307.7,459.3,307.4,460,307z"/>
+<path style="fill:#F7E3E5;" d="M289,305.2c-0.3,0.4-0.7,0.5-1,0C288.3,305.2,288.7,305.2,289,305.2z"/>
+<path style="fill:#FFFFFF;" d="M243,387c0-7,0-14.1,0-21.1c0-1.6,0-3.3-1-4.7c2.9-2.3,4.3-0.9,6,1.8c3.8,6.3,8.2,12.1,16.7,11.8
+ c1.9,1.8,1.1,4.1,1.1,6.1c0.1,4.5-0.9,9.5,7.2,9.5c-6.7-0.7-6.4,2.3-5.5,6.4c0.9,4,0.7,8.2,1.2,12.3c2.5,20.8,5,41.7,8.2,62.4
+ c0.2,1.3,0.5,2.3,2,2.4c1.3,6,0.8,12.4,4.2,18c-3.8,6.2-6.7,6.4-12,1.4c-2.3-2.2-3.3-5.9-7-6.6c-2.2-20.1-4.8-40.2-6.6-60.3
+ c-1.1-11.9-4.5-23-9.7-33.6C246.8,390.6,245.9,387.8,243,387z"/>
+<path style="fill:#FFFFFF;" d="M280,363c0.3-1.1,0.6-2.2,1.6-2.8c4.3,8.1,6.9,9.6,13.3,7.9c0.2,26.1,1.2,52.1,4,79
+ c2.9-3.7,1.6-7,2.2-10.1c0.3,0,0.5,0,0.8,0c0.1,13.9-1.8,27.8-1.1,41.7c0.1,2.5-0.3,4.9-1,7.3c-3.5,5.1-8.8,6-14.4,6
+ c0.6-31.6,2-63.2,1.5-94.8c-0.1-6.1-1.9-12.1-6-17C280.4,374.4,282.2,368.6,280,363z"/>
+<path style="fill:#5F5F5F;" d="M299.9,486c0.8-2.4,1.2-4.8,1-7.3c-0.8-13.9,1.1-27.8,1.1-41.7c1.8-13.9,1.8-27.9,2.6-41.8
+ c0.4-6.4,0.9-12.9,1.3-19.3c0.6-2.5,0.3-6.3,3.1-6.7c3.3-0.5,3.8,3.4,5.2,5.8c2.6,4.3,2.5,9,2.3,14c-0.7,14-0.6,28-2.2,41.9
+ c-2,17.5-3.7,35-6.6,52.4c-0.9,5.1,3.7,3.5,4.4,5.9c-3.4-0.6-6.2-0.2-6.6,4.1c-0.1,0.6-0.5,1.3-1.3,1.1c-1-0.2-1.8-0.8-1.9-2
+ C301.9,490,301.7,487.7,299.9,486z"/>
+<path style="fill:#5F5F5F;" d="M324.9,488.1c5.2-21.8,7-44.2,11.1-66.3c2.2-12.1,3.7-24.3,6.7-36.2c0.3-1.4,0-3.3,2.1-3.4
+ c1.8,0,1.6,1.9,2.1,3c1.5,3.2,2.1,6.4,1.5,10.1c-2,13.9-3.6,27.9-5.3,41.9c-0.5,4,1,7.9,1.6,11.8c-2.1,0.1-1.1-2.5-2.3-2.9
+ c-2.9,11.6-3.1,23.6-6.5,35c-1.4,4.5-3.4,8-9.2,6.3C326.2,487.2,325.5,487.8,324.9,488.1z"/>
+<path style="fill:#5F5F5F;" d="M280.9,380.1c4.1,4.9,5.9,10.9,6,17c0.5,31.6-0.8,63.2-1.5,94.8c-0.8,0-1.5,0-2.3,0.1
+ c-3.4-5.5-2.9-11.9-4.2-18c0-17.9,1.2-35.9-0.8-53.7c-1-8.9-1.8-17.9-1-26.8C277.6,388.9,276.6,383.7,280.9,380.1z"/>
+<path style="fill:#110D0E;" d="M392.6,450.6c7.1-28.3,19.5-54.7,30.9-81.3c3.9-9,7.8-18,12.7-26.5c0.7-1.2,1.3-3.3,3.2-2.3
+ c1.5,0.8,0.2,2.5-0.3,3.6c-15.2,35.7-30.5,71.4-45.7,107.1C393,451,392.8,450.8,392.6,450.6z"/>
+<path style="fill:#110D0E;" d="M403.3,362.9c-6.5,32.8-17.8,64.3-27.7,96.7C372.7,444.3,395.1,376.5,403.3,362.9z"/>
+<path style="fill:#110D0E;" d="M354.8,451.9c7.6-25.3,14.6-50.8,21.2-76.4c0.3-1.1,0.8-2.2,1.2-3.3c1.5,10.2,0,20-3.1,29.8
+ c-7.2,23.1-14.5,46-22.7,68.8c-0.2,0.5-0.5,0.9-1.4,2.4c0.8-7.4,4.3-13,3.9-19.3c0.1-0.3,0.2-0.5,0.3-0.8
+ C354.9,452.9,355,452.5,354.8,451.9z"/>
+<path style="fill:#110D0E;" d="M448.3,352.3c-5.5,13.7-11,27.4-16.7,41c-6.1,14.7-12.4,29.3-18.7,44c-0.6,1.5-1.4,3-2.5,5.3
+ c-0.7-3.4,1-5.2,1.7-7.1c10.5-26.9,19.7-54.3,33.5-79.8c0.7-1.3,1.5-2.6,2.2-3.9C448,352,448.1,352.1,448.3,352.3z"/>
+<path style="fill:#BDBCBC;" d="M378.1,356.9c0-2.3-0.1-4.6-0.1-6.9C379.4,352.3,379.6,354.6,378.1,356.9z"/>
+<path style="fill:#110D0E;" d="M450,348c0.1,1.7-0.7,3-1.7,4.3c-0.1-0.1-0.3-0.3-0.4-0.4C448.2,350.4,448.5,348.8,450,348L450,348z"
+ />
+<path style="fill:#110D0E;" d="M393.3,451.2c-0.4,0.9-0.8,1.8-1.3,2.7v0.1c0.2-1.1,0.3-2.3,0.5-3.4C392.8,450.8,393,451,393.3,451.2
+ z"/>
+<path style="fill:#5F5F5F;" d="M406.6,450.6c0,0.1,0.1,0.3,0.1,0.4c-0.2,0-0.4,0.1-0.6,0.1c0-0.1,0-0.3,0.1-0.4
+ C406.3,450.6,406.5,450.6,406.6,450.6z"/>
+<path style="fill:#110D0E;" d="M451.6,343.8c0,0.1,0.1,0.3,0.1,0.4c-0.1,0.5-0.2,0.5-0.4,0.1c0-0.1,0-0.3,0.1-0.4
+ C451.4,343.9,451.5,343.9,451.6,343.8z"/>
+<path style="fill:#110D0E;" d="M450,348c0.1-0.5,0.2-0.9,0.2-1.4c0.2,0.1,0.4,0.2,0.5,0.4C450.5,347.3,450.3,347.7,450,348L450,348z
+ "/>
+<path style="fill:#110D0E;" d="M409.8,444c-0.2,0.2-0.4,0.3-0.6,0.4c0-0.3-0.1-0.6-0.1-0.9c0-0.1,0.2-0.3,0.3-0.4
+ C409.6,443.5,409.7,443.8,409.8,444z"/>
+<path style="fill:#5F5F5F;" d="M407.9,448.3c-0.1,0.1-0.3,0.2-0.4,0.3c-0.1-0.3-0.2-0.5-0.3-0.8c0-0.1,0.2-0.2,0.4-0.3
+ C407.7,447.7,407.8,448,407.9,448.3z"/>
+<path style="fill:#110D0E;" d="M392,454c-0.1,0.1-0.1,0.2-0.2,0.3C391.9,454.2,392,454.1,392,454C392.1,454,392,454,392,454z"/>
+<path style="fill:#F7E3E5;" d="M306.9,116.8c0.5-2.2,2.6-2.7,3.9-4c6.5,0.6,12.5,2.7,18,6.2c1.1,0.6,2.2,1.2,1.9,2.5
+ c-0.4,1.9-1.8,0.6-2.8,0.6c-4.2,0.1-8.4-1-12.6,1.3C311.6,125.6,307.9,122.4,306.9,116.8z"/>
+<path style="fill:#F7E3E5;" d="M343.1,91.8c-0.1,0-0.2-0.1-0.3-0.1c0.1-0.1,0.3-0.3,0.4-0.4c0.1,0.1,0.2,0.2,0.3,0.3
+ C343.3,91.6,343.2,91.7,343.1,91.8z"/>
+<path style="fill:#E794A1;" d="M111.9,299.9c-0.9,0.8-1.4,2.3-3.1,2c-3.5-0.7-6.7-2.1-9.7-4c-0.7-0.6-1.3-1.2-2-1.8
+ c-0.1-0.2-0.2-0.3-0.3-0.4c0.5-2.7,2.6-1.4,4.1-1.7C104.5,296.1,108.8,297,111.9,299.9z"/>
+<path style="fill:#F7E3E5;" d="M101,294c-1.5,0.4-3.6-1-4.1,1.7c-2.9,0-4-1.9-4.7-4.4c0.4,0.2,0.8,0.5,1.2,0.7
+ c0.1-0.4,0.5-0.9,0.3-1.2c-0.5-1-1.2-1.9-1.9-2.8c1.3-1.9,3.3-0.7,5.4-1.2c-3.4,2.3-1.6,3.3,0.4,3.8C99.7,291.2,101.3,291.5,101,294
+ z"/>
+<path style="fill:#E794A1;" d="M80.1,275.2c0-0.7-0.1-1.5-0.1-2.2c2.1,1.6,4.9,2.7,4,6.2c-1.1,0-0.7,0.9-0.8,1.5
+ C82.1,278.8,81.1,277,80.1,275.2z"/>
+<path style="fill:#110D0E;" d="M90,286c0,0.7,1.1,1.3,0.1,2c-2.3-1.2-3.3-3.5-4.8-5.4c0.3-0.2,0.5-0.5,0.8-0.7
+ c0.9,0.8,1.8,1.5,2.7,2.3l-0.1-0.1C89.2,284.8,89.6,285.4,90,286z"/>
+<path style="fill:#110D0E;" d="M91.9,288.1c0.6,0.9,1.3,1.8,1.9,2.8c0.1,0.2-0.2,0.8-0.3,1.2c-0.4-0.2-0.8-0.5-1.2-0.7
+ c-0.7-1.1-1.5-2.3-2.2-3.4C90.8,287,91.3,288.1,91.9,288.1z"/>
+<path style="fill:#F7E3E5;" d="M176,333c2.3-0.6,4.1,0.1,5.5,2C179.4,335.2,177.4,334.9,176,333z"/>
+<path style="fill:#FFFFFF;" d="M187.5,338c2.2-0.3,4.1,0.3,5.5,2C190.8,340.2,189.1,339.3,187.5,338z"/>
+<path style="fill:#F7E3E5;" d="M97.2,296.1c0.7,0.6,1.3,1.2,2,1.8C98,298,97,297.7,97.2,296.1z"/>
+<path style="fill:#FFFFFF;" d="M193,340c0.9,0,1.7,0,2,1C194.1,341.1,193.3,341,193,340z"/>
+<path style="fill:#F7E3E5;" d="M181.5,335c1.2-0.2,2.2,0.1,3,1C183.4,336.1,182.3,335.9,181.5,335z"/>
+<path style="fill:#110D0E;" d="M83.9,280.9c0.8-1,1.3,0.2,2,0.2c0,0.3,0.1,0.5,0.1,0.8c-0.3,0.2-0.5,0.5-0.8,0.7
+ C84.8,282.1,84.4,281.5,83.9,280.9z"/>
+<path style="fill:#110D0E;" d="M83.2,280.7c0.1-0.6-0.4-1.4,0.8-1.5c0,0.6,1.1,1.2,0.1,1.9C83.8,280.9,83.5,280.8,83.2,280.7z"/>
+<path style="fill:#BDBCBC;" d="M280.9,380.1c-4.3,3.6-3.4,8.8-3.8,13.4c-0.8,8.9,0,17.9,1,26.8c2.1,17.9,0.9,35.8,0.8,53.7
+ c-1.5-0.1-1.8-1.1-2-2.4c-3.2-20.8-5.7-41.6-8.2-62.4c-0.5-4.1-0.3-8.3-1.2-12.3c-0.9-4.1-1.3-7.1,5.5-6.4c-8.1,0-7.1-5-7.2-9.5
+ c0-2.1,0.7-4.4-1.1-6.1c6.4-2.2,11-6.8,15.2-11.8C282.2,368.6,280.4,374.4,280.9,380.1z"/>
+<path style="fill:#BDBCBC;" d="M305.8,375.9c-0.4,6.4-0.9,12.9-1.3,19.3c-0.9,0.3-1.4,0.8-1.4,1.7c-0.6,13.4-1.3,26.7-1.9,40.1
+ c-0.6,3,0.7,6.3-2.2,10.1c-2.8-26.9-3.7-52.9-4-79c3.9-2.8,5.6-7.1,7.9-11c1.7,4.1,1.7,8.5,2.1,12.8
+ C305.2,371.8,304.3,374,305.8,375.9z"/>
+<path style="fill:#F7E3E5;" d="M86.1,281.9c0-0.3-0.1-0.5-0.1-0.8c0.2-0.7,0.1-1.8,1.3-1.3c2.2,0.8,1.7,2.7,1.5,4.4
+ C87.9,283.5,87,282.7,86.1,281.9z"/>
+<path style="fill:#F7E3E5;" d="M90,286c-0.4-0.6-0.8-1.2-1.2-1.8C90,284.2,91.6,284.1,90,286z"/>
+<path style="fill:#E794A1;" d="M80,273c-1.5-0.3-1.6-1.6-1.8-2.8C79.4,270.7,80,271.7,80,273z"/>
+<path style="fill:#F7E3E5;" d="M144,315c-0.3-0.3-0.7-0.6-1-0.9C143.3,314.3,143.7,314.7,144,315z"/>
+<path style="fill:#110D0E;" d="M484.4,307.4c0.6-1.4,1.1-2.8,2.5-3.5c0.1,0.1,0.3,0.3,0.4,0.4c-0.7,1.3-1.4,2.6-2.1,3.9
+ C485,307.9,484.7,307.7,484.4,307.4z"/>
+<path style="fill:#110D0E;" d="M487.9,302.9c-0.2-1,0.1-1.6,1-2l0.2,0.2C489.1,302,488.9,302.7,487.9,302.9L487.9,302.9z"/>
+<path style="fill:#110D0E;" d="M488.9,300.9c0.2-0.2,0.3-0.5,0.5-0.7c0.5,0.5,0.3,0.8-0.3,0.9C489.2,301.2,488.9,300.9,488.9,300.9z
+ "/>
+<path style="fill:#110D0E;" d="M486.9,303.9c0.2-0.5,0.3-1.1,1.1-0.9l0.1,0.1c-0.2,0.4-0.5,0.9-0.7,1.3
+ C487.2,304.2,487,304,486.9,303.9z"/>
+<path style="fill:#F7E3E5;" d="M115.9,209.5c2-5.7,6.1-10.2,8.7-15.5c0.7-1.3,1.8-2.4,1.4-4.1l0,0c0.3-0.3,0.6-0.5,0.9-0.8
+ c2.4-2.6,4.2-0.8,5.7,1c2.9,3.2,5.6,6.5,8.3,9.8l0,0c-1.6-0.4-3.2-0.7-4.8-1.1c-0.9-0.2-2.6-0.7-2.7-0.5c-2,3.8-7.1,3.8-8.8,7.1
+ c-1.6,3.1-1.1,7.1,0.3,10.6c1.3,3.4,2.8,6.7,3.7,10.3C121.4,222.9,115.8,218.3,115.9,209.5z"/>
+<path style="fill:#F5CFD6;" d="M100.9,230.8c0.1-0.6,0.2-1.3,0.3-1.9c1.3,1.4,2,3.1,1.9,5L103,234
+ C100.9,233.9,101,232.3,100.9,230.8z"/>
+<path style="fill:#F5CFD6;" d="M103,234c1,0.2,1.9,0.5,1.7,1.8c-0.1,0.3-0.7,0.8-0.9,0.7C102.6,236.1,103,235,103,234L103,234z"/>
+<path style="fill:#F7E3E5;" d="M141,200c0.7,0.1,1.6,0.1,1.5,1.2c0,0.1-0.2,0.3-0.3,0.3C141.1,201.6,141.1,200.7,141,200L141,200z"
+ />
+<path style="fill:#E794A1;" d="M126.9,189.2c-0.3,0.3-0.6,0.5-0.9,0.8C126.3,189.8,126.6,189.5,126.9,189.2z"/>
+<path style="fill:#F7E3E5;" d="M400,184.2c1.3-0.1,2.6-0.2,4-0.2c2.5,3.2,6.5,2.7,9.8,3.8c2.5,0.8,4.7,2.3,7.3,2.9
+ c0.9,0.2,1.9,0.9,1.6,2.1s-1.4,1.2-2.4,1.2c-7.1,0.4-14.2-0.5-21.3-1.4c-7.7-1-15.4-2.3-23.1-2.4c-6.4,0-12.4-1.8-18.7-2.4
+ c-9.1-0.9-18.1-3.4-27.2-5.3c-1.1-0.2-2.1-0.9-3.1-1.4c1.3-0.2,2.8,0.4,3.9-0.9c17.3,2.5,34.8,3.8,52.3,4.7
+ c5.3,0.3,10.6,0.1,15.8-0.6C399.3,184.7,399.7,184.6,400,184.2z"/>
+<path style="fill:#F7E3E5;" d="M185,317c-2-0.7-4-1.5-6-2.2C181.6,314,184,313.8,185,317L185,317z"/>
+<path style="fill:#F7E3E5;" d="M227.3,110c-0.1-1-0.2-2-0.3-3C228.3,107.9,228.2,108.9,227.3,110z"/>
+<path style="fill:#F5CFD6;" d="M400,184.2c-0.3,0.4-0.7,0.5-1,0C399.3,184.2,399.7,184.2,400,184.2z"/>
+<path style="fill:#7A7A7A;" d="M354.8,451.9c0.2,0.6,0,1.1-0.6,1.3C354,452.5,354.3,452.1,354.8,451.9z"/>
+<path style="fill:#F7E3E5;" d="M301.2,437c0.6-13.4,1.3-26.7,1.9-40.1c0-1,0.6-1.5,1.4-1.7c-0.8,13.9-0.8,27.9-2.6,41.8
+ C301.7,437,301.4,437,301.2,437z"/>
+</svg>
diff --git a/login/app/sprinkles/core/assets/userfrosting/js/AdminLTE-custom.js b/login/app/sprinkles/core/assets/userfrosting/js/AdminLTE-custom.js new file mode 100755 index 0000000..177c845 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/js/AdminLTE-custom.js @@ -0,0 +1,117 @@ +/*! AdminLTE userfrosting.js + * ================ + * Userfrosting JS file for AdminLTE v2. This file + * should be included in all pages. It controls some layout + * options and implements functions related to Userfrosting. + * + * @Author Louis Charette + * @Support <https://github.com/userfrosting> + * @license MIT <http://opensource.org/licenses/MIT> + */ + +//Make sure jQuery has been loaded before app.js +if (typeof jQuery === "undefined") { + throw new Error("AdminLTE requires jQuery"); +} + +/* Tree_UF() +* ====== +* Overwrite the default behavior for UF menu compatibility +* +* @type Function +* @Usage: $.AdminLTE.tree_UF('.sidebar') +*/ +$.AdminLTE.tree_UF = function (menu) { + var _this = this; + var animationSpeed = $.AdminLTE.options.animationSpeed; + $(document).off('click', menu + ' li a').on('click', menu + ' li a', function (e) { + + //Get the clicked link and the next element + var $this = $(this); + var checkElement = $this.next(); + + //Check if the next element is a menu and is visible + if ((checkElement.is('.treeview-menu')) && (checkElement.is(':visible')) && (!$('body').hasClass('sidebar-collapse'))) { + //Close the menu + checkElement.slideUp(animationSpeed, function () { + checkElement.removeClass('menu-open'); + //Fix the layout in case the sidebar stretches over the height of the window + //_this.layout.fix(); + }); + checkElement.parent("li").removeClass("active"); + } + //If the menu is not visible + else if ((checkElement.is('.treeview-menu')) && (!checkElement.is(':visible'))) { + //Get the parent menu + var parent = $this.parents('ul').first(); + //Close all open menus within the parent + var ul = parent.find('ul:visible').slideUp(animationSpeed); + //Remove the menu-open class from the parent + ul.removeClass('menu-open'); + //Get the parent li + var parent_li = $this.parent("li"); + + //Open the target menu and add the menu-open class + checkElement.slideDown(animationSpeed, function () { + + //Add the class active to the parent li + checkElement.addClass('menu-open'); + parent.find('li.treeview.active').removeClass('active'); //<<-- Overwrite here. Otherwise, the menu href based active will be removed + parent_li.addClass('active'); + + //Fix the layout in case the sidebar stretches over the height of the window + _this.layout.fix(); + }); + } + //if this isn't a link, prevent the page from being redirected + if (checkElement.is('.treeview-menu')) { + e.preventDefault(); + } + }); +}; + +/* initMenu() +* ====== +* Activate the menu based on the url and href attr. +* +* @type Function +* @Usage: $.AdminLTE.initMenu('.sidebar') +*/ +$.AdminLTE.initMenu = function (searchElement) { + var _this = this; + var element = $(searchElement).filter(function() { + // Strip out everything after the hash, if present + var url_head = window.location.href.split('#', 1)[0]; + return this.href == url_head; // || url.href.indexOf(this.href) == 0 // Include this if you want to color all parent URIs as well + }).parent(); + $(element).addClass('active'); + $(element).parents('.treeview').addClass('active'); + $(element).parents('.treeview-menu').addClass('menu-open'); +}; + +$(function() { + //Init menu and trees + $.AdminLTE.initMenu('ul.sidebar-menu a'); + $.AdminLTE.tree_UF('.sidebar'); + + // Apply select2 to all js-select2 elements + $('.js-select2').select2({ minimumResultsForSearch: Infinity }); + + // Apply iCheck to all js-icheck elements + $('.js-icheck').iCheck({ + checkboxClass: 'icheckbox_square-blue', + radioClass: 'iradio_square-blue', + increaseArea: '20%' // optional + }); + + // Remember the sidebar collapse state + // See: https://github.com/almasaeed2010/AdminLTE/issues/896#issuecomment-264723101 + $('.sidebar-toggle').click(function(event) { + event.preventDefault(); + if (Boolean(sessionStorage.getItem('sidebar-toggle-collapsed'))) { + sessionStorage.setItem('sidebar-toggle-collapsed', ''); + } else { + sessionStorage.setItem('sidebar-toggle-collapsed', '1'); + } + }); +});
\ No newline at end of file diff --git a/login/app/sprinkles/core/assets/userfrosting/js/AdminLTE.js b/login/app/sprinkles/core/assets/userfrosting/js/AdminLTE.js new file mode 100755 index 0000000..54b6055 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/js/AdminLTE.js @@ -0,0 +1,763 @@ +/*! AdminLTE app.js + * ================ + * Main JS application file for AdminLTE v2. This file + * should be included in all pages. It controls some layout + * options and implements exclusive AdminLTE plugins. + * + * @Author Almsaeed Studio + * @Support <http://www.almsaeedstudio.com> + * @Email <abdullah@almsaeedstudio.com> + * @version 2.3.6 + * @license MIT <http://opensource.org/licenses/MIT> + */ + +//Make sure jQuery has been loaded before app.js +if (typeof jQuery === "undefined") { + throw new Error("AdminLTE requires jQuery"); +} + +/* AdminLTE + * + * @type Object + * @description $.AdminLTE is the main object for the template's app. + * It's used for implementing functions and options related + * to the template. Keeping everything wrapped in an object + * prevents conflict with other plugins and is a better + * way to organize our code. + */ +$.AdminLTE = {}; + +/* -------------------- + * - AdminLTE Options - + * -------------------- + * Modify these options to suit your implementation + */ +$.AdminLTE.options = { + //Add slimscroll to navbar menus + //This requires you to load the slimscroll plugin + //in every page before app.js + navbarMenuSlimscroll: true, + navbarMenuSlimscrollWidth: "3px", //The width of the scroll bar + navbarMenuHeight: "200px", //The height of the inner menu + //General animation speed for JS animated elements such as box collapse/expand and + //sidebar treeview slide up/down. This options accepts an integer as milliseconds, + //'fast', 'normal', or 'slow' + animationSpeed: 500, + //Sidebar push menu toggle button selector + sidebarToggleSelector: "[data-toggle='offcanvas']", + //Activate sidebar push menu + sidebarPushMenu: true, + //Activate sidebar slimscroll if the fixed layout is set (requires SlimScroll Plugin) + sidebarSlimScroll: true, + //Enable sidebar expand on hover effect for sidebar mini + //This option is forced to true if both the fixed layout and sidebar mini + //are used together + sidebarExpandOnHover: false, + //BoxRefresh Plugin + enableBoxRefresh: true, + //Bootstrap.js tooltip + enableBSToppltip: true, + BSTooltipSelector: "[data-toggle='tooltip']", + //Enable Fast Click. Fastclick.js creates a more + //native touch experience with touch devices. If you + //choose to enable the plugin, make sure you load the script + //before AdminLTE's app.js + enableFastclick: false, + //Control Sidebar Options + enableControlSidebar: true, + controlSidebarOptions: { + //Which button should trigger the open/close event + toggleBtnSelector: "[data-toggle='control-sidebar']", + //The sidebar selector + selector: ".control-sidebar", + //Enable slide over content + slide: true + }, + //Box Widget Plugin. Enable this plugin + //to allow boxes to be collapsed and/or removed + enableBoxWidget: true, + //Box Widget plugin options + boxWidgetOptions: { + boxWidgetIcons: { + //Collapse icon + collapse: 'fa-minus', + //Open icon + open: 'fa-plus', + //Remove icon + remove: 'fa-times' + }, + boxWidgetSelectors: { + //Remove button selector + remove: '[data-widget="remove"]', + //Collapse button selector + collapse: '[data-widget="collapse"]' + } + }, + //Direct Chat plugin options + directChat: { + //Enable direct chat by default + enable: true, + //The button to open and close the chat contacts pane + contactToggleSelector: '[data-widget="chat-pane-toggle"]' + }, + //Define the set of colors to use globally around the website + colors: { + lightBlue: "#3c8dbc", + red: "#f56954", + green: "#00a65a", + aqua: "#00c0ef", + yellow: "#f39c12", + blue: "#0073b7", + navy: "#001F3F", + teal: "#39CCCC", + olive: "#3D9970", + lime: "#01FF70", + orange: "#FF851B", + fuchsia: "#F012BE", + purple: "#8E24AA", + maroon: "#D81B60", + black: "#222222", + gray: "#d2d6de" + }, + //The standard screen sizes that bootstrap uses. + //If you change these in the variables.less file, change + //them here too. + screenSizes: { + xs: 480, + sm: 768, + md: 992, + lg: 1200 + } +}; + +/* ------------------ + * - Implementation - + * ------------------ + * The next block of code implements AdminLTE's + * functions and plugins as specified by the + * options above. + */ +$(function () { + "use strict"; + + //Fix for IE page transitions + $("body").removeClass("hold-transition"); + + //Extend options if external options exist + if (typeof AdminLTEOptions !== "undefined") { + $.extend(true, + $.AdminLTE.options, + AdminLTEOptions); + } + + //Easy access to options + var o = $.AdminLTE.options; + + //Set up the object + _init(); + + //Activate the layout maker + $.AdminLTE.layout.activate(); + + //Enable sidebar tree view controls + $.AdminLTE.tree('.sidebar'); + + //Enable control sidebar + if (o.enableControlSidebar) { + $.AdminLTE.controlSidebar.activate(); + } + + //Add slimscroll to navbar dropdown + if (o.navbarMenuSlimscroll && typeof $.fn.slimscroll != 'undefined') { + $(".navbar .menu").slimscroll({ + height: o.navbarMenuHeight, + alwaysVisible: false, + size: o.navbarMenuSlimscrollWidth + }).css("width", "100%"); + } + + //Activate sidebar push menu + if (o.sidebarPushMenu) { + $.AdminLTE.pushMenu.activate(o.sidebarToggleSelector); + } + + //Activate Bootstrap tooltip + if (o.enableBSToppltip) { + $('body').tooltip({ + selector: o.BSTooltipSelector + }); + } + + //Activate box widget + if (o.enableBoxWidget) { + $.AdminLTE.boxWidget.activate(); + } + + //Activate fast click + if (o.enableFastclick && typeof FastClick != 'undefined') { + FastClick.attach(document.body); + } + + //Activate direct chat widget + if (o.directChat.enable) { + $(document).on('click', o.directChat.contactToggleSelector, function () { + var box = $(this).parents('.direct-chat').first(); + box.toggleClass('direct-chat-contacts-open'); + }); + } + + /* + * INITIALIZE BUTTON TOGGLE + * ------------------------ + */ + $('.btn-group[data-toggle="btn-toggle"]').each(function () { + var group = $(this); + $(this).find(".btn").on('click', function (e) { + group.find(".btn.active").removeClass("active"); + $(this).addClass("active"); + e.preventDefault(); + }); + + }); +}); + +/* ---------------------------------- + * - Initialize the AdminLTE Object - + * ---------------------------------- + * All AdminLTE functions are implemented below. + */ +function _init() { + 'use strict'; + /* Layout + * ====== + * Fixes the layout height in case min-height fails. + * + * @type Object + * @usage $.AdminLTE.layout.activate() + * $.AdminLTE.layout.fix() + * $.AdminLTE.layout.fixSidebar() + */ + $.AdminLTE.layout = { + activate: function () { + var _this = this; + _this.fix(); + _this.fixSidebar(); + $(window, ".wrapper").resize(function () { + _this.fix(); + _this.fixSidebar(); + }); + }, + fix: function () { + //Get window height and the wrapper height + var neg = $('.main-header').outerHeight() + $('.main-footer').outerHeight(); + var window_height = $(window).height(); + var sidebar_height = $(".sidebar").height(); + //Set the min-height of the content and sidebar based on the + //the height of the document. + if ($("body").hasClass("fixed")) { + $(".content-wrapper, .right-side").css('min-height', window_height - $('.main-footer').outerHeight()); + } else { + var postSetWidth; + if (window_height >= sidebar_height) { + $(".content-wrapper, .right-side").css('min-height', window_height - neg); + postSetWidth = window_height - neg; + } else { + $(".content-wrapper, .right-side").css('min-height', sidebar_height); + postSetWidth = sidebar_height; + } + + //Fix for the control sidebar height + var controlSidebar = $($.AdminLTE.options.controlSidebarOptions.selector); + if (typeof controlSidebar !== "undefined") { + if (controlSidebar.height() > postSetWidth) + $(".content-wrapper, .right-side").css('min-height', controlSidebar.height()); + } + + } + }, + fixSidebar: function () { + //Make sure the body tag has the .fixed class + if (!$("body").hasClass("fixed")) { + if (typeof $.fn.slimScroll != 'undefined') { + $(".sidebar").slimScroll({destroy: true}).height("auto"); + } + return; + } else if (typeof $.fn.slimScroll == 'undefined' && window.console) { + window.console.error("Error: the fixed layout requires the slimscroll plugin!"); + } + //Enable slimscroll for fixed layout + if ($.AdminLTE.options.sidebarSlimScroll) { + if (typeof $.fn.slimScroll != 'undefined') { + //Destroy if it exists + $(".sidebar").slimScroll({destroy: true}).height("auto"); + //Add slimscroll + $(".sidebar").slimscroll({ + height: ($(window).height() - $(".main-header").height()) + "px", + color: "rgba(0,0,0,0.2)", + size: "3px" + }); + } + } + } + }; + + /* PushMenu() + * ========== + * Adds the push menu functionality to the sidebar. + * + * @type Function + * @usage: $.AdminLTE.pushMenu("[data-toggle='offcanvas']") + */ + $.AdminLTE.pushMenu = { + activate: function (toggleBtn) { + //Get the screen sizes + var screenSizes = $.AdminLTE.options.screenSizes; + + //Enable sidebar toggle + $(document).on('click', toggleBtn, function (e) { + e.preventDefault(); + + //Enable sidebar push menu + if ($(window).width() > (screenSizes.sm - 1)) { + if ($("body").hasClass('sidebar-collapse')) { + $("body").removeClass('sidebar-collapse').trigger('expanded.pushMenu'); + } else { + $("body").addClass('sidebar-collapse').trigger('collapsed.pushMenu'); + } + } + //Handle sidebar push menu for small screens + else { + if ($("body").hasClass('sidebar-open')) { + $("body").removeClass('sidebar-open').removeClass('sidebar-collapse').trigger('collapsed.pushMenu'); + } else { + $("body").addClass('sidebar-open').trigger('expanded.pushMenu'); + } + } + }); + + $(".content-wrapper").click(function () { + //Enable hide menu when clicking on the content-wrapper on small screens + if ($(window).width() <= (screenSizes.sm - 1) && $("body").hasClass("sidebar-open")) { + $("body").removeClass('sidebar-open'); + } + }); + + //Enable expand on hover for sidebar mini + if ($.AdminLTE.options.sidebarExpandOnHover + || ($('body').hasClass('fixed') + && $('body').hasClass('sidebar-mini'))) { + this.expandOnHover(); + } + }, + expandOnHover: function () { + var _this = this; + var screenWidth = $.AdminLTE.options.screenSizes.sm - 1; + //Expand sidebar on hover + $('.main-sidebar').hover(function () { + if ($('body').hasClass('sidebar-mini') + && $("body").hasClass('sidebar-collapse') + && $(window).width() > screenWidth) { + _this.expand(); + } + }, function () { + if ($('body').hasClass('sidebar-mini') + && $('body').hasClass('sidebar-expanded-on-hover') + && $(window).width() > screenWidth) { + _this.collapse(); + } + }); + }, + expand: function () { + $("body").removeClass('sidebar-collapse').addClass('sidebar-expanded-on-hover'); + }, + collapse: function () { + if ($('body').hasClass('sidebar-expanded-on-hover')) { + $('body').removeClass('sidebar-expanded-on-hover').addClass('sidebar-collapse'); + } + } + }; + + /* Tree() + * ====== + * Converts the sidebar into a multilevel + * tree view menu. + * + * @type Function + * @Usage: $.AdminLTE.tree('.sidebar') + */ + $.AdminLTE.tree = function (menu) { + var _this = this; + var animationSpeed = $.AdminLTE.options.animationSpeed; + $(document).off('click', menu + ' li a') + .on('click', menu + ' li a', function (e) { + //Get the clicked link and the next element + var $this = $(this); + var checkElement = $this.next(); + + //Check if the next element is a menu and is visible + if ((checkElement.is('.treeview-menu')) && (checkElement.is(':visible')) && (!$('body').hasClass('sidebar-collapse'))) { + //Close the menu + checkElement.slideUp(animationSpeed, function () { + checkElement.removeClass('menu-open'); + //Fix the layout in case the sidebar stretches over the height of the window + //_this.layout.fix(); + }); + checkElement.parent("li").removeClass("active"); + } + //If the menu is not visible + else if ((checkElement.is('.treeview-menu')) && (!checkElement.is(':visible'))) { + //Get the parent menu + var parent = $this.parents('ul').first(); + //Close all open menus within the parent + var ul = parent.find('ul:visible').slideUp(animationSpeed); + //Remove the menu-open class from the parent + ul.removeClass('menu-open'); + //Get the parent li + var parent_li = $this.parent("li"); + + //Open the target menu and add the menu-open class + checkElement.slideDown(animationSpeed, function () { + //Add the class active to the parent li + checkElement.addClass('menu-open'); + parent.find('li.active').removeClass('active'); + parent_li.addClass('active'); + //Fix the layout in case the sidebar stretches over the height of the window + _this.layout.fix(); + }); + } + //if this isn't a link, prevent the page from being redirected + if (checkElement.is('.treeview-menu')) { + e.preventDefault(); + } + }); + }; + + /* ControlSidebar + * ============== + * Adds functionality to the right sidebar + * + * @type Object + * @usage $.AdminLTE.controlSidebar.activate(options) + */ + $.AdminLTE.controlSidebar = { + //instantiate the object + activate: function () { + //Get the object + var _this = this; + //Update options + var o = $.AdminLTE.options.controlSidebarOptions; + //Get the sidebar + var sidebar = $(o.selector); + //The toggle button + var btn = $(o.toggleBtnSelector); + + //Listen to the click event + btn.on('click', function (e) { + e.preventDefault(); + //If the sidebar is not open + if (!sidebar.hasClass('control-sidebar-open') + && !$('body').hasClass('control-sidebar-open')) { + //Open the sidebar + _this.open(sidebar, o.slide); + } else { + _this.close(sidebar, o.slide); + } + }); + + //If the body has a boxed layout, fix the sidebar bg position + var bg = $(".control-sidebar-bg"); + _this._fix(bg); + + //If the body has a fixed layout, make the control sidebar fixed + if ($('body').hasClass('fixed')) { + _this._fixForFixed(sidebar); + } else { + //If the content height is less than the sidebar's height, force max height + if ($('.content-wrapper, .right-side').height() < sidebar.height()) { + _this._fixForContent(sidebar); + } + } + }, + //Open the control sidebar + open: function (sidebar, slide) { + //Slide over content + if (slide) { + sidebar.addClass('control-sidebar-open'); + } else { + //Push the content by adding the open class to the body instead + //of the sidebar itself + $('body').addClass('control-sidebar-open'); + } + }, + //Close the control sidebar + close: function (sidebar, slide) { + if (slide) { + sidebar.removeClass('control-sidebar-open'); + } else { + $('body').removeClass('control-sidebar-open'); + } + }, + _fix: function (sidebar) { + var _this = this; + if ($("body").hasClass('layout-boxed')) { + sidebar.css('position', 'absolute'); + sidebar.height($(".wrapper").height()); + if (_this.hasBindedResize) { + return; + } + $(window).resize(function () { + _this._fix(sidebar); + }); + _this.hasBindedResize = true; + } else { + sidebar.css({ + 'position': 'fixed', + 'height': 'auto' + }); + } + }, + _fixForFixed: function (sidebar) { + sidebar.css({ + 'position': 'fixed', + 'max-height': '100%', + 'overflow': 'auto', + 'padding-bottom': '50px' + }); + }, + _fixForContent: function (sidebar) { + $(".content-wrapper, .right-side").css('min-height', sidebar.height()); + } + }; + + /* BoxWidget + * ========= + * BoxWidget is a plugin to handle collapsing and + * removing boxes from the screen. + * + * @type Object + * @usage $.AdminLTE.boxWidget.activate() + * Set all your options in the main $.AdminLTE.options object + */ + $.AdminLTE.boxWidget = { + selectors: $.AdminLTE.options.boxWidgetOptions.boxWidgetSelectors, + icons: $.AdminLTE.options.boxWidgetOptions.boxWidgetIcons, + animationSpeed: $.AdminLTE.options.animationSpeed, + activate: function (_box) { + var _this = this; + if (!_box) { + _box = document; // activate all boxes per default + } + //Listen for collapse event triggers + $(_box).on('click', _this.selectors.collapse, function (e) { + e.preventDefault(); + _this.collapse($(this)); + }); + + //Listen for remove event triggers + $(_box).on('click', _this.selectors.remove, function (e) { + e.preventDefault(); + _this.remove($(this)); + }); + }, + collapse: function (element) { + var _this = this; + //Find the box parent + var box = element.parents(".box").first(); + //Find the body and the footer + var box_content = box.find("> .box-body, > .box-footer, > form >.box-body, > form > .box-footer"); + if (!box.hasClass("collapsed-box")) { + //Convert minus into plus + element.children(":first") + .removeClass(_this.icons.collapse) + .addClass(_this.icons.open); + //Hide the content + box_content.slideUp(_this.animationSpeed, function () { + box.addClass("collapsed-box"); + }); + } else { + //Convert plus into minus + element.children(":first") + .removeClass(_this.icons.open) + .addClass(_this.icons.collapse); + //Show the content + box_content.slideDown(_this.animationSpeed, function () { + box.removeClass("collapsed-box"); + }); + } + }, + remove: function (element) { + //Find the box parent + var box = element.parents(".box").first(); + box.slideUp(this.animationSpeed); + } + }; +} + +/* ------------------ + * - Custom Plugins - + * ------------------ + * All custom plugins are defined below. + */ + +/* + * BOX REFRESH BUTTON + * ------------------ + * This is a custom plugin to use with the component BOX. It allows you to add + * a refresh button to the box. It converts the box's state to a loading state. + * + * @type plugin + * @usage $("#box-widget").boxRefresh( options ); + */ +(function ($) { + + "use strict"; + + $.fn.boxRefresh = function (options) { + + // Render options + var settings = $.extend({ + //Refresh button selector + trigger: ".refresh-btn", + //File source to be loaded (e.g: ajax/src.php) + source: "", + //Callbacks + onLoadStart: function (box) { + return box; + }, //Right after the button has been clicked + onLoadDone: function (box) { + return box; + } //When the source has been loaded + + }, options); + + //The overlay + var overlay = $('<div class="overlay"><div class="fa fa-refresh fa-spin"></div></div>'); + + return this.each(function () { + //if a source is specified + if (settings.source === "") { + if (window.console) { + window.console.log("Please specify a source first - boxRefresh()"); + } + return; + } + //the box + var box = $(this); + //the button + var rBtn = box.find(settings.trigger).first(); + + //On trigger click + rBtn.on('click', function (e) { + e.preventDefault(); + //Add loading overlay + start(box); + + //Perform ajax call + box.find(".box-body").load(settings.source, function () { + done(box); + }); + }); + }); + + function start(box) { + //Add overlay and loading img + box.append(overlay); + + settings.onLoadStart.call(box); + } + + function done(box) { + //Remove overlay and loading img + box.find(overlay).remove(); + + settings.onLoadDone.call(box); + } + + }; + +})(jQuery); + +/* + * EXPLICIT BOX CONTROLS + * ----------------------- + * This is a custom plugin to use with the component BOX. It allows you to activate + * a box inserted in the DOM after the app.js was loaded, toggle and remove box. + * + * @type plugin + * @usage $("#box-widget").activateBox(); + * @usage $("#box-widget").toggleBox(); + * @usage $("#box-widget").removeBox(); + */ +(function ($) { + + 'use strict'; + + $.fn.activateBox = function () { + $.AdminLTE.boxWidget.activate(this); + }; + + $.fn.toggleBox = function () { + var button = $($.AdminLTE.boxWidget.selectors.collapse, this); + $.AdminLTE.boxWidget.collapse(button); + }; + + $.fn.removeBox = function () { + var button = $($.AdminLTE.boxWidget.selectors.remove, this); + $.AdminLTE.boxWidget.remove(button); + }; + +})(jQuery); + +/* + * TODO LIST CUSTOM PLUGIN + * ----------------------- + * This plugin depends on iCheck plugin for checkbox and radio inputs + * + * @type plugin + * @usage $("#todo-widget").todolist( options ); + */ +(function ($) { + + 'use strict'; + + $.fn.todolist = function (options) { + // Render options + var settings = $.extend({ + //When the user checks the input + onCheck: function (ele) { + return ele; + }, + //When the user unchecks the input + onUncheck: function (ele) { + return ele; + } + }, options); + + return this.each(function () { + + if (typeof $.fn.iCheck != 'undefined') { + $('input', this).on('ifChecked', function () { + var ele = $(this).parents("li").first(); + ele.toggleClass("done"); + settings.onCheck.call(ele); + }); + + $('input', this).on('ifUnchecked', function () { + var ele = $(this).parents("li").first(); + ele.toggleClass("done"); + settings.onUncheck.call(ele); + }); + } else { + $('input', this).on('change', function () { + var ele = $(this).parents("li").first(); + ele.toggleClass("done"); + if ($('input', ele).is(":checked")) { + settings.onCheck.call(ele); + } else { + settings.onUncheck.call(ele); + } + }); + } + }); + }; +}(jQuery)); diff --git a/login/app/sprinkles/core/assets/userfrosting/js/attrchange.js b/login/app/sprinkles/core/assets/userfrosting/js/attrchange.js new file mode 100755 index 0000000..00878d3 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/js/attrchange.js @@ -0,0 +1,124 @@ +/* +A simple jQuery function that can add listeners on attribute change. +http://meetselva.github.io/attrchange/ + +About License: +Copyright (C) 2013-2014 Selvakumar Arumugam +You may use attrchange plugin under the terms of the MIT Licese. +https://github.com/meetselva/attrchange/blob/master/MIT-License.txt + */ +(function($) { + function isDOMAttrModifiedSupported() { + var p = document.createElement('p'); + var flag = false; + + if (p.addEventListener) { + p.addEventListener('DOMAttrModified', function() { + flag = true + }, false); + } else if (p.attachEvent) { + p.attachEvent('onDOMAttrModified', function() { + flag = true + }); + } else { return false; } + p.setAttribute('id', 'target'); + return flag; + } + + function checkAttributes(chkAttr, e) { + if (chkAttr) { + var attributes = this.data('attr-old-value'); + + if (e.attributeName.indexOf('style') >= 0) { + if (!attributes['style']) + attributes['style'] = {}; //initialize + var keys = e.attributeName.split('.'); + e.attributeName = keys[0]; + e.oldValue = attributes['style'][keys[1]]; //old value + e.newValue = keys[1] + ':' + + this.prop("style")[$.camelCase(keys[1])]; //new value + attributes['style'][keys[1]] = e.newValue; + } else { + e.oldValue = attributes[e.attributeName]; + e.newValue = this.attr(e.attributeName); + attributes[e.attributeName] = e.newValue; + } + + this.data('attr-old-value', attributes); //update the old value object + } + } + + //initialize Mutation Observer + var MutationObserver = window.MutationObserver + || window.WebKitMutationObserver; + + $.fn.attrchange = function(a, b) { + if (typeof a == 'object') {//core + var cfg = { + trackValues : false, + callback : $.noop + }; + //backward compatibility + if (typeof a === "function") { cfg.callback = a; } else { $.extend(cfg, a); } + + if (cfg.trackValues) { //get attributes old value + this.each(function(i, el) { + var attributes = {}; + for ( var attr, i = 0, attrs = el.attributes, l = attrs.length; i < l; i++) { + attr = attrs.item(i); + attributes[attr.nodeName] = attr.value; + } + $(this).data('attr-old-value', attributes); + }); + } + + if (MutationObserver) { //Modern Browsers supporting MutationObserver + var mOptions = { + subtree : false, + attributes : true, + attributeOldValue : cfg.trackValues + }; + var observer = new MutationObserver(function(mutations) { + mutations.forEach(function(e) { + var _this = e.target; + //get new value if trackValues is true + if (cfg.trackValues) { + e.newValue = $(_this).attr(e.attributeName); + } + if ($(_this).data('attrchange-status') === 'connected') { //execute if connected + cfg.callback.call(_this, e); + } + }); + }); + + return this.data('attrchange-method', 'Mutation Observer').data('attrchange-status', 'connected') + .data('attrchange-obs', observer).each(function() { + observer.observe(this, mOptions); + }); + } else if (isDOMAttrModifiedSupported()) { //Opera + //Good old Mutation Events + return this.data('attrchange-method', 'DOMAttrModified').data('attrchange-status', 'connected').on('DOMAttrModified', function(event) { + if (event.originalEvent) { event = event.originalEvent; }//jQuery normalization is not required + event.attributeName = event.attrName; //property names to be consistent with MutationObserver + event.oldValue = event.prevValue; //property names to be consistent with MutationObserver + if ($(this).data('attrchange-status') === 'connected') { //disconnected logically + cfg.callback.call(this, event); + } + }); + } else if ('onpropertychange' in document.body) { //works only in IE + return this.data('attrchange-method', 'propertychange').data('attrchange-status', 'connected').on('propertychange', function(e) { + e.attributeName = window.event.propertyName; + //to set the attr old value + checkAttributes.call($(this), cfg.trackValues, e); + if ($(this).data('attrchange-status') === 'connected') { //disconnected logically + cfg.callback.call(this, e); + } + }); + } + return this; + } else if (typeof a == 'string' && $.fn.attrchange.hasOwnProperty('extensions') && + $.fn.attrchange['extensions'].hasOwnProperty(a)) { //extensions/options + return $.fn.attrchange['extensions'][a].call(this, b); + } + } +})(jQuery);
\ No newline at end of file diff --git a/login/app/sprinkles/core/assets/userfrosting/js/fortress-jqueryvalidation-methods.js b/login/app/sprinkles/core/assets/userfrosting/js/fortress-jqueryvalidation-methods.js new file mode 100755 index 0000000..b008cf2 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/js/fortress-jqueryvalidation-methods.js @@ -0,0 +1,57 @@ +$.validator.addMethod("equals", function(value, element, params) { + params = $.extend( + true, // deep extend + { + value: '', + caseSensitive: false + }, params); + + if (!params.caseSensitive) { + params.value = params.value.toLowerCase(); + value = value.toLowerCase(); + } + return this.optional(element) || value == params.value; +}, "Value is not correct."); + +$.validator.addMethod("notEquals", function(value, element, params) { + params = $.extend( + true, // deep extend + { + value: '', + caseSensitive: false + }, params); + + if (!params.caseSensitive) { + params.value = params.value.toLowerCase(); + value = value.toLowerCase(); + } + return this.optional(element) || value != params.value; +}, "Value is not correct."); + +$.validator.addMethod("noLeadingWhitespace", function(value, element) { + return this.optional(element) || /^\S.*$/i.test(value); +}, "No leading whitespace allowed"); + +$.validator.addMethod("noTrailingWhitespace", function(value, element) { + return this.optional(element) || /^.*\S$/i.test(value); +}, "No trailing whitespace allowed"); + +jQuery.validator.addMethod("memberOf", function(value, element, arr) { + return $.inArray(value, arr) != -1; +}, "Data provided must match one of the provided options."); + +jQuery.validator.addMethod("notMemberOf", function(value, element, arr) { + return $.inArray(value, arr) == -1; +}, "Data provided must NOT match one of the provided options."); + +jQuery.validator.addMethod("matchFormField", function(value, element, field) { + return value === $(element).closest('form').find("input[name=" + field + "]").val(); +}, "The specified fields must match."); + +jQuery.validator.addMethod("notMatchFormField", function(value, element, field) { + return value !== $(element).closest('form').find("input[name=" + field + "]").val(); +}, "The specified fields must NOT match."); + +$.validator.addMethod("username", function(value, element) { + return this.optional(element) || /^([a-z0-9\.\-_])+$/.test(value); +}, "The field may only contain lowercase letters, digits, '.', '-', and '_'."); diff --git a/login/app/sprinkles/core/assets/userfrosting/js/handlebars-helpers.js b/login/app/sprinkles/core/assets/userfrosting/js/handlebars-helpers.js new file mode 100755 index 0000000..96f47bb --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/js/handlebars-helpers.js @@ -0,0 +1,119 @@ +/** + * This file contains extra helper functions for Handlebars.js. + * + * @see http://handlebarsjs.com/#helpers + */ + + /** + * Improved comparison operator + * See https://stackoverflow.com/a/16315366/2970321 + */ +Handlebars.registerHelper('ifx', function (v1, operator, v2, options) { + switch (operator) { + case '==': + return (v1 == v2) ? options.fn(this) : options.inverse(this); + case '===': + return (v1 === v2) ? options.fn(this) : options.inverse(this); + case '!=': + return (v1 != v2) ? options.fn(this) : options.inverse(this); + case '!==': + return (v1 !== v2) ? options.fn(this) : options.inverse(this); + case '<': + return (v1 < v2) ? options.fn(this) : options.inverse(this); + case '<=': + return (v1 <= v2) ? options.fn(this) : options.inverse(this); + case '>': + return (v1 > v2) ? options.fn(this) : options.inverse(this); + case '>=': + return (v1 >= v2) ? options.fn(this) : options.inverse(this); + case '&&': + return (v1 && v2) ? options.fn(this) : options.inverse(this); + case '||': + return (v1 || v2) ? options.fn(this) : options.inverse(this); + default: + return (v1 == v2) ? options.fn(this) : options.inverse(this); + } +}); + +/** + * Perform simple calculations. + * + * usage: {{calc x '+' 2}} + */ +Handlebars.registerHelper('calc', function (v1, operator, v2, options) { + lvalue = parseFloat(v1); + rvalue = parseFloat(v2); + + return { + "+": lvalue + rvalue, + "-": lvalue - rvalue, + "*": lvalue * rvalue, + "/": lvalue / rvalue, + "%": lvalue % rvalue + }[operator]; +}); + +/** + * format an ISO date using Moment.js + * + * moment syntax example: moment(Date("2011-07-18T15:50:52")).format("MMMM YYYY") + * usage: {{dateFormat creation_date format="MMMM YYYY"}} + * @requires momentjs http://momentjs.com/ + */ +Handlebars.registerHelper('dateFormat', function(context, block) { + if (window.moment) { + var f = block.hash.format || "MMM Do, YYYY"; + return moment(context).format(f); + } else { + // moment plugin not available. return data as is. + console.log("The moment.js plugin is not loaded. Please make sure you have included moment.js on this page."); + return context; + } +}); + +/** + * Format a phone number. + */ +Handlebars.registerHelper("phoneUSFormat", function(phoneNumber) { + if (typeof phoneNumber === 'undefined') { + return ''; + } + + phoneNumber = phoneNumber.toString(); + return "(" + phoneNumber.substr(0,3) + ") " + phoneNumber.substr(3,3) + "-" + phoneNumber.substr(6,4); +}); + +/** + * Format currency (USD). + */ +Handlebars.registerHelper("currencyUsdFormat", function(amount) { + var parsedAmount = parseFloat(amount); + if (parsedAmount < 0) { + return "-$" + Math.abs(parsedAmount).toFixed(2); + } else { + return "$" + parsedAmount.toFixed(2); + } +}); + +/** + * Convert a string to a slug using speakingurl.js. + * + * @requires speakingurl https://pid.github.io/speakingurl/ + */ +Handlebars.registerHelper('slug', function(text) { + return getSlug(text); +}); + +/** + * Equality helper for Handlebars + * http://stackoverflow.com/questions/8853396/logical-operator-in-a-handlebars-js-if-conditional/21915381#21915381 + * @deprecated since 4.1 - use ifx instead + * usage: {{ifCond apple orange}} + */ +Handlebars.registerHelper('ifCond', function(v1, v2, options) { + if(v1 == v2) { + return options.fn(this); + } + + return options.inverse(this); +}); diff --git a/login/app/sprinkles/core/assets/userfrosting/js/query-string.js b/login/app/sprinkles/core/assets/userfrosting/js/query-string.js new file mode 100755 index 0000000..5e0d780 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/js/query-string.js @@ -0,0 +1,65 @@ +/** + * @add jQuery.String + */ +$.String = $.extend($.String || {}, { + /** + * @function deparam + * + * Takes a string of name value pairs and returns a Object literal that represents those params. + * + * @param {String} params a string like <code>"foo=bar&person[age]=3"</code> + * @return {Object} A JavaScript Object that represents the params: + * + * { + * foo: "bar", + * person: { + * age: "3" + * } + * } + */ + deparam: function(params){ + var digitTest = /^\d+$/, + keyBreaker = /([^\[\]]+)|(\[\])/g, + plus = /\+/g, + paramTest = /([^?#]*)(#.*)?$/; + + if(! params || ! paramTest.test(params) ) { + return {}; + } + + + var data = {}, + pairs = params.split('&'), + current; + + for(var i=0; i < pairs.length; i++){ + current = data; + var pair = pairs[i].split('='); + + // if we find foo=1+1=2 + if(pair.length != 2) { + pair = [pair[0], pair.slice(1).join("=")] + } + + var key = decodeURIComponent(pair[0].replace(plus, " ")), + value = decodeURIComponent(pair[1].replace(plus, " ")), + parts = key.match(keyBreaker); + + for ( var j = 0; j < parts.length - 1; j++ ) { + var part = parts[j]; + if (!current[part] ) { + // if what we are pointing to looks like an array + current[part] = digitTest.test(parts[j+1]) || parts[j+1] == "[]" ? [] : {} + } + current = current[part]; + } + lastPart = parts[parts.length - 1]; + if(lastPart == "[]"){ + current.push(value) + }else{ + current[lastPart] = value; + } + } + return data; + } +}); diff --git a/login/app/sprinkles/core/assets/userfrosting/js/tablesorter/widget-sort2Hash.js b/login/app/sprinkles/core/assets/userfrosting/js/tablesorter/widget-sort2Hash.js new file mode 100755 index 0000000..1be9c85 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/js/tablesorter/widget-sort2Hash.js @@ -0,0 +1,271 @@ +/*! Widget: sort2Hash (BETA) - updated 8/12/2017 (v2.28.15.uf) */ +/* Requires tablesorter v2.8+ and jQuery 1.7+ + * by Rob Garrison + * + * Temporary patched version of widget to handle browser history issues (#712). + */ +;( function( $ ) { + 'use strict'; + var ts = $.tablesorter || {}, + s2h = ts.sort2Hash = { + init : function( c, wo ) { + var filter, temp, page, size, + table = c.table, + pager = c.pager, + hasSaveSort = ts.hasWidget( table, 'saveSort' ), + sort = s2h.decodeHash( c, wo, 'sort' ); + if ( ( sort && !hasSaveSort ) || ( sort && hasSaveSort && wo.sort2Hash_overrideSaveSort ) ) { + s2h.convertString2Sort( c, wo, sort ); + } + if ( ts.hasWidget( c.table, 'pager' ) ) { + temp = parseInt( s2h.decodeHash( c, wo, 'page' ), 10 ); + page = pager.page = ( temp < 0 ? 0 : ( temp > pager.totalPages ? pager.totalPages - 1 : temp ) ); + size = pager.size = parseInt( s2h.decodeHash( c, wo, 'size' ), 10 ); + } + if ( ts.hasWidget( table, 'filter' ) ) { + filter = s2h.decodeHash( c, wo, 'filter' ); + if ( filter ) { + filter = filter.split( wo.sort2Hash_separator ); + c.$table.one( 'tablesorter-ready', function() { + setTimeout(function(){ + c.$table.one( 'filterEnd', function() { + $(this).triggerHandler( 'pageAndSize', [ page, size ] ); + }); + // use the newest filter comparison code + if ( ts.filter.equalFilters ) { + temp = ts.filter.equalFilters( c, c.lastSearch, filter ); + } else { + // quick n' dirty comparison... it will miss filter changes of + // the same value in a different column, see #1363 + temp = ( c.lastSearch || [] ).join( '' ) !== ( filter || [] ).join( '' ); + } + // don't set filters if they haven't changed + if ( !temp ) { + $.tablesorter.setFilters( table, filter, true ); + } + }, 100 ); + }); + } + } + if ( !filter ) { + c.$table.one( 'tablesorter-ready', function() { + c.$table.triggerHandler( 'pageAndSize', [ page, size ] ); + }); + } + + c.$table.on( 'sortEnd.sort2hash filterEnd.sort2hash pagerComplete.sort2Hash', function() { + if ( this.hasInitialized ) { + s2h.setHash( this.config, this.config.widgetOptions ); + } + }); + }, + + getTableId : function( c, wo ) { + // option > table id > table index on page + return wo.sort2Hash_tableId || + c.table.id || + 'table' + $( 'table' ).index( c.$table ); + }, + regexEscape : function( v ) { + return v.replace( /([\.\^\$\*\+\-\?\(\)\[\]\{\}\\\|])/g, '\\$1'); + }, + // convert 'first%20name,asc,last%20name,desc' into [[0,0], [1,1]] + convertString2Sort : function( c, wo, sortHash ) { + var regex, column, direction, temp, index, $cell, + arry = sortHash.split( wo.sort2Hash_separator ), + indx = 0, + len = arry.length, + sort = []; + while ( indx < len ) { + // column index or text + column = arry[ indx++ ]; + temp = parseInt( column, 10 ); + // ignore wo.sort2Hash_useHeaderText setting & + // just see if column contains a number + if ( isNaN( temp ) || temp > c.columns ) { + regex = new RegExp( '(' + s2h.regexEscape( column ) + ')', 'i' ); + for ( index = 0; index < c.columns; index++ ) { + $cell = c.$headerIndexed[ index ]; + if ( regex.test( $cell.attr( wo.sort2Hash_headerTextAttr ) ) ) { + column = index; + index = c.columns; + } + } + } + direction = arry[ indx++ ]; + // ignore unpaired values + if ( typeof column !== 'undefined' && typeof direction !== 'undefined' ) { + // convert text to 0, 1 + if ( isNaN( direction ) ) { + // default to ascending sort + direction = direction.indexOf( wo.sort2Hash_directionText[ 1 ] ) > -1 ? 1 : 0; + } + sort.push( [ column, direction ] ); + } + } + if ( sort.length ) { + c.sortList = sort; + } + }, + + // convert [[0,0],[1,1]] to 'first%20name,asc,last%20name,desc' + convertSort2String : function( c, wo ) { + var index, txt, column, direction, + sort = [], + arry = c.sortList || [], + len = arry.length; + for ( index = 0; index < len; index++ ) { + column = arry[ index ][ 0 ]; + txt = $.trim( c.$headerIndexed[ column ].attr( wo.sort2Hash_headerTextAttr ) ); + sort.push( txt !== '' ? encodeURIComponent( txt ) : column ); + direction = wo.sort2Hash_directionText[ arry[ index ][ 1 ] ]; + sort.push( direction ); + } + // join with separator + return sort.join( wo.sort2Hash_separator ); + }, + + convertFilter2String : function( c, wo ) { + var index, txt, column, direction, + sort = [], + arry = c.sortList || [], + len = arry.length; + for ( index = 0; index < len; index++ ) { + column = arry[ index ][ 0 ]; + txt = $.trim( c.$headerIndexed[ column ].attr( wo.sort2Hash_headerTextAttr ) ); + column = typeof txt !== 'undefined' ? encodeURIComponent( txt ) : column; + sort.push( column ); + direction = wo.sort2Hash_directionText[ arry[ index ][ 1 ] ]; + sort.push( direction ); + } + // join with separator + return sort.join( wo.sort2Hash_separator ); + }, + + // Get URL Parameters (getParam) + // modified from http://www.netlobo.com/url_query_string_javascript.html + getParam : function ( name, hash, returnRegex ) { + if ( !hash ) { hash = window.location.hash; } + var regex = new RegExp( '[\\?&]' + s2h.regexEscape( name ) + '=([^&#]*)' ), + match = regex.exec( hash ); + if ( returnRegex ) { return regex; } + return match === null ? '' : decodeURIComponent( match[ 1 ] ); + }, + + // remove parameter from hash + removeParam : function( name, hash ) { + if ( !hash ) { hash = window.location.hash; } + var index, + regex = s2h.getParam( name, hash, true ), + result = [], + parts = hash.split( '&' ), + len = parts.length; + for ( index = 0; index < len; index++ ) { + // regex expects a leading '&'... + if ( !regex.test( '&' + parts[ index ] ) ) { + result.push( parts[ index ] ); + } + } + return result.length ? result.join( '&' ) : ''; + }, + + encodeHash : function( c, wo, component, value, rawValue ) { + var result = false, + tableId = s2h.getTableId( c, wo ); + if ( typeof wo.sort2Hash_encodeHash === 'function' ) { + result = wo.sort2Hash_encodeHash( c, tableId, component, value, rawValue || value ); + } + if ( result === false ) { + result = '&' + component + '[' + tableId + ']=' + value; + } + return result; + }, + + decodeHash : function( c, wo, component ) { + var result = false, + tableId = s2h.getTableId( c, wo ); + if ( typeof wo.sort2Hash_decodeHash === 'function' ) { + // return a string + result = wo.sort2Hash_decodeHash( c, tableId, component ); + } + if ( result === false ) { + result = s2h.getParam( component + '[' + tableId + ']' ); + } + return result || ''; + }, + + cleanHash : function( c, wo, component, hash ) { + var result = false, + tableId = s2h.getTableId( c, wo ); + if ( typeof wo.sort2Hash_cleanHash === 'function' ) { + // can return an array or string + result = wo.sort2Hash_cleanHash( c, tableId, component, hash ); + } + if ( result === false ) { + // parameter example: 'sort[table0]=0,0' + result = s2h.removeParam( component + '[' + tableId + ']', hash ); + } + return result || ''; + }, + + setHash : function( c, wo ) { + var str = '', + hash = window.location.hash, + hasPager = ts.hasWidget( c.table, 'pager' ), + hasFilter = ts.hasWidget( c.table, 'filter' ), + sortList = s2h.convertSort2String( c, wo ), + filters = ( hasFilter && c.lastSearch.join('') !== '' ? c.lastSearch : [] ), + filtersStr = encodeURIComponent( filters.join( c.widgetOptions.sort2Hash_separator ) ), + components = { + 'sort' : sortList ? s2h.encodeHash( c, wo, 'sort', sortList, c.sortList ) : '', + 'page' : hasPager ? s2h.encodeHash( c, wo, 'page', c.pager.page + 1 ) : '', + 'size' : hasPager ? s2h.encodeHash( c, wo, 'size', c.pager.size ) : '', + 'filter' : filtersStr ? s2h.encodeHash( c, wo, 'filter', filtersStr, filters ) : '' + }; + // remove old hash + $.each( components, function( component, value ) { + hash = s2h.cleanHash( c, wo, component, hash ); + str += value; + }); + + // Combine new hash with any existing hashes + var hashChar = c.widgetOptions.sort2Hash_hash; + var newHash = ( ( window.location.hash || '' ).replace( hashChar, '' ).length ? hash : wo.sort2Hash_hash ) + str; + var baseUrl = window.location.href.split(hashChar)[0]; + // Ensure that there is a leading hash character + var firstChar = newHash[0]; + if (firstChar != hashChar) { + newHash = hashChar + newHash; + } + + // Update URL in browser + window.location.replace(baseUrl + newHash); + } + }; + + ts.addWidget({ + id: 'sort2Hash', + priority: 60, // after saveSort & pager + options: { + sort2Hash_hash : '#', // hash prefix + sort2Hash_separator : '-', // don't '#' or '=' here + sort2Hash_headerTextAttr : 'data-header', // data attribute containing alternate header text + sort2Hash_directionText : [ 0, 1 ], // [ 'asc', 'desc' ], + sort2Hash_overrideSaveSort : false, // if true, override saveSort widget if saved sort available + + // this option > table ID > table index on page + sort2Hash_tableId : null, + // custom hash processing functions + sort2Hash_encodeHash : null, + sort2Hash_decodeHash : null, + sort2Hash_cleanHash : null + }, + init: function(table, thisWidget, c, wo) { + s2h.init( c, wo ); + }, + remove: function(table, c) { + c.$table.off( '.sort2hash' ); + } + }); + +})(jQuery); diff --git a/login/app/sprinkles/core/assets/userfrosting/js/uf-alerts.js b/login/app/sprinkles/core/assets/userfrosting/js/uf-alerts.js new file mode 100755 index 0000000..06a889c --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/js/uf-alerts.js @@ -0,0 +1,285 @@ +/** + * ufAlerts jQuery plugin. Fetches and renders alerts from the UF alert stream. + * + * Based on template from https://github.com/jquery-boilerplate/jquery-boilerplate + * + * === USAGE === + + * ufAlerts can be initialized on any container element as follows: + * + * $('#myDiv').ufAlerts(options); + * + * `options` is an object containing any of the following parameters: + * @param {string} url The absolute URL from which to fetch flash alerts. + * @param {bool} scrollToTop Whether to automatically scroll back to the top of the page after rendering alerts. + * @param {string} alertMessageClass The CSS class(es) to be applied to each alert message. + * @param {string} alertTemplateId The CSS id(es) for the Handlebar alert template. + * @param {bool} agglomerate Set to true to render all alerts together, applying styling for the highest-priority alert being rendered. + * + * == EVENTS == + * + * uf-form triggers the following events: + * + * `fetch.ufAlerts`: triggered when the alerts have been successfully fetched from the server. + * `render.ufAlerts`: triggered when all alerts have been rendered and the call to render() has completed. + * + * == METHODS == + * + * `fetch()`: Asynchronously gets alerts from the server. + * `push(options)`: Adds a alert of a specified type (danger, warning, info, success) to the internal collection of alerts. + * `clear()`: Removes all messages from the internal collection. + * `render()`: Renders the collection of alerts to the container, awaiting results of `fetch()` if required. + * + * UserFrosting https://www.userfrosting.com + * @author Alexander Weissman <https://alexanderweissman.com> + */ +;(function($, window, document, undefined) { + 'use strict'; + + // Define plugin name and defaults. + var pluginName = 'ufAlerts', + defaults = { + url : site.uri.public + '/alerts', + scrollToTop : true, + scrollWhenVisible : false, + agglomerate : false, + alertMessageClass : 'uf-alert-message', + alertTemplateId : 'uf-alert-template', + DEBUG : false + }; + + // Constructor + function Plugin (element, options) { + this.element = element[0]; + this.$element = $(this.element); + this.settings = $.extend(true, {}, defaults, options); + this._defaults = defaults; + this._name = pluginName; + + // Detect changes to element attributes + this.$element.attrchange({ callback: function (event) { this.element = event.target; }.bind(this) }); + + // Plugin variables + this.alerts = []; + this._newAlertsPromise = $.Deferred().resolve(); + this._alertTemplateHtml = $('#' + this.settings.alertTemplateId).html(); + this._alertTypePriorities = { + danger : 3, + warning: 2, + success: 1, + info : 0 + }; + this._alertTypeIcon = { + danger : 'fa-ban', + warning: 'fa-warning', + success: 'fa-check', + info : 'fa-info' + }; + + return this; + } + + // Functions + $.extend(Plugin.prototype, { + /** + * Clear all alerts from the current uf-alerts collection. + */ + clear: function() { + // See http://stackoverflow.com/a/1232046/2970321 + this.alerts.length = 0; + + if (this.settings.agglomerate) { + this.element.toggleClass('alert', false) + .toggleClass('alert-info', false) + .toggleClass('alert-success', false) + .toggleClass('alert-warning', false) + .toggleClass('alert-danger', false); + } + + // Clear any alert HTML + this.$element.empty(); + + return this.$element; + }, + /** + * Fetches alerts from the alert stream + */ + fetch: function() { + // Set a promise, so that any chained calls after fetch can wait until the messages have been retrieved + this._newAlertsPromise = $.ajax({ + url: this.settings.url, + cache: false + }).then( + // Success + this._fetchSuccess.bind(this), + // Failure + this._fetchFailure.bind(this) + ); + + return this.$element; + }, + /** + * Success callback for fetch + */ + _fetchSuccess: function(alerts) { + if (alerts != null) this.alerts = $.merge(this.alerts, alerts); + this.$element.trigger('fetch.' + this._name); + }, + /** + * Failure callback for fetch + */ + _fetchFailure: function(response) { + this.$element.trigger('error.' + this._name); + if ((typeof site !== 'undefined') && site.debug.ajax && response.responseText) { + document.write(response.responseText); + document.close(); + } else { + if (this.settings.DEBUG) { + console.warn('Error (' + response.status + '): ' + response.responseText ); + } + } + }, + /** + * Push a given message to the current uf-alerts collection. + */ + push: function(options) { + this.alerts.push({ + type : options[0], + message: options[1] + }); + + return this.$element; + }, + /** + * Renders the alerts. + */ + render: function() { + // Wait for promise completion, only if promise is unresolved. + if (this._newAlertsPromise.state() == 'resolved' || this._newAlertsPromise.state() == 'rejected') { + this._render(); + } + else { + $.when(this._newAlertsPromise).then(this._render.bind(this)); + } + + return this.$element; + }, + /* + * Internal private method that physically handles rendering operation. + */ + _render: function() { + // Holds generated HTML + var alertHtml = ''; + // Only compile alerts if there are alerts to display + if (this.alerts.length > 0) { + // Prepare template + var alertTemplate = Handlebars.compile(this._alertTemplateHtml, {noEscape: true}); + var i; + // If agglomeration is enabled, set the container to the highest priority alert type + if (this.settings.agglomerate) { + // Holds generated agglomerated alerts + var alertMessage = '<ul>'; + + // Determine overall alert priority + var alertContainerType = 'info'; + for (i = 0; i < this.alerts.length; i++) { + if (this._alertTypePriorities[this.alerts[i].type] > this._alertTypePriorities[alertContainerType]) { + alertContainerType = this.alerts[i].type; + } + } + + // Compile each alert + var aggTemplate = Handlebars.compile('<li class=' + this.settings.alertMessageClass + '>{{ message }}</li>'); + for (i = 0; i < this.alerts.length; i++) { + alertMessage += aggTemplate(this.alerts[i]); + } + + alertMessage += '</ul>'; + + // Generate complete alert HTML + alertHtml = alertTemplate({ + type : alertContainerType, + message: alertMessage, + icon : this._alertTypeIcon[alertContainerType] + }); + } + else { + // Compile each alert. + for (i = 0; i < this.alerts.length; i++) { + var alert = this.alerts[i]; + + // Inject icon + alert.icon = this._alertTypeIcon[alert.type]; + + // Compile alert + alertHtml += alertTemplate(alert); + } + } + } + // Show alerts + this.$element.html(alertHtml); + + // Scroll to top of alert location is new alerts output, and auto scrolling is enabled + if (this.settings.scrollToTop && alertHtml !== '') { + // Don't scroll if already visible, unless scrollWhenVisible is true + if (!this._alertsVisible() || this.settings.scrollWhenVisible) { + $('html, body').animate({ scrollTop: this.$element.offset().top }, 'fast'); + } + } + + // Trigger render events + this.$element.trigger('render.' + this._name); + }, + /** + * Returns true if alerts container is completely within the viewport. + */ + _alertsVisible: function() { + var rect = this.element.getBoundingClientRect(); + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && + rect.right <= (window.innerWidth || document.documentElement.clientWidth) + ); + }, + /** + * Completely destroy the ufAlerts plugin on the element. + */ + destroy: function() { + // Unbind any bound events + this.$element.off('.' + this._name); + + // Remove plugin from element + this.$element.removeData(this._name); + + return this.$element; + } + }); + + // Handles instantiation and access to non-private methods. + $.fn[pluginName] = function(methodOrOptions) { + // Grab plugin instance + var instance = $(this).data(pluginName); + // If undefined or object, initalise plugin. + if (methodOrOptions === undefined || typeof methodOrOptions === 'object') { + // Only initalise if not previously done. + if (!instance) { + $(this).data(pluginName, new Plugin(this, methodOrOptions)); + } + return this; + } + // Otherwise ensure first parameter is a valid string, and is the name of an actual function. + else if (typeof methodOrOptions === 'string' && typeof instance[methodOrOptions] === 'function') { + // Ensure not a private function + if (methodOrOptions.indexOf('_') !== 0) { + return instance[methodOrOptions]( Array.prototype.slice.call(arguments, 1)); + } + else { + console.warn('Method ' + methodOrOptions + ' is private!'); + } + } + else { + console.warn('Method ' + methodOrOptions + ' does not exist.'); + } + }; +})(jQuery, window, document);
\ No newline at end of file diff --git a/login/app/sprinkles/core/assets/userfrosting/js/uf-captcha.js b/login/app/sprinkles/core/assets/userfrosting/js/uf-captcha.js new file mode 100755 index 0000000..ed3ecea --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/js/uf-captcha.js @@ -0,0 +1,13 @@ +/** + * This plugin reloads the captcha in the specified element. + */ +(function( $ ) { + $.fn.captcha = function() { + // Set the new captcha image + $(this).attr('src', site.uri.public + "/account/captcha?" + new Date().getTime()); + + // Clear whatever the user entered for the captcha value last time + var target = $(this).data('target'); + $(target).val(""); + }; +}( jQuery )); diff --git a/login/app/sprinkles/core/assets/userfrosting/js/uf-collection.js b/login/app/sprinkles/core/assets/userfrosting/js/uf-collection.js new file mode 100755 index 0000000..a2afc6e --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/js/uf-collection.js @@ -0,0 +1,345 @@ +/** + * uf-collection plugin. Widget for attaching/detaching related items to a single parent item (e.g. roles for a user, etc). + * + * === USAGE === + * + * uf-collection can be initialized on a div element as follows: + * + * $("#myCollection").ufCollection(options); + * + * `options` is an object containing any of the following parameters: + * @param {bool} useDropdown Set to true if rows should be added using a select2 dropdown, false for free text inputs (see https://ux.stackexchange.com/a/15637/53990). + * @param {Object} dropdown The options to pass to the select2 plugin for the add item dropdown. + * @param {string} dropdown.ajax.url The url from which to fetch options (as JSON data) in the dropdown selector menu. + * @param {bool} selectOnClose Set to true if you want the currently highlighted dropdown item to be automatically added when the dropdown is closed for any reason. + * @param {string} dropdown.theme The select2 theme to use for the dropdown menu. Defaults to "default". + * @param {string} dropdown.placeholder Placeholder text to use in the dropdown menu before a selection is made. Defaults to "Item". + * @param {string} dropdown.width Width of the dropdown selector, when used. Defaults to "100%". + * @param {callback} transformDropdownSelection Custom transformation on objects from the dropdown before passing them to render in the collection table. + * @param {Object} dropdownControl a jQuery selector specifying the dropdown select2 control. Defaults to looking for a .js-select-new element inside the parent object. + * @param {string} dropdownTemplate A Handlebars template to use for rendering the dropdown items. + * @param {Object} rowContainer a jQuery selector specifying the place where rows should be added. Defaults to looking for the first tbody element inside the parent object. + * @param {string} rowTemplate A Handlebars template to use for rendering each row in the table. + * + * == EVENTS == + * + * ufCollection triggers the following events: + * + * `rowAdd.ufCollection`: triggered when a new row is added to the collection. + * `rowDelete.ufCollection`: triggered when a row is removed from the collection. + * `rowTouch.ufCollection`: triggered when any inputs in a row are brought into focus. + * + * UserFrosting https://www.userfrosting.com + * @author Alexander Weissman <https://alexanderweissman.com> + */ +;(function($, window, document, undefined) { + "use strict"; + + // Define plugin name and defaults. + var pluginName = "ufCollection", + defaults = { + useDropdown: true, + dropdown: { + ajax : { + url : "", + dataType : "json", + delay : 250, + data : function (params) { + return { + filters: { + info : params.term + } + }; + }, + processResults : function (data, params) { + // Process the data into dropdown options + var suggestions = []; + if (data && data.rows) { + suggestions = data.rows; + } + return { + results: suggestions + }; + }, + cache : true + }, + placeholder : "Item", + selectOnClose : false, // Make a selection when they click out of the box/press the next button + theme : "default", + width : "100%", + }, + transformDropdownSelection: function (item) { + return item; + }, + dropdownControl : null, + dropdownTemplate: "", + rowContainer : null, + rowTemplate : "", + DEBUG : false + }; + + // Constructor + function Plugin (element, options) { + this.element = element[0]; + this.$element = $(this.element); + var lateDefaults = { + dropdownControl: this.$element.find('.js-select-new'), + rowContainer: this.$element.find('tbody').first() + }; + this.settings = $.extend(true, {}, defaults, lateDefaults, options); + this._defaults = defaults; + this._name = pluginName; + + // Detect changes to element attributes + this.$element.attrchange({ callback: function (event) { this.element = event.target; }.bind(this) }); + + // Internal counter for adding rows to the collection. Gets updated every time `_createRow` is called. + this._rownum = 0; + + // Keep track of last added row + this._lastRow = null; + + // Handlebars template method + this._dropdownTemplateCompiled = Handlebars.compile(this.settings.dropdownTemplate); + + this._rowTemplateCompiled = Handlebars.compile(this.settings.rowTemplate); + + // Add container class + this.$element.toggleClass("uf-collection", true); + + // Add bindings for any rows already present in the DOM + $.each(this.settings.rowContainer.find('.uf-collection-row'), $.proxy(function(idx, row) { + this._onNewRow($(row)); + this._lastRow = row; + }, this)); + + // If we're using dropdown options, create the select2 and add bindings to add a new row when an option is selected + if (this.settings.useDropdown) { + this._initDropdownField(this.settings.dropdownControl); + + this.settings.dropdownControl.on("select2:select", $.proxy(function(e) { + var item = $(e.target).select2("data")[0]; + // Apply any transformations before rendering as a row + var transformed = this.settings.transformDropdownSelection(item); + this._createRow(transformed); + }, this)); + } + else { + // Otherwise, add a new virgin row + this._createVirginRow(); + } + + return this; + } + + // Functions + $.extend(Plugin.prototype, { + /** + * Add a new row to the collection, optionally passing in prepopulated template data. + */ + addRow: function(options) { + // Grab params, if any + var params = {}; + if (typeof options !== 'undefined') { + params = options[0]; + } + + this._createRow(params); + + return this.$element; + }, + /** + * Add a new 'virgin' row to the collection, optionally passing in prepopulated template data. + * Virgin rows are rows that have not yet been brought into focus by the user. + * When a virgin row is brought into focus, it loses its virgin status and a new virgin row is created. + */ + addVirginRow: function(options) { + // Grab params, if any + var params = {}; + if (typeof options !== 'undefined') { + params = options[0]; + } + + this._createVirginRow(params); + + return this.$element; + }, + /** + * Delete a target row. + */ + deleteRow: function(row) { + this._deleteRow(row); + + return this.$element; + }, + /** + * Get the dropdown control for the collection, if one exists. + */ + getDropdown: function() { + return this.settings.dropdownControl; + }, + /** + * Get the last row added in the collection. + */ + getLastRow: function () { + return this._lastRow; + }, + /** + * Touch a target row. + */ + touchRow: function(row) { + this._touchRow(row); + + return this.$element; + }, + /** + * Create a new row and attach the handler for deletion to the js-delete-row button + */ + _createRow: function(params) { + params = $.extend(true, + { + id: "", + rownum: this._rownum + }, params); + + // Generate the row and append to table + var newRowTemplate = this._rowTemplateCompiled(params), + newRow; + + // Add the new row before any virgin rows in the table. + var virginRows = this.settings.rowContainer.find('.uf-collection-row-virgin').length; + if (virginRows) { + newRow = $(newRowTemplate).insertBefore(this.settings.rowContainer.find('.uf-collection-row-virgin:first')); + } else { + newRow = $(newRowTemplate).appendTo(this.settings.rowContainer); + } + + this._lastRow = newRow; + + // Add bindings and fire event + this._onNewRow(newRow); + + return newRow; + }, + /** + * Create a new, blank row with the 'virgin' status. + */ + _createVirginRow: function(params) { + // Generate the row and append to table + var newRow = this._createRow(params); + + // Set the row's 'virgin' status + newRow.addClass('uf-collection-row-virgin'); + newRow.find('.js-delete-row').hide(); + + return newRow; + }, + /** + * Delete a row from the collection. + */ + _deleteRow: function(row) { + row.remove(); + this.$element.trigger('rowDelete.ufCollection'); + }, + /** + * Add delete and touch bindings for a row, increment the internal row counter, and fire the rowAdd event + */ + _onNewRow: function(row) { + // Trigger to delete row + row.find('.js-delete-row').on('click', $.proxy(function(e) { + this._deleteRow($(e.target).closest('.uf-collection-row')); + }, this)); + + // Once the new row comes into focus for the first time, it has been "touched" + row.find(':input').on('focus', $.proxy(function() { + this._touchRow(row); + }, this)); + + this._rownum += 1; + + // Fire event when row has been constructed + this.$element.trigger('rowAdd.ufCollection', row); + }, + /** + * Remove a row's virgin status, show the delete button, and add a new virgin row if needed + */ + _touchRow: function(row) { + row.removeClass('uf-collection-row-virgin'); + row.find('.js-delete-row').show(); + + this.$element.trigger('rowTouch.ufCollection', row); + + // If we're not using dropdowns, assert that the table doesn't already have a virgin row. If not, create a new virgin row. + if (!this.settings.useDropdown) { + var virginRows = this.settings.rowContainer.find('.uf-collection-row-virgin').length; + if (!virginRows) { + this._createVirginRow(); + } + } + }, + /** + * Initialize the select2 dropdown for this collection on a specified control element. + */ + _initDropdownField: function(field) { + var options = this.settings.dropdown; + + if (!("templateResult" in options)) { + options.templateResult = $.proxy(function(item) { + // Display loading text if the item is marked as "loading" + if (item.loading) return item.text; + + // Must wrap this in a jQuery selector to render as HTML + return $(this._dropdownTemplateCompiled(item)); + }, this); + } + // Legacy options (<= v4.0.9) + if ("dataUrl" in this.settings) { + options.ajax.url = this.settings.dataUrl; + } + if ("ajaxDelay" in this.settings) { + options.ajax.delay = this.settings.ajaxDelay; + } + if ("dropdownTheme" in this.settings) { + options.theme = this.settings.dropdownTheme; + } + if ("placeholder" in this.settings) { + options.placeholder = this.settings.placeholder; + } + if ("selectOnClose" in this.settings) { + options.selectOnClose = this.settings.selectOnClose; + } + if ("width" in this.settings) { + options.width = this.settings.width; + } + + return field.select2(options); + } + }); + + // Handles instantiation and access to non-private methods. + $.fn[pluginName] = function(methodOrOptions) { + // Grab plugin instance + var instance = $(this).data(pluginName); + // If undefined or object, initalise plugin. + if (methodOrOptions === undefined || typeof methodOrOptions === 'object') { + // Only initalise if not previously done. + if (!instance) { + $(this).data(pluginName, new Plugin(this, methodOrOptions)); + } + return this; + } + // Otherwise ensure first parameter is a valid string, and is the name of an actual function. + else if (typeof methodOrOptions === 'string' && typeof instance[methodOrOptions] === 'function') { + // Ensure not a private function + if (methodOrOptions.indexOf('_') !== 0) { + return instance[methodOrOptions]( Array.prototype.slice.call(arguments, 1)); + } + else { + console.warn( 'Method ' + methodOrOptions + ' is private!' ); + } + } + else { + console.warn( 'Method ' + methodOrOptions + ' does not exist.' ); + } + }; +})(jQuery, window, document); diff --git a/login/app/sprinkles/core/assets/userfrosting/js/uf-copy.js b/login/app/sprinkles/core/assets/userfrosting/js/uf-copy.js new file mode 100755 index 0000000..a47f6b8 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/js/uf-copy.js @@ -0,0 +1,50 @@ +/** + * Copies text or control fields to clipboard. Wrap a .js-copy-target and .js-copy-trigger inside a common .js-copy-container. + */ + +if (typeof $.uf === 'undefined') { + $.uf = {}; +} + +$.uf.copy = function (button) { + var _this = this; + + var clipboard = new Clipboard(button, { + text: function(trigger) { + var el = $(trigger).closest('.js-copy-container').find('.js-copy-target'); + if (el.is(':input')) { + return el.val(); + } else { + return el.html(); + } + } + }); + + clipboard.on('success', function(e) { + setTooltip(e.trigger, 'Copied!'); + hideTooltip(e.trigger); + }); + + clipboard.on('error', function(e) { + setTooltip(e.trigger, 'Failed!'); + hideTooltip(e.trigger); + }); + + function setTooltip(btn, message) { + $(btn) + .attr('data-original-title', message) + .tooltip('show'); + } + + function hideTooltip(btn) { + setTimeout(function() { + $(btn).tooltip('hide') + .attr('data-original-title', ""); + }, 1000); + } + + // Tooltip + $(button).tooltip({ + trigger: 'click' + }); +}; diff --git a/login/app/sprinkles/core/assets/userfrosting/js/uf-form.js b/login/app/sprinkles/core/assets/userfrosting/js/uf-form.js new file mode 100755 index 0000000..15952ab --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/js/uf-form.js @@ -0,0 +1,297 @@ +/** + * uf-form plugin. Handles validation and submission for basic UserFrosting forms. + * + * This plugin uses the jQueryvalidation plugin (https://jqueryvalidation.org/) to perform instant, client-side form validation. + * UserFrosting forms must be wrapped in a <form> element, and contain a <button type=submit> element for submission. + * + * Forms are then set to submit via AJAX when the submit button is clicked. + * + * === USAGE === + * + * uf-form can be initialized on a form element as follows: + * + * $('#myForm').ufForm(options); + * + * `options` is an object containing any of the following parameters: + * @param {JSON} validators An object containing two keys, "rules" and "messages", which specify the jQueryvalidation rules to use. + * @param {Object} msgTarget a jQuery selector specifying the element where any error messages should be inserted. Defaults to looking for a container with class .js-form-alerts inside this form. + * @param {Callback} beforeSubmitCallback a callback function to execute immediately after form validation, before the form is submitted. + * @param {bool} binaryCheckboxes specify whether to submit checkboxes as binary values 0 and 1, instead of omitting unchecked checkboxes from submission. + * + * == EVENTS == + * + * ufForm triggers the following events: + * + * `submitSuccess.ufForm`: triggered when the form is successfully submitted, after re-enabling the submit button. + * `submitError.ufForm`: triggered when the form submission (not validation) fails, after re-enabling the submit button + * and displaying any error messages. + * + * UserFrosting https://www.userfrosting.com + * @author Alexander Weissman <https://alexanderweissman.com> + * + * @todo Implement proper fallback for when `set` function isn't supported by FormData. + */ +;(function($, window, document, undefined) { + 'use strict'; + + // Define plugin name and defaults. + var pluginName = 'ufForm', + defaults = { + validators: { + 'rules' : {}, + 'messages': {} + }, + submittingText : "<i class='fa fa-spinner fa-spin'></i>", + beforeSubmitCallback: null, + binaryCheckboxes : true, // submit checked/unchecked checkboxes as 0/1 values + keyupDelay : 0, + DEBUG: false + }; + + // Constructor + function Plugin (element, options) { + this.element = element[0]; + this.$element = $(this.element); + var lateDefaults = { + encType : (typeof this.$element.attr('enctype') !== 'undefined') ? this.$element.attr('enctype') : '', + msgTarget: this.$element.find('.js-form-alerts:first') + }; + this.settings = $.extend(true, {}, defaults, lateDefaults, options); + this._defaults = $.extend(true, {}, defaults, lateDefaults); + this._name = pluginName; + this._debugAjax = (typeof site !== 'undefined') && site.debug.ajax; + + // Detect changes to element attributes + this.$element.attrchange({ + callback: function (event) { + this.element = event.target; + }.bind(this) + }); + + // Setup validator + this.validator = this.$element.validate({ + rules : this.settings.validators.rules, + messages : this.settings.validators.messages, + submitHandler: $.proxy(this._submitHandler, this), + onkeyup : $.proxy(this._onKeyUp, this) + }); + + return this; + } + + // Functions + + /** + * Handles the form submission after successful client-side validation. + */ + Plugin.prototype._submitHandler = function(form, event) { + // Execute any "before submit" callback + if (this.settings.beforeSubmitCallback) { + this.settings.beforeSubmitCallback(); + } + + var $form = $(form); + + // Set "loading" text for submit button, if it exists, and disable button + var submitButton = $form.find('button[type=submit]'); + if (submitButton) { + var submitButtonText = submitButton.html(); + submitButton.prop('disabled', true); + submitButton.html(this.settings.submittingText); + } + + // Get basic request parameters. + var reqParams = { + converters: { + // Override jQuery's strict JSON parsing + 'text json': function(result) { + try { + // First try to use native browser parsing + if (typeof JSON === 'object' && typeof JSON.parse === 'function') { + return JSON.parse(result); + } else { + return $.parseJSON(result); + } + } catch (e) { + // statements to handle any exceptions + console.warn('Could not parse expected JSON response.'); + return {}; + } + } + }, + + dataType: this._debugAjax ? 'html' : 'json', + type: this.$element.attr('method'), + url: this.$element.attr('action') + }; + + // Get the form encoding type from the users HTML, and chose an encoding form. + if (this.settings.encType.toLowerCase() === 'multipart/form-data') { + reqParams.data = this._multipartData($form); + // add additional params to fix jquery errors + reqParams.cache = false; + reqParams.contentType = false; + reqParams.processData = false; + } else { + reqParams.data = this._urlencodeData($form); + } + + // Submit the form via AJAX + $.ajax(reqParams).then( + // Submission successful + $.proxy(function(data, textStatus, jqXHR) { + // Restore button text and re-enable submit button + if (submitButton) { + submitButton.prop('disabled', false ); + submitButton.html(submitButtonText); + } + + this.$element.trigger('submitSuccess.ufForm', [data, textStatus, jqXHR]); + return jqXHR; + }, this), + // Submission failed + $.proxy(function(jqXHR, textStatus, errorThrown) { + // Restore button text and re-enable submit button + if (submitButton) { + submitButton.prop('disabled', false ); + submitButton.html(submitButtonText); + } + // Error messages + if (this._debugAjax && jqXHR.responseText) { + this.$element.trigger('submitError.ufForm', [jqXHR, textStatus, errorThrown]); + document.write(jqXHR.responseText); + document.close(); + } else { + if (this.settings.DEBUG) { + console.log('Error (' + jqXHR.status + '): ' + jqXHR.responseText ); + } + // Display errors on failure + // TODO: ufAlerts widget should have a 'destroy' method + if (!this.settings.msgTarget.data('ufAlerts')) { + this.settings.msgTarget.ufAlerts(); + } else { + this.settings.msgTarget.ufAlerts('clear'); + } + + this.settings.msgTarget.ufAlerts('fetch').ufAlerts('render'); + this.settings.msgTarget.on('render.ufAlerts', $.proxy(function () { + this.$element.trigger('submitError.ufForm', [jqXHR, textStatus, errorThrown]); + }, this)); + } + return jqXHR; + }, this) + ); + }; + + /** + * Helper function for encoding data as urlencoded + */ + Plugin.prototype._urlencodeData = function(form) { + // Serialize and post to the backend script in ajax mode + var serializedData; + if (this.settings.binaryCheckboxes) { + serializedData = form.find(':input').not(':checkbox').serialize(); + // Get unchecked checkbox values, set them to 0 + form.find('input[type=checkbox]:enabled').each(function() { + if ($(this).is(':checked')) { + serializedData += '&' + encodeURIComponent(this.name) + '=1'; + } else { + serializedData += '&' + encodeURIComponent(this.name) + '=0'; + } + }); + } + else { + serializedData = form.find(':input').serialize(); + } + + return serializedData; + }; + + /** + * Helper function for encoding data as multipart/form-data + */ + Plugin.prototype._multipartData = function(form) { + // Use FormData to wrap form contents. + // https://developer.mozilla.org/en/docs/Web/API/FormData + var formData = new FormData(form[0]); + // Serialize and post to the backend script in ajax mode + if (this.settings.binaryCheckboxes) { + // Get unchecked checkbox values, set them to 0. + var checkboxes = form.find('input[type=checkbox]:enabled'); + // Feature detection. Several browsers don't support `set` + if (typeof formData.set !== 'function') { + this.settings.msgTarget.ufAlerts('push', 'danger', "Your browser is missing a required feature. This form will still attempt to submit, but if it fails, you'll need to use Chrome for desktop or FireFox for desktop."); + } else { + checkboxes.each(function() { + if ($(this).is(':checked')) { + // this replaces checkbox value with 1 (as we're using binaryCheckboxes). + formData.set(this.name, 1); + // this explicitly adds unchecked boxes. + } else { + formData.set(this.name, 0); + } + }); + } + } + + return formData; + }; + + Plugin.prototype._onKeyUp = function(element, event) { + var validator = this.validator; + // See http://stackoverflow.com/questions/41363409/jquery-validate-add-delay-to-keyup-validation + setTimeout(function() { + // Avoid revalidate the field when pressing one of the following keys + // Shift => 16 + // Ctrl => 17 + // Alt => 18 + // Caps lock => 20 + // End => 35 + // Home => 36 + // Left arrow => 37 + // Up arrow => 38 + // Right arrow => 39 + // Down arrow => 40 + // Insert => 45 + // Num lock => 144 + // AltGr key => 225 + var excludedKeys = [ + 16, 17, 18, 20, 35, 36, 37, + 38, 39, 40, 45, 144, 225 + ]; + + if ( event.which === 9 && validator.elementValue( element ) === '' || $.inArray( event.keyCode, excludedKeys ) !== -1 ) { + return; + } else if ( element.name in validator.submitted || element.name in validator.invalid ) { + validator.element( element ); + } + }, this.settings.keyupDelay); + }; + + // Handles instantiation and access to non-private methods. + $.fn[pluginName] = function(methodOrOptions) { + // Grab plugin instance + var instance = $(this).data(pluginName); + // If undefined or object, initalise plugin. + if (methodOrOptions === undefined || typeof methodOrOptions === 'object') { + // Only initalise if not previously done. + if (!instance) { + $(this).data(pluginName, new Plugin(this, methodOrOptions)); + } + return this; + } + // Otherwise ensure first parameter is a valid string, and is the name of an actual function. + else if (typeof methodOrOptions === 'string' && typeof instance[methodOrOptions] === 'function') { + // Ensure not a private function + if (methodOrOptions.indexOf('_') !== 0) { + return instance[methodOrOptions]( Array.prototype.slice.call(arguments, 1)); + } + else { + console.warn( 'Method ' + methodOrOptions + ' is private!' ); + } + } + else { + console.warn( 'Method ' + methodOrOptions + ' does not exist.' ); + } + }; +})(jQuery, window, document); diff --git a/login/app/sprinkles/core/assets/userfrosting/js/uf-init.js b/login/app/sprinkles/core/assets/userfrosting/js/uf-init.js new file mode 100755 index 0000000..2d447dd --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/js/uf-init.js @@ -0,0 +1,26 @@ +/** + * Contains code that should be initialized in all UF pages. + */ + +$(document).ready(function() { + + // Override Bootstrap's tendency to steal focus from child elements in modals (such as select2). + // See https://github.com/select2/select2/issues/1436#issuecomment-21028474 + $.fn.modal.Constructor.prototype.enforceFocus = function() {}; + + // Link all copy buttons + $.uf.copy('.js-copy-trigger'); + + // Display page alerts + if ($("#alerts-page").length) { + $("#alerts-page").ufAlerts(); + $("#alerts-page").ufAlerts('fetch').ufAlerts('render'); + } + + // Set any JS variables that might be missing from config.js.twig + if (typeof site.uf_table === 'undefined') { + site['uf_table'] = { + use_loading_transition: true + }; + } +}); diff --git a/login/app/sprinkles/core/assets/userfrosting/js/uf-jqueryvalidation-config.js b/login/app/sprinkles/core/assets/userfrosting/js/uf-jqueryvalidation-config.js new file mode 100755 index 0000000..3969ad6 --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/js/uf-jqueryvalidation-config.js @@ -0,0 +1,49 @@ +/** + * Set jQuery.validate settings for bootstrap integration + */ +jQuery.validator.setDefaults({ + highlight: function(element) { + var formGroup = jQuery(element).closest('.form-group'); + formGroup.addClass('has-error has-feedback'); + formGroup.removeClass('has-success'); + formGroup.find('.form-control-feedback').remove(); + formGroup.find('.form-control-icon').show(); + + // Hide any help block text + formGroup.find('.help-block').hide(); + }, + unhighlight: function(element) { + var formGroup = jQuery(element).closest('.form-group'); + + formGroup.removeClass('has-error'); + + // Completely remove the error block, rather than just clearing the text (jqueryvalidation's default action) + formGroup.find('.error-block').remove(); + + // Re-show any help block text + formGroup.find('.help-block').show(); + + // Reshow any non-feedback icons if there is an error + if (formGroup.hasClass('has-error')) { + formGroup.find('.form-control-icon').show(); + } + }, + errorElement: 'p', + errorClass: 'error-block', + errorPlacement: function(error, element) { + if(element.parent('.input-group').length) { + error.insertAfter(element.parent()); + } else { + error.insertAfter(element); + } + }, + success: function(element) { + var formGroup = jQuery(element).closest('.form-group'); + formGroup.addClass('has-success has-feedback'); + formGroup.find('.form-control-feedback').remove(); + // Hide any non-feedback icons + formGroup.find('.form-control-icon').hide(); + // Add a new check mark + jQuery(element).after('<i class="fa fa-check form-control-feedback" aria-hidden="true"></i>'); + } +}); diff --git a/login/app/sprinkles/core/assets/userfrosting/js/uf-modal.js b/login/app/sprinkles/core/assets/userfrosting/js/uf-modal.js new file mode 100755 index 0000000..b84a59a --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/js/uf-modal.js @@ -0,0 +1,192 @@ +/** + * ufModal plugin. Handles modal windows that dynamically their fetch content from a specified URL. + * + * + * UserFrosting https://www.userfrosting.com + * @author Alexander Weissman https://alexanderweissman.com + */ +(function( $ ) +{ + /** + * The plugin namespace, ie for $('.selector').ufModal(options) + * + * Also the id for storing the object state via $('.selector').data() + */ + var PLUGIN_NS = 'ufModal'; + + var Plugin = function ( target, options ) + { + + this.$T = $(target); + + /** #### OPTIONS #### */ + this.options= $.extend( + true, // deep extend + { + sourceUrl : "", + ajaxParams: {}, + msgTarget : null, + DEBUG: false + }, + options + ); + + this.modal = null; + + this._init( target ); + + return this; + }; + + /** #### INITIALISER #### */ + Plugin.prototype._init = function ( target ) + { + var base = this; + var $el = $(target); + + // Delete any existing modals attached to the element (should have been deleted already anyway) + if ($el.find(".modal").length) { + $el.find(".modal").remove(); + } + + // Fetch and render the form + $.ajax({ + type: "GET", + url: base.options.sourceUrl, + data: base.options.ajaxParams, + cache: false + }) + .then( + // Fetch successful + function (data) { + // Append the form as a modal dialog to the body + base.modal = $(data); + $el.append(base.modal); + + base.modal.modal('show'); + + // Bind modal to be deleted when closed + base.modal.on("hidden.bs.modal", function () { + base.destroy(); + }); + + base.$T.trigger('renderSuccess.ufModal'); + return data; + }, + // Fetch failed + function (data) { + // Error messages + if ((typeof site !== "undefined") && site.debug.ajax && data.responseText) { + base.$T.trigger('renderError.ufModal'); + document.write(data.responseText); + document.close(); + } else { + if (base.options.DEBUG) { + console.log("Error (" + data.status + "): " + data.responseText ); + } + // Display errors on failure + // TODO: ufAlerts widget should have a 'destroy' method + if (!base.options.msgTarget.data('ufAlerts')) { + base.options.msgTarget.ufAlerts(); + } else { + base.options.msgTarget.ufAlerts('clear'); + } + + base.options.msgTarget.ufAlerts('fetch').ufAlerts('render'); + base.options.msgTarget.on("render.ufAlerts", function () { + base.$T.trigger('renderError.ufModal'); + }); + } + + base.destroy(); + + return data; + } + ); + }; + + Plugin.prototype.destroy = function () { + var base = this; + var $el = base.$T; + + // Delete the plugin object + base.delete; + + // Remove the modal from the selector + if (base.modal) { + base.modal.remove(); + } + + // Unbind any modal events bound to the selector + $el.off('.ufModal'); + + // Remove plugin name from selector's data-* attribute + $el.removeData(PLUGIN_NS); + }; + + Plugin.prototype.getModal = function () { + return this.modal; + }; + + /** + * EZ Logging/Warning (technically private but saving an '_' is worth it imo) + */ + Plugin.prototype.DLOG = function () + { + if (!this.DEBUG) return; + for (var i in arguments) { + console.log( PLUGIN_NS + ': ', arguments[i] ); + } + } + Plugin.prototype.DWARN = function () + { + this.DEBUG && console.warn( arguments ); + } + + +/*################################################################################### + * JQUERY HOOK + ###################################################################################*/ + + /** + * Generic jQuery plugin instantiation method call logic + * + * Method options are stored via jQuery's data() method in the relevant element(s) + * Notice, myActionMethod mustn't start with an underscore (_) as this is used to + * indicate private methods on the PLUGIN class. + */ + $.fn[ PLUGIN_NS ] = function( methodOrOptions ) + { + if (!$(this).length) { + return $(this); + } + var instance = $(this).data(PLUGIN_NS); + + // CASE: action method (public method on PLUGIN class) + if ( instance + && methodOrOptions.indexOf('_') != 0 + && instance[ methodOrOptions ] + && typeof( instance[ methodOrOptions ] ) == 'function' ) { + + return instance[ methodOrOptions ]( Array.prototype.slice.call( arguments, 1 ) ); + + + // CASE: argument is options object or empty = initialise + } else if ( typeof methodOrOptions === 'object' || ! methodOrOptions ) { + + instance = new Plugin( $(this), methodOrOptions ); // ok to overwrite if this is a re-init + $(this).data( PLUGIN_NS, instance ); + return $(this); + + // CASE: method called before init + } else if ( !instance ) { + console.warn( 'Plugin must be initialised before using method: ' + methodOrOptions ); + + // CASE: invalid method + } else if ( methodOrOptions.indexOf('_') == 0 ) { + console.warn( 'Method ' + methodOrOptions + ' is private!' ); + } else { + console.warn( 'Method ' + methodOrOptions + ' does not exist.' ); + } + }; +})(jQuery);
\ No newline at end of file diff --git a/login/app/sprinkles/core/assets/userfrosting/js/uf-table.js b/login/app/sprinkles/core/assets/userfrosting/js/uf-table.js new file mode 100755 index 0000000..77aa30a --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/js/uf-table.js @@ -0,0 +1,704 @@ +/** + * uf-table plugin. Sets up a Tablesorter table with sorting, pagination, and search, and fetches data from a JSON API. + * + * This plugin depends on query-string.js, which is used to convert a query string into a JSON object. + * + * jQuery plugin template adapted from https://gist.github.com/Air-Craft/1300890 + * + * === USAGE === + * + * Create a container element, and within it place your table, paging controls, and Handlebars templates for rendering the cells. + * + * - Your table should have a unique id, and your paging controls should be wrapped in an element with the `.js-uf-table-pager` class. + * - Create a button with the `.js-uf-table-download` class, and it will be automatically bound to trigger an AJAX request for downloading the table (CSV, etc). + * + <div id="widget-users"> + <table id="table-users" class="tablesorter table table-bordered table-hover table-striped" data-sortlist="[[0, 0]]"> + <thead> + <tr> + <th class="sorter-metatext" data-column-name="name" data-column-template="#user-table-column-info">User <i class="fa fa-sort"></i></th> + <th class="sorter-metanum" data-column-name="last_activity" data-column-template="#user-table-column-last-activity">Last Activity <i class="fa fa-sort"></i></th> + </tr> + </thead> + <tbody> + </tbody> + </table> + + <script id="user-table-column-info" type="text/x-handlebars-template"> + <td data-text="{{row.last_name}}"> + <strong> + <a href="{{site.uri.public}}/users/u/{{row.user_name}}">{{row.first_name}} {{row.last_name}} ({{row.user_name}})</a> + </strong> + <div> + <i class="fa fa-envelope"></i> <a href="mailto:{{row.email}}">{{row.email}}</a> + </div> + </td> + </script> + + <script id="user-table-column-last-activity" type="text/x-handlebars-template"> + {{#if row.last_activity_at }} + <td data-num="{{dateFormat row.last_activity_at format='x'}}"> + {{dateFormat row.last_activity_at format="dddd"}}<br>{{dateFormat row.last_activity_at format="MMM Do, YYYY h:mm a"}} + <br> + <i>{{row.last_activity.description}}</i> + </td> + {{ else }} + <td data-num="0"> + <i>Unknown</i> + </td> + {{/if }} + </script> + + <div class="pager pager-lg tablesorter-pager js-uf-table-pager"> + <span class="pager-control first" title="First page"><i class="fa fa-angle-double-left"></i></span> + <span class="pager-control prev" title="Previous page"><i class="fa fa-angle-left"></i></span> + <span class="pagedisplay"></span> + <span class="pager-control next" title="Next page"><i class="fa fa-angle-right"></i></span> + <span class="pager-control last" title= "Last page"><i class="fa fa-angle-double-right"></i></span> + <br><br> + Jump to Page: <select class="gotoPage"></select> • Show: + <select class="pagesize"> + <option value="5">5</option> + <option value="10">10</option> + </select> + </div> + + <button class="btn btn-sm btn-default js-uf-table-download">Download CSV</button> + </div> + * + * Initialize ufTable on your container object: + * + * $("#widget-users").ufTable(options); + * + * `options` is an object containing any of the following parameters: + * @param {string} dataUrl The absolute URL from which to fetch table data. + * @param {mixed} addParams An object containing any additional key-value pairs that you want appended to the AJAX requests. + * @param {mixed} tablesorter An object containing tablesorter's configuration options (https://mottie.github.io/tablesorter/docs/#Configuration) + * @param {mixed} pager An object containing tablesorter's paging options (https://mottie.github.io/tablesorter/docs/#pager) + * + * == EVENTS == + * + * ufTable triggers the following events: + * + * `pagerComplete.ufTable`: triggered when the tablesorter pager plugin has completed rendering of the table. + * + * == METHODS == + * + * `getTableStateVars( table )`: fetches the current page size, page number, sort order, sort field, and column filters. + * + * UserFrosting https://www.userfrosting.com + * @author Alexander Weissman <https://alexanderweissman.com> + */ +;(function($, window, document, undefined) { + 'use strict'; + + // Define plugin name and defaults. + var pluginName = 'ufTable', + defaults = { + DEBUG : false, + site : site, // global site variables + dataUrl : '', + msgTarget : $('#alerts-page'), + addParams : {}, + filterAllField : '_all', + useLoadingTransition : true, + rowTemplate : null, + columnTemplates : {}, + tablesorter : { + debug: false, + theme : 'bootstrap', + widthFixed: true, + // Set up pagination of data via an AJAX source + // See http://jsfiddle.net/Mottie/uwZc2/ + // Also see https://mottie.github.io/tablesorter/docs/example-pager-ajax.html + widgets: ['saveSort', 'sort2Hash', 'filter', 'pager', 'columnSelector', 'reflow2'], + widgetOptions : { + columnSelector_layout : '<label><input type="checkbox"> <span>{name}</span></label>', + filter_cssFilter: 'form-control', + filter_saveFilters : true, + filter_serversideFiltering : true, + filter_selectSource : { + '.filter-select' : function() { return null; } + }, + + // apply disabled classname to the pager arrows when the rows at either extreme is visible + pager_updateArrows: true, + + // starting page of the pager (zero based index) + pager_startPage: 0, + + // Number of visible rows + pager_size: 10, + + // Save pager page & size if the storage script is loaded (requires $.tablesorter.storage in jquery.tablesorter.widgets.js) + pager_savePages: true, + + // if true, the table will remain the same height no matter how many records are displayed. The space is made up by an empty + // table row set to a height to compensate; default is false + pager_fixedHeight: false, + + // remove rows from the table to speed up the sort of large tables. + // setting this to false, only hides the non-visible rows; needed if you plan to add/remove rows with the pager enabled. + pager_removeRows: false, // removing rows in larger tables speeds up the sort + + // target the pager markup - see the HTML block below + pager_css: { + errorRow : 'uf-table-error-row', // error information row + disabled : 'disabled' // Note there is no period "." in front of this class name + }, + + // Must be initialized with a 'data' key + pager_ajaxObject: { + data: {}, + dataType: 'json' + }, + + // hash prefix + sort2Hash_hash : '#', + // don't '#' or '=' here + sort2Hash_separator : '|', + // this option > table ID > table index on page + sort2Hash_tableId : null, + // if true, show header cell text instead of a zero-based column index + sort2Hash_headerTextAttr : 'data-column-name', + // direction text shown in the URL e.g. [ 'asc', 'desc' ] + sort2Hash_directionText : [ 'asc', 'desc' ], // default values + // if true, override saveSort widget sort, if used & stored sort is available + sort2Hash_overrideSaveSort : true, // default = false + } + } + }; + + // Constructor + function Plugin (element, options) { + this.element = element[0]; + this.$element = $(this.element); + + var lateDefaults = { + download: { + button: this.$element.find('.js-uf-table-download'), + callback: $.proxy(this._onDownload, this) + }, + info: { + container: this.$element.find('.js-uf-table-info'), + callback: $.proxy(this._renderInfoMessages, this) + }, + overlay: { + container: this.$element.find('.js-uf-table-overlay') + }, + tableElement: this.$element.find('.tablesorter'), + tablesorter: { + widgetOptions: { + columnSelector_container : this.$element.find('.js-uf-table-cs-options'), + filter_external : this.$element.find('.js-uf-table-search input'), + + // Pager selectors + pager_selectors: { + container : this.$element.find('.js-uf-table-pager'), + first : '.first', // go to first page arrow + prev : '.prev', // previous page arrow + next : '.next', // next page arrow + last : '.last', // go to last page arrow + gotoPage : '.gotoPage', // go to page selector - select dropdown that sets the current page + pageDisplay : '.pagedisplay', // location of where the "output" is displayed + pageSize : '.pagesize' // page size selector - select dropdown that sets the "size" option + }, + // We need to use $.proxy to properly bind the context for callbacks that will be called by Tablesorter + + // Generate the URL for the AJAX request, with the relevant parameters + pager_customAjaxUrl: $.proxy(this._generateUrl, this), + + // Callback to process the response from the AJAX request + pager_ajaxProcessing: $.proxy(this._processAjax, this), + + // Callback to display errors + pager_ajaxError: $.proxy(this._pagerAjaxError, this), + + sort2Hash_encodeHash: $.proxy(this._encodeHash, this), + + sort2Hash_decodeHash: $.proxy(this._decodeHash, this), + + sort2Hash_cleanHash: $.proxy(this._cleanHash, this) + } + } + }; + this.settings = $.extend(true, {}, defaults, lateDefaults, options); + this._defaults = defaults; + this._name = pluginName; + this._debugAjax = (typeof this.settings.site !== 'undefined') && this.settings.site.debug.ajax; + + // Fall back to attributes from data-*, default values if not specified in options + var pagerContainer = this.settings.tablesorter.widgetOptions.pager_selectors.container; + var infoContainer = this.settings.info.container; + var dataAttributeDefaults = { + info: { + messageEmptyRows: infoContainer.data('message-empty-rows') ? + infoContainer.data('message-empty-rows') : + "Sorry, we've got nothing here." + }, + tablesorter: { + widgetOptions: { + // possible variables: {size}, {page}, {totalPages}, {filteredPages}, {startRow}, {endRow}, {filteredRows} and {totalRows} + // also {page:input} & {startRow:input} will add a modifiable input in place of the value + pager_output: pagerContainer.data('output-template') ? + pagerContainer.data('output-template') : + '{startRow} to {endRow} of {filteredRows} ({totalRows})' // default if not set on data-* attribute + } + } + }; + + this.settings = $.extend(true, {}, dataAttributeDefaults, this.settings); + + // Check that tableElement exists + var tableElement = this.settings.tableElement; + if (!tableElement.length) { + if (window.console && console.error) { + console.error('ufTable could not be initialized: wrapper element does not exist, or does not contain a matched tableElement (see https://learn.userfrosting.com/client-side-code/components/tables )'); + } + return; + } + + // Copy over dataUrl to pager_ajaxUrl + this.settings.tablesorter.widgetOptions.pager_ajaxUrl = this.settings.dataUrl; + + // Set up 'loading' overlays + if (this.settings.useLoadingTransition) { + var overlay = this.settings.overlay.container; + tableElement.bind('sortStart filterStart pageMoved', function() { + overlay.removeClass('hidden'); + }).bind('pagerComplete updateComplete', function() { + overlay.addClass('hidden'); + }); + } + + // Set up tablesorter and pager + this.ts = tableElement.tablesorter(this.settings.tablesorter); + + // Map default column template selectors based on data-column-template attribute in each column header + var columns = this.ts[0].config.$headerIndexed; + var columnTemplates = {}; + for (var col = 0; col < columns.length; col++) { + var columnName = columns[col].data('column-name'); + if (!columnName && this.settings.DEBUG) { + console.error('Column number ' + col + ' is missing a data-column-name attribute.'); + } + columnTemplates[columnName] = columns[col].data('column-template'); + } + + // Merge in any column template selectors that were set in the ctor options + columnTemplates = $.extend(true, columnTemplates, this.settings.columnTemplates); + + // Locate and compile templates for any string-identified column renderers + // At the same time, build out a numerically indexed array of templates + this.columnTemplatesIndexed = []; + for (var col = 0; col < columns.length; col++) { + var columnName = columns[col].data('column-name'); + if (!columnTemplates[columnName] && this.settings.DEBUG) { + console.error("No template found for column '" + columnName + "'."); + } + var columnTemplate = columnTemplates[columnName]; + if (typeof columnTemplate === 'string') { + this.columnTemplatesIndexed.push(Handlebars.compile($(columnTemplate).html())); + } else { + this.columnTemplatesIndexed.push(columnTemplate); + } + } + + // Locate and compile row template + this.rowTemplate = Handlebars.compile('<tr>'); + // If rowTemplateSelector is set, then find the DOM element that it references, which contains the template + if (this.settings.rowTemplate) { + var rowTemplate = this.settings.rowTemplate; + if (typeof rowTemplate === 'string') { + this.rowTemplate = Handlebars.compile($(this.settings.rowTemplate).html()); + } else { + this.rowTemplate = rowTemplate; + } + } + + // Link CSV download button + this.settings.download.button.on('click', this.settings.download.callback); + + // Allow clicking on the labels in the table menu without closing the menu + $(this.settings.tablesorter.widgetOptions.columnSelector_container).find('label').on('click', function(e) { + e.stopPropagation(); + }); + + // Propagate our own pagerComplete event + this.ts.on('pagerComplete', $.proxy(function () { + this.$element.trigger('pagerComplete.ufTable'); + }, this)); + + // Show info messages when there are no rows/no results + this.ts.on('filterEnd filterReset pagerComplete', this.settings.info.callback); + + // Detect changes to element attributes + this.$element.attrchange({ + callback: function (event) { + this.element = event.target; + }.bind(this) + }); + + return this; + } + + /** + * Get state variables for this table, as required by the AJAX data source: sorts, filters, size, page + */ + Plugin.prototype.getTableStateVars = function(table) { + var base = this; + + // Get sort column and order + var sortOrders = { + '0': 'asc', + '1': 'desc' + }; + + // Set sorts in URL. Assumes each th has a data-column-name attribute that corresponds to the name in the API + var sortList = table.config.sortList; + var sorts = {}; + for (var i = 0; i < sortList.length; i++) { + var columnIndex = sortList[i][0]; + var columnDirection = sortOrders[sortList[i][1]]; // Converts to 'asc' or 'desc' + if (sortList[i]) { + var columnName = table.config.$headerIndexed[columnIndex].data('column-name'); + sorts[columnName] = columnDirection; + } + } + + // Set filters in URL. Assumes each th has a data-column-name attribute that corresponds to the name in the API + var filterList = base.getSavedFilters(table); + var filters = {}; + for (i = 0; i < filterList.length; i++) { + if (filterList[i]) { + var columnName = base.settings.filterAllField; + + if (table.config.$headerIndexed[i]) { + columnName = table.config.$headerIndexed[i].data('column-name'); + } + + filters[columnName] = filterList[i]; + } + } + + var state = { + size: table.config.pager.size, + page: table.config.pager.page, + sorts: sorts, + filters: filters + }; + + return state; + }; + + /** + * Get saved filters from the browser local storage. Those should always be up to date + */ + Plugin.prototype.getSavedFilters = function(table) { + + // Fallback to `getFilters` or empty in case of failure + var filterList = $.tablesorter.getFilters(table) || []; + + // Overwrite list with saved filter for filter-select not setup by ts + var isArray, saved, + wo = table.config.widgetOptions; + if ( wo.filter_saveFilters && $.tablesorter.storage ) { + saved = $.tablesorter.storage( table, 'tablesorter-filters' ) || []; + isArray = $.isArray( saved ); + // make sure we're not just getting an empty array + if ( !( isArray && saved.join( '' ) === '' || !isArray ) ) { + filterList = $.tablesorter.filter.processFilters( saved ); + } + } + + return filterList; + }; + + /** + * Generate the AJAX url. + * Used as the default callback for pager_customAjaxUrl + * @private + */ + Plugin.prototype._generateUrl = function(table, url) { + var tableState = this.getTableStateVars(table); + + if (this.settings.DEBUG) { + console.log(tableState); + } + + $.extend(table.config.pager.ajaxObject.data, tableState); + + // Merge in any additional parameters + $.extend(table.config.pager.ajaxObject.data, this.settings.addParams); + + return url; + }; + /** + * Process data returned from the AJAX request and rendering the table cells. + * Used as the default callback for pager_ajaxProcessing + * @private + */ + Plugin.prototype._processAjax = function(data) { + var ts = this.ts[0]; + var json = {}, + rows = ''; + + if (data) { + var size = data.rows.length; + + // Render table rows and cells via Handlebars + for (var row = 0; row < size; row++) { + var cellData = { + rownum: row, + row : data.rows[row], // It is safe to use the data from the API because Handlebars escapes HTML + site : this.settings.site + }; + + rows += this.rowTemplate(cellData); + + for (var col = 0; col < this.columnTemplatesIndexed.length; col++) { + rows += this.columnTemplatesIndexed[col](cellData); + } + + rows += '</tr>'; + } + + // Initialize any dropdown filters + var columns = ts.config.$headerIndexed; + this._ajaxInitFilterSelects(columns, data.listable); + + json.total = data.count; // Get total rows without pagination + json.filteredRows = data.count_filtered; // no filtering + json.rows = $(rows); + json.output = data.output; + } else { + json.total = 0; + json.filteredRows = 0; + json.rows = ''; + } + + return json; + }; + + /** + * Initialize filter select menus using the ajax `listable` values + * @private + */ + Plugin.prototype._ajaxInitFilterSelects = function(columns, listable) { + var ts = this.ts[0]; + var filters = this.getSavedFilters(ts); + // Find columns with `.filter-select` and match them to column numbers based on their data-column-name + for (var col = 0; col < columns.length; col++) { + var column = columns[col]; + // If the column is designated for filter-select, get the listables from the data and recreate it + if (column.hasClass('filter-select')) { + var columnName = column.data('column-name'); + if (listable[columnName]) { + $.tablesorter.filter.buildSelect(ts, col, listable[columnName], true); + // If there is a filter actually set for this column, update the selected option. + if (filters[col]) { + var selectControl = $(ts).find(".tablesorter-filter[data-column='" + col + "']"); + selectControl.val(filters[col]); + } + } + } + } + }; + + /** + * Implements handler for the "download CSV" button. + * Default callback for download.callback + * @private + */ + Plugin.prototype._onDownload = function () { + var tableState = this.getTableStateVars(this.ts[0]); + tableState.format = 'csv'; + delete tableState.page; + delete tableState.size; + + // Merge in any additional request parameters + $.extend(tableState, this.settings.addParams); + + // Causes download to begin + window.location = this.settings.dataUrl + '?' + $.param(tableState); + }; + + /** + * Handle pager ajax errors. + * @private + */ + Plugin.prototype._pagerAjaxError = function(c, jqXHR, settings, exception) { + this._ajaxError(jqXHR); + + // Let TS handle the in-table error message + return ''; + }; + + /** + * Handle ajax error + * @private + */ + Plugin.prototype._ajaxError = function(jqXHR) { + if (typeof jqXHR === 'object') { + // Error messages + if (this._debugAjax && jqXHR.responseText) { + document.write(jqXHR.responseText); + document.close(); + } else { + if (this.settings.DEBUG) { + console.log('Error (' + jqXHR.status + '): ' + jqXHR.responseText ); + } + // Display errors on failure + // TODO: ufAlerts widget should have a 'destroy' method + if (!this.settings.msgTarget.data('ufAlerts')) { + this.settings.msgTarget.ufAlerts(); + } else { + this.settings.msgTarget.ufAlerts('clear'); + } + + this.settings.msgTarget.ufAlerts('fetch').ufAlerts('render'); + } + } + }; + + /** + * Render info messages, such as when there are no results. + * Default callback for info.callback + * @private + */ + Plugin.prototype._renderInfoMessages = function () { + var table = this.ts[0]; + var infoMessages = this.settings.info.container; + if (table.config.pager) { + infoMessages.html(''); + var fr = table.config.pager.filteredRows; + if (fr === 0) { + infoMessages.html(this.settings.info.messageEmptyRows); + } + } + }; + + /** + * Encode the current table state variables into a URL hash. + * Default callback for sort2Hash_encodeHash + * @private + */ + Plugin.prototype._encodeHash = function(config, tableId, component, value, rawValue) { + var wo = config.widgetOptions; + if ( component === 'filter' ) { + // rawValue is an array of filter values, numerically indexed + var encodedFilters = ''; + var len = rawValue.length; + for (var index = 0; index < len; index++) { + if (rawValue[index]) { + var columnName = this.settings.filterAllField; + if (config.$headerIndexed[index]) { + columnName = $(config.$headerIndexed[index][0]).attr(wo.sort2Hash_headerTextAttr); + } + encodedFilters += '&filter[' + tableId + '][' + columnName + ']=' + encodeURIComponent(rawValue[index]); + } + } + return encodedFilters; + } else if ( component === 'sort' ) { + // rawValue is an array of sort pairs [columnNum, sortDirection] + var encodedFilters = ''; + var len = rawValue.length; + for (var index = 0; index < len; index++) { + var columnNum = rawValue[index][0]; + var sortDirection = rawValue[index][1]; + var columnName = $(config.$headerIndexed[columnNum][0]).attr(wo.sort2Hash_headerTextAttr); + encodedFilters += '&sort[' + tableId + '][' + columnName + ']=' + wo.sort2Hash_directionText[sortDirection]; + } + return encodedFilters; + } + return false; + }; + + /** + * Decode the current table state variables from the URL hash. + * Default callback for sort2Hash_decodeHash + * @private + */ + Plugin.prototype._decodeHash = function(config, tableId, component) { + var wo = config.widgetOptions; + var result; + // Convert hash into JSON object + var urlObject = $.String.deparam(window.location.hash); + delete urlObject[wo.sort2Hash_hash]; // Remove hash character + if (component === 'filter') { + var decodedFilters = []; + // Extract filter names and values for the specified table + var pageFilters = urlObject.filter ? urlObject.filter : []; + if (pageFilters[tableId]) { + var tableFilters = pageFilters[tableId]; + // Build a numerically indexed array of filter values + var len = config.$headerIndexed.length; + for (var index = 0; index < len; index++) { + var columnName = $(config.$headerIndexed[index][0]).attr(wo.sort2Hash_headerTextAttr); + if (tableFilters[columnName] && tableFilters[columnName] != this.settings.filterAllField) { + decodedFilters.push(tableFilters[columnName]); + } else { + decodedFilters.push(''); + } + } + // Convert array of filter values to a delimited string + result = decodedFilters.join(wo.sort2Hash_separator); + // make sure to use decodeURIComponent on the result + return decodeURIComponent(result); + } else { + return ''; + } + } + return false; + }; + + /** + * Clean up URL hash. + * Default callback for sort2Hash_cleanHash + * @private + */ + Plugin.prototype._cleanHash = function(config, tableId, component, hash) { + var wo = config.widgetOptions; + // Convert hash to JSON object + var urlObject = $.String.deparam(hash); + delete urlObject[wo.sort2Hash_hash]; // Remove hash character + // Remove specified component for specified table + if (urlObject[component]) { + if (urlObject[component][tableId]) { + delete urlObject[component][tableId]; + } + // Delete entire component if no other tables remaining + if (jQuery.isEmptyObject(urlObject[component])) { + delete urlObject[component]; + } + } + // Convert modified JSON object back into serialized representation + var result = decodeURIComponent(jQuery.param(urlObject)); + return result.length ? result : ''; + }; + + // Handles instantiation and access to non-private methods. + $.fn[pluginName] = function(methodOrOptions) { + // Grab plugin instance + var instance = $(this).data(pluginName); + // If undefined or object, initalise plugin. + if (methodOrOptions === undefined || typeof methodOrOptions === 'object') { + // Only initalise if not previously done. + if (!instance) { + $(this).data(pluginName, new Plugin(this, methodOrOptions)); + } + return this; + // Otherwise ensure first parameter is a valid string, and is the name of an actual function. + } else if (typeof methodOrOptions === 'string' && typeof instance[methodOrOptions] === 'function') { + // Ensure not a private function + if (methodOrOptions.indexOf('_') !== 0) { + return instance[methodOrOptions]( Array.prototype.slice.call(arguments, 1)); + } + else { + console.warn( 'Method ' + methodOrOptions + ' is private!' ); + } + } else { + console.warn( 'Method ' + methodOrOptions + ' does not exist.' ); + } + }; +})(jQuery, window, document); diff --git a/login/app/sprinkles/core/assets/userfrosting/js/uf-tablesorter-parsers.js b/login/app/sprinkles/core/assets/userfrosting/js/uf-tablesorter-parsers.js new file mode 100755 index 0000000..444b4ac --- /dev/null +++ b/login/app/sprinkles/core/assets/userfrosting/js/uf-tablesorter-parsers.js @@ -0,0 +1,53 @@ +// Parser for sorting integers, timestamps, etc based on metadata from <td> attributes
+// Adapted from http://mottie.github.io/tablesorter/docs/example-parsers-advanced.html
+$.tablesorter.addParser({
+ // set a unique id
+ id: 'metanum',
+ is: function(s) {
+ // return false so this parser is not auto detected
+ return false;
+ },
+ format: function(s, table, cell, cellIndex) {
+ var $cell = $(cell);
+ // returns metadata, or cell text (s) if it doesn't exist
+ return $cell.attr('data-num') || s;
+
+ },
+ // set type to numeric
+ type: 'numeric'
+});
+
+$.tablesorter.addParser({
+ // set a unique id
+ id: 'metatext',
+ is: function(s) {
+ // return false so this parser is not auto detected
+ return false;
+ },
+ format: function(s, table, cell, cellIndex) {
+ var $cell = $(cell);
+ // returns metadata, or cell text (s) if it doesn't exist
+ return $cell.attr('data-text') || s;
+
+ },
+
+ type: 'text'
+});
+
+$.tablesorter.addParser({
+ // set a unique id
+ id: 'isblank',
+ is: function(s) {
+ // return false so this parser is not auto detected
+ return false;
+ },
+ format: function(s, table, cell, cellIndex) {
+ var $cell = $(cell);
+ // returns 1 if blank (whitespace), 0 otherwise
+ var isBlank = $cell.html().trim() == " " ? 1 : 0;
+ return isBlank;
+
+ },
+
+ type: 'numeric'
+});
diff --git a/login/app/sprinkles/core/bower.json b/login/app/sprinkles/core/bower.json new file mode 100755 index 0000000..fb38c72 --- /dev/null +++ b/login/app/sprinkles/core/bower.json @@ -0,0 +1,47 @@ +{ + "name": "userfrosting-sprinkle-core", + "description": "Core module for UserFrosting.", + "homepage": "https://github.com/userfrosting", + "license": "MIT", + "authors": [ + { + "name": "Alexander Weissman", + "homepage": "https://alexanderweissman.com" + }, + "ssnukala" + ], + "dependencies": { + "jquery": ">= 2.2.4", + "bootstrap": "^3.3.6", + "font-awesome": "^4.7", + "handlebars": "^3.0", + "clipboard": "^1.5", + "ionicons": "^2.0", + "icheck": "^1.0", + "speakingurl": "^11.0", + "urijs": "^1.18", + "fastclick": "^1.0.6", + "moment": "^2.17", + "jquery-slimscroll": "^1.3", + "jquery-validation": "~1.14.0", + "select2": "~4.0.5", + "tablesorter": "jquery.tablesorter#2.28" + }, + "resolutions": { + "jquery": ">= 2.2.4" + }, + "moduleType": [ + "node" + ], + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "assets/vendor", + "examples", + "demo-resources", + "demo", + "test", + "tests" + ] +} diff --git a/login/app/sprinkles/core/composer.json b/login/app/sprinkles/core/composer.json new file mode 100755 index 0000000..f94c486 --- /dev/null +++ b/login/app/sprinkles/core/composer.json @@ -0,0 +1,44 @@ +{ + "name": "userfrosting/sprinkle-core", + "type": "userfrosting-sprinkle", + "description": "Core module for UserFrosting.", + "keywords": ["php user management", "usercake", "bootstrap"], + "homepage": "https://github.com/userfrosting/UserFrosting", + "license" : "MIT", + "authors" : [ + { + "name": "Alexander Weissman", + "homepage": "https://alexanderweissman.com" + } + ], + "require": { + "doctrine/dbal": "^2.5", + "filp/whoops": "^2.1", + "illuminate/cache": "5.4.*", + "illuminate/database": "5.4.*", + "illuminate/events": "5.4.*", + "illuminate/filesystem": "5.4.*", + "league/csv": "^8.1", + "monolog/monolog": "^1", + "phpmailer/phpmailer": "5.2.10", + "rockettheme/toolbox": "1.3.1", + "slim/csrf": "^0.8", + "slim/slim": "^3", + "slim/twig-view": "^1.2", + "symfony/http-foundation": "*", + "twig/twig": "^1.18", + "userfrosting/assets": "~4.1.0", + "userfrosting/config": "~4.1.0", + "userfrosting/cache": "~4.1.0", + "userfrosting/fortress": "~4.1.1", + "userfrosting/i18n": "~4.1.0", + "userfrosting/session": "~4.1.0", + "userfrosting/support": "~4.1.1", + "vlucas/phpdotenv": "^2" + }, + "autoload": { + "psr-4": { + "UserFrosting\\Sprinkle\\Core\\": "src/" + } + } +} diff --git a/login/app/sprinkles/core/config/default.php b/login/app/sprinkles/core/config/default.php new file mode 100755 index 0000000..b6862e6 --- /dev/null +++ b/login/app/sprinkles/core/config/default.php @@ -0,0 +1,182 @@ +<?php + + /** + * Core configuration file for UserFrosting. You must override/extend this in your site's configuration file. + * + * Sensitive credentials should be stored in an environment variable or your .env file. + * Database password: DB_PASSWORD + * SMTP server password: SMTP_PASSWORD + */ + + return [ + 'address_book' => [ + 'admin' => [ + 'email' => getenv('SMTP_USER') ?: null, + 'name' => 'Site Administrator' + ] + ], + 'alert' => [ + 'storage' => 'session', // Set to one of `cache` or `session` + 'key' => 'site.alerts', // the key to use to store flash messages + ], + 'assets' => [ + 'compiled' => [ + 'path' => 'assets', + 'schema' => 'bundle.result.json' + ], + 'raw' => [ + 'path' => 'assets-raw', + 'schema' => 'asset-bundles.json' + ], + 'use_raw' => true + ], + 'cache' => [ + 'driver' => 'file', // Set to one of `file`, `memcached`, `redis` + 'prefix' => 'userfrosting', // Edit prefix to something unique when multiple instance of memcached/redis are used on the same server + 'memcached' => [ + 'host' => '127.0.0.1', + 'port' => 11211, + 'weight' => 100 + ], + 'redis' => [ + 'host' => '127.0.0.1', + 'password' => null, + 'port' => 6379, + 'database' => 0 + ], + 'twig' => false + ], + // CSRF middleware settings (see https://github.com/slimphp/Slim-Csrf) + 'csrf' => [ + 'name' => 'csrf', + 'storage_limit' => 200, + 'strength' => 16, + 'persistent_token' => true, + // A list of url paths to ignore CSRF checks on + 'blacklist' => [ + // URL paths will be matched against each regular expression in this list. + // Each regular expression should map to an array of methods. + // Regular expressions will be delimited with ~ in preg_match, so if you + // have routes with ~ in them, you must escape this character in your regex. + // Also, remember to use ^ when you only want to match the beginning of a URL path! + ] + ], + 'db' => [ + 'default' => [ + 'driver' => getenv('DB_DRIVER') ?: 'mysql', + 'host' => getenv('DB_HOST') ?: null, + 'port' => getenv('DB_PORT') ?: null, + 'database' => getenv('DB_NAME') ?: null, + 'username' => getenv('DB_USER') ?: null, + 'password' => getenv('DB_PASSWORD') ?: null, + 'charset' => 'utf8', + 'collation' => 'utf8_unicode_ci', + 'prefix' => '' + ] + ], + 'debug' => [ + 'queries' => false, + 'smtp' => false, + 'twig' => false + ], + 'mail' => [ + 'mailer' => 'smtp', // Set to one of 'smtp', 'mail', 'qmail', 'sendmail' + 'host' => getenv('SMTP_HOST') ?: null, + 'port' => 587, + 'auth' => true, + 'secure' => 'tls', + 'username' => getenv('SMTP_USER') ?: null, + 'password' => getenv('SMTP_PASSWORD') ?: null, + 'smtp_debug' => 4, + 'message_options' => [ + 'CharSet' => 'UTF-8', + 'isHtml' => true, + 'Timeout' => 15 + ] + ], + // Filesystem paths + 'path' => [ + 'document_root' => str_replace(DIRECTORY_SEPARATOR, \UserFrosting\DS, $_SERVER['DOCUMENT_ROOT']), + 'public_relative' => dirname($_SERVER['SCRIPT_NAME']) // The location of `index.php` relative to the document root. Use for sites installed in subdirectories of your web server's document root. + ], + 'session' => [ + 'handler' => 'file', + // Config values for when using db-based sessions + 'database' => [ + 'table' => 'sessions' + ], + 'name' => 'uf4', + 'minutes' => 120, + 'cache_limiter' => false, + // Decouples the session keys used to store certain session info + 'keys' => [ + 'csrf' => 'site.csrf', // the key (prefix) used to store an ArrayObject of CSRF tokens. + ] + ], + // Slim settings - see http://www.slimframework.com/docs/objects/application.html#slim-default-settings + 'settings' => [ + 'displayErrorDetails' => true + ], + // "Site" settings that are automatically passed to Twig + 'site' => [ + 'AdminLTE' => [ + 'skin' => 'blue' + ], + 'analytics' => [ + 'google' => [ + 'code' => '', + 'enabled' => false + ] + ], + 'author' => 'Author', + 'csrf' => null, // Do not set this variable. The core Twig extension will override it with values from the CSRF service. + 'debug' => [ + 'ajax' => false, + 'info' => true + ], + 'locales' => [ + // Should be ordered according to https://en.wikipedia.org/wiki/List_of_languages_by_total_number_of_speakers, + // with the exception of English, which as the default language comes first. + 'available' => [ + 'en_US' => 'English', + 'zh_CN' => '中文', + 'es_ES' => 'Español', + 'ar' => 'العربية', + 'pt_PT' => 'Português', + 'ru_RU' => 'русский', + 'de_DE' => 'Deutsch', + 'fr_FR' => 'Français', + 'tr' => 'Türk', + 'it_IT' => 'Italiano', + 'th_TH' => 'ภาษาไทย', + 'fa' => 'فارسی' + ], + // This can be a comma-separated list, to load multiple fallback locales + 'default' => 'en_US' + ], + 'title' => 'UserFrosting', + // Global ufTable settings + 'uf_table' => [ + 'use_loading_transition' => true + ], + // URLs + 'uri' => [ + 'base' => [ + 'host' => isset($_SERVER['SERVER_NAME']) ? trim($_SERVER['SERVER_NAME'], '/') : 'localhost', + 'scheme' => empty($_SERVER['HTTPS']) || $_SERVER['HTTPS'] === 'off' ? 'http' : 'https', + 'port' => isset($_SERVER['SERVER_PORT']) ? (int) $_SERVER['SERVER_PORT'] : null, + 'path' => isset($_SERVER['SCRIPT_NAME']) ? trim(dirname($_SERVER['SCRIPT_NAME']), '/\\') : '' + ], + 'author' => 'https://www.userfrosting.com', + 'publisher' => '' + ] + ], + 'php' => [ + 'timezone' => 'America/New_York', + 'error_reporting' => E_ALL, // Development - report all errors and suggestions + 'display_errors' => 'true', + 'log_errors' => 'false', + // Let PHP itself render errors natively. Useful if a fatal error is raised in our custom shutdown handler. + 'display_errors_native' => 'false' + ] + ]; diff --git a/login/app/sprinkles/core/config/dev.php b/login/app/sprinkles/core/config/dev.php new file mode 100755 index 0000000..daab24c --- /dev/null +++ b/login/app/sprinkles/core/config/dev.php @@ -0,0 +1,30 @@ +<?php + + /** + * Default development config file for UserFrosting. Sets up UserFrosting for easier development. + * + */ + + return [ + 'assets' => [ + 'use_raw' => true + ], + 'cache' => [ + 'twig' => false + ], + 'debug' => [ + 'twig' => true, + 'auth' => true, + 'smtp' => true + ], + // Slim settings - see http://www.slimframework.com/docs/objects/application.html#slim-default-settings + 'settings' => [ + 'displayErrorDetails' => true + ], + 'site' => [ + 'debug' => [ + 'ajax' => true, + 'info' => true + ] + ] + ];
\ No newline at end of file diff --git a/login/app/sprinkles/core/config/production.php b/login/app/sprinkles/core/config/production.php new file mode 100755 index 0000000..d0154d4 --- /dev/null +++ b/login/app/sprinkles/core/config/production.php @@ -0,0 +1,40 @@ +<?php + + /** + * Default production config file for UserFrosting. You may override/extend this in your site's configuration file to customize deploy settings. + * + */ + + return [ + 'assets' => [ + 'use_raw' => false + ], + 'cache' => [ + 'twig' => true + ], + 'debug' => [ + 'twig' => false, + 'auth' => false, + 'smtp' => false + ], + // Slim settings - see http://www.slimframework.com/docs/objects/application.html#slim-default-settings + 'settings' => [ + 'routerCacheFile' => \UserFrosting\ROOT_DIR . '/' . \UserFrosting\APP_DIR_NAME . '/' . \UserFrosting\CACHE_DIR_NAME . '/' . 'routes.cache', + 'displayErrorDetails' => false + ], + 'site' => [ + 'analytics' => [ + 'google' => [ + 'enabled' => true + ] + ], + 'debug' => [ + 'ajax' => false, + 'info' => false + ] + ], + 'php' => [ + 'display_errors' => 'false', + 'log_errors' => 'true' + ] + ]; diff --git a/login/app/sprinkles/core/config/testing.php b/login/app/sprinkles/core/config/testing.php new file mode 100755 index 0000000..7da03c2 --- /dev/null +++ b/login/app/sprinkles/core/config/testing.php @@ -0,0 +1,23 @@ +<?php + + /** + * Default testing config file for UserFrosting. You may override/extend this in your site's configuration file to customize deploy settings. + * + */ + + return [ + 'cache' => [ + 'illuminate' => [ + 'default' => 'array', + ] + ], + 'db' => [ + 'test_integration' => [ + 'driver' => 'sqlite', + 'database' => ':memory:', + ] + ], + 'settings' => [ + 'displayErrorDetails' => false + ] + ]; diff --git a/login/app/sprinkles/core/extra/adjectives.php b/login/app/sprinkles/core/extra/adjectives.php new file mode 100755 index 0000000..fbdbeb0 --- /dev/null +++ b/login/app/sprinkles/core/extra/adjectives.php @@ -0,0 +1,1221 @@ +<?php + +return [ + 'able', + 'above', + 'absent', + 'absolute', + 'abstract', + 'abundant', + 'academic', + 'acceptable', + 'accepted', + 'accessible', + 'accurate', + 'accused', + 'active', + 'actual', + 'acute', + 'added', + 'additional', + 'adequate', + 'adjacent', + 'administrative', + 'adorable', + 'advanced', + 'adverse', + 'advisory', + 'aesthetic', + 'afraid', + 'aggregate', + 'aggressive', + 'agreeable', + 'agreed', + 'agricultural', + 'alert', + 'alive', + 'alleged', + 'allied', + 'alone', + 'alright', + 'alternative', + 'amateur', + 'amazing', + 'ambitious', + 'american', + 'amused', + 'ancient', + 'angry', + 'annoyed', + 'annual', + 'anonymous', + 'anxious', + 'appalling', + 'apparent', + 'applicable', + 'appropriate', + 'arab', + 'arbitrary', + 'architectural', + 'armed', + 'arrogant', + 'artificial', + 'artistic', + 'ashamed', + 'asian', + 'asleep', + 'assistant', + 'associated', + 'atomic', + 'attractive', + 'australian', + 'automatic', + 'autonomous', + 'available', + 'average', + 'awake', + 'aware', + 'awful', + 'awkward', + 'back', + 'bad', + 'balanced', + 'bare', + 'basic', + 'beautiful', + 'beneficial', + 'better', + 'bewildered', + 'big', + 'binding', + 'biological', + 'bitter', + 'bizarre', + 'black', + 'blank', + 'blind', + 'blonde', + 'bloody', + 'blue', + 'blushing', + 'bodacious', + 'boiling', + 'bold', + 'bored', + 'boring', + 'bottom', + 'brainy', + 'brave', + 'breakable', + 'breezy', + 'brief', + 'bright', + 'brilliant', + 'british', + 'broad', + 'broken', + 'brown', + 'bumpy', + 'burning', + 'busy', + 'calm', + 'canadian', + 'capable', + 'capitalist', + 'careful', + 'casual', + 'catholic', + 'causal', + 'cautious', + 'central', + 'certain', + 'changing', + 'characteristic', + 'charming', + 'cheap', + 'cheerful', + 'chemical', + 'chief', + 'chilly', + 'chinese', + 'chosen', + 'christian', + 'chronic', + 'chubby', + 'circular', + 'civic', + 'civil', + 'civilian', + 'classic', + 'classical', + 'clean', + 'clear', + 'clever', + 'clinical', + 'close', + 'closed', + 'cloudy', + 'clumsy', + 'coastal', + 'cognitive', + 'coherent', + 'cold', + 'collective', + 'colonial', + 'colorful', + 'colossal', + 'coloured', + 'colourful', + 'combative', + 'combined', + 'comfortable', + 'coming', + 'commercial', + 'common', + 'communist', + 'compact', + 'comparable', + 'comparative', + 'compatible', + 'competent', + 'competitive', + 'complete', + 'complex', + 'complicated', + 'comprehensive', + 'compulsory', + 'conceptual', + 'concerned', + 'concrete', + 'condemned', + 'confident', + 'confidential', + 'confused', + 'conscious', + 'conservation', + 'conservative', + 'considerable', + 'consistent', + 'constant', + 'constitutional', + 'contemporary', + 'content', + 'continental', + 'continued', + 'continuing', + 'continuous', + 'controlled', + 'controversial', + 'convenient', + 'conventional', + 'convinced', + 'convincing', + 'cooing', + 'cool', + 'cooperative', + 'corporate', + 'correct', + 'corresponding', + 'costly', + 'courageous', + 'crazy', + 'creative', + 'creepy', + 'criminal', + 'critical', + 'crooked', + 'crowded', + 'crucial', + 'crude', + 'cruel', + 'cuddly', + 'cultural', + 'curious', + 'curly', + 'current', + 'curved', + 'cute', + 'daily', + 'damaged', + 'damp', + 'dangerous', + 'dark', + 'dead', + 'deaf', + 'deafening', + 'dear', + 'decent', + 'decisive', + 'deep', + 'defeated', + 'defensive', + 'defiant', + 'definite', + 'deliberate', + 'delicate', + 'delicious', + 'delighted', + 'delightful', + 'democratic', + 'dependent', + 'depressed', + 'desirable', + 'desperate', + 'detailed', + 'determined', + 'developed', + 'developing', + 'devoted', + 'different', + 'difficult', + 'digital', + 'diplomatic', + 'direct', + 'dirty', + 'disabled', + 'disappointed', + 'disastrous', + 'disciplinary', + 'disgusted', + 'distant', + 'distinct', + 'distinctive', + 'distinguished', + 'disturbed', + 'disturbing', + 'diverse', + 'divine', + 'dizzy', + 'domestic', + 'dominant', + 'double', + 'doubtful', + 'drab', + 'dramatic', + 'dreadful', + 'driving', + 'drunk', + 'dry', + 'dual', + 'due', + 'dull', + 'dumbfounded', + 'dusty', + 'dutch', + 'dying', + 'dynamic', + 'eager', + 'early', + 'eastern', + 'easy', + 'economic', + 'educational', + 'eerie', + 'effective', + 'efficient', + 'elaborate', + 'elated', + 'elderly', + 'eldest', + 'electoral', + 'electric', + 'electrical', + 'electronic', + 'elegant', + 'eligible', + 'embarrassed', + 'embarrassing', + 'emotional', + 'empirical', + 'empty', + 'enchanting', + 'encouraging', + 'endless', + 'energetic', + 'english', + 'enormous', + 'enthusiastic', + 'entire', + 'entitled', + 'envious', + 'environmental', + 'equal', + 'equivalent', + 'essential', + 'established', + 'estimated', + 'ethical', + 'ethnic', + 'european', + 'eventual', + 'everyday', + 'evident', + 'evil', + 'evolutionary', + 'exact', + 'excellent', + 'exceptional', + 'excess', + 'excessive', + 'excited', + 'exciting', + 'exclusive', + 'existing', + 'exotic', + 'expected', + 'expensive', + 'experienced', + 'experimental', + 'explicit', + 'extended', + 'extensive', + 'external', + 'extra', + 'extraordinary', + 'extreme', + 'exuberant', + 'faint', + 'fair', + 'faithful', + 'familiar', + 'famous', + 'fancy', + 'fantastic', + 'far', + 'fascinating', + 'fashionable', + 'fast', + 'fat', + 'fatal', + 'favourable', + 'favourite', + 'federal', + 'fellow', + 'female', + 'feminist', + 'few', + 'fierce', + 'filthy', + 'final', + 'financial', + 'fine', + 'firm', + 'fiscal', + 'fit', + 'fixed', + 'flaky', + 'flat', + 'flexible', + 'fluffy', + 'fluttering', + 'flying', + 'following', + 'fond', + 'foolish', + 'foreign', + 'formal', + 'formidable', + 'forthcoming', + 'fortunate', + 'forward', + 'fragile', + 'frail', + 'frantic', + 'free', + 'french', + 'frequent', + 'fresh', + 'friendly', + 'frightened', + 'front', + 'frozen', + 'full', + 'full-time', + 'fun', + 'functional', + 'fundamental', + 'funny', + 'furious', + 'future', + 'fuzzy', + 'gastric', + 'gay', + 'general', + 'generous', + 'genetic', + 'gentle', + 'genuine', + 'geographical', + 'german', + 'giant', + 'gigantic', + 'given', + 'glad', + 'glamorous', + 'gleaming', + 'global', + 'glorious', + 'golden', + 'good', + 'gorgeous', + 'gothic', + 'governing', + 'graceful', + 'gradual', + 'grand', + 'grateful', + 'greasy', + 'great', + 'greek', + 'green', + 'grey', + 'grieving', + 'grim', + 'gross', + 'grotesque', + 'growing', + 'grubby', + 'grumpy', + 'guilty', + 'handicapped', + 'handsome', + 'happy', + 'hard', + 'harsh', + 'head', + 'healthy', + 'heavy', + 'helpful', + 'helpless', + 'hidden', + 'high', + 'high-pitched', + 'hilarious', + 'hissing', + 'historic', + 'historical', + 'hollow', + 'holy', + 'homeless', + 'homely', + 'hon', + 'honest', + 'horizontal', + 'horrible', + 'hostile', + 'hot', + 'huge', + 'human', + 'hungry', + 'hurt', + 'hushed', + 'husky', + 'icy', + 'ideal', + 'identical', + 'ideological', + 'ill', + 'illegal', + 'imaginative', + 'immediate', + 'immense', + 'imperial', + 'implicit', + 'important', + 'impossible', + 'impressed', + 'impressive', + 'improved', + 'inadequate', + 'inappropriate', + 'inclined', + 'increased', + 'increasing', + 'incredible', + 'independent', + 'indian', + 'indirect', + 'individual', + 'industrial', + 'inevitable', + 'influential', + 'informal', + 'inherent', + 'initial', + 'injured', + 'inland', + 'inner', + 'innocent', + 'innovative', + 'inquisitive', + 'instant', + 'institutional', + 'insufficient', + 'intact', + 'integral', + 'integrated', + 'intellectual', + 'intelligent', + 'intense', + 'intensive', + 'interested', + 'interesting', + 'interim', + 'interior', + 'intermediate', + 'internal', + 'international', + 'intimate', + 'invisible', + 'involved', + 'iraqi', + 'irish', + 'irrelevant', + 'isolated', + 'israeli', + 'italian', + 'itchy', + 'japanese', + 'jealous', + 'jittery', + 'joint', + 'jolly', + 'joyous', + 'judicial', + 'juicy', + 'junior', + 'just', + 'keen', + 'key', + 'kind', + 'known', + 'korean', + 'labour', + 'large', + 'large-scale', + 'late', + 'latin', + 'lazy', + 'leading', + 'left', + 'legal', + 'legislative', + 'legitimate', + 'lengthy', + 'lesser', + 'level', + 'lexical', + 'liable', + 'liberal', + 'light', + 'like', + 'likely', + 'limited', + 'linear', + 'linguistic', + 'liquid', + 'literary', + 'little', + 'live', + 'lively', + 'living', + 'local', + 'logical', + 'lonely', + 'long', + 'long-term', + 'loose', + 'lost', + 'loud', + 'lovely', + 'low', + 'loyal', + 'ltd', + 'lucky', + 'mad', + 'magenta', + 'magic', + 'magnetic', + 'magnificent', + 'main', + 'major', + 'male', + 'mammoth', + 'managerial', + 'managing', + 'manual', + 'many', + 'marginal', + 'marine', + 'marked', + 'married', + 'marvellous', + 'marxist', + 'mass', + 'massive', + 'mathematical', + 'mature', + 'maximum', + 'mean', + 'meaningful', + 'mechanical', + 'medical', + 'medieval', + 'melodic', + 'melted', + 'mental', + 'mere', + 'metropolitan', + 'mid', + 'middle', + 'middle-class', + 'mighty', + 'mild', + 'military', + 'miniature', + 'minimal', + 'minimum', + 'ministerial', + 'minor', + 'miserable', + 'misleading', + 'missing', + 'misty', + 'mixed', + 'moaning', + 'mobile', + 'moderate', + 'modern', + 'modest', + 'molecular', + 'monetary', + 'monthly', + 'moral', + 'motionless', + 'muddy', + 'multiple', + 'mushy', + 'musical', + 'mute', + 'mutual', + 'mysterious', + 'naked', + 'narrow', + 'nasty', + 'national', + 'native', + 'natural', + 'naughty', + 'naval', + 'near', + 'nearby', + 'neat', + 'necessary', + 'negative', + 'neighbouring', + 'nervous', + 'net', + 'neutral', + 'new', + 'nice', + 'nineteenth-century', + 'noble', + 'noisy', + 'normal', + 'northern', + 'nosy', + 'notable', + 'novel', + 'nuclear', + 'numerous', + 'nursing', + 'nutritious', + 'nutty', + 'obedient', + 'objective', + 'obliged', + 'obnoxious', + 'obvious', + 'occasional', + 'occupational', + 'odd', + 'official', + 'ok', + 'okay', + 'old', + 'old-fashioned', + 'olympic', + 'only', + 'open', + 'operational', + 'opposite', + 'optimistic', + 'oral', + 'orange', + 'ordinary', + 'organic', + 'organisational', + 'original', + 'orthodox', + 'other', + 'outdoor', + 'outer', + 'outrageous', + 'outside', + 'outstanding', + 'overall', + 'overseas', + 'overwhelming', + 'painful', + 'pale', + 'panicky', + 'parallel', + 'parental', + 'parliamentary', + 'part-time', + 'partial', + 'particular', + 'passing', + 'passive', + 'past', + 'patient', + 'payable', + 'peaceful', + 'peculiar', + 'perfect', + 'permanent', + 'persistent', + 'personal', + 'petite', + 'philosophical', + 'physical', + 'pink', + 'plain', + 'planned', + 'plastic', + 'pleasant', + 'pleased', + 'poised', + 'polish', + 'polite', + 'political', + 'poor', + 'popular', + 'positive', + 'possible', + 'post-war', + 'potential', + 'powerful', + 'practical', + 'precious', + 'precise', + 'preferred', + 'pregnant', + 'preliminary', + 'premier', + 'prepared', + 'present', + 'presidential', + 'pretty', + 'previous', + 'prickly', + 'primary', + 'prime', + 'primitive', + 'principal', + 'printed', + 'prior', + 'private', + 'probable', + 'productive', + 'professional', + 'profitable', + 'profound', + 'progressive', + 'prominent', + 'promising', + 'proper', + 'proposed', + 'prospective', + 'protective', + 'protestant', + 'proud', + 'provincial', + 'psychiatric', + 'psychological', + 'public', + 'puny', + 'pure', + 'purple', + 'purring', + 'puzzled', + 'quaint', + 'qualified', + 'quick', + 'quickest', + 'quiet', + 'racial', + 'radical', + 'rainy', + 'random', + 'rapid', + 'rare', + 'raspy', + 'rational', + 'ratty', + 'raw', + 'ready', + 'real', + 'realistic', + 'rear', + 'reasonable', + 'recent', + 'red', + 'reduced', + 'redundant', + 'regional', + 'registered', + 'regular', + 'regulatory', + 'related', + 'relative', + 'relaxed', + 'relevant', + 'reliable', + 'relieved', + 'religious', + 'reluctant', + 'remaining', + 'remarkable', + 'remote', + 'renewed', + 'representative', + 'repulsive', + 'required', + 'resident', + 'residential', + 'resonant', + 'respectable', + 'respective', + 'responsible', + 'resulting', + 'retail', + 'retired', + 'revolutionary', + 'rich', + 'ridiculous', + 'right', + 'rigid', + 'ripe', + 'rising', + 'rival', + 'roasted', + 'robust', + 'rolling', + 'roman', + 'romantic', + 'rotten', + 'rough', + 'round', + 'royal', + 'rubber', + 'rude', + 'ruling', + 'running', + 'rural', + 'russian', + 'sacred', + 'sad', + 'safe', + 'salty', + 'satisfactory', + 'satisfied', + 'scared', + 'scary', + 'scattered', + 'scientific', + 'scornful', + 'scottish', + 'scrawny', + 'screeching', + 'secondary', + 'secret', + 'secure', + 'select', + 'selected', + 'selective', + 'selfish', + 'semantic', + 'senior', + 'sensible', + 'sensitive', + 'separate', + 'serious', + 'severe', + 'sexual', + 'shaggy', + 'shaky', + 'shallow', + 'shared', + 'sharp', + 'sheer', + 'shiny', + 'shivering', + 'shocked', + 'short', + 'short-term', + 'shrill', + 'shy', + 'sick', + 'significant', + 'silent', + 'silky', + 'silly', + 'similar', + 'simple', + 'single', + 'skilled', + 'skinny', + 'sleepy', + 'slight', + 'slim', + 'slimy', + 'slippery', + 'slow', + 'small', + 'smart', + 'smelly', + 'smiling', + 'smoggy', + 'smooth', + 'so-called', + 'social', + 'socialist', + 'soft', + 'solar', + 'sole', + 'solid', + 'sophisticated', + 'sore', + 'sorry', + 'sound', + 'sour', + 'southern', + 'soviet', + 'spanish', + 'spare', + 'sparkling', + 'spatial', + 'special', + 'specific', + 'specified', + 'spectacular', + 'spicy', + 'spiritual', + 'splendid', + 'spontaneous', + 'sporting', + 'spotless', + 'spotty', + 'square', + 'squealing', + 'stable', + 'stale', + 'standard', + 'static', + 'statistical', + 'statutory', + 'steady', + 'steep', + 'sticky', + 'stiff', + 'still', + 'stingy', + 'stormy', + 'straight', + 'straightforward', + 'strange', + 'strategic', + 'strict', + 'striking', + 'striped', + 'strong', + 'structural', + 'stuck', + 'stupid', + 'subjective', + 'subsequent', + 'substantial', + 'subtle', + 'successful', + 'successive', + 'sudden', + 'sufficient', + 'suitable', + 'sunny', + 'super', + 'superb', + 'superior', + 'supporting', + 'supposed', + 'supreme', + 'sure', + 'surprised', + 'surprising', + 'surrounding', + 'surviving', + 'suspicious', + 'sweet', + 'swift', + 'swiss', + 'symbolic', + 'sympathetic', + 'systematic', + 'tall', + 'tame', + 'tan', + 'tart', + 'tasteless', + 'tasty', + 'technical', + 'technological', + 'teenage', + 'temporary', + 'tender', + 'tense', + 'terrible', + 'territorial', + 'testy', + 'then', + 'theoretical', + 'thick', + 'thin', + 'thirsty', + 'thorough', + 'thoughtful', + 'thoughtless', + 'thundering', + 'tight', + 'tiny', + 'tired', + 'top', + 'tory', + 'total', + 'tough', + 'toxic', + 'traditional', + 'tragic', + 'tremendous', + 'tricky', + 'tropical', + 'troubled', + 'turkish', + 'typical', + 'ugliest', + 'ugly', + 'ultimate', + 'unable', + 'unacceptable', + 'unaware', + 'uncertain', + 'unchanged', + 'uncomfortable', + 'unconscious', + 'underground', + 'underlying', + 'unemployed', + 'uneven', + 'unexpected', + 'unfair', + 'unfortunate', + 'unhappy', + 'uniform', + 'uninterested', + 'unique', + 'united', + 'universal', + 'unknown', + 'unlikely', + 'unnecessary', + 'unpleasant', + 'unsightly', + 'unusual', + 'unwilling', + 'upper', + 'upset', + 'uptight', + 'urban', + 'urgent', + 'used', + 'useful', + 'useless', + 'usual', + 'vague', + 'valid', + 'valuable', + 'variable', + 'varied', + 'various', + 'varying', + 'vast', + 'verbal', + 'vertical', + 'very', + 'victorian', + 'victorious', + 'video-taped', + 'violent', + 'visible', + 'visiting', + 'visual', + 'vital', + 'vivacious', + 'vivid', + 'vocational', + 'voiceless', + 'voluntary', + 'vulnerable', + 'wandering', + 'warm', + 'wasteful', + 'watery', + 'weak', + 'wealthy', + 'weary', + 'wee', + 'weekly', + 'weird', + 'welcome', + 'well', + 'well-known', + 'welsh', + 'western', + 'wet', + 'whispering', + 'white', + 'whole', + 'wicked', + 'wide', + 'wide-eyed', + 'widespread', + 'wild', + 'willing', + 'wise', + 'witty', + 'wonderful', + 'wooden', + 'working', + 'working-class', + 'worldwide', + 'worried', + 'worrying', + 'worthwhile', + 'worthy', + 'written', + 'wrong', + 'yellow', + 'young', + 'yummy', + 'zany', + 'zealous' +]; diff --git a/login/app/sprinkles/core/extra/nouns.php b/login/app/sprinkles/core/extra/nouns.php new file mode 100755 index 0000000..424b32c --- /dev/null +++ b/login/app/sprinkles/core/extra/nouns.php @@ -0,0 +1,90 @@ +<?php + +return [ + 'albatross', + 'auklet', + 'bittern', + 'blackbird', + 'bluebird', + 'booby', + 'bunting', + 'chickadee', + 'cormorant', + 'cowbird', + 'crow', + 'dove', + 'dowitcher', + 'duck', + 'eagle', + 'egret', + 'falcon', + 'finch', + 'flycatcher', + 'gallinule', + 'gnatcatcher', + 'godwit', + 'goldeneye', + 'goldfinch', + 'goose', + 'grackle', + 'grebe', + 'grosbeak', + 'gull', + 'hawk', + 'heron', + 'hummingbird', + 'ibis', + 'jaeger', + 'jay', + 'junco', + 'kingbird', + 'kinglet', + 'kite', + 'loon', + 'magpie', + 'meadowlark', + 'merganser', + 'murrelet', + 'nuthatch', + 'oriole', + 'owl', + 'pelican', + 'petrel', + 'pewee', + 'phalarope', + 'phoebe', + 'pigeon', + 'pipit', + 'plover', + 'puffin', + 'quail', + 'rail', + 'raven', + 'redstart', + 'sandpiper', + 'sapsucker', + 'scaup', + 'scoter', + 'shearwater', + 'shrike', + 'skua', + 'sparrow', + 'storm-petrel', + 'swallow', + 'swift', + 'tanager', + 'teal', + 'tern', + 'thrasher', + 'thrush', + 'titmouse', + 'towhee', + 'turnstone', + 'vireo', + 'vulture', + 'warbler', + 'wigeon', + 'woodpecker', + 'wren', + 'yellowlegs' +]; diff --git a/login/app/sprinkles/core/locale/ar/errors.php b/login/app/sprinkles/core/locale/ar/errors.php new file mode 100755 index 0000000..54465d4 --- /dev/null +++ b/login/app/sprinkles/core/locale/ar/errors.php @@ -0,0 +1,51 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Modern Standard Arabic message token translations for the 'core' sprinkle. + * + * @package userfrosting\i18n\ar + * @author Alexander Weissman and Abdullah Seba + */ + +return [ + "ERROR" => [ + "@TRANSLATION" => "خطأ", + + "400" => [ + "TITLE" => "الخطأ 400:اقتراح غير جيد", + "DESCRIPTION" => "على الارجح ليس خطأك", + ], + + "404" => [ + "TITLE" => "الخطأ 404: الصفحة غير موجودة", + "DESCRIPTION" => " لا يبدو للعثور على ما كنت تبحث عن", + "DETAIL" => "حاولنا العثور على صفحتك", + "EXPLAIN" => "لم نتمكن من العثور على الصفحة التي تبحث عنها", + "RETURN" => 'وفي كلتا الحالتين، اضغط <a href="{{url}}">هنا</a> للعودة إلى الصفحة الأولى' + ], + + "CONFIG" => [ + "TITLE" => "مشكلة في تكوين UserFrosting", + "DESCRIPTION" => "لم تتحقق بعض متطلبات التكوين UserFrosting", + "DETAIL" => "شيء ليس صحيحا هنا", + "RETURN" => 'يرجى تصحيح الأخطاء التالية، ثم <a href="{{url}}">إعادة تحميل</a>' + ], + + "DESCRIPTION" => "لقد لمست اضطراب كبير في الموقع", + "DETAIL" => "وهنا ما عندنا من معلومات", + + "ENCOUNTERED" => "حدث شيء لا نعرف ما هو", + + "MAIL" => "خطأ فادح في محاولة البريد الإلكتروني، اتصل بمسؤول المقع إذا كنت المشرف، يرجى التحقق من التسجل البريد الإلكتروني UF", + + "RETURN" => 'اضغط <a href="{{url}}">هنا</a> للعودة إلى الصفحة الأولى', + + "SERVER" => "يبدو خادمنا قد أخطأ إذا كنت المسير، يرجى مراجعة سجلات الخطأ PHP أو UF", + + "TITLE" => "اضطراب في الموقع" + ] +]; diff --git a/login/app/sprinkles/core/locale/ar/messages.php b/login/app/sprinkles/core/locale/ar/messages.php new file mode 100755 index 0000000..e1ee685 --- /dev/null +++ b/login/app/sprinkles/core/locale/ar/messages.php @@ -0,0 +1,112 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Modern Standard Arabic message token translations for the 'core' sprinkle. + * + * @package userfrosting\i18n\ar + * @author Alexander Weissman and Abdullah Seba + */ + +return [ + "@PLURAL_RULE" => 1, + + "ABOUT" => "عن", + + "CAPTCHA" => [ + "@TRANSLATION" => "كلمة التحقق", + "FAIL" => "لم تقم بإدخال رمز كلمة التحقق بشكل صحيح", + "SPECIFY" => "أدخل كلمة التحقق", + "VERIFY" => "التحقق من كلمة التحقق" + ], + + "CSRF_MISSING" => "رمز CSRF غير موجود حاول تحديث الصفحة ومن ثم إرساله مرة أخرى", + + "DB_INVALID" => "لا يمكن الاتصال بقاعدة البيانات إذا كنت مسؤولا، يرجى مراجعة سجل خطأ", + "DESCRIPTION" => "وصف", + "DOWNLOAD" => [ + "@TRANSLATION" => "تحميل", + "CSV" => "تحميل CSV" + ], + + "EMAIL" => [ + "@TRANSLATION" => "البريد الإلكتروني", + "YOUR" => "عنوان بريدك الإلكتروني" + ], + + "HOME" => "الصفحة الرئيسية", + + "LEGAL" => [ + "@TRANSLATION" => "السياسة القانونية", + "DESCRIPTION" => "تسري سياستنا القانونية على استخدامك لهذا الموقع وخدماتنا" + ], + + "LOCALE" => [ + "@TRANSLATION" => "اللغه", + ], + + "NAME" => "اسم", + "NAVIGATION" => "التنقل", + + "PAGINATION" => [ + "GOTO" => "انتقال إلى الصفحة", + "SHOW" => "عرض", + "NEXT" => "الصفحة التالية", + "PREVIOUS" => "الصفحة السابقة", + "FIRST" => "الصفحة الأولى", + "LAST" => "آخر صفحة" + ], + "PRIVACY" => [ + "@TRANSLATION" => "سياسة الخصوصية", + "DESCRIPTION" => "تحدد سياسة الخصوصية لدينا نوع المعلومات التي نجمعها منك وكيفية استخدامها." + ], + + "SLUG" => "Slug", + "SLUG_CONDITION" => "Slug/الظروف", + "STATUS" => "الحالة", + + "UNKNOWN" => "غير معروف", + + // Actions words + "ACTIONS" => "الأفعال", + "ACTIVATE" => "تفعيل", + "ACTIVE" => "نشيط", + "ADD" => "إضافة", + "CANCEL" => "إلغاء", + "CONFIRM" => "تؤكد", + "CREATE" => "أنتج", + "DELETE" => "حذف", + "DELETE_CONFIRM" => "هل أنت متأكد أنك تريد حذف هذا", + "DELETE_CONFIRM_YES" => "نعم، احذف", + "DELETE_CONFIRM_NAMED" => "هل أنت متأكد أنك تريد حذف {{name}}?", + "DELETE_CONFIRM_YES_NAMED" => "نعم، احذف {{name}}", + "DELETE_CANNOT_UNDONE" => "لا يمكن التراجع عن هذا الإجراء", + "DELETE_NAMED" => "احذف {{name}}", + "DENY" => "رفض", + "DISABLE" => "تعطيل", + "DISABLED" => "معطل", + "EDIT" => "تصحيح", + "ENABLE" => "تمكين", + "ENABLED" => "مكين", + "OVERRIDE" => "كتب فوق الكتابة", + "RESET" => "إعادة تعيين", + "SAVE" => "احفظ", + "SEARCH" => "ابحث", + "SORT" => "فرز", + "SUBMIT" => "ارسل", + "PRINT" => "اطباعة", + "REMOVE" => "إزالة", + "UNACTIVATED" => "إبطال", + "UPDATE" => "تحديث", + "YES" => "نعم", + "NO" => "لا", + "OPTIONAL" => "اختياري", + + // Misc + "BUILT_WITH_UF" => "بنيت مع <a href=\"http://www.userfrosting.com\">UserFrosting</a>", + "ADMINLTE_THEME_BY" => "فكرة رئيسية <strong><a href=\"http://almsaeedstudio.com\">Almsaeed Studio</a></strong> كل الحقوق محفوظة", + "WELCOME_TO" => "مرحبا بك في {{title}}!" +]; diff --git a/login/app/sprinkles/core/locale/ar/validate.php b/login/app/sprinkles/core/locale/ar/validate.php new file mode 100755 index 0000000..669a214 --- /dev/null +++ b/login/app/sprinkles/core/locale/ar/validate.php @@ -0,0 +1,25 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Modern Standard Arabic message token translations for the 'core' sprinkle. + * + * @package userfrosting\i18n\ar + * @author Alexander Weissman and Abdullah Seba + */ + +return [ + "VALIDATE" => [ + "ARRAY" => "القيمات ل <strong>{{label}}</strong> يجب أن تكون في مجموعة", + "BOOLEAN" => "القيم ل <strong>{{label}}</strong> يجب أن يكون إما '٠' أو '١'", + "INTEGER" => "القيم ل <strong>{{label}}</strong> يجب أن يكون رقم", + "INVALID_EMAIL" => "عنوان البريد الإلكتروني غير صالح", + "LENGTH_RANGE" => "{{label}} لابد ان تكون بين {{min}} و {{max}} حورف", + "NO_LEAD_WS" => "القيم ل <strong>{{label}}</strong> لا يمكن أن تبدأ المساحات، علامات، أو بيضاء أخرى", + "NO_TRAIL_WS" => "القيم ل <strong>{{label}}</strong> لا يمكن أن ينتهي مع مسافات، علامات، أو بيضاء أخرى", + "REQUIRED" => " تحديد قيمة ل <strong>{{label}}</strong>" + ] +]; diff --git a/login/app/sprinkles/core/locale/de_DE/errors.php b/login/app/sprinkles/core/locale/de_DE/errors.php new file mode 100755 index 0000000..cde296b --- /dev/null +++ b/login/app/sprinkles/core/locale/de_DE/errors.php @@ -0,0 +1,53 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * German message token translations for the 'core' sprinkle. + * + * @package userfrosting\i18n\de + * @author X-Anonymous-Y + * @author kevinrombach + * @author splitt3r + */ + +return [ + "ERROR" => [ + "@TRANSLATION" => "Fehler", + + "400" => [ + "TITLE" => "Fehler 400: Ungültige Anforderung", + "DESCRIPTION" => "Die Anfrage-Nachricht war fehlerhaft aufgebaut.", + ], + + "404" => [ + "TITLE" => "Fehler 404: Seite nicht gefunden", + "DESCRIPTION" => "Die angeforderte Ressource wurde nicht gefunden.", + "DETAIL" => "Wir haben versucht Ihre Seite zu finden ...", + "EXPLAIN" => "Die von Ihnen gesuchte Seite konnte nicht gefunden werden.", + "RETURN" => "Klicken Sie <a href='{{url}}'>Hier</a>, um zur Startseite zurückzukehren." + ], + + "CONFIG" => [ + "TITLE" => "UserFrosting Konfigurationsproblem!", + "DESCRIPTION" => "Einige UserFrosting-Konfigurationsanforderungen wurden nicht erfüllt.", + "DETAIL" => "Etwas stimmt hier nicht.", + "RETURN" => "Bitte beheben Sie die folgenden Fehler dann laden Sie die <a href='{{url}}'>Website</a> neu." + ], + + "DESCRIPTION" => "Wir haben eine große Störung in der Macht erkannt.", + "DETAIL" => "Hier haben wir:", + + "ENCOUNTERED" => "Uhhh ... etwas ist passiert. Wir wissen nicht was.", + + "MAIL" => "Schwerwiegender Fehler beim Mailversand, wenden Sie sich an Ihren Serveradministrator. Wenn Sie der Administrator sind, überprüfen Sie bitte das UF-Mail-Protokoll.", + + "RETURN" => "Klicken Sie <a href='{{url}}'>Hier</a>, um zur Startseite zurückzukehren.", + + "SERVER" => "Hoppla, sieht aus als hätte der Server möglicherweise gepatzt. Wenn Sie ein Administrator sind, überprüfen Sie bitte die PHP- oder UF-Fehlerprotokolle.", + + "TITLE" => "Störung in der Kraft" + ] +]; diff --git a/login/app/sprinkles/core/locale/de_DE/messages.php b/login/app/sprinkles/core/locale/de_DE/messages.php new file mode 100755 index 0000000..e254c65 --- /dev/null +++ b/login/app/sprinkles/core/locale/de_DE/messages.php @@ -0,0 +1,123 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * German message token translations for the 'core' sprinkle. + * + * @package userfrosting\i18n\de + * @author X-Anonymous-Y + * @author kevinrombach + * @author splitt3r + */ + +return [ + "@PLURAL_RULE" => 1, + + "ABOUT" => "Über", + + "CAPTCHA" => [ + "@TRANSLATION" => "Sicherheitscode", + "FAIL" => "Sie haben den Sicherheitscode nicht korrekt eingegeben.", + "SPECIFY" => "Geben Sie den Sicherheitscode ein", + "VERIFY" => "Überprüfen Sie den Sicherheitscode" + ], + + "CSRF_MISSING" => "Fehlender CSRF-Token. Versuchen, die Seite zu aktualisieren und erneut zu senden?", + + "DB_INVALID" => "Keine Verbindung zur Datenbank möglich. Wenn Sie ein Administrator sind, überprüfen Sie bitte Ihr Fehlerprotokoll.", + "DESCRIPTION" => "Beschreibung", + "DOWNLOAD" => [ + "@TRANSLATION" => "Herunterladen", + "CSV" => "CSV herunterladen" + ], + + "EMAIL" => [ + "@TRANSLATION" => "E-Mail", + "YOUR" => "Ihre E-Mail-Adresse" + ], + + "HOME" => "Startseite", + + "LEGAL" => [ + "@TRANSLATION" => "Rechtsgrundsatz", + "DESCRIPTION" => "Unser Rechtsgrundsatz gilt für die Benutzung dieser Internetseite und unserer Dienste." + ], + + "LOCALE" => [ + "@TRANSLATION" => "Sprache" + ], + + "NAME" => "Name", + "NAVIGATION" => "Navigation", + "NO_RESULTS" => "Sorry, hier gibt es bisher nichts zu sehen.", + + "PAGINATION" => [ + "GOTO" => "Gehe zu Seite", + "SHOW" => "Anzeigen", + + // Paginator + // possible variables: {size}, {page}, {totalPages}, {filteredPages}, {startRow}, {endRow}, {filteredRows} and {totalRows} + // also {page:input} & {startRow:input} will add a modifiable input in place of the value + "OUTPUT" => "{startRow} bis {endRow} von {filteredRows} ({totalRows})", + + "NEXT" => "Nächste Seite", + "PREVIOUS" => "Vorherige Seite", + "FIRST" => "Erste Seite", + "LAST" => "Letzte Seite" + ], + "PRIVACY" => [ + "@TRANSLATION" => "Datenschutzbestimmungen", + "DESCRIPTION" => "In unsere Datenschutzbestimmungen erklären wir Ihnen, welche Daten wir sammeln und wozu wir diese benutzen." + ], + + "SLUG" => "Schnecke", + "SLUG_CONDITION" => "Schnecke/Bedingungen", + "SLUG_IN_USE" => "Die Schnecke <strong>{{slug}}</strong> existiert bereits", + "STATUS" => "Status", + "SUGGEST" => "Vorschlagen", + + "UNKNOWN" => "Unbekannt", + + // Actions words + "ACTIONS" => "Aktionen", + "ACTIVATE" => "Aktivieren", + "ACTIVE" => "Aktiv", + "ADD" => "Hinzufügen", + "CANCEL" => "Abbrechen", + "CONFIRM" => "Bestätigen", + "CREATE" => "Erstellen", + "DELETE" => "Löschen", + "DELETE_CONFIRM" => "Möchten Sie diese wirklich löschen?", + "DELETE_CONFIRM_YES" => "Ja, löschen", + "DELETE_CONFIRM_NAMED" => "Möchten Sie {{name}} wirklich löschen?", + "DELETE_CONFIRM_YES_NAMED" => "Ja, {{name}} löschen", + "DELETE_CANNOT_UNDONE" => "Diese Aktion kann nicht rückgängig gemacht werden.", + "DELETE_NAMED" => "{{name}} löschen", + "DENY" => "Verweigern", + "DISABLE" => "Deaktivieren", + "DISABLED" => "Deaktiviert", + "EDIT" => "Bearbeiten", + "ENABLE" => "Aktivieren", + "ENABLED" => "Aktiviert", + "OVERRIDE" => "Überschreiben", + "RESET" => "Zurücksetzen", + "SAVE" => "Speichern", + "SEARCH" => "Suchen", + "SORT" => "Sortieren", + "SUBMIT" => "Einreichen", + "PRINT" => "Drucken", + "REMOVE" => "Entfernen", + "UNACTIVATED" => "Unaktiviert", + "UPDATE" => "Aktualisieren", + "YES" => "Ja", + "NO" => "Nein", + "OPTIONAL" => "Optional", + + // Misc. + "BUILT_WITH_UF" => "Errichtet mit <a href=\"http://www.userfrosting.com\">UserFrosting</a>", + "ADMINLTE_THEME_BY" => "Theme von <strong><a href=\"http://almsaeedstudio.com\">Almsaeed Studio</a>.</strong> Alle Rechte vorbehalten", + "WELCOME_TO" => "Willkommen auf {{title}}!" +]; diff --git a/login/app/sprinkles/core/locale/de_DE/validate.php b/login/app/sprinkles/core/locale/de_DE/validate.php new file mode 100755 index 0000000..c10a20a --- /dev/null +++ b/login/app/sprinkles/core/locale/de_DE/validate.php @@ -0,0 +1,32 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * German message token translations for the 'core' sprinkle. + * + * @package userfrosting\i18n\de + * @author X-Anonymous-Y + * @author kevinrombach + * @author splitt3r + */ + +return [ + "VALIDATE" => [ + "ARRAY" => "Die Werte für <strong>{{label}}</strong> müssen in einem Feld liegen.", + "BOOLEAN" => "Der Wert für <strong>{{label}}</strong> muss entweder '0' oder '1' sein.", + "INTEGER" => "Der Wert für <strong>{{label}}</strong> muss eine ganze Zahl sein.", + "INVALID_EMAIL" => "Ungültige E-Mail-Adresse.", + "LENGTH_RANGE" => "{{label}} muss zwischen {{min}} und {{max}} Zeichen lang sein.", + "NO_LEAD_WS" => "Der Wert für <strong>{{label}}</strong> kann nicht mit Leerzeichen, Tabulatoren oder anderen Leerzeichen beginnen.", + "NO_TRAIL_WS" => "Der Wert für <strong>{{label}}</strong> kann nicht mit Leerzeichen, Tabulatoren oder anderen Leerzeichen enden.", + "REQUIRED" => "Bitte geben Sie einen Wert für <strong>{{label}}</strong> an.", + "SPRUNJE" => [ + "BAD_FILTER" => "<strong>{{name}}</strong> ist kein gültiger Filter für dieses Sprunje.", + "BAD_LIST" => "<strong>{{name}}</strong> ist kein gültige Liste für dieses Sprunje.", + "BAD_SORT" => "<strong>{{name}}</strong> ist kein gültiges Sortierungsfeld für dieses Sprunje." + ] + ] +]; diff --git a/login/app/sprinkles/core/locale/en_US/errors.php b/login/app/sprinkles/core/locale/en_US/errors.php new file mode 100755 index 0000000..e868974 --- /dev/null +++ b/login/app/sprinkles/core/locale/en_US/errors.php @@ -0,0 +1,51 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * US English message token translations for the 'core' sprinkle. + * + * @package userfrosting\i18n\en_US + * @author Alexander Weissman + */ + +return [ + "ERROR" => [ + "@TRANSLATION" => "Error", + + "400" => [ + "TITLE" => "Error 400: Bad Request", + "DESCRIPTION" => "It's probably not your fault.", + ], + + "404" => [ + "TITLE" => "Error 404: Not Found", + "DESCRIPTION" => "We can't seem to find what you're looking for.", + "DETAIL" => "We tried to find your page...", + "EXPLAIN" => "We could not find the page you were looking for.", + "RETURN" => 'Either way, click <a href="{{url}}">here</a> to return to the front page.' + ], + + "CONFIG" => [ + "TITLE" => "UserFrosting Configuration Issue!", + "DESCRIPTION" => "Some UserFrosting configuration requirements have not been met.", + "DETAIL" => "Something's not right here.", + "RETURN" => 'Please fix the following errors, then <a href="{{url}}">reload</a>.' + ], + + "DESCRIPTION" => "We've sensed a great disturbance in the Force.", + "DETAIL" => "Here's what we got:", + + "ENCOUNTERED" => "Uhhh...something happened. We don't know what.", + + "MAIL" => "Fatal error attempting mail, contact your server administrator. If you are the admin, please check the UserFrosting log.", + + "RETURN" => 'Click <a href="{{url}}">here</a> to return to the front page.', + + "SERVER" => "Oops, looks like our server might have goofed. If you're an admin, please check the PHP or UserFrosting logs.", + + "TITLE" => "Disturbance in the Force" + ] +]; diff --git a/login/app/sprinkles/core/locale/en_US/messages.php b/login/app/sprinkles/core/locale/en_US/messages.php new file mode 100755 index 0000000..40668c8 --- /dev/null +++ b/login/app/sprinkles/core/locale/en_US/messages.php @@ -0,0 +1,120 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * US English message token translations for the 'core' sprinkle. + * + * @package userfrosting\i18n\en_US + * @author Alexander Weissman + */ + +return [ + "@PLURAL_RULE" => 1, + + "ABOUT" => "About", + + "CAPTCHA" => [ + "@TRANSLATION" => "Captcha", + "FAIL" => "You did not enter the captcha code correctly.", + "SPECIFY" => "Enter the captcha", + "VERIFY" => "Verify the captcha" + ], + + "CSRF_MISSING" => "Missing CSRF token. Try refreshing the page and then submitting again?", + + "DB_INVALID" => "Cannot connect to the database. If you are an administrator, please check your error log.", + "DESCRIPTION" => "Description", + "DOWNLOAD" => [ + "@TRANSLATION" => "Download", + "CSV" => "Download CSV" + ], + + "EMAIL" => [ + "@TRANSLATION" => "Email", + "YOUR" => "Your email address" + ], + + "HOME" => "Home", + + "LEGAL" => [ + "@TRANSLATION" => "Legal Policy", + "DESCRIPTION" => "Our legal policy applies to your usage of this website and our services." + ], + + "LOCALE" => [ + "@TRANSLATION" => "Locale" + ], + + "NAME" => "Name", + "NAVIGATION" => "Navigation", + "NO_RESULTS" => "Sorry, we've got nothing here.", + + "PAGINATION" => [ + "GOTO" => "Jump to Page", + "SHOW" => "Show", + + // Paginator + // possible variables: {size}, {page}, {totalPages}, {filteredPages}, {startRow}, {endRow}, {filteredRows} and {totalRows} + // also {page:input} & {startRow:input} will add a modifiable input in place of the value + "OUTPUT" => "{startRow} to {endRow} of {filteredRows} ({totalRows})", + "NEXT" => "Next page", + "PREVIOUS" => "Previous page", + "FIRST" => "First page", + "LAST" => "Last page" + ], + "PRIVACY" => [ + "@TRANSLATION" => "Privacy Policy", + "DESCRIPTION" => "Our privacy policy outlines what kind of information we collect from you and how we will use it." + ], + + "SLUG" => "Slug", + "SLUG_CONDITION" => "Slug/Conditions", + "SLUG_IN_USE" => "A <strong>{{slug}}</strong> slug already exists", + "STATUS" => "Status", + "SUGGEST" => "Suggest", + + "UNKNOWN" => "Unknown", + + // Actions words + "ACTIONS" => "Actions", + "ACTIVATE" => "Activate", + "ACTIVE" => "Active", + "ADD" => "Add", + "CANCEL" => "Cancel", + "CONFIRM" => "Confirm", + "CREATE" => "Create", + "DELETE" => "Delete", + "DELETE_CONFIRM" => "Are you sure you want to delete this?", + "DELETE_CONFIRM_YES" => "Yes, delete", + "DELETE_CONFIRM_NAMED" => "Are you sure you want to delete {{name}}?", + "DELETE_CONFIRM_YES_NAMED" => "Yes, delete {{name}}", + "DELETE_CANNOT_UNDONE" => "This action cannot be undone.", + "DELETE_NAMED" => "Delete {{name}}", + "DENY" => "Deny", + "DISABLE" => "Disable", + "DISABLED" => "Disabled", + "EDIT" => "Edit", + "ENABLE" => "Enable", + "ENABLED" => "Enabled", + "OVERRIDE" => "Override", + "RESET" => "Reset", + "SAVE" => "Save", + "SEARCH" => "Search", + "SORT" => "Sort", + "SUBMIT" => "Submit", + "PRINT" => "Print", + "REMOVE" => "Remove", + "UNACTIVATED" => "Unactivated", + "UPDATE" => "Update", + "YES" => "Yes", + "NO" => "No", + "OPTIONAL" => "Optional", + + // Misc. + "BUILT_WITH_UF" => "Built with <a href=\"http://www.userfrosting.com\">UserFrosting</a>", + "ADMINLTE_THEME_BY" => "Theme by <strong><a href=\"http://almsaeedstudio.com\">Almsaeed Studio</a>.</strong> All rights reserved", + "WELCOME_TO" => "Welcome to {{title}}!" +]; diff --git a/login/app/sprinkles/core/locale/en_US/validate.php b/login/app/sprinkles/core/locale/en_US/validate.php new file mode 100755 index 0000000..c4225a2 --- /dev/null +++ b/login/app/sprinkles/core/locale/en_US/validate.php @@ -0,0 +1,33 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * US English message token translations for the 'core' sprinkle. + * + * @package userfrosting\i18n\en_US + * @author Alexander Weissman + */ + +return [ + "VALIDATE" => [ + "ARRAY" => "The values for <strong>{{label}}</strong> must be in an array.", + "BOOLEAN" => "The value for <strong>{{label}}</strong> must be either '0' or '1'.", + "INTEGER" => "The value for <strong>{{label}}</strong> must be an integer.", + "INVALID_EMAIL" => "Invalid email address.", + "LENGTH_RANGE" => "{{label}} must be between {{min}} and {{max}} characters in length.", + "MAX_LENGTH" => "{{label}} must be maximum {{max}} characters in length.", + "MIN_LENGTH" => "{{label}} must be minimum {{min}} characters in length.", + "NO_LEAD_WS" => "The value for <strong>{{label}}</strong> cannot begin with spaces, tabs, or other whitespace.", + "NO_TRAIL_WS" => "The value for <strong>{{label}}</strong> cannot end with spaces, tabs, or other whitespace.", + "RANGE" => "The value for <strong>{{label}}</strong> must be between {{min}} and {{max}}.", + "REQUIRED" => "Please specify a value for <strong>{{label}}</strong>.", + "SPRUNJE" => [ + "BAD_FILTER" => "<strong>{{name}}</strong> is not a valid filter for this Sprunje.", + "BAD_LIST" => "<strong>{{name}}</strong> is not a valid list for this Sprunje.", + "BAD_SORT" => "<strong>{{name}}</strong> is not a valid sort field for this Sprunje." + ] + ] +]; diff --git a/login/app/sprinkles/core/locale/es_ES/errors.php b/login/app/sprinkles/core/locale/es_ES/errors.php new file mode 100755 index 0000000..0e4c671 --- /dev/null +++ b/login/app/sprinkles/core/locale/es_ES/errors.php @@ -0,0 +1,51 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Spanish message token translations for the 'core' sprinkle. + * + * @package userfrosting\i18n\es_ES + * @author rafa31gz + */ + +return [ + "ERROR" => [ + "@TRANSLATION" => "Error", + + "400" => [ + "TITLE" => "Error 400: solicitud incorrecta", + "DESCRIPTION" => "Probablemente no es tu culpa.", + ], + + "404" => [ + "TITLE" => "Error 404 - Página no encontrada", + "DESCRIPTION" => "Parece que no podemos encontrar lo que buscas.", + "DETAIL" => "Intentamos encontrar tu página ...", + "EXPLAIN" => "No pudimos encontrar la página que buscabas.", + "RETURN" => 'De cualquier manera, haga clic en <a href="{{url}}"> aquí </a> para volver a la página principal.' + ], + + "CONFIG" => [ + "TITLE" => "¡Problema de configuración del Servidor!", + "DESCRIPTION" => "Algunos requisitos de configuración de Servidor no se han cumplido.", + "DETAIL" => "Algo no está bien aquí.", + "RETURN" => 'Corrija los siguientes errores, luego <a href="{{url}}"> recargue </a>.' + ], + + "DESCRIPTION" => "Hemos sentido una gran perturbación en la Fuerza.", + "DETAIL" => "Esto es lo que tenemos:", + + "ENCOUNTERED" => "Uhhh ... sucedió algo. No sabemos qué.", + + "MAIL" => "Error fatal al intentar enviar correo, póngase en contacto con el administrador del servidor. Si usted es el administrador, compruebe el log de errores.", + + "RETURN" => 'Haga clic en <a href="{{url}}"> aquí </a> para volver a la página principal.', + + "SERVER" => "¡Vaya, parece que nuestro servidor pudo haber metido la pata. Si eres un administrador, comprueba los registros de errores de PHP o el log de UserFrosting.", + + "TITLE" => "Perturbación en la Fuerza", + ] +]; diff --git a/login/app/sprinkles/core/locale/es_ES/messages.php b/login/app/sprinkles/core/locale/es_ES/messages.php new file mode 100755 index 0000000..9bd097a --- /dev/null +++ b/login/app/sprinkles/core/locale/es_ES/messages.php @@ -0,0 +1,115 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Spanish message token translations for the 'core' sprinkle. + * + * @package userfrosting\i18n\es_ES + * @author rafa31gz + */ + +return [ + "@PLURAL_RULE" => 1, + + "ABOUT" => "Acerca de", + "WELCOME_TO" => "¡Bienvenido a {{title}}!", + + "CAPTCHA" => [ + "@TRANSLATION" => "Captcha", + "FAIL" => "No ha introducido correctamente el código de captcha.", + "SPECIFY" => "Introduzca el captcha", + "VERIFY" => "Verificar el captcha" + ], + + "CSRF_MISSING" => "¿Falta el símbolo CSRF?. Intente refrescar la página y luego volver a enviarla", + + "DB_INVALID" => "No se puede conectar a la base de datos. Si es un administrador, compruebe su registro de errores.", + "DESCRIPTION" => "Descripción", + "DOWNLOAD" => [ + "@TRANSLATION" => "Descargar", + "CSV" => "Descargar CSV" + ], + + "EMAIL" => [ + "@TRANSLATION" => "Email", + "YOUR" => "Tu correo electrónico" + ], + + "HOME" => "Inicio", + + "LEGAL" => "Política Legal", + + "LOCALE" => [ + "@TRANSLATION" => "Traducción" + ], + + "MAIL_ERROR" => "Error fatal al intentar enviar correo, póngase en contacto con el administrador del servidor. Si eres el administrador, comprueba el registro de correo de UF.", + + "NAME" => "Nombre", + "NAVIGATION" => "Navegación", + + "PAGINATION" => [ + "GOTO" => "Ir a la página", + "SHOW" => "Mostrar", + + // Paginator + // possible variables: {size}, {page}, {totalPages}, {filteredPages}, {startRow}, {endRow}, {filteredRows} and {totalRows} + // also {page:input} & {startRow:input} will add a modifiable input in place of the value + "OUTPUT" => "{startRow} a {endRow} de {filteredRows} ({totalRows})", + "NEXT" => "Siguiente página", + "PREVIOUS" => "Pagina anterior", + "FIRST" => "Primera página", + "LAST" => "Última página" + ], + "PRIVACY" => "Política de privacidad", + + "SLUG" => "Slug", + "SLUG_CONDITION" => "Slug/Condiciones", + "SLUG_IN_USE" => "A <strong>{{slug}}</strong> slug ya existe", + "STATUS" => "Estado", + "SUGGEST" => "Sugerencia", + + "UNKNOWN" => "Desconocido", + + // Actions words + "ACTIONS" => "Acciones", + "ACTIVATE" => "Activar", + "ACTIVE" => "Activo", + "ADD" => "Añadir", + "CANCEL" => "Cancelar", + "CONFIRM" => "Confirmar", + "CREATE" => "Crear", + "DELETE" => "Eliminar", + "DELETE_CONFIRM" => "¿Estás seguro que quieres eliminar esto?", + "DELETE_CONFIRM_YES" => "Sí, borrar", + "DELETE_CONFIRM_NAMED" => "¿Seguro que quieres eliminar {{name}}?", + "DELETE_CONFIRM_YES_NAMED" => "Sí, eliminar {{nombre}}", + "DELETE_CANNOT_UNDONE" => "Esta acción no se puede deshacer.", + "DELETE_NAMED" => "Eliminar {{name}}", + "DENY" => "Negar", + "DISABLE" => "Inhabilitar", + "DISABLED" => "Deshabilidato", + "EDIT" => "Editar", + "ENABLE" => "Habilitar", + "ENABLED" => "Habilitado", + "OVERRIDE" => "Anular", + "RESET" => "Reiniciar", + "SAVE" => "Guardar", + "SEARCH" => "Buscar", + "SORT" => "Ordenar", + "SUBMIT" => "Enviar", + "PRINT" => "Imprimir", + "REMOVE" => "Remover", + "UNACTIVATED" => "Desactivado", + "UPDATE" => "Actualizar", + "YES" => "Si", + "NO" => "No", + "OPTIONAL" => "Opcional", + + // Misc. + "BUILT_WITH_UF" => "Construido con <a href=\"http://www.userfrosting.com\"> UserFrosting </a>", + "ADMINLTE_THEME_BY" => "Theme by <strong><a href=\"http://almsaeedstudio.com\">Almsaeed Studio</a>.</strong> All rights reserved" +]; diff --git a/login/app/sprinkles/core/locale/es_ES/validate.php b/login/app/sprinkles/core/locale/es_ES/validate.php new file mode 100755 index 0000000..38218f5 --- /dev/null +++ b/login/app/sprinkles/core/locale/es_ES/validate.php @@ -0,0 +1,35 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Spanish message token translations for the 'core' sprinkle. + * + * @package userfrosting\i18n\es_ES + * @author rafa31gz + */ + +return [ + "VALIDATE" => [ + "ARRAY" => "Los valores de <strong> {{label}} </strong> deben estar en una matriz.", + "BOOLEAN" => "El valor de <strong> {{label}} </strong> debe ser '0' o '1'.", + "INTEGER" => "El valor de <strong> {{label}} </strong> debe ser un entero.", + "NUMERIC" => "El valor de <strong> {{label}} </strong> debe ser sólo números.", + "INVALID_EMAIL" => "Dirección de correo electrónico no válida.", + "LENGTH_RANGE" => "{{label}} debe tener entre {{min}} y {{max}} caracteres de longitud.", + "MAX_LENGTH" => "{{label}} debe tener un máximo de {{max}} caracteres de longitud.", + "MIN_LENGTH" => "{{label}} debe tener un mínimo de {{min}} caracteres de longitud.", + "NO_LEAD_WS" => "El valor de <strong> {{label}} </strong> no puede comenzar con espacios, pestañas u otros espacios en blanco.", + "NO_TRAIL_WS" => "El valor de <strong> {{label}} </strong> no puede finalizar con espacios, pestañas u otros espacios en blanco.", + "RANGE" => "El valor de <strong> {{label}} </strong> debe estar entre {{min}} y {{max}}.", + "REQUIRED" => "Especifique un valor para <strong> {{label}} </strong>.", + "PHONE" => "El numero proporcionado para el télefono es invalido.", + "SPRUNJE" => [ + "BAD_FILTER" => "<strong> {{name}} </strong> no es un filtro válido para este Sprunje.", + "BAD_LIST" => "<strong> {{name}} </strong> no es una lista válida para este Sprunje.", + "BAD_SORT" => "<strong>{{name}}</strong> no es un campo de clasificación válido para este Sprunje." + ] + ] +]; diff --git a/login/app/sprinkles/core/locale/fa/errors.php b/login/app/sprinkles/core/locale/fa/errors.php new file mode 100755 index 0000000..77bfe4f --- /dev/null +++ b/login/app/sprinkles/core/locale/fa/errors.php @@ -0,0 +1,52 @@ +<?php + +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Standard Farsi/Persian message token translations for the 'core' sprinkle. + * + * @package userfrosting\i18n\fa + * @author aminakbari + */ + +return [ + "ERROR" => [ + "@TRANSLATION" => "خطا", + + "400" => [ + "TITLE" => "خطا 400: درخواست بد", + "DESCRIPTION" => "احتمالا تقصیر شما نیست.", + ], + + "404" => [ + "TITLE" => "خطا 404: صفحه یافت نشد", + "DESCRIPTION" => "به نظر نمیرسد چیزی را که دنبالش هستید پیدا کنیم.", + "DETAIL" => "ما سعی کردیم صفحه شما را پیدا کنیم...", + "EXPLAIN" => "ما نتوانستیم صفحه ی مورد نظر شما را پیدا کنیم.", + "RETURN" => 'در هر حال، <a href="{{url}}">اینجا</a> کلیک کنید تا به صفحه اصلی بازگردید.' + ], + + "CONFIG" => [ + "TITLE" => "خطای تنظیمات یوزرفروستینگ!", + "DESCRIPTION" => "برخی از الزامات پیکربندی یوزرفروستینگ به نتیجه نرسید.", + "DETAIL" => "خطایی پیش آمد.", + "RETURN" => 'لطفا خطاهای زیر را اصلاح کنید و سپس مجددا <a href="{{url}}">بارگذاری</a> نمایید.' + ], + + "DESCRIPTION" => "ما یک اختلال بزرگ در سیستم احساس کردیم.", + "DETAIL" => "این چیزی است که ما دریافت کردیم", + + "ENCOUNTERED" => "خطایی ویژه. نمیدانیم مشکل چیست.", + + "MAIL" => "خطا در ارسال ایمیل. لطفا با مدیر سیستم تماس برقرار کنید. لطفا لاگ را بررسی کنید.", + + "RETURN" => '<a href="{{url}}">اینجا</a> کلیک تا به صفحه اصلی بازگردید.', + + "SERVER" => "به نظر می آید که در سرور خطایی بوجود آمد. لطفا لاگ پی اچ پی و یوزرفروستینگ را چک کنید.", + + "TITLE" => "اختلالی پدید آمد." + ] +]; diff --git a/login/app/sprinkles/core/locale/fa/messages.php b/login/app/sprinkles/core/locale/fa/messages.php new file mode 100755 index 0000000..00611c5 --- /dev/null +++ b/login/app/sprinkles/core/locale/fa/messages.php @@ -0,0 +1,110 @@ +<?php + +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Standard Farsi/Persian message token translations for the 'core' sprinkle. + * + * @package userfrosting\i18n\fa + * @author aminakbari + */ + +return [ + "@PLURAL_RULE" => 1, + + "ABOUT" => "درباره", + + "CAPTCHA" => [ + "@TRANSLATION" => "کد امنیتی", + "FAIL" => "کد امنیتی درست نیست", + "SPECIFY" => "کد امنیتی را وارد کنید", + "VERIFY" => "کد امنیتی را بررسی کنید" + ], + + "CSRF_MISSING" => "سی اس آر اف توکن یافت نشد. لطفا صفحه را از نو بارگذاری کرده و دوباره تلاش کنید.", + + "DB_INVALID" => "خطا در اتصال به پایگاه داده ها. لطفا لاگ پی اچ پی را چک کنید.", + "DESCRIPTION" => "توضیحات", + "DOWNLOAD" => [ + "@TRANSLATION" => "دانلود", + "CSV" => "دانلود سی اس وی" + ], + + "EMAIL" => [ + "@TRANSLATION" => "ایمیل", + "YOUR" => "آدرس ایمیل" + ], + + "HOME" => "خانه", + + "LEGAL" => "سیاست حقوقی", + + "LOCALE" => [ + "@TRANSLATION" => "زبان" + ], + + "NAME" => "نام", + "NAVIGATION" => "جهت یابی", + "NO_RESULTS" => "با عرض پوزش، چیزی یافت نشد.", + + "PAGINATION" => [ + "GOTO" => "پرش به صفحه", + "SHOW" => "نمایش", + + // Paginator + // possible variables: {size}, {page}, {totalPages}, {filteredPages}, {startRow}, {endRow}, {filteredRows} and {totalRows} + // also {page:input} & {startRow:input} will add a modifiable input in place of the value + "OUTPUT" => "{startRow} تا {endRow} از {filteredRows} ({totalRows})" + ], + "PRIVACY" => "سیاست حفظ حریم خصوصی", + + "SLUG" => "اسلاگ", + "SLUG_CONDITION" => "اسلاگ/شرایط", + "SLUG_IN_USE" => "<strong>{{slug}}</strong> وجود دارد", + "STATUS" => "وضعیت", + "SUGGEST" => "پیشنهاد", + + "UNKNOWN" => "ناشناخته", + + // Actions words + "ACTIONS" => "اقدام ها", + "ACTIVATE" => "فعال سازی", + "ACTIVE" => "فعال", + "ADD" => "اضافه کردن", + "CANCEL" => "لغو", + "CONFIRM" => "تایید", + "CREATE" => "اضافه کردن", + "DELETE" => "حذف", + "DELETE_CONFIRM" => "آیا مطمئن هستید که میخواهید این را حذف کنید؟", + "DELETE_CONFIRM_YES" => "بله، حذف شود", + "DELETE_CONFIRM_NAMED" => "اطمینان دارید که میخواهید {{name}} را حذف کنید؟", + "DELETE_CONFIRM_YES_NAMED" => "بله، {{name}} حذف شود", + "DELETE_CANNOT_UNDONE" => "این عملیات قابل بازگشت نیست.", + "DELETE_NAMED" => "{{name}} حذف شود", + "DENY" => "انکار", + "DISABLE" => "غیر فعال", + "DISABLED" => "غیر فعال", + "EDIT" => "ویرایش", + "ENABLE" => "فعال", + "ENABLED" => "فعال", + "OVERRIDE" => "تغییر", + "RESET" => "تنظیم مجدد", + "SAVE" => "ذخیره", + "SEARCH" => "جست و جو", + "SORT" => "مرتب سازی", + "SUBMIT" => "ارسال", + "PRINT" => "چاپ", + "REMOVE" => "حذف", + "UNACTIVATED" => "غیر فعال", + "UPDATE" => "به روز رسانی", + "YES" => "بله", + "NO" => "خیر", + "OPTIONAL" => "اختیاری", + + // Misc. + "BUILT_WITH_UF" => "ساخته شده با <a href=\"http://www.userfrosting.com\">یوزرفراستینگ</a>", + "ADMINLTE_THEME_BY" => "قالب از <strong><a href=\"http://almsaeedstudio.com\">Almsaeed Studio</a>.</strong> تمامی حقوق محفوظ است" +]; diff --git a/login/app/sprinkles/core/locale/fa/validate.php b/login/app/sprinkles/core/locale/fa/validate.php new file mode 100755 index 0000000..db364b1 --- /dev/null +++ b/login/app/sprinkles/core/locale/fa/validate.php @@ -0,0 +1,31 @@ +<?php + +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Standard Farsi/Persian message token translations for the 'core' sprinkle. + * + * @package userfrosting\i18n\fa + * @author aminakbari + */ + +return [ + "VALIDATE" => [ + "ARRAY" => "مقادیر <strong>{{label}}</strong> باید از یک آرایه باشند.", + "BOOLEAN" => "مقدار <strong>{{label}}</strong> باید 1 یا 0 باشد.", + "INTEGER" => "مقدار <strong>{{label}}</strong> باید یک عدد اینتجر باشد.", + "INVALID_EMAIL" => "آدرس پست الکترونیکی صحیح نیست.", + "LENGTH_RANGE" => "{{label}} باید بین {{min}} و {{max}} حرف باشد.", + "NO_LEAD_WS" => "مقدار <strong>{{label}}</strong> نباید با فاصله شروع شود.", + "NO_TRAIL_WS" => "مقدار <strong>{{label}}</strong> نباید با فاصله تمام شود.", + "REQUIRED" => "لطفا برای <strong>{{label}}</strong> مقداری تعیین کنید.", + "SPRUNJE" => [ + "BAD_FILTER" => "<strong>{{name}}</strong> فیلتر صحیحی نیست.", + "BAD_LIST" => "<strong>{{name}}</strong> لیست صحیحی نیست.", + "BAD_SORT" => "<strong>{{name}}</strong> فیلد مرتب سازی صحیحی نیست." + ] + ] +]; diff --git a/login/app/sprinkles/core/locale/fr_FR/errors.php b/login/app/sprinkles/core/locale/fr_FR/errors.php new file mode 100755 index 0000000..570a676 --- /dev/null +++ b/login/app/sprinkles/core/locale/fr_FR/errors.php @@ -0,0 +1,51 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * French message token translations for the 'core' sprinkle. + * + * @package userfrosting\i18n\fr + * @author Louis Charette + */ + +return [ + "ERROR" => [ + "@TRANSLATION" => "Erreur", + + "400" => [ + "TITLE" => "Erreur 400: Mauvaise requête", + "DESCRIPTION" => "Ce n'est probablement pas de votre faute.", + ], + + "404" => [ + "TITLE" => "Erreur 404: Page introuvable", + "DESCRIPTION" => "Nous ne pouvons trouver ce que vous cherchez.", + "DETAIL" => "Nous avons tout tenté...", + "EXPLAIN" => "Nous ne pouvons trouver la page que vous cherchez.", + "RETURN" => 'Cliquez <a href="{{url}}">ici</a> pour retourner à la page d\'accueil.' + ], + + "CONFIG" => [ + "TITLE" => "Problème de configuration UserFrosting!", + "DESCRIPTION" => "Les exigences de configuration de UserFrosting n'ont pas été satisfaites.", + "DETAIL" => "Quelque chose cloche ici...", + "RETURN" => 'Corrigez les erreurs suivantes, ensuite <a href="{{url}}"> recharger la page</a>.' + ], + + "DESCRIPTION" => "Nous avons ressenti un grand bouleversement de la Force.", + "DETAIL" => "Voici les détails :", + + "ENCOUNTERED" => "D'oh! Quelque chose s'est produit. Aucune idée c'est quoi.", + + "MAIL" => "Erreur fatale lors de l'envoie du courriel. Contactez votre administrateur. Si vous être administrateur, consultez les logs.", + + "RETURN" => 'Cliquez <a href="{{url}}">ici</a> pour retourner à la page d\'accueil.', + + "SERVER" => "Oops, il semblerait que le serveur a gaffé. Si vous êtes administrateur, s-v-p vérifier les logs d'erreurs PHP ou ceux de UserFrosting.", + + "TITLE" => "Bouleversement de la Force" + ] +]; diff --git a/login/app/sprinkles/core/locale/fr_FR/messages.php b/login/app/sprinkles/core/locale/fr_FR/messages.php new file mode 100755 index 0000000..c053569 --- /dev/null +++ b/login/app/sprinkles/core/locale/fr_FR/messages.php @@ -0,0 +1,105 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * French message token translations for the 'core' sprinkle. + * + * @package userfrosting\i18n\fr + * @author Louis Charette + */ + +return [ + "@PLURAL_RULE" => 2, + + "ABOUT" => "À propos", + + "CAPTCHA" => [ + "@TRANSLATE" => "Captcha", + "VERIFY" => "Vérification du captcha", + "SPECIFY" => "Entrer la valeur du captcha", + "FAIL" => "La valeur du captcha n'a pas été entrée correctement." + ], + + "CSRF_MISSING" => "Jeton CSRF manquant. Essayez de rafraîchir la page et de soumettre de nouveau?", + + "DB_INVALID" => "Impossible de se connecter à la base de données. Si vous êtes un administrateur, vérifiez votre journal d'erreurs.", + "DESCRIPTION" => "Description", + "DOWNLOAD" => [ + "@TRANSLATION" => "Télécharger", + "CSV" => "Télécharger CSV" + ], + + "EMAIL" => [ + "@TRANSLATION" => "Email", + "YOUR" => "Votre adresse email" + ], + + "HOME" => "Accueil", + + "LEGAL" => "Politique légale", + + "LOCALE" => [ + "@TRANSLATION" => "Langue" + ], + + "NAME" => "Nom", + "NAVIGATION" => "Menu principal", + "NO_RESULTS" => "Aucun résultat trouvé.", + + "PAGINATION" => [ + "GOTO" => "Aller à la page", + "SHOW" => "Afficher", + "OUTPUT" => "{startRow} à {endRow} de {filteredRows} ({totalRows})" + ], + "PRIVACY" => "Politique de confidentialité", + + "SLUG" => "Jeton", + "SLUG_CONDITION" => "Jeton/Conditions", + "SLUG_IN_USE" => "Un jeton <strong>{{slug}}</strong> existe déjà", + "STATUS" => "Statut", + "SUGGEST" => "Suggérer", + + "UNKNOWN" => "Inconnu", + + // Actions words + "ACTIONS" => "Actions", + "ACTIVATE" => "Autoriser", + "ACTIVE" => "Activé", + "ADD" => "Ajouter", + "CANCEL" => "Annuler", + "CONFIRM" => "Confirmer", + "CREATE" => "Créer", + "DELETE" => "Supprimer", + "DELETE_CONFIRM" => "Êtes-vous sûr de vouloir supprimer ceci?", + "DELETE_CONFIRM_YES" => "Oui, supprimer", + "DELETE_CONFIRM_NAMED" => "Êtes-vous sûr de vouloir supprimer {{name}}?", + "DELETE_CONFIRM_YES_NAMED" => "Oui, supprimer {{name}}", + "DELETE_CANNOT_UNDONE" => "Cette action ne peut être annulée.", //This action cannot be undone + "DELETE_NAMED" => "Supprimer {{name}}", + "DENY" => "Refuser", + "DISABLE" => "Désactiver", + "DISABLED" => "Désactivé", + "EDIT" => "Modifier", + "ENABLE" => "Activer", + "ENABLED" => "Activé", + "OVERRIDE" => "Forcer", + "RESET" => "Réinitialiser", + "SAVE" => "Sauvegarder", + "SEARCH" => "Rechercher", + "SORT" => "Trier", + "SUBMIT" => "Envoyer", + "PRINT" => "Imprimer", + "REMOVE" => "Supprimer", + "UNACTIVATED" => "Non activé", + "UPDATE" => "Mettre à jour", + "YES" => "Oui", + "NO" => "Non", + "OPTIONAL" => "Facultatif", + + // Misc. + "BUILT_WITH_UF" => "Créé avec <a href=\"http://www.userfrosting.com\">UserFrosting</a>", + "ADMINLTE_THEME_BY" => "Thème par <strong><a href=\"http://almsaeedstudio.com\">Almsaeed Studio</a>.</strong> Tous droits réservés" +];
\ No newline at end of file diff --git a/login/app/sprinkles/core/locale/fr_FR/validate.php b/login/app/sprinkles/core/locale/fr_FR/validate.php new file mode 100755 index 0000000..ea85677 --- /dev/null +++ b/login/app/sprinkles/core/locale/fr_FR/validate.php @@ -0,0 +1,33 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * French message token translations for the 'core' sprinkle. + * + * @package userfrosting\i18n\fr + * @author Louis Charette + */ + +return [ + "VALIDATE" => [ + "ARRAY" => "Les valeurs de <strong>{{label}}</strong> doivent être dans un tableau.", + "BOOLEAN" => "La valeur de <strong>{{label}}</strong> doit être '0' ou '1'.", + "INTEGER" => "La valeur de <strong>{{label}}</strong> doit être un nombre entier.", + "INVALID_EMAIL" => "Addresse email invalide.", + "LENGTH_RANGE" => "La valeur de {{label}} doit faire entre {{min}} et {{max}} caractères.", + "MAX_LENGTH" => "La valeur de {{label}} doit être d'un maximum de {{max}} caractères.", + "MIN_LENGTH" => "La valeur de {{label}} doit être d'un minimum de {{min}} caractères.", + "NO_LEAD_WS" => "La valeur de <strong>{{label}}</strong> ne peut pas commencer par des espaces, des tabulations ou d'autres caractères invisibles", + "NO_TRAIL_WS" => "La valeur de <strong>{{label}}</strong> ne peut pas se terminer par des espaces, des tabulations ou d'autres caractères invisibles", + "RANGE" => "Le champ <strong>{{label}}</strong> doit être une valeur entre {{min}} et {{max}}.", + "REQUIRED" => "Le champ <strong>{{label}}</strong> doit être rempli.", + "SPRUNJE" => [ + "BAD_FILTER" => "<strong>{{name}}</strong> ne peut pas être utilisé pour filtrer ce Sprunje.", + "BAD_LIST" => "<strong>{{name}}</strong> is not a valid list for this Sprunje.", + "BAD_SORT" => "<strong>{{name}}</strong> ne peut pas être utilisé pour trier Sprunje." + ] + ] +];
\ No newline at end of file diff --git a/login/app/sprinkles/core/locale/it_IT/errors.php b/login/app/sprinkles/core/locale/it_IT/errors.php new file mode 100755 index 0000000..c5d4eaa --- /dev/null +++ b/login/app/sprinkles/core/locale/it_IT/errors.php @@ -0,0 +1,53 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Italian message token translations for the 'core' sprinkle. + * This translation was generated with Google translate. Please contribute if you are a native speaker. + * + * @package userfrosting\i18n\it + * @author Alexander Weissman + * @author Pietro Marangon (@Pe46dro) + */ + +return [ + "ERROR" => [ + "@TRANSLATION" => "Errore", + + "400" => [ + "TITLE" => "Errore 400: Cattiva Richiesta", + "DESCRIPTION" => "Scusa per l'errore.", + ], + + "404" => [ + "TITLE" => "Errore 404 - Pagina Non Trovata", + "DESCRIPTION" => "Non possiamo sembrare trovare quello che stai cercando.", + "DETAIL" => "Abbiamo cercato di trovare la tua pagina ...", + "EXPLAIN" => "Non abbiamo trovato la pagina che stavi cercando.", + "RETURN" => "Fai clic su <a href=\"{{url}}\">qui</a> per tornare alla prima pagina." + ], + + "CONFIG" => [ + "TITLE" => "Problema di configurazione di UserFrosting!", + "DESCRIPTION" => "Alcuni requisiti di configurazione UserFrosting non sono stati soddisfatti.", + "DETAIL" => "Qualcosa non è proprio qui.", + "RETURN" => "Correggi i seguenti errori, quindi <a href=\"{{url}}\">ricarica</a>." + ], + + "DESCRIPTION" => "Abbiamo sentito un grande disturbo nella Forza.", + "DETAIL" => "Ecco quello che sappiamo:", + + "ENCOUNTERED" => "Uhhh...qualcosa è accaduto. Non sappiamo cosa.", + + "MAIL" => "Errore nell'invio della mail, contatta l'amministratore di sistema", + + "RETURN" => "Fai clic su <a href=\"{{url}}\">qui</a> per tornare alla prima pagina.", + + "SERVER" => "Sembra esserci un errore nel server. Se sei un admin, controlla i log di PHP o UserFrosting.", + + "TITLE" => "Disturbo nella Forza" + ] +]; diff --git a/login/app/sprinkles/core/locale/it_IT/messages.php b/login/app/sprinkles/core/locale/it_IT/messages.php new file mode 100755 index 0000000..255d732 --- /dev/null +++ b/login/app/sprinkles/core/locale/it_IT/messages.php @@ -0,0 +1,123 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Italian message token translations for the 'core' sprinkle. + * This translation was generated with Google translate. Please contribute if you are a native speaker. + * + * @package userfrosting\i18n\it + * @author Alexander Weissman + * @author Pietro Marangon (@Pe46dro) + */ + +return [ + "@PLURAL_RULE" => 1, + + "ABOUT" => "Riguardo a noi", + + "CAPTCHA" => [ + "@TRANSLATION" => "Captcha", + "FAIL" => "Domanda di sicurezza sbagliata", + "SPECIFY" => "Inserire il captcha", + "VERIFY" => "Verifica la captcha" + ], + + "CSRF_MISSING" => "Sigillo CSRF mancante. Prova a aggiornare la pagina e poi di inviarlo nuovamente?", + + "DB_INVALID" => "Impossibile connettersi al database. Se sei un amministratore, controlla il registro PHP o UserFrosting.", + "DESCRIPTION" => "Descrizione", + "DOWNLOAD" => [ + "@TRANSLATION" => "Scaricare", + "CSV" => "Scarica CSV" + ], + + "EMAIL" => [ + "@TRANSLATION" => "E-mail", + "YOUR" => "La tua email" + ], + + "HOME" => "Inizio", + + "LEGAL" => [ + "@TRANSLATION" => "Politica Legale", + "DESCRIPTION" => "La nostra politica legale si applica al tuo utilizzo di questo sito e dei nostri servizi." + ], + + "LOCALE" => [ + "@TRANSLATION" => "Località" + ], + + "NAME" => "Nome", + "NAVIGATION" => "Navigazione", + "NO_RESULTS" => "Spiacenti, non abbiamo niente qui.", + + "PAGINATION" => [ + "GOTO" => "Vai alla pagina", + "SHOW" => "Mostrare", + + // Paginator + // possible variables: {size}, {page}, {totalPages}, {filteredPages}, {startRow}, {endRow}, {filteredRows} and {totalRows} + // also {page:input} & {startRow:input} will add a modifiable input in place of the value + "OUTPUT" => "{startRow} a {endRow} di {filteredRows} ({totalRows})", + "NEXT" => "Pagina successiva", + "PREVIOUS" => "Pagina precedente", + "FIRST" => "Prima pagina", + "LAST" => "Ultima pagina" + ], + "PRIVACY" => [ + "@TRANSLATION" => "Politica sulla riservatezza", + "DESCRIPTION" => "La nostra politica sulla privacy descrive quali tipi di informazioni raccoglieremo da te e come lo useremo." + ], + + "SLUG" => "Slug", + "SLUG_CONDITION" => "Slug/Condizioni", + "SLUG_IN_USE" => "Esiste già uno slug <strong>{{slug}}</strong>", + "STATUS" => "Stato", + "SUGGEST" => "Suggerire", + + "UNKNOWN" => "Sconosciuto", + + // Actions words + "ACTIONS" => "Azioni", + "ACTIVATE" => "Attivare", + "ACTIVE" => "Attivo", + "ADD" => "Aggiungere", + "CANCEL" => "Annulla", + "CONFIRM" => "Conferma", + "CREATE" => "Creare", + "DELETE" => "Elimina", + "DELETE_CONFIRM" => "Sei sicuro di voler cancellare questo?", + "DELETE_CONFIRM_YES" => "Sì, elimini", + "DELETE_CONFIRM_NAMED" => "Sei sicuro di voler eliminare {{name}}?", + "DELETE_CONFIRM_YES_NAMED" => "Sì, eliminare {{name}}", + "DELETE_CANNOT_UNDONE" => "Questa azione non può essere annullata.", + "DELETE_NAMED" => "Elimina {{name}}", + "DENY" => "Nega", + "DISABLE" => "Disattivare", + "DISABLED" => "Disabilitato", + "EDIT" => "Modifica", + "ENABLE" => "Abilitare", + "ENABLED" => "Abilitato", + "OVERRIDE" => "Alterare", + "RESET" => "Azzerare", + "SAVE" => "Memorizzare", + "SEARCH" => "Cercare", + "SORT" => "Ordinare", + "SUBMIT" => "Inviare", + "SUCCESS" => "Successo", + "PRINT" => "Stampare", + "REMOVE" => "Rimuovere", + "UNACTIVATED" => "Non attivato", + "UPDATE" => "Aggiornare", + "YES" => "Sì", + "NO" => "No", + "OPTIONAL" => "Opzionale", + + // Misc. + "BUILT_WITH_UF" => "Construito <a href=\"http://www.userfrosting.com\">UserFrosting</a>", + "ADMINLTE_THEME_BY" => "Tema da <strong><a href=\"http://almsaeedstudio.com\">Almsaeed Studio</a>.</strong> Tutti i diritti riservati", + "WELCOME_TO" => "Benvenuto a {{title}}!" +]; diff --git a/login/app/sprinkles/core/locale/it_IT/validate.php b/login/app/sprinkles/core/locale/it_IT/validate.php new file mode 100755 index 0000000..99e4a57 --- /dev/null +++ b/login/app/sprinkles/core/locale/it_IT/validate.php @@ -0,0 +1,32 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Italian message token translations for the 'core' sprinkle. + * This translation was generated with Google translate. Please contribute if you are a native speaker. + * + * @package userfrosting\i18n\it + * @author Alexander Weissman + * @author Pietro Marangon (@Pe46dro) + */ + +return [ + "VALIDATE" => [ + "ARRAY" => "I valori per <strong>{{label}}</strong> devono essere in un vettore.", + "BOOLEAN" => "Il valore per <strong>{{label}}</strong> deve essere '0' o '1'.", + "INTEGER" => "Il valore per <strong>{{label}}</strong> deve essere un intero.", + "INVALID_EMAIL" => "Indirizzo mail non valido", + "LENGTH_RANGE" => "{{label}} deve essere compreso tra i caratteri {{min}} e {{max}} in lunghezza.", + "NO_LEAD_WS" => "Il valore di <strong>{{label}}</strong> non può iniziare con spazi, tabulazioni o altri spazi vuoti.", + "NO_TRAIL_WS" => "Il valore di <strong>{{label}}</strong> non può terminare con spazi, tabulazioni o altri spazi vuoti.", + "REQUIRED" => "Il campo <strong>{{label}}</strong> deve essere specificato.", + "SPRUNJE" => [ + "BAD_FILTER" => "<strong>{{name}}</strong> non è un filtro valido per questo Sprunje.", + "BAD_LIST" => "<strong> {{name}}</strong> non è un elenco valido per questo Sprunje.", + "BAD_SORT" => "<strong>{{name}}</strong> non è un campo di ordinamento valido per questo Sprunje." + ] + ] +]; diff --git a/login/app/sprinkles/core/locale/pt_PT/errors.php b/login/app/sprinkles/core/locale/pt_PT/errors.php new file mode 100755 index 0000000..2c938ec --- /dev/null +++ b/login/app/sprinkles/core/locale/pt_PT/errors.php @@ -0,0 +1,51 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Portuguese message token translations for the 'core' sprinkle. + * + * @package userfrosting\i18n\pt + * @author Bruno Silva (brunomnsilva@gmail.com) + */ + +return [ + "ERROR" => [ + "@TRANSLATION" => "Erro", + + "400" => [ + "TITLE" => "Erro 400: Pedido Inválido", + "DESCRIPTION" => "Provavelmente a culpa não é sua.", + ], + + "404" => [ + "TITLE" => "Erro 404: Página não Encontrada", + "DESCRIPTION" => "Parece que não conseguimos encontrar a página que procura.", + "DETAIL" => "Tentámos encontrar a sua página...", + "EXPLAIN" => "Não conseguimos encontrar a página que procura.", + "RETURN" => 'De qualquer forma, clique <a href="{{url}}">aqui</a> para regressar à página inicial.' + ], + + "CONFIG" => [ + "TITLE" => "Problema de Configuração do UserFrosting!", + "DESCRIPTION" => "Alguns requisitos de configuração do UserFrosting não foram satisfeitos.", + "DETAIL" => "Algo não está bem.", + "RETURN" => 'Por favor corrija os seguintes erros, depois <a href="{{url}}">refresque</a> a página.' + ], + + "DESCRIPTION" => "Sentimos uma grande perturbância na Força.", + "DETAIL" => "Eis o que sabemos:", + + "ENCOUNTERED" => "Uhhh...algo aconteceu. Não sabemos bem o quê.", + + "MAIL" => "Erro fatal ao tentar enviar email, contate o administrator do servidor. Se é administrador, por favor consulte o log de mail do UF.", + + "RETURN" => 'Clique <a href="{{url}}">aqui</a> para regressar à página inicial.', + + "SERVER" => "Oops, parece que o nosso servidor deu o berro. Se é um administrador, por favor consulte o log de erros PHP ou UF.", + + "TITLE" => "Perturbância na Força" + ] +]; diff --git a/login/app/sprinkles/core/locale/pt_PT/messages.php b/login/app/sprinkles/core/locale/pt_PT/messages.php new file mode 100755 index 0000000..a97704c --- /dev/null +++ b/login/app/sprinkles/core/locale/pt_PT/messages.php @@ -0,0 +1,102 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Portuguese message token translations for the 'core' sprinkle. + * + * @package userfrosting\i18n\pt + * @author Bruno Silva (brunomnsilva@gmail.com) + */ + +return [ + "@PLURAL_RULE" => 1, + + "ABOUT" => "Acerca", + + "CAPTCHA" => [ + "@TRANSLATION" => "Captcha", + "FAIL" => "Código captcha não introduzido corretamente.", + "SPECIFY" => "Introduza o código captcha", + "VERIFY" => "Verifique o código captcha" + ], + + "CSRF_MISSING" => "Token CSRF em falta. Tente refrescar a página e submeter de novo?", + + "DB_INVALID" => "Não é possível estabelecer ligação com a base de dados. Se é administrador, por favor consulte o log do servidor.", + "DESCRIPTION" => "Descrição", + "DOWNLOAD" => [ + "@TRANSLATION" => "Descarregar", + "CSV" => "Descarregar CSV" + ], + + "EMAIL" => [ + "@TRANSLATION" => "Email", + "YOUR" => "O seu endereço de email" + ], + + "HOME" => "Início", + + "LEGAL" => "Política Legal", + + "LOCALE" => [ + "@TRANSLATION" => "Localização" + ], + + "NAME" => "Nome", + "NAVIGATION" => "Navegação", + + "PAGINATION" => [ + "GOTO" => "Saltar para Página", + "SHOW" => "Mostrar", + "OUTPUT" => "{startRow} to {endRow} of {filteredRows} ({totalRows})" + ], + "PRIVACY" => "Política de Privacidade", + + "SLUG" => "Slug", + "SLUG_CONDITION" => "Slug/Condições", + "STATUS" => "Estado", + + "UNKNOWN" => "Desconhecido", + + // Actions words + "ACTIONS" => "Ações", + "ACTIVATE" => "Ativar", + "ACTIVE" => "Ativo", + "ADD" => "Adicionar", + "CANCEL" => "Cancelar", + "CONFIRM" => "Confirmar", + "CREATE" => "Criar", + "DELETE" => "Remover", + "DELETE_CONFIRM" => "Tem a certeza que deseja remover isto?", + "DELETE_CONFIRM_YES" => "Sim, remover", + "DELETE_CONFIRM_NAMED" => "Tem a certeza que deseja remover {{name}}?", + "DELETE_CONFIRM_YES_NAMED" => "Sim, remover {{name}}", + "DELETE_CANNOT_UNDONE" => "Esta ação não pode ser desfeita.", + "DELETE_NAMED" => "Remover {{name}}", + "DENY" => "Recusar", + "DISABLE" => "Desativar", + "DISABLED" => "Inativo", + "EDIT" => "Editar", + "ENABLE" => "Ativar", + "ENABLED" => "Ativo", + "OVERRIDE" => "Alterar", + "RESET" => "Apagar", + "SAVE" => "Guardar", + "SEARCH" => "Procurar", + "SORT" => "Ordenar", + "SUBMIT" => "Submeter", + "PRINT" => "Imprimir", + "REMOVE" => "Remover", + "UNACTIVATED" => "Inativo", + "UPDATE" => "Atualizar", + "YES" => "Sim", + "NO" => "Não", + "OPTIONAL" => "Opcional", + + // Misc. + "BUILT_WITH_UF" => "Desenvolvido sobre <a href=\"http://www.userfrosting.com\">UserFrosting</a>", + "ADMINLTE_THEME_BY" => "Tema por <strong><a href=\"http://almsaeedstudio.com\">Almsaeed Studio</a>.</strong> Todos os direitos reservados" +]; diff --git a/login/app/sprinkles/core/locale/pt_PT/validate.php b/login/app/sprinkles/core/locale/pt_PT/validate.php new file mode 100755 index 0000000..55fce76 --- /dev/null +++ b/login/app/sprinkles/core/locale/pt_PT/validate.php @@ -0,0 +1,25 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Portuguese message token translations for the 'core' sprinkle. + * + * @package userfrosting\i18n\pt + * @author Bruno Silva (brunomnsilva@gmail.com) + */ + +return [ + "VALIDATE" => [ + "ARRAY" => "Os valores para <strong>{{label}}</strong> devem estar contidos num array.", + "BOOLEAN" => "O valor para <strong>{{label}}</strong> deve ser '0' ou '1'.", + "INTEGER" => "O valor para <strong>{{label}}</strong> deve ser um inteiro.", + "INVALID_EMAIL" => "Endereço de email inválido.", + "LENGTH_RANGE" => "{{label}} deve conter entre {{min}} e {{max}} caracteres.", + "NO_LEAD_WS" => "O valor para <strong>{{label}}</strong> não pode começar por espaços, tabulações, ou outros espaços em branco.", + "NO_TRAIL_WS" => "O valor para <strong>{{label}}</strong> não pode terminar em espaços, tabulações, ou outros espaços em branco.", + "REQUIRED" => "Por favor especifique um valor para <strong>{{label}}</strong>." + ] +]; diff --git a/login/app/sprinkles/core/locale/ru_RU/errors.php b/login/app/sprinkles/core/locale/ru_RU/errors.php new file mode 100755 index 0000000..d2dd617 --- /dev/null +++ b/login/app/sprinkles/core/locale/ru_RU/errors.php @@ -0,0 +1,51 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Russian message token translations for the 'core' sprinkle. + * + * @package userfrosting\i18n\ru_RU + * @author @rendername + */ + +return [ + "ERROR" => [ + "@TRANSLATION" => "Ошибка", + + "400" => [ + "TITLE" => "Ошибка 400: Неправильный запрос", + "DESCRIPTION" => "Это, вероятно, не ваша вина.", + ], + + "404" => [ + "TITLE" => "Ошибка 404: Не найдено", + "DESCRIPTION" => "Кажется, мы не можем найти то, что вам нужно.", + "DETAIL" => "Мы пытались найти вашу страницу...", + "EXPLAIN" => "Мы не можем найти страницу, которую вы искали.", + "RETURN" => 'В любом случае, нажмите <a href="{{url}}"> здесь</a> чтобы вернуться на главную страницу.' + ], + + "CONFIG" => [ + "TITLE" => "Проблема в конфигурации!", + "DESCRIPTION" => "Некоторые требования к конфигурации UserFrosting, не были соблюдены.", + "DETAIL" => "Что-то здесь не так.", + "RETURN" => 'Пожалуйста, исправьте следующие ошибки, затем <a href="{{url}}"> перезагрузите</a>.' + ], + + "DESCRIPTION" => "Мы обнаружили большое и сильное нарушение.", + "DETAIL" => "Вот что мы получили:", + + "ENCOUNTERED" => "Ох... что-то произошло. Мы не знаем, что.", + + "MAIL" => "Неустранимая ошибка почтовой службы, обратитесь к администратору сервера. Если вы являетесь администратором, пожалуйста, проверьте логи.", + + "RETURN" => 'Нажмите <a href="{{url}}"> здесь</a> для возврата на главную страницу.', + + "SERVER" => "К сожалению, кажется сервер имеет ошибки. Если вы являетесь администратором сервера, пожалуйста проверьте логи.", + + "TITLE" => "Сильное нарушение" + ] +]; diff --git a/login/app/sprinkles/core/locale/ru_RU/messages.php b/login/app/sprinkles/core/locale/ru_RU/messages.php new file mode 100755 index 0000000..8de3730 --- /dev/null +++ b/login/app/sprinkles/core/locale/ru_RU/messages.php @@ -0,0 +1,120 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Russian message token translations for the 'core' sprinkle. + * + * @package userfrosting\i18n\ru_RU + * @author @rendername + */ + +return [ + "@PLURAL_RULE" => 1, + + "ABOUT" => "О нас", + + "CAPTCHA" => [ + "@TRANSLATION" => "Капча", + "FAIL" => "Код безопасности был введен с ошибками.", + "SPECIFY" => "Введите код капчи", + "VERIFY" => "Проверьте капчу" + ], + + "CSRF_MISSING" => "Отсутствует CSRF токен. Попробуйте обновить страницу и повторить попытку ещё раз?", + + "DB_INVALID" => "Не удается подключиться к базе данных. Если вы являетесь администратором, пожалуйста проверьте лог ошибок.", + "DESCRIPTION" => "Описание", + "DOWNLOAD" => [ + "@TRANSLATION" => "Скачать", + "CSV" => "Скачать CSV" + ], + + "EMAIL" => [ + "@TRANSLATION" => "Email", + "YOUR" => "Ваш e-mail" + ], + + "HOME" => "Главная", + + "LEGAL" => [ + "@TRANSLATION" => "Правовая информация", + "DESCRIPTION" => "Наша правовая политика применима к использованию вами данного веб-сайта и наших услуг." + ], + + "LOCALE" => [ + "@TRANSLATION" => "Язык" + ], + + "NAME" => "Имя", + "NAVIGATION" => "Навигация", + "NO_RESULTS" => "Извини, здесь ничего нет.", + + "PAGINATION" => [ + "GOTO" => "Перейти к странице", + "SHOW" => "Показать", + + // Paginator + // possible variables: {size}, {page}, {totalPages}, {filteredPages}, {startRow}, {endRow}, {filteredRows} and {totalRows} + // also {page:input} & {startRow:input} will add a modifiable input in place of the value + "OUTPUT" => "{startRow} к {endRow} из {filteredRows} ({totalRows})", + "NEXT" => "Следующая", + "PREVIOUS" => "Предыдущая", + "FIRST" => "Первая", + "LAST" => "Последняя" + ], + "PRIVACY" => [ + "@TRANSLATION" => "Политика конфиденциальности", + "DESCRIPTION" => "Наша политика конфиденциальности описывает, какую информацию мы собираем от вас и как мы будем использовать её." + ], + + "SLUG" => "Метка", + "SLUG_CONDITION" => "Метка/Условия", + "SLUG_IN_USE" => "<strong>{{slug}}</strong> метка уже существует", + "STATUS" => "Статус", + "SUGGEST" => "Предложить", + + "UNKNOWN" => "Неизвестно", + + // Actions words + "ACTIONS" => "Действия", + "ACTIVATE" => "Активировать", + "ACTIVE" => "Активные", + "ADD" => "Добавить", + "CANCEL" => "Отмена", + "CONFIRM" => "Подтвердить", + "CREATE" => "Создать", + "DELETE" => "Удалить", + "DELETE_CONFIRM" => "Вы уверены, что хотите удалить это?", + "DELETE_CONFIRM_YES" => "Да, удалить", + "DELETE_CONFIRM_NAMED" => "Вы уверены, что хотите удалить {{name}}?", + "DELETE_CONFIRM_YES_NAMED" => "Да, удалить {{name}}", + "DELETE_CANNOT_UNDONE" => "Это действие нельзя будет отменить.", + "DELETE_NAMED" => "Удаление {{name}}", + "DENY" => "Запретить", + "DISABLE" => "Отключить", + "DISABLED" => "Отключено", + "EDIT" => "Изменить", + "ENABLE" => "Включить", + "ENABLED" => "Включено", + "OVERRIDE" => "Отменить", + "RESET" => "Сброс", + "SAVE" => "Сохранить", + "SEARCH" => "Поиск", + "SORT" => "Сортировка", + "SUBMIT" => "Отправить", + "PRINT" => "Печать", + "REMOVE" => "Удалить", + "UNACTIVATED" => "Не активировано", + "UPDATE" => "Обновить", + "YES" => "Да", + "NO" => "Нет", + "OPTIONAL" => "Дополнительно", + + // Misc. + "BUILT_WITH_UF" => "Создано через <a href=\"http://www.userfrosting.com\"> UserFrosting</a>", + "ADMINLTE_THEME_BY" => "Тема от <strong><a href=\"http://almsaeedstudio.com\"> Almsaeed Studio</a>.</strong> Все права защищены", + "WELCOME_TO" => "Добро пожаловать на {{title}}!" +]; diff --git a/login/app/sprinkles/core/locale/ru_RU/validate.php b/login/app/sprinkles/core/locale/ru_RU/validate.php new file mode 100755 index 0000000..6d684de --- /dev/null +++ b/login/app/sprinkles/core/locale/ru_RU/validate.php @@ -0,0 +1,33 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Russian message token translations for the 'core' sprinkle. + * + * @package userfrosting\i18n\ru_RU + * @author @rendername + */ + +return [ + "VALIDATE" => [ + "ARRAY" => "Значения для <strong>{{label}}</strong> должны быть элементами массива.", + "BOOLEAN" => "Значение <strong>{{label}}</strong> должно быть '0' или '1'.", + "INTEGER" => "Значение <strong>{{label}}</strong> должно быть целым.", + "INVALID_EMAIL" => "Неправильный email.", + "LENGTH_RANGE" => "{{label}} должно быть между {{min}} и {{max}} символов в длину.", + "MAX_LENGTH" => "{{label}} должны быть максимально {{max}} символов в длину.", + "MIN_LENGTH" => "{{label}} должно быть минимально {{min}} символов в длину.", + "NO_LEAD_WS" => "Значение <strong>{{label}}</strong> не может начинаться с пробелов, табуляции или других пробелов.", + "NO_TRAIL_WS" => "Значение <strong>{{label}}</strong> не может заканчиваться пробелами, табуляции или другими пробелами.", + "RANGE" => "Значение <strong>{{label}}</strong> должно быть между {{min}} и {{max}} симв.", + "REQUIRED" => "Пожалуйста, укажите значение для <strong>{{label}}</strong>.", + "SPRUNJE" => [ + "BAD_FILTER" => "<strong>{{name}}</strong> не является допустимым фильтром.", + "BAD_LIST" => "<strong>{{name}}</strong> не является допустимым списком.", + "BAD_SORT" => "<strong>{{name}}</strong> не является допустимым для сортировки полей." + ] + ] +]; diff --git a/login/app/sprinkles/core/locale/th_TH/errors.php b/login/app/sprinkles/core/locale/th_TH/errors.php new file mode 100755 index 0000000..8f9413e --- /dev/null +++ b/login/app/sprinkles/core/locale/th_TH/errors.php @@ -0,0 +1,51 @@ +<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * Thai message token translations for the 'core' sprinkle.
+ *
+ * @package userfrosting\i18n\th
+ * @author Karuhut Komol
+ */
+
+return [
+ "ERROR" => [
+ "@TRANSLATION" => "ข้อผิดพลาด",
+
+ "400" => [
+ "TITLE" => "ข้อผิดพลาด 400: การร้องขอไม่ถูกต้อง",
+ "DESCRIPTION" => "นี่ไม่น่าจะเป็นความผิดพลาดของคุณ",
+ ],
+
+ "404" => [
+ "TITLE" => "ข้อผิดพลาด 404: ไม่พบหน้านี้",
+ "DESCRIPTION" => "ดูเหมือนเราจะไม่สามารถหาสิ่งที่คุณต้องการได้",
+ "DETAIL" => "เราพยายามได้ที่จะหาหน้าของคุณ...",
+ "EXPLAIN" => "เราไม่สามารถหาหน้าที่คุณมองหาอยู่ได้",
+ "RETURN" => 'อย่างไรก็ตาม คลิก <a href="{{url}}">ที่นี่</a> เพื่อกลับไปยังหน้าแรก'
+ ],
+
+ "CONFIG" => [
+ "TITLE" => "เกิดปัญหาจากการตั้งค่า UserFrosting!",
+ "DESCRIPTION" => "การตั้งค่าบางอย่างของ UserFrosting ยังไม่ตรงตามความต้องการ",
+ "DETAIL" => "มีบางอย่างไม่ถูกต้องอยู่",
+ "RETURN" => 'กรุณาแก้ไขข้อผิดพลาดดังกล่าว จากนั้น <a href="{{url}}">โหลดหน้านี้อีกครั้ง</a>'
+ ],
+
+ "DESCRIPTION" => "เรารู้สึกความโกลาหลในกองทัพได้เป็นอย่างดี",
+ "DETAIL" => "นี่คือสิ่งที่เราพบ:",
+
+ "ENCOUNTERED" => "อืมม...บางอย่างเกิดขึ้น แต่เราไม่รู้ว่าคืออะไร",
+
+ "MAIL" => "เกิดข้อผิดพลาดร้ายแรงระหว่างการพยายามส่งอีเมล กรุณาติดต่อผู้ดูแลระบบของเซิฟเวอร์นี้ หากคุณเป็นผู้ดูแล กรุณาตรวจสอบบันทึกอีเมลของ UF",
+
+ "RETURN" => 'คลิก <a href="{{url}}">ที่นี่</a> เพื่อกลับไปยังหน้าแรก',
+
+ "SERVER" => "โอ้ว ดูเหมือนระบบของเราอาจจะผิดพลาดเอง หากคุณเป็นผู้ดูแล กรุณาตรวจสอบบันทึกข้อผิดพลาดของ PHP หรือ UF",
+
+ "TITLE" => "เกิดความโกลาหลในกองทัพ"
+ ]
+];
diff --git a/login/app/sprinkles/core/locale/th_TH/messages.php b/login/app/sprinkles/core/locale/th_TH/messages.php new file mode 100755 index 0000000..9d14041 --- /dev/null +++ b/login/app/sprinkles/core/locale/th_TH/messages.php @@ -0,0 +1,102 @@ +<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * Thai message token translations for the 'core' sprinkle.
+ *
+ * @package userfrosting\i18n\th
+ * @author Karuhut Komol
+ */
+
+return [
+ "@PLURAL_RULE" => 1,
+
+ "ABOUT" => "เกี่ยวกับ",
+
+ "CAPTCHA" => [
+ "@TRANSLATION" => "รหัสยืนยัน",
+ "FAIL" => "คุณยังกรอกรหัสยืนยันไม่ถูกต้อง",
+ "SPECIFY" => "กรอกรหัสยืนยัน",
+ "VERIFY" => "ตรวจสอบรหัสยืนยัน"
+ ],
+
+ "CSRF_MISSING" => "ไม่พบโทเคน CSRF กรุณารีเฟรชแล้วส่งข้อมูลใหม่",
+
+ "DB_INVALID" => "ไม่สามารถเชื่อมต่อกับฐานข้อมูลได้ หากคุณเป็นผู้ดูแลระบบ กรุณาตรวจสอบบันทึกข้อผิดพลาด",
+ "DESCRIPTION" => "รายละเอียด",
+ "DOWNLOAD" => [
+ "@TRANSLATION" => "ดาวน์โหลด",
+ "CSV" => "ดาวน์โหลด CSV"
+ ],
+
+ "EMAIL" => [
+ "@TRANSLATION" => "อีเมล",
+ "YOUR" => "ที่อยู่อีเมลของคุณ"
+ ],
+
+ "HOME" => "หน้าแรก",
+
+ "LEGAL" => "นโยบายทางกฎหมาย",
+
+ "LOCALE" => [
+ "@TRANSLATION" => "ภาษา"
+ ],
+
+ "NAME" => "ชื่อ",
+ "NAVIGATION" => "เมนูนำทาง",
+
+ "PAGINATION" => [
+ "GOTO" => "ข้ามไปยังหน้า",
+ "SHOW" => "แสดง",
+ "OUTPUT" => "{startRow} to {endRow} of {filteredRows} ({totalRows})"
+ ],
+ "PRIVACY" => "นโยบายความเป็นส่วนตัว",
+
+ "SLUG" => "ข้อกำหนด",
+ "SLUG_CONDITION" => "ข้อกำหนด/เงื่อนไข",
+ "STATUS" => "สถานะ",
+
+ "UNKNOWN" => "ไม่ทราบ",
+
+ // Actions words
+ "ACTIONS" => "การดำเนินการ",
+ "ACTIVATE" => "เปิดใช้งาน",
+ "ACTIVE" => "เปิดใช้งานอยู่",
+ "ADD" => "เพิ่ม",
+ "CANCEL" => "ยกเลิก",
+ "CONFIRM" => "ยืนยัน",
+ "CREATE" => "สร้าง",
+ "DELETE" => "ลบ",
+ "DELETE_CONFIRM" => "คุณต้องการที่จะลบใช่หรือไม่?",
+ "DELETE_CONFIRM_YES" => "ใช่ ลบเลย",
+ "DELETE_CONFIRM_NAMED" => "คุณต้องการที่จะลบ {{name}} ใช่หรือไม่?",
+ "DELETE_CONFIRM_YES_NAMED" => "ใช่ ลบ {{name}} เลย",
+ "DELETE_CANNOT_UNDONE" => "การดำเนินการนี้ไม่สามารถยกเลิกได้",
+ "DELETE_NAMED" => "ลบ {{name}}",
+ "DENY" => "ปฏิเสธ",
+ "DISABLE" => "ปิดการใช้งาน",
+ "DISABLED" => "ปิดการใช้งานอยู่",
+ "EDIT" => "แก้ไข",
+ "ENABLE" => "เปิด",
+ "ENABLED" => "เปิดอยู่",
+ "OVERRIDE" => "เขียนทับ",
+ "RESET" => "รีเซ็ต",
+ "SAVE" => "บันทึก",
+ "SEARCH" => "ค้นหา",
+ "SORT" => "ประเภท",
+ "SUBMIT" => "ส่ง",
+ "PRINT" => "พิมพ์",
+ "REMOVE" => "เอาออก",
+ "UNACTIVATED" => "ไม่มีการเปิดใช้",
+ "UPDATE" => "ปรับปรุง",
+ "YES" => "ใช่",
+ "NO" => "ไม่",
+ "OPTIONAL" => "ตัวเลือกเพิ่มเติม",
+
+ // Misc.
+ "BUILT_WITH_UF" => "สร้างด้วย <a href=\"http://www.userfrosting.com\">UserFrosting</a>",
+ "ADMINLTE_THEME_BY" => "ธีมโดย <strong><a href=\"http://almsaeedstudio.com\">Almsaeed Studio</a></strong> สงวนลิขสิทธิ์"
+];
diff --git a/login/app/sprinkles/core/locale/th_TH/validate.php b/login/app/sprinkles/core/locale/th_TH/validate.php new file mode 100755 index 0000000..b28c021 --- /dev/null +++ b/login/app/sprinkles/core/locale/th_TH/validate.php @@ -0,0 +1,25 @@ +<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ *
+ * Thai message token translations for the 'core' sprinkle.
+ *
+ * @package userfrosting\i18n\th
+ * @author Karuhut Komol
+ */
+
+return [
+ "VALIDATE" => [
+ "ARRAY" => "ค่าของ <strong>{{label}}</strong> จะต้องเป็น Array",
+ "BOOLEAN" => "ค่าของ <strong>{{label}}</strong> จะต้องเป็น '0' หรือ '1'",
+ "INTEGER" => "ค่าของ <strong>{{label}}</strong> จะต้องเป็นตัวเลข",
+ "INVALID_EMAIL" => "ที่อยู่อีเมลไม่ถูกต้อง",
+ "LENGTH_RANGE" => "ความยาวของ {{label}} จะต้องอยู่ระหว่าง {{min}} ถึง {{max}} ตัวอักษร",
+ "NO_LEAD_WS" => "ค่าของ <strong>{{label}}</strong> ไม่สามารถเริ่มต้นด้วยช่องว่าง หรือ แท็บ",
+ "NO_TRAIL_WS" => "ค่าของ <strong>{{label}}</strong> ไม่สามารถลงท้ายด้วยช่องว่าง หรือ แท็บ",
+ "REQUIRED" => "กรุณากำหนดค่าของ <strong>{{label}}</strong>"
+ ]
+];
diff --git a/login/app/sprinkles/core/locale/valitron/ar.php b/login/app/sprinkles/core/locale/valitron/ar.php new file mode 100755 index 0000000..0c03547 --- /dev/null +++ b/login/app/sprinkles/core/locale/valitron/ar.php @@ -0,0 +1,28 @@ +<?php + +return array( + 'required' => "مطلوب", + 'equals' => "يجب أن يكون مساوي لي '%s'", + 'different' => "يجب ان يكون غير '%s'", + 'accepted' => "يجب ان يكون نعم", + 'numeric' => "يجب ان يكون رقم", + 'integer' => "يجب ان يكون رقم (0-9)", + 'length' => "يجب ان يكون أطول من %d", + 'min' => "يجب ان يكون اعلي من %s", + 'max' => "يجب ان يكون اقل من %s", + 'in' => "الُمدخل يغير صحيح", + 'notIn' => "الُمدخل يغير صحيح", + 'ip' => "رقم الإتصال غير صحيح", + 'email' => "البريد الألكتروني غير صحيح", + 'url' => "الرابط غير صحيح", + 'urlActive' => "يجب أن يكون نطاق فعال", + 'alpha' => "يجب أن يحتوي فقط علي a-z", + 'alphaNum' => "يجب ان يحتوي فقط a-z او ارقام 0-9", + 'slug' => "يجب ان يحتوي فقط علي a-z, و ارقام 0-9, شرطات و خط سفلي", + 'regex' => "خطا بالصيغة", + 'date' => "خطا بالتاريخ", + 'dateFormat' => "يجب ان يكون تاريخ بهذه الصيغة '%s'", + 'dateBefore' => "التاريخ يجب ان يكون قبل '%s'", + 'dateAfter' => "التاريخ يجب ان يكون بعد '%s'", + 'contains' => "يجب ان يحتوي %s" +); diff --git a/login/app/sprinkles/core/locale/valitron/de.php b/login/app/sprinkles/core/locale/valitron/de.php new file mode 100755 index 0000000..4847bd0 --- /dev/null +++ b/login/app/sprinkles/core/locale/valitron/de.php @@ -0,0 +1,33 @@ +<?php + +return array( + 'required' => "ist erforderlich", + 'equals' => "muss identisch mit '%s' sein", + 'different' => "muss sich von '%s' unterscheiden", + 'accepted' => "muss markiert sein", + 'numeric' => "muss eine Zahl sein", + 'integer' => "muss eine ganze Zahl sein", + 'length' => "kann nicht länger als %d sein", + 'min' => "muss größer als %s sein", + 'max' => "muss kleiner als %s sein", + 'in' => "enthält einen ungültigen Wert", + 'notIn' => "enthält einen ungültigen Wert", + 'ip' => "enthält keine gültige IP-Addresse", + 'email' => "enthält keine gültige E-Mail-Adresse", + 'url' => "enthält keine gültige URL", + 'urlActive' => "muss eine aktive Domain sein", + 'alpha' => "darf nur Buchstaben enthalten", + 'alphaNum' => "darf nur Buchstaben und Ganzzahlen enthalten", + 'slug' => "darf nur Buchstaben, Ganzzahlen, Schrägstriche und Grundstriche enthalten", + 'regex' => "enthält ungültige Zeichen", + 'date' => "enthält kein gültiges Datum", + 'dateFormat' => "benötigt ein Datum im Format '%s'", + 'dateBefore' => "benötigt ein Datum, das vor dem '%s' liegt", + 'dateAfter' => "benötigt ein Datum, das nach dem '%s' liegt", + 'contains' => "muss %s beinhalten", + 'boolean' => "muss ein Wahrheitswert sein", + 'lengthBetween' => "benötigt zwischen %d und %d Zeichen", + 'creditCard' => "muss eine gültige Kreditkartennummer sein", + "lengthMin" => "muss mindestens %d Zeichen enthalten", + "lengthMax" => "kann nicht mehr als %d Zeichen enthalten" +); diff --git a/login/app/sprinkles/core/locale/valitron/el.php b/login/app/sprinkles/core/locale/valitron/el.php new file mode 100755 index 0000000..8f27d39 --- /dev/null +++ b/login/app/sprinkles/core/locale/valitron/el.php @@ -0,0 +1,34 @@ +<?php + +return array( + 'required' => "είναι απαραίτητο", + 'equals' => "πρέπει να είναι ίδιο με '%s'", + 'different' => "πρέπει να διαφέρει από '%s'", + 'accepted' => "πρέπει να έχει αποδεχτεί", + 'numeric' => "πρέπει να είναι αριθμός", + 'integer' => "πρέπει να είναι ακέραιος αριθμός (0-9)", + 'length' => "πρέπει να είναι μεγαλύτερο από %d", + 'min' => "πρέπει να είναι τουλάχιστον %s", + 'max' => "δεν πρέπει να είναι περισσότερο από %s", + 'in' => "περιέχει μη έγκυρη τιμή", + 'notIn' => "περιέχει μη έγκυρη τιμή", + 'ip' => "δεν είναι έγκυρη διεύθυνση IP", + 'email' => "δεν είναι έγκυρη διεύθυνση email", + 'url' => "δεν είναι URL", + 'urlActive' => "πρέπει να είναι ενεργό domain", + 'alpha' => "πρέπει να περιέχει μόνο χαρακτήρες", + 'alphaNum' => "πρέπει να περιέχει μόνο χαρακτήρες και/ή αριθμούς", + 'slug' => "πρέπει να περιέχει μόνο χαρακτήρες, αριθμούς, παύλες και κάτω παύλες", + 'regex' => "περιέχει μη έγκυρους χαρακτήρες", + 'date' => "δεν είναι έγκυρη ημερομηνία", + 'dateFormat' => "πρέπει να είναι ημερομηνία της μορφής '%s'", + 'dateBefore' => "πρέπει να είναι ημερομηνία πριν από '%s'", + 'dateAfter' => "πρέπει να είναι ημερομηνία μετά από '%s'", + 'contains' => "πρέπει να περιέχει %s", + 'boolean' => "πρέπει να είναι boolean", + 'lengthBetween' => "πρέπει να είναι μεταξύ %d και %d χαρακτήρων", + 'creditCard' => "πρέπει να είναι ένα έγκυρο νούμερο πιστωτικής κάρτας", + "lengthMin" => "πρέπει να περιέχει περισσότερους από %d χαρακτήρες", + "lengthMax" => "πρέπει να περιέχει λιγότερους από %d χαρακτήρες", + "instanceOf" => "πρέπει να είναι αντικείμενο της '%s'" +); diff --git a/login/app/sprinkles/core/locale/valitron/en.php b/login/app/sprinkles/core/locale/valitron/en.php new file mode 100755 index 0000000..9dd9cd8 --- /dev/null +++ b/login/app/sprinkles/core/locale/valitron/en.php @@ -0,0 +1,34 @@ +<?php + +return array( + 'required' => "is required", + 'equals' => "must be the same as '%s'", + 'different' => "must be different than '%s'", + 'accepted' => "must be accepted", + 'numeric' => "must be numeric", + 'integer' => "must be an integer (0-9)", + 'length' => "must be longer than %d", + 'min' => "must be at least %s", + 'max' => "must be no more than %s", + 'in' => "contains invalid value", + 'notIn' => "contains invalid value", + 'ip' => "is not a valid IP address", + 'email' => "is not a valid email address", + 'url' => "not a URL", + 'urlActive' => "must be an active domain", + 'alpha' => "must contain only letters a-z", + 'alphaNum' => "must contain only letters a-z and/or numbers 0-9", + 'slug' => "must contain only letters a-z, numbers 0-9, dashes and underscores", + 'regex' => "contains invalid characters", + 'date' => "is not a valid date", + 'dateFormat' => "must be date with format '%s'", + 'dateBefore' => "must be date before '%s'", + 'dateAfter' => "must be date after '%s'", + 'contains' => "must contain %s", + 'boolean' => "must be a boolean", + 'lengthBetween' => "must be between %d and %d characters", + 'creditCard' => "must be a valid credit card number", + "lengthMin" => "must contain greater than %d characters", + "lengthMax" => "must contain less than %d characters", + "instanceOf" => "must be an instance of '%s'" +); diff --git a/login/app/sprinkles/core/locale/valitron/es.php b/login/app/sprinkles/core/locale/valitron/es.php new file mode 100755 index 0000000..3c177f6 --- /dev/null +++ b/login/app/sprinkles/core/locale/valitron/es.php @@ -0,0 +1,34 @@ +<?php + +return array( + 'required' => "es requerido", + 'equals' => "debe ser igual a '%s'", + 'different' => "debe ser diferente a '%s'", + 'accepted' => "debe ser aceptado", + 'numeric' => "debe ser numérico", + 'integer' => "debe ser un entero (0-9)", + 'length' => "debe ser mas largo de %d", + 'min' => "debe ser mayor a %s", + 'max' => "debe ser menor a %s", + 'in' => "contiene un valor invalido", + 'notIn' => "contiene un valor invalido", + 'ip' => "no es una dirección IP", + 'email' => "no es un correo electrónico válido", + 'url' => "no es una URL", + 'urlActive' => "debe ser un dominio activo", + 'alpha' => "debe contener solo letras a-z", + 'alphaNum' => "debe contener solo letras a-z o números 0-9", + 'slug' => "debe contener solo letras a-z, números 0-9, diagonales y guiones bajos", + 'regex' => "contiene caracteres inválidos", + 'date' => "no es una fecha válida", + 'dateFormat' => "debe ser una fecha con formato '%s'", + 'dateBefore' => "debe ser una fecha antes de '%s'", + 'dateAfter' => "debe ser una fecha después de '%s'", + 'contains' => "debe contener %s", + 'boolean' => "debe ser booleano", + 'lengthBetween' => "debe ser entre %d y %d caracteres", + 'creditCard' => "debe ser un numero de tarjeta de crédito válido", + "lengthMin" => "debe contener mas de %d caracteres", + "lengthMax" => "debe contener menos de %d caracteres", + "instanceOf" => "debe ser una instancia de '%s'" +); diff --git a/login/app/sprinkles/core/locale/valitron/fr.php b/login/app/sprinkles/core/locale/valitron/fr.php new file mode 100755 index 0000000..2572201 --- /dev/null +++ b/login/app/sprinkles/core/locale/valitron/fr.php @@ -0,0 +1,34 @@ +<?php + +return array( + 'required' => "est obligatoire", + 'equals' => "doit être identique à '%s'", + 'different' => "doit être différent de '%s'", + 'accepted' => "doit être accepté", + 'numeric' => "doit être numérique", + 'integer' => "doit être un entier (0-9)", + 'length' => "doit être plus long que %d", + 'min' => "doit être plus grand que %s", + 'max' => "doit être plus petit que %s", + 'in' => "contient une valeur non valide", + 'notIn' => "contient une valeur non valide", + 'ip' => "n'est pas une adresse IP valide", + 'email' => "n'est pas une adresse email valide", + 'url' => "n'est pas une URL", + 'urlActive' => "doit être un domaine actif", + 'alpha' => "doit contenir uniquement les lettres a-z", + 'alphaNum' => "doit contenir uniquement des lettres de a-z et/ou des chiffres 0-9", + 'slug' => "doit contenir uniquement des lettres de a-z, des chiffres 0-9, des tirets et des traits soulignés", + 'regex' => "contient des caractères invalides", + 'date' => "n'est pas une date valide", + 'dateFormat' => "doit être une date avec le format '%s'", + 'dateBefore' => "doit être une date avant '%s'", + 'dateAfter' => "doit être une date après '%s'", + 'contains' => "doit contenir %s", + 'boolean' => "doit être un booléen", + 'lengthBetween' => "doit contenir entre %d et %d caractères", + 'creditCard' => "doit être un numéro de carte de crédit valide", + "lengthMin" => "doit contenir plus de %d caractères", + "lengthMax" => "doit contenir moins de %d caractères", + "instanceOf" => "doit être une instance de '%s'" +); diff --git a/login/app/sprinkles/core/locale/valitron/id.php b/login/app/sprinkles/core/locale/valitron/id.php new file mode 100755 index 0000000..e4044cf --- /dev/null +++ b/login/app/sprinkles/core/locale/valitron/id.php @@ -0,0 +1,33 @@ +<?php + +return array( + 'required' => "harus diisi", + 'equals' => "harus sama dengan '%s'", + 'different' => "harus berbeda dengan '%s'", + 'accepted' => "harus diterima (accepted)", + 'numeric' => "harus berupa nomor/angka", + 'integer' => "harus berupa nilai integer (0-9)", + 'length' => "harus lebih panjang dari %d", + 'min' => "harus lebih besar dari %s", + 'max' => "harus kurang dari %s", + 'in' => "berisi nilai/value yang tidak valid", + 'notIn' => "berisi nilai/value yang tidak valid", + 'ip' => "format alamat IP tidak benar", + 'email' => "format alamat email tidak benar", + 'url' => "bukan format URL yang benar", + 'urlActive' => "harus berupa domain aktif", + 'alpha' => "hanya boleh menggunakan huruf a-z", + 'alphaNum' => "hanya boleh menggunakan huruf a-z dan atau nomor 0-9", + 'slug' => "hanya boleh menggunakan huruf a-z, nomor 0-9, tanda minus (-), dan uderscore atau strip bawah (_)", + 'regex' => "berisi karakter yang tidak valid", + 'date' => "format tanggal tidak valid", + 'dateFormat' => "harus berupa tanggal dengan format '%s'", + 'dateBefore' => "tanggal harus sebelum tanggal '%s'", + 'dateAfter' => "tanggal harus sesudah tanggal '%s'", + 'contains' => "harus berisi %s", + 'boolean' => "harus berupa nilai boolean", + 'lengthBetween' => "harus diantara karakter %d dan %d", + 'creditCard' => "nomor kartu kredit harus valid", + "lengthMin" => "minimal berisi %d karakter", + "lengthMax" => "maksimal berisi %d karakter" +); diff --git a/login/app/sprinkles/core/locale/valitron/it.php b/login/app/sprinkles/core/locale/valitron/it.php new file mode 100755 index 0000000..ee7a5c1 --- /dev/null +++ b/login/app/sprinkles/core/locale/valitron/it.php @@ -0,0 +1,31 @@ +<?php + +return array( + 'required' => "è obbligatorio", + 'equals' => "deve essere uguale a '%s'", + 'different' => "deve essere differente da '%s'", + 'accepted' => "deve essere accettato", + 'numeric' => "deve essere numerico", + 'integer' => "deve essere un intero (0-9)", + 'length' => "deve avere una lunghezza di %d", + 'min' => "deve essere superiore a %s", + 'max' => "deve essere inferiore a %s", + 'in' => "contiene un valore non valido", + 'notIn' => "contiene un valore non valido", + 'ip' => "non è un indirizzo IP valido", + 'email' => "non è un indirizzo email valido", + 'url' => "non è una URL", + 'urlActive' => "deve essere un dominio attivo", + 'alpha' => "deve contenere solamente lettere (a-z)", + 'alphaNum' => "deve contenere solamente lettere (a-z) e/o numeri (0-9)", + 'slug' => "deve contenere solamente lettere (a-z), numeri (0-9), trattini (-) e trattini bassi (_)", + 'regex' => "contiene caratteri non validi", + 'date' => "non è una data valida", + 'dateFormat' => "deve essere una data nel formato '%s'", + 'dateBefore' => "deve essere una data precedente al '%s'", + 'dateAfter' => "deve essere una data successiva al '%s'", + 'contains' => "deve contenere %s", + 'boolean' => "deve essere un booleano", + 'lengthBetween' => "deve essere compreso tra %d e %d caratteri", + 'creditCard' => "deve essere un numero di carta di credito valido" +); diff --git a/login/app/sprinkles/core/locale/valitron/ja.php b/login/app/sprinkles/core/locale/valitron/ja.php new file mode 100755 index 0000000..6fa494b --- /dev/null +++ b/login/app/sprinkles/core/locale/valitron/ja.php @@ -0,0 +1,33 @@ +<?php + +return array( + 'required' => "を入力してください", + 'equals' => "は「%s」と同じ内容を入力してください", + 'different' => "は「%s」と異なる内容を入力してください", + 'accepted' => "に同意してください", + 'numeric' => "は数値を入力してください", + 'integer' => "は半角数字で入力してください", + 'length' => "は%d文字で入力してください", + 'min' => "には%sより大きな値を入力してください", + 'max' => "には%sより小さな値を入力してください", + 'in' => "には選択できない値が含まれています", + 'notIn' => "には選択できない値が含まれています", + 'ip' => "はIPアドレスの書式として正しくありません", + 'email' => "はメールアドレスの書式として正しくありません", + 'url' => "はURLの書式として正しくありません", + 'urlActive' => "はアクティブなドメインではありません", + 'alpha' => "は半角英字で入力してください", + 'alphaNum' => "は半角英数字で入力してください", + 'slug' => "は半角英数字、もしくは「-」「_」の文字で入力してください", + 'regex' => "の書式が正しくありません", + 'date' => "は日付の書式として正しくありません", + 'dateFormat' => "は「%s」の書式で日付を入力してください", + 'dateBefore' => "は「%s」以前の日付を入力してください", + 'dateAfter' => "は「%s」以後の日付を入力してください", + 'contains' => "は「%s」を含んでいなければいけません", + 'boolean' => "は真偽値である必要があります", + 'lengthBetween' => "は%d〜%d文字で入力してください", + 'creditCard' => "はクレジットカード番号の書式として正しくありません", + "lengthMin" => "は%d文字以上入力してください", + "lengthMax" => "は%d文字以内で入力してください" +); diff --git a/login/app/sprinkles/core/locale/valitron/lv.php b/login/app/sprinkles/core/locale/valitron/lv.php new file mode 100755 index 0000000..9b5e54d --- /dev/null +++ b/login/app/sprinkles/core/locale/valitron/lv.php @@ -0,0 +1,31 @@ +<?php + +return array( + 'required' => "ir obligāts lauks", + 'equals' => "jāsakrīt ar '%s'", + 'different' => "nedrīkst sakrist ar '%s' lauku", + 'accepted' => "laukam jābūt apstiprinātam", + 'numeric' => "jābūt skaitliskai vērtībai", + 'integer' => "jābūt ciparam (0-9)", + 'length' => "nedrīkst būt garāks par %d simboliem", + 'min' => "jābūt garākam par %s simboliem", + 'max' => "jābūt īsākam par %s simboliem", + 'in' => "lauks satur nederīgu vērtību", + 'notIn' => "lauks satur nederīgu vērtību", + 'ip' => " lauks nav derīga IP adrese", + 'email' => "lauks nav norādīta derīga epasta adrese", + 'url' => "lauks nav tīmekļa saite", + 'urlActive' => "saite neatrodas esošajā domēna vārdā", + 'alpha' => "lauks var saturēt tikai alfabēta burtus a-z", + 'alphaNum' => "lauks var saturēt tikai alfabēta burtus un/vai ciparus 0-9", + 'slug' => "lauks var saturēt tikai alfabēta burtus un/vai ciparus 0-9, domuzīmes and zemsvītras", + 'regex' => "lauks satur nederīgus simbolus", + 'date' => "lauks ir nederīgā datuma formātā", + 'dateFormat' => "laukam jābūt datuma formātā '%s'", + 'dateBefore' => "lauka datumam jābūt pirms '%s'", + 'dateAfter' => "lauka datumam jābūt pēc '%s'", + 'contains' => "laukam jāsatur %s", + 'boolean' => "laukam jābūt ir/nav vērtībai", + 'lengthBetween' => "lauka garumam jābūt no %d līdz %d simbolu garam", + 'creditCard' => "laukam jābūt derīgam kredītkartes numuram" +); diff --git a/login/app/sprinkles/core/locale/valitron/pt-br.php b/login/app/sprinkles/core/locale/valitron/pt-br.php new file mode 100755 index 0000000..74c5660 --- /dev/null +++ b/login/app/sprinkles/core/locale/valitron/pt-br.php @@ -0,0 +1,28 @@ +<?php + +return array( + 'required' => "é obrigatório", + 'equals' => "deve ser o mesmo que '%s'", + 'different' => "deve ser diferente de '%s'", + 'accepted' => "deve ser aceito", + 'numeric' => "deve ser um número", + 'integer' => "deve ser um inteiro (0-9)", + 'length' => "deve ter mais que %d caracteres", + 'min' => "deve ser maior que %s", + 'max' => "deve ser menor que %s", + 'in' => "contém um valor inválido", + 'notIn' => "contém um valor inválido", + 'ip' => "não é um IP válido", + 'email' => "não é um email válido", + 'url' => "não é uma URL válida", + 'urlActive' => "deve ser um domínio ativo", + 'alpha' => "deve conter as letras a-z", + 'alphaNum' => "deve conter apenas letras a-z e/ou números 0-9", + 'slug' => "deve conter apenas letras a-z, números 0-9, ou os caracteres - ou _", + 'regex' => "contém caracteres inválidos", + 'date' => "não é uma data válida", + 'dateFormat' => "deve ser uma data no formato '%s'", + 'dateBefore' => "deve ser uma data anterior a '%s'", + 'dateAfter' => "deve ser uma data posterior a '%s'", + 'contains' => "deve conter %s" +); diff --git a/login/app/sprinkles/core/locale/valitron/ro.php b/login/app/sprinkles/core/locale/valitron/ro.php new file mode 100755 index 0000000..f7acce3 --- /dev/null +++ b/login/app/sprinkles/core/locale/valitron/ro.php @@ -0,0 +1,33 @@ +<?php +return array( + 'required' => "este necesar", + 'equals' => "trebuie sa fie la fel ca '%s'", + 'different' => "trebuie sa fie diferit ca '%s'", + 'accepted' => "trebuie acceptat", + 'numeric' => "trebuie sa fie numeric", + 'integer' => "trebuie sa fie o cifra (0-9)", + 'length' => "trebuie sa fie mai lung de %d", + 'min' => "trebuie sa fie minim %s", + 'max' => "nu trebuie sa fie mai mare de %s", + 'in' => "contine valori invalide", + 'notIn' => "contine valori invalide", + 'ip' => "nu este o adresa de IP valida", + 'email' => "nu este o adresa de email valida", + 'url' => "nu este un URL", + 'urlActive' => "trebuie sa fie un domeniu activ", + 'alpha' => "poate contine doar litere a-z", + 'alphaNum' => "poate contine doar litere a-z si/sau numere 0-9", + 'slug' => "poate contine doar litere a-z, numere 0-9, minus si underscore", + 'regex' => "contine caractere invalide", + 'date' => "nu este o data valida", + 'dateFormat' => "trebuie sa fie o data cu formatul '%s'", + 'dateBefore' => "trebuie sa fie o data inainte de '%s'", + 'dateAfter' => "trebuie sa fie o data dupa '%s'", + 'contains' => "trebuie sa contina %s", + 'boolean' => "trebuie sa fie valoare logica (boolean)", + 'lengthBetween' => "trebuie sa fie intre %d si %d caractere", + 'creditCard' => "trebuie sa fie un card de credit valid", + "lengthMin" => "trebuie sa contina mai mult de %d caractere", + "lengthMax" => "trebuie sa contina mai putin de %d caractere", + "instanceOf" => "trebuie sa fie o instanta a '%s'" +); diff --git a/login/app/sprinkles/core/locale/valitron/ru.php b/login/app/sprinkles/core/locale/valitron/ru.php new file mode 100755 index 0000000..78fe98f --- /dev/null +++ b/login/app/sprinkles/core/locale/valitron/ru.php @@ -0,0 +1,33 @@ +<?php + +return array( + 'required' => "обязательно для заполнения", + 'equals' => "должно содержать '%s'", + 'different' => "должно отличаться от '%s'", + 'accepted' => "должно быть указано", + 'numeric' => "должно содержать числовое значение", + 'integer' => "должно быть числом", + 'length' => "должно быть длиннее, чем %d", + 'min' => "должно быть больше, чем %s", + 'max' => "должно быть меньше, чем %s", + 'in' => "содержит неверное значение", + 'notIn' => "содержит неверное значение", + 'ip' => "не является валидным IP адресом", + 'email' => "не является валидным email адресом", + 'url' => "не является ссылкой", + 'urlActive' => "содержит не активную ссылку", + 'alpha' => "должно содержать только латинские символы", + 'alphaNum' => "должно содержать только латинские символы и/или цифры", + 'slug' => "должно содержать только латинские символы, цифры, тире и подчёркивания", + 'regex' => "содержит недопустимые символы", + 'date' => "не является датой", + 'dateFormat' => "должно содержать дату следующего формата: %s", + 'dateBefore' => "должно содержать дату не позднее, чем %s", + 'dateAfter' => "должно содержать дату не ранее, чем %s", + 'contains' => "должно содержать %s", + 'boolean' => "должно содержать логическое значение", + 'lengthBetween' => "должно содержать от %d до %d символов", + 'creditCard' => "должно быть номером кредитной карты", + "lengthMin" => "должно содержать более %d символов", + "lengthMax" => "должно содержать менее %d символов" +); diff --git a/login/app/sprinkles/core/locale/valitron/th.php b/login/app/sprinkles/core/locale/valitron/th.php new file mode 100755 index 0000000..2e4fa25 --- /dev/null +++ b/login/app/sprinkles/core/locale/valitron/th.php @@ -0,0 +1,34 @@ +<?php
+
+return array(
+ 'required' => "ต้องการ",
+ 'equals' => "จะต้องเหมือนกับ '%s'",
+ 'different' => "จะต้องไม่ใช่ '%s'",
+ 'accepted' => "จะต้องยอมรับ",
+ 'numeric' => "จะต้องเป็นตัวเลข",
+ 'integer' => "จะต้องเป็นตัวเลขหลักเดียว (0-9)",
+ 'length' => "จะต้องมีความยาวมากกว่า %d",
+ 'min' => "จะต้องมีอย่างน้อย %s",
+ 'max' => "จะต้องมีไม่มากไปกว่า %s",
+ 'in' => "ประกอบด้วยค่าที่ไม่ถูกต้อง",
+ 'notIn' => "ประกอบด้วยค่าที่ไม่ถูกต้อง",
+ 'ip' => "ไม่ใช่ที่อยู่ไอพีที่ถูกต้อง",
+ 'email' => "ไม่ใช่ที่อยู่อีเมลที่ถูกต้อง",
+ 'url' => "ไม่ใช่ลิงก์",
+ 'urlActive' => "จะต้องเป็นโดเมนที่มีการใช้งานอยู่",
+ 'alpha' => "จะต้องประกอบไปด้วยตัวอักษร a-z เท่านั้น",
+ 'alphaNum' => "จะต้องประกอบไปด้วยตัวอักษร a-z และ/หรือ เลข 0-9",
+ 'slug' => "จะต้องประกอบไปด้วยตัวอักษร a-z เลข 0-9 ขีดกลาง และขีดล่าง",
+ 'regex' => "ประกอบด้วยอักขระที่ไม่ถูกต้อง",
+ 'date' => "ไม่ใช่วันที่ที่ถูกต้อง",
+ 'dateFormat' => "จะต้องเป็นวันที่ที่มีรูปแบบ '%s'",
+ 'dateBefore' => "จะต้องเป็นวันที่ก่อน '%s'",
+ 'dateAfter' => "จะต้องเป็นวันที่หลังจาก '%s'",
+ 'contains' => "จะต้องประกอบไปด้วย %s",
+ 'boolean' => "จะต้องเป็นใช่ หรือ ไม่ใช่",
+ 'lengthBetween' => "จะต้องอยู่ระหว่าง %d ถึง %d ตัวอักษร",
+ 'creditCard' => "จะต้องเป็นหมายเลขบัตรเครดิตที่ถูกต้อง",
+ "lengthMin" => "จะต้องมีความยาวมากกว่า %d ตัวอักษร",
+ "lengthMax" => "จะต้องมีความยาวน้อยกว่า %d ตัวอักษร",
+ "instanceOf" => "จะต้องเป็นกรณีของ '%s'"
+);
diff --git a/login/app/sprinkles/core/locale/valitron/zh-cn.php b/login/app/sprinkles/core/locale/valitron/zh-cn.php new file mode 100755 index 0000000..4471ba8 --- /dev/null +++ b/login/app/sprinkles/core/locale/valitron/zh-cn.php @@ -0,0 +1,28 @@ +<?php + +return array( + 'required' => "不能为空", + 'equals' => "必须和 '%s' 一致", + 'different' => "必须和 '%s' 不一致", + 'accepted' => "必须接受", + 'numeric' => "只能是数字", + 'integer' => "只能是整数(0-9)", + 'length' => "长度必须大于 %d", + 'min' => "必须大于 %s", + 'max' => "必须小于 %s", + 'in' => "无效的值", + 'notIn' => "无效的值", + 'ip' => "无效IP地址", + 'email' => "无效邮箱地址", + 'url' => "无效的URL", + 'urlActive' => "必须是可用的域名", + 'alpha' => "只能包括英文字母(a-z)", + 'alphaNum' => "只能包括英文字母(a-z)和数字(0-9)", + 'slug' => "只能包括英文字母(a-z)、数字(0-9)、破折号和下划线", + 'regex' => "无效格式", + 'date' => "无效的日期", + 'dateFormat' => "日期的格式应该为 '%s'", + 'dateBefore' => "日期必须在 '%s' 之前", + 'dateAfter' => "日期必须在 '%s' 之后", + 'contains' => "必须包含 %s" +); diff --git a/login/app/sprinkles/core/locale/valitron/zh-tw.php b/login/app/sprinkles/core/locale/valitron/zh-tw.php new file mode 100755 index 0000000..df702c5 --- /dev/null +++ b/login/app/sprinkles/core/locale/valitron/zh-tw.php @@ -0,0 +1,28 @@ +<?php + +return array( + 'required' => "不能為空", + 'equals' => "必須和 '%s' 一致", + 'different' => "必須和 '%s' 不一致", + 'accepted' => "必須接受", + 'numeric' => "只能是數字", + 'integer' => "只能是整數(0-9)", + 'length' => "長度必須大於 %d", + 'min' => "必須大於 %s", + 'max' => "必須小於 %s", + 'in' => "無效的值", + 'notIn' => "無效的值", + 'ip' => "無效IP地址", + 'email' => "無效郵箱地址", + 'url' => "無效的URL", + 'urlActive' => "必須是可用的域名", + 'alpha' => "只能包括英文字母(a-z)", + 'alphaNum' => "只能包括英文字母(a-z)和數字(0-9)", + 'slug' => "只能包括英文字母(a-z)、數字(0-9)、破折號和下劃線", + 'regex' => "無效格式", + 'date' => "無效的日期", + 'dateFormat' => "日期的格式應該為 '%s'", + 'dateBefore' => "日期必須在 '%s' 之前", + 'dateAfter' => "日期必須在 '%s' 之後", + 'contains' => "必須包含 %s" +); diff --git a/login/app/sprinkles/core/locale/zh_CN/errors.php b/login/app/sprinkles/core/locale/zh_CN/errors.php new file mode 100755 index 0000000..9192e32 --- /dev/null +++ b/login/app/sprinkles/core/locale/zh_CN/errors.php @@ -0,0 +1,49 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Chinese message token translations for the 'core' sprinkle. + * + * @package userfrosting\i18n\zh_CN + * @author @BruceGui (https://github.com/BruceGui) + */ + +return [ + "ERROR" => [ + "@TRANSLATION" => "错误", + + "400" => [ + "TITLE" => "错误 400: 无效请求", + "DESCRIPTION" => "这好像不是你的错.", + ], + + "404" => [ + "TITLE" => "错误 404: 页面丢失", + "DESCRIPTION" => "我们无法找到你想要的东西.", + "DETAIL" => "我们正努力寻找网页...", + "EXPLAIN" => "我们无法找到你想要的网页.", + "RETURN" => '不管怎样, 点击 <a href="{{url}}">这里</a> 返回前一页.' + ], + + "CONFIG" => [ + "TITLE" => "UserFrosting 配置问题!", + "DESCRIPTION" => "一些 UserFrosting 配置要求没有达到.", + "DETAIL" => "这里有些东西不正确.", + "RETURN" => '请更正如下问题, 然后 <a href="{{url}}">重新加载</a>.' + ], + + "DESCRIPTION" => "我们发现一股强力干扰.", + "DETAIL" => "下面是我们得到的信息:", + + "ENCOUNTERED" => "嗯...发生了一些情况. 然而我们并不知道这是什么.", + + "RETURN" => '<a href="{{url}}">点击</a>返回上一页.', + + "SERVER" => "哦, 看起来我们的服务器出错了. 如果你是管理员, 请检查PHP及UF的logs.", + + "TITLE" => "强力干扰" + ] +]; diff --git a/login/app/sprinkles/core/locale/zh_CN/messages.php b/login/app/sprinkles/core/locale/zh_CN/messages.php new file mode 100755 index 0000000..55e769a --- /dev/null +++ b/login/app/sprinkles/core/locale/zh_CN/messages.php @@ -0,0 +1,105 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Chinese message token translations for the 'core' sprinkle. + * + * @package userfrosting\i18n\zh_CN + * @author @BruceGui (https://github.com/BruceGui) + */ + +return [ + "@PLURAL_RULE" => 0, + + "ABOUT" => "关于", + + "CAPTCHA" => [ + "@TRANSLATION" => "验证码", + "FAIL" => "Y验证码输入错误.", + "SPECIFY" => "输入验证码", + "VERIFY" => "验证" + ], + + "CSRF_MISSING" => " CSRF 标记丢失. 请尝试重新加载页面?", + + "DB_INVALID" => "无法连接到数据库. 如果你是管理员, 请检查错误日志文件.", + "DESCRIPTION" => "描述", + "DOWNLOAD" => [ + "@TRANSLATION" => "下载", + "CSV" => "下载 CSV 文件" + ], + + "EMAIL" => [ + "@TRANSLATION" => "邮件", + "YOUR" => "你的邮件地址" + ], + + "HOME" => "首页", + + "LEGAL" => "法律政策", + + "LOCALE" => [ + "@TRANSLATION" => "本地" + ], + + "MAIL_ERROR" => "尝试发送邮件发送致命错误, 联系网站管理员. 如果你是管理员,请检查UF邮件错误日志.", + + "NAME" => "名字", + "NAVIGATION" => "导航", + + "PAGINATION" => [ + "GOTO" => "跳到页", + "SHOW" => "显示" + ], + "PRIVACY" => "隐私政策", + + "SLUG" => "Slug", + "SLUG_CONDITION" => "Slug/Conditions", + "SLUG_IN_USE" => "A <strong>{{slug}}</strong> slug already exists", + "STATUS" => "状态", + "SUGGEST" => "建议", + + "UNKNOWN" => "未知", + + // Actions words + "ACTIONS" => "动作", + "ACTIVATE" => "激活", + "ACTIVE" => "Active", + "ADD" => "添加", + "CANCEL" => "取消", + "CONFIRM" => "确认", + "CREATE" => "创建", + "DELETE" => "删除", + "DELETE_CONFIRM" => "你确定要删除这个?", + "DELETE_CONFIRM_YES" => "是的, 删除", + "DELETE_CONFIRM_NAMED" => "你确定要删除 {{name}}?", + "DELETE_CONFIRM_YES_NAMED" => "是的, 删除 {{name}}", + "DELETE_CANNOT_UNDONE" => "这个动作无法撤销.", + "DELETE_NAMED" => "删除 {{name}}", + "DENY" => "拒绝", + "DISABLE" => "禁用", + "DISABLED" => "禁用", + "EDIT" => "编辑", + "ENABLE" => "启用", + "ENABLED" => "启用", + "OVERRIDE" => "覆盖", + "RESET" => "重置", + "SAVE" => "保存", + "SEARCH" => "搜寻", + "SORT" => "排序", + "SUBMIT" => "提交", + "PRINT" => "打印", + "REMOVE" => "删除", + "UNACTIVATED" => "未激活", + "UPDATE" => "更新", + "YES" => "是", + "NO" => "不是", + "OPTIONAL" => "可选择的", + + // Misc. + "BUILT_WITH_UF" => "使用 <a href=\"http://www.userfrosting.com\">UserFrosting</a>", + "ADMINLTE_THEME_BY" => "主题作者 <strong><a href=\"http://almsaeedstudio.com\">Almsaeed Studio</a>.</strong>保留所有权" +]; diff --git a/login/app/sprinkles/core/locale/zh_CN/validate.php b/login/app/sprinkles/core/locale/zh_CN/validate.php new file mode 100755 index 0000000..ed68198 --- /dev/null +++ b/login/app/sprinkles/core/locale/zh_CN/validate.php @@ -0,0 +1,29 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + * + * Chinese message token translations for the 'core' sprinkle. + * + * @package userfrosting\i18n\zh_CN + * @author @BruceGui (https://github.com/BruceGui) + */ + +return [ + "VALIDATE" => [ + "ARRAY" => " <strong>{{label}}</strong> 的值必须在一个数组中.", + "BOOLEAN" => " <strong>{{label}}</strong> 的值必须是 '0' 或 '1'.", + "INTEGER" => " <strong>{{label}}</strong> 必须是整数.", + "INVALID_EMAIL" => "无效的邮箱地址.", + "LENGTH_RANGE" => "{{label}} 的长度必须在 {{min}} - {{max}} 之间.", + "NO_LEAD_WS" => "<strong>{{label}}</strong> 的值不能以空格、TAB或其他空白开始.", + "NO_TRAIL_WS" => " <strong>{{label}}</strong> 的值不能以空格、TAB或其他空白结束.", + "REQUIRED" => "请为 <strong>{{label}}</strong> 确定一个值.", + "SPRUNJE" => [ + "BAD_FILTER" => "<strong>{{name}}</strong> 不是一个有效的 Sprunje 过滤器.", + "BAD_SORT" => "<strong>{{name}}</strong> 不是一个有效的 Sprunje 排序." + ] + ] +]; diff --git a/login/app/sprinkles/core/routes/routes.php b/login/app/sprinkles/core/routes/routes.php new file mode 100755 index 0000000..3598b7e --- /dev/null +++ b/login/app/sprinkles/core/routes/routes.php @@ -0,0 +1,24 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ + +global $app; +$config = $app->getContainer()->get('config'); + +$app->get('/', 'UserFrosting\Sprinkle\Core\Controller\CoreController:pageIndex') + ->add('checkEnvironment') + ->setName('index'); + +$app->get('/about','UserFrosting\Sprinkle\Core\Controller\CoreController:pageAbout')->add('checkEnvironment'); + +$app->get('/alerts', 'UserFrosting\Sprinkle\Core\Controller\CoreController:jsonAlerts'); + +$app->get('/legal', 'UserFrosting\Sprinkle\Core\Controller\CoreController:pageLegal'); + +$app->get('/privacy', 'UserFrosting\Sprinkle\Core\Controller\CoreController:pagePrivacy'); + +$app->get('/' . $config['assets.raw.path'] . '/{url:.+}', 'UserFrosting\Sprinkle\Core\Controller\CoreController:getAsset'); diff --git a/login/app/sprinkles/core/schema/.gitkeep b/login/app/sprinkles/core/schema/.gitkeep new file mode 100755 index 0000000..e69de29 --- /dev/null +++ b/login/app/sprinkles/core/schema/.gitkeep diff --git a/login/app/sprinkles/core/src/Alert/AlertStream.php b/login/app/sprinkles/core/src/Alert/AlertStream.php new file mode 100755 index 0000000..3946cbf --- /dev/null +++ b/login/app/sprinkles/core/src/Alert/AlertStream.php @@ -0,0 +1,144 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Alert; + +use UserFrosting\Fortress\ServerSideValidator; + +/** + * AlertStream Class + * + * Implements an alert stream for use between HTTP requests, with i18n support via the MessageTranslator class + * + * @author Alex Weissman (https://alexanderweissman.com) + * @see http://www.userfrosting.com/components/#messages + */ +abstract class AlertStream +{ + + /** + * @var string + */ + protected $messagesKey; + + /** + * @var UserFrosting\I18n\MessageTranslator|null + */ + protected $messageTranslator = null; + + /** + * Create a new message stream. + */ + public function __construct($messagesKey, $translator = null) + { + $this->messagesKey = $messagesKey; + + $this->setTranslator($translator); + } + + /** + * Set the translator to be used for all message streams. Must be done before `addMessageTranslated` can be used. + * + * @param UserFrosting\I18n\MessageTranslator $translator A MessageTranslator to be used to translate messages when added via `addMessageTranslated`. + */ + public function setTranslator($translator) + { + $this->messageTranslator = $translator; + return $this; + } + + /** + * Adds a raw text message to the cache message stream. + * + * @param string $type The type of message, indicating how it will be styled when outputted. Should be set to "success", "danger", "warning", or "info". + * @param string $message The message to be added to the message stream. + * @return MessageStream this MessageStream object. + */ + public function addMessage($type, $message) + { + $messages = $this->messages(); + $messages[] = array( + "type" => $type, + "message" => $message + ); + $this->saveMessages($messages); + return $this; + } + + /** + * Adds a text message to the cache message stream, translated into the currently selected language. + * + * @param string $type The type of message, indicating how it will be styled when outputted. Should be set to "success", "danger", "warning", or "info". + * @param string $messageId The message id for the message to be added to the message stream. + * @param array[string] $placeholders An optional hash of placeholder names => placeholder values to substitute into the translated message. + * @return MessageStream this MessageStream object. + */ + public function addMessageTranslated($type, $messageId, $placeholders = array()) + { + if (!$this->messageTranslator){ + throw new \RuntimeException("No translator has been set! Please call MessageStream::setTranslator first."); + } + + $message = $this->messageTranslator->translate($messageId, $placeholders); + return $this->addMessage($type, $message); + } + + /** + * Get the messages and then clear the message stream. + * This function does the same thing as `messages()`, except that it also clears all messages afterwards. + * This is useful, because typically we don't want to view the same messages more than once. + * + * @return array An array of messages, each of which is itself an array containing "type" and "message" fields. + */ + public function getAndClearMessages() + { + $messages = $this->messages(); + $this->resetMessageStream(); + return $messages; + } + + /** + * Add error messages from a ServerSideValidator object to the message stream. + * + * @param ServerSideValidator $validator + */ + public function addValidationErrors(ServerSideValidator $validator) + { + foreach ($validator->errors() as $idx => $field) { + foreach($field as $eidx => $error) { + $this->addMessage("danger", $error); + } + } + } + + /** + * Return the translator for this message stream. + * + * @return MessageTranslator The translator for this message stream. + */ + public function translator() + { + return $this->messageTranslator; + } + + /** + * Get the messages from this message stream. + * + * @return array An array of messages, each of which is itself an array containing "type" and "message" fields. + */ + abstract public function messages(); + + /** + * Clear all messages from this message stream. + */ + abstract public function resetMessageStream(); + + /** + * Save messages to the stream + */ + abstract protected function saveMessages($message); +} diff --git a/login/app/sprinkles/core/src/Alert/CacheAlertStream.php b/login/app/sprinkles/core/src/Alert/CacheAlertStream.php new file mode 100755 index 0000000..1fd5131 --- /dev/null +++ b/login/app/sprinkles/core/src/Alert/CacheAlertStream.php @@ -0,0 +1,84 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Alert; + +use Illuminate\Cache\Repository as Cache; +use UserFrosting\I18n\MessageTranslator; +use UserFrosting\Support\Repository\Repository; + +/** + * CacheAlertStream Class + * Implements a message stream for use between HTTP requests, with i18n + * support via the MessageTranslator class using the cache system to store + * the alerts. Note that the tags are added each time instead of the + * constructor since the session_id can change when the user logs in or out + * + * @author Louis Charette + */ +class CacheAlertStream extends AlertStream +{ + /** + * @var Cache Object We use the cache object so that added messages will automatically appear in the cache. + */ + protected $cache; + + /** + * @var Repository Object We use the cache object so that added messages will automatically appear in the cache. + */ + protected $config; + + /** + * Create a new message stream. + * + * @param string $messagesKey Store the messages under this key + * @param MessageTranslator|null $translator + * @param Cache $cache + * @param Repository $config + */ + public function __construct($messagesKey, MessageTranslator $translator = null, Cache $cache, Repository $config) + { + $this->cache = $cache; + $this->config = $config; + parent::__construct($messagesKey, $translator); + } + + /** + * Get the messages from this message stream. + * + * @return array An array of messages, each of which is itself an array containing 'type' and 'message' fields. + */ + public function messages() + { + if ($this->cache->tags('_s'.session_id())->has($this->messagesKey)) { + return $this->cache->tags('_s'.session_id())->get($this->messagesKey) ?: []; + } else { + return []; + } + } + + /** + * Clear all messages from this message stream. + * + * @return void + */ + public function resetMessageStream() + { + $this->cache->tags('_s'.session_id())->forget($this->messagesKey); + } + + /** + * Save messages to the stream + * + * @param string $messages The message + * @return void + */ + protected function saveMessages($messages) + { + $this->cache->tags('_s'.session_id())->forever($this->messagesKey, $messages); + } +} diff --git a/login/app/sprinkles/core/src/Alert/SessionAlertStream.php b/login/app/sprinkles/core/src/Alert/SessionAlertStream.php new file mode 100755 index 0000000..8b4604b --- /dev/null +++ b/login/app/sprinkles/core/src/Alert/SessionAlertStream.php @@ -0,0 +1,70 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Alert; + +use UserFrosting\I18n\MessageTranslator; +use UserFrosting\Session\Session; + +/** + * SessionAlertStream Class + * Implements a message stream for use between HTTP requests, with i18n support via the MessageTranslator class + * Using the session storage to store the alerts + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class SessionAlertStream extends AlertStream +{ + /** + * @var Session We use the session object so that added messages will automatically appear in the session. + */ + protected $session; + + /** + * Create a new message stream. + * + * @param string $messagesKey Store the messages under this key + * @param MessageTranslator|null $translator + * @param Session $session + */ + public function __construct($messagesKey, MessageTranslator $translator = null, Session $session) + { + $this->session = $session; + parent::__construct($messagesKey, $translator); + } + + /** + * Get the messages from this message stream. + * + * @return array An array of messages, each of which is itself an array containing "type" and "message" fields. + */ + public function messages() + { + return $this->session[$this->messagesKey] ?: []; + } + + /** + * Clear all messages from this message stream. + * + * @return void + */ + public function resetMessageStream() + { + $this->session[$this->messagesKey] = []; + } + + /** + * Save messages to the stream + * + * @param string $messages The message + * @return void + */ + protected function saveMessages($messages) + { + $this->session[$this->messagesKey] = $messages; + } +} diff --git a/login/app/sprinkles/core/src/Controller/CoreController.php b/login/app/sprinkles/core/src/Controller/CoreController.php new file mode 100755 index 0000000..0dd8165 --- /dev/null +++ b/login/app/sprinkles/core/src/Controller/CoreController.php @@ -0,0 +1,95 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Controller; + +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; +use Slim\Exception\NotFoundException as NotFoundException; + +/** + * CoreController Class + * + * Implements some common sitewide routes. + * @author Alex Weissman (https://alexanderweissman.com) + * @see http://www.userfrosting.com/navigating/#structure + */ +class CoreController extends SimpleController +{ + /** + * Renders the default home page for UserFrosting. + * + * By default, this is the page that non-authenticated users will first see when they navigate to your website's root. + * Request type: GET + */ + public function pageIndex($request, $response, $args) + { + return $this->ci->view->render($response, 'pages/index.html.twig'); + } + + /** + * Renders a sample "about" page for UserFrosting. + * + * Request type: GET + */ + public function pageAbout($request, $response, $args) + { + return $this->ci->view->render($response, 'pages/about.html.twig'); + } + + /** + * Renders terms of service page. + * + * Request type: GET + */ + public function pageLegal($request, $response, $args) + { + return $this->ci->view->render($response, 'pages/legal.html.twig'); + } + + /** + * Renders privacy page. + * + * Request type: GET + */ + public function pagePrivacy($request, $response, $args) + { + return $this->ci->view->render($response, 'pages/privacy.html.twig'); + } + + /** + * Render the alert stream as a JSON object. + * + * The alert stream contains messages which have been generated by calls to `MessageStream::addMessage` and `MessageStream::addMessageTranslated`. + * Request type: GET + */ + public function jsonAlerts($request, $response, $args) + { + return $response->withJson($this->ci->alerts->getAndClearMessages()); + } + + /** + * Handle all requests for raw assets. + * Request type: GET + */ + public function getAsset($request, $response, $args) + { + // By starting this service, we ensure that the timezone gets set. + $config = $this->ci->config; + + $assetLoader = $this->ci->assetLoader; + + if (!$assetLoader->loadAsset($args['url'])) { + throw new NotFoundException($request, $response); + } + + return $response + ->withHeader('Content-Type', $assetLoader->getType()) + ->withHeader('Content-Length', $assetLoader->getLength()) + ->write($assetLoader->getContent()); + } +} diff --git a/login/app/sprinkles/core/src/Controller/SimpleController.php b/login/app/sprinkles/core/src/Controller/SimpleController.php new file mode 100755 index 0000000..b0fc152 --- /dev/null +++ b/login/app/sprinkles/core/src/Controller/SimpleController.php @@ -0,0 +1,36 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Controller; + +use Interop\Container\ContainerInterface; + +/** + * SimpleController Class + * + * Basic controller class, that imports the entire DI container for easy access to services. + * Your controller classes may extend this controller class. + * @author Alex Weissman (https://alexanderweissman.com) + * @see http://www.userfrosting.com/navigating/#structure + */ +class SimpleController +{ + /** + * @var ContainerInterface The global container object, which holds all your services. + */ + protected $ci; + + /** + * Constructor. + * + * @param ContainerInterface $ci The global container object, which holds all your services. + */ + public function __construct(ContainerInterface $ci) + { + $this->ci = $ci; + } +} diff --git a/login/app/sprinkles/core/src/Core.php b/login/app/sprinkles/core/src/Core.php new file mode 100755 index 0000000..d7e1dcb --- /dev/null +++ b/login/app/sprinkles/core/src/Core.php @@ -0,0 +1,121 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core; + +use RocketTheme\Toolbox\Event\Event; +use UserFrosting\Sprinkle\Core\Database\Models\Model; +use UserFrosting\Sprinkle\Core\Util\EnvironmentInfo; +use UserFrosting\Sprinkle\Core\Util\ShutdownHandler; +use UserFrosting\System\Sprinkle\Sprinkle; + +/** + * Bootstrapper class for the core sprinkle. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class Core extends Sprinkle +{ + /** + * Defines which events in the UF lifecycle our Sprinkle should hook into. + */ + public static function getSubscribedEvents() + { + return [ + 'onSprinklesInitialized' => ['onSprinklesInitialized', 0], + 'onSprinklesRegisterServices' => ['onSprinklesRegisterServices', 0], + 'onAddGlobalMiddleware' => ['onAddGlobalMiddleware', 0] + ]; + } + + /** + * Set static references to DI container in necessary classes. + */ + public function onSprinklesInitialized() + { + // Set container for data model + Model::$ci = $this->ci; + + // Set container for environment info class + EnvironmentInfo::$ci = $this->ci; + } + + /** + * Get shutdownHandler set up. This needs to be constructed explicitly because it's invoked natively by PHP. + */ + public function onSprinklesRegisterServices() + { + // Set up any global PHP settings from the config service. + $config = $this->ci->config; + + // Display PHP fatal errors natively. + if (isset($config['php.display_errors_native'])) { + ini_set('display_errors', $config['php.display_errors_native']); + } + + // Log PHP fatal errors + if (isset($config['php.log_errors'])) { + ini_set('log_errors', $config['php.log_errors']); + } + + // Configure error-reporting level + if (isset($config['php.error_reporting'])) { + error_reporting($config['php.error_reporting']); + } + + // Configure time zone + if (isset($config['php.timezone'])) { + date_default_timezone_set($config['php.timezone']); + } + + // Determine if error display is enabled in the shutdown handler. + $displayErrors = false; + if (in_array(strtolower($config['php.display_errors']), [ + '1', + 'on', + 'true', + 'yes' + ])) { + $displayErrors = true; + } + + $sh = new ShutdownHandler($this->ci, $displayErrors); + $sh->register(); + } + + /** + * Add CSRF middleware. + */ + public function onAddGlobalMiddleware(Event $event) + { + $request = $this->ci->request; + $path = $request->getUri()->getPath(); + $method = $request->getMethod(); + + // Normalize path to always have a leading slash + $path = '/' . ltrim($path, '/'); + // Normalize method to uppercase + $method = strtoupper($method); + + $csrfBlacklist = $this->ci->config['csrf.blacklist']; + $isBlacklisted = false; + + // Go through the blacklist and determine if the path and method match any of the blacklist entries. + foreach ($csrfBlacklist as $pattern => $methods) { + $methods = array_map('strtoupper', (array) $methods); + if (in_array($method, $methods) && $pattern != '' && preg_match('~' . $pattern . '~', $path)) { + $isBlacklisted = true; + break; + } + } + + if (!$path || !$isBlacklisted) { + $app = $event->getApp(); + $app->add($this->ci->csrf); + } + } +} diff --git a/login/app/sprinkles/core/src/Database/Builder.php b/login/app/sprinkles/core/src/Database/Builder.php new file mode 100755 index 0000000..8e27b7c --- /dev/null +++ b/login/app/sprinkles/core/src/Database/Builder.php @@ -0,0 +1,210 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Database; + +use Illuminate\Database\Capsule\Manager as DB; +use Illuminate\Database\Query\Builder as LaravelBuilder; + +/** + * UFBuilder Class + * + * The base Eloquent data model, from which all UserFrosting data classes extend. + * @author Alex Weissman (https://alexanderweissman.com) + */ +class Builder extends LaravelBuilder +{ + protected $excludedColumns = null; + + /** + * Perform a "begins with" pattern match on a specified column in a query. + * + * @param $query + * @param $field string The column to match + * @param $value string The value to match + */ + public function beginsWith($field, $value) + { + return $this->where($field, 'LIKE', "$value%"); + } + + /** + * Perform an "ends with" pattern match on a specified column in a query. + * + * @param $query + * @param $field string The column to match + * @param $value string The value to match + */ + public function endsWith($field, $value) + { + return $this->where($field, 'LIKE', "%$value"); + } + + /** + * Add columns to be excluded from the query. + * + * @param $value array|string The column(s) to exclude + * @return $this + */ + public function exclude($column) + { + $column = is_array($column) ? $column : func_get_args(); + + $this->excludedColumns = array_merge((array) $this->excludedColumns, $column); + + return $this; + } + + /** + * Perform a pattern match on a specified column in a query. + * @param $query + * @param $field string The column to match + * @param $value string The value to match + */ + public function like($field, $value) + { + return $this->where($field, 'LIKE', "%$value%"); + } + + /** + * Perform a pattern match on a specified column in a query. + * @param $query + * @param $field string The column to match + * @param $value string The value to match + */ + public function orLike($field, $value) + { + return $this->orWhere($field, 'LIKE', "%$value%"); + } + + /** + * Execute the query as a "select" statement. + * + * @param array $columns + * @return \Illuminate\Support\Collection + */ + public function get($columns = ['*']) + { + $original = $this->columns; + + if (is_null($original)) { + $this->columns = $columns; + } + + // Exclude any explicitly excluded columns + if (!is_null($this->excludedColumns)) { + $this->removeExcludedSelectColumns(); + } + + $results = $this->processor->processSelect($this, $this->runSelect()); + + $this->columns = $original; + + return collect($results); + } + + /** + * Remove excluded columns from the select column list. + */ + protected function removeExcludedSelectColumns() + { + // Convert current column list and excluded column list to fully-qualified list + $this->columns = $this->convertColumnsToFullyQualified($this->columns); + $excludedColumns = $this->convertColumnsToFullyQualified($this->excludedColumns); + + // Remove any explicitly referenced excludable columns + $this->columns = array_diff($this->columns, $excludedColumns); + + // Replace any remaining wildcard columns (*, table.*, etc) with a list + // of fully-qualified column names + $this->columns = $this->replaceWildcardColumns($this->columns); + + $this->columns = array_diff($this->columns, $excludedColumns); + } + + /** + * Find any wildcard columns ('*'), remove it from the column list and replace with an explicit list of columns. + * + * @param array $columns + * @return array + */ + protected function replaceWildcardColumns(array $columns) + { + $wildcardTables = $this->findWildcardTables($columns); + + foreach ($wildcardTables as $wildColumn => $table) { + $schemaColumns = $this->getQualifiedColumnNames($table); + + // Remove the `*` or `.*` column and replace with the individual schema columns + $columns = array_diff($columns, [$wildColumn]); + $columns = array_merge($columns, $schemaColumns); + } + + return $columns; + } + + /** + * Return a list of wildcard columns from the list of columns, mapping columns to their corresponding tables. + * + * @param array $columns + * @return array + */ + protected function findWildcardTables(array $columns) + { + $tables = []; + + foreach ($columns as $column) { + if ($column == '*') { + $tables[$column] = $this->from; + continue; + } + + if (substr($column, -1) == '*') { + $tableName = explode('.', $column)[0]; + if ($tableName) { + $tables[$column] = $tableName; + } + } + } + + return $tables; + } + + /** + * Gets the fully qualified column names for a specified table. + * + * @param string $table + * @return array + */ + protected function getQualifiedColumnNames($table = null) + { + $schema = $this->getConnection()->getSchemaBuilder(); + + return $this->convertColumnsToFullyQualified($schema->getColumnListing($table), $table); + } + + /** + * Fully qualify any unqualified columns in a list with this builder's table name. + * + * @param array $columns + * @return array + */ + protected function convertColumnsToFullyQualified($columns, $table = null) + { + if (is_null($table)) { + $table = $this->from; + } + + array_walk($columns, function (&$item, $key) use ($table) { + if (strpos($item, '.') === false) { + $item = "$table.$item"; + } + }); + + return $columns; + } +} diff --git a/login/app/sprinkles/core/src/Database/DatabaseInvalidException.php b/login/app/sprinkles/core/src/Database/DatabaseInvalidException.php new file mode 100755 index 0000000..08f8a31 --- /dev/null +++ b/login/app/sprinkles/core/src/Database/DatabaseInvalidException.php @@ -0,0 +1,20 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Database; + +use UserFrosting\Support\Exception\ForbiddenException; + +/** + * Invalid database exception. Used when the database cannot be accessed. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class DatabaseInvalidException extends ForbiddenException +{ + protected $defaultMessage = 'DB_INVALID'; +} diff --git a/login/app/sprinkles/core/src/Database/Migrations/v400/SessionsTable.php b/login/app/sprinkles/core/src/Database/Migrations/v400/SessionsTable.php new file mode 100755 index 0000000..ac86ceb --- /dev/null +++ b/login/app/sprinkles/core/src/Database/Migrations/v400/SessionsTable.php @@ -0,0 +1,48 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Database\Migrations\v400; + +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Schema\Builder; +use UserFrosting\System\Bakery\Migration; + +/** + * Sessions table migration + * Version 4.0.0 + * + * See https://laravel.com/docs/5.4/migrations#tables + * @extends Migration + * @author Alex Weissman (https://alexanderweissman.com) + */ +class SessionsTable extends Migration +{ + /** + * {@inheritDoc} + */ + public function up() + { + if (!$this->schema->hasTable('sessions')) { + $this->schema->create('sessions', function (Blueprint $table) { + $table->string('id')->unique(); + $table->integer('user_id')->nullable(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->text('payload'); + $table->integer('last_activity'); + }); + } + } + + /** + * {@inheritDoc} + */ + public function down() + { + $this->schema->drop('sessions'); + } +} diff --git a/login/app/sprinkles/core/src/Database/Migrations/v400/ThrottlesTable.php b/login/app/sprinkles/core/src/Database/Migrations/v400/ThrottlesTable.php new file mode 100755 index 0000000..1c742f7 --- /dev/null +++ b/login/app/sprinkles/core/src/Database/Migrations/v400/ThrottlesTable.php @@ -0,0 +1,52 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Database\Migrations\v400; + +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Schema\Builder; +use UserFrosting\System\Bakery\Migration; + +/** + * Throttles table migration + * Version 4.0.0 + * + * @extends Migration + * @author Alex Weissman (https://alexanderweissman.com) + */ +class ThrottlesTable extends Migration +{ + /** + * {@inheritDoc} + */ + public function up() + { + if (!$this->schema->hasTable('throttles')) { + $this->schema->create('throttles', function (Blueprint $table) { + $table->increments('id'); + $table->string('type'); + $table->string('ip')->nullable(); + $table->text('request_data')->nullable(); + $table->timestamps(); + + $table->engine = 'InnoDB'; + $table->collation = 'utf8_unicode_ci'; + $table->charset = 'utf8'; + $table->index('type'); + $table->index('ip'); + }); + } + } + + /** + * {@inheritDoc} + */ + public function down() + { + $this->schema->drop('throttles'); + } +} diff --git a/login/app/sprinkles/core/src/Database/Models/Concerns/HasRelationships.php b/login/app/sprinkles/core/src/Database/Models/Concerns/HasRelationships.php new file mode 100755 index 0000000..4fe9a30 --- /dev/null +++ b/login/app/sprinkles/core/src/Database/Models/Concerns/HasRelationships.php @@ -0,0 +1,278 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Database\Models\Concerns; + +use Illuminate\Support\Arr; +use Illuminate\Support\Str; + +use Illuminate\Database\Eloquent\Relations\MorphMany; +use Illuminate\Database\Eloquent\Relations\MorphToMany; + +use UserFrosting\Sprinkle\Core\Database\Relations\BelongsToManyConstrained; +use UserFrosting\Sprinkle\Core\Database\Relations\BelongsToManyThrough; +use UserFrosting\Sprinkle\Core\Database\Relations\BelongsToManyUnique; +use UserFrosting\Sprinkle\Core\Database\Relations\HasManySyncable; +use UserFrosting\Sprinkle\Core\Database\Relations\MorphManySyncable; +use UserFrosting\Sprinkle\Core\Database\Relations\MorphToManyUnique; + +/** + * HasRelationships trait + * + * Extends Laravel's Model class to add some additional relationships. + * @author Alex Weissman (https://alexanderweissman.com) + */ +trait HasRelationships +{ + /** + * The many to many relationship methods. + * + * @var array + */ + public static $manyMethodsExtended = ['belongsToMany', 'morphToMany', 'morphedByMany', 'morphToManyUnique']; + + /** + * Overrides the default Eloquent hasMany relationship to return a HasManySyncable. + * + * {@inheritDoc} + * @return \UserFrosting\Sprinkle\Core\Database\Relations\HasManySyncable + */ + public function hasMany($related, $foreignKey = null, $localKey = null) + { + $instance = $this->newRelatedInstance($related); + + $foreignKey = $foreignKey ?: $this->getForeignKey(); + + $localKey = $localKey ?: $this->getKeyName(); + + return new HasManySyncable( + $instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey + ); + } + + /** + * Overrides the default Eloquent morphMany relationship to return a MorphManySyncable. + * + * {@inheritDoc} + * @return \UserFrosting\Sprinkle\Core\Database\Relations\MorphManySyncable + */ + public function morphMany($related, $name, $type = null, $id = null, $localKey = null) + { + $instance = $this->newRelatedInstance($related); + + // Here we will gather up the morph type and ID for the relationship so that we + // can properly query the intermediate table of a relation. Finally, we will + // get the table and create the relationship instances for the developers. + list($type, $id) = $this->getMorphs($name, $type, $id); + $table = $instance->getTable(); + $localKey = $localKey ?: $this->getKeyName(); + + return new MorphManySyncable($instance->newQuery(), $this, $table.'.'.$type, $table.'.'.$id, $localKey); + } + + /** + * Define a many-to-many 'through' relationship. + * This is basically hasManyThrough for many-to-many relationships. + * + * @param string $related + * @param string $through + * @param string $firstJoiningTable + * @param string $firstForeignKey + * @param string $firstRelatedKey + * @param string $secondJoiningTable + * @param string $secondForeignKey + * @param string $secondRelatedKey + * @param string $throughRelation + * @param string $relation + * @return \UserFrosting\Sprinkle\Core\Database\Relations\BelongsToManyThrough + */ + public function belongsToManyThrough( + $related, + $through, + $firstJoiningTable = null, + $firstForeignKey = null, + $firstRelatedKey = null, + $secondJoiningTable = null, + $secondForeignKey = null, + $secondRelatedKey = null, + $throughRelation = null, + $relation = null + ) + { + // If no relationship name was passed, we will pull backtraces to get the + // name of the calling function. We will use that function name as the + // title of this relation since that is a great convention to apply. + if (is_null($relation)) { + $relation = $this->guessBelongsToManyRelation(); + } + + // Create models for through and related + $through = new $through; + $related = $this->newRelatedInstance($related); + + if (is_null($throughRelation)) { + $throughRelation = $through->getTable(); + } + + // If no table names were provided, we can guess it by concatenating the parent + // and through table names. The two model names are transformed to snake case + // from their default CamelCase also. + if (is_null($firstJoiningTable)) { + $firstJoiningTable = $this->joiningTable($through); + } + + if (is_null($secondJoiningTable)) { + $secondJoiningTable = $through->joiningTable($related); + } + + $firstForeignKey = $firstForeignKey ?: $this->getForeignKey(); + $firstRelatedKey = $firstRelatedKey ?: $through->getForeignKey(); + $secondForeignKey = $secondForeignKey ?: $through->getForeignKey(); + $secondRelatedKey = $secondRelatedKey ?: $related->getForeignKey(); + + // This relationship maps the top model (this) to the through model. + $intermediateRelationship = $this->belongsToMany($through, $firstJoiningTable, $firstForeignKey, $firstRelatedKey, $throughRelation) + ->withPivot($firstForeignKey); + + // Now we set up the relationship with the related model. + $query = new BelongsToManyThrough( + $related->newQuery(), $this, $intermediateRelationship, $secondJoiningTable, $secondForeignKey, $secondRelatedKey, $relation + ); + + return $query; + } + + /** + * Define a unique many-to-many relationship. Similar to a regular many-to-many relationship, but removes duplicate child objects. + * Can also be used to implement ternary relationships. + * + * {@inheritDoc} + * @return \UserFrosting\Sprinkle\Core\Database\Relations\BelongsToManyUnique + */ + public function belongsToManyUnique($related, $table = null, $foreignKey = null, $relatedKey = null, $relation = null) + { + // If no relationship name was passed, we will pull backtraces to get the + // name of the calling function. We will use that function name as the + // title of this relation since that is a great convention to apply. + if (is_null($relation)) { + $relation = $this->guessBelongsToManyRelation(); + } + + // First, we'll need to determine the foreign key and "other key" for the + // relationship. Once we have determined the keys we'll make the query + // instances as well as the relationship instances we need for this. + $instance = $this->newRelatedInstance($related); + + $foreignKey = $foreignKey ?: $this->getForeignKey(); + + $relatedKey = $relatedKey ?: $instance->getForeignKey(); + + // If no table name was provided, we can guess it by concatenating the two + // models using underscores in alphabetical order. The two model names + // are transformed to snake case from their default CamelCase also. + if (is_null($table)) { + $table = $this->joiningTable($related); + } + + return new BelongsToManyUnique( + $instance->newQuery(), $this, $table, $foreignKey, $relatedKey, $relation + ); + } + + /** + * Define a unique morphs-to-many relationship. Similar to a regular morphs-to-many relationship, but removes duplicate child objects. + * + * {@inheritDoc} + * @return \UserFrosting\Sprinkle\Core\Database\Relations\MorphToManyUnique + */ + public function morphToManyUnique($related, $name, $table = null, $foreignKey = null, $otherKey = null, $inverse = false) + { + $caller = $this->getBelongsToManyCaller(); + + // First, we will need to determine the foreign key and "other key" for the + // relationship. Once we have determined the keys we will make the query + // instances, as well as the relationship instances we need for these. + $foreignKey = $foreignKey ?: $name.'_id'; + + $instance = new $related; + + $otherKey = $otherKey ?: $instance->getForeignKey(); + + // Now we're ready to create a new query builder for this related model and + // the relationship instances for this relation. This relations will set + // appropriate query constraints then entirely manages the hydrations. + $query = $instance->newQuery(); + + $table = $table ?: Str::plural($name); + + return new MorphToManyUnique( + $query, $this, $name, $table, $foreignKey, + $otherKey, $caller, $inverse + ); + } + + /** + * Define a constrained many-to-many relationship. + * This is similar to a regular many-to-many, but constrains the child results to match an additional constraint key in the parent object. + * This has been superseded by the belongsToManyUnique relationship's `withTernary` method since 4.1.7. + * + * @deprecated since 4.1.6 + * @param string $related + * @param string $constraintKey + * @param string $table + * @param string $foreignKey + * @param string $relatedKey + * @param string $relation + * @return \UserFrosting\Sprinkle\Core\Database\Relations\BelongsToManyConstrained + */ + public function belongsToManyConstrained($related, $constraintKey, $table = null, $foreignKey = null, $relatedKey = null, $relation = null) + { + // If no relationship name was passed, we will pull backtraces to get the + // name of the calling function. We will use that function name as the + // title of this relation since that is a great convention to apply. + if (is_null($relation)) { + $relation = $this->guessBelongsToManyRelation(); + } + + // First, we'll need to determine the foreign key and "other key" for the + // relationship. Once we have determined the keys we'll make the query + // instances as well as the relationship instances we need for this. + $instance = $this->newRelatedInstance($related); + + $foreignKey = $foreignKey ?: $this->getForeignKey(); + + $relatedKey = $relatedKey ?: $instance->getForeignKey(); + + // If no table name was provided, we can guess it by concatenating the two + // models using underscores in alphabetical order. The two model names + // are transformed to snake case from their default CamelCase also. + if (is_null($table)) { + $table = $this->joiningTable($related); + } + + return new BelongsToManyConstrained( + $instance->newQuery(), $this, $constraintKey, $table, $foreignKey, $relatedKey, $relation + ); + } + + /** + * Get the relationship name of the belongs to many. + * + * @return string + */ + protected function getBelongsToManyCaller() + { + $self = __FUNCTION__; + + $caller = Arr::first(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), function ($key, $trace) use ($self) { + $caller = $trace['function']; + return ! in_array($caller, HasRelationships::$manyMethodsExtended) && $caller != $self; + }); + + return ! is_null($caller) ? $caller['function'] : null; + } +} diff --git a/login/app/sprinkles/core/src/Database/Models/Model.php b/login/app/sprinkles/core/src/Database/Models/Model.php new file mode 100755 index 0000000..1c18c2c --- /dev/null +++ b/login/app/sprinkles/core/src/Database/Models/Model.php @@ -0,0 +1,140 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Database\Models; + +use Illuminate\Database\Capsule\Manager as DB; +use Illuminate\Database\Eloquent\Model as LaravelModel; +use UserFrosting\Sprinkle\Core\Database\Models\Concerns\HasRelationships; + +/** + * Model Class + * + * UserFrosting's base data model, from which all UserFrosting data classes extend. + * @author Alex Weissman (https://alexanderweissman.com) + */ +abstract class Model extends LaravelModel +{ + use HasRelationships; + + /** + * @var ContainerInterface The DI container for your application. + */ + public static $ci; + + /** + * @var bool Disable timestamps for now. + */ + public $timestamps = false; + + public function __construct(array $attributes = []) + { + // Hacky way to force the DB service to load before attempting to use the model + static::$ci['db']; + + parent::__construct($attributes); + } + + /** + * Determine if an attribute exists on the model - even if it is null. + * + * @param string $key + * @return bool + */ + public function attributeExists($key) + { + return array_key_exists($key, $this->attributes); + } + + /** + * Determines whether a model exists by checking a unique column, including checking soft-deleted records + * + * @param mixed $value + * @param string $identifier + * @param bool $checkDeleted set to true to include soft-deleted records + * @return \UserFrosting\Sprinkle\Core\Database\Models\Model|null + */ + public static function findUnique($value, $identifier, $checkDeleted = true) + { + $query = static::where($identifier, $value); + + if ($checkDeleted) { + $query = $query->withTrashed(); + } + + return $query->first(); + } + + /** + * Determine if an relation exists on the model - even if it is null. + * + * @param string $key + * @return bool + */ + public function relationExists($key) + { + return array_key_exists($key, $this->relations); + } + + /** + * Store the object in the DB, creating a new row if one doesn't already exist. + * + * Calls save(), then returns the id of the new record in the database. + * @return int the id of this object. + */ + public function store() + { + $this->save(); + + // Store function should always return the id of the object + return $this->id; + } + + /** + * Overrides Laravel's base Model to return our custom query builder object. + * + * @return \UserFrosting\Sprinkles\Core\Database\Builder + */ + protected function newBaseQueryBuilder() + { + /** @var UserFrosting\Sprinkle\Core\Util\ClassMapper $classMapper */ + $classMapper = static::$ci->classMapper; + + $connection = $this->getConnection(); + + return $classMapper->createInstance( + 'query_builder', + $connection, + $connection->getQueryGrammar(), + $connection->getPostProcessor() + ); + } + + /** + * Get the properties of this object as an associative array. Alias for toArray(). + * + * @deprecated since 4.1.8 There is no point in having this alias. + * @return array + */ + public function export() + { + return $this->toArray(); + } + + /** + * For raw array fetching. Must be static, otherwise PHP gets confused about where to find $table. + * + * @deprecated since 4.1.8 setFetchMode is no longer available as of Laravel 5.4. + * @link https://github.com/laravel/framework/issues/17728 + */ + public static function queryBuilder() + { + // Set query builder to fetch result sets as associative arrays (instead of creating stdClass objects) + DB::connection()->setFetchMode(\PDO::FETCH_ASSOC); + return DB::table(static::$table); + } +} diff --git a/login/app/sprinkles/core/src/Database/Models/Throttle.php b/login/app/sprinkles/core/src/Database/Models/Throttle.php new file mode 100755 index 0000000..d13a7c1 --- /dev/null +++ b/login/app/sprinkles/core/src/Database/Models/Throttle.php @@ -0,0 +1,36 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Database\Models; + +/** + * Throttle Class + * + * Represents a throttleable request from a user agent. + * @author Alex Weissman (https://alexanderweissman.com) + * @property string type + * @property string ip + * @property string request_data + */ +class Throttle extends Model +{ + /** + * @var string The name of the table for the current model. + */ + protected $table = "throttles"; + + protected $fillable = [ + "type", + "ip", + "request_data" + ]; + + /** + * @var bool Enable timestamps for Throttles. + */ + public $timestamps = true; +} diff --git a/login/app/sprinkles/core/src/Database/Relations/BelongsToManyConstrained.php b/login/app/sprinkles/core/src/Database/Relations/BelongsToManyConstrained.php new file mode 100755 index 0000000..d652b56 --- /dev/null +++ b/login/app/sprinkles/core/src/Database/Relations/BelongsToManyConstrained.php @@ -0,0 +1,122 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Database\Relations; + +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Collection; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; + +/** + * A BelongsToMany relationship that constrains on the value of an additional foreign key in the pivot table. + * This has been superseded by the BelongsToTernary relationship since 4.1.6. + * + * @deprecated since 4.1.6 + * @author Alex Weissman (https://alexanderweissman.com) + * @link https://github.com/laravel/framework/blob/5.4/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php + */ +class BelongsToManyConstrained extends BelongsToMany +{ + /** + * @var The pivot foreign key on which to constrain the result sets for this relation. + */ + protected $constraintKey; + + /** + * Create a new belongs to many constrained relationship instance. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Model $parent + * @param string $constraintKey + * @param string $table + * @param string $foreignKey + * @param string $relatedKey + * @param string $relationName + * @return void + */ + public function __construct(Builder $query, Model $parent, $constraintKey, $table, $foreignKey, $relatedKey, $relationName = null) + { + $this->constraintKey = $constraintKey; + parent::__construct($query, $parent, $table, $foreignKey, $relatedKey, $relationName); + } + + /** + * Set the constraints for an eager load of the relation. + * + * @param array $models + * @return void + */ + public function addEagerConstraints(array $models) + { + // To make the query more efficient, we only bother querying related models if their pivot key value + // matches the pivot key value of one of the parent models. + $pivotKeys = $this->getPivotKeys($models, $this->constraintKey); + $this->query->whereIn($this->getQualifiedForeignKeyName(), $this->getKeys($models)) + ->whereIn($this->constraintKey, $pivotKeys); + } + + /** + * Gets a list of unique pivot key values from an array of models. + */ + protected function getPivotKeys(array $models, $pivotKey) + { + $pivotKeys = []; + foreach ($models as $model) { + $pivotKeys[] = $model->getRelation('pivot')->{$pivotKey}; + } + return array_unique($pivotKeys); + } + + /** + * Match the eagerly loaded results to their parents, constraining the results by matching the values of $constraintKey + * in the parent object to the child objects. + * + * @link Called in https://github.com/laravel/framework/blob/2f4135d8db5ded851d1f4f611124c53b768a3c08/src/Illuminate/Database/Eloquent/Builder.php + * @param array $models + * @param \Illuminate\Database\Eloquent\Collection $results + * @param string $relation + * @return array + */ + public function match(array $models, Collection $results, $relation) + { + $dictionary = $this->buildDictionary($results); + + // Once we have an array dictionary of child objects we can easily match the + // children back to their parent using the dictionary and the keys on the + // the parent models. Then we will return the hydrated models back out. + foreach ($models as $model) { + $pivotValue = $model->getRelation('pivot')->{$this->constraintKey}; + if (isset($dictionary[$key = $model->getKey()])) { + // Only match children if their pivot key value matches that of the parent model + $items = $this->findMatchingPivots($dictionary[$key], $pivotValue); + $model->setRelation( + $relation, $this->related->newCollection($items) + ); + } + } + + return $models; + } + + /** + * Filter an array of models, only taking models whose $constraintKey value matches $pivotValue. + * + * @param mixed $pivotValue + * @return array + */ + protected function findMatchingPivots($items, $pivotValue) + { + $result = []; + foreach ($items as $item) { + if ($item->getRelation('pivot')->{$this->constraintKey} == $pivotValue) { + $result[] = $item; + } + } + return $result; + } +} diff --git a/login/app/sprinkles/core/src/Database/Relations/BelongsToManyThrough.php b/login/app/sprinkles/core/src/Database/Relations/BelongsToManyThrough.php new file mode 100755 index 0000000..33be507 --- /dev/null +++ b/login/app/sprinkles/core/src/Database/Relations/BelongsToManyThrough.php @@ -0,0 +1,232 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Database\Relations; + +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Collection; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\Relation; +use UserFrosting\Sprinkle\Core\Database\Relations\Concerns\Unique; + +/** + * A BelongsToMany relationship that queries through an additional intermediate model. + * + * @author Alex Weissman (https://alexanderweissman.com) + * @link https://github.com/laravel/framework/blob/5.4/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php + */ +class BelongsToManyThrough extends BelongsToMany +{ + use Unique; + + /** + * The relation through which we are joining. + * + * @var Relation + */ + protected $intermediateRelation; + + /** + * Create a new belongs to many relationship instance. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Model $parent + * @param \Illuminate\Database\Eloquent\Relations\Relation $intermediateRelation + * @param string $table + * @param string $foreignKey + * @param string $relatedKey + * @param string $relationName + * @return void + */ + public function __construct(Builder $query, Model $parent, Relation $intermediateRelation, $table, $foreignKey, $relatedKey, $relationName = null) + { + $this->intermediateRelation = $intermediateRelation; + + parent::__construct($query, $parent, $table, $foreignKey, $relatedKey, $relationName); + } + + /** + * Use the intermediate relationship to determine the "parent" pivot key name + * + * This is a crazy roundabout way to get the name of the intermediate relation's foreign key. + * It would be better if BelongsToMany had a simple accessor for its foreign key. + * @return string + */ + public function getParentKeyName() + { + return $this->intermediateRelation->newExistingPivot()->getForeignKey(); + } + + /** + * Get the key for comparing against the parent key in "has" query. + * + * @see \Illuminate\Database\Eloquent\Relations\BelongsToMany + * @return string + */ + public function getExistenceCompareKey() + { + return $this->intermediateRelation->getQualifiedForeignKeyName(); + } + + /** + * Add a "via" query to load the intermediate models through which the child models are related. + * + * @param string $viaRelationName + * @param callable $viaCallback + * @return $this + */ + public function withVia($viaRelationName = null, $viaCallback = null) + { + $this->tertiaryRelated = $this->intermediateRelation->getRelated(); + + // Set tertiary key and related model + $this->tertiaryKey = $this->foreignKey; + + $this->tertiaryRelationName = is_null($viaRelationName) ? $this->intermediateRelation->getRelationName() . '_via' : $viaRelationName; + + $this->tertiaryCallback = is_null($viaCallback) + ? function () { + // + } + : $viaCallback; + + return $this; + } + + /** + * Set the constraints for an eager load of the relation. + * + * @param array $models + * @return void + */ + public function addEagerConstraints(array $models) + { + // Constraint to only load models where the intermediate relation's foreign key matches the parent model + $intermediateForeignKeyName = $this->intermediateRelation->getQualifiedForeignKeyName(); + + return $this->query->whereIn($intermediateForeignKeyName, $this->getKeys($models)); + } + + /** + * Set the where clause for the relation query. + * + * @return $this + */ + protected function addWhereConstraints() + { + $parentKeyName = $this->getParentKeyName(); + + $this->query->where( + $parentKeyName, '=', $this->parent->getKey() + ); + + return $this; + } + + /** + * Match the eagerly loaded results to their parents + * + * @param array $models + * @param \Illuminate\Database\Eloquent\Collection $results + * @param string $relation + * @return array + */ + public function match(array $models, Collection $results, $relation) + { + // Build dictionary of parent (e.g. user) to related (e.g. permission) models + list($dictionary, $nestedViaDictionary) = $this->buildDictionary($results, $this->getParentKeyName()); + + // Once we have an array dictionary of child objects we can easily match the + // children back to their parent using the dictionary and the keys on the + // the parent models. Then we will return the hydrated models back out. + foreach ($models as $model) { + if (isset($dictionary[$key = $model->getKey()])) { + /** @var array */ + $items = $dictionary[$key]; + + // Eliminate any duplicates + $items = $this->related->newCollection($items)->unique(); + + // If set, match up the via models to the models in the related collection + if (!is_null($nestedViaDictionary)) { + $this->matchTertiaryModels($nestedViaDictionary[$key], $items); + } + + // Remove the tertiary pivot key from the condensed models + foreach ($items as $relatedModel) { + unset($relatedModel->pivot->{$this->foreignKey}); + } + + $model->setRelation( + $relation, $items + ); + } + } + + return $models; + } + + /** + * Unset tertiary pivots on a collection or array of models. + * + * @param \Illuminate\Database\Eloquent\Collection $models + * @return void + */ + protected function unsetTertiaryPivots(Collection $models) + { + foreach ($models as $model) { + unset($model->pivot->{$this->foreignKey}); + } + } + + /** + * Set the join clause for the relation query. + * + * @param \Illuminate\Database\Eloquent\Builder|null $query + * @return $this + */ + protected function performJoin($query = null) + { + $query = $query ?: $this->query; + + parent::performJoin($query); + + // We need to join to the intermediate table on the related model's primary + // key column with the intermediate table's foreign key for the related + // model instance. Then we can set the "where" for the parent models. + $intermediateTable = $this->intermediateRelation->getTable(); + + $key = $this->intermediateRelation->getQualifiedRelatedKeyName(); + + $query->join($intermediateTable, $key, '=', $this->getQualifiedForeignKeyName()); + + return $this; + } + + /** + * Get the pivot columns for the relation. + * + * "pivot_" is prefixed to each column for easy removal later. + * + * @return array + */ + protected function aliasedPivotColumns() + { + $defaults = [$this->foreignKey, $this->relatedKey]; + $aliasedPivotColumns = collect(array_merge($defaults, $this->pivotColumns))->map(function ($column) { + return $this->table.'.'.$column.' as pivot_'.$column; + }); + + $parentKeyName = $this->getParentKeyName(); + + // Add pivot column for the intermediate relation + $aliasedPivotColumns[] = "{$this->intermediateRelation->getQualifiedForeignKeyName()} as pivot_$parentKeyName"; + + return $aliasedPivotColumns->unique()->all(); + } +} diff --git a/login/app/sprinkles/core/src/Database/Relations/BelongsToManyUnique.php b/login/app/sprinkles/core/src/Database/Relations/BelongsToManyUnique.php new file mode 100755 index 0000000..f256f17 --- /dev/null +++ b/login/app/sprinkles/core/src/Database/Relations/BelongsToManyUnique.php @@ -0,0 +1,22 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Database\Relations; + +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use UserFrosting\Sprinkle\Core\Database\Relations\Concerns\Unique; + +/** + * A BelongsToMany relationship that reduces the related members to a unique (by primary key) set. + * + * @author Alex Weissman (https://alexanderweissman.com) + * @link https://github.com/laravel/framework/blob/5.4/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php + */ +class BelongsToManyUnique extends BelongsToMany +{ + use Unique; +} diff --git a/login/app/sprinkles/core/src/Database/Relations/Concerns/Syncable.php b/login/app/sprinkles/core/src/Database/Relations/Concerns/Syncable.php new file mode 100755 index 0000000..278b762 --- /dev/null +++ b/login/app/sprinkles/core/src/Database/Relations/Concerns/Syncable.php @@ -0,0 +1,132 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Database\Relations\Concerns; + +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Collection; + +/** + * Implements the `sync` method for HasMany relationships. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +trait Syncable +{ + /** + * Synchronizes an array of data for related models with a parent model. + * + * @param mixed[] $data + * @param bool $deleting Delete models from the database that are not represented in the input data. + * @param bool $forceCreate Ignore mass assignment restrictions on child models. + * @param string $relatedKeyName The primary key used to determine which child models are new, updated, or deleted. + */ + public function sync($data, $deleting = true, $forceCreate = false, $relatedKeyName = null) + { + $changes = [ + 'created' => [], 'deleted' => [], 'updated' => [], + ]; + + if (is_null($relatedKeyName)) { + $relatedKeyName = $this->related->getKeyName(); + } + + // First we need to attach any of the associated models that are not currently + // in the child entity table. We'll spin through the given IDs, checking to see + // if they exist in the array of current ones, and if not we will insert. + $current = $this->newQuery()->pluck( + $relatedKeyName + )->all(); + + // Separate the submitted data into "update" and "new" + $updateRows = []; + $newRows = []; + foreach ($data as $row) { + // We determine "updateable" rows as those whose $relatedKeyName (usually 'id') is set, not empty, and + // match a related row in the database. + if (isset($row[$relatedKeyName]) && !empty($row[$relatedKeyName]) && in_array($row[$relatedKeyName], $current)) { + $id = $row[$relatedKeyName]; + $updateRows[$id] = $row; + } else { + $newRows[] = $row; + } + } + + // Next, we'll determine the rows in the database that aren't in the "update" list. + // These rows will be scheduled for deletion. Again, we determine based on the relatedKeyName (typically 'id'). + $updateIds = array_keys($updateRows); + $deleteIds = []; + foreach ($current as $currentId) { + if (!in_array($currentId, $updateIds)) { + $deleteIds[] = $currentId; + } + } + + // Delete any non-matching rows + if ($deleting && count($deleteIds) > 0) { + // Remove global scopes to avoid ambiguous keys + $this->getRelated() + ->withoutGlobalScopes() + ->whereIn($relatedKeyName, $deleteIds) + ->delete(); + + $changes['deleted'] = $this->castKeys($deleteIds); + } + + // Update the updatable rows + foreach ($updateRows as $id => $row) { + // Remove global scopes to avoid ambiguous keys + $this->getRelated() + ->withoutGlobalScopes() + ->where($relatedKeyName, $id) + ->update($row); + } + + $changes['updated'] = $this->castKeys($updateIds); + + // Insert the new rows + $newIds = []; + foreach ($newRows as $row) { + if ($forceCreate) { + $newModel = $this->forceCreate($row); + } else { + $newModel = $this->create($row); + } + $newIds[] = $newModel->$relatedKeyName; + } + + $changes['created'] = $this->castKeys($newIds); + + return $changes; + } + + + /** + * Cast the given keys to integers if they are numeric and string otherwise. + * + * @param array $keys + * @return array + */ + protected function castKeys(array $keys) + { + return (array) array_map(function ($v) { + return $this->castKey($v); + }, $keys); + } + + /** + * Cast the given key to an integer if it is numeric. + * + * @param mixed $key + * @return mixed + */ + protected function castKey($key) + { + return is_numeric($key) ? (int) $key : (string) $key; + } +} diff --git a/login/app/sprinkles/core/src/Database/Relations/Concerns/Unique.php b/login/app/sprinkles/core/src/Database/Relations/Concerns/Unique.php new file mode 100755 index 0000000..4b529bb --- /dev/null +++ b/login/app/sprinkles/core/src/Database/Relations/Concerns/Unique.php @@ -0,0 +1,563 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Database\Relations\Concerns; + +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Collection; +use Illuminate\Database\Query\Expression; + +/** + * Enforce uniqueness for BelongsToManyUnique, MorphToManyUnique, and BelongsToManyThrough. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +trait Unique +{ + /** + * The related tertiary model instance. + * + * @var \Illuminate\Database\Eloquent\Model + */ + protected $tertiaryRelated = null; + + /** + * The name to use for the tertiary relation (e.g. 'roles_via', etc) + * + * @var string + */ + protected $tertiaryRelationName = null; + + /** + * The foreign key to the related tertiary model instance. + * + * @var string + */ + protected $tertiaryKey; + + /** + * A callback to apply to the tertiary query. + * + * @var callable|null + */ + protected $tertiaryCallback = null; + + /** + * The limit to apply on the number of related models retrieved. + * + * @var int|null + */ + protected $limit = null; + + /** + * The offset to apply on the related models retrieved. + * + * @var int|null + */ + protected $offset = null; + + /** + * Alias to set the "offset" value of the query. + * + * @param int $value + * @return $this + */ + public function skip($value) + { + return $this->offset($value); + } + + /** + * Set the "offset" value of the query. + * + * @todo Implement for 'unionOffset' as well? (By checking the value of $this->query->getQuery()->unions) + * @see \Illuminate\Database\Query\Builder + * @param int $value + * @return $this + */ + public function offset($value) + { + $this->offset = max(0, $value); + + return $this; + } + + /** + * Alias to set the "limit" value of the query. + * + * @param int $value + * @return $this + */ + public function take($value) + { + return $this->limit($value); + } + + /** + * Set the "limit" value of the query. + * + * @todo Implement for 'unionLimit' as well? (By checking the value of $this->query->getQuery()->unions) + * @see \Illuminate\Database\Query\Builder + * @param int $value + * @return $this + */ + public function limit($value) + { + if ($value >= 0) { + $this->limit = $value; + } + + return $this; + } + + /** + * Set the limit on the number of intermediate models to load. + * + * @deprecated since 4.1.7 + * @param int $value + * @return $this + */ + public function withLimit($value) + { + return $this->limit($value); + } + + /** + * Set the offset when loading the intermediate models. + * + * @deprecated since 4.1.7 + * @param int $value + * @return $this + */ + public function withOffset($value) + { + return $this->offset($value); + } + + /** + * Add a query to load the nested tertiary models for this relationship. + * + * @param \Illuminate\Database\Eloquent\Model $tertiaryRelated + * @param string $tertiaryRelationName + * @param string $tertiaryKey + * @param callable $tertiaryCallback + * @return $this + */ + public function withTertiary($tertiaryRelated, $tertiaryRelationName = null, $tertiaryKey = null, $tertiaryCallback = null) + { + $this->tertiaryRelated = new $tertiaryRelated; + + // Try to guess the tertiary related key from the tertiaryRelated model. + $this->tertiaryKey = $tertiaryKey ?: $this->tertiaryRelated->getForeignKey(); + + // Also add the tertiary key as a pivot + $this->withPivot($this->tertiaryKey); + + $this->tertiaryRelationName = is_null($tertiaryRelationName) ? $this->tertiaryRelated->getTable() : $tertiaryRelationName; + + $this->tertiaryCallback = is_null($tertiaryCallback) + ? function () { + // + } + : $tertiaryCallback; + + return $this; + } + + /** + * Return the count of child models for this relationship. + * + * @see http://stackoverflow.com/a/29728129/2970321 + * @return int + */ + public function count() + { + $constrainedBuilder = clone $this->query; + + $constrainedBuilder = $constrainedBuilder->distinct(); + + return $constrainedBuilder->count($this->relatedKey); + } + + /** + * Add the constraints for a relationship count query. + * + * @see \Illuminate\Database\Eloquent\Relations\Relation + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parentQuery + * @return \Illuminate\Database\Eloquent\Builder + */ + public function getRelationExistenceCountQuery(Builder $query, Builder $parentQuery) + { + return $this->getRelationExistenceQuery( + $query, $parentQuery, new Expression("count(distinct {$this->relatedKey})") + ); + } + + /** + * Match the eagerly loaded results to their parents + * + * @param array $models + * @param \Illuminate\Database\Eloquent\Collection $results + * @param string $relation + * @return array + */ + public function match(array $models, Collection $results, $relation) + { + // Build dictionary of parent (e.g. user) to related (e.g. permission) models + list($dictionary, $nestedTertiaryDictionary) = $this->buildDictionary($results, $this->foreignKey); + + // Once we have an array dictionary of child objects we can easily match the + // children back to their parent using the dictionary and the keys on the + // the parent models. Then we will return the hydrated models back out. + foreach ($models as $model) { + if (isset($dictionary[$key = $model->getKey()])) { + /** @var array */ + $items = $dictionary[$key]; + + // Eliminate any duplicates + $items = $this->related->newCollection($items)->unique(); + + // If set, match up the tertiary models to the models in the related collection + if (!is_null($nestedTertiaryDictionary)) { + $this->matchTertiaryModels($nestedTertiaryDictionary[$key], $items); + } + + $model->setRelation( + $relation, $items + ); + } + } + + return $models; + } + + /** + * Execute the query as a "select" statement, getting all requested models + * and matching up any tertiary models. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function get($columns = ['*']) + { + // Get models and condense the result set + $models = $this->getModels($columns, true); + + // Remove the tertiary pivot key from the condensed models + $this->unsetTertiaryPivots($models); + + return $models; + } + + /** + * If we are applying either a limit or offset, we'll first determine a limited/offset list of model ids + * to select from in the final query. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param int $limit + * @param int $offset + * @return \Illuminate\Database\Eloquent\Builder + */ + public function getPaginatedQuery(Builder $query, $limit = null, $offset = null) + { + $constrainedBuilder = clone $query; + + // Since some unique models will be represented by more than one row in the database, + // we cannot apply limit/offset directly to the query. If we did that, we'd miss + // some of the records that are to be coalesced into the final set of models. + // Instead, we perform an additional query with grouping and limit/offset to determine + // the desired set of unique model _ids_, and then constrain our final query + // to these models with a whereIn clause. + $relatedKeyName = $this->related->getQualifiedKeyName(); + + // Apply an additional scope to override any selected columns in other global scopes + $uniqueIdScope = function ($subQuery) use ($relatedKeyName) { + $subQuery->select($relatedKeyName) + ->groupBy($relatedKeyName); + }; + + $identifier = spl_object_hash($uniqueIdScope); + + $constrainedBuilder->withGlobalScope($identifier, $uniqueIdScope); + + if ($limit) { + $constrainedBuilder->limit($limit); + } + + if ($offset) { + $constrainedBuilder->offset($offset); + } + + $primaryKeyName = $this->getParent()->getKeyName(); + $modelIds = $constrainedBuilder->get()->pluck($primaryKeyName)->toArray(); + + // Modify the unconstrained query to limit to these models + return $query->whereIn($relatedKeyName, $modelIds); + } + + /** + * Get the full join results for this query, overriding the default getEager() method. + * The default getEager() method would normally just call get() on this relationship. + * This is not what we want here though, because our get() method removes records before + * `match` has a chance to build out the substructures. + * + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getEager() + { + return $this->getModels(['*'], false); + } + + /** + * Get the hydrated models and eager load their relations, optionally + * condensing the set of models before performing the eager loads. + * + * @param array $columns + * @param bool $condenseModels + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getModels($columns = ['*'], $condenseModels = true) + { + // First we'll add the proper select columns onto the query so it is run with + // the proper columns. Then, we will get the results and hydrate out pivot + // models with the result of those columns as a separate model relation. + $columns = $this->query->getQuery()->columns ? [] : $columns; + + // Add any necessary pagination on the related models + if ($this->limit || $this->offset) { + $this->getPaginatedQuery($this->query, $this->limit, $this->offset); + } + + // Apply scopes to the Eloquent\Builder instance. + $builder = $this->query->applyScopes(); + + $builder = $builder->addSelect( + $this->shouldSelect($columns) + ); + + $models = $builder->getModels(); + + // Hydrate the pivot models so we can load the via models + $this->hydratePivotRelation($models); + + if ($condenseModels) { + $models = $this->condenseModels($models); + } + + // If we actually found models we will also eager load any relationships that + // have been specified as needing to be eager loaded. This will solve the + // n + 1 query problem for the developer and also increase performance. + if (count($models) > 0) { + $models = $builder->eagerLoadRelations($models); + } + + return $this->related->newCollection($models); + } + + /** + * Condense the raw join query results into a set of unique models. + * + * Before doing this, we may optionally find any tertiary models that should be + * set as sub-relations on these models. + * @param array $models + * @return array + */ + protected function condenseModels(array $models) + { + // Build dictionary of tertiary models, if `withTertiary` was called + $dictionary = null; + if ($this->tertiaryRelated) { + $dictionary = $this->buildTertiaryDictionary($models); + } + + // Remove duplicate models from collection + $models = $this->related->newCollection($models)->unique(); + + // If using withTertiary, use the dictionary to set the tertiary relation on each model. + if (!is_null($dictionary)) { + $this->matchTertiaryModels($dictionary, $models); + } + + return $models->all(); + } + + /** + * Build dictionary of related models keyed by the top-level "parent" id. + * If there is a tertiary query set as well, then also build a two-level dictionary + * that maps parent ids to arrays of related ids, which in turn map to arrays + * of tertiary models corresponding to each relationship. + * + * @param \Illuminate\Database\Eloquent\Collection $results + * @param string $parentKey + * @return array + */ + protected function buildDictionary(Collection $results, $parentKey = null) + { + // First we will build a dictionary of child models keyed by the "parent key" (foreign key + // of the intermediate relation) so that we will easily and quickly match them to their + // parents without having a possibly slow inner loops for every models. + $dictionary = []; + + //Example nested dictionary: + //[ + // // User 1 + // '1' => [ + // // Permission 3 + // '3' => [ + // Role1, + // Role2 + // ], + // ... + // ], + // ... + //] + $nestedTertiaryDictionary = null; + $tertiaryModels = null; + + if ($this->tertiaryRelationName) { + // Get all tertiary models from the result set matching any of the parent models. + $tertiaryModels = $this->getTertiaryModels($results->all()); + } + + foreach ($results as $result) { + $parentKeyValue = $result->pivot->$parentKey; + + // Set the related model in the main dictionary. + // Note that this can end up adding duplicate models. It's cheaper to simply + // go back and remove the duplicates when we actually use the dictionary, + // rather than check for duplicates on each insert. + $dictionary[$parentKeyValue][] = $result; + + // If we're loading tertiary models, then set the keys in the nested dictionary as well. + if (!is_null($tertiaryModels)) { + $tertiaryKeyValue = $result->pivot->{$this->tertiaryKey}; + + if (!is_null($tertiaryKeyValue)) { + $tertiaryModel = clone $tertiaryModels[$tertiaryKeyValue]; + + // We also transfer the pivot relation at this point, since we have already coalesced + // any tertiary models into the nested dictionary. + $this->transferPivotsToTertiary($result, $tertiaryModel); + + $nestedTertiaryDictionary[$parentKeyValue][$result->getKey()][] = $tertiaryModel; + } + } + } + + return [$dictionary, $nestedTertiaryDictionary]; + } + + /** + * Build dictionary of tertiary models keyed by the corresponding related model keys. + * + * @param array $models + * @return array + */ + protected function buildTertiaryDictionary(array $models) + { + $dictionary = []; + + // Find the related tertiary entities (e.g. tasks) for all related models (e.g. locations) + $tertiaryModels = $this->getTertiaryModels($models); + + // Now for each related model (e.g. location), we will build out a dictionary of their tertiary models (e.g. tasks) + foreach ($models as $model) { + $tertiaryKeyValue = $model->pivot->{$this->tertiaryKey}; + + $tertiaryModel = clone $tertiaryModels[$tertiaryKeyValue]; + + $this->transferPivotsToTertiary($model, $tertiaryModel); + + $dictionary[$model->getKey()][] = $tertiaryModel; + } + + return $dictionary; + } + + protected function transferPivotsToTertiary($model, $tertiaryModel) + { + $pivotAttributes = []; + foreach ($this->pivotColumns as $column) { + $pivotAttributes[$column] = $model->pivot->$column; + unset($model->pivot->$column); + } + // Copy the related key pivot as well, but don't unset on the related model + $pivotAttributes[$this->relatedKey] = $model->pivot->{$this->relatedKey}; + + // Set the tertiary key pivot as well + $pivotAttributes[$this->tertiaryKey] = $tertiaryModel->getKey(); + + $pivot = $this->newExistingPivot($pivotAttributes); + $tertiaryModel->setRelation('pivot', $pivot); + } + + /** + * Get the tertiary models for the relationship. + * + * @param array $models + * @return \Illuminate\Database\Eloquent\Collection + */ + protected function getTertiaryModels(array $models) + { + $tertiaryClass = $this->tertiaryRelated; + + $keys = []; + foreach ($models as $model) { + $keys[] = $model->getRelation('pivot')->{$this->tertiaryKey}; + } + $keys = array_unique($keys); + + $query = $tertiaryClass->whereIn($tertiaryClass->getQualifiedKeyName(), $keys); + + // Add any additional constraints/eager loads to the tertiary query + $callback = $this->tertiaryCallback; + $callback($query); + + $tertiaryModels = $query + ->get() + ->keyBy($tertiaryClass->getKeyName()); + + return $tertiaryModels; + } + + /** + * Match a collection of child models into a collection of parent models using a dictionary. + * + * @param array $dictionary + * @param \Illuminate\Database\Eloquent\Collection $results + * @return void + */ + protected function matchTertiaryModels(array $dictionary, Collection $results) + { + // Now go through and set the tertiary relation on each child model + foreach ($results as $model) { + if (isset($dictionary[$key = $model->getKey()])) { + $tertiaryModels = $dictionary[$key]; + + $model->setRelation( + $this->tertiaryRelationName, $this->tertiaryRelated->newCollection($tertiaryModels) + ); + } + } + } + + /** + * Unset tertiary pivots on a collection or array of models. + * + * @param \Illuminate\Database\Eloquent\Collection $models + * @return void + */ + protected function unsetTertiaryPivots(Collection $models) + { + foreach ($models as $model) { + foreach ($this->pivotColumns as $column) { + unset($model->pivot->$column); + } + } + } +} diff --git a/login/app/sprinkles/core/src/Database/Relations/HasManySyncable.php b/login/app/sprinkles/core/src/Database/Relations/HasManySyncable.php new file mode 100755 index 0000000..bcf2a9d --- /dev/null +++ b/login/app/sprinkles/core/src/Database/Relations/HasManySyncable.php @@ -0,0 +1,22 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Database\Relations; + +use Illuminate\Database\Eloquent\Relations\HasMany; +use UserFrosting\Sprinkle\Core\Database\Relations\Concerns\Syncable; + +/** + * A HasMany relationship that supports a `sync` method. + * + * @author Alex Weissman (https://alexanderweissman.com) + * @link https://github.com/laravel/framework/blob/5.4/src/Illuminate/Database/Eloquent/Relations/HasMany.php + */ +class HasManySyncable extends HasMany +{ + use Syncable; +} diff --git a/login/app/sprinkles/core/src/Database/Relations/MorphManySyncable.php b/login/app/sprinkles/core/src/Database/Relations/MorphManySyncable.php new file mode 100755 index 0000000..2786193 --- /dev/null +++ b/login/app/sprinkles/core/src/Database/Relations/MorphManySyncable.php @@ -0,0 +1,22 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Database\Relations; + +use Illuminate\Database\Eloquent\Relations\MorphMany; +use UserFrosting\Sprinkle\Core\Database\Relations\Concerns\Syncable; + +/** + * A MorphMany relationship that constrains on the value of an additional foreign key in the pivot table. + * + * @author Alex Weissman (https://alexanderweissman.com) + * @link https://github.com/laravel/framework/blob/5.4/src/Illuminate/Database/Eloquent/Relations/MorphMany.php + */ +class MorphManySyncable extends MorphMany +{ + use Syncable; +} diff --git a/login/app/sprinkles/core/src/Database/Relations/MorphToManyUnique.php b/login/app/sprinkles/core/src/Database/Relations/MorphToManyUnique.php new file mode 100755 index 0000000..cc9a03f --- /dev/null +++ b/login/app/sprinkles/core/src/Database/Relations/MorphToManyUnique.php @@ -0,0 +1,22 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Database\Relations; + +use Illuminate\Database\Eloquent\Relations\MorphToMany; +use UserFrosting\Sprinkle\Core\Database\Relations\Concerns\Unique; + +/** + * A MorphToMany relationship that reduces the related members to a unique (by primary key) set. + * + * @author Alex Weissman (https://alexanderweissman.com) + * @link https://github.com/laravel/framework/blob/5.4/src/Illuminate/Database/Eloquent/Relations/MorphToMany.php + */ +class MorphToManyUnique extends MorphToMany +{ + use Unique; +} diff --git a/login/app/sprinkles/core/src/Error/ExceptionHandlerManager.php b/login/app/sprinkles/core/src/Error/ExceptionHandlerManager.php new file mode 100755 index 0000000..4680da5 --- /dev/null +++ b/login/app/sprinkles/core/src/Error/ExceptionHandlerManager.php @@ -0,0 +1,93 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Error; + +use Interop\Container\ContainerInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use UserFrosting\Sprinkle\Core\Handler\ExceptionHandlerInterface; + +/** + * Default UserFrosting application error handler + * + * It outputs the error message and diagnostic information in either JSON, XML, or HTML based on the Accept header. + * @author Alex Weissman (https://alexanderweissman.com) + */ +class ExceptionHandlerManager +{ + /** + * @var ContainerInterface The global container object, which holds all your services. + */ + protected $ci; + + /** + * @var array[string] An array that maps Exception types to callbacks, for special processing of certain types of errors. + */ + protected $exceptionHandlers = []; + + /** + * @var bool + */ + protected $displayErrorDetails; + + /** + * Constructor + * + * @param ContainerInterface $ci The global container object, which holds all your services. + * @param boolean $displayErrorDetails Set to true to display full details + */ + public function __construct(ContainerInterface $ci, $displayErrorDetails = false) + { + $this->ci = $ci; + $this->displayErrorDetails = (bool)$displayErrorDetails; + } + + /** + * Invoke error handler + * + * @param ServerRequestInterface $request The most recent Request object + * @param ResponseInterface $response The most recent Response object + * @param Throwable $exception The caught Exception object + * + * @return ResponseInterface + */ + public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $exception) + { + // Default exception handler class + $handlerClass = '\UserFrosting\Sprinkle\Core\Error\Handler\ExceptionHandler'; + + // Get the last matching registered handler class, and instantiate it + foreach ($this->exceptionHandlers as $exceptionClass => $matchedHandlerClass) { + if ($exception instanceof $exceptionClass) { + $handlerClass = $matchedHandlerClass; + } + } + + $handler = new $handlerClass($this->ci, $request, $response, $exception, $this->displayErrorDetails); + + return $handler->handle(); + } + + /** + * Register an exception handler for a specified exception class. + * + * The exception handler must implement \UserFrosting\Sprinkle\Core\Handler\ExceptionHandlerInterface. + * + * @param string $exceptionClass The fully qualified class name of the exception to handle. + * @param string $handlerClass The fully qualified class name of the assigned handler. + * @throws InvalidArgumentException If the registered handler fails to implement ExceptionHandlerInterface + */ + public function registerHandler($exceptionClass, $handlerClass) + { + if (!is_a($handlerClass, '\UserFrosting\Sprinkle\Core\Error\Handler\ExceptionHandlerInterface', true)) { + throw new \InvalidArgumentException("Registered exception handler must implement ExceptionHandlerInterface!"); + } + + $this->exceptionHandlers[$exceptionClass] = $handlerClass; + } +} diff --git a/login/app/sprinkles/core/src/Error/Handler/ExceptionHandler.php b/login/app/sprinkles/core/src/Error/Handler/ExceptionHandler.php new file mode 100755 index 0000000..4fdc51d --- /dev/null +++ b/login/app/sprinkles/core/src/Error/Handler/ExceptionHandler.php @@ -0,0 +1,275 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Error\Handler; + +use Interop\Container\ContainerInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use UserFrosting\Sprinkle\Core\Error\Renderer\HtmlRenderer; +use UserFrosting\Sprinkle\Core\Error\Renderer\JsonRenderer; +use UserFrosting\Sprinkle\Core\Error\Renderer\PlainTextRenderer; +use UserFrosting\Sprinkle\Core\Error\Renderer\WhoopsRenderer; +use UserFrosting\Sprinkle\Core\Error\Renderer\XmlRenderer; +use UserFrosting\Sprinkle\Core\Http\Concerns\DeterminesContentType; +use UserFrosting\Support\Message\UserMessage; + +/** + * Generic handler for exceptions. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class ExceptionHandler implements ExceptionHandlerInterface +{ + use DeterminesContentType; + + /** + * @var ContainerInterface The global container object, which holds all your services. + */ + protected $ci; + + /** + * @var ServerRequestInterface + */ + protected $request; + + /** + * @var ResponseInterface + */ + protected $response; + + /** + * @var Throwable + */ + protected $exception; + + /** + * @var ErrorRendererInterface + */ + protected $renderer = null; + + /** + * @var string + */ + protected $contentType; + + /** + * @var int + */ + protected $statusCode; + + /** + * Tells the handler whether or not to output detailed error information to the client. + * Each handler may choose if and how to implement this. + * + * @var bool + */ + protected $displayErrorDetails; + + /** + * Create a new ExceptionHandler object. + * + * @param ContainerInterface $ci + * @param ServerRequestInterface $request The most recent Request object + * @param ResponseInterface $response The most recent Response object + * @param Throwable $exception The caught Exception object + * @param bool $displayErrorDetails + */ + public function __construct( + ContainerInterface $ci, + ServerRequestInterface $request, + ResponseInterface $response, + $exception, + $displayErrorDetails = false + ) { + $this->ci = $ci; + $this->request = $request; + $this->response = $response; + $this->exception = $exception; + $this->displayErrorDetails = $displayErrorDetails; + $this->statusCode = $this->determineStatusCode(); + $this->contentType = $this->determineContentType($request, $this->ci->config['site.debug.ajax']); + $this->renderer = $this->determineRenderer(); + } + + /** + * Handle the caught exception. + * The handler may render a detailed debugging error page, a generic error page, write to logs, and/or add messages to the alert stream. + * + * @return ResponseInterface + */ + public function handle() + { + // If displayErrorDetails is set to true, we'll halt and immediately respond with a detailed debugging page. + // We do not log errors in this case. + if ($this->displayErrorDetails) { + $response = $this->renderDebugResponse(); + } else { + // Write exception to log + $this->writeToErrorLog(); + + // Render generic error page + $response = $this->renderGenericResponse(); + } + + // If this is an AJAX request and AJAX debugging is turned off, write messages to the alert stream + if ($this->request->isXhr() && !$this->ci->config['site.debug.ajax']) { + $this->writeAlerts(); + } + + return $response; + } + + /** + * Render a detailed response with debugging information. + * + * @return ResponseInterface + */ + public function renderDebugResponse() + { + $body = $this->renderer->renderWithBody(); + + return $this->response + ->withStatus($this->statusCode) + ->withHeader('Content-type', $this->contentType) + ->withBody($body); + } + + /** + * Render a generic, user-friendly response without sensitive debugging information. + * + * @return ResponseInterface + */ + public function renderGenericResponse() + { + $messages = $this->determineUserMessages(); + $httpCode = $this->statusCode; + + try { + $template = $this->ci->view->getEnvironment()->loadTemplate("pages/error/$httpCode.html.twig"); + } catch (\Twig_Error_Loader $e) { + $template = $this->ci->view->getEnvironment()->loadTemplate("pages/abstract/error.html.twig"); + } + + return $this->response + ->withStatus($httpCode) + ->withHeader('Content-type', $this->contentType) + ->write($template->render([ + 'messages' => $messages + ])); + } + + /** + * Write to the error log + * + * @return void + */ + public function writeToErrorLog() + { + $renderer = new PlainTextRenderer($this->request, $this->response, $this->exception, true); + $error = $renderer->render(); + $error .= PHP_EOL . 'View in rendered output by enabling the "displayErrorDetails" setting.' . PHP_EOL; + $this->logError($error); + } + + /** + * Write user-friendly error messages to the alert message stream. + * + * @return void + */ + public function writeAlerts() + { + $messages = $this->determineUserMessages(); + + foreach ($messages as $message) { + $this->ci->alerts->addMessageTranslated('danger', $message->message, $message->parameters); + } + } + + /** + * Determine which renderer to use based on content type + * Overloaded $renderer from calling class takes precedence over all + * + * @return ErrorRendererInterface + * + * @throws \RuntimeException + */ + protected function determineRenderer() + { + $renderer = $this->renderer; + + if ((!is_null($renderer) && !class_exists($renderer)) + || (!is_null($renderer) && !in_array('UserFrosting\Sprinkle\Core\Error\Renderer\ErrorRendererInterface', class_implements($renderer))) + ) { + throw new \RuntimeException(sprintf( + 'Non compliant error renderer provided (%s). ' . + 'Renderer must implement the ErrorRendererInterface', + $renderer + )); + } + + if (is_null($renderer)) { + switch ($this->contentType) { + case 'application/json': + $renderer = JsonRenderer::class; + break; + + case 'text/xml': + case 'application/xml': + $renderer = XmlRenderer::class; + break; + + case 'text/plain': + $renderer = PlainTextRenderer::class; + break; + + default: + case 'text/html': + $renderer = WhoopsRenderer::class; + break; + } + } + + return new $renderer($this->request, $this->response, $this->exception, $this->displayErrorDetails); + } + + /** + * Resolve the status code to return in the response from this handler. + * + * @return int + */ + protected function determineStatusCode() + { + if ($this->request->getMethod() === 'OPTIONS') { + return 200; + } + return 500; + } + + /** + * Resolve a list of error messages to present to the end user. + * + * @return array + */ + protected function determineUserMessages() + { + return [ + new UserMessage("ERROR.SERVER") + ]; + } + + /** + * Monolog logging for errors + * + * @param $message + * @return void + */ + protected function logError($message) + { + $this->ci->errorLogger->error($message); + } +} diff --git a/login/app/sprinkles/core/src/Error/Handler/ExceptionHandlerInterface.php b/login/app/sprinkles/core/src/Error/Handler/ExceptionHandlerInterface.php new file mode 100755 index 0000000..a928b69 --- /dev/null +++ b/login/app/sprinkles/core/src/Error/Handler/ExceptionHandlerInterface.php @@ -0,0 +1,32 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Error\Handler; + +use Interop\Container\ContainerInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; + +/** + * All exception handlers must implement this interface. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +interface ExceptionHandlerInterface +{ + public function __construct(ContainerInterface $ci, ServerRequestInterface $request, ResponseInterface $response, $exception, $displayErrorDetails = false); + + public function handle(); + + public function renderDebugResponse(); + + public function renderGenericResponse(); + + public function writeToErrorLog(); + + public function writeAlerts(); +} diff --git a/login/app/sprinkles/core/src/Error/Handler/HttpExceptionHandler.php b/login/app/sprinkles/core/src/Error/Handler/HttpExceptionHandler.php new file mode 100755 index 0000000..946bda7 --- /dev/null +++ b/login/app/sprinkles/core/src/Error/Handler/HttpExceptionHandler.php @@ -0,0 +1,64 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Error\Handler; + +use UserFrosting\Support\Exception\HttpException; + +/** + * Handler for HttpExceptions. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class HttpExceptionHandler extends ExceptionHandler +{ + /** + * For HttpExceptions, only write to the error log if the status code is 500 + * + * @return void + */ + public function writeToErrorLog() + { + if ($this->statusCode != 500) { + return; + } + + parent::writeToErrorLog(); + } + + /** + * Resolve the status code to return in the response from this handler. + * + * @return int + */ + protected function determineStatusCode() + { + if ($this->request->getMethod() === 'OPTIONS') { + return 200; + } elseif ($this->exception instanceof HttpException) { + return $this->exception->getHttpErrorCode(); + } + return 500; + } + + /** + * Resolve a list of error messages to present to the end user. + * + * @return array + */ + protected function determineUserMessages() + { + if ($this->exception instanceof HttpException) { + return $this->exception->getUserMessages(); + } + + // Fallback + return [ + new UserMessage("ERROR.SERVER") + ]; + } +} diff --git a/login/app/sprinkles/core/src/Error/Handler/NotFoundExceptionHandler.php b/login/app/sprinkles/core/src/Error/Handler/NotFoundExceptionHandler.php new file mode 100755 index 0000000..306feed --- /dev/null +++ b/login/app/sprinkles/core/src/Error/Handler/NotFoundExceptionHandler.php @@ -0,0 +1,38 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Error\Handler; + +use UserFrosting\Sprinkle\Core\Error\Handler\HttpExceptionHandler; +use UserFrosting\Support\Exception\HttpException; +use UserFrosting\Support\Message\UserMessage; + +/** + * Handler for NotFoundExceptions. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class NotFoundExceptionHandler extends HttpExceptionHandler +{ + /** + * Custom handling for NotFoundExceptions. Always render a generic response! + * + * @return Response + */ + public function handle() + { + // Render generic error page + $response = $this->renderGenericResponse(); + + // If this is an AJAX request and AJAX debugging is turned off, write messages to the alert stream + if ($this->request->isXhr() && !$this->ci->config['site.debug.ajax']) { + $this->writeAlerts(); + } + + return $response; + } +} diff --git a/login/app/sprinkles/core/src/Error/Handler/PhpMailerExceptionHandler.php b/login/app/sprinkles/core/src/Error/Handler/PhpMailerExceptionHandler.php new file mode 100755 index 0000000..45f0e8d --- /dev/null +++ b/login/app/sprinkles/core/src/Error/Handler/PhpMailerExceptionHandler.php @@ -0,0 +1,30 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Error\Handler; + +use UserFrosting\Support\Message\UserMessage; + +/** + * Handler for phpMailer exceptions. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class PhpMailerExceptionHandler extends ExceptionHandler +{ + /** + * Resolve a list of error messages to present to the end user. + * + * @return array + */ + protected function determineUserMessages() + { + return [ + new UserMessage("ERROR.MAIL") + ]; + } +} diff --git a/login/app/sprinkles/core/src/Error/Renderer/ErrorRenderer.php b/login/app/sprinkles/core/src/Error/Renderer/ErrorRenderer.php new file mode 100755 index 0000000..f065af0 --- /dev/null +++ b/login/app/sprinkles/core/src/Error/Renderer/ErrorRenderer.php @@ -0,0 +1,64 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Error\Renderer; + +use Slim\Http\Body; + +abstract class ErrorRenderer implements ErrorRendererInterface +{ + /** + * @var ServerRequestInterface + */ + protected $request; + + /** + * @var ResponseInterface + */ + protected $response; + + /** + * @var Exception + */ + protected $exception; + + /** + * Tells the renderer whether or not to output detailed error information to the client. + * Each renderer may choose if and how to implement this. + * + * @var bool + */ + protected $displayErrorDetails; + + /** + * Create a new ErrorRenderer object. + * + * @param ServerRequestInterface $request The most recent Request object + * @param ResponseInterface $response The most recent Response object + * @param Exception $exception The caught Exception object + * @param bool $displayErrorDetails + */ + public function __construct($request, $response, $exception, $displayErrorDetails = false) + { + $this->request = $request; + $this->response = $response; + $this->exception = $exception; + $this->displayErrorDetails = $displayErrorDetails; + } + + abstract public function render(); + + /** + * @return Body + */ + public function renderWithBody() + { + $body = new Body(fopen('php://temp', 'r+')); + $body->write($this->render()); + return $body; + } +} diff --git a/login/app/sprinkles/core/src/Error/Renderer/ErrorRendererInterface.php b/login/app/sprinkles/core/src/Error/Renderer/ErrorRendererInterface.php new file mode 100755 index 0000000..7af269a --- /dev/null +++ b/login/app/sprinkles/core/src/Error/Renderer/ErrorRendererInterface.php @@ -0,0 +1,29 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Error\Renderer; + +interface ErrorRendererInterface +{ + /** + * @param ServerRequestInterface $request The most recent Request object + * @param ResponseInterface $response The most recent Response object + * @param Exception $exception The caught Exception object + * @param bool $displayErrorDetails + */ + public function __construct($request, $response, $exception, $displayErrorDetails = false); + + /** + * @return string + */ + public function render(); + + /** + * @return \Slim\Http\Body + */ + public function renderWithBody(); +} diff --git a/login/app/sprinkles/core/src/Error/Renderer/HtmlRenderer.php b/login/app/sprinkles/core/src/Error/Renderer/HtmlRenderer.php new file mode 100755 index 0000000..1f32675 --- /dev/null +++ b/login/app/sprinkles/core/src/Error/Renderer/HtmlRenderer.php @@ -0,0 +1,151 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Error\Renderer; + +class HtmlRenderer extends ErrorRenderer +{ + /** + * Render HTML error report. + * + * @return string + */ + public function render() + { + $title = 'UserFrosting Application Error'; + + if ($this->displayErrorDetails) { + $html = '<p>The application could not run because of the following error:</p>'; + $html .= '<h2>Details</h2>'; + $html .= $this->renderException($this->exception); + + $html .= '<h2>Your request</h2>'; + $html .= $this->renderRequest(); + + $html .= '<h2>Response headers</h2>'; + $html .= $this->renderResponseHeaders(); + + $exception = $this->exception; + while ($exception = $exception->getPrevious()) { + $html .= '<h2>Previous exception</h2>'; + $html .= $this->renderException($exception); + } + } else { + $html = '<p>A website error has occurred. Sorry for the temporary inconvenience.</p>'; + } + + $output = sprintf( + "<html><head><meta http-equiv='Content-Type' content='text/html; charset=utf-8'>" . + "<title>%s</title><style>body{margin:0;padding:30px;font:12px/1.5 Helvetica,Arial,Verdana," . + "sans-serif;}h1{margin:0;font-size:48px;font-weight:normal;line-height:48px;}strong{" . + "display:inline-block;width:65px;}table,th,td{font:12px Helvetica,Arial,Verdana," . + "sans-serif;border:1px solid black;border-collapse:collapse;padding:5px;text-align: left;}" . + "th{font-weight:600;}" . + "</style></head><body><h1>%s</h1>%s</body></html>", + $title, + $title, + $html + ); + + return $output; + } + + /** + * Render a summary of the exception. + * + * @param Exception $exception + * @return string + */ + public function renderException($exception) + { + $html = sprintf('<div><strong>Type:</strong> %s</div>', get_class($exception)); + + if (($code = $exception->getCode())) { + $html .= sprintf('<div><strong>Code:</strong> %s</div>', $code); + } + + if (($message = $exception->getMessage())) { + $html .= sprintf('<div><strong>Message:</strong> %s</div>', htmlentities($message)); + } + + if (($file = $exception->getFile())) { + $html .= sprintf('<div><strong>File:</strong> %s</div>', $file); + } + + if (($line = $exception->getLine())) { + $html .= sprintf('<div><strong>Line:</strong> %s</div>', $line); + } + + if (($trace = $exception->getTraceAsString())) { + $html .= '<h2>Trace</h2>'; + $html .= sprintf('<pre>%s</pre>', htmlentities($trace)); + } + + return $html; + } + + /** + * Render HTML representation of original request. + * + * @return string + */ + public function renderRequest() + { + $method = $this->request->getMethod(); + $uri = $this->request->getUri(); + $params = $this->request->getParams(); + $requestHeaders = $this->request->getHeaders(); + + $html = '<h3>Request URI:</h3>'; + + $html .= sprintf('<div><strong>%s</strong> %s</div>', $method, $uri); + + $html .= '<h3>Request parameters:</h3>'; + + $html .= $this->renderTable($params); + + $html .= '<h3>Request headers:</h3>'; + + $html .= $this->renderTable($requestHeaders); + + return $html; + } + + /** + * Render HTML representation of response headers. + * + * @return string + */ + public function renderResponseHeaders() + { + $html = '<h3>Response headers:</h3>'; + $html .= '<em>Additional response headers may have been set by Slim after the error handling routine. Please check your browser console for a complete list.</em><br>'; + + $html .= $this->renderTable($this->response->getHeaders()); + + return $html; + } + + /** + * Render HTML representation of a table of data. + * + * @param mixed[] $data the array of data to render. + * + * @return string + */ + protected function renderTable($data) + { + $html = '<table><tr><th>Name</th><th>Value</th></tr>'; + foreach ($data as $name => $value) { + $value = print_r($value, true); + $html .= "<tr><td>$name</td><td>$value</td></tr>"; + } + $html .= '</table>'; + + return $html; + } +} diff --git a/login/app/sprinkles/core/src/Error/Renderer/JsonRenderer.php b/login/app/sprinkles/core/src/Error/Renderer/JsonRenderer.php new file mode 100755 index 0000000..3adfd45 --- /dev/null +++ b/login/app/sprinkles/core/src/Error/Renderer/JsonRenderer.php @@ -0,0 +1,57 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Error\Renderer; + +/** + * Default JSON Error Renderer + */ +class JsonRenderer extends ErrorRenderer +{ + /** + * @return string + */ + public function render() + { + $message = $this->exception->getMessage(); + return $this->formatExceptionPayload($message); + } + + /** + * @param $message + * @return string + */ + public function formatExceptionPayload($message) + { + $e = $this->exception; + $error = ['message' => $message]; + + if ($this->displayErrorDetails) { + $error['exception'] = []; + do { + $error['exception'][] = $this->formatExceptionFragment($e); + } while ($e = $e->getPrevious()); + } + + return json_encode($error, JSON_PRETTY_PRINT); + } + + /** + * @param \Exception|\Throwable $e + * @return array + */ + public function formatExceptionFragment($e) + { + return [ + 'type' => get_class($e), + 'code' => $e->getCode(), + 'message' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + ]; + } +} diff --git a/login/app/sprinkles/core/src/Error/Renderer/PlainTextRenderer.php b/login/app/sprinkles/core/src/Error/Renderer/PlainTextRenderer.php new file mode 100755 index 0000000..a4984fc --- /dev/null +++ b/login/app/sprinkles/core/src/Error/Renderer/PlainTextRenderer.php @@ -0,0 +1,65 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Error\Renderer; + +/** + * Plain Text Error Renderer + */ +class PlainTextRenderer extends ErrorRenderer +{ + public function render() + { + if ($this->displayErrorDetails) { + return $this->formatExceptionBody(); + } + + return $this->exception->getMessage(); + } + + public function formatExceptionBody() + { + $e = $this->exception; + + $text = 'UserFrosting Application Error:' . PHP_EOL; + $text .= $this->formatExceptionFragment($e); + + while ($e = $e->getPrevious()) { + $text .= PHP_EOL . 'Previous Error:' . PHP_EOL; + $text .= $this->formatExceptionFragment($e); + } + + return $text; + } + + /** + * @param \Exception|\Throwable $e + * @return string + */ + public function formatExceptionFragment($e) + { + $text = sprintf('Type: %s' . PHP_EOL, get_class($e)); + + if ($code = $e->getCode()) { + $text .= sprintf('Code: %s' . PHP_EOL, $code); + } + if ($message = $e->getMessage()) { + $text .= sprintf('Message: %s' . PHP_EOL, htmlentities($message)); + } + if ($file = $e->getFile()) { + $text .= sprintf('File: %s' . PHP_EOL, $file); + } + if ($line = $e->getLine()) { + $text .= sprintf('Line: %s' . PHP_EOL, $line); + } + if ($trace = $e->getTraceAsString()) { + $text .= sprintf('Trace: %s', $trace); + } + + return $text; + } +} diff --git a/login/app/sprinkles/core/src/Error/Renderer/WhoopsRenderer.php b/login/app/sprinkles/core/src/Error/Renderer/WhoopsRenderer.php new file mode 100755 index 0000000..767ce1b --- /dev/null +++ b/login/app/sprinkles/core/src/Error/Renderer/WhoopsRenderer.php @@ -0,0 +1,712 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Error\Renderer; + +use InvalidArgumentException; +use RuntimeException; +use Symfony\Component\VarDumper\Cloner\AbstractCloner; +use Symfony\Component\VarDumper\Cloner\VarCloner; +use UnexpectedValueException; +use UserFrosting\Sprinkle\Core\Util\Util; +use Whoops\Exception\Formatter; +use Whoops\Exception\Inspector; +use Whoops\Handler\PlainTextHandler; +use Whoops\Util\Misc; +use Whoops\Util\TemplateHelper; + +class WhoopsRenderer extends ErrorRenderer +{ + /** + * Search paths to be scanned for resources, in the reverse + * order they're declared. + * + * @var array + */ + private $searchPaths = []; + + /** + * Fast lookup cache for known resource locations. + * + * @var array + */ + private $resourceCache = []; + + /** + * The name of the custom css file. + * + * @var string + */ + private $customCss = null; + + /** + * @var array[] + */ + private $extraTables = []; + + /** + * @var bool + */ + private $handleUnconditionally = false; + + /** + * @var string + */ + private $pageTitle = 'Whoops! There was an error.'; + + /** + * @var array[] + */ + private $applicationPaths; + + /** + * @var array[] + */ + private $blacklist = [ + '_GET' => [], + '_POST' => [], + '_FILES' => [], + '_COOKIE' => [], + '_SESSION' => [], + '_SERVER' => ['DB_PASSWORD', 'SMTP_PASSWORD'], + '_ENV' => ['DB_PASSWORD', 'SMTP_PASSWORD'], + ]; + + /** + * A string identifier for a known IDE/text editor, or a closure + * that resolves a string that can be used to open a given file + * in an editor. If the string contains the special substrings + * %file or %line, they will be replaced with the correct data. + * + * @example + * "txmt://open?url=%file&line=%line" + * @var mixed $editor + */ + protected $editor; + + /** + * A list of known editor strings + * @var array + */ + protected $editors = [ + "sublime" => "subl://open?url=file://%file&line=%line", + "textmate" => "txmt://open?url=file://%file&line=%line", + "emacs" => "emacs://open?url=file://%file&line=%line", + "macvim" => "mvim://open/?url=file://%file&line=%line", + "phpstorm" => "phpstorm://open?file=%file&line=%line", + "idea" => "idea://open?file=%file&line=%line", + ]; + + /** + * @var Inspector + */ + protected $inspector; + + /** + * @var TemplateHelper + */ + private $templateHelper; + + /** + * {@inheritDoc} + */ + public function __construct($request, $response, $exception, $displayErrorDetails = false) + { + $this->request = $request; + $this->response = $response; + $this->exception = $exception; + $this->displayErrorDetails = $displayErrorDetails; + + if (ini_get('xdebug.file_link_format') || extension_loaded('xdebug')) { + // Register editor using xdebug's file_link_format option. + $this->editors['xdebug'] = function ($file, $line) { + return str_replace(['%f', '%l'], [$file, $line], ini_get('xdebug.file_link_format')); + }; + } + + // Add the default, local resource search path: + $this->searchPaths[] = \UserFrosting\VENDOR_DIR . '/filp/whoops/src/Whoops/Resources'; + + // blacklist php provided auth based values + $this->blacklist('_SERVER', 'PHP_AUTH_PW'); + + $this->templateHelper = new TemplateHelper(); + + // Set up dummy inspector + $this->inspector = new Inspector($exception); + + if (class_exists('Symfony\Component\VarDumper\Cloner\VarCloner')) { + $cloner = new VarCloner(); + // Only dump object internals if a custom caster exists. + $cloner->addCasters(['*' => function ($obj, $a, $stub, $isNested, $filter = 0) { + $class = $stub->class; + $classes = [$class => $class] + class_parents($class) + class_implements($class); + + foreach ($classes as $class) { + if (isset(AbstractCloner::$defaultCasters[$class])) { + return $a; + } + } + + // Remove all internals + return []; + }]); + $this->templateHelper->setCloner($cloner); + } + } + + /** + * {@inheritDoc} + */ + public function render() + { + if (!$this->handleUnconditionally()) { + // Check conditions for outputting HTML: + // @todo: Make this more robust + if (php_sapi_name() === 'cli') { + // Help users who have been relying on an internal test value + // fix their code to the proper method + if (isset($_ENV['whoops-test'])) { + throw new \Exception( + 'Use handleUnconditionally instead of whoops-test' + .' environment variable' + ); + } + + return Handler::DONE; + } + } + + $templateFile = $this->getResource("views/layout.html.php"); + $cssFile = $this->getResource("css/whoops.base.css"); + $zeptoFile = $this->getResource("js/zepto.min.js"); + $clipboard = $this->getResource("js/clipboard.min.js"); + $jsFile = $this->getResource("js/whoops.base.js"); + + if ($this->customCss) { + $customCssFile = $this->getResource($this->customCss); + } + + $inspector = $this->getInspector(); + $frames = $inspector->getFrames(); + + $code = $inspector->getException()->getCode(); + + if ($inspector->getException() instanceof \ErrorException) { + // ErrorExceptions wrap the php-error types within the "severity" property + $code = Misc::translateErrorCode($inspector->getException()->getSeverity()); + } + + // Detect frames that belong to the application. + if ($this->applicationPaths) { + /* @var \Whoops\Exception\Frame $frame */ + foreach ($frames as $frame) { + foreach ($this->applicationPaths as $path) { + if (substr($frame->getFile(), 0, strlen($path)) === $path) { + $frame->setApplication(true); + break; + } + } + } + } + + // Nicely format the session object + $session = isset($_SESSION) ? $this->masked($_SESSION, '_SESSION') : []; + $session = ['session' => Util::prettyPrintArray($session)]; + + // List of variables that will be passed to the layout template. + $vars = [ + "page_title" => $this->getPageTitle(), + + // @todo: Asset compiler + "stylesheet" => file_get_contents($cssFile), + "zepto" => file_get_contents($zeptoFile), + "clipboard" => file_get_contents($clipboard), + "javascript" => file_get_contents($jsFile), + + // Template paths: + "header" => $this->getResource("views/header.html.php"), + "header_outer" => $this->getResource("views/header_outer.html.php"), + "frame_list" => $this->getResource("views/frame_list.html.php"), + "frames_description" => $this->getResource("views/frames_description.html.php"), + "frames_container" => $this->getResource("views/frames_container.html.php"), + "panel_details" => $this->getResource("views/panel_details.html.php"), + "panel_details_outer" => $this->getResource("views/panel_details_outer.html.php"), + "panel_left" => $this->getResource("views/panel_left.html.php"), + "panel_left_outer" => $this->getResource("views/panel_left_outer.html.php"), + "frame_code" => $this->getResource("views/frame_code.html.php"), + "env_details" => $this->getResource("views/env_details.html.php"), + + "title" => $this->getPageTitle(), + "name" => explode("\\", $inspector->getExceptionName()), + "message" => $inspector->getException()->getMessage(), + "code" => $code, + "plain_exception" => Formatter::formatExceptionPlain($inspector), + "frames" => $frames, + "has_frames" => !!count($frames), + "handler" => $this, + "handlers" => [$this], + + "active_frames_tab" => count($frames) && $frames->offsetGet(0)->isApplication() ? 'application' : 'all', + "has_frames_tabs" => $this->getApplicationPaths(), + + "tables" => [ + "GET Data" => $this->masked($_GET, '_GET'), + "POST Data" => $this->masked($_POST, '_POST'), + "Files" => isset($_FILES) ? $this->masked($_FILES, '_FILES') : [], + "Cookies" => $this->masked($_COOKIE, '_COOKIE'), + "Session" => $session, + "Server/Request Data" => $this->masked($_SERVER, '_SERVER'), + "Environment Variables" => $this->masked($_ENV, '_ENV'), + ], + ]; + + if (isset($customCssFile)) { + $vars["stylesheet"] .= file_get_contents($customCssFile); + } + + // Add extra entries list of data tables: + // @todo: Consolidate addDataTable and addDataTableCallback + $extraTables = array_map(function ($table) use ($inspector) { + return $table instanceof \Closure ? $table($inspector) : $table; + }, $this->getDataTables()); + $vars["tables"] = array_merge($extraTables, $vars["tables"]); + + $plainTextHandler = new PlainTextHandler(); + $plainTextHandler->setException($this->getException()); + $plainTextHandler->setInspector($this->getInspector()); + $vars["preface"] = "<!--\n\n\n" . $plainTextHandler->generateResponse() . "\n\n\n\n\n\n\n\n\n\n\n-->"; + + $this->templateHelper->setVariables($vars); + + ob_start(); + $this->templateHelper->render($templateFile); + + $result = ob_get_clean(); + return $result; + } + + /** + * Adds an entry to the list of tables displayed in the template. + * The expected data is a simple associative array. Any nested arrays + * will be flattened with print_r + * @param string $label + * @param array $data + */ + public function addDataTable($label, array $data) + { + $this->extraTables[$label] = $data; + } + + /** + * Lazily adds an entry to the list of tables displayed in the table. + * The supplied callback argument will be called when the error is rendered, + * it should produce a simple associative array. Any nested arrays will + * be flattened with print_r. + * + * @throws InvalidArgumentException If $callback is not callable + * @param string $label + * @param callable $callback Callable returning an associative array + */ + public function addDataTableCallback($label, /* callable */ $callback) + { + if (!is_callable($callback)) { + throw new InvalidArgumentException('Expecting callback argument to be callable'); + } + + $this->extraTables[$label] = function (\Whoops\Exception\Inspector $inspector = null) use ($callback) { + try { + $result = call_user_func($callback, $inspector); + + // Only return the result if it can be iterated over by foreach(). + return is_array($result) || $result instanceof \Traversable ? $result : []; + } catch (\Exception $e) { + // Don't allow failure to break the rendering of the original exception. + return []; + } + }; + } + + /** + * blacklist a sensitive value within one of the superglobal arrays. + * + * @param $superGlobalName string the name of the superglobal array, e.g. '_GET' + * @param $key string the key within the superglobal + */ + public function blacklist($superGlobalName, $key) + { + $this->blacklist[$superGlobalName][] = $key; + } + + /** + * Returns all the extra data tables registered with this handler. + * Optionally accepts a 'label' parameter, to only return the data + * table under that label. + * @param string|null $label + * @return array[]|callable + */ + public function getDataTables($label = null) + { + if ($label !== null) { + return isset($this->extraTables[$label]) ? + $this->extraTables[$label] : []; + } + + return $this->extraTables; + } + + /** + * Allows to disable all attempts to dynamically decide whether to + * handle or return prematurely. + * Set this to ensure that the handler will perform no matter what. + * @param bool|null $value + * @return bool|null + */ + public function handleUnconditionally($value = null) + { + if (func_num_args() == 0) { + return $this->handleUnconditionally; + } + + $this->handleUnconditionally = (bool) $value; + } + + /** + * Adds an editor resolver, identified by a string + * name, and that may be a string path, or a callable + * resolver. If the callable returns a string, it will + * be set as the file reference's href attribute. + * + * @example + * $run->addEditor('macvim', "mvim://open?url=file://%file&line=%line") + * @example + * $run->addEditor('remove-it', function($file, $line) { + * unlink($file); + * return "http://stackoverflow.com"; + * }); + * @param string $identifier + * @param string $resolver + */ + public function addEditor($identifier, $resolver) + { + $this->editors[$identifier] = $resolver; + } + + /** + * Set the editor to use to open referenced files, by a string + * identifier, or a callable that will be executed for every + * file reference, with a $file and $line argument, and should + * return a string. + * + * @example + * $run->setEditor(function($file, $line) { return "file:///{$file}"; }); + * @example + * $run->setEditor('sublime'); + * + * @throws InvalidArgumentException If invalid argument identifier provided + * @param string|callable $editor + */ + public function setEditor($editor) + { + if (!is_callable($editor) && !isset($this->editors[$editor])) { + throw new InvalidArgumentException( + "Unknown editor identifier: $editor. Known editors:" . + implode(",", array_keys($this->editors)) + ); + } + + $this->editor = $editor; + } + + /** + * Given a string file path, and an integer file line, + * executes the editor resolver and returns, if available, + * a string that may be used as the href property for that + * file reference. + * + * @throws InvalidArgumentException If editor resolver does not return a string + * @param string $filePath + * @param int $line + * @return string|bool + */ + public function getEditorHref($filePath, $line) + { + $editor = $this->getEditor($filePath, $line); + + if (empty($editor)) { + return false; + } + + // Check that the editor is a string, and replace the + // %line and %file placeholders: + if (!isset($editor['url']) || !is_string($editor['url'])) { + throw new UnexpectedValueException( + __METHOD__ . " should always resolve to a string or a valid editor array; got something else instead." + ); + } + + $editor['url'] = str_replace("%line", rawurlencode($line), $editor['url']); + $editor['url'] = str_replace("%file", rawurlencode($filePath), $editor['url']); + + return $editor['url']; + } + + /** + * Given a boolean if the editor link should + * act as an Ajax request. The editor must be a + * valid callable function/closure + * + * @throws UnexpectedValueException If editor resolver does not return a boolean + * @param string $filePath + * @param int $line + * @return bool + */ + public function getEditorAjax($filePath, $line) + { + $editor = $this->getEditor($filePath, $line); + + // Check that the ajax is a bool + if (!isset($editor['ajax']) || !is_bool($editor['ajax'])) { + throw new UnexpectedValueException( + __METHOD__ . " should always resolve to a bool; got something else instead." + ); + } + return $editor['ajax']; + } + + /** + * @param string $title + * @return void + */ + public function setPageTitle($title) + { + $this->pageTitle = (string) $title; + } + + /** + * @return string + */ + public function getPageTitle() + { + return $this->pageTitle; + } + + /** + * Adds a path to the list of paths to be searched for + * resources. + * + * @throws InvalidArgumentException If $path is not a valid directory + * + * @param string $path + * @return void + */ + public function addResourcePath($path) + { + if (!is_dir($path)) { + throw new InvalidArgumentException( + "'$path' is not a valid directory" + ); + } + + array_unshift($this->searchPaths, $path); + } + + /** + * Adds a custom css file to be loaded. + * + * @param string $name + * @return void + */ + public function addCustomCss($name) + { + $this->customCss = $name; + } + + /** + * @return array + */ + public function getResourcePaths() + { + return $this->searchPaths; + } + + /** + * @deprecated + * + * @return string + */ + public function getResourcesPath() + { + $allPaths = $this->getResourcePaths(); + + // Compat: return only the first path added + return end($allPaths) ?: null; + } + + /** + * @deprecated + * + * @param string $resourcesPath + * @return void + */ + public function setResourcesPath($resourcesPath) + { + $this->addResourcePath($resourcesPath); + } + + /** + * Return the application paths. + * + * @return array + */ + public function getApplicationPaths() + { + return $this->applicationPaths; + } + + /** + * Set the application paths. + * + * @param array $applicationPaths + */ + public function setApplicationPaths($applicationPaths) + { + $this->applicationPaths = $applicationPaths; + } + + /** + * Set the application root path. + * + * @param string $applicationRootPath + */ + public function setApplicationRootPath($applicationRootPath) + { + $this->templateHelper->setApplicationRootPath($applicationRootPath); + } + + /** + * Given a boolean if the editor link should + * act as an Ajax request. The editor must be a + * valid callable function/closure + * + * @param string $filePath + * @param int $line + * @return array + */ + protected function getEditor($filePath, $line) + { + if (!$this->editor || (!is_string($this->editor) && !is_callable($this->editor))) { + return []; + } + + if (is_string($this->editor) && isset($this->editors[$this->editor]) && !is_callable($this->editors[$this->editor])) { + return [ + 'ajax' => false, + 'url' => $this->editors[$this->editor], + ]; + } + + if (is_callable($this->editor) || (isset($this->editors[$this->editor]) && is_callable($this->editors[$this->editor]))) { + if (is_callable($this->editor)) { + $callback = call_user_func($this->editor, $filePath, $line); + } else { + $callback = call_user_func($this->editors[$this->editor], $filePath, $line); + } + + if (is_string($callback)) { + return [ + 'ajax' => false, + 'url' => $callback, + ]; + } + + return [ + 'ajax' => isset($callback['ajax']) ? $callback['ajax'] : false, + 'url' => isset($callback['url']) ? $callback['url'] : $callback, + ]; + } + + return []; + } + + /** + * @return \Throwable + */ + protected function getException() + { + return $this->exception; + } + + /** + * @return Inspector + */ + protected function getInspector() + { + return $this->inspector; + } + + /** + * Finds a resource, by its relative path, in all available search paths. + * The search is performed starting at the last search path, and all the + * way back to the first, enabling a cascading-type system of overrides + * for all resources. + * + * @throws RuntimeException If resource cannot be found in any of the available paths + * + * @param string $resource + * @return string + */ + protected function getResource($resource) + { + // If the resource was found before, we can speed things up + // by caching its absolute, resolved path: + if (isset($this->resourceCache[$resource])) { + return $this->resourceCache[$resource]; + } + + // Search through available search paths, until we find the + // resource we're after: + foreach ($this->searchPaths as $path) { + $fullPath = $path . "/$resource"; + + if (is_file($fullPath)) { + // Cache the result: + $this->resourceCache[$resource] = $fullPath; + return $fullPath; + } + } + + // If we got this far, nothing was found. + throw new RuntimeException( + "Could not find resource '$resource' in any resource paths." + . "(searched: " . join(", ", $this->searchPaths). ")" + ); + } + + /** + * Checks all values within the given superGlobal array. + * Blacklisted values will be replaced by a equal length string cointaining only '*' characters. + * + * We intentionally dont rely on $GLOBALS as it depends on 'auto_globals_jit' php.ini setting. + * + * @param array $superGlobal One of the superglobal arrays + * @param string $superGlobalName the name of the superglobal array, e.g. '_GET' + * @return array $values without sensitive data + */ + private function masked(array $superGlobal, $superGlobalName) + { + $blacklisted = $this->blacklist[$superGlobalName]; + + $values = $superGlobal; + foreach($blacklisted as $key) { + if (isset($superGlobal[$key])) { + $values[$key] = str_repeat('*', strlen($superGlobal[$key])); + } + } + return $values; + } +} diff --git a/login/app/sprinkles/core/src/Error/Renderer/XmlRenderer.php b/login/app/sprinkles/core/src/Error/Renderer/XmlRenderer.php new file mode 100755 index 0000000..52e71cf --- /dev/null +++ b/login/app/sprinkles/core/src/Error/Renderer/XmlRenderer.php @@ -0,0 +1,48 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Error\Renderer; + +/** + * Default XML Error Renderer + */ +class XmlRenderer extends ErrorRenderer +{ + /** + * @return string + */ + public function render() + { + $e = $this->exception; + $xml = "<error>\n <message>UserFrosting Application Error</message>\n"; + if ($this->displayErrorDetails) { + do { + $xml .= " <exception>\n"; + $xml .= " <type>" . get_class($e) . "</type>\n"; + $xml .= " <code>" . $e->getCode() . "</code>\n"; + $xml .= " <message>" . $this->createCdataSection($e->getMessage()) . "</message>\n"; + $xml .= " <file>" . $e->getFile() . "</file>\n"; + $xml .= " <line>" . $e->getLine() . "</line>\n"; + $xml .= " </exception>\n"; + } while ($e = $e->getPrevious()); + } + $xml .= "</error>"; + + return $xml; + } + + /** + * Returns a CDATA section with the given content. + * + * @param string $content + * @return string + */ + private function createCdataSection($content) + { + return sprintf('<![CDATA[%s]]>', str_replace(']]>', ']]]]><![CDATA[>', $content)); + } +} diff --git a/login/app/sprinkles/core/src/Facades/Debug.php b/login/app/sprinkles/core/src/Facades/Debug.php new file mode 100755 index 0000000..86ef450 --- /dev/null +++ b/login/app/sprinkles/core/src/Facades/Debug.php @@ -0,0 +1,28 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Facades; + +use UserFrosting\System\Facade; + +/** + * Implements facade for the "debugLogger" service + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class Debug extends Facade +{ + /** + * Get the registered name of the component. + * + * @return string + */ + protected static function getFacadeAccessor() + { + return 'debugLogger'; + } +} diff --git a/login/app/sprinkles/core/src/Facades/Translator.php b/login/app/sprinkles/core/src/Facades/Translator.php new file mode 100755 index 0000000..e6fcccc --- /dev/null +++ b/login/app/sprinkles/core/src/Facades/Translator.php @@ -0,0 +1,28 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Facades; + +use UserFrosting\System\Facade; + +/** + * Implements facade for the "translator" service + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class Translator extends Facade +{ + /** + * Get the registered name of the component. + * + * @return string + */ + protected static function getFacadeAccessor() + { + return 'translator'; + } +} diff --git a/login/app/sprinkles/core/src/Http/Concerns/DeterminesContentType.php b/login/app/sprinkles/core/src/Http/Concerns/DeterminesContentType.php new file mode 100755 index 0000000..e963afa --- /dev/null +++ b/login/app/sprinkles/core/src/Http/Concerns/DeterminesContentType.php @@ -0,0 +1,76 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Http\Concerns; + +use Psr\Http\Message\ServerRequestInterface; + +/** + * Trait for classes that need to determine a request's accepted content type(s). + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +trait DeterminesContentType +{ + /** + * Known handled content types + * + * @var array + */ + protected $knownContentTypes = [ + 'application/json', + 'application/xml', + 'text/xml', + 'text/html', + 'text/plain' + ]; + + /** + * Determine which content type we know about is wanted using Accept header + * + * Note: This method is a bare-bones implementation designed specifically for + * Slim's error handling requirements. Consider a fully-feature solution such + * as willdurand/negotiation for any other situation. + * + * @param ServerRequestInterface $request + * @return string + */ + protected function determineContentType(ServerRequestInterface $request, $ajaxDebug = false) + { + // For AJAX requests, if AJAX debugging is turned on, always return html + if ($ajaxDebug && $request->isXhr()) { + return 'text/html'; + } + + $acceptHeader = $request->getHeaderLine('Accept'); + $selectedContentTypes = array_intersect(explode(',', $acceptHeader), $this->knownContentTypes); + $count = count($selectedContentTypes); + + if ($count) { + $current = current($selectedContentTypes); + + /** + * Ensure other supported content types take precedence over text/plain + * when multiple content types are provided via Accept header. + */ + if ($current === 'text/plain' && $count > 1) { + return next($selectedContentTypes); + } + + return $current; + } + + if (preg_match('/\+(json|xml)/', $acceptHeader, $matches)) { + $mediaType = 'application/' . $matches[1]; + if (in_array($mediaType, $this->knownContentTypes)) { + return $mediaType; + } + } + + return 'text/html'; + } +} diff --git a/login/app/sprinkles/core/src/Log/DatabaseHandler.php b/login/app/sprinkles/core/src/Log/DatabaseHandler.php new file mode 100755 index 0000000..c78308c --- /dev/null +++ b/login/app/sprinkles/core/src/Log/DatabaseHandler.php @@ -0,0 +1,53 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Log; + +use Monolog\Logger; +use Monolog\Handler\AbstractProcessingHandler; + +/** + * Monolog handler for storing the record to a database. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class DatabaseHandler extends AbstractProcessingHandler +{ + /** + * @var UserFrosting\Sprinkle\Core\Util\ClassMapper + */ + protected $classMapper; + + /** + * @var string + */ + protected $modelIdentifier; + + /** + * Create a new DatabaseHandler object. + * + * @param ClassMapper $classMapper Maps the modelIdentifier to the specific Eloquent model. + * @param string $modelIdentifier + * @param int $level The minimum logging level at which this handler will be triggered + * @param Boolean $bubble Whether the messages that are handled can bubble up the stack or not + */ + public function __construct($classMapper, $modelIdentifier, $level = Logger::DEBUG, $bubble = true) + { + $this->classMapper = $classMapper; + $this->modelName = $modelIdentifier; + parent::__construct($level, $bubble); + } + + /** + * {@inheritDoc} + */ + protected function write(array $record) + { + $log = $this->classMapper->createInstance($this->modelName, $record['extra']); + $log->save(); + } +} diff --git a/login/app/sprinkles/core/src/Log/MixedFormatter.php b/login/app/sprinkles/core/src/Log/MixedFormatter.php new file mode 100755 index 0000000..beae788 --- /dev/null +++ b/login/app/sprinkles/core/src/Log/MixedFormatter.php @@ -0,0 +1,59 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Log; + +use Monolog\Formatter\LineFormatter; + +/** + * Monolog formatter for pretty-printing arrays and objects. + * + * This class extends the basic Monolog LineFormatter class, and provides basically the same functionality but with one exception: + * if the second parameter of any logging method (debug, error, info, etc) is an array, it will print it as a nicely formatted, + * multi-line JSON object instead of all on a single line. + * @author Alex Weissman (https://alexanderweissman.com) + */ +class MixedFormatter extends LineFormatter +{ + + /** + * Return the JSON representation of a value + * + * @param mixed $data + * @param bool $ignoreErrors + * @throws \RuntimeException if encoding fails and errors are not ignored + * @return string + */ + protected function toJson($data, $ignoreErrors = false) + { + // suppress json_encode errors since it's twitchy with some inputs + if ($ignoreErrors) { + return @$this->jsonEncodePretty($data); + } + + $json = $this->jsonEncodePretty($data); + + if ($json === false) { + $json = $this->handleJsonError(json_last_error(), $data); + } + + return $json; + } + + /** + * @param mixed $data + * @return string JSON encoded data or null on failure + */ + private function jsonEncodePretty($data) + { + if (version_compare(PHP_VERSION, '5.4.0', '>=')) { + return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + + return json_encode($data); + } +} diff --git a/login/app/sprinkles/core/src/Mail/EmailRecipient.php b/login/app/sprinkles/core/src/Mail/EmailRecipient.php new file mode 100755 index 0000000..0b9381a --- /dev/null +++ b/login/app/sprinkles/core/src/Mail/EmailRecipient.php @@ -0,0 +1,136 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Mail; + +/** + * EmailRecipient Class + * + * A class representing a recipient for a MailMessage, with associated parameters. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class EmailRecipient +{ + + /** + * @var string The email address for this recipient. + */ + protected $email; + + /** + * @var string The name for this recipient. + */ + protected $name; + + /** + * @var array Any additional parameters (name => value) to use when rendering an email template for this recipient. + */ + protected $params = []; + + /** + * @var array A list of CCs for this recipient. Each CC is an associative array with `email` and `name` properties. + */ + protected $cc = []; + + /** + * @var array A list of BCCs for this recipient. Each BCC is an associative array with `email` and `name` properties. + */ + protected $bcc = []; + + /** + * Create a new EmailRecipient instance. + * + * @param string $email The primary recipient email address. + * @param string $name The primary recipient name. + * @param array $params An array of template parameters to render the email message with for this particular recipient. + */ + public function __construct($email, $name = "", $params = []) + { + $this->email = $email; + $this->name = $name; + $this->params = $params; + } + + /** + * Add a CC for this primary recipient. + * + * @param string $email The CC recipient email address. + * @param string $name The CC recipient name. + */ + public function cc($email, $name = "") + { + $this->cc[] = [ + "email" => $email, + "name" => $name + ]; + } + + /** + * Add a BCC for this primary recipient. + * + * @param string $email The BCC recipient email address. + * @param string $name The BCC recipient name. + */ + public function bcc($email, $name = "") + { + $this->bcc[] = [ + "email" => $email, + "name" => $name + ]; + } + + /** + * Get the primary recipient email address. + * + * @return string the primary recipient email address. + */ + public function getEmail() + { + return $this->email; + } + + /** + * Get the primary recipient name. + * + * @return string the primary recipient name. + */ + public function getName() + { + return $this->name; + } + + /** + * Get the parameters to use when rendering the template this recipient. + * + * @return array The parameters (name => value) to use when rendering an email template for this recipient. + */ + public function getParams() + { + return $this->params; + } + + /** + * Get the list of CCs for this recipient. + * + * @return array A list of CCs for this recipient. Each CC is an associative array with `email` and `name` properties. + */ + public function getCCs() + { + return $this->cc; + } + + /** + * Get the list of BCCs for this recipient. + * + * @return array A list of BCCs for this recipient. Each BCC is an associative array with `email` and `name` properties. + */ + public function getBCCs() + { + return $this->bcc; + } +} diff --git a/login/app/sprinkles/core/src/Mail/MailMessage.php b/login/app/sprinkles/core/src/Mail/MailMessage.php new file mode 100755 index 0000000..29bcf15 --- /dev/null +++ b/login/app/sprinkles/core/src/Mail/MailMessage.php @@ -0,0 +1,186 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Mail; + +/** + * MailMessage Class + * + * Represents a basic mail message, containing a static subject and body. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +abstract class MailMessage +{ + /** + * @var string The current sender email address. + */ + protected $fromEmail = ""; + + /** + * @var string The current sender name. + */ + protected $fromName = null; + + /** + * @var EmailRecipient[] A list of recipients for this message. + */ + protected $recipients = []; + + /** + * @var string The current reply-to email. + */ + protected $replyEmail = null; + + /** + * @var string The current reply-to name. + */ + protected $replyName = null; + + /** + * Gets the fully rendered text of the message body. + * + * @return string + */ + abstract public function renderBody($params = []); + + /** + * Gets the fully rendered text of the message subject. + * + * @return string + */ + abstract public function renderSubject($params = []); + + /** + * Add an email recipient. + * + * @param EmailRecipient $recipient + */ + public function addEmailRecipient(EmailRecipient $recipient) + { + $this->recipients[] = $recipient; + return $this; + } + + /** + * Clears out all recipients for this message. + */ + public function clearRecipients() + { + $this->recipients = array(); + } + + /** + * Set sender information for this message. + * + * This is a shortcut for calling setFromEmail, setFromName, setReplyEmail, and setReplyName. + * @param string $fromInfo An array containing 'email', 'name', 'reply_email', and 'reply_name'. + */ + public function from($fromInfo = []) + { + $this->setFromEmail(isset($fromInfo['email']) ? $fromInfo['email'] : ""); + $this->setFromName(isset($fromInfo['name']) ? $fromInfo['name'] : null); + $this->setReplyEmail(isset($fromInfo['reply_email']) ? $fromInfo['reply_email'] : null); + $this->setReplyName(isset($fromInfo['reply_name']) ? $fromInfo['reply_name'] : null); + + return $this; + } + + /** + * Get the sender email address. + * + * @return string + */ + public function getFromEmail() + { + return $this->fromEmail; + } + + /** + * Get the sender name. Defaults to the email address if name is not set. + * + * @return string + */ + public function getFromName() + { + return isset($this->fromName) ? $this->fromName : $this->getFromEmail(); + } + + /** + * Get the list of recipients for this message. + * + * @return EmailRecipient[] + */ + public function getRecipients() + { + return $this->recipients; + } + + /** + * Get the 'reply-to' address for this message. Defaults to the sender email. + * + * @return string + */ + public function getReplyEmail() + { + return isset($this->replyEmail) ? $this->replyEmail : $this->getFromEmail(); + } + + /** + * Get the 'reply-to' name for this message. Defaults to the sender name. + * + * @return string + */ + public function getReplyName() + { + return isset($this->replyName) ? $this->replyName : $this->getFromName(); + } + + /** + * Set the sender email address. + * + * @param string $fromEmail + */ + public function setFromEmail($fromEmail) + { + $this->fromEmail = $fromEmail; + return $this; + } + + /** + * Set the sender name. + * + * @param string $fromName + */ + public function setFromName($fromName) + { + $this->fromName = $fromName; + return $this; + } + + /** + * Set the sender 'reply-to' address. + * + * @param string $replyEmail + */ + public function setReplyEmail($replyEmail) + { + $this->replyEmail = $replyEmail; + return $this; + } + + /** + * Set the sender 'reply-to' name. + * + * @param string $replyName + */ + public function setReplyName($replyName) + { + $this->replyName = $replyName; + return $this; + } +} diff --git a/login/app/sprinkles/core/src/Mail/Mailer.php b/login/app/sprinkles/core/src/Mail/Mailer.php new file mode 100755 index 0000000..5b346b4 --- /dev/null +++ b/login/app/sprinkles/core/src/Mail/Mailer.php @@ -0,0 +1,204 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Mail; + +use Monolog\Logger; + +/** + * Mailer Class + * + * A basic wrapper for sending template-based emails. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class Mailer +{ + /** + * @var Logger + */ + protected $logger; + + /** + * @var \PHPMailer + */ + protected $phpMailer; + + /** + * Create a new Mailer instance. + * + * @param Logger $logger A Monolog logger, used to dump debugging info for SMTP server transactions. + * @param mixed[] $config An array of configuration parameters for phpMailer. + * @throws \phpmailerException Wrong mailer config value given. + */ + public function __construct($logger, $config = []) + { + $this->logger = $logger; + + // 'true' tells PHPMailer to use exceptions instead of error codes + $this->phpMailer = new \PHPMailer(true); + + // Configuration options + if (isset($config['mailer'])) { + if (!in_array($config['mailer'], ['smtp', 'mail', 'qmail', 'sendmail'])) { + throw new \phpmailerException("'mailer' must be one of 'smtp', 'mail', 'qmail', or 'sendmail'."); + } + + if ($config['mailer'] == 'smtp') { + $this->phpMailer->isSMTP(true); + $this->phpMailer->Host = $config['host']; + $this->phpMailer->Port = $config['port']; + $this->phpMailer->SMTPAuth = $config['auth']; + $this->phpMailer->SMTPSecure = $config['secure']; + $this->phpMailer->Username = $config['username']; + $this->phpMailer->Password = $config['password']; + $this->phpMailer->SMTPDebug = $config['smtp_debug']; + + if (isset($config['smtp_options'])) { + $this->phpMailer->SMTPOptions = $config['smtp_options']; + } + } + + // Set any additional message-specific options + // TODO: enforce which options can be set through this subarray + if (isset($config['message_options'])) { + $this->setOptions($config['message_options']); + } + } + + // Pass logger into phpMailer object + $this->phpMailer->Debugoutput = function($message, $level) { + $this->logger->debug($message); + }; + } + + /** + * Get the underlying PHPMailer object. + * + * @return \PHPMailer + */ + public function getPhpMailer() + { + return $this->phpMailer; + } + + /** + * Send a MailMessage message. + * + * Sends a single email to all recipients, as well as their CCs and BCCs. + * Since it is a single-header message, recipient-specific template data will not be included. + * @param MailMessage $message + * @param bool $clearRecipients Set to true to clear the list of recipients in the message after calling send(). This helps avoid accidentally sending a message multiple times. + * @throws \phpmailerException The message could not be sent. + */ + public function send(MailMessage $message, $clearRecipients = true) + { + $this->phpMailer->From = $message->getFromEmail(); + $this->phpMailer->FromName = $message->getFromName(); + $this->phpMailer->addReplyTo($message->getReplyEmail(), $message->getReplyName()); + + // Add all email recipients, as well as their CCs and BCCs + foreach ($message->getRecipients() as $recipient) { + $this->phpMailer->addAddress($recipient->getEmail(), $recipient->getName()); + + // Add any CCs and BCCs + if ($recipient->getCCs()) { + foreach($recipient->getCCs() as $cc) { + $this->phpMailer->addCC($cc['email'], $cc['name']); + } + } + + if ($recipient->getBCCs()) { + foreach($recipient->getBCCs() as $bcc) { + $this->phpMailer->addBCC($bcc['email'], $bcc['name']); + } + } + } + + $this->phpMailer->Subject = $message->renderSubject(); + $this->phpMailer->Body = $message->renderBody(); + + // Try to send the mail. Will throw an exception on failure. + $this->phpMailer->send(); + + // Clear recipients from the PHPMailer object for this iteration, + // so that we can use the same object for other emails. + $this->phpMailer->clearAllRecipients(); + + // Clear out the MailMessage's internal recipient list + if ($clearRecipients) { + $message->clearRecipients(); + } + } + + /** + * Send a MailMessage message, sending a separate email to each recipient. + * + * If the message object supports message templates, this will render the template with the corresponding placeholder values for each recipient. + * @param MailMessage $message + * @param bool $clearRecipients Set to true to clear the list of recipients in the message after calling send(). This helps avoid accidentally sending a message multiple times. + * @throws \phpmailerException The message could not be sent. + */ + public function sendDistinct(MailMessage $message, $clearRecipients = true) + { + $this->phpMailer->From = $message->getFromEmail(); + $this->phpMailer->FromName = $message->getFromName(); + $this->phpMailer->addReplyTo($message->getReplyEmail(), $message->getReplyName()); + + // Loop through email recipients, sending customized content to each one + foreach ($message->getRecipients() as $recipient) { + $this->phpMailer->addAddress($recipient->getEmail(), $recipient->getName()); + + // Add any CCs and BCCs + if ($recipient->getCCs()) { + foreach($recipient->getCCs() as $cc) { + $this->phpMailer->addCC($cc['email'], $cc['name']); + } + } + + if ($recipient->getBCCs()) { + foreach($recipient->getBCCs() as $bcc) { + $this->phpMailer->addBCC($bcc['email'], $bcc['name']); + } + } + + $this->phpMailer->Subject = $message->renderSubject($recipient->getParams()); + $this->phpMailer->Body = $message->renderBody($recipient->getParams()); + + // Try to send the mail. Will throw an exception on failure. + $this->phpMailer->send(); + + // Clear recipients from the PHPMailer object for this iteration, + // so that we can send a separate email to the next recipient. + $this->phpMailer->clearAllRecipients(); + } + + // Clear out the MailMessage's internal recipient list + if ($clearRecipients) { + $message->clearRecipients(); + } + } + + /** + * Set option(s) on the underlying phpMailer object. + * + * @param mixed[] $options + * @return Mailer + */ + public function setOptions($options) + { + if (isset($options['isHtml'])) { + $this->phpMailer->isHTML($options['isHtml']); + } + + foreach ($options as $name => $value) { + $this->phpMailer->set($name, $value); + } + + return $this; + } +} diff --git a/login/app/sprinkles/core/src/Mail/StaticMailMessage.php b/login/app/sprinkles/core/src/Mail/StaticMailMessage.php new file mode 100755 index 0000000..098bbfc --- /dev/null +++ b/login/app/sprinkles/core/src/Mail/StaticMailMessage.php @@ -0,0 +1,78 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Mail; + +/** + * StaticMailMessage Class + * + * Represents a basic mail message, containing a static subject and body. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class StaticMailMessage extends MailMessage +{ + /** + * @var string The default body for this message. + */ + protected $body; + + /** + * @var string The default subject for this message. + */ + protected $subject; + + /** + * Create a new MailMessage instance. + * + * @param string $subject + * @param string $body + */ + public function __construct($subject = "", $body = "") + { + $this->subject = $subject; + $this->body = $body; + } + + /** + * {@inheritDoc} + */ + public function renderBody($params = []) + { + return $this->body; + } + + /** + * {@inheritDoc} + */ + public function renderSubject($params = []) + { + return $this->subject; + } + + /** + * Set the text of the message subject. + * + * @param string $subject + */ + public function setSubject($subject) + { + $this->subject = $subject; + return $this; + } + + /** + * Set the text of the message body. + * + * @param string $body + */ + public function setBody($body) + { + $this->body = $body; + return $this; + } +} diff --git a/login/app/sprinkles/core/src/Mail/TwigMailMessage.php b/login/app/sprinkles/core/src/Mail/TwigMailMessage.php new file mode 100755 index 0000000..aa65240 --- /dev/null +++ b/login/app/sprinkles/core/src/Mail/TwigMailMessage.php @@ -0,0 +1,93 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Mail; + +/** + * MailMessage Class + * + * Represents a basic mail message, containing a static subject and body. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class TwigMailMessage extends MailMessage +{ + /** + * @var mixed[] A list of Twig placeholder values to use when rendering this message. + */ + protected $params; + + /** + * @var Twig_Template The Twig template object, to source the content for this message. + */ + protected $template; + + /** + * @var \Slim\Views\Twig The view object, used to render mail templates. + */ + protected $view; + + /** + * Create a new TwigMailMessage instance. + * + * @param Slim\Views\Twig $view The Twig view object used to render mail templates. + * @param string $filename optional Set the Twig template to use for this message. + */ + public function __construct($view, $filename = null) + { + $this->view = $view; + + $twig = $this->view->getEnvironment(); + // Must manually merge in global variables for block rendering + // TODO: should we keep this separate from the local parameters? + $this->params = $twig->getGlobals(); + + if ($filename !== null) { + $this->template = $twig->loadTemplate($filename); + } + } + + /** + * Merge in any additional global Twig variables to use when rendering this message. + * + * @param mixed[] $params + */ + public function addParams($params = []) + { + $this->params = array_replace_recursive($this->params, $params); + return $this; + } + + /** + * {@inheritDoc} + */ + public function renderSubject($params = []) + { + $params = array_replace_recursive($this->params, $params); + return $this->template->renderBlock('subject', $params); + } + + /** + * {@inheritDoc} + */ + public function renderBody($params = []) + { + $params = array_replace_recursive($this->params, $params); + return $this->template->renderBlock('body', $params); + } + + /** + * Sets the Twig template object for this message. + * + * @param Twig_Template $template The Twig template object, to source the content for this message. + */ + public function setTemplate($template) + { + $this->template = $template; + return $this; + } +} diff --git a/login/app/sprinkles/core/src/Model/UFModel.php b/login/app/sprinkles/core/src/Model/UFModel.php new file mode 100755 index 0000000..0d9feff --- /dev/null +++ b/login/app/sprinkles/core/src/Model/UFModel.php @@ -0,0 +1,27 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Model; + +use UserFrosting\Sprinkle\Core\Database\Models\Model; +use UserFrosting\Sprinkle\Core\Facades\Debug; + +/** + * UFModel Class + * + * @deprecated since 4.1 + * @author Alex Weissman (https://alexanderweissman.com) + */ +abstract class UFModel extends Model +{ + public function __construct(array $attributes = []) + { + Debug::debug("UFModel has been deprecated and will be removed in future versions. Please move your model " . static::class . " to Database/Models/ and have it extend the base Database/Models/Model class."); + + parent::__construct($attributes); + } +} diff --git a/login/app/sprinkles/core/src/Router.php b/login/app/sprinkles/core/src/Router.php new file mode 100755 index 0000000..8a10c85 --- /dev/null +++ b/login/app/sprinkles/core/src/Router.php @@ -0,0 +1,101 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core; + +use FastRoute\Dispatcher; +use Illuminate\Filesystem\Filesystem; +use InvalidArgumentException; +use RuntimeException; +use Psr\Http\Message\ServerRequestInterface; +use FastRoute\RouteCollector; +use FastRoute\RouteParser; +use FastRoute\RouteParser\Std as StdParser; +use FastRoute\DataGenerator; +use Slim\Interfaces\RouteGroupInterface; +use Slim\Interfaces\RouterInterface; +use Slim\Interfaces\RouteInterface; + +/** + * Router + * + * This class extends Slim's router, to permit overriding of routes with the same signature. + * @author Alex Weissman (https://alexanderweissman.com) + */ +class Router extends \Slim\Router implements RouterInterface +{ + + /* + * @var string[] a reverse lookup of route identifiers, indexed by route signature + */ + protected $identifiers; + + /** + * Add route + * + * @param string[] $methods Array of HTTP methods + * @param string $pattern The route pattern + * @param callable $handler The route callable + * + * @return RouteInterface + * + * @throws InvalidArgumentException if the route pattern isn't a string + */ + public function map($methods, $pattern, $handler) + { + if (!is_string($pattern)) { + throw new InvalidArgumentException('Route pattern must be a string'); + } + + // Prepend parent group pattern(s) + if ($this->routeGroups) { + $pattern = $this->processGroups() . $pattern; + } + + // According to RFC methods are defined in uppercase (See RFC 7231) + $methods = array_map("strtoupper", $methods); + + // Determine route signature + $signature = implode('-', $methods) . '-' . $pattern; + + // If a route with the same signature already exists, then we must replace it + if (isset($this->identifiers[$signature])) { + $route = new \Slim\Route($methods, $pattern, $handler, $this->routeGroups, str_replace('route', '', $this->identifiers[$signature])); + } else { + $route = new \Slim\Route($methods, $pattern, $handler, $this->routeGroups, $this->routeCounter); + } + + $this->routes[$route->getIdentifier()] = $route; + + // Record identifier in reverse lookup array + $this->identifiers[$signature] = $route->getIdentifier(); + + $this->routeCounter++; + + return $route; + } + + /** + * Delete the cache file + * + * @access public + * @return bool true/false if operation is successfull + */ + public function clearCache() + { + // Get Filesystem instance + $fs = new FileSystem; + + // Make sure file exist and delete it + if ($fs->exists($this->cacheFile)) { + return $fs->delete($this->cacheFile); + } + + // It's still considered a success if file doesn't exist + return true; + } +} diff --git a/login/app/sprinkles/core/src/ServicesProvider/ServicesProvider.php b/login/app/sprinkles/core/src/ServicesProvider/ServicesProvider.php new file mode 100755 index 0000000..5fcffbc --- /dev/null +++ b/login/app/sprinkles/core/src/ServicesProvider/ServicesProvider.php @@ -0,0 +1,618 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\ServicesProvider; + +use Dotenv\Dotenv; +use Dotenv\Exception\InvalidPathException; +use Illuminate\Container\Container; +use Illuminate\Database\Capsule\Manager as Capsule; +use Illuminate\Database\Events\QueryExecuted; +use Illuminate\Events\Dispatcher; +use Illuminate\Filesystem\Filesystem; +use Illuminate\Session\DatabaseSessionHandler; +use Illuminate\Session\FileSessionHandler; +use Interop\Container\ContainerInterface; +use League\FactoryMuffin\FactoryMuffin; +use League\FactoryMuffin\Faker\Facade as Faker; +use Monolog\Formatter\LineFormatter; +use Monolog\Handler\ErrorLogHandler; +use Monolog\Handler\StreamHandler; +use Monolog\Logger; +use Slim\Csrf\Guard; +use Slim\Http\Uri; +use Slim\Views\Twig; +use Slim\Views\TwigExtension; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\NullSessionHandler; +use UserFrosting\Assets\AssetBundleSchema; +use UserFrosting\Assets\AssetLoader; +use UserFrosting\Assets\AssetManager; +use UserFrosting\Assets\UrlBuilder\AssetUrlBuilder; +use UserFrosting\Assets\UrlBuilder\CompiledAssetUrlBuilder; +use UserFrosting\Cache\TaggableFileStore; +use UserFrosting\Cache\MemcachedStore; +use UserFrosting\Cache\RedisStore; +use UserFrosting\Config\ConfigPathBuilder; +use UserFrosting\I18n\LocalePathBuilder; +use UserFrosting\I18n\MessageTranslator; +use UserFrosting\Session\Session; +use UserFrosting\Sprinkle\Core\Error\ExceptionHandlerManager; +use UserFrosting\Sprinkle\Core\Error\Handler\NotFoundExceptionHandler; +use UserFrosting\Sprinkle\Core\Log\MixedFormatter; +use UserFrosting\Sprinkle\Core\Mail\Mailer; +use UserFrosting\Sprinkle\Core\Alert\CacheAlertStream; +use UserFrosting\Sprinkle\Core\Alert\SessionAlertStream; +use UserFrosting\Sprinkle\Core\Router; +use UserFrosting\Sprinkle\Core\Throttle\Throttler; +use UserFrosting\Sprinkle\Core\Throttle\ThrottleRule; +use UserFrosting\Sprinkle\Core\Twig\CoreExtension; +use UserFrosting\Sprinkle\Core\Util\CheckEnvironment; +use UserFrosting\Sprinkle\Core\Util\ClassMapper; +use UserFrosting\Support\Exception\BadRequestException; +use UserFrosting\Support\Exception\NotFoundException; +use UserFrosting\Support\Repository\Loader\ArrayFileLoader; +use UserFrosting\Support\Repository\Repository; + +/** + * UserFrosting core services provider. + * + * Registers core services for UserFrosting, such as config, database, asset manager, translator, etc. + * @author Alex Weissman (https://alexanderweissman.com) + */ +class ServicesProvider +{ + /** + * Register UserFrosting's core services. + * + * @param ContainerInterface $container A DI container implementing ArrayAccess and container-interop. + */ + public function register(ContainerInterface $container) + { + /** + * Flash messaging service. + * + * Persists error/success messages between requests in the session. + */ + $container['alerts'] = function ($c) { + $config = $c->config; + + if ($config['alert.storage'] == 'cache') { + return new CacheAlertStream($config['alert.key'], $c->translator, $c->cache, $c->config); + } elseif ($config['alert.storage'] == 'session') { + return new SessionAlertStream($config['alert.key'], $c->translator, $c->session); + } else { + throw new \Exception("Bad alert storage handler type '{$config['alert.storage']}' specified in configuration file."); + } + }; + + /** + * Asset loader service. + * + * Loads assets from a specified relative location. + * Assets are Javascript, CSS, image, and other files used by your site. + */ + $container['assetLoader'] = function ($c) { + $basePath = \UserFrosting\SPRINKLES_DIR; + $pattern = "/^[A-Za-z0-9_\-]+\/assets\//"; + + $al = new AssetLoader($basePath, $pattern); + return $al; + }; + + /** + * Asset manager service. + * + * Loads raw or compiled asset information from your bundle.config.json schema file. + * Assets are Javascript, CSS, image, and other files used by your site. + */ + $container['assets'] = function ($c) { + $config = $c->config; + $locator = $c->locator; + + // Load asset schema + if ($config['assets.use_raw']) { + $baseUrl = $config['site.uri.public'] . '/' . $config['assets.raw.path']; + $removePrefix = \UserFrosting\APP_DIR_NAME . \UserFrosting\DS . \UserFrosting\SPRINKLES_DIR_NAME; + $aub = new AssetUrlBuilder($locator, $baseUrl, $removePrefix, 'assets'); + + $as = new AssetBundleSchema($aub); + + // Load Sprinkle assets + $sprinkles = $c->sprinkleManager->getSprinkleNames(); + + // TODO: move this out into PathBuilder and Loader classes in userfrosting/assets + // This would also allow us to define and load bundles in themes + $bundleSchemas = array_reverse($locator->findResources('sprinkles://' . $config['assets.raw.schema'], true, true)); + + foreach ($bundleSchemas as $schema) { + if (file_exists($schema)) { + $as->loadRawSchemaFile($schema); + } + } + } else { + $baseUrl = $config['site.uri.public'] . '/' . $config['assets.compiled.path']; + $aub = new CompiledAssetUrlBuilder($baseUrl); + + $as = new AssetBundleSchema($aub); + $as->loadCompiledSchemaFile($locator->findResource("build://" . $config['assets.compiled.schema'], true, true)); + } + + $am = new AssetManager($aub, $as); + + return $am; + }; + + /** + * Cache service. + * + * @return \Illuminate\Cache\Repository + */ + $container['cache'] = function ($c) { + + $config = $c->config; + + if ($config['cache.driver'] == 'file') { + $path = $c->locator->findResource('cache://', true, true); + $cacheStore = new TaggableFileStore($path); + } elseif ($config['cache.driver'] == 'memcached') { + // We need to inject the prefix in the memcached config + $config = array_merge($config['cache.memcached'], ['prefix' => $config['cache.prefix']]); + $cacheStore = new MemcachedStore($config); + } elseif ($config['cache.driver'] == 'redis') { + // We need to inject the prefix in the redis config + $config = array_merge($config['cache.redis'], ['prefix' => $config['cache.prefix']]); + $cacheStore = new RedisStore($config); + } else { + throw new \Exception("Bad cache store type '{$config['cache.driver']}' specified in configuration file."); + } + + return $cacheStore->instance(); + }; + + /** + * Middleware to check environment. + * + * @todo We should cache the results of this, the first time that it succeeds. + */ + $container['checkEnvironment'] = function ($c) { + $checkEnvironment = new CheckEnvironment($c->view, $c->locator, $c->cache); + return $checkEnvironment; + }; + + /** + * Class mapper. + * + * Creates an abstraction on top of class names to allow extending them in sprinkles. + */ + $container['classMapper'] = function ($c) { + $classMapper = new ClassMapper(); + $classMapper->setClassMapping('query_builder', 'UserFrosting\Sprinkle\Core\Database\Builder'); + $classMapper->setClassMapping('throttle', 'UserFrosting\Sprinkle\Core\Database\Models\Throttle'); + return $classMapper; + }; + + /** + * Site config service (separate from Slim settings). + * + * Will attempt to automatically determine which config file(s) to use based on the value of the UF_MODE environment variable. + */ + $container['config'] = function ($c) { + // Grab any relevant dotenv variables from the .env file + try { + $dotenv = new Dotenv(\UserFrosting\APP_DIR); + $dotenv->load(); + } catch (InvalidPathException $e) { + // Skip loading the environment config file if it doesn't exist. + } + + // Get configuration mode from environment + $mode = getenv('UF_MODE') ?: ''; + + // Construct and load config repository + $builder = new ConfigPathBuilder($c->locator, 'config://'); + $loader = new ArrayFileLoader($builder->buildPaths($mode)); + $config = new Repository($loader->load()); + + // Construct base url from components, if not explicitly specified + if (!isset($config['site.uri.public'])) { + $base_uri = $config['site.uri.base']; + + $public = new Uri( + $base_uri['scheme'], + $base_uri['host'], + $base_uri['port'], + $base_uri['path'] + ); + + // Slim\Http\Uri likes to add trailing slashes when the path is empty, so this fixes that. + $config['site.uri.public'] = trim($public, '/'); + } + + // Hacky fix to prevent sessions from being hit too much: ignore CSRF middleware for requests for raw assets ;-) + // See https://github.com/laravel/framework/issues/8172#issuecomment-99112012 for more information on why it's bad to hit Laravel sessions multiple times in rapid succession. + $csrfBlacklist = $config['csrf.blacklist']; + $csrfBlacklist['^/' . $config['assets.raw.path']] = [ + 'GET' + ]; + + $config->set('csrf.blacklist', $csrfBlacklist); + + return $config; + }; + + /** + * Initialize CSRF guard middleware. + * + * @see https://github.com/slimphp/Slim-Csrf + */ + $container['csrf'] = function ($c) { + $csrfKey = $c->config['session.keys.csrf']; + + // Workaround so that we can pass storage into CSRF guard. + // If we tried to directly pass the indexed portion of `session` (for example, $c->session['site.csrf']), + // we would get an 'Indirect modification of overloaded element of UserFrosting\Session\Session' error. + // If we tried to assign an array and use that, PHP would only modify the local variable, and not the session. + // Since ArrayObject is an object, PHP will modify the object itself, allowing it to persist in the session. + if (!$c->session->has($csrfKey)) { + $c->session[$csrfKey] = new \ArrayObject(); + } + $csrfStorage = $c->session[$csrfKey]; + + $onFailure = function ($request, $response, $next) { + $e = new BadRequestException("The CSRF code was invalid or not provided."); + $e->addUserMessage('CSRF_MISSING'); + throw $e; + + return $next($request, $response); + }; + + return new Guard($c->config['csrf.name'], $csrfStorage, $onFailure, $c->config['csrf.storage_limit'], $c->config['csrf.strength'], $c->config['csrf.persistent_token']); + }; + + /** + * Initialize Eloquent Capsule, which provides the database layer for UF. + * + * @todo construct the individual objects rather than using the facade + */ + $container['db'] = function ($c) { + $config = $c->config; + + $capsule = new Capsule; + + foreach ($config['db'] as $name => $dbConfig) { + $capsule->addConnection($dbConfig, $name); + } + + $queryEventDispatcher = new Dispatcher(new Container); + + $capsule->setEventDispatcher($queryEventDispatcher); + + // Register as global connection + $capsule->setAsGlobal(); + + // Start Eloquent + $capsule->bootEloquent(); + + if ($config['debug.queries']) { + $logger = $c->queryLogger; + + foreach ($config['db'] as $name => $dbConfig) { + $capsule->connection($name)->enableQueryLog(); + } + + // Register listener + $queryEventDispatcher->listen(QueryExecuted::class, function ($query) use ($logger) { + $logger->debug("Query executed on database [{$query->connectionName}]:", [ + 'query' => $query->sql, + 'bindings' => $query->bindings, + 'time' => $query->time . ' ms' + ]); + }); + } + + return $capsule; + }; + + /** + * Debug logging with Monolog. + * + * Extend this service to push additional handlers onto the 'debug' log stack. + */ + $container['debugLogger'] = function ($c) { + $logger = new Logger('debug'); + + $logFile = $c->locator->findResource('log://userfrosting.log', true, true); + + $handler = new StreamHandler($logFile); + + $formatter = new MixedFormatter(null, null, true); + + $handler->setFormatter($formatter); + $logger->pushHandler($handler); + + return $logger; + }; + + /** + * Custom error-handler for recoverable errors. + */ + $container['errorHandler'] = function ($c) { + $settings = $c->settings; + + $handler = new ExceptionHandlerManager($c, $settings['displayErrorDetails']); + + // Register the base HttpExceptionHandler. + $handler->registerHandler('\UserFrosting\Support\Exception\HttpException', '\UserFrosting\Sprinkle\Core\Error\Handler\HttpExceptionHandler'); + + // Register the NotFoundExceptionHandler. + $handler->registerHandler('\UserFrosting\Support\Exception\NotFoundException', '\UserFrosting\Sprinkle\Core\Error\Handler\NotFoundExceptionHandler'); + + // Register the PhpMailerExceptionHandler. + $handler->registerHandler('\phpmailerException', '\UserFrosting\Sprinkle\Core\Error\Handler\PhpMailerExceptionHandler'); + + return $handler; + }; + + /** + * Error logging with Monolog. + * + * Extend this service to push additional handlers onto the 'error' log stack. + */ + $container['errorLogger'] = function ($c) { + $log = new Logger('errors'); + + $logFile = $c->locator->findResource('log://userfrosting.log', true, true); + + $handler = new StreamHandler($logFile, Logger::WARNING); + + $formatter = new LineFormatter(null, null, true); + + $handler->setFormatter($formatter); + $log->pushHandler($handler); + + return $log; + }; + + /** + * Factory service with FactoryMuffin. + * + * Provide access to factories for the rapid creation of objects for the purpose of testing + */ + $container['factory'] = function ($c) { + + // Get the path of all of the sprinkle's factories + $factoriesPath = $c->locator->findResources('factories://', true, true); + + // Create a new Factory Muffin instance + $fm = new FactoryMuffin(); + + // Load all of the model definitions + $fm->loadFactories($factoriesPath); + + // Set the locale. Could be the config one, but for testing English should do + Faker::setLocale('en_EN'); + + return $fm; + }; + + /** + * Builds search paths for locales in all Sprinkles. + */ + $container['localePathBuilder'] = function ($c) { + $config = $c->config; + + // Make sure the locale config is a valid string + if (!is_string($config['site.locales.default']) || $config['site.locales.default'] == '') { + throw new \UnexpectedValueException('The locale config is not a valid string.'); + } + + // Load the base locale file(s) as specified in the configuration + $locales = explode(',', $config['site.locales.default']); + + return new LocalePathBuilder($c->locator, 'locale://', $locales); + }; + + /** + * Mail service. + */ + $container['mailer'] = function ($c) { + $mailer = new Mailer($c->mailLogger, $c->config['mail']); + + // Use UF debug settings to override any service-specific log settings. + if (!$c->config['debug.smtp']) { + $mailer->getPhpMailer()->SMTPDebug = 0; + } + + return $mailer; + }; + + /** + * Mail logging service. + * + * PHPMailer will use this to log SMTP activity. + * Extend this service to push additional handlers onto the 'mail' log stack. + */ + $container['mailLogger'] = function ($c) { + $log = new Logger('mail'); + + $logFile = $c->locator->findResource('log://userfrosting.log', true, true); + + $handler = new StreamHandler($logFile); + $formatter = new LineFormatter(null, null, true); + + $handler->setFormatter($formatter); + $log->pushHandler($handler); + + return $log; + }; + + /** + * Error-handler for 404 errors. Notice that we manually create a UserFrosting NotFoundException, + * and a NotFoundExceptionHandler. This lets us pass through to the UF error handling system. + */ + $container['notFoundHandler'] = function ($c) { + return function ($request, $response) use ($c) { + $exception = new NotFoundException; + $handler = new NotFoundExceptionHandler($c, $request, $response, $exception, $c->settings['displayErrorDetails']); + return $handler->handle(); + }; + }; + + /** + * Error-handler for PHP runtime errors. Notice that we just pass this through to our general-purpose + * error-handling service. + */ + $container['phpErrorHandler'] = function ($c) { + return $c->errorHandler; + }; + + /** + * Laravel query logging with Monolog. + * + * Extend this service to push additional handlers onto the 'query' log stack. + */ + $container['queryLogger'] = function ($c) { + $logger = new Logger('query'); + + $logFile = $c->locator->findResource('log://userfrosting.log', true, true); + + $handler = new StreamHandler($logFile); + + $formatter = new MixedFormatter(null, null, true); + + $handler->setFormatter($formatter); + $logger->pushHandler($handler); + + return $logger; + }; + + /** + * Override Slim's default router with the UF router. + */ + $container['router'] = function ($c) { + $routerCacheFile = false; + if (isset($c->config['settings.routerCacheFile'])) { + $routerCacheFile = $c->config['settings.routerCacheFile']; + } + + return (new Router)->setCacheFile($routerCacheFile); + }; + + /** + * Start the PHP session, with the name and parameters specified in the configuration file. + */ + $container['session'] = function ($c) { + $config = $c->config; + + // Create appropriate handler based on config + if ($config['session.handler'] == 'file') { + $fs = new FileSystem; + $handler = new FileSessionHandler($fs, $c->locator->findResource('session://'), $config['session.minutes']); + } elseif ($config['session.handler'] == 'database') { + $connection = $c->db->connection(); + // Table must exist, otherwise an exception will be thrown + $handler = new DatabaseSessionHandler($connection, $config['session.database.table'], $config['session.minutes']); + } else { + throw new \Exception("Bad session handler type '{$config['session.handler']}' specified in configuration file."); + } + + // Create, start and return a new wrapper for $_SESSION + $session = new Session($handler, $config['session']); + $session->start(); + + return $session; + }; + + /** + * Request throttler. + * + * Throttles (rate-limits) requests of a predefined type, with rules defined in site config. + */ + $container['throttler'] = function ($c) { + $throttler = new Throttler($c->classMapper); + + $config = $c->config; + + if ($config->has('throttles') && ($config['throttles'] !== null)) { + foreach ($config['throttles'] as $type => $rule) { + if ($rule) { + $throttleRule = new ThrottleRule($rule['method'], $rule['interval'], $rule['delays']); + $throttler->addThrottleRule($type, $throttleRule); + } else { + $throttler->addThrottleRule($type, null); + } + } + } + + return $throttler; + }; + + /** + * Translation service, for translating message tokens. + */ + $container['translator'] = function ($c) { + // Load the translations + $paths = $c->localePathBuilder->buildPaths(); + $loader = new ArrayFileLoader($paths); + + // Create the $translator object + $translator = new MessageTranslator($loader->load()); + + return $translator; + }; + + /** + * Set up Twig as the view, adding template paths for all sprinkles and the Slim Twig extension. + * + * Also adds the UserFrosting core Twig extension, which provides additional functions, filters, global variables, etc. + */ + $container['view'] = function ($c) { + $templatePaths = $c->locator->findResources('templates://', true, true); + + $view = new Twig($templatePaths); + + $loader = $view->getLoader(); + + $sprinkles = $c->sprinkleManager->getSprinkleNames(); + + // Add Sprinkles' templates namespaces + foreach ($sprinkles as $sprinkle) { + $path = \UserFrosting\SPRINKLES_DIR . \UserFrosting\DS . + $sprinkle . \UserFrosting\DS . + \UserFrosting\TEMPLATE_DIR_NAME . \UserFrosting\DS; + + if (is_dir($path)) { + $loader->addPath($path, $sprinkle); + } + } + + $twig = $view->getEnvironment(); + + if ($c->config['cache.twig']) { + $twig->setCache($c->locator->findResource('cache://twig', true, true)); + } + + if ($c->config['debug.twig']) { + $twig->enableDebug(); + $view->addExtension(new \Twig_Extension_Debug()); + } + + // Register the Slim extension with Twig + $slimExtension = new TwigExtension( + $c->router, + $c->request->getUri() + ); + $view->addExtension($slimExtension); + + // Register the core UF extension with Twig + $coreExtension = new CoreExtension($c); + $view->addExtension($coreExtension); + + return $view; + }; + } +} diff --git a/login/app/sprinkles/core/src/Sprunje/Sprunje.php b/login/app/sprinkles/core/src/Sprunje/Sprunje.php new file mode 100755 index 0000000..5525dc4 --- /dev/null +++ b/login/app/sprinkles/core/src/Sprunje/Sprunje.php @@ -0,0 +1,566 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Sprunje; + +use Carbon\Carbon; +use Illuminate\Database\Eloquent\Builder; +use League\Csv\Writer; +use Psr\Http\Message\ResponseInterface as Response; +use UserFrosting\Sprinkle\Core\Facades\Debug; +use UserFrosting\Sprinkle\Core\Util\ClassMapper; +use UserFrosting\Support\Exception\BadRequestException; +use Valitron\Validator; + +/** + * Sprunje + * + * Implements a versatile API for sorting, filtering, and paginating an Eloquent query builder. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +abstract class Sprunje +{ + /** + * @var UserFrosting\Sprinkle\Core\Util\ClassMapper + */ + protected $classMapper; + + /** + * Name of this Sprunje, used when generating output files. + * + * @var string + */ + protected $name = ''; + + /** + * The base (unfiltered) query. + * + * @var \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation + */ + protected $query; + + /** + * Default HTTP request parameters + * + * @var array[string] + */ + protected $options = [ + 'sorts' => [], + 'filters' => [], + 'lists' => [], + 'size' => 'all', + 'page' => null, + 'format' => 'json' + ]; + + /** + * Fields to allow filtering upon. + * + * @var array[string] + */ + protected $filterable = []; + + /** + * Fields to allow listing (enumeration) upon. + * + * @var array[string] + */ + protected $listable = []; + + /** + * Fields to allow sorting upon. + * + * @var array[string] + */ + protected $sortable = []; + + /** + * List of fields to exclude when processing an "_all" filter. + * + * @var array[string] + */ + protected $excludeForAll = []; + + /** + * Separator to use when splitting filter values to treat them as ORs. + * + * @var string + */ + protected $orSeparator = '||'; + + /** + * Array key for the total unfiltered object count. + * + * @var string + */ + protected $countKey = 'count'; + + /** + * Array key for the filtered object count. + * + * @var string + */ + protected $countFilteredKey = 'count_filtered'; + + /** + * Array key for the actual result set. + * + * @var string + */ + protected $rowsKey = 'rows'; + + /** + * Array key for the list of enumerated columns and their enumerations. + * + * @var string + */ + protected $listableKey = 'listable'; + + /** + * Constructor. + * + * @param ClassMapper $classMapper + * @param mixed[] $options + */ + public function __construct(ClassMapper $classMapper, array $options) + { + $this->classMapper = $classMapper; + + // Validation on input data + $v = new Validator($options); + $v->rule('array', ['sorts', 'filters', 'lists']); + $v->rule('regex', 'sorts.*', '/asc|desc/i'); + $v->rule('regex', 'size', '/all|[0-9]+/i'); + $v->rule('integer', 'page'); + $v->rule('regex', 'format', '/json|csv/i'); + + // TODO: translated rules + if(!$v->validate()) { + $e = new BadRequestException(); + foreach ($v->errors() as $idx => $field) { + foreach($field as $eidx => $error) { + $e->addUserMessage($error); + } + } + throw $e; + } + + $this->options = array_replace_recursive($this->options, $options); + + $this->query = $this->baseQuery(); + + // Start a new query on any Model instances + if (is_a($this->baseQuery(), '\Illuminate\Database\Eloquent\Model')) { + $this->query = $this->baseQuery()->newQuery(); + } + } + + /** + * Extend the query by providing a callback. + * + * @param callable $callback A callback which accepts and returns a Builder instance. + * @return $this + */ + public function extendQuery(callable $callback) + { + $this->query = $callback($this->query); + return $this; + } + + /** + * Execute the query and build the results, and append them in the appropriate format to the response. + * + * @param ResponseInterface $response + * @return ResponseInterface + */ + public function toResponse(Response $response) + { + $format = $this->options['format']; + + if ($format == 'csv') { + $result = $this->getCsv(); + + // Prepare response + $settings = http_build_query($this->options); + $date = Carbon::now()->format('Ymd'); + $response = $response->withAddedHeader('Content-Disposition', "attachment;filename=$date-{$this->name}-$settings.csv"); + $response = $response->withAddedHeader('Content-Type', 'text/csv; charset=utf-8'); + return $response->write($result); + // Default to JSON + } else { + $result = $this->getArray(); + return $response->withJson($result, 200, JSON_PRETTY_PRINT); + } + } + + /** + * Executes the sprunje query, applying all sorts, filters, and pagination. + * + * Returns an array containing `count` (the total number of rows, before filtering), `count_filtered` (the total number of rows after filtering), + * and `rows` (the filtered result set). + * @return mixed[] + */ + public function getArray() + { + list($count, $countFiltered, $rows) = $this->getModels(); + + // Return sprunjed results + return [ + $this->countKey => $count, + $this->countFilteredKey => $countFiltered, + $this->rowsKey => $rows->values()->toArray(), + $this->listableKey => $this->getListable() + ]; + } + + /** + * Run the query and build a CSV object by flattening the resulting collection. Ignores any pagination. + * + * @return SplTempFileObject + */ + public function getCsv() + { + $filteredQuery = clone $this->query; + + // Apply filters + $this->applyFilters($filteredQuery); + + // Apply sorts + $this->applySorts($filteredQuery); + + $collection = collect($filteredQuery->get()); + + // Perform any additional transformations on the dataset + $this->applyTransformations($collection); + + $csv = Writer::createFromFileObject(new \SplTempFileObject()); + + $columnNames = []; + + // 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()); + foreach ($item as $itemKey => $itemValue) { + if (!in_array($itemKey, $columnNames)) { + $columnNames[] = $itemKey; + } + } + return $item; + }); + + $csv->insertOne($columnNames); + + // Insert the data as rows in the CSV document + $collection->each(function ($item) use ($csv, $columnNames) { + $row = []; + foreach ($columnNames as $itemKey) { + // Only add the value if it is set and not an array. Laravel's array_dot sometimes creates empty child arrays :( + // See https://github.com/laravel/framework/pull/13009 + if (isset($item[$itemKey]) && !is_array($item[$itemKey])) { + $row[] = $item[$itemKey]; + } else { + $row[] = ''; + } + } + + $csv->insertOne($row); + }); + + return $csv; + } + + /** + * Executes the sprunje query, applying all sorts, filters, and pagination. + * + * Returns the filtered, paginated result set and the counts. + * @return mixed[] + */ + public function getModels() + { + // Count unfiltered total + $count = $this->count($this->query); + + // Clone the Query\Builder, Eloquent\Builder, or Relation + $filteredQuery = clone $this->query; + + // Apply filters + $this->applyFilters($filteredQuery); + + // Count filtered total + $countFiltered = $this->countFiltered($filteredQuery); + + // Apply sorts + $this->applySorts($filteredQuery); + + // Paginate + $this->applyPagination($filteredQuery); + + $collection = collect($filteredQuery->get()); + + // Perform any additional transformations on the dataset + $this->applyTransformations($collection); + + return [$count, $countFiltered, $collection]; + } + + /** + * Get lists of values for specified fields in 'lists' option, calling a custom lister callback when appropriate. + * + * @return array + */ + public function getListable() + { + $result = []; + foreach ($this->listable as $name) { + + // Determine if a custom filter method has been defined + $methodName = 'list'.studly_case($name); + + if (method_exists($this, $methodName)) { + $result[$name] = $this->$methodName(); + } else { + $result[$name] = $this->getColumnValues($name); + } + } + + return $result; + } + + /** + * Get the underlying queriable object in its current state. + * + * @return Builder + */ + public function getQuery() + { + return $this->query; + } + + /** + * Set the underlying QueryBuilder object. + * + * @param Builder $query + * @return $this + */ + public function setQuery($query) + { + $this->query = $query; + return $this; + } + + /** + * Apply any filters from the options, calling a custom filter callback when appropriate. + * + * @param Builder $query + * @return $this + */ + public function applyFilters($query) + { + foreach ($this->options['filters'] as $name => $value) { + // Check that this filter is allowed + if (($name != '_all') && !in_array($name, $this->filterable)) { + $e = new BadRequestException(); + $e->addUserMessage('VALIDATE.SPRUNJE.BAD_FILTER', ['name' => $name]); + throw $e; + } + // Since we want to match _all_ of the fields, we wrap the field callback in a 'where' callback + $query->where(function ($fieldQuery) use ($name, $value) { + $this->buildFilterQuery($fieldQuery, $name, $value); + }); + } + + return $this; + } + + /** + * Apply any sorts from the options, calling a custom sorter callback when appropriate. + * + * @param Builder $query + * @return $this + */ + public function applySorts($query) + { + foreach ($this->options['sorts'] as $name => $direction) { + // Check that this sort is allowed + if (!in_array($name, $this->sortable)) { + $e = new BadRequestException(); + $e->addUserMessage('VALIDATE.SPRUNJE.BAD_SORT', ['name' => $name]); + throw $e; + } + + // Determine if a custom sort method has been defined + $methodName = 'sort'.studly_case($name); + + if (method_exists($this, $methodName)) { + $this->$methodName($query, $direction); + } else { + $query->orderBy($name, $direction); + } + } + + return $this; + } + + /** + * Apply pagination based on the `page` and `size` options. + * + * @param Builder $query + * @return $this + */ + public function applyPagination($query) + { + if ( + ($this->options['page'] !== null) && + ($this->options['size'] !== null) && + ($this->options['size'] != 'all') + ) { + $offset = $this->options['size']*$this->options['page']; + $query->skip($offset) + ->take($this->options['size']); + } + + return $this; + } + + /** + * Match any filter in `filterable`. + * + * @param Builder $query + * @param mixed $value + * @return $this + */ + protected function filterAll($query, $value) + { + foreach ($this->filterable as $name) { + if (studly_case($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); + }); + } + } + + return $this; + } + + /** + * Build the filter query for a single field. + * + * @param Builder $query + * @param string $name + * @param mixed $value + * @return $this + */ + protected function buildFilterQuery($query, $name, $value) + { + $methodName = 'filter'.studly_case($name); + + // Determine if a custom filter method has been defined + if (method_exists($this, $methodName)) { + $this->$methodName($query, $value); + } else { + $this->buildFilterDefaultFieldQuery($query, $name, $value); + } + + return $this; + } + + /** + * Perform a 'like' query on a single field, separating the value string on the or separator and + * matching any of the supplied values. + * + * @param Builder $query + * @param string $name + * @param mixed $value + * @return $this + */ + protected function buildFilterDefaultFieldQuery($query, $name, $value) + { + // Default filter - split value on separator for OR queries + // and search by column name + $values = explode($this->orSeparator, $value); + foreach ($values as $value) { + $query->orLike($name, $value); + } + + return $this; + } + + /** + * Set any transformations you wish to apply to the collection, after the query is executed. + * + * @param \Illuminate\Database\Eloquent\Collection $collection + * @return \Illuminate\Database\Eloquent\Collection + */ + protected function applyTransformations($collection) + { + return $collection; + } + + /** + * Set the initial query used by your Sprunje. + * + * @return Builder|Relation|Model + */ + abstract protected function baseQuery(); + + /** + * Returns a list of distinct values for a specified column. + * Formats results to have a "value" and "text" attribute. + * + * @param string $column + * @return array + */ + protected function getColumnValues($column) + { + $rawValues = $this->query->select($column)->distinct()->orderBy($column, 'asc')->get(); + $values = []; + foreach ($rawValues as $raw) { + $values[] = [ + 'value' => $raw[$column], + 'text' => $raw[$column] + ]; + } + return $values; + } + + /** + * Get the unpaginated count of items (before filtering) in this query. + * + * @param Builder $query + * @return int + */ + protected function count($query) + { + return $query->count(); + } + + /** + * Get the unpaginated count of items (after filtering) in this query. + * + * @param Builder $query + * @return int + */ + protected function countFiltered($query) + { + return $query->count(); + } + + /** + * Executes the sprunje query, applying all sorts, filters, and pagination. + * + * Returns an array containing `count` (the total number of rows, before filtering), `count_filtered` (the total number of rows after filtering), + * and `rows` (the filtered result set). + * @deprecated since 4.1.7 Use getArray() instead. + * @return mixed[] + */ + public function getResults() + { + return $this->getArray(); + } +} diff --git a/login/app/sprinkles/core/src/Throttle/ThrottleRule.php b/login/app/sprinkles/core/src/Throttle/ThrottleRule.php new file mode 100755 index 0000000..b71f296 --- /dev/null +++ b/login/app/sprinkles/core/src/Throttle/ThrottleRule.php @@ -0,0 +1,140 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Throttle; + +/** + * ThrottleRule Class + * + * Represents a request throttling rule. + * @author Alex Weissman (https://alexanderweissman.com) + */ +class ThrottleRule +{ + /** @var string Set to 'ip' for ip-based throttling, 'data' for request-data-based throttling. */ + protected $method; + + /** @var int The amount of time, in seconds, to look back in determining attempts to consider. */ + protected $interval; + + /** + * @var int[] A mapping of minimum observation counts (x) to delays (y), in seconds. + * Any throttleable event that has occurred more than x times in this rule's interval, + * must wait y seconds after the last occurrence before another attempt is permitted. + */ + protected $delays; + + /** + * Create a new ThrottleRule object. + * + * @param string $method Set to 'ip' for ip-based throttling, 'data' for request-data-based throttling. + * @param int $interval The amount of time, in seconds, to look back in determining attempts to consider. + * @param int[] $delays A mapping of minimum observation counts (x) to delays (y), in seconds. + */ + public function __construct($method, $interval, $delays) + { + $this->setMethod($method); + $this->setInterval($interval); + $this->setDelays($delays); + } + + /** + * Get the current delay on this rule for a particular number of event counts. + * + * @param Carbon\Carbon $lastEventTime The timestamp for the last countable event. + * @param int $count The total number of events which have occurred in an interval. + */ + public function getDelay($lastEventTime, $count) + { + // Zero occurrences always maps to a delay of 0 seconds. + if ($count == 0) { + return 0; + } + + foreach ($this->delays as $observations => $delay) { + // Skip any delay rules for which we haven't met the requisite number of observations + if ($count < $observations) { + continue; + } + + // If this rule meets the observed number of events, and violates the required delay, then return the remaining time left + if ($lastEventTime->diffInSeconds() < $delay) { + return $lastEventTime->addSeconds($delay)->diffInSeconds(); + } + } + + return 0; + } + + /** + * Gets the current mapping of attempts (int) to delays (seconds). + * + * @return int[] + */ + public function getDelays() + { + return $this->delays; + } + + /** + * Gets the current throttling interval (seconds). + * + * @return int + */ + public function getInterval() + { + return $this->interval; + } + + /** + * Gets the current throttling method ('ip' or 'data'). + * + * @return string + */ + public function getMethod() + { + return $this->method; + } + + /** + * Sets the current mapping of attempts (int) to delays (seconds). + * + * @param int[] A mapping of minimum observation counts (x) to delays (y), in seconds. + */ + public function setDelays($delays) + { + // Sort the array by key, from highest to lowest value + $this->delays = $delays; + krsort($this->delays); + + return $this; + } + + /** + * Sets the current throttling interval (seconds). + * + * @param int The amount of time, in seconds, to look back in determining attempts to consider. + */ + public function setInterval($interval) + { + $this->interval = $interval; + + return $this; + } + + /** + * Sets the current throttling method ('ip' or 'data'). + * + * @param string Set to 'ip' for ip-based throttling, 'data' for request-data-based throttling. + */ + public function setMethod($method) + { + $this->method = $method; + + return $this; + } +} diff --git a/login/app/sprinkles/core/src/Throttle/Throttler.php b/login/app/sprinkles/core/src/Throttle/Throttler.php new file mode 100755 index 0000000..0d42442 --- /dev/null +++ b/login/app/sprinkles/core/src/Throttle/Throttler.php @@ -0,0 +1,178 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Throttle; + +use Carbon\Carbon; +use UserFrosting\Sprinkle\Core\Util\ClassMapper; + +/** + * Handles throttling (rate limiting) of specific types of requests. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class Throttler +{ + /** + * @var UserFrosting\Sprinkle\Core\Util\ClassMapper + */ + protected $classMapper; + + /** + * @var ThrottleRule[] An array mapping throttle names to throttle rules. + */ + protected $throttleRules; + + /** + * Create a new Throttler object. + * + * @param ClassMapper $classMapper Maps generic class identifiers to specific class names. + */ + public function __construct(ClassMapper $classMapper) + { + $this->classMapper = $classMapper; + $this->throttleRules = []; + } + + /** + * Add a throttling rule for a particular throttle event type. + * + * @param string $type The type of throttle event to check against. + * @param ThrottleRule $rule The rule to use when throttling this type of event. + */ + public function addThrottleRule($type, $rule) + { + if (!($rule instanceof ThrottleRule || ($rule === null))) { + throw new ThrottlerException('$rule must be of type ThrottleRule (or null).'); + } + + $this->throttleRules[$type] = $rule; + + return $this; + } + + /** + * Check the current request against a specified throttle rule. + * + * @param string $type The type of throttle event to check against. + * @param mixed[] $requestData Any additional request parameters to use in checking the throttle. + * @return bool + */ + public function getDelay($type, $requestData = []) + { + $throttleRule = $this->getRule($type); + + if (is_null($throttleRule)) { + return 0; + } + + // Get earliest time to start looking for throttleable events + $startTime = Carbon::now() + ->subSeconds($throttleRule->getInterval()); + + // Fetch all throttle events of the specified type, that match the specified rule + if ($throttleRule->getMethod() == 'ip') { + $events = $this->classMapper->staticMethod('throttle', 'where', 'type', $type) + ->where('created_at', '>', $startTime) + ->where('ip', $_SERVER['REMOTE_ADDR']) + ->get(); + } else { + $events = $this->classMapper->staticMethod('throttle', 'where', 'type', $type) + ->where('created_at', '>', $startTime) + ->get(); + + // Filter out only events that match the required JSON data + $events = $events->filter(function ($item, $key) use ($requestData) { + $data = json_decode($item->request_data); + + // If a field is not specified in the logged data, or it doesn't match the value we're searching for, + // then filter out this event from the collection. + foreach ($requestData as $name => $value) { + if (!isset($data->$name) || ($data->$name != $value)) { + return false; + } + } + + return true; + }); + } + + // Check the collection of events against the specified throttle rule. + return $this->computeDelay($events, $throttleRule); + } + + /** + * Get a registered rule of a particular type. + * + * @param string $type + * @throws ThrottlerException + * @return ThrottleRule[] + */ + public function getRule($type) + { + if (!array_key_exists($type, $this->throttleRules)) { + throw new ThrottlerException("The throttling rule for '$type' could not be found."); + } + + return $this->throttleRules[$type]; + } + + /** + * Get the current throttling rules. + * + * @return ThrottleRule[] + */ + public function getThrottleRules() + { + return $this->throttleRules; + } + + /** + * Log a throttleable event to the database. + * + * @param string $type the type of event + * @param string[] $requestData an array of field names => values that are relevant to throttling for this event (e.g. username, email, etc). + */ + public function logEvent($type, $requestData = []) + { + // Just a check to make sure the rule exists + $throttleRule = $this->getRule($type); + + if (is_null($throttleRule)) { + return $this; + } + + $event = $this->classMapper->createInstance('throttle', [ + 'type' => $type, + 'ip' => $_SERVER['REMOTE_ADDR'], + 'request_data' => json_encode($requestData) + ]); + + $event->save(); + + return $this; + } + + /** + * Returns the current delay for a specified throttle rule. + * + * @param Throttle[] $events a Collection of throttle events. + * @param ThrottleRule $throttleRule a rule representing the strategy to use for throttling a particular type of event. + * @return int seconds remaining until a particular event is permitted to be attempted again. + */ + protected function computeDelay($events, $throttleRule) + { + // If no matching events found, then there is no delay + if (!$events->count()) { + return 0; + } + + // Great, now we compare our delay against the most recent attempt + $lastEvent = $events->last(); + return $throttleRule->getDelay($lastEvent->created_at, $events->count()); + } +} diff --git a/login/app/sprinkles/core/src/Throttle/ThrottlerException.php b/login/app/sprinkles/core/src/Throttle/ThrottlerException.php new file mode 100755 index 0000000..2fd9035 --- /dev/null +++ b/login/app/sprinkles/core/src/Throttle/ThrottlerException.php @@ -0,0 +1,18 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Throttle; + +/** + * Throttler exception. Used when there is a problem with the request throttler. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class ThrottlerException extends \RuntimeException +{ + +} diff --git a/login/app/sprinkles/core/src/Twig/CacheHelper.php b/login/app/sprinkles/core/src/Twig/CacheHelper.php new file mode 100755 index 0000000..14aea49 --- /dev/null +++ b/login/app/sprinkles/core/src/Twig/CacheHelper.php @@ -0,0 +1,58 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Twig; + +use Interop\Container\ContainerInterface; +use Illuminate\Filesystem\Filesystem; + +/** + * Provides helper function to delete the Twig cache directory + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class CacheHelper +{ + + /** + * @var ContainerInterface The global container object, which holds all your services. + */ + protected $ci; + + /** + * Constructor. + * + * @param ContainerInterface $ci The global container object, which holds all your services. + */ + public function __construct(ContainerInterface $ci) + { + $this->ci = $ci; + } + + /** + * Function that delete the Twig cache directory content + * + * @access public + * @return bool true/false if operation is successfull + */ + public function clearCache() + { + // Get location + $path = $this->ci->locator->findResource('cache://twig', true, true); + + // Get Filesystem instance + $fs = new FileSystem; + + // Make sure directory exist and delete it + if ($fs->exists($path)) { + return $fs->deleteDirectory($path, true); + } + + // It's still considered a success if directory doesn't exist yet + return true; + } +} diff --git a/login/app/sprinkles/core/src/Twig/CoreExtension.php b/login/app/sprinkles/core/src/Twig/CoreExtension.php new file mode 100755 index 0000000..6a89d12 --- /dev/null +++ b/login/app/sprinkles/core/src/Twig/CoreExtension.php @@ -0,0 +1,124 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Twig; + +use Interop\Container\ContainerInterface; +use UserFrosting\Sprinkle\Core\Util\Util; + +/** + * Extends Twig functionality for the Core sprinkle. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class CoreExtension extends \Twig_Extension implements \Twig_Extension_GlobalsInterface +{ + + /** + * @var ContainerInterface The global container object, which holds all your services. + */ + protected $services; + + /** + * Constructor. + * + * @param ContainerInterface $services The global container object, which holds all your services. + */ + 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[\Twig_SimpleFunction] + */ + public function getFunctions() + { + return array( + // Add Twig function for fetching alerts + new \Twig_SimpleFunction('getAlerts', function ($clear = true) { + if ($clear) { + return $this->services['alerts']->getAndClearMessages(); + } else { + return $this->services['alerts']->messages(); + } + }), + new \Twig_SimpleFunction('translate', function ($hook, $params = array()) { + return $this->services['translator']->translate($hook, $params); + }, [ + 'is_safe' => ['html'] + ]) + ); + } + + /** + * Adds Twig filters `unescape`. + * + * @return array[\Twig_SimpleFilter] + */ + public function getFilters() + { + return array( + /** + * Converts phone numbers to a standard format. + * + * @param String $num A unformatted phone number + * @return String Returns the formatted phone number + */ + new \Twig_SimpleFilter('phone', function ($num) { + return Util::formatPhoneNumber($num); + }), + new \Twig_SimpleFilter('unescape', function ($string) { + return html_entity_decode($string); + }) + ); + } + + /** + * Adds Twig global variables `site` and `assets`. + * + * @return array[mixed] + */ + public function getGlobals() + { + // CSRF token name and value + $csrfNameKey = $this->services->csrf->getTokenNameKey(); + $csrfValueKey = $this->services->csrf->getTokenValueKey(); + $csrfName = $this->services->csrf->getTokenName(); + $csrfValue = $this->services->csrf->getTokenValue(); + + $csrf = [ + 'csrf' => [ + 'keys' => [ + 'name' => $csrfNameKey, + 'value' => $csrfValueKey + ], + 'name' => $csrfName, + 'value' => $csrfValue + ] + ]; + + $site = array_replace_recursive($this->services->config['site'], $csrf); + + return [ + 'site' => $site, + 'assets' => $this->services->assets + ]; + } +} diff --git a/login/app/sprinkles/core/src/Util/BadClassNameException.php b/login/app/sprinkles/core/src/Util/BadClassNameException.php new file mode 100755 index 0000000..09c4ea5 --- /dev/null +++ b/login/app/sprinkles/core/src/Util/BadClassNameException.php @@ -0,0 +1,18 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Util; + +/** + * Bad class name exception. Used when a class name is dynamically invoked, but the class does not exist. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class BadClassNameException extends \LogicException +{ + // +} diff --git a/login/app/sprinkles/core/src/Util/Captcha.php b/login/app/sprinkles/core/src/Util/Captcha.php new file mode 100755 index 0000000..c788b77 --- /dev/null +++ b/login/app/sprinkles/core/src/Util/Captcha.php @@ -0,0 +1,159 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Util; + +use UserFrosting\Session\Session; + +/** + * Captcha Class + * + * Implements the captcha for user registration. + * + * @author r3wt + * @author Alex Weissman (https://alexanderweissman.com) + * @see http://www.userfrosting.com/components/#messages + */ +class Captcha +{ + /** + * @var string The randomly generated captcha code. + */ + protected $code; + + /** + * @var string The captcha image, represented as a binary string. + */ + protected $image; + + /** + * @var UserFrosting\Session\Session We use the session object so that the hashed captcha token will automatically appear in the session. + */ + protected $session; + + /** + * @var string + */ + protected $key; + + /** + * Create a new captcha. + */ + public function __construct($session, $key) + { + $this->session = $session; + $this->key = $key; + + if (!$this->session->has($key)) { + $this->session[$key] = array(); + } + } + + /** + * Generates a new captcha for the user registration form. + * + * This generates a random 5-character captcha and stores it in the session with an md5 hash. + * Also, generates the corresponding captcha image. + */ + public function generateRandomCode() + { + $md5_hash = md5(rand(0,99999)); + $this->code = substr($md5_hash, 25, 5); + $enc = md5($this->code); + + // Store the generated captcha value to the session + $this->session[$this->key] = $enc; + + $this->generateImage(); + } + + /** + * Returns the captcha code. + */ + public function getCaptcha() + { + return $this->code; + } + + /** + * Returns the captcha image. + */ + public function getImage() + { + return $this->image; + } + + /** + * Check that the specified code, when hashed, matches the code in the session. + * + * Also, stores the specified code in the session with an md5 hash. + * @param string + * @return bool + */ + public function verifyCode($code) + { + return (md5($code) == $this->session[$this->key]); + } + + /** + * Generate the image for the current captcha. + * + * This generates an image as a binary string. + */ + protected function generateImage() + { + $width = 150; + $height = 30; + + $image = imagecreatetruecolor(150, 30); + + //color pallette + $white = imagecolorallocate($image, 255, 255, 255); + $black = imagecolorallocate($image, 0, 0, 0); + $red = imagecolorallocate($image,255,0,0); + $yellow = imagecolorallocate($image, 255, 255, 0); + $dark_grey = imagecolorallocate($image, 64,64,64); + $blue = imagecolorallocate($image, 0,0,255); + + //create white rectangle + imagefilledrectangle($image,0,0,150,30,$white); + + //add some lines + for($i=0;$i<2;$i++) { + imageline($image,0,rand()%10,10,rand()%30,$dark_grey); + imageline($image,0,rand()%30,150,rand()%30,$red); + imageline($image,0,rand()%30,150,rand()%30,$yellow); + } + + // RandTab color pallette + $randc[0] = imagecolorallocate($image, 0, 0, 0); + $randc[1] = imagecolorallocate($image,255,0,0); + $randc[2] = imagecolorallocate($image, 255, 255, 0); + $randc[3] = imagecolorallocate($image, 64,64,64); + $randc[4] = imagecolorallocate($image, 0,0,255); + + //add some dots + for($i=0;$i<1000;$i++) { + imagesetpixel($image,rand()%200,rand()%50,$randc[rand()%5]); + } + + //calculate center of text + $x = ( 150 - 0 - imagefontwidth( 5 ) * strlen( $this->code ) ) / 2 + 0 + 5; + + //write string twice + imagestring($image,5, $x, 7, $this->code, $black); + imagestring($image,5, $x, 7, $this->code, $black); + //start ob + ob_start(); + imagepng($image); + + //get binary image data + $this->image = ob_get_clean(); + + return $this->image; + } +} diff --git a/login/app/sprinkles/core/src/Util/CheckEnvironment.php b/login/app/sprinkles/core/src/Util/CheckEnvironment.php new file mode 100755 index 0000000..26925d9 --- /dev/null +++ b/login/app/sprinkles/core/src/Util/CheckEnvironment.php @@ -0,0 +1,340 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Util; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Slim\Http\Body; + +/** + * Performs pre-flight tests on your server environment to check that it meets the requirements. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class CheckEnvironment +{ + /** + * @var \RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator Locator service for stream resources. + */ + protected $locator; + + /** + * @var array The results of any failed checks performed. + */ + protected $resultsFailed = []; + + /** + * @var array The results of any successful checks performed. + */ + protected $resultsSuccess = []; + + /** + * @var \Slim\Views\Twig The view object, needed for rendering error page. + */ + protected $view; + + /** + * @var \Illuminate\Cache\CacheManager Cache service for cache access. + */ + protected $cache; + + /** + * Constructor. + * + * @param $view \Slim\Views\Twig The view object, needed for rendering error page. + * @param $locator \RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator Locator service for stream resources. + */ + public function __construct($view, $locator, $cache) + { + $this->view = $view; + $this->locator = $locator; + $this->cache = $cache; + } + + /** + * Invoke the CheckEnvironment middleware, performing all pre-flight checks and returning an error page if problems were found. + * + * @param \Psr\Http\Message\ServerRequestInterface $request PSR7 request + * @param \Psr\Http\Message\ResponseInterface $response PSR7 response + * @param callable $next Next middleware + * + * @return \Psr\Http\Message\ResponseInterface + */ + public function __invoke($request, $response, $next) + { + $problemsFound = false; + + // If production environment and no cached checks, perform environment checks + if ($this->isProduction() && $this->cache->get('checkEnvironment') != 'pass') { + $problemsFound = $this->checkAll(); + + // Cache if checks passed + if (!$problemsFound) { + $this->cache->forever('checkEnvironment', 'pass'); + } + } elseif (!$this->isProduction()) { + $problemsFound = $this->checkAll(); + } + + if ($problemsFound) { + $results = array_merge($this->resultsFailed, $this->resultsSuccess); + + $response = $this->view->render($response, 'pages/error/config-errors.html.twig', [ + "messages" => $results + ]); + } else { + $response = $next($request, $response); + } + + return $response; + } + + /** + * Run through all pre-flight checks. + */ + public function checkAll() + { + $problemsFound = false; + + if ($this->checkApache()) $problemsFound = true; + + if ($this->checkPhp()) $problemsFound = true; + + if ($this->checkPdo()) $problemsFound = true; + + if ($this->checkGd()) $problemsFound = true; + + if ($this->checkImageFunctions()) $problemsFound = true; + + if ($this->checkPermissions()) $problemsFound = true; + + return $problemsFound; + } + + /** + * For Apache environments, check that required Apache modules are installed. + */ + public function checkApache() + { + $problemsFound = false; + + // Perform some Apache checks. We may also need to do this before any routing takes place. + if (strpos(php_sapi_name(), 'apache') !== false) { + + $require_apache_modules = ['mod_rewrite']; + $apache_modules = apache_get_modules(); + + $apache_status = []; + + foreach ($require_apache_modules as $module) { + if (!in_array($module, $apache_modules)) { + $problemsFound = true; + $this->resultsFailed['apache-' . $module] = [ + "title" => "<i class='fa fa-server fa-fw'></i> Missing Apache module <b>$module</b>.", + "message" => "Please make sure that the <code>$module</code> Apache module is installed and enabled. If you use shared hosting, you will need to ask your web host to do this for you.", + "success" => false + ]; + } else { + $this->resultsSuccess['apache-' . $module] = [ + "title" => "<i class='fa fa-server fa-fw'></i> Apache module <b>$module</b> is installed and enabled.", + "message" => "Great, we found the <code>$module</code> Apache module!", + "success" => true + ]; + } + } + } + + return $problemsFound; + } + + /** + * Check for GD library (required for Captcha). + */ + public function checkGd() + { + $problemsFound = false; + + if (!(extension_loaded('gd') && function_exists('gd_info'))) { + $problemsFound = true; + $this->resultsFailed['gd'] = [ + "title" => "<i class='fa fa-image fa-fw'></i> GD library not installed", + "message" => "We could not confirm that the <code>GD</code> library is installed and enabled. GD is an image processing library that UserFrosting uses to generate captcha codes for user account registration.", + "success" => false + ]; + } else { + $this->resultsSuccess['gd'] = [ + "title" => "<i class='fa fa-image fa-fw'></i> GD library installed!", + "message" => "Great, you have <code>GD</code> installed and enabled.", + "success" => true + ]; + } + + return $problemsFound; + } + + /** + * Check that all image* functions used by Captcha exist. + * + * Some versions of GD are missing one or more of these functions, thus why we check for them explicitly. + */ + public function checkImageFunctions() + { + $problemsFound = false; + + $funcs = [ + 'imagepng', + 'imagecreatetruecolor', + 'imagecolorallocate', + 'imagefilledrectangle', + 'imageline', + 'imagesetpixel', + 'imagefontwidth', + 'imagestring' + ]; + + foreach ($funcs as $func) { + if (!function_exists($func)) { + $problemsFound = true; + $this->resultsFailed['function-' . $func] = [ + "title" => "<i class='fa fa-code fa-fw'></i> Missing image manipulation function.", + "message" => "It appears that function <code>$func</code> is not available. UserFrosting needs this to render captchas.", + "success" => false + ]; + } else { + $this->resultsSuccess['function-' . $func] = [ + "title" => "<i class='fa fa-code fa-fw'></i> Function <b>$func</b> is available!", + "message" => "Sweet!", + "success" => true + ]; + } + } + + return $problemsFound; + } + + /** + * Check that PDO is installed and enabled. + */ + public function checkPdo() + { + $problemsFound = false; + + if (!class_exists('PDO')) { + $problemsFound = true; + $this->resultsFailed['pdo'] = [ + "title" => "<i class='fa fa-database fa-fw'></i> PDO is not installed.", + "message" => "I'm sorry, you must have PDO installed and enabled in order for UserFrosting to access the database. If you don't know what PDO is, please see <a href='http://php.net/manual/en/book.pdo.php'>http://php.net/manual/en/book.pdo.php</a>.", + "success" => false + ]; + } else { + $this->resultsSuccess['pdo'] = [ + "title" => "<i class='fa fa-database fa-fw'></i> PDO is installed!", + "message" => "You've got PDO installed. Good job!", + "success" => true + ]; + } + + return $problemsFound; + } + + /** + * Check that log, cache, and session directories are writable, and that other directories are set appropriately for the environment. + */ + function checkPermissions() + { + $problemsFound = false; + + $shouldBeWriteable = [ + $this->locator->findResource('log://') => true, + $this->locator->findResource('cache://') => true, + $this->locator->findResource('session://') => true + ]; + + if ($this->isProduction()) { + // Should be write-protected in production! + $shouldBeWriteable = array_merge($shouldBeWriteable, [ + \UserFrosting\SPRINKLES_DIR => false, + \UserFrosting\VENDOR_DIR => false + ]); + } + + // Check for essential files & perms + foreach ($shouldBeWriteable as $file => $assertWriteable) { + $is_dir = false; + if (!file_exists($file)) { + $problemsFound = true; + $this->resultsFailed['file-' . $file] = [ + "title" => "<i class='fa fa-file-o fa-fw'></i> File or directory does not exist.", + "message" => "We could not find the file or directory <code>$file</code>.", + "success" => false + ]; + } else { + $writeable = is_writable($file); + if ($assertWriteable !== $writeable) { + $problemsFound = true; + $this->resultsFailed['file-' . $file] = [ + "title" => "<i class='fa fa-file-o fa-fw'></i> Incorrect permissions for file or directory.", + "message" => "<code>$file</code> is " + . ($writeable ? "writeable" : "not writeable") + . ", but it should " + . ($assertWriteable ? "be writeable" : "not be writeable") + . ". Please modify the OS user or group permissions so that user <b>" + . exec('whoami') . "</b> " + . ($assertWriteable ? "has" : "does not have") . " write permissions for this directory.", + "success" => false + ]; + } else { + $this->resultsSuccess['file-' . $file] = [ + "title" => "<i class='fa fa-file-o fa-fw'></i> File/directory check passed!", + "message" => "<code>$file</code> exists and is correctly set as <b>" + . ($writeable ? "writeable" : "not writeable") + . "</b>.", + "success" => true + ]; + } + } + } + return $problemsFound; + } + + /** + * Check that PHP meets the minimum required version. + */ + public function checkPhp() + { + $problemsFound = false; + + // Check PHP version + if (version_compare(phpversion(), \UserFrosting\PHP_MIN_VERSION, '<')) { + $problemsFound = true; + $this->resultsFailed['phpVersion'] = [ + "title" => "<i class='fa fa-code fa-fw'></i> You need to upgrade your PHP installation.", + "message" => "I'm sorry, UserFrosting requires version " . \UserFrosting\PHP_MIN_VERSION . " or greater. Please upgrade your version of PHP, or contact your web hosting service and ask them to upgrade it for you.", + "success" => false + ]; + } else { + $this->resultsSuccess['phpVersion'] = [ + "title" => "<i class='fa fa-code fa-fw'></i> PHP version checks out!", + "message" => "You're using PHP " . \UserFrosting\PHP_MIN_VERSION . "or higher. Great!", + "success" => true + ]; + } + + return $problemsFound; + } + + /** + * Determine whether or not we are running in production mode. + * + * @return bool + */ + public function isProduction() + { + return (getenv('UF_MODE') == 'production'); + } +} diff --git a/login/app/sprinkles/core/src/Util/ClassMapper.php b/login/app/sprinkles/core/src/Util/ClassMapper.php new file mode 100755 index 0000000..5fa0881 --- /dev/null +++ b/login/app/sprinkles/core/src/Util/ClassMapper.php @@ -0,0 +1,94 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ + +namespace UserFrosting\Sprinkle\Core\Util; + +/** + * UserFrosting class mapper. + * + * This creates an abstraction layer for overrideable classes. + * For example, if we want to replace usages of the User class with MyUser, this abstraction layer handles that. + * + * @author Alex Weissman (https://alexanderweissman.com) + * @author Roger Ardibee + */ +class ClassMapper +{ + /** + * Mapping of generic class identifiers to specific class names. + */ + protected $classMappings = []; + + /** + * Creates an instance for a requested class identifier. + * + * @param string $identifier The identifier for the class, e.g. 'user' + * @param mixed ...$arg Whatever needs to be passed to the constructor. + */ + public function createInstance($identifier) + { + $className = $this->getClassMapping($identifier); + + $params = array_slice(func_get_args(), 1); + + // We must use reflection in PHP < 5.6. See http://stackoverflow.com/questions/8734522/dynamically-call-class-with-variable-number-of-parameters-in-the-constructor + $reflection = new \ReflectionClass($className); + + return $reflection->newInstanceArgs($params); + } + + /** + * Gets the fully qualified class name for a specified class identifier. + * + * @param string $identifier + * @return string + */ + public function getClassMapping($identifier) + { + if (isset($this->classMappings[$identifier])) { + return $this->classMappings[$identifier]; + } else { + throw new \OutOfBoundsException("There is no class mapped to the identifier '$identifier'."); + } + } + + /** + * Assigns a fully qualified class name to a specified class identifier. + * + * @param string $identifier + * @param string $className + * @return ClassMapper + */ + public function setClassMapping($identifier, $className) + { + // Check that class exists + if (!class_exists($className)) { + throw new BadClassNameException("Unable to find the class '$className'." ); + } + + $this->classMappings[$identifier] = $className; + + return $this; + } + + /** + * Call a static method for a specified class. + * + * @param string $identifier The identifier for the class, e.g. 'user' + * @param string $methodName The method to be invoked. + * @param mixed ...$arg Whatever needs to be passed to the method. + */ + public function staticMethod($identifier, $methodName) + { + $className = $this->getClassMapping($identifier); + + $params = array_slice(func_get_args(), 2); + + return call_user_func_array("$className::$methodName", $params); + } +} diff --git a/login/app/sprinkles/core/src/Util/EnvironmentInfo.php b/login/app/sprinkles/core/src/Util/EnvironmentInfo.php new file mode 100755 index 0000000..aba9837 --- /dev/null +++ b/login/app/sprinkles/core/src/Util/EnvironmentInfo.php @@ -0,0 +1,68 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Util; + +use Illuminate\Database\Capsule\Manager as Capsule; + +/** + * EnvironmentInfo Class + * + * Gets basic information about the application environment. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class EnvironmentInfo +{ + /** + * @var ContainerInterface The DI container for your application. + */ + public static $ci; + + /** + * Get an array of key-value pairs containing basic information about the default database. + * + * @return string[] the properties of this database. + */ + public static function database() + { + static::$ci['db']; + + $pdo = Capsule::connection()->getPdo(); + $results = []; + + try { + $results['type'] = $pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); + } catch (Exception $e) { + $results['type'] = "Unknown"; + } + + try { + $results['version'] = $pdo->getAttribute(\PDO::ATTR_SERVER_VERSION); + } catch (Exception $e) { + $results['version'] = ""; + } + + return $results; + } + + /** + * Test whether a DB connection can be established. + * + * @return bool true if the connection can be established, false otherwise. + */ + public static function canConnectToDatabase() + { + try { + Capsule::connection()->getPdo(); + } catch (\PDOException $e) { + return false; + } + + return true; + } +} diff --git a/login/app/sprinkles/core/src/Util/ShutdownHandler.php b/login/app/sprinkles/core/src/Util/ShutdownHandler.php new file mode 100755 index 0000000..e7a6903 --- /dev/null +++ b/login/app/sprinkles/core/src/Util/ShutdownHandler.php @@ -0,0 +1,167 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Util; + +use Interop\Container\ContainerInterface; +use UserFrosting\Sprinkle\Core\Http\Concerns\DeterminesContentType; + +/** + * Registers a handler to be invoked whenever the application shuts down. + * If it shut down due to a fatal error, will generate a clean error message. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class ShutdownHandler +{ + use DeterminesContentType; + + /** + * @var ContainerInterface The global container object, which holds all your services. + */ + protected $ci; + + /** + * @var bool + */ + protected $displayErrorInfo; + + /** + * Constructor. + * + * @param ContainerInterface $ci The global container object, which holds all your services. + * @param bool $displayErrorInfo + */ + public function __construct(ContainerInterface $ci, $displayErrorInfo) + { + $this->ci = $ci; + $this->displayErrorInfo = $displayErrorInfo; + } + + /** + * Register this class with the shutdown handler. + * + * @return void + */ + public function register() + { + register_shutdown_function([$this, 'fatalHandler']); + } + + /** + * Set up the fatal error handler, so that we get a clean error message and alert instead of a WSOD. + */ + public function fatalHandler() + { + $error = error_get_last(); + $fatalErrorTypes = [ + E_ERROR, + E_PARSE, + E_CORE_ERROR, + E_COMPILE_ERROR, + E_RECOVERABLE_ERROR + ]; + + // Handle fatal errors and parse errors + if ($error !== NULL && in_array($error['type'], $fatalErrorTypes)) { + + // Build the appropriate error message (debug or client) + if ($this->displayErrorInfo) { + $errorMessage = $this->buildErrorInfoMessage($error); + } else { + $errorMessage = "Oops, looks like our server might have goofed. If you're an admin, please ensure that <code>php.log_errors</code> is enabled, and then check the <strong>PHP</strong> error log."; + } + + // For CLI, just print the message and exit. + if (php_sapi_name() === 'cli') { + exit($errorMessage . PHP_EOL); + } + + // For all other environments, print a debug response for the requested data type + echo $this->buildErrorPage($errorMessage); + + // If this is an AJAX request and AJAX debugging is turned off, write message to the alert stream + if ($this->ci->request->isXhr() && !$this->ci->config['site.debug.ajax']) { + if ($this->ci->alerts && is_object($this->ci->alerts)) { + $this->ci->alerts->addMessageTranslated('danger', $errorMessage); + } + } + + header('HTTP/1.1 500 Internal Server Error'); + exit(); + } + } + + /** + * Build the error message string. + * + * @param array $error + * @return string + */ + protected function buildErrorInfoMessage(array $error) + { + $errfile = $error['file']; + $errline = (string) $error['line']; + $errstr = $error['message']; + + $errorTypes = [ + E_ERROR => 'Fatal error', + E_PARSE => 'Parse error', + E_CORE_ERROR => 'PHP core error', + E_COMPILE_ERROR => 'Zend compile error', + E_RECOVERABLE_ERROR => 'Catchable fatal error' + ]; + + return "<strong>" . $errorTypes[$error['type']] . "</strong>: $errstr in <strong>$errfile</strong> on line <strong>$errline</strong>"; + } + + /** + * Build an error response of the appropriate type as determined by the request's Accept header. + * + * @param string $message + * @return string + */ + protected function buildErrorPage($message) + { + $contentType = $this->determineContentType($this->ci->request, $this->ci->config['site.debug.ajax']); + + switch ($contentType) { + case 'application/json': + $error = ['message' => $message]; + return json_encode($error, JSON_PRETTY_PRINT); + + case 'text/html': + return $this->buildHtmlErrorPage($message); + + default: + case 'text/plain': + return $message; + } + } + + /** + * Build an HTML error page from an error string. + * + * @param string $errorMessage + * @return string + */ + protected function buildHtmlErrorPage($message) + { + $title = 'UserFrosting Application Error'; + $html = "<p>$message</p>"; + + return sprintf( + "<html><head><meta http-equiv='Content-Type' content='text/html; charset=utf-8'>" . + "<title>%s</title><style>body{margin:0;padding:30px;font:12px/1.5 Helvetica,Arial,Verdana," . + "sans-serif;}h1{margin:0;font-size:48px;font-weight:normal;line-height:48px;}" . + "</style></head><body><h1>%s</h1>%s</body></html>", + $title, + $title, + $html + ); + } +} diff --git a/login/app/sprinkles/core/src/Util/Util.php b/login/app/sprinkles/core/src/Util/Util.php new file mode 100755 index 0000000..ae551cf --- /dev/null +++ b/login/app/sprinkles/core/src/Util/Util.php @@ -0,0 +1,173 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Sprinkle\Core\Util; + +/** + * Util Class + * + * Static utility functions. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class Util +{ + /** + * Extracts specific fields from one associative array, and places them into another. + * + * @param mixed[] $inputArray + * @param string[] $fieldArray + * @param bool $remove + * @return mixed[] + */ + static public function extractFields(&$inputArray, $fieldArray, $remove = true) + { + $result = []; + + foreach ($fieldArray as $name) { + if (array_key_exists($name, $inputArray)) { + $result[$name] = $inputArray[$name]; + + // Optionally remove value from input array + if ($remove) { + unset($inputArray[$name]); + } + } + } + + return $result; + } + + /** + * Extracts numeric portion of a string (for example, for normalizing phone numbers). + * + * @param string $str + * @return string + */ + static public function extractDigits($str) + { + return preg_replace('/[^0-9]/', '', $str); + } + + /** + * Formats a phone number as a standard 7- or 10-digit string (xxx) xxx-xxxx + * + * @param string $phone + * @return string + */ + static public function formatPhoneNumber($phone) + { + $num = static::extractDigits($phone); + + $len = strlen($num); + + if ($len == 7) { + $num = preg_replace('/([0-9]{3})([0-9]{4})/', '$1-$2', $num); + } elseif ($len == 10) { + $num = preg_replace('/([0-9]{3})([0-9]{3})([0-9]{4})/', '($1) $2-$3', $num); + } + + return $num; + } + + /** + * Nicely format an array for printing. + * See https://stackoverflow.com/a/9776726/2970321 + * + * @param array $arr + * @return string + */ + static public function prettyPrintArray(array $arr) + { + $json = json_encode($arr); + $result = ''; + $level = 0; + $inQuotes = false; + $inEscape = false; + $endsLineLevel = NULL; + $jsonLength = strlen($json); + + for ($i = 0; $i < $jsonLength; $i++) { + $char = $json[$i]; + $newLineLevel = NULL; + $post = ''; + if ($endsLineLevel !== NULL) { + $newLineLevel = $endsLineLevel; + $endsLineLevel = NULL; + } + if ($inEscape) { + $inEscape = false; + } elseif ($char === '"') { + $inQuotes = !$inQuotes; + } elseif (!$inQuotes) { + switch ($char) { + case '}': case ']': + $level--; + $endsLineLevel = NULL; + $newLineLevel = $level; + break; + + case '{': case '[': + $level++; + + case ',': + $endsLineLevel = $level; + break; + + case ':': + $post = ' '; + break; + + case ' ': case '\t': case '\n': case '\r': + $char = ''; + $endsLineLevel = $newLineLevel; + $newLineLevel = NULL; + break; + } + } elseif ($char === '\\') { + $inEscape = true; + } + + if ($newLineLevel !== NULL) { + $result .= '<br>'.str_repeat( ' ', $newLineLevel); + } + + $result .= $char.$post; + } + + return $result; + } + + /** + * Generate a random phrase, consisting of a specified number of adjectives, followed by a noun. + * + * @param int $numAdjectives + * @param int $maxLength + * @param int $maxTries + * @param string $separator + * @return string + */ + static public function randomPhrase($numAdjectives, $maxLength = 9999999, $maxTries = 10, $separator = '-') + { + $adjectives = include('extra://adjectives.php'); + $nouns = include('extra://nouns.php'); + + for ($n = 0; $n < $maxTries; $n++) { + $keys = array_rand($adjectives, $numAdjectives); + $matches = array_only($adjectives, $keys); + + $result = implode($separator, $matches); + $result .= $separator . $nouns[array_rand($nouns)]; + $result = str_slug($result, $separator); + if (strlen($result) < $maxLength) { + return $result; + } + } + + return ''; + } +} diff --git a/login/app/sprinkles/core/templates/forms/csrf.html.twig b/login/app/sprinkles/core/templates/forms/csrf.html.twig new file mode 100755 index 0000000..989f88c --- /dev/null +++ b/login/app/sprinkles/core/templates/forms/csrf.html.twig @@ -0,0 +1,2 @@ +<input type="hidden" name="{{site.csrf.keys.name}}" value="{{site.csrf.name}}"> +<input type="hidden" name="{{site.csrf.keys.value}}" value="{{site.csrf.value}}"> diff --git a/login/app/sprinkles/core/templates/mail/.gitkeep b/login/app/sprinkles/core/templates/mail/.gitkeep new file mode 100755 index 0000000..e69de29 --- /dev/null +++ b/login/app/sprinkles/core/templates/mail/.gitkeep diff --git a/login/app/sprinkles/core/templates/modals/modal.html.twig b/login/app/sprinkles/core/templates/modals/modal.html.twig new file mode 100755 index 0000000..cb17dfd --- /dev/null +++ b/login/app/sprinkles/core/templates/modals/modal.html.twig @@ -0,0 +1,27 @@ +{# Base layout for modals. +#} + +{# Conditional block. See http://stackoverflow.com/a/13806784/2970321 #} +{% set _modal_footer = block('modal_footer') %} + +<div class="modal fade" role="dialog"> + <div class="modal-dialog {% block modal_size %}{% endblock %}" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h4 class="modal-title">{% block modal_title %}{% endblock %}</h4> + </div> + <div class="modal-body"> + <div class="alerts-modal"> + </div> + {% block modal_body %} + {% endblock %} + </div> + {% if _modal_footer is not empty %} + <div class="modal-footer"> + {{ _modal_footer | raw }} + </div> + {% endif %} + </div> + </div> +</div> diff --git a/login/app/sprinkles/core/templates/navigation/breadcrumb.html.twig b/login/app/sprinkles/core/templates/navigation/breadcrumb.html.twig new file mode 100755 index 0000000..e95e302 --- /dev/null +++ b/login/app/sprinkles/core/templates/navigation/breadcrumb.html.twig @@ -0,0 +1,4 @@ +<ol class="breadcrumb"> + <li><a href="{{site.uri.public}}"><i class="fa fa-home"></i> {{site.title}}</a></li> + <li class="active">{{ page_title | raw }}</li> +</ol>
\ No newline at end of file diff --git a/login/app/sprinkles/core/templates/navigation/main-nav.html.twig b/login/app/sprinkles/core/templates/navigation/main-nav.html.twig new file mode 100755 index 0000000..3acbb87 --- /dev/null +++ b/login/app/sprinkles/core/templates/navigation/main-nav.html.twig @@ -0,0 +1,31 @@ +<nav class="navbar navbar-static-top"> + <div class="container"> + <div class="navbar-header"> + {% block navbar_logo %}<a href="{{site.uri.public}}" class="navbar-brand">{{site.title}}</a>{% endblock %} + <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar-collapse"> + <i class="fa fa-bars"></i> + </button> + </div> + + <!-- Collect the nav links, forms, and other content for toggling --> + <div class="collapse navbar-collapse pull-left" id="navbar-collapse"> + <ul class="nav navbar-nav"> + {% block main_nav %} + <li> + <a href="{{site.uri.public}}/about">{{translate("ABOUT")}}</a> + </li> + {% endblock %} + </ul> + </div> + + <!-- /.navbar-collapse --> + <!-- Navbar Right Menu --> + <div class="navbar-custom-menu"> + <ul class="nav navbar-nav"> + {% block secondary_nav %}{% endblock %} + </ul> + </div> + <!-- /.navbar-custom-menu --> + </div> + <!-- /.container-fluid --> +</nav>
\ No newline at end of file diff --git a/login/app/sprinkles/core/templates/pages/about.html.twig b/login/app/sprinkles/core/templates/pages/about.html.twig new file mode 100755 index 0000000..95f9b72 --- /dev/null +++ b/login/app/sprinkles/core/templates/pages/about.html.twig @@ -0,0 +1,173 @@ +{% extends "pages/abstract/default.html.twig" %} + +{% set page_active = "about" %} + +{# Overrides blocks in head of base template #} +{% block page_title %}{{translate("ABOUT")}}{% endblock %} + +{% block page_description %}All about my UserFrosting website.{% endblock %} + +{% block body_matter %} + + <!-- Intro Content --> + <div class="row"> + <div class="col-md-6"> + <img class="img-responsive" src="//placehold.it/750x450" alt=""> + </div> + <div class="col-md-6"> + <h2>About Modern Business</h2> + <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Sed voluptate nihil eum consectetur similique? Consectetur, quod, incidunt, harum nisi dolores delectus reprehenderit voluptatem perferendis dicta dolorem non blanditiis ex fugiat.</p> + <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Saepe, magni, aperiam vitae illum voluptatum aut sequi impedit non velit ab ea pariatur sint quidem corporis eveniet. Odit, temporibus reprehenderit dolorum!</p> + <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Et, consequuntur, modi mollitia corporis ipsa voluptate corrupti eum ratione ex ea praesentium quibusdam? Aut, in eum facere corrupti necessitatibus perspiciatis quis?</p> + </div> + </div> + <!-- /.row --> + + <!-- Team Members --> + <div class="row"> + <div class="col-lg-12"> + <h2 class="page-header">Our Team</h2> + </div> + <div class="col-md-4 text-center"> + <div class="thumbnail"> + <img class="img-responsive" src="//placehold.it/750x450" alt=""> + <div class="caption"> + <h3>John Smith<br> + <small>Job Title</small> + </h3> + <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Iste saepe et quisquam nesciunt maxime.</p> + <ul class="list-inline"> + <li><a href="#"><i class="fa fa-2x fa-facebook-square"></i></a> + </li> + <li><a href="#"><i class="fa fa-2x fa-linkedin-square"></i></a> + </li> + <li><a href="#"><i class="fa fa-2x fa-twitter-square"></i></a> + </li> + </ul> + </div> + </div> + </div> + <div class="col-md-4 text-center"> + <div class="thumbnail"> + <img class="img-responsive" src="//placehold.it/750x450" alt=""> + <div class="caption"> + <h3>John Smith<br> + <small>Job Title</small> + </h3> + <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Iste saepe et quisquam nesciunt maxime.</p> + <ul class="list-inline"> + <li><a href="#"><i class="fa fa-2x fa-facebook-square"></i></a> + </li> + <li><a href="#"><i class="fa fa-2x fa-linkedin-square"></i></a> + </li> + <li><a href="#"><i class="fa fa-2x fa-twitter-square"></i></a> + </li> + </ul> + </div> + </div> + </div> + <div class="col-md-4 text-center"> + <div class="thumbnail"> + <img class="img-responsive" src="//placehold.it/750x450" alt=""> + <div class="caption"> + <h3>John Smith<br> + <small>Job Title</small> + </h3> + <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Iste saepe et quisquam nesciunt maxime.</p> + <ul class="list-inline"> + <li><a href="#"><i class="fa fa-2x fa-facebook-square"></i></a> + </li> + <li><a href="#"><i class="fa fa-2x fa-linkedin-square"></i></a> + </li> + <li><a href="#"><i class="fa fa-2x fa-twitter-square"></i></a> + </li> + </ul> + </div> + </div> + </div> + <div class="col-md-4 text-center"> + <div class="thumbnail"> + <img class="img-responsive" src="//placehold.it/750x450" alt=""> + <div class="caption"> + <h3>John Smith<br> + <small>Job Title</small> + </h3> + <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Iste saepe et quisquam nesciunt maxime.</p> + <ul class="list-inline"> + <li><a href="#"><i class="fa fa-2x fa-facebook-square"></i></a> + </li> + <li><a href="#"><i class="fa fa-2x fa-linkedin-square"></i></a> + </li> + <li><a href="#"><i class="fa fa-2x fa-twitter-square"></i></a> + </li> + </ul> + </div> + </div> + </div> + <div class="col-md-4 text-center"> + <div class="thumbnail"> + <img class="img-responsive" src="//placehold.it/750x450" alt=""> + <div class="caption"> + <h3>John Smith<br> + <small>Job Title</small> + </h3> + <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Iste saepe et quisquam nesciunt maxime.</p> + <ul class="list-inline"> + <li><a href="#"><i class="fa fa-2x fa-facebook-square"></i></a> + </li> + <li><a href="#"><i class="fa fa-2x fa-linkedin-square"></i></a> + </li> + <li><a href="#"><i class="fa fa-2x fa-twitter-square"></i></a> + </li> + </ul> + </div> + </div> + </div> + <div class="col-md-4 text-center"> + <div class="thumbnail"> + <img class="img-responsive" src="//placehold.it/750x450" alt=""> + <div class="caption"> + <h3>John Smith<br> + <small>Job Title</small> + </h3> + <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Iste saepe et quisquam nesciunt maxime.</p> + <ul class="list-inline"> + <li><a href="#"><i class="fa fa-2x fa-facebook-square"></i></a> + </li> + <li><a href="#"><i class="fa fa-2x fa-linkedin-square"></i></a> + </li> + <li><a href="#"><i class="fa fa-2x fa-twitter-square"></i></a> + </li> + </ul> + </div> + </div> + </div> + </div> + <!-- /.row --> + + <!-- Our Customers --> + <div class="row"> + <div class="col-lg-12"> + <h2 class="page-header">Our Customers</h2> + </div> + <div class="col-md-2 col-sm-4 col-xs-6"> + <img class="img-responsive customer-img" src="//placehold.it/500x300" alt=""> + </div> + <div class="col-md-2 col-sm-4 col-xs-6"> + <img class="img-responsive customer-img" src="//placehold.it/500x300" alt=""> + </div> + <div class="col-md-2 col-sm-4 col-xs-6"> + <img class="img-responsive customer-img" src="//placehold.it/500x300" alt=""> + </div> + <div class="col-md-2 col-sm-4 col-xs-6"> + <img class="img-responsive customer-img" src="//placehold.it/500x300" alt=""> + </div> + <div class="col-md-2 col-sm-4 col-xs-6"> + <img class="img-responsive customer-img" src="//placehold.it/500x300" alt=""> + </div> + <div class="col-md-2 col-sm-4 col-xs-6"> + <img class="img-responsive customer-img" src="//placehold.it/500x300" alt=""> + </div> + </div> + <!-- /.row --> +{% endblock %} diff --git a/login/app/sprinkles/core/templates/pages/abstract/base.html.twig b/login/app/sprinkles/core/templates/pages/abstract/base.html.twig new file mode 100755 index 0000000..43e839e --- /dev/null +++ b/login/app/sprinkles/core/templates/pages/abstract/base.html.twig @@ -0,0 +1,103 @@ +{# This is the base layout template for all pages. #} + +{% block page %} +<!DOCTYPE html> +<html lang="en-US"> + {% block head %} + <head> + <!-- The 'X-UA-Compatible' tag must be first, otherwise IE will not respect it. See http://stackoverflow.com/questions/25557299/internet-explorer-11-disable-display-intranet-sites-in-compatibility-view-via --> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta charset="utf-8"> + <meta name="generator" content="UserFrosting" /> + <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> + <meta name="description" content="{% block page_description %}This page has not yet been configured.{% endblock %}"> + <meta name="author" content="{% block page_author %}{{ site.author }}{% endblock %}"> + + <title>{{ site.title }} | {% block page_title %}New Page{% endblock %}</title> + + {% include "pages/partials/favicons.html.twig" %} + + {# Use this block to add extra content in page head without having to override the entire base layout #} + {% block head_extra %}{% endblock %} + + {% block stylesheets %} + {# Override this block in a child layout template or page template to override site-level stylesheets. #} + {% block stylesheets_site %} + <!-- Include main CSS asset bundle --> + {{ assets.css() | raw }} + {% endblock %} + + {# Override this block in a child layout template or page template to specify or override stylesheets for groups of similar pages. #} + {% block stylesheets_page_group %} + {% endblock %} + + {# Override this block in a child layout template or page template to specify or override page-level stylesheets. #} + {% block stylesheets_page %} + {% endblock %} + {% endblock %} + + {# Site author link #} + {% if site.uri.author %} + <link href="{% block page_author_link %}{{ site.uri.author }}{% endblock %}" rel="author" /> + {% endif %} + + {# Site publisher link #} + {% if site.uri.publisher %} + <link href="{% block page_publisher_link %}{{ site.uri.publisher }}{% endblock %}" rel="publisher" /> + {% endif %} + + {# Canonical page link #} + {% if block('page_canonical') %} + <link href="{{site.uri.base.scheme}}://{% block page_canonical %}{% endblock %}" rel="canonical" /> + {% endif %} + + <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> + <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> + <!--[if lt IE 9]> + <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> + <script src="//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script> + <![endif]--> + + {% include "pages/partials/analytics.html.twig" %} + </head> + {% endblock %} + + {% block body %} + <body {% block body_attributes %}{% endblock %}> + {# Page Content #} + {% block content %}{% endblock %} + + {# Handlebars template for uf-alerts. #} + {% block uf_alerts_template %} + {% include "pages/partials/alerts.html.twig" %} + {% endblock %} + + <!-- Javascript configuration --> + <script> + {% include "pages/partials/config.js.twig" %} + </script> + + {% block scripts %} + {# Override this block in a child layout template or page template to override site-level scripts. #} + {% block scripts_site %} + <!-- Load jQuery --> + <script src="//code.jquery.com/jquery-2.2.4.min.js" ></script> + <!-- Fallback if CDN is unavailable --> + <script>window.jQuery || document.write('<script src="{{ assets.url('assets://vendor/jquery/dist/jquery.min.js', true) }}"><\/script>')</script> + + {{ assets.js() | raw }} + {% endblock %} + + {# Override this block in a child layout template or page template to specify or override scripts for groups of similar pages. #} + {% block scripts_page_group %} + {% endblock %} + + {# Override this block in a child layout template or page template to specify or override page-level scripts. #} + {% block scripts_page %} + {% endblock %} + {% endblock %} + + </body> + {% endblock %} +</html> +{% endblock %} diff --git a/login/app/sprinkles/core/templates/pages/abstract/default.html.twig b/login/app/sprinkles/core/templates/pages/abstract/default.html.twig new file mode 100755 index 0000000..625dea1 --- /dev/null +++ b/login/app/sprinkles/core/templates/pages/abstract/default.html.twig @@ -0,0 +1,45 @@ +{% extends "pages/abstract/base.html.twig" %} + +{% block stylesheets_page_group %} +{% endblock %} + +{% block body_attributes %} + class="hold-transition skin-{{site.AdminLTE.skin}} layout-top-nav" +{% endblock %} + +{% block content %} + +<div class="wrapper"> + + <header class="main-header"> + {% include "navigation/main-nav.html.twig" %} + </header> + + <!-- Full Width Column --> + <div class="content-wrapper"> + <div class="container"> + <!-- Content Header (Page header) --> + {% block content_header %} + <section class="content-header"> + <h1>{% block header_title %}{{ block('page_title') }}{% endblock %}</h1> + {% if block('page_description') is not empty %}<h1><small>{% block header_description %}{{ block('page_description') }}{% endblock %}</small></h1>{% endif %} + {% block breadcrumb %} + {% include 'navigation/breadcrumb.html.twig' with {page_title: block('page_title')} %} + {% endblock %} + <div id="alerts-page"></div> + </section> + {% endblock %} + <section class="content"> + {% block body_matter %}{% endblock %} + </section> + </div> + </div> + <!-- /.content-wrapper --> + + {% block footer %} + {% include "pages/partials/footer.html.twig" %} + {% endblock %} +</div> +<!-- ./wrapper --> + +{% endblock %}
\ No newline at end of file diff --git a/login/app/sprinkles/core/templates/pages/abstract/error.html.twig b/login/app/sprinkles/core/templates/pages/abstract/error.html.twig new file mode 100755 index 0000000..c7c7205 --- /dev/null +++ b/login/app/sprinkles/core/templates/pages/abstract/error.html.twig @@ -0,0 +1,32 @@ +{# This is the layout template for error pages. Note that there is no navigation bar. #} +{% extends "pages/abstract/base.html.twig" %} + +{% block page_title %}{{ translate('ERROR.TITLE') }}{% endblock %} + +{% block page_description %}{{ translate('ERROR.DESCRIPTION') }}{% endblock %} + +{% block body %} + <body {% block body_attributes %}{% endblock %}> + <!-- Main content --> + <section class="content"> + <div class="error-page"> + {% block headline %}<h2 class="headline text-red"><i class="fa fa-terminal" aria-hidden="true"></i></h2>{% endblock %} + + <div class="error-content"> + <br /><h3>{% block heading %}<i class="fa fa-warning text-red"></i> {{ translate('ERROR.ENCOUNTERED') }}{% endblock %}</h3> + + {% block content %} + {% if messages %}<p>{{ translate('ERROR.DETAIL') }}</p>{% endif %} + {% for message in messages %} + <div class="alert alert-danger">{{ translate(message.message, message.parameters) }}</div> + {% endfor %} + <p class="lead">{{ translate('ERROR.RETURN', {url : site.uri.public})|raw }}</p> + {% endblock %} + </div> + <!-- /.error-content --> + </div> + <!-- /.error-page --> + </section> + <!-- /.content --> + </body> +{% endblock %}
\ No newline at end of file diff --git a/login/app/sprinkles/core/templates/pages/error/400.html.twig b/login/app/sprinkles/core/templates/pages/error/400.html.twig new file mode 100755 index 0000000..3570df8 --- /dev/null +++ b/login/app/sprinkles/core/templates/pages/error/400.html.twig @@ -0,0 +1,9 @@ +{% extends "pages/abstract/error.html.twig" %} + +{% block page_title %}{{ translate('ERROR.400.TITLE') }}{% endblock %} + +{% block page_description %}{{ translate('ERROR.400.DESCRIPTION') }}{% endblock %} + +{% block headline %}<h2 class="headline text-yellow">400</h2>{% endblock %} + +{% block heading %}<i class="fa fa-warning text-yellow"></i> {{ translate('ERROR.400.TITLE') }}{% endblock %} diff --git a/login/app/sprinkles/core/templates/pages/error/404.html.twig b/login/app/sprinkles/core/templates/pages/error/404.html.twig new file mode 100755 index 0000000..6630e1b --- /dev/null +++ b/login/app/sprinkles/core/templates/pages/error/404.html.twig @@ -0,0 +1,16 @@ +{% extends "pages/abstract/error.html.twig" %} + +{% block page_title %}{{ translate('ERROR.404.TITLE') }}{% endblock %} + +{% block page_description %}{{ translate('ERROR.404.DESCRIPTION') }}{% endblock %} + +{% block headline %}<h2 class="headline text-yellow">404</h2>{% endblock %} + +{% block heading %}<i class="fa fa-warning text-yellow"></i> {{ translate('ERROR.404.DETAIL') }}{% endblock %} + +{% block content %} + <p> + {{ translate('ERROR.404.EXPLAIN') }} + {{ translate('ERROR.404.RETURN', {url: site.uri.public})|raw }} + </p> +{% endblock %} diff --git a/login/app/sprinkles/core/templates/pages/error/config-errors.html.twig b/login/app/sprinkles/core/templates/pages/error/config-errors.html.twig new file mode 100755 index 0000000..314b3b8 --- /dev/null +++ b/login/app/sprinkles/core/templates/pages/error/config-errors.html.twig @@ -0,0 +1,22 @@ +{% extends "pages/abstract/error.html.twig" %} + +{% block page_title %}{{ translate('ERROR.CONFIG.TITLE') }}{% endblock %} + +{% block page_description %}{{ translate('ERROR.CONFIG.DESCRIPTION') }}{% endblock %} + +{% block heading %}<i class="fa fa-warning text-yellow"></i> {{ translate('ERROR.CONFIG.DETAIL') }}{% endblock %} + +{% block content %} + <p>{{ translate('ERROR.CONFIG.RETURN', {url: "#{site.uri.public}"})|raw }}.</p> + + {% autoescape false %} + {% for message in messages %} + <div class="list-group"> + <div class="list-group-item {% if message.success %}list-group-item-success{% else %}list-group-item-danger{% endif %}"> + <h4 class="list-group-item-heading">{{message.title}}</h4> + <p class="list-group-item-text">{{message.message | raw}}</p> + </div> + </div> + {% endfor %} + {% endautoescape %} +{% endblock %} diff --git a/login/app/sprinkles/core/templates/pages/index.html.twig b/login/app/sprinkles/core/templates/pages/index.html.twig new file mode 100755 index 0000000..fea1213 --- /dev/null +++ b/login/app/sprinkles/core/templates/pages/index.html.twig @@ -0,0 +1,185 @@ +{% extends "pages/abstract/default.html.twig" %} + +{% set page_active = "home" %} + +{# Overrides blocks in head of base template #} +{% block page_title %}{{translate("HOME")}}{% endblock %} + +{% block page_description %}{{translate("WELCOME_TO", {'title': site.title})}}{% endblock %} + +{% block body_matter %} + + <!-- Header Carousel --> + <div class="box box-solid"> + <div class="box-body"> + <div id="carousel-example-generic" class="carousel slide" data-ride="carousel"> + <ol class="carousel-indicators"> + <li data-target="#carousel-example-generic" data-slide-to="0" class="active"></li> + <li data-target="#carousel-example-generic" data-slide-to="1" class=""></li> + <li data-target="#carousel-example-generic" data-slide-to="2" class=""></li> + </ol> + <div class="carousel-inner"> + <div class="item active"> + <img src="//placehold.it/1900x1080&text=Slide One" alt="First slide"> + + <div class="carousel-caption"> + First Slide + </div> + </div> + <div class="item"> + <img src="//placehold.it/1900x1080&text=Slide Two" alt="Second slide"> + + <div class="carousel-caption"> + Second Slide + </div> + </div> + <div class="item"> + <img src="//placehold.it/1900x1080&text=Slide Three" alt="Third slide"> + + <div class="carousel-caption"> + Third Slide + </div> + </div> + </div> + <a class="left carousel-control" href="#carousel-example-generic" data-slide="prev"> + <span class="fa fa-angle-left"></span> + </a> + <a class="right carousel-control" href="#carousel-example-generic" data-slide="next"> + <span class="fa fa-angle-right"></span> + </a> + </div> + <!-- /.carousel --> + </div> + <!-- /.box-body --> + </div> + <!-- /.box --> + + <!-- Marketing Icons Section --> + <div class="row"> + <div class="col-lg-12"> + <h1 class="page-header"> + Welcome to AdminLTE + </h1> + </div> + <div class="col-md-4"> + <div class="panel panel-default"> + <div class="panel-heading"> + <h4><i class="fa fa-fw fa-check"></i> Bootstrap v3.2.0</h4> + </div> + <div class="panel-body"> + <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Itaque, optio corporis quae nulla aspernatur in alias at numquam rerum ea excepturi expedita tenetur assumenda voluptatibus eveniet incidunt dicta nostrum quod?</p> + <a href="#" class="btn btn-default">Learn More</a> + </div> + </div> + </div> + <div class="col-md-4"> + <div class="panel panel-default"> + <div class="panel-heading"> + <h4><i class="fa fa-fw fa-gift"></i> Free & Open Source</h4> + </div> + <div class="panel-body"> + <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Itaque, optio corporis quae nulla aspernatur in alias at numquam rerum ea excepturi expedita tenetur assumenda voluptatibus eveniet incidunt dicta nostrum quod?</p> + <a href="#" class="btn btn-default">Learn More</a> + </div> + </div> + </div> + <div class="col-md-4"> + <div class="panel panel-default"> + <div class="panel-heading"> + <h4><i class="fa fa-fw fa-compass"></i> Easy to Use</h4> + </div> + <div class="panel-body"> + <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Itaque, optio corporis quae nulla aspernatur in alias at numquam rerum ea excepturi expedita tenetur assumenda voluptatibus eveniet incidunt dicta nostrum quod?</p> + <a href="#" class="btn btn-default">Learn More</a> + </div> + </div> + </div> + </div> + <!-- /.row --> + + <!-- Portfolio Section --> + <div class="row"> + <div class="col-lg-12"> + <h2 class="page-header">Portfolio Heading</h2> + </div> + <div class="col-md-4 col-sm-6"> + <a href="portfolio-item.html"> + <img class="img-responsive img-portfolio img-hover" src="//placehold.it/700x450" alt=""> + </a> + </div> + <div class="col-md-4 col-sm-6"> + <a href="portfolio-item.html"> + <img class="img-responsive img-portfolio img-hover" src="//placehold.it/700x450" alt=""> + </a> + </div> + <div class="col-md-4 col-sm-6"> + <a href="portfolio-item.html"> + <img class="img-responsive img-portfolio img-hover" src="//placehold.it/700x450" alt=""> + </a> + </div> + <div class="col-md-4 col-sm-6"> + <a href="portfolio-item.html"> + <img class="img-responsive img-portfolio img-hover" src="//placehold.it/700x450" alt=""> + </a> + </div> + <div class="col-md-4 col-sm-6"> + <a href="portfolio-item.html"> + <img class="img-responsive img-portfolio img-hover" src="//placehold.it/700x450" alt=""> + </a> + </div> + <div class="col-md-4 col-sm-6"> + <a href="portfolio-item.html"> + <img class="img-responsive img-portfolio img-hover" src="//placehold.it/700x450" alt=""> + </a> + </div> + </div> + <!-- /.row --> + + <!-- Features Section --> + <div class="row"> + <div class="col-lg-12"> + <h2 class="page-header">Modern Business Features</h2> + </div> + <div class="col-md-6"> + <p>The Modern Business template by Start Bootstrap includes:</p> + <ul> + <li><strong>Bootstrap v3.2.0</strong> + </li> + <li>jQuery v1.11.0</li> + <li>Font Awesome v4.1.0</li> + <li>Unstyled page elements for easy customization</li> + <li>17 HTML pages</li> + </ul> + <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Corporis, omnis doloremque non cum id reprehenderit, quisquam totam aspernatur tempora minima unde aliquid ea culpa sunt. Reiciendis quia dolorum ducimus unde.</p> + </div> + <div class="col-md-6"> + <img class="img-responsive" src="//placehold.it/700x450" alt=""> + </div> + </div> + <!-- /.row --> + + <hr> + + <!-- Call to Action Section --> + <div class="well"> + <div class="row"> + <div class="col-md-8"> + <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Molestias, expedita, saepe, vero rerum deleniti beatae veniam harum neque nemo praesentium cum alias asperiores commodi.</p> + </div> + <div class="col-md-4"> + <a class="btn btn-lg btn-default btn-block" href="#">Call to Action</a> + </div> + </div> + </div> +{% endblock %} + + + +{% block scripts_page %} + <!-- Script to Activate the Carousel --> + <script> + $('.carousel').carousel({ + interval: false // set to false to prevent auto-advance + }) + </script> +{% endblock %} diff --git a/login/app/sprinkles/core/templates/pages/legal.html.twig b/login/app/sprinkles/core/templates/pages/legal.html.twig new file mode 100755 index 0000000..c1eac19 --- /dev/null +++ b/login/app/sprinkles/core/templates/pages/legal.html.twig @@ -0,0 +1,12 @@ +{% extends "pages/abstract/default.html.twig" %} + +{% set page_active = "home" %} + +{# Overrides blocks in head of base template #} +{% block page_title %}{{translate("LEGAL")}}{% endblock %} + +{% block page_description %}{{translate("LEGAL.DESCRIPTION")}}{% endblock %} + +{% block body_matter %} + {% include 'pages/partials/legal.html.twig' %} +{% endblock %} diff --git a/login/app/sprinkles/core/templates/pages/partials/alerts.html.twig b/login/app/sprinkles/core/templates/pages/partials/alerts.html.twig new file mode 100755 index 0000000..a7f9c08 --- /dev/null +++ b/login/app/sprinkles/core/templates/pages/partials/alerts.html.twig @@ -0,0 +1,13 @@ +{# This contains a client-side Handlebars template. + # Note that these are NOT Twig templates, although the syntax is similar. We wrap them in the `verbatim` tag, + # so that Twig will output them directly into the DOM instead of trying to treat them like Twig templates. + #} + +{% verbatim %} +<script id="uf-alert-template" type="text/x-handlebars-template"> + <div class="alert alert-{{type}} alert-dismissible uf-alert"> + <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button> + <i class="icon fa fa-fw {{icon}} pull-left"></i> <div class="uf-alert-message-container">{{message}}</div> + </div> +</script> +{% endverbatim %}
\ No newline at end of file diff --git a/login/app/sprinkles/core/templates/pages/partials/analytics.html.twig b/login/app/sprinkles/core/templates/pages/partials/analytics.html.twig new file mode 100755 index 0000000..3fbeeed --- /dev/null +++ b/login/app/sprinkles/core/templates/pages/partials/analytics.html.twig @@ -0,0 +1,15 @@ +{% if site.analytics.google.code and site.analytics.google.enabled %} + <script> + (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ + (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), + m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) + })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); + + ga('create', '{{site.analytics.google.code}}', 'auto'); + ga('send', 'pageview'); + </script> +{% elseif site.debug.info %} + <script> + console.log("Google Analytics is disabled or code has not been set."); + </script> +{% endif %} diff --git a/login/app/sprinkles/core/templates/pages/partials/config.js.twig b/login/app/sprinkles/core/templates/pages/partials/config.js.twig new file mode 100755 index 0000000..a7a60f0 --- /dev/null +++ b/login/app/sprinkles/core/templates/pages/partials/config.js.twig @@ -0,0 +1,13 @@ +{# Configuration variables needed by client-side code (Javascript). #} +{% autoescape 'js' %} + var site = { + "uri": { + "public": "{{ site.uri.public | raw }}" + }, + "debug": { + "ajax": {{ site.debug.ajax ? 'true' : 'false' }} + }, + "csrf": {{ site.csrf | json_encode(constant('JSON_PRETTY_PRINT')) | raw }}, + "uf_table": {{ site.uf_table | json_encode(constant('JSON_PRETTY_PRINT')) | raw }} + }; +{% endautoescape %} diff --git a/login/app/sprinkles/core/templates/pages/partials/favicons.html.twig b/login/app/sprinkles/core/templates/pages/partials/favicons.html.twig new file mode 100755 index 0000000..11b0c52 --- /dev/null +++ b/login/app/sprinkles/core/templates/pages/partials/favicons.html.twig @@ -0,0 +1,39 @@ +{# Favicons generated with https://realfavicongenerator.net #} + +{# Basic favicon #} +<link rel="shortcut icon" type="image/x-icon" href="{{ assets.url('assets://userfrosting/favicons/favicon.ico') }}"> + +{# PNG versions #} +<link rel="icon" type="image/png" sizes="32x32" href="{{ assets.url('assets://userfrosting/favicons/favicon-32x32.png') }}"> +<link rel="icon" type="image/png" sizes="16x16" href="{{ assets.url('assets://userfrosting/favicons/favicon-16x16.png') }}"> + +{# + IE 10 Metro tile icon (Metro equivalent of apple-touch-icon-precomposed) + (see https://github.com/audreyr/favicon-cheat-sheet) +#} +<meta name="msapplication-TileColor" content="#603cba"> +<meta name="msapplication-TileImage" content="{{ assets.url('assets://userfrosting/favicons/mstile-144x144.png') }}"> + +{# IE 11 Tile for Windows 8.1 Start Screen #} +<meta name="application-name" content="UserFrosting"> +<meta name="msapplication-tooltip" content="A secure, modern user management system for PHP."> +<meta name="msapplication-config" content="{{ assets.url('assets://userfrosting/favicons/ieconfig.xml') }}"> + +{# Apple touch icons #} +<link rel="apple-touch-icon-precomposed" href="{{ assets.url('assets://userfrosting/favicons/apple-touch-icon-precomposed.png') }}"> +<link rel="apple-touch-icon-precomposed" sizes="60x60" href="{{ assets.url('assets://userfrosting/favicons/apple-touch-icon-60x60-precomposed.png') }}"> +<link rel="apple-touch-icon-precomposed" sizes="72x72" href="{{ assets.url('assets://userfrosting/favicons/apple-touch-icon-72x72-precomposed.png') }}"> +<link rel="apple-touch-icon-precomposed" sizes="76x76" href="{{ assets.url('assets://userfrosting/favicons/apple-touch-icon-76x76-precomposed.png') }}"> +<link rel="apple-touch-icon-precomposed" sizes="114x114" href="{{ assets.url('assets://userfrosting/favicons/apple-touch-icon-114x114-precomposed.png') }}"> +<link rel="apple-touch-icon-precomposed" sizes="120x120" href="{{ assets.url('assets://userfrosting/favicons/apple-touch-icon-120x120-precomposed.png') }}"> +<link rel="apple-touch-icon-precomposed" sizes="144x144" href="{{ assets.url('assets://userfrosting/favicons/apple-touch-icon-144x144-precomposed.png') }}"> +<link rel="apple-touch-icon-precomposed" sizes="152x152" href="{{ assets.url('assets://userfrosting/favicons/apple-touch-icon-152x152-precomposed.png') }}"> +<link rel="apple-touch-icon-precomposed" sizes="180x180" href="{{ assets.url('assets://userfrosting/favicons/apple-touch-icon-180x180-precomposed.png') }}"> + +{# Chrome icons and manifest #} +<link rel="icon" type="image/png" sizes="192x192" href="{{ assets.url('assets://userfrosting/favicons/android-chrome-192x192.png') }}"> +<link rel="manifest" href="{{ assets.url('assets://userfrosting/favicons/manifest.json') }}"> +<meta name="theme-color" content="#f3f2e4"> + +{# Safari pinned sites icons #} +<link rel="mask-icon" href="{{ assets.url('assets://userfrosting/favicons/safari-pinned-tab.svg') }}" color="#f39ca1"> diff --git a/login/app/sprinkles/core/templates/pages/partials/footer.html.twig b/login/app/sprinkles/core/templates/pages/partials/footer.html.twig new file mode 100755 index 0000000..e7a93c3 --- /dev/null +++ b/login/app/sprinkles/core/templates/pages/partials/footer.html.twig @@ -0,0 +1,8 @@ +<footer class="main-footer"> + <div class="pull-right hidden-xs"> + {{ translate("BUILT_WITH_UF") | raw }} · + <a href="{{site.uri.public}}/legal">{{ translate("TOS") }}</a> · + <a href="{{site.uri.public}}/privacy">{{ translate("PRIVACY") }}</a> + </div> + © <a href="{{site.uri.public}}">{{site.title}}</a>, {{ "now"|date("Y") }}. <span class="hidden-xs">{{ translate("ADMINLTE_THEME_BY") | raw }}.</span> +</footer>
\ No newline at end of file diff --git a/login/app/sprinkles/core/templates/pages/partials/legal.html.twig b/login/app/sprinkles/core/templates/pages/partials/legal.html.twig new file mode 100755 index 0000000..65fe61c --- /dev/null +++ b/login/app/sprinkles/core/templates/pages/partials/legal.html.twig @@ -0,0 +1,95 @@ +<!-- Generated by http://www.bennadel.com/coldfusion/privacy-policy-generator.htm --> +<h2> + Web Site Terms and Conditions of Use +</h2> + +<h3> + 1. Terms +</h3> + +<p> + By accessing this web site, you are agreeing to be bound by these + web site Terms and Conditions of Use, all applicable laws and regulations, + and agree that you are responsible for compliance with any applicable local + laws. If you do not agree with any of these terms, you are prohibited from + using or accessing this site. The materials contained in this web site are + protected by applicable copyright and trade mark law. +</p> + +<h3> + 2. Use License +</h3> + +<ol type="a"> + <li> + Permission is granted to temporarily download one copy of the materials + (information or software) on {{site.title}}'s web site for personal, + non-commercial transitory viewing only. This is the grant of a license, + not a transfer of title, and under this license you may not: + + <ol type="i"> + <li>modify or copy the materials;</li> + <li>use the materials for any commercial purpose, or for any public display (commercial or non-commercial);</li> + <li>attempt to decompile or reverse engineer any software contained on {{site.title}}'s web site;</li> + <li>remove any copyright or other proprietary notations from the materials; or</li> + <li>transfer the materials to another person or "mirror" the materials on any other server.</li> + </ol> + </li> + <li> + This license shall automatically terminate if you violate any of these restrictions and may be terminated by {{site.title}} at any time. Upon terminating your viewing of these materials or upon the termination of this license, you must destroy any downloaded materials in your possession whether in electronic or printed format. + </li> +</ol> + +<h3> + 3. Disclaimer +</h3> + +<ol type="a"> + <li> + The materials on {{site.title}}'s web site are provided "as is". {{site.title}} makes no warranties, expressed or implied, and hereby disclaims and negates all other warranties, including without limitation, implied warranties or conditions of merchantability, fitness for a particular purpose, or non-infringement of intellectual property or other violation of rights. Further, {{site.title}} does not warrant or make any representations concerning the accuracy, likely results, or reliability of the use of the materials on its Internet web site or otherwise relating to such materials or on any sites linked to this site. + </li> +</ol> + +<h3> + 4. Limitations +</h3> + +<p> + In no event shall {{site.title}} or its suppliers be liable for any damages (including, without limitation, damages for loss of data or profit, or due to business interruption,) arising out of the use or inability to use the materials on {{site.title}}'s Internet site, even if {{site.title}} or a {{site.title}} authorized representative has been notified orally or in writing of the possibility of such damage. Because some jurisdictions do not allow limitations on implied warranties, or limitations of liability for consequential or incidental damages, these limitations may not apply to you. +</p> + +<h3> + 5. Revisions and Errata +</h3> + +<p> + The materials appearing on {{site.title}}'s web site could include technical, typographical, or photographic errors. {{site.title}} does not warrant that any of the materials on its web site are accurate, complete, or current. {{site.title}} may make changes to the materials contained on its web site at any time without notice. {{site.title}} does not, however, make any commitment to update the materials. +</p> + +<h3> + 6. Links +</h3> + +<p> + {{site.title}} has not reviewed all of the sites linked to its Internet web site and is not responsible for the contents of any such linked site. The inclusion of any link does not imply endorsement by {{site.title}} of the site. Use of any such linked web site is at the user's own risk. +</p> + +<h3> + 7. Site Terms of Use Modifications +</h3> + +<p> + {{site.title}} may revise these terms of use for its web site at any time without notice. By using this web site you are agreeing to be bound by the then current version of these Terms and Conditions of Use. +</p> + +<h3> + 8. Governing Law +</h3> + +<p> + Any claim relating to {{site.title}}'s web site shall be governed by the laws of {{site.site_location}} without regard to its conflict of law provisions. +</p> + +<p> + General Terms and Conditions applicable to Use of a Web Site. +</p> diff --git a/login/app/sprinkles/core/templates/pages/partials/page.js.twig b/login/app/sprinkles/core/templates/pages/partials/page.js.twig new file mode 100755 index 0000000..51a1703 --- /dev/null +++ b/login/app/sprinkles/core/templates/pages/partials/page.js.twig @@ -0,0 +1,4 @@ +{# Page variables needed by client-side code (Javascript). #} +{% autoescape 'js' %} + var page = {{ page | json_encode(constant('JSON_PRETTY_PRINT')) | raw }}; +{% endautoescape %} diff --git a/login/app/sprinkles/core/templates/pages/partials/privacy.html.twig b/login/app/sprinkles/core/templates/pages/partials/privacy.html.twig new file mode 100755 index 0000000..6a6cafc --- /dev/null +++ b/login/app/sprinkles/core/templates/pages/partials/privacy.html.twig @@ -0,0 +1,35 @@ +<h2> + Privacy Policy +</h2> + +<p> + Your privacy is very important to us. Accordingly, we have developed this Policy in order for you to understand how we collect, use, communicate and disclose and make use of personal information. The following outlines our privacy policy. +</p> + +<ul> + <li> + Before or at the time of collecting personal information, we will identify the purposes for which information is being collected. + </li> + <li> + We will collect and use of personal information solely with the objective of fulfilling those purposes specified by us and for other compatible purposes, unless we obtain the consent of the individual concerned or as required by law. + </li> + <li> + We will only retain personal information as long as necessary for the fulfillment of those purposes. + </li> + <li> + We will collect personal information by lawful and fair means and, where appropriate, with the knowledge or consent of the individual concerned. + </li> + <li> + Personal data should be relevant to the purposes for which it is to be used, and, to the extent necessary for those purposes, should be accurate, complete, and up-to-date. + </li> + <li> + We will protect personal information by reasonable security safeguards against loss or theft, as well as unauthorized access, disclosure, copying, use or modification. + </li> + <li> + We will make readily available to customers information about our policies and practices relating to the management of personal information. + </li> +</ul> + +<p> + We are committed to conducting our business in accordance with these principles in order to ensure that the confidentiality of personal information is protected and maintained. +</p> diff --git a/login/app/sprinkles/core/templates/pages/privacy.html.twig b/login/app/sprinkles/core/templates/pages/privacy.html.twig new file mode 100755 index 0000000..75db423 --- /dev/null +++ b/login/app/sprinkles/core/templates/pages/privacy.html.twig @@ -0,0 +1,12 @@ +{% extends "pages/abstract/default.html.twig" %} + +{% set page_active = "home" %} + +{# Overrides blocks in head of base template #} +{% block page_title %}{{translate("PRIVACY")}}{% endblock %} + +{% block page_description %}{{translate("PRIVACY.DESCRIPTION")}}{% endblock %} + +{% block body_matter %} + {% include 'pages/partials/privacy.html.twig' %} +{% endblock %} diff --git a/login/app/sprinkles/core/templates/tables/table-paginated.html.twig b/login/app/sprinkles/core/templates/tables/table-paginated.html.twig new file mode 100755 index 0000000..5b94120 --- /dev/null +++ b/login/app/sprinkles/core/templates/tables/table-paginated.html.twig @@ -0,0 +1,59 @@ +{# Base layout for paginated tablesorter tables. + # Requires tablesorter-custom.css for proper styling of pager elements. +#} + +{# Set some default values for the child template. #} +{% set table = { + 'id': "uf-table", + 'sortlist': "[[0, 0]]" + } | merge(table) +%} + +{# + Global search field for the table. By default, only shown in mobile sizes. + To customize this behavior, see core/assets/userfrosting/css/userfrosting.css +#} +{% block table_search %} + <div class="form-group has-feedback uf-table-search js-uf-table-search"> + <input type="search" class="form-control" data-column="all"> + <i class="fa fa-search form-control-icon" aria-hidden="true"></i> + </div> +{% endblock %} +<div class="table overlay-wrapper"> + {% block table %} + {# Define your table skeleton in this block in your child template #} + {% endblock %} + + {% block table_cell_templates %} + {# Define your Handlebars cell templates in this block in your child template #} + {% endblock %} + + {% block table_info %} + <div class="uf-table-info js-uf-table-info" data-message-empty-rows="{{translate('NO_RESULTS')}}"> + </div> + {% endblock %} + + {% block table_pager_controls %} + <div class="pager pager-lg tablesorter-pager js-uf-table-pager" data-output-template="{{translate('PAGINATION.OUTPUT')}}"> + <span class="pager-control first" title="{{translate("PAGINATION.FIRST")}}"><i class="fa fa-angle-double-left"></i></span> + <span class="pager-control prev" title="{{translate("PAGINATION.PREVIOUS")}}"><i class="fa fa-angle-left"></i></span> + <span class="pagedisplay"></span> {# this can be any element, including an input #} + <span class="pager-control next" title="{{translate("PAGINATION.NEXT")}}"><i class="fa fa-angle-right"></i></span> + <span class="pager-control last" title= "{{translate("PAGINATION.LAST")}}"><i class="fa fa-angle-double-right"></i></span> + <br><br> + {{translate("PAGINATION.GOTO")}}: <select class="gotoPage"></select> • {{translate("PAGINATION.SHOW")}}: + <select class="pagesize"> + {% for count in pager.take|default([5, 10, 50, 100]) %} + <option value="{{count}}">{{count}}</option> + {% endfor %} + </select> + </div> + {% endblock %} + {% block table_overlay %} + {% if site.uf_table.use_loading_transition %} + <div class="overlay js-uf-table-overlay hidden"> + <i class="fa fa-refresh fa-spin"></i> + </div> + {% endif %} + {% endblock %} +</div> diff --git a/login/app/sprinkles/core/templates/tables/table-tool-menu.html.twig b/login/app/sprinkles/core/templates/tables/table-tool-menu.html.twig new file mode 100755 index 0000000..0c4dfed --- /dev/null +++ b/login/app/sprinkles/core/templates/tables/table-tool-menu.html.twig @@ -0,0 +1,25 @@ +{# Tool menu for table panels #} + +<div class="box-tools pull-right"> + <div class="btn-group"> + <button type="button" class="btn btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> + <i class="fa fa-bars"></i> <span class="caret"></span> + </button> + <ul class="dropdown-menu box-tool-menu"> + <li> + <a href="#" class="js-uf-table-download"> + <i class="fa fa-table fa-fw"></i> {{ translate("DOWNLOAD.CSV") }} + </a> + </li> + <li role="separator" class="divider"></li> + <li> + <div class="uf-table-cs-title"> + {{translate('TOGGLE_COLUMNS')}}: + </div> + <div class="uf-table-cs-options js-uf-table-cs-options"> + + </div> + </li> + </ul> + </div> +</div> diff --git a/login/app/sprinkles/core/tests/Integration/DatabaseTests.php b/login/app/sprinkles/core/tests/Integration/DatabaseTests.php new file mode 100755 index 0000000..231bb86 --- /dev/null +++ b/login/app/sprinkles/core/tests/Integration/DatabaseTests.php @@ -0,0 +1,1315 @@ +<?php + +namespace UserFrosting\Tests\Integration; + +use Exception; + +use UserFrosting\Tests\TestCase; +use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Database\Capsule\Manager as DB; + +use UserFrosting\Sprinkle\Core\Database\Models\Model; + +use Illuminate\Database\Eloquent\Relations\Relation; + +class DatabaseTests extends TestCase +{ + protected $schemaName = 'test_integration'; + + /** + * Setup the database schema. + * + * @return void + */ + public function setUp() + { + // Boot parent TestCase, which will set up the database and connections for us. + parent::setUp(); + + // Boot database + $this->ci->db; + + $this->createSchema(); + } + + protected function createSchema() + { + $this->schema($this->schemaName)->create('users', function ($table) { + $table->increments('id'); + $table->string('name')->nullable(); + }); + + // Users have multiple email addresses + $this->schema($this->schemaName)->create('emails', function ($table) { + $table->increments('id'); + $table->integer('user_id'); + $table->string('label'); + $table->string('email'); + }); + + // Users have multiple phones (polymorphic - other entities can have phones as well) + $this->schema($this->schemaName)->create('phones', function ($table) { + $table->increments('id'); + $table->string('label'); + $table->string('number', 20); + $table->morphs('phoneable'); + }); + + // Users have multiple roles... (m:m) + $this->schema($this->schemaName)->create('role_users', function ($table) { + $table->integer('user_id')->unsigned(); + $table->integer('role_id')->unsigned(); + }); + + $this->schema($this->schemaName)->create('roles', function ($table) { + $table->increments('id'); + $table->string('slug'); + }); + + // And Roles have multiple permissions... (m:m) + $this->schema($this->schemaName)->create('permission_roles', function ($table) { + $table->integer('permission_id')->unsigned(); + $table->integer('role_id')->unsigned(); + }); + + $this->schema($this->schemaName)->create('permissions', function($table) { + $table->increments('id'); + $table->string('slug'); + }); + + // A user can be assigned to a specific task at a specific location + $this->schema($this->schemaName)->create('tasks', function($table) { + $table->increments('id'); + $table->string('name'); + }); + + $this->schema($this->schemaName)->create('locations', function($table) { + $table->increments('id'); + $table->string('name'); + }); + + $this->schema($this->schemaName)->create('assignments', function($table) { + $table->integer('task_id')->unsigned(); + $table->integer('location_id')->unsigned(); + $table->morphs('assignable'); + }); + + $this->schema($this->schemaName)->create('jobs', function($table) { + $table->integer('user_id')->unsigned(); + $table->integer('location_id')->unsigned(); + $table->integer('role_id')->unsigned(); + $table->string('title'); + }); + } + + /** + * Tear down the database schema. + * + * @return void + */ + public function tearDown() + { + $this->schema($this->schemaName)->drop('users'); + $this->schema($this->schemaName)->drop('emails'); + $this->schema($this->schemaName)->drop('phones'); + $this->schema($this->schemaName)->drop('role_users'); + $this->schema($this->schemaName)->drop('roles'); + $this->schema($this->schemaName)->drop('permission_roles'); + $this->schema($this->schemaName)->drop('permissions'); + $this->schema($this->schemaName)->drop('tasks'); + $this->schema($this->schemaName)->drop('locations'); + $this->schema($this->schemaName)->drop('assignments'); + + Relation::morphMap([], false); + } + + /** + * Tests... + */ + public function testOneToManyRelationship() + { + $user = EloquentTestUser::create(['name' => 'David']); + $user->emails()->create([ + 'label' => 'primary', + 'email' => 'david@owlfancy.com' + ]); + $user->emails()->create([ + 'label' => 'work', + 'email' => 'david@attenboroughsreef.com' + ]); + + $emails = $user->emails; + + $this->assertInstanceOf('Illuminate\Database\Eloquent\Collection', $emails); + $this->assertEquals(2, $emails->count()); + $this->assertInstanceOf('UserFrosting\Tests\Integration\EloquentTestEmail', $emails[0]); + $this->assertInstanceOf('UserFrosting\Tests\Integration\EloquentTestEmail', $emails[1]); + } + + /** + * Tests our custom HasManySyncable class. + */ + public function testSyncOneToMany() + { + $user = EloquentTestUser::create(['name' => 'David']); + // Set up original emails + $user->emails()->create([ + 'id' => 1, + 'label' => 'primary', + 'email' => 'david@owlfancy.com' + ]); + $user->emails()->create([ + 'id' => 2, + 'label' => 'work', + 'email' => 'david@attenboroughsreef.com' + ]); + + // Delete `work`, update `primary`, and add `gmail` + $user->emails()->sync([ + [ + 'id' => 1, + 'email' => 'david@aol.com' + ], + [ + 'label' => 'gmail', + 'email' => 'davidattenborough@gmail.com' + ] + ]); + + $emails = $user->emails->toArray(); + + $this->assertEquals([ + [ + 'id' => 1, + 'user_id'=> 1, + 'label' => 'primary', + 'email' => 'david@aol.com' + ], + [ + 'id' => 3, + 'user_id' => 1, + 'label' => 'gmail', + 'email' => 'davidattenborough@gmail.com' + ] + ], $emails); + } + + /** + * Tests our custom MorphManySyncable class. + */ + public function testSyncMorphMany() + { + $user = EloquentTestUser::create(['name' => 'David']); + // Set up original phones + $user->phones()->create([ + 'id' => 1, + 'label' => 'primary', + 'number' => '5555551212' + ]); + $user->phones()->create([ + 'id' => 2, + 'label' => 'work', + 'number' => '2223334444' + ]); + + // Delete `work`, update `primary`, and add `fax` + $user->phones()->sync([ + [ + 'id' => 1, + 'number' => '8883332222' + ], + [ + 'label' => 'fax', + 'number' => '5550005555' + ] + ]); + + $phones = $user->phones->toArray(); + + $this->assertEquals([ + [ + 'id' => 1, + 'phoneable_id'=> 1, + 'phoneable_type' => 'UserFrosting\Tests\Integration\EloquentTestUser', + 'label' => 'primary', + 'number' => '8883332222' + ], + [ + 'id' => 3, + 'phoneable_id'=> 1, + 'phoneable_type' => 'UserFrosting\Tests\Integration\EloquentTestUser', + 'label' => 'fax', + 'number' => '5550005555' + ] + ], $phones); + } + + public function testBelongsToManyUnique() + { + $user = EloquentTestUser::create(['name' => 'David']); + + $this->generateLocations(); + $this->generateRoles(); + $this->generateJobs(); + + $expectedRoles = [ + [ + 'id' => 2, + 'slug' => 'soldier', + 'pivot' => [ + 'user_id' => 1, + 'role_id' => 2 + ] + ], + [ + 'id' => 3, + 'slug' => 'egg-layer', + 'pivot' => [ + 'user_id' => 1, + 'role_id' => 3 + ] + ] + ]; + + $roles = $user->jobRoles; + $this->assertEquals($expectedRoles, $roles->toArray()); + + // Test eager loading + $users = EloquentTestUser::with('jobRoles')->get(); + $this->assertEquals($expectedRoles, $users->toArray()[0]['job_roles']); + } + + public function testMorphsToManyUnique() + { + $user = EloquentTestUser::create(['name' => 'David']); + $user2 = EloquentTestUser::create(['name' => 'Alex']); + + $this->generateTasks(); + $this->generateLocations(); + $this->generateAssignments(); + + $expectedTasks = [ + [ + 'id' => 2, + 'name' => 'Chopping', + 'pivot' => [ + 'assignable_id' => 1, + 'task_id' => 2 + ] + ], + [ + 'id' => 3, + 'name' => 'Baleing', + 'pivot' => [ + 'assignable_id' => 1, + 'task_id' => 3 + ] + ] + ]; + + $tasks = $user->assignmentTasks; + $this->assertEquals($expectedTasks, $tasks->toArray()); + + // Test eager loading + $users = EloquentTestUser::with('assignmentTasks')->get(); + $this->assertEquals($expectedTasks, $users->toArray()[0]['assignment_tasks']); + } + + public function testMorphsToManyUniqueWithTertiary() + { + $user = EloquentTestUser::create(['name' => 'David']); + $user2 = EloquentTestUser::create(['name' => 'Alex']); + + $this->generateTasks(); + $this->generateLocations(); + $this->generateAssignments(); + + $expectedTasks = [ + [ + 'id' => 2, + 'name' => 'Chopping', + 'pivot' => [ + 'assignable_id' => 1, + 'task_id' => 2 + ], + 'locations' => [ + [ + 'id' => 1, + 'name' => 'Hatchery', + 'pivot' => [ + 'location_id' => 1, + 'task_id' => 2 + ], + ], + [ + 'id' => 2, + 'name' => 'Nexus', + 'pivot' => [ + 'location_id' => 2, + 'task_id' => 2 + ], + ] + ] + ], + [ + 'id' => 3, + 'name' => 'Baleing', + 'pivot' => [ + 'assignable_id' => 1, + 'task_id' => 3 + ], + 'locations' => [ + [ + 'id' => 2, + 'name' => 'Nexus', + 'pivot' => [ + 'location_id' => 2, + 'task_id' => 3 + ], + ] + ] + ] + ]; + + $tasks = $user->tasks; + $this->assertEquals($expectedTasks, $tasks->toArray()); + + // Test eager loading + $users = EloquentTestUser::with('tasks')->get(); + $this->assertEquals($expectedTasks, $users->toArray()[0]['tasks']); + } + + public function testBelongsToManyUniqueWithTertiary() + { + $user = EloquentTestUser::create(['name' => 'David']); + + $this->generateLocations(); + $this->generateRoles(); + $this->generateJobs(); + + $expectedJobs = [ + [ + 'id' => 2, + 'slug' => 'soldier', + 'pivot' => [ + 'user_id' => 1, + 'role_id' => 2 + ], + 'locations' => [ + [ + 'id' => 1, + 'name' => 'Hatchery', + 'pivot' => [ + 'title' => 'Grunt', + 'location_id' => 1, + 'role_id' => 2 + ] + ], + [ + 'id' => 2, + 'name' => 'Nexus', + 'pivot' => [ + 'title' => 'Sergeant', + 'location_id' => 2, + 'role_id' => 2 + ] + ] + ] + ], + [ + 'id' => 3, + 'slug' => 'egg-layer', + 'pivot' => [ + 'user_id' => 1, + 'role_id' => 3 + ], + 'locations' => [ + [ + 'id' => 2, + 'name' => 'Nexus', + 'pivot' => [ + 'title' => 'Queen', + 'location_id' => 2, + 'role_id' => 3 + ] + ] + ] + ] + ]; + + $jobs = $user->jobs()->withPivot('title')->get(); + $this->assertEquals($expectedJobs, $jobs->toArray()); + + // Test eager loading + $users = EloquentTestUser::with(['jobs' => function ($relation) { + return $relation->withPivot('title'); + }])->get(); + + $this->assertEquals($expectedJobs, $users->toArray()[0]['jobs']); + } + + public function testBelongsToManyUniqueWithTertiaryEagerLoad() + { + $user1 = EloquentTestUser::create(['name' => 'David']); + $user2 = EloquentTestUser::create(['name' => 'Alex']); + + $this->generateLocations(); + $this->generateRoles(); + $this->generateJobs(); + + $users = EloquentTestUser::with('jobs')->get(); + + $this->assertEquals([ + [ + 'id' => 1, + 'name' => 'David', + 'jobs' => [ + [ + 'id' => 2, + 'slug' => 'soldier', + 'pivot' => [ + 'user_id' => 1, + 'role_id' => 2 + ], + 'locations' => [ + [ + 'id' => 1, + 'name' => 'Hatchery', + 'pivot' => [ + 'location_id' => 1, + 'role_id' => 2 + ] + ], + [ + 'id' => 2, + 'name' => 'Nexus', + 'pivot' => [ + 'location_id' => 2, + 'role_id' => 2 + ] + ] + ] + ], + [ + 'id' => 3, + 'slug' => 'egg-layer', + 'pivot' => [ + 'user_id' => 1, + 'role_id' => 3 + ], + 'locations' => [ + [ + 'id' => 2, + 'name' => 'Nexus', + 'pivot' => [ + 'location_id' => 2, + 'role_id' => 3 + ] + ] + ] + ] + ] + ], + [ + 'id' => 2, + 'name' => 'Alex', + 'jobs' => [ + [ + 'id' => 3, + 'slug' => 'egg-layer', + 'pivot' => [ + 'user_id' => 2, + 'role_id' => 3 + ], + 'locations' => [ + [ + 'id' => 1, + 'name' => 'Hatchery', + 'pivot' => [ + 'location_id' => 1, + 'role_id' => 3 + ] + ], + ] + ] + ] + ] + ], $users->toArray()); + } + + /** + * Test the ability of a BelongsToManyThrough relationship to retrieve structured data on a single model or set of models. + */ + public function testBelongsToManyThrough() + { + $this->generateRolesWithPermissions(); + + $user = EloquentTestUser::create(['name' => 'David']); + + $user->roles()->attach([1,2]); + + // Test retrieval of via models as well + $this->assertEquals([ + [ + 'id' => 1, + 'slug' => 'uri_harvest', + 'pivot' => [ + 'user_id' => 1, + 'permission_id' => 1 + ] + ], + [ + 'id' => 2, + 'slug' => 'uri_spit_acid', + 'pivot' => [ + 'user_id' => 1, + 'permission_id' => 2 + ] + ], + [ + 'id' => 3, + 'slug' => 'uri_slash', + 'pivot' => [ + 'user_id' => 1, + 'permission_id' => 3 + ] + ] + ], $user->permissions->toArray()); + + // Test counting + $this->assertEquals(3, $user->permissions()->count()); + + $user2 = EloquentTestUser::create(['name' => 'Alex']); + $user2->roles()->attach([2,3]); + + // Test eager load + $users = EloquentTestUser::with('permissions')->get(); + $usersWithPermissions = $users->toArray(); + + $this->assertEquals([ + [ + 'id' => 2, + 'slug' => 'uri_spit_acid', + 'pivot' => [ + 'user_id' => 2, + 'permission_id' => 2 + ] + ], + [ + 'id' => 3, + 'slug' => 'uri_slash', + 'pivot' => [ + 'user_id' => 2, + 'permission_id' => 3 + ] + ], + [ + 'id' => 4, + 'slug' => 'uri_royal_jelly', + 'pivot' => [ + 'user_id' => 2, + 'permission_id' => 4 + ] + ] + ],$usersWithPermissions[1]['permissions']); + + // Test counting related models (withCount) + $users = EloquentTestUser::withCount('permissions')->get(); + $this->assertEquals(3, $users[0]->permissions_count); + $this->assertEquals(3, $users[1]->permissions_count); + + $this->assertInstanceOf(EloquentTestPermission::class, $users[0]->permissions[0]); + $this->assertInstanceOf(EloquentTestPermission::class, $users[0]->permissions[1]); + $this->assertInstanceOf(EloquentTestPermission::class, $users[0]->permissions[2]); + } + + /** + * Test the ability of a BelongsToManyThrough relationship to retrieve and count paginated queries. + */ + public function testBelongsToManyThroughPaginated() + { + $this->generateRolesWithPermissions(); + + $user = EloquentTestUser::create(['name' => 'David']); + + $user->roles()->attach([1,2]); + + $paginatedPermissions = $user->permissions()->take(2)->offset(1); + + $this->assertEquals([ + [ + 'id' => 2, + 'slug' => 'uri_spit_acid', + 'pivot' => [ + 'user_id' => 1, + 'permission_id' => 2 + ] + ], + [ + 'id' => 3, + 'slug' => 'uri_slash', + 'pivot' => [ + 'user_id' => 1, + 'permission_id' => 3 + ] + ] + ], $paginatedPermissions->get()->toArray()); + + $this->assertEquals(2, $paginatedPermissions->count()); + } + + /** + * Test the ability of a BelongsToManyThrough relationship to retrieve structured data on a single model or set of models, + * eager loading the "via" models at the same time. + */ + public function testBelongsToManyThroughWithVia() + { + $this->generateRolesWithPermissions(); + + $user = EloquentTestUser::create(['name' => 'David']); + + $user->roles()->attach([1,2]); + + // Test retrieval of via models as well + $this->assertBelongsToManyThroughForDavid($user->permissions()->withVia('roles_via')->get()->toArray()); + + $user2 = EloquentTestUser::create(['name' => 'Alex']); + $user2->roles()->attach([2,3]); + + // Test eager loading + $users = EloquentTestUser::with(['permissions' => function ($query) { + return $query->withVia('roles_via'); + }])->get(); + + $this->assertInstanceOf(EloquentTestPermission::class, $users[0]->permissions[0]); + $this->assertInstanceOf(EloquentTestRole::class, $users[0]->permissions[0]->roles_via[0]); + + $usersWithPermissions = $users->toArray(); + + $this->assertBelongsToManyThroughForDavid($usersWithPermissions[0]['permissions']); + $this->assertBelongsToManyThroughForAlex($usersWithPermissions[1]['permissions']); + } + + public function testQueryExclude() + { + $this->generateRoles(); + $this->generateJobs(); + $job = EloquentTestJob::exclude('location_id', 'title')->first(); + + $this->assertEquals([ + 'role_id' => 2, + 'user_id' => 1 + ], $job->toArray()); + } + + + public function testQueryExcludeOnJoinedTable() + { + $this->generateRolesWithPermissions(); + + $user = EloquentTestUser::create(['name' => 'David']); + + $user->roles()->attach([1,2]); + + $users = EloquentTestUser::with(['permissions' => function ($query) { + $query->exclude('slug'); + }])->get(); + + $this->assertEquals([ + [ + 'id' => 1, + 'name' => 'David', + 'permissions' => [ + [ + 'id' => 1, + 'pivot' => [ + 'user_id' => 1, + 'permission_id' => 1 + ] + ], + [ + 'id' => 2, + 'pivot' => [ + 'user_id' => 1, + 'permission_id' => 2 + ] + ], + [ + 'id' => 3, + 'pivot' => [ + 'user_id' => 1, + 'permission_id' => 3 + ] + ] + ] + ] + ], $users->toArray()); + } + + public function testQueryExcludeUseQualifiedNamesOnJoinedTable() + { + $this->generateRolesWithPermissions(); + + $user = EloquentTestUser::create(['name' => 'David']); + + $user->roles()->attach([1,2]); + + $users = EloquentTestUser::with(['roles' => function ($query) { + $query->addSelect('roles.*', 'jobs.*')->leftJoin('jobs', 'jobs.role_id', '=', 'roles.id') + ->exclude('slug', 'jobs.user_id', 'jobs.location_id', 'jobs.role_id'); + }])->get(); + + $this->assertEquals([ + [ + 'id' => 1, + 'name' => 'David', + 'roles' => [ + [ + 'id' => 1, + 'title' => null, + 'pivot' => [ + 'user_id' => 1, + 'role_id' => 1 + ] + ], + [ + 'id' => 2, + 'title' => null, + 'pivot' => [ + 'user_id' => 1, + 'role_id' => 2 + ] + ] + ] + ] + ], $users->toArray()); + } + + public function testQueryExcludeWildcard() + { + $this->generateRoles(); + $this->generateJobs(); + $job = EloquentTestJob::select('*')->addSelect('user_id')->exclude('*')->first(); + + $this->assertEquals([ + 'user_id' => 1 + ], $job->toArray()); + + $job = EloquentTestJob::select('jobs.*')->addSelect('user_id')->exclude('*')->first(); + + $this->assertEquals([ + 'user_id' => 1 + ], $job->toArray()); + + $job = EloquentTestJob::select('*')->addSelect('user_id')->exclude('jobs.*')->first(); + + $this->assertEquals([ + 'user_id' => 1 + ], $job->toArray()); + } + + /** + * Helpers... + */ + + /** + * Get a database connection instance. + * + * @return \Illuminate\Database\Connection + */ + protected function connection($connection = 'test_integration') + { + return Model::getConnectionResolver()->connection($connection); + } + + /** + * Get a schema builder instance. + * + * @return \Illuminate\Database\Schema\Builder + */ + protected function schema($connection = 'test_integration') + { + return $this->connection($connection)->getSchemaBuilder(); + } + + protected function generateRoles() + { + return [ + EloquentTestRole::create([ + 'id' => 1, + 'slug' => 'forager' + ]), + + EloquentTestRole::create([ + 'id' => 2, + 'slug' => 'soldier' + ]), + + EloquentTestRole::create([ + 'id' => 3, + 'slug' => 'egg-layer' + ]) + ]; + } + + protected function generatePermissions() + { + return [ + EloquentTestPermission::create([ + 'id' => 1, + 'slug' => 'uri_harvest' + ]), + + EloquentTestPermission::create([ + 'id' => 2, + 'slug' => 'uri_spit_acid' + ]), + + EloquentTestPermission::create([ + 'id' => 3, + 'slug' => 'uri_slash' + ]), + + EloquentTestPermission::create([ + 'id' => 4, + 'slug' => 'uri_royal_jelly' + ]) + ]; + } + + protected function generateRolesWithPermissions() + { + $roles = $this->generateRoles(); + + $this->generatePermissions(); + + $roles[0]->permissions()->attach([1,2]); + // We purposefully want a permission that belongs to more than one role + $roles[1]->permissions()->attach([2,3]); + $roles[2]->permissions()->attach([2,4]); + + return $roles; + } + + protected function generateJobs() + { + + /** + * Sample data + + | user_id | role_id | location_id | + |---------|---------|-------------| + | 1 | 2 | 1 | + | 1 | 2 | 2 | + | 1 | 3 | 2 | + | 2 | 3 | 1 | + */ + + return [ + EloquentTestJob::create([ + 'role_id' => 2, + 'location_id' => 1, + 'user_id' => 1, + 'title' => 'Grunt' + ]), + EloquentTestJob::create([ + 'role_id' => 2, + 'location_id' => 2, + 'user_id' => 1, + 'title' => 'Sergeant' + ]), + EloquentTestJob::create([ + 'role_id' => 3, + 'location_id' => 2, + 'user_id' => 1, + 'title' => 'Queen' + ]), + EloquentTestJob::create([ + 'role_id' => 3, + 'location_id' => 1, + 'user_id' => 2, + 'title' => 'Demi-queen' + ]) + ]; + } + + protected function generateLocations() + { + return [ + EloquentTestLocation::create([ + 'id' => 1, + 'name' => 'Hatchery' + ]), + + EloquentTestLocation::create([ + 'id' => 2, + 'name' => 'Nexus' + ]) + ]; + } + + protected function generateTasks() + { + return [ + EloquentTestTask::create([ + 'id' => 1, + 'name' => 'Digging' + ]), + + EloquentTestTask::create([ + 'id' => 2, + 'name' => 'Chopping' + ]), + + EloquentTestTask::create([ + 'id' => 3, + 'name' => 'Baleing' + ]) + ]; + } + + protected function generateAssignments() + { + return [ + EloquentTestAssignment::create([ + 'task_id' => 2, + 'location_id' => 1, + 'assignable_id' => 1, + 'assignable_type' => 'UserFrosting\Tests\Integration\EloquentTestUser' + ]), + EloquentTestAssignment::create([ + 'task_id' => 2, + 'location_id' => 2, + 'assignable_id' => 1, + 'assignable_type' => 'UserFrosting\Tests\Integration\EloquentTestUser' + ]), + EloquentTestAssignment::create([ + 'task_id' => 3, + 'location_id' => 2, + 'assignable_id' => 1, + 'assignable_type' => 'UserFrosting\Tests\Integration\EloquentTestUser' + ]), + EloquentTestAssignment::create([ + 'task_id' => 3, + 'location_id' => 3, + 'assignable_id' => 1, + 'assignable_type' => 'UserFrosting\Tests\Integration\EloquentTestNonExistant' + ]), + EloquentTestAssignment::create([ + 'task_id' => 3, + 'location_id' => 1, + 'assignable_id' => 2, + 'assignable_type' => 'UserFrosting\Tests\Integration\EloquentTestUser' + ]) + ]; + } + + protected function assertBelongsToManyThroughForDavid($permissions) + { + // User should have effective permissions uri_harvest, uri_spit_acid, and uri_slash. + // We also check that the 'roles_via' relationship is properly set. + $this->assertEquals('uri_harvest', $permissions[0]['slug']); + $this->assertEquals([ + [ + 'id' => 1, + 'slug' => 'forager', + 'pivot' => [ + 'permission_id' => 1, + 'role_id' => 1 + ] + ] + ], $permissions[0]['roles_via']); + $this->assertEquals('uri_spit_acid', $permissions[1]['slug']); + $this->assertEquals([ + [ + 'id' => 1, + 'slug' => 'forager', + 'pivot' => [ + 'permission_id' => 2, + 'role_id' => 1 + ] + ], + [ + 'id' => 2, + 'slug' => 'soldier', + 'pivot' => [ + 'permission_id' => 2, + 'role_id' => 2 + ] + ] + ], $permissions[1]['roles_via']); + $this->assertEquals('uri_slash', $permissions[2]['slug']); + $this->assertEquals([ + [ + 'id' => 2, + 'slug' => 'soldier', + 'pivot' => [ + 'permission_id' => 3, + 'role_id' => 2 + ] + ] + ], $permissions[2]['roles_via']); + } + + protected function assertBelongsToManyThroughForAlex($permissions) + { + // User should have effective permissions uri_spit_acid, uri_slash, and uri_royal_jelly. + // We also check that the 'roles_via' relationship is properly set. + $this->assertEquals('uri_spit_acid', $permissions[0]['slug']); + $this->assertEquals([ + [ + 'id' => 2, + 'slug' => 'soldier', + 'pivot' => [ + 'permission_id' => 2, + 'role_id' => 2 + ] + ], + [ + 'id' => 3, + 'slug' => 'egg-layer', + 'pivot' => [ + 'permission_id' => 2, + 'role_id' => 3 + ] + ] + ], $permissions[0]['roles_via']); + $this->assertEquals('uri_slash', $permissions[1]['slug']); + $this->assertEquals([ + [ + 'id' => 2, + 'slug' => 'soldier', + 'pivot' => [ + 'permission_id' => 3, + 'role_id' => 2 + ] + ] + ], $permissions[1]['roles_via']); + $this->assertEquals('uri_royal_jelly', $permissions[2]['slug']); + $this->assertEquals([ + [ + 'id' => 3, + 'slug' => 'egg-layer', + 'pivot' => [ + 'permission_id' => 4, + 'role_id' => 3 + ] + ] + ], $permissions[2]['roles_via']); + } +} + +/** + * Eloquent Models... + */ +class EloquentTestModel extends Model +{ + protected $connection = 'test_integration'; +} + +class EloquentTestUser extends EloquentTestModel +{ + protected $table = 'users'; + protected $guarded = []; + + public function emails() + { + return $this->hasMany('UserFrosting\Tests\Integration\EloquentTestEmail', 'user_id'); + } + + public function phones() + { + return $this->morphMany('UserFrosting\Tests\Integration\EloquentTestPhone', 'phoneable'); + } + + /** + * Get all roles to which this user belongs. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + public function roles() + { + return $this->belongsToMany('UserFrosting\Tests\Integration\EloquentTestRole', 'role_users', 'user_id', 'role_id'); + } + + /** + * Get all of the permissions this user has, via its roles. + * + * @return \UserFrosting\Sprinkle\Core\Database\Relations\BelongsToManyThrough + */ + public function permissions() + { + return $this->belongsToManyThrough( + 'UserFrosting\Tests\Integration\EloquentTestPermission', + 'UserFrosting\Tests\Integration\EloquentTestRole', + 'role_users', + 'user_id', + 'role_id', + 'permission_roles', + 'role_id', + 'permission_id' + ); + } + + /** + * Get all of the user's unique tasks. + */ + public function assignmentTasks() + { + $relation = $this->morphToManyUnique( + 'UserFrosting\Tests\Integration\EloquentTestTask', + 'assignable', + 'assignments', + null, + 'task_id' // Need to explicitly set this, since it doesn't match our related model name + ); + + return $relation; + } + + /** + * Get all of the user's unique tasks along with the task locations. + */ + public function tasks() + { + $relation = $this->morphToManyUnique( + 'UserFrosting\Tests\Integration\EloquentTestTask', + 'assignable', + 'assignments', + null, + 'task_id' // Need to explicitly set this, since it doesn't match our related model name + )->withTertiary(EloquentTestLocation::class, null, 'location_id'); + + return $relation; + } + + /** + * Get all of the user's unique roles based on their jobs. + */ + public function jobRoles() + { + $relation = $this->belongsToManyUnique( + 'UserFrosting\Tests\Integration\EloquentTestRole', + 'jobs', + 'user_id', + 'role_id' + ); + + return $relation; + } + + /** + * Get all of the user's unique roles based on their jobs as a tertiary relationship. + */ + public function jobs() + { + $relation = $this->belongsToManyUnique( + EloquentTestRole::class, + 'jobs', + 'user_id', + 'role_id' + )->withTertiary(EloquentTestLocation::class, null, 'location_id'); + + return $relation; + } +} + +class EloquentTestEmail extends EloquentTestModel +{ + protected $table = 'emails'; + protected $guarded = []; + + public function user() + { + return $this->belongsTo('UserFrosting\Tests\Integration\EloquentTestUser', 'user_id'); + } +} + +class EloquentTestPhone extends EloquentTestModel +{ + protected $table = 'phones'; + protected $guarded = []; + + public function phoneable() + { + return $this->morphTo(); + } +} + +class EloquentTestRole extends EloquentTestModel +{ + protected $table = 'roles'; + protected $guarded = []; + + /** + * Get a list of permissions assigned to this role. + */ + public function permissions() + { + return $this->belongsToMany('UserFrosting\Tests\Integration\EloquentTestPermission', 'permission_roles', 'role_id', 'permission_id'); + } +} + +class EloquentTestPermission extends EloquentTestModel +{ + protected $table = 'permissions'; + protected $guarded = []; + + /** + * Get a list of roles that have this permission. + */ + public function roles() + { + return $this->belongsToMany('UserFrosting\Tests\Integration\EloquentTestRole', 'permission_roles', 'permission_id', 'role_id'); + } +} + +class EloquentTestTask extends EloquentTestModel +{ + protected $table = 'tasks'; + protected $guarded = []; + + public function locations() + { + return $this->belongsToMany( + 'UserFrosting\Tests\Integration\EloquentTestLocation', + 'assignments', + 'task_id', + 'location_id' + ); + } +} + +class EloquentTestLocation extends EloquentTestModel +{ + protected $table = 'locations'; + protected $guarded = []; +} + +class EloquentTestAssignment extends EloquentTestModel +{ + protected $table = 'assignments'; + protected $guarded = []; + + /** + * Get all of the users that are assigned to this assignment. + */ + public function users() + { + return $this->morphedByMany('UserFrosting\Tests\Integration\EloquentTestUser', 'assignable'); + } +} + +class EloquentTestJob extends EloquentTestModel +{ + protected $table = 'jobs'; + protected $guarded = []; + + /** + * Get the role for this job. + */ + public function role() + { + return $this->belongsTo('UserFrosting\Tests\Integration\EloquentTestRole', 'role_id'); + } +} diff --git a/login/app/sprinkles/core/tests/Unit/BelongsToManyThroughTest.php b/login/app/sprinkles/core/tests/Unit/BelongsToManyThroughTest.php new file mode 100755 index 0000000..d4fd4f8 --- /dev/null +++ b/login/app/sprinkles/core/tests/Unit/BelongsToManyThroughTest.php @@ -0,0 +1,103 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Tests\Unit; + +use Illuminate\Database\Capsule\Manager as DB; +use Illuminate\Database\Eloquent\Builder as EloquentBuilder; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Mockery as m; +use ReflectionClass; +use UserFrosting\Tests\TestCase; +use UserFrosting\Tests\DatabaseTransactions; +use UserFrosting\Sprinkle\Core\Database\Builder as QueryBuilder; +use UserFrosting\Sprinkle\Core\Database\Models\Model; +use UserFrosting\Sprinkle\Core\Database\Relations\BelongsToManyThrough; + +/** + * Tests the BelongsToManyThrough relation. + * + * @extends TestCase + */ +class BelongsToManyThroughTest extends TestCase +{ + public function tearDown() + { + m::close(); + } + + function testPaginatedQuery() + { + // Creates a real BelongsToManyThrough object + $relation = $this->getRelation(); + + // We need to define a mock base query, because Eloquent\Builder will pass through many calls + // to this underlying Query\Builder object. + $baseQuery = m::mock(QueryBuilder::class); + $builder = m::mock(EloquentBuilder::class, [$baseQuery])->makePartial(); + + $related = $relation->getRelated(); + $related->shouldReceive('getQualifiedKeyName')->once()->andReturn('users.id'); + + $builder->shouldReceive('withGlobalScope')->once()->andReturnSelf(); + + $builder->shouldReceive('limit')->once()->with(2)->andReturnSelf(); + $builder->shouldReceive('offset')->once()->with(1)->andReturnSelf(); + + // Mock the collection generated by the constrained query + $collection = m::mock('Illuminate\Database\Eloquent\Collection'); + $collection->shouldReceive('pluck')->once()->with('id')->andReturn($collection); + $collection->shouldReceive('toArray')->once()->andReturn([1,2]); + $builder->shouldReceive('get')->once()->andReturn($collection); + + // Test the final modification to the original unpaginated query + $builder->shouldReceive('whereIn')->once()->with('users.id', [1,2])->andReturnSelf(); + + $paginatedQuery = $relation->getPaginatedQuery($builder, 2, 1); + } + + /** + * Set up and simulate base expectations for arguments to relationship. + */ + protected function getRelation() + { + // We simulate a BelongsToManyThrough relationship that gets all related users for a specified permission(s). + $builder = m::mock(EloquentBuilder::class); + $related = m::mock('Illuminate\Database\Eloquent\Model'); + $related->shouldReceive('getKey')->andReturn(1); + $related->shouldReceive('getTable')->andReturn('users'); + $related->shouldReceive('getKeyName')->andReturn('id'); + // Tie the mocked builder to the mocked related model + $builder->shouldReceive('getModel')->andReturn($related); + + // Mock the intermediate role->permission BelongsToMany relation + $intermediateRelationship = m::mock(BelongsToMany::class); + $intermediateRelationship->shouldReceive('getTable')->once()->andReturn('permission_roles'); + $intermediateRelationship->shouldReceive('getQualifiedRelatedKeyName')->once()->andReturn('permission_roles.role_id'); + // Crazy pivot query stuff + $newPivot = m::mock('\Illuminate\Database\Eloquent\Relations\Pivot'); + $newPivot->shouldReceive('getForeignKey')->andReturn('permission_id'); + $intermediateRelationship->shouldReceive('newExistingPivot')->andReturn($newPivot); + + // Expectations for joining the main relation - users to roles + $builder->shouldReceive('join')->once()->with('role_users', 'users.id', '=', 'role_users.user_id'); + + // Expectations for joining the intermediate relation - roles to permissions + $builder->shouldReceive('join')->once()->with('permission_roles', 'permission_roles.role_id', '=', 'role_users.role_id'); + $builder->shouldReceive('where')->once()->with('permission_id', '=', 1); + + // Now we set up the relationship with the related model. + return new BelongsToManyThrough( + $builder, $related, $intermediateRelationship, 'role_users', 'role_id', 'user_id', 'relation_name' + ); + } +} + +class EloquentBelongsToManyModelStub extends Model +{ + protected $guarded = []; +} diff --git a/login/app/sprinkles/core/tests/Unit/DatabaseSyncableTest.php b/login/app/sprinkles/core/tests/Unit/DatabaseSyncableTest.php new file mode 100755 index 0000000..188b012 --- /dev/null +++ b/login/app/sprinkles/core/tests/Unit/DatabaseSyncableTest.php @@ -0,0 +1,119 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Tests\Unit; + +use stdClass; +use Mockery as m; +use ReflectionClass; +use PHPUnit\Framework\TestCase; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Collection; +use Illuminate\Support\Collection as BaseCollection; +use Illuminate\Database\Eloquent\ModelNotFoundException; + +use UserFrosting\Sprinkle\Core\Database\Relations\HasManySyncable; + +class DatabaseSyncableTest extends TestCase +{ + public function tearDown() + { + m::close(); + } + + /** + * @dataProvider syncMethodHasManyListProvider + */ + public function testSyncMethod($list) + { + $relation = $this->getRelation(); + + // Simulate determination of related key from builder + $relation->getRelated()->shouldReceive('getKeyName')->once()->andReturn('id'); + + // Simulate fetching of current relationships (1,2,3) + $query = m::mock('stdClass'); + $relation->shouldReceive('newQuery')->once()->andReturn($query); + $query->shouldReceive('pluck')->once()->with('id')->andReturn(new BaseCollection([1, 2, 3])); + + // withoutGlobalScopes will get called exactly 3 times + $relation->getRelated()->shouldReceive('withoutGlobalScopes')->times(3)->andReturn($query); + + // Test deletions of items removed from relationship (1) + $query->shouldReceive('whereIn')->once()->with('id', [1])->andReturn($query); + $query->shouldReceive('delete')->once()->andReturn($query); + + // Test updates to existing items in relationship (2,3) + $query->shouldReceive('where')->once()->with('id', 2)->andReturn($query); + $query->shouldReceive('update')->once()->with(['id' => 2, 'species' => 'Tyto'])->andReturn($query); + $query->shouldReceive('where')->once()->with('id', 3)->andReturn($query); + $query->shouldReceive('update')->once()->with(['id' => 3, 'species' => 'Megascops'])->andReturn($query); + + // Test creation of new items ('x') + $model = $this->expectCreatedModel($relation, [ + 'id' => 'x' + ]); + $model->shouldReceive('getAttribute')->with('id')->andReturn('x'); + + $this->assertEquals(['created' => ['x'], 'deleted' => [1], 'updated' => [2,3]], $relation->sync($list)); + } + + /** + * Set up and simulate base expectations for arguments to relationship. + */ + protected function getRelation() + { + $builder = m::mock('Illuminate\Database\Eloquent\Builder'); + $builder->shouldReceive('whereNotNull')->with('table.foreign_key'); + $builder->shouldReceive('where')->with('table.foreign_key', '=', 1); + $related = m::mock('Illuminate\Database\Eloquent\Model'); + $builder->shouldReceive('getModel')->andReturn($related); + $parent = m::mock('Illuminate\Database\Eloquent\Model'); + $parent->shouldReceive('getAttribute')->with('id')->andReturn(1); + $parent->shouldReceive('getCreatedAtColumn')->andReturn('created_at'); + $parent->shouldReceive('getUpdatedAtColumn')->andReturn('updated_at'); + return new HasManySyncable($builder, $parent, 'table.foreign_key', 'id'); + } + + public function syncMethodHasManyListProvider() + { + return [ + // First test set + [ + // First argument + [ + [ + 'id' => 2, + 'species' => 'Tyto' + ], + [ + 'id' => 3, + 'species' => 'Megascops' + ], + [ + 'id' => 'x' + ] + ] + ] + // Additional test sets here + ]; + } + + protected function expectNewModel($relation, $attributes = null) + { + $relation->getRelated()->shouldReceive('newInstance')->once()->with($attributes)->andReturn($model = m::mock(Model::class)); + $model->shouldReceive('setAttribute')->with('foreign_key', 1)->andReturn($model); + return $model; + } + + protected function expectCreatedModel($relation, $attributes) + { + $model = $this->expectNewModel($relation, $attributes); + $model->shouldReceive('save')->andReturn($model); + return $model; + } +} diff --git a/login/app/sprinkles/core/tests/Unit/SprunjeTest.php b/login/app/sprinkles/core/tests/Unit/SprunjeTest.php new file mode 100755 index 0000000..fa19319 --- /dev/null +++ b/login/app/sprinkles/core/tests/Unit/SprunjeTest.php @@ -0,0 +1,100 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Tests\Unit; + +use Illuminate\Database\Capsule\Manager as DB; +use Mockery as m; +use UserFrosting\Tests\TestCase; +use UserFrosting\Tests\DatabaseTransactions; +use UserFrosting\Sprinkle\Core\Database\Builder as Builder; +use UserFrosting\Sprinkle\Core\Database\Models\Model; +use UserFrosting\Sprinkle\Core\Sprunje\Sprunje; +use UserFrosting\Sprinkle\Core\Util\ClassMapper; + +/** + * SprunjeTest class. + * Tests a basic Sprunje. + * + * @extends TestCase + */ +class SprunjeTest extends TestCase +{ + public function tearDown() + { + m::close(); + } + + function testSprunjeApplyFiltersDefault() + { + $sprunje = new SprunjeStub([ + 'filters' => [ + 'species' => 'Tyto' + ] + ]); + + $builder = $sprunje->getQuery(); + + // Need to mock the new Builder instance that Laravel spawns in the where() closure. + // See https://stackoverflow.com/questions/20701679/mocking-callbacks-in-laravel-4-mockery + $builder->shouldReceive('newQuery')->andReturn( + $subBuilder = m::mock(Builder::class, function ($subQuery) { + $subQuery->makePartial(); + $subQuery->shouldReceive('orLike')->with('species', 'Tyto')->once()->andReturn($subQuery); + }) + ); + + $sprunje->applyFilters($builder); + } + + function testSprunjeApplySortsDefault() + { + $sprunje = new SprunjeStub([ + 'sorts' => [ + 'species' => 'asc' + ] + ]); + + $builder = $sprunje->getQuery(); + $builder->shouldReceive('orderBy')->once()->with('species', 'asc'); + $sprunje->applySorts($builder); + } + +} + +class SprunjeStub extends Sprunje +{ + protected $filterable = [ + 'species' + ]; + + protected $sortable = [ + 'species' + ]; + + public function __construct($options) + { + $classMapper = new ClassMapper(); + parent::__construct($classMapper, $options); + } + + protected function baseQuery() + { + // We use a partial mock for Builder, because we need to be able to run some of its actual methods. + // For example, we need to be able to run the `where` method with a closure. + $builder = m::mock(Builder::class); + $builder->makePartial(); + + return $builder; + } +} + +class SprunjeTestModelStub extends Model +{ + protected $table = 'table'; +} + diff --git a/login/app/system/Bakery/Bakery.php b/login/app/system/Bakery/Bakery.php new file mode 100755 index 0000000..8be8480 --- /dev/null +++ b/login/app/system/Bakery/Bakery.php @@ -0,0 +1,166 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\System\Bakery; + +use Symfony\Component\Console\Application; +use UserFrosting\System\UserFrosting; +use Illuminate\Support\Str; + +/** + * Base class for UserFrosting Bakery CLI tools. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class Bakery +{ + /** + * @var $app Symfony\Component\Console\Application + */ + protected $app; + + /** + * @var ContainerInterface The global container object, which holds all your services. + */ + protected $ci; + + /** + * Constructor + */ + public function __construct() + { + // Check for Sprinkles schema file + $sprinklesFile = @file_get_contents(\UserFrosting\SPRINKLES_SCHEMA_FILE); + if ($sprinklesFile === false) { + $sprinklesFile = $this->setupBaseSprinkleList(); + } + + // Create Symfony Console App + $this->app = new Application("UserFrosting Bakery", \UserFrosting\VERSION); + + // Setup the sprinkles + $uf = new UserFrosting(); + + // Set argument as false, we are using the CLI + $uf->setupSprinkles(false); + + // Get the container + $this->ci = $uf->getContainer(); + + // Add each commands to the Console App + $this->loadCommands(); + } + + /** + * Run the Symfony Console App + */ + public function run() + { + $this->app->run(); + } + + /** + * Return the list of available commands for a specific sprinkle + */ + protected function loadCommands() + { + // Get base Bakery command + $commands = $this->getBakeryCommands(); + + // Get the sprinkles commands + $sprinkles = $this->ci->sprinkleManager->getSprinkleNames(); + foreach ($sprinkles as $sprinkle) { + $commands = $commands->merge($this->getSprinkleCommands($sprinkle)); + } + + // Add commands to the App + $commands->each(function($command) { + $instance = new $command(); + $instance->setContainer($this->ci); + $this->app->add($instance); + }); + } + + /** + * Return the list of available commands for a specific sprinkle + * Sprinkles commands should be located in `src/Bakery/` + */ + protected function getSprinkleCommands($sprinkle) + { + // Find all the migration files + $path = $this->commandDirectoryPath($sprinkle); + $files = glob($path . "*.php"); + $commands = collect($files); + + // Transform the path into a class names + $commands->transform(function ($file) use ($sprinkle, $path) { + $className = basename($file, '.php'); + $sprinkleName = Str::studly($sprinkle); + $className = "\\UserFrosting\\Sprinkle\\".$sprinkleName."\\Bakery\\".$className; + return $className; + }); + + return $commands; + } + + /** + * Return the list of available commands in system/Bakery/Command/ + */ + protected function getBakeryCommands() + { + // Find all the migration files + $files = glob(\UserFrosting\APP_DIR . "/system/Bakery/Command/" . "*.php"); + $commands = collect($files); + + // Transform the path into a class names + $commands->transform(function ($file) { + $className = basename($file, '.php'); + $className = "\\UserFrosting\\System\\Bakery\\Command\\".$className; + return $className; + }); + + return $commands; + } + + /** + * Returns the path of the Migration directory. + * + * @access protected + * @param mixed $sprinkleName + * @return void + */ + protected function commandDirectoryPath($sprinkleName) + { + return \UserFrosting\SPRINKLES_DIR . + \UserFrosting\DS . + $sprinkleName . + \UserFrosting\DS . + \UserFrosting\SRC_DIR_NAME . + "/Bakery/"; + } + + /** + * Write the base Sprinkles schema file if it doesn't exist. + * + * @access protected + * @return void + */ + protected function setupBaseSprinkleList() + { + $model = \UserFrosting\APP_DIR . '/sprinkles.example.json'; + $destination = \UserFrosting\SPRINKLES_SCHEMA_FILE; + $sprinklesModelFile = @file_get_contents($model); + if ($sprinklesModelFile === false) { + $this->io->error("File `$sprinklesModelFile` not found. Please create '$destination' manually and try again."); + exit(1); + } + + file_put_contents($destination, $sprinklesModelFile); + + return $sprinklesModelFile; + } +} diff --git a/login/app/system/Bakery/BaseCommand.php b/login/app/system/Bakery/BaseCommand.php new file mode 100755 index 0000000..1a59141 --- /dev/null +++ b/login/app/system/Bakery/BaseCommand.php @@ -0,0 +1,58 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\System\Bakery; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Formatter\OutputFormatterStyle; +use Symfony\Component\Console\Style\SymfonyStyle; +use Interop\Container\ContainerInterface; + +/** + * Base class for UserFrosting Bakery CLI tools. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +abstract class BaseCommand extends Command +{ + /** + * @var @Symfony\Component\Console\Style\SymfonyStyle + * See http://symfony.com/doc/current/console/style.html + */ + protected $io; + + /** + * @var string Path to the project root folder + */ + protected $projectRoot; + + /** + * @var ContainerInterface $ci The global container object, which holds all of the UserFrosting services. + */ + protected $ci; + + /** + * {@inheritDoc} + */ + protected function initialize(InputInterface $input, OutputInterface $output) + { + $this->io = new SymfonyStyle($input, $output); + $this->projectRoot = \UserFrosting\ROOT_DIR; + } + + /** + * Setup the global container object + */ + public function setContainer(ContainerInterface $ci) + { + $this->ci = $ci; + } +} diff --git a/login/app/system/Bakery/Command/Bake.php b/login/app/system/Bakery/Command/Bake.php new file mode 100755 index 0000000..5dc0e27 --- /dev/null +++ b/login/app/system/Bakery/Command/Bake.php @@ -0,0 +1,77 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\System\Bakery\Command; + +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use UserFrosting\System\Bakery\BaseCommand; + +/** + * Bake command. + * Shortcut to run multiple commands at once + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class Bake extends BaseCommand +{ + /** + * @var string Path to the build/ directory + */ + protected $buildPath; + + /** + * @var String $ufArt The UserFrosting ASCII art. + */ + public $title = " + _ _ ______ _ _ +| | | | | ___| | | (_) +| | | |___ ___ _ __| |_ _ __ ___ ___| |_ _ _ __ __ _ +| | | / __|/ _ \ '__| _| '__/ _ \/ __| __| | '_ \ / _` | +| |_| \__ \ __/ | | | | | | (_) \__ \ |_| | | | | (_| | + \___/|___/\___|_| \_| |_| \___/|___/\__|_|_| |_|\__, | + __/ | + |___/"; + + /** + * {@inheritDoc} + */ + protected function configure() + { + $this->setName("bake") + ->setDescription("UserFrosting installation command") + ->setHelp("This command combine the <info>debug</info>, <info>migrate</info> and <info>build-assets</info> commands."); + } + + /** + * {@inheritDoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->io->writeln("<info>{$this->title}</info>"); + + $command = $this->getApplication()->find('setup'); + $command->run($input, $output); + + $command = $this->getApplication()->find('debug'); + $command->run($input, $output); + + $command = $this->getApplication()->find('migrate'); + $command->run($input, $output); + + $command = $this->getApplication()->find('create-admin'); + $command->run($input, $output); + + $command = $this->getApplication()->find('build-assets'); + $command->run($input, $output); + + $command = $this->getApplication()->find('clear-cache'); + $command->run($input, $output); + } +}
\ No newline at end of file diff --git a/login/app/system/Bakery/Command/BuildAssets.php b/login/app/system/Bakery/Command/BuildAssets.php new file mode 100755 index 0000000..055fa43 --- /dev/null +++ b/login/app/system/Bakery/Command/BuildAssets.php @@ -0,0 +1,180 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\System\Bakery\Command; + +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use UserFrosting\System\Bakery\BaseCommand; + +/** + * Assets builder CLI Tools. + * Wrapper for npm/node commands + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class BuildAssets extends BaseCommand +{ + /** + * @var string Path to the build/ directory + */ + protected $buildPath; + + /** + * {@inheritDoc} + */ + protected function configure() + { + $this->setName("build-assets") + ->setDescription("Build the assets using node and npm") + ->setHelp("The build directory contains the scripts and configuration files required to download Javascript, CSS, and other assets used by UserFrosting. This command will install Gulp, Bower, and several other required npm packages locally. With <info>npm</info> set up with all of its required packages, it can be use it to automatically download and install the assets in the correct directories. For more info, see <comment>https://learn.userfrosting.com/basics/installation</comment>") + ->addOption("compile", "c", InputOption::VALUE_NONE, "Compile the assets and asset bundles for production environment") + ->addOption("force", "f", InputOption::VALUE_NONE, "Force assets compilation by deleting cached data and installed assets before proceeding"); + } + + /** + * {@inheritDoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + // Display header, + $this->io->title("UserFrosting's Assets Builder"); + + // Set $path + $this->buildPath = $this->projectRoot . \UserFrosting\DS . \UserFrosting\BUILD_DIR_NAME; + + // Delete cached data is requested + if ($input->getOption('force')) { + $this->clean(); + } + + // Perform tasks + $this->npmInstall(); + $this->assetsInstall(); + + // Compile if requested + if ($input->getOption('compile') || $this->isProduction()) { + $this->buildAssets(); + } + + // Test the result + $this->checkAssets(); + + // If all went well and there's no fatal errors, we are successful + $this->io->success("Assets install looks successful"); + } + + /** + * Install npm package + * + * @access protected + * @return void + */ + protected function npmInstall() + { + $this->io->section("<info>Installing npm dependencies</info>"); + $this->io->writeln("> <comment>npm install</comment>"); + + // Temporarily change the working directory so we can install npm dependencies + $wd = getcwd(); + chdir($this->buildPath); + passthru("npm install"); + chdir($wd); + } + + /** + * Perform UF Assets installation + * + * @access protected + * @return void + */ + protected function assetsInstall() + { + $this->io->section("Installing assets bundles"); + $this->io->writeln("> <comment>npm run uf-assets-install</comment>"); + passthru("npm run uf-assets-install --prefix " . $this->buildPath); + } + + /** + * Build the production bundle. + * + * @access protected + * @return void + */ + protected function buildAssets() + { + $this->io->section("Building assets for production"); + + $this->io->writeln("> <comment>npm run uf-bundle-build</comment>"); + passthru("npm run uf-bundle-build --prefix " . $this->buildPath); + + $this->io->writeln("> <comment>npm run uf-bundle</comment>"); + passthru("npm run uf-bundle --prefix " . $this->buildPath); + + $this->io->writeln("> <comment>npm run uf-bundle-clean</comment>"); + passthru("npm run uf-bundle-clean --prefix " . $this->buildPath); + } + + /** + * Check that the assets where installed in the core sprinkles + * + * @access protected + * @return void + */ + protected function checkAssets() + { + $this->io->section("Testing assets installation"); + + // Get path and vendor files + $vendorPath = \UserFrosting\SPRINKLES_DIR . "/core/assets/vendor/*"; + $coreVendorFiles = glob($vendorPath); + + if (!$coreVendorFiles){ + $this->io->error("Assets installation seems to have failed. Directory `$vendorPath` is empty, but it shouldn't be. Check the above log for any errors."); + exit(1); + } + + // Check that `bundle.result.json` is present in production mode + $config = $this->ci->config; + $resultFile = \UserFrosting\ROOT_DIR . \UserFrosting\DS . \UserFrosting\BUILD_DIR_NAME . \UserFrosting\DS . $config['assets.compiled.schema']; + if ($this->isProduction() && !file_exists($resultFile)) { + $this->io->error("Assets building seems to have failed. File `$resultFile` not found. This file is required for production envrionement. Check the above log for any errors."); + exit(1); + } + } + + /** + * Run the `uf-clean` command to delete installed assets, delete compiled + * bundle config file and delete compiled assets + * + * @access protected + * @return void + */ + protected function clean() + { + $this->io->section("Cleaning cached data"); + $this->io->writeln("> <comment>npm run uf-clean</comment>"); + passthru("npm run uf-clean --prefix " . $this->buildPath); + } + + /** + * Return if the app is in production mode + * + * @access protected + * @return bool + */ + protected function isProduction() + { + // N.B.: Need to touch the config service first to load dotenv values + $config = $this->ci->config; + $mode = getenv("UF_MODE") ?: ''; + + return ($mode == "production"); + } +}
\ No newline at end of file diff --git a/login/app/system/Bakery/Command/ClearCache.php b/login/app/system/Bakery/Command/ClearCache.php new file mode 100755 index 0000000..d38f382 --- /dev/null +++ b/login/app/system/Bakery/Command/ClearCache.php @@ -0,0 +1,95 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\System\Bakery\Command; + +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use UserFrosting\System\Bakery\BaseCommand; +use UserFrosting\Sprinkle\Core\Twig\CacheHelper; + +/** + * ClearCache CLI Command. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class ClearCache extends BaseCommand +{ + /** + * {@inheritDoc} + */ + protected function configure() + { + $this->setName("clear-cache") + ->setDescription("Clears the application cache. Includes cache service, Twig and Router cached data"); + } + + /** + * {@inheritDoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->io->title("Clearing cache"); + + // Clear normal cache + $this->io->writeln("<info> > Clearing Illuminate cache instance</info>", OutputInterface::VERBOSITY_VERBOSE); + $this->clearIlluminateCache(); + + // Clear Twig cache + $this->io->writeln("<info> > Clearing Twig cached data</info>", OutputInterface::VERBOSITY_VERBOSE); + if (!$this->clearTwigCache()) { + $this->io->error("Failed to clear Twig cached data. Make sure you have write access to the `app/cache/twig` directory."); + exit(1); + } + + // Clear router cache + $this->io->writeln("<info> > Clearing Router cache file</info>", OutputInterface::VERBOSITY_VERBOSE); + if (!$this->clearRouterCache()) { + $file = $this->ci->config['settings.routerCacheFile']; + $this->io->error("Failed to delete Router cache file. Make sure you have write access to the `$file` file."); + exit(1); + } + + $this->io->success("Cache cleared !"); + } + + /** + * Flush the cached data from the cache service + * + * @access protected + * @return void + */ + protected function clearIlluminateCache() + { + $this->ci->cache->flush(); + } + + /** + * Clear the Twig cache using the Twig CacheHelper class + * + * @access protected + * @return bool true/false if operation is successfull + */ + protected function clearTwigCache() + { + $cacheHelper = new CacheHelper($this->ci); + return $cacheHelper->clearCache(); + } + + /** + * Clear the Router cache data file + * + * @access protected + * @return bool true/false if operation is successfull + */ + protected function clearRouterCache() + { + return $this->ci->router->clearCache(); + } +}
\ No newline at end of file diff --git a/login/app/system/Bakery/Command/Debug.php b/login/app/system/Bakery/Command/Debug.php new file mode 100755 index 0000000..4e8a3e4 --- /dev/null +++ b/login/app/system/Bakery/Command/Debug.php @@ -0,0 +1,185 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\System\Bakery\Command; + +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use UserFrosting\System\Bakery\BaseCommand; +use UserFrosting\System\Bakery\DatabaseTest; + +/** + * Debug CLI tool. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class Debug extends BaseCommand +{ + use DatabaseTest; + + /** + * {@inheritDoc} + */ + protected function configure() + { + $this->setName("debug") + ->setDescription("Test the UserFrosting installation and setup the database") + ->setHelp("This command is used to check if the various dependencies of UserFrosting are met and display useful debugging information. \nIf any error occurs, check out the online documentation for more info about that error. \nThis command also provide the necessary tools to setup the database credentials"); + } + + /** + * {@inheritDoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + // Display header, + $this->io->title("UserFrosting"); + $this->io->writeln("UserFrosing version : " . \UserFrosting\VERSION); + $this->io->writeln("OS Name : " . php_uname('s')); + $this->io->writeln("Project Root : {$this->projectRoot}"); + + // Need to touch the config service first to load dotenv values + $config = $this->ci->config; + $this->io->writeln("Environment mode : " . getenv("UF_MODE")); + + // Perform tasks + $this->checkPhpVersion(); + $this->checkNodeVersion(); + $this->checkNpmVersion(); + $this->listSprinkles(); + $this->showConfig(); + $this->checkDatabase(); + + // If all went well and there's no fatal errors, we are ready to bake + $this->io->success("Ready to bake !"); + } + + /** + * Check the minimum version of php. + * This is done by composer itself, but we do it again for good mesure + * + * @access public + * @return void + */ + protected function checkPhpVersion() + { + $this->io->writeln("PHP Version : " . phpversion()); + if (version_compare(phpversion(), \UserFrosting\PHP_MIN_VERSION, '<')) { + $this->io->error("UserFrosting requires php version ".\UserFrosting\PHP_MIN_VERSION." or above. You'll need to update you PHP version before you can continue."); + exit(1); + } + } + + /** + * Check the minimum version requirement of Node installed + * + * @access public + * @return void + */ + protected function checkNodeVersion() + { + $npmVersion = trim(exec('node -v')); + $this->io->writeln("Node Version : $npmVersion"); + + if (version_compare($npmVersion, 'v4', '<')) { + $this->io->error("UserFrosting requires Node version 4.x or above. Check the documentation for more details."); + exit(1); + } + } + + /** + * Check the minimum version requirement for Npm + * + * @access public + * @return void + */ + protected function checkNpmVersion() + { + $npmVersion = trim(exec('npm -v')); + $this->io->writeln("NPM Version : $npmVersion"); + + if (version_compare($npmVersion, '3', '<')) { + $this->io->error("UserFrosting requires npm version 3.x or above. Check the documentation for more details."); + exit(1); + } + } + + /** + * List all sprinkles defined in the Sprinkles schema file, + * making sure this file exist at the same time + * + * @access protected + * @return void + */ + protected function listSprinkles() + { + // Check for Sprinkles schema file + $path = \UserFrosting\SPRINKLES_SCHEMA_FILE; + $sprinklesFile = @file_get_contents($path); + if ($sprinklesFile === false) { + $this->io->error("The file `$path` not found."); + } + + // List installed sprinkles + $sprinkles = json_decode($sprinklesFile)->base; + $this->io->section("Loaded sprinkles"); + $this->io->listing($sprinkles); + + // Throw fatal error if the `core` sprinkle is missing + if (!in_array("core", $sprinkles)) { + $this->io->error("The `core` sprinkle is missing from the 'sprinkles.json' file."); + exit(1); + } + } + + /** + * Check the database connexion and setup the `.env` file if we can't + * connect and there's no one found. + * + * @access protected + * @return void + */ + protected function checkDatabase() + { + $this->io->section("Testing database connection..."); + + try { + $this->testDB(); + $this->io->writeln("Database connection successful"); + return; + } catch (\Exception $e) { + $error = $e->getMessage(); + $this->io->error($error); + exit(1); + } + } + + /** + * Display database config as for debug purposes + * + * @access protected + * @return void + */ + protected function showConfig() + { + // Get config + $config = $this->ci->config; + + // Display database info + $this->io->section("Database config"); + $this->io->writeln([ + "DRIVER : " . $config['db.default.driver'], + "HOST : " . $config['db.default.host'], + "PORT : " . $config['db.default.port'], + "DATABASE : " . $config['db.default.database'], + "USERNAME : " . $config['db.default.username'], + "PASSWORD : " . ($config['db.default.password'] ? "*********" : "") + ]); + } +} diff --git a/login/app/system/Bakery/Command/Migrate.php b/login/app/system/Bakery/Command/Migrate.php new file mode 100755 index 0000000..c0a5b4f --- /dev/null +++ b/login/app/system/Bakery/Command/Migrate.php @@ -0,0 +1,48 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\System\Bakery\Command; + +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use UserFrosting\System\Bakery\BaseCommand; +use UserFrosting\System\Bakery\Migrator; + +/** + * Migrate CLI Tools. + * Perform database migrations commands + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class Migrate extends BaseCommand +{ + /** + * {@inheritDoc} + */ + protected function configure() + { + $this->setName("migrate") + ->setDescription("Perform database migration") + ->setHelp("This command runs all the pending database migrations.") + ->addOption('pretend', 'p', InputOption::VALUE_NONE, 'Run migrations in "dry run" mode'); + } + + /** + * {@inheritDoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->io->title("UserFrosting's Migrator"); + + $pretend = $input->getOption('pretend'); + + $migrator = new Migrator($this->io, $this->ci); + $migrator->runUp($pretend); + } +}
\ No newline at end of file diff --git a/login/app/system/Bakery/Command/MigrateRefresh.php b/login/app/system/Bakery/Command/MigrateRefresh.php new file mode 100755 index 0000000..3d18c41 --- /dev/null +++ b/login/app/system/Bakery/Command/MigrateRefresh.php @@ -0,0 +1,52 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\System\Bakery\Command; + +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use UserFrosting\System\Bakery\BaseCommand; +use UserFrosting\System\Bakery\Migrator; + +/** + * Migrate CLI Tools. + * Perform database migrations commands + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class MigrateRefresh extends BaseCommand +{ + /** + * {@inheritDoc} + */ + protected function configure() + { + $this->setName("migrate:refresh") + ->setDescription("Rollback the last migration operation and run it up again") + ->addOption('steps', 's', InputOption::VALUE_REQUIRED, 'Number of steps to rollback', 1) + ->addOption('sprinkle', null, InputOption::VALUE_REQUIRED, 'The sprinkle to rollback', "") + ->addOption('pretend', 'p', InputOption::VALUE_NONE, 'Run migrations in "dry run" mode'); + } + + /** + * {@inheritDoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->io->title("Migration refresh"); + + $step = $input->getOption('steps'); + $sprinkle = $input->getOption('sprinkle'); + $pretend = $input->getOption('pretend'); + + $migrator = new Migrator($this->io, $this->ci); + $migrator->runDown($step, $sprinkle, $pretend); + $migrator->runUp($pretend); + } +}
\ No newline at end of file diff --git a/login/app/system/Bakery/Command/MigrateReset.php b/login/app/system/Bakery/Command/MigrateReset.php new file mode 100755 index 0000000..9e38cbb --- /dev/null +++ b/login/app/system/Bakery/Command/MigrateReset.php @@ -0,0 +1,49 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\System\Bakery\Command; + +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use UserFrosting\System\Bakery\BaseCommand; +use UserFrosting\System\Bakery\Migrator; + +/** + * Migrate CLI Tools. + * Perform database migrations commands + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class MigrateReset extends BaseCommand +{ + /** + * {@inheritDoc} + */ + protected function configure() + { + $this->setName("migrate:reset") + ->setDescription("Reset the whole database to an empty state") + ->addOption('sprinkle', null, InputOption::VALUE_REQUIRED, 'The sprinkle to rollback', "") + ->addOption('pretend', 'p', InputOption::VALUE_NONE, 'Run migrations in "dry run" mode'); + } + + /** + * {@inheritDoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->io->title("Migration reset"); + + $sprinkle = $input->getOption('sprinkle'); + $pretend = $input->getOption('pretend'); + + $migrator = new Migrator($this->io, $this->ci); + $migrator->runDown(-1, $sprinkle, $pretend); + } +}
\ No newline at end of file diff --git a/login/app/system/Bakery/Command/MigrateRollback.php b/login/app/system/Bakery/Command/MigrateRollback.php new file mode 100755 index 0000000..916f5ee --- /dev/null +++ b/login/app/system/Bakery/Command/MigrateRollback.php @@ -0,0 +1,51 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\System\Bakery\Command; + +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use UserFrosting\System\Bakery\BaseCommand; +use UserFrosting\System\Bakery\Migrator; + +/** + * Migrate CLI Tools. + * Perform database migrations commands + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class MigrateRollback extends BaseCommand +{ + /** + * {@inheritDoc} + */ + protected function configure() + { + $this->setName("migrate:rollback") + ->setDescription("Rollback last database migration") + ->addOption('steps', 's', InputOption::VALUE_REQUIRED, 'Number of steps to rollback', 1) + ->addOption('sprinkle', null, InputOption::VALUE_REQUIRED, 'The sprinkle to rollback', "") + ->addOption('pretend', 'p', InputOption::VALUE_NONE, 'Run migrations in "dry run" mode'); + } + + /** + * {@inheritDoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->io->title("Migration rollback"); + + $step = $input->getOption('steps'); + $sprinkle = $input->getOption('sprinkle'); + $pretend = $input->getOption('pretend'); + + $migrator = new Migrator($this->io, $this->ci); + $migrator->runDown($step, $sprinkle, $pretend); + } +}
\ No newline at end of file diff --git a/login/app/system/Bakery/Command/Setup.php b/login/app/system/Bakery/Command/Setup.php new file mode 100755 index 0000000..b489ce2 --- /dev/null +++ b/login/app/system/Bakery/Command/Setup.php @@ -0,0 +1,223 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\System\Bakery\Command; + +use Illuminate\Database\Capsule\Manager as Capsule; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use UserFrosting\System\Bakery\BaseCommand; + +/** + * Setup wizard CLI Tools. + * Helper command to setup .env file + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class Setup extends BaseCommand +{ + /** + * envfile path + */ + protected $envPath = \UserFrosting\APP_DIR. '/.env'; + + /** + * {@inheritDoc} + */ + protected function configure() + { + $this->setName("setup") + ->setDescription("UserFrosting configuration wizard") + ->setHelp("Helper command to setup the database and email configuration. This can also be done manually by editing the <comment>app/.env</comment> file or using global server environment variables.") + ->addOption("force", "f", InputOption::VALUE_NONE, "If `.env` file exist, force setup to run again"); + } + + /** + * {@inheritDoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + // Get config + $config = $this->ci->config; + + // Get options + $force = $input->getOption('force'); + + // Display header, + $this->io->title("UserFrosting's Setup Wizard"); + + // Check if the .env file exist. + if (!$force && file_exists($this->envPath)) { + $this->io->note("File `{$this->envPath}` already exist. Use the `php bakery setup -f` command to force setup to run again."); + return; + } + + // There might not be any `.env` file because there may be some custom config or global env values defined on the server. + // We'll check for that. If the configs are empty, we'll assume nothing is defined and go strait to setup. + if (!$force && $config["db.default.host"] != "" && $config["db.default.database"] != "" && $config["db.default.username"] != "") { + $this->io->note("File `{$this->envPath}` was not found, but some database configuration variables are present. Global system environment variables might be defined. If this is not right, use -f option to force setup to run."); + return; + } + + //Goto setup + $this->setupEnv(); + } + + /** + * Setup the `.env` file. + * + * @access public + * @return void + */ + public function setupEnv() + { + // Get config + $config = $this->ci->config; + + // Get the db driver choices + $drivers = $this->databaseDrivers(); + + + // Ask the questions + $this->io->section("Setting up database"); + $this->io->note("Database credentials will be saved in `app/.env`"); + + $driver = $this->io->choice("Database type", $drivers->pluck('name')->toArray()); + $driver = $drivers->where('name', $driver)->first(); + + $driverName = $driver['driver']; + $defaultDBName = $driver['defaultDBName']; + + if ($driverName == 'sqlite') { + $name = $this->io->ask("Database name", $defaultDBName); + + $dbParams = [ + 'driver' => $driverName, + 'database' => $name + ]; + } else { + $defaultPort = $driver['defaultPort']; + + $host = $this->io->ask("Hostname", "localhost"); + $port = $this->io->ask("Port", $defaultPort); + $name = $this->io->ask("Database name", $defaultDBName); + $user = $this->io->ask("Username", "userfrosting"); + $password = $this->io->askHidden("Password", function ($password) { + // Use custom validator to accept empty password + return $password; + }); + + $dbParams = [ + 'driver' => $driverName, + 'host' => $host, + 'port' => $port, + 'database' => $name, + 'username' => $user, + 'password' => $password, + 'charset' => $config['db.default.charset'] + ]; + } + + // Setup a new db connection + $capsule = new Capsule; + $capsule->addConnection($dbParams); + + // Test the db connexion. + try { + $conn = $capsule->getConnection(); + $conn->getPdo(); + $this->io->success("Database connection successful"); + $success = true; + } catch (\PDOException $e) { + $message = "Could not connect to the database '{$dbParams['username']}@{$dbParams['host']}/{$dbParams['database']}':".PHP_EOL; + $message .= "Exception: " . $e->getMessage() . PHP_EOL.PHP_EOL; + $message .= "Please check your database configuration and/or google the exception shown above and run the command again."; + $this->io->error($message); + exit(1); + } + + // Ask for the smtp values now + $this->io->section("Enter your SMTP credentials"); + $this->io->write("This is used to send emails from the system. Edit `app/.env` if you have problems with this later."); + $smtpHost = $this->io->ask("SMTP Host", "host.example.com"); + $smtpUser = $this->io->ask("SMTP User", "relay@example.com"); + $smtpPassword = $this->io->askHidden("SMTP Password", function ($password) { + // Use custom validator to accept empty password + return $password; + }); + + if ($driverName == 'sqlite') { + $fileContent = [ + "UF_MODE=\"\"\n", + "DB_DRIVER=\"{$dbParams['driver']}\"\n", + "DB_NAME=\"{$dbParams['database']}\"\n", + "SMTP_HOST=\"$smtpHost\"\n", + "SMTP_USER=\"$smtpUser\"\n", + "SMTP_PASSWORD=\"$smtpPassword\"\n" + ]; + } else { + $fileContent = [ + "UF_MODE=\"\"\n", + "DB_DRIVER=\"{$dbParams['driver']}\"\n", + "DB_HOST=\"{$dbParams['host']}\"\n", + "DB_PORT=\"{$dbParams['port']}\"\n", + "DB_NAME=\"{$dbParams['database']}\"\n", + "DB_USER=\"{$dbParams['username']}\"\n", + "DB_PASSWORD=\"{$dbParams['password']}\"\n", + "SMTP_HOST=\"$smtpHost\"\n", + "SMTP_USER=\"$smtpUser\"\n", + "SMTP_PASSWORD=\"$smtpPassword\"\n" + ]; + } + + // Let's save this config + file_put_contents($this->envPath, $fileContent); + + // At this point, `$this->uf` is still using the old configs. + // We need to refresh the `db.default` config values + $newConfig = array_merge($config['db.default'], $dbParams); + $this->ci->config->set("db.default", $newConfig); + } + + /** + * Return the database choices for the env setup. + * + * @access protected + * @return void + */ + protected function databaseDrivers() + { + return collect([ + [ + "driver" => "mysql", + "name" => "MySQL / MariaDB", + "defaultDBName" => "userfrosting", + "defaultPort" => 3306 + ], + [ + "driver" => "pgsql", + "name" => "ProgreSQL", + "defaultDBName" => "userfrosting", + "defaultPort" => 5432 + ], + [ + "driver" => "sqlsrv", + "name" => "SQL Server", + "defaultDBName" => "userfrosting", + "defaultPort" => 1433 + ], + [ + "driver" => "sqlite", + "name" => "SQLite", + "defaultDBName" => \UserFrosting\DB_DIR . \UserFrosting\DS . 'userfrosting.db', + "defaultPort" => null + ] + ]); + } +} diff --git a/login/app/system/Bakery/Command/Test.php b/login/app/system/Bakery/Command/Test.php new file mode 100755 index 0000000..553fddd --- /dev/null +++ b/login/app/system/Bakery/Command/Test.php @@ -0,0 +1,56 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\System\Bakery\Command; + +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use UserFrosting\System\Bakery\BaseCommand; + +/** + * Automated testing CLI tool. + * Sets up environment and runs PHPUnit tests in each Sprinkle. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class Test extends BaseCommand +{ + /** + * @var string Path to the build/ directory + */ + protected $buildPath; + + /** + * {@inheritDoc} + */ + protected function configure() + { + $this->setName("test") + ->setDescription("Run tests") + ->setHelp("Run php unit tests"); + } + + /** + * {@inheritDoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->io->title("UserFrosting's Tester"); + + // Get command + $command = \UserFrosting\VENDOR_DIR . "/bin/phpunit --colors=always"; + if ($output->isVerbose() || $output->isVeryVerbose()) { + $command .= " -v"; + } + + // Execute + $this->io->writeln("> <comment>$command</comment>"); + passthru($command); + } +}
\ No newline at end of file diff --git a/login/app/system/Bakery/DatabaseTest.php b/login/app/system/Bakery/DatabaseTest.php new file mode 100755 index 0000000..0e4f3bf --- /dev/null +++ b/login/app/system/Bakery/DatabaseTest.php @@ -0,0 +1,52 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\System\Bakery; + +use Illuminate\Database\Capsule\Manager as Capsule; + +/** + * Database Test Trait. Include method to test the db connexion + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +trait DatabaseTest +{ + + /** + * Function to test the db connexion. + * + * @access protected + * @return bool True if success + */ + protected function testDB() + { + // Boot db + $this->ci->db; + + // Get config + $config = $this->ci->config; + + // Check params are valids + $dbParams = $config['db.default']; + if (!$dbParams) { + throw new \Exception("'default' database connection not found. Please double-check your configuration."); + } + + // Test database connection directly using PDO + try { + Capsule::connection()->getPdo(); + } catch (\PDOException $e) { + $message = "Could not connect to the database '{$dbParams['username']}@{$dbParams['host']}/{$dbParams['database']}':".PHP_EOL; + $message .= "Exception: " . $e->getMessage() . PHP_EOL.PHP_EOL; + $message .= "Please check your database configuration and/or google the exception shown above and run command again."; + throw new \Exception($message); + } + + return true; + } +}
\ No newline at end of file diff --git a/login/app/system/Bakery/Migration.php b/login/app/system/Bakery/Migration.php new file mode 100755 index 0000000..e6c6ae0 --- /dev/null +++ b/login/app/system/Bakery/Migration.php @@ -0,0 +1,64 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\System\Bakery; + +use Illuminate\Database\Schema\Builder; +use Symfony\Component\Console\Style\SymfonyStyle; + +/** + * Abstract Migration class. + * + * @abstract + * @author Alex Weissman (https://alexanderweissman.com) + */ +abstract class Migration +{ + /** + * @var Illuminate\Database\Schema\Builder $schema + */ + protected $schema; + + /** + * @var @Composer\IO\IOInterface + */ + protected $io; + + /** + * List of dependencies for this migration. + * Should return an array of class required to be run before this migration + */ + public $dependencies = []; + + /** + * __construct function. + * + * @access public + * @param Illuminate\Database\Schema\Builder $schema + * @return void + */ + public function __construct(Builder $schema, SymfonyStyle $io) + { + $this->schema = $schema; + $this->io = $io; + } + + /** + * Method to apply changes to the database + */ + public function up() {} + + /** + * Method to revert changes applied by the `up` method + */ + public function down() {} + + /** + * Method to seed new information to the database + */ + public function seed() {} +} diff --git a/login/app/system/Bakery/Migrator.php b/login/app/system/Bakery/Migrator.php new file mode 100755 index 0000000..611f73f --- /dev/null +++ b/login/app/system/Bakery/Migrator.php @@ -0,0 +1,584 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\System\Bakery; + +use Illuminate\Database\Capsule\Manager as Capsule; +use Illuminate\Support\Str; +use Interop\Container\ContainerInterface; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use UserFrosting\Sprinkle\Core\Util\BadClassNameException; +use UserFrosting\System\Database\Model\Migrations; +use UserFrosting\System\Bakery\DatabaseTest; + +/** + * Migrator class + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class Migrator +{ + use DatabaseTest; + + /** + * @var @Symfony\Component\Console\Style\SymfonyStyle + * See http://symfony.com/doc/current/console/style.html + */ + protected $io; + + /** + * @var ContainerInterface $ci The global container object, which holds all of UserFristing services. + */ + protected $ci; + + /** + * @var @Illuminate\Database\Schema + */ + protected $schema; + + /** + * @var String table The name of the migration table + */ + protected $table = "migrations"; + + /** + * @var Array sprinkles The list of defined sprinkles + */ + protected $sprinkles; + + /** + * @var Int Current batch number. All the migration class run `up` in a single command will be grouped by this batch number + */ + protected $batch; + + /** + * @var Collection List of pending migration that require installation + */ + protected $pending; + + /** + * @var Collection List of installed migration. This is built from the log data in the database + */ + protected $installed; + + /** + * @var List of fulfillable migration (Migration that needs to be run and their dependencies are met) + * This list is very important. While `pending` is a list of migrations that needs to be run, `fulfillable` + * contain the order in which they are required to be run. When resolving the dependencies, this list will + * automatically be sorted to make sure the dependencies are run in the correct order + */ + protected $fulfillable; + + /** + * @var List of unfulfillable migration (Migration that needs to be run and their dependencies are NOT met) + * Note : An error could be thrown when an unfulfillable migration is met, but it makes much nicer cli error this way + */ + protected $unfulfillable; + + /** + * Constructor. + * + * @access public + * @param SymfonyStyle $io + * @param ContainerInterface $ci + * @return void + */ + public function __construct(SymfonyStyle $io, ContainerInterface $ci) + { + $this->io = $io; + $this->ci = $ci; + + // Start by testing the DB connexion, just in case + try { + $this->io->writeln("<info>Testing database connection</info>", OutputInterface::VERBOSITY_VERBOSE); + $this->testDB(); + $this->io->writeln("Ok", OutputInterface::VERBOSITY_VERBOSE); + } catch (\Exception $e) { + $this->io->error($e->getMessage()); + exit(1); + } + + // Get schema required to run the table blueprints + $this->schema = Capsule::schema(); + + // Make sure the setup table exist + $this->setupVersionTable(); + } + + /** + * Run all the migrations available + * + * @access public + * @param bool $pretend (default: false) + * @return void + */ + public function runUp($pretend = false) + { + // Get installed migrations and pluck by class name. We only need this for now + $migrations = Migrations::get(); + $this->installed = $migrations->pluck('migration'); + + $this->io->writeln("\n<info>Installed migrations:</info>", OutputInterface::VERBOSITY_VERBOSE); + $this->io->writeln($this->installed->toArray(), OutputInterface::VERBOSITY_VERBOSE); + + // Get pending migrations + $this->io->section("Fetching available migrations"); + $this->pending = $this->getPendingMigrations(); + + // If there's no pending migration, don't need to go further + if ($this->pending->isEmpty()) { + $this->io->success("Nothing to migrate !"); + return; + } + + // Resolve the dependencies + $this->resolveDependencies(); + + // If there are any unfulfillable migration, we can't continue + if (!$this->unfulfillable->isEmpty()) { + + $msg = "\nSome migrations dependencies can't be met. Check those migrations for unmet dependencies and try again:"; + + foreach ($this->unfulfillable as $migration) { + $msg .= "\n{$migration->className} depends on \n - "; + $msg .= implode("\n - ", $migration->dependencies); + $msg .= "\n"; + } + + $this->io->error($msg); + exit(1); + } + + // Ready to run ! + $this->io->section("Running migrations"); + + if ($pretend) { + $this->io->note("Running migration in pretend mode"); + } + + // We have a list of fulfillable migration, we run them up! + foreach ($this->fulfillable as $migration) { + $this->io->write("\n> <info>Migrating {$migration->className}...</info>"); + + if ($pretend) { + $this->io->newLine(); + $this->pretendToRun($migration, 'up'); + } else { + $migration->up(); + $migration->seed(); + $this->log($migration); + $this->io->writeln(" Done!"); + } + } + + // If all went well and there's no fatal errors, we are ready to bake + $this->io->success("Migration successful !"); + } + + /** + * Rollback the last btach of migrations. + * + * @access public + * @param int $step (default: 1). Number of batch we will be going back. -1 revert all migrations + * @param string $sprinkle (default: "") Limit rollback to a specific sprinkle + * @param bool $pretend (default: false) + * @return void + */ + public function runDown($step = 1, $sprinkle = "", $pretend = false) + { + // Can't go furhter down than 1 step + if ($step <= 0 && $step != -1) { + throw new \InvalidArgumentException("Step can't be 0 or less"); + } + + // Get last batch number + $batch = $this->getNextBatchNumber(); + + // Calculate the number of steps back we need to take + if ($step == -1) { + $stepsBack = 1; + $this->io->warning("Rolling back all migrations"); + } else { + $stepsBack = max($batch - $step, 1); + $this->io->note("Rolling back $step steps to batch $stepsBack", OutputInterface::VERBOSITY_VERBOSE); + } + + // Get installed migrations + $migrations = Migrations::orderBy("id", "desc")->where('batch', '>=', $stepsBack); + + // Add the sprinkle requirement too + if ($sprinkle != "") { + $this->io->note("Rolling back sprinkle `$sprinkle`", OutputInterface::VERBOSITY_VERBOSE); + $migrations->where('sprinkle', $sprinkle); + } + + // Run query + $migrations = $migrations->get(); + + // If there's nothing to rollback, stop here + if ($migrations->isEmpty()) { + $this->io->writeln("<info>Nothing to rollback</info>"); + exit(1); + } + + // Get pending migrations + $this->io->writeln("<info>Migration to rollback:</info>"); + $this->io->listing($migrations->pluck('migration')->toArray()); + + // Ask confirmation to continue. + if (!$pretend && !$this->io->confirm("Continue?", false)) { + exit(1); + } + + if ($pretend) { + $this->io->note("Rolling back in pretend mode"); + } + + // Loop again to run down each migration + foreach ($migrations as $migration) { + + // Check if those migration class are available + if (!class_exists($migration->migration)) { + $this->io->warning("Migration class {$migration->migration} doesn't exist."); + continue; + } + + $this->io->write("> <info>Rolling back {$migration->migration}...</info>"); + $migrationClass = $migration->migration; + $instance = new $migrationClass($this->schema, $this->io); + + if ($pretend) { + $this->io->newLine(); + $this->pretendToRun($instance, 'down'); + } else { + $instance->down(); + $migration->delete(); + $this->io->writeln(" Done!"); + } + + $this->io->newLine(); + } + + // If all went well and there's no fatal errors, we are ready to bake + $this->io->success("Rollback successful !"); + } + + /** + * Pretend to run migration class. + * + * @access protected + * @param mixed $migration + * @param string $method up/down + */ + protected function pretendToRun($migration, $method) + { + foreach ($this->getQueries($migration, $method) as $query) { + $this->io->writeln($query['query'], OutputInterface::VERBOSITY_VERBOSE); + } + } + + /** + * Return all of the queries that would be run for a migration. + * + * @access protected + * @param mixed $migration + * @param string $method up/down + * @return void + */ + protected function getQueries($migration, $method) + { + $db = $this->schema->getConnection(); + + return $db->pretend(function () use ($migration, $method) { + if (method_exists($migration, $method)) { + $migration->{$method}(); + } + }); + } + + /** + * Get pending migrations by looking at all the migration files + * and finding the one not yet runed by compairing with the ran migrations + * + * @access protected + * @return void + */ + protected function getPendingMigrations() + { + $pending = collect([]); + + // Get the sprinkle list + $sprinkles = $this->ci->sprinkleManager->getSprinkleNames(); + + // Loop all the sprinkles to find their pending migrations + foreach ($sprinkles as $sprinkle) { + + $this->io->writeln("> Fetching from `$sprinkle`"); + + // We get all the migrations. This will return them as a colleciton of class names + $migrations = $this->getMigrations($sprinkle); + + // We filter the available migration by removing the one that have already been run + // This reject the class name found in the installed collection + $migrations = $migrations->reject(function ($value, $key) { + return $this->installed->contains($value); + }); + + // Load each class + foreach ($migrations as $migrationClass) { + + // Make sure the class exist + if (!class_exists($migrationClass)) { + throw new BadClassNameException("Unable to find the migration class '$migrationClass'." ); + } + + // Load the migration class + $migration = new $migrationClass($this->schema, $this->io); + + //Set the sprinkle + $migration->sprinkle = $sprinkle; + + // Also set the class name. We could find it using ::class, but this + // will make it easier to manipulate the collection + $migration->className = $migrationClass; + + // Add it to the pending list + $pending->push($migration); + } + } + + // Get pending migrations + $pendingArray = ($pending->pluck('className')->toArray()) ?: ""; + $this->io->writeln("\n<info>Pending migrations:</info>", OutputInterface::VERBOSITY_VERBOSE); + $this->io->writeln($pendingArray, OutputInterface::VERBOSITY_VERBOSE); + + return $pending; + } + + /** + * Get the list of migrations avaiables in the filesystem. + * Return a list of resolved className + * + * @access public + * @param string $sprinkleName + * @return void + */ + public function getMigrations($sprinkle) + { + // Find all the migration files + $path = $this->migrationDirectoryPath($sprinkle); + $files = glob($path . "*/*.php"); + + // Transform the array in a collection + $migrations = collect($files); + + // We transform the path into a migration object + $migrations->transform(function ($file) use ($sprinkle, $path) { + // Deconstruct the path + $migration = str_replace($path, "", $file); + $className = basename($file, '.php'); + $sprinkleName = Str::studly($sprinkle); + $version = str_replace("/$className.php", "", $migration); + + // Reconstruct the classname + $className = "\\UserFrosting\\Sprinkle\\".$sprinkleName."\\Database\\Migrations\\".$version."\\".$className; + + return $className; + }); + + return $migrations; + } + + /** + * Resolve all the dependencies for all the pending migrations + * This function fills in the `fullfillable` and `unfulfillable` list + * + * @access protected + * @return void + */ + protected function resolveDependencies() + { + $this->io->writeln("\n<info>Resolving migrations dependencies...</info>", OutputInterface::VERBOSITY_VERBOSE); + + // Reset fulfillable/unfulfillable lists + $this->fulfillable = collect([]); + $this->unfulfillable = collect([]); + + // Loop pending and check for dependencies + foreach ($this->pending as $migration) { + $this->validateClassDependencies($migration); + } + + $fulfillable = ($this->fulfillable->pluck('className')->toArray()) ?: ""; + $this->io->writeln("\n<info>Fulfillable migrations:</info>", OutputInterface::VERBOSITY_VERBOSE); + $this->io->writeln($fulfillable, OutputInterface::VERBOSITY_VERBOSE); + + $unfulfillable = ($this->unfulfillable->pluck('className')->toArray()) ?: ""; + $this->io->writeln("\n<info>Unfulfillable migrations:</info>", OutputInterface::VERBOSITY_VERBOSE); + $this->io->writeln($unfulfillable, OutputInterface::VERBOSITY_VERBOSE); + } + + /** + * Check if a migration dependencies are met. + * To test if a migration is fulfillable, the class must : + * Already been installed OR exist and have all it's dependencies met + * + * @access protected + * @param $migration + * @return bool true/false if all conditions are met + */ + protected function validateClassDependencies($migration) + { + $this->io->writeln("> Checking dependencies for {$migration->className}", OutputInterface::VERBOSITY_VERBOSE); + + // If it's already marked as fulfillable, it's fulfillable + // Return true directly (it's already marked) + if ($this->fulfillable->contains($migration)) { + return true; + } + + // If it's already marked as unfulfillable, it's unfulfillable + // Return false directly (it's already marked) + if ($this->unfulfillable->contains($migration)) { + return false; + } + + // If it's already run, it's fulfillable + // Mark it as such for next time it comes up in this loop + if ($this->installed->contains($migration->className)) { + return $this->markAsFulfillable($migration); + } + + // Loop dependencies. If one is not fulfillable, then this one is not either + foreach ($migration->dependencies as $dependencyClass) { + + // The dependency might already be installed. Check that first + if ($this->installed->contains($dependencyClass)) { + continue; + } + + // Try to find it in the `pending` list. Cant' find it? Then it's not fulfillable + $dependency = $this->pending->where('className', $dependencyClass)->first(); + + // Check migration dependencies of this one right now + // If ti's not fullfillable, then this one isn't either + if (!$dependency || !$this->validateClassDependencies($dependency)) { + return $this->markAsUnfulfillable($migration); + } + } + + // If no dependencies returned false, it's fulfillable + return $this->markAsFulfillable($migration); + } + + /** + * Mark a dependency as fulfillable. + * Removes it from the pending list and add it to the fulfillable list + * + * @access protected + * @param $migration + * @return true + */ + protected function markAsFulfillable($migration) + { + $this->fulfillable->push($migration); + return true; + } + + /** + * Mark a dependency as unfulfillable. + * Removes it from the pending list and add it to the unfulfillable list + * + * @access protected + * @param $migration + * @return false + */ + protected function markAsUnfulfillable($migration) + { + $this->unfulfillable->push($migration); + return false; + } + + /** + * Log that a migration was run. + * + * @access public + * @param mixed $migration + * @return void + */ + protected function log($migration) + { + // Get the next batch number if not defined + if (!$this->batch) { + $this->batch = $this->getNextBatchNumber(); + } + + $log = new Migrations([ + 'sprinkle' => $migration->sprinkle, + 'migration' => $migration->className, + 'batch' => $this->batch + ]); + $log->save(); + } + + /** + * Return the next batch number from the db. + * Batch number is used to group together migration run in the same operation + * + * @access public + * @return int the next batch number + */ + public function getNextBatchNumber() + { + $batch = Migrations::max('batch'); + return ($batch) ? $batch + 1 : 1; + } + + /** + * Create the migration history table if needed. + * Also check if the tables requires migrations + * We run the migration file manually for this one + * + * @access public + * @return void + */ + protected function setupVersionTable() + { + // Check if the `migrations` table exist. Create it manually otherwise + if (!$this->schema->hasColumn($this->table, 'id')) { + $this->io->section("Creating the `{$this->table}` table"); + + $migration = new \UserFrosting\System\Database\Migrations\v410\MigrationTable($this->schema, $this->io); + $migration->up(); + + $this->io->success("Table `{$this->table}` created"); + } + } + + /** + * Returns the path of the Migration directory. + * + * @access protected + * @param mixed $sprinkleName + * @return void + */ + protected function migrationDirectoryPath($sprinkleName) + { + $path = \UserFrosting\SPRINKLES_DIR . + \UserFrosting\DS . + $sprinkleName . + \UserFrosting\DS . + \UserFrosting\SRC_DIR_NAME . + "/Database/Migrations/"; + + return $path; + } +} diff --git a/login/app/system/Database/Migrations/v410/MigrationTable.php b/login/app/system/Database/Migrations/v410/MigrationTable.php new file mode 100755 index 0000000..fb833df --- /dev/null +++ b/login/app/system/Database/Migrations/v410/MigrationTable.php @@ -0,0 +1,59 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\System\Database\Migrations\v410; + +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Schema\Builder; +use UserFrosting\System\Bakery\Migration; + +/** + * Migration table migration + * Version 4.1.0 + * + * See https://laravel.com/docs/5.4/migrations#tables + * @extends Migration + * @author Alex Weissman (https://alexanderweissman.com) + */ +class MigrationTable extends Migration +{ + /** + * {@inheritDoc} + */ + public $dependencies = []; + + /** + * {@inheritDoc} + */ + public function up() + { + $this->schema->create('migrations', function (Blueprint $table) { + $table->increments('id'); + $table->string('sprinkle'); + $table->string('migration'); + $table->integer('batch'); + $table->timestamps(); + + $table->engine = 'InnoDB'; + $table->collation = 'utf8_unicode_ci'; + $table->charset = 'utf8'; + }); + + // Drop the old `version` table if found + if ($this->schema->hasTable('version')) { + $this->schema->drop('version'); + } + } + + /** + * {@inheritDoc} + */ + public function down() + { + $this->schema->drop('migrations'); + } +} diff --git a/login/app/system/Database/Model/Migrations.php b/login/app/system/Database/Model/Migrations.php new file mode 100755 index 0000000..6a0942e --- /dev/null +++ b/login/app/system/Database/Model/Migrations.php @@ -0,0 +1,55 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\System\Database\Model; + +use Illuminate\Database\Capsule\Manager as Capsule; +use Illuminate\Database\Eloquent\Model; + +/** + * Migration Model + * + * Represents the model for the `migrations` table + * + * @author Alex Weissman (https://alexanderweissman.com) + * @property string sprinkle + * @property string version + */ +class Migrations extends Model +{ + /** + * @var string The name of the table for the current model. + */ + protected $table = "migrations"; + + /** + * @var bool Enable timestamps for this class. + */ + public $timestamps = true; + + /** + * @var array List of fields that can be edited by this model + */ + protected $fillable = [ + "sprinkle", + "migration", + "batch" + ]; + + /** + * scopeForSprinkle function. + * + * @access protected + * @param mixed $query + * @param string $sprinkleName + * @return void + */ + protected function scopeForSprinkle($query, $sprinkleName) + { + return $query->where('sprinkle', $sprinkleName); + } +} diff --git a/login/app/system/Facade.php b/login/app/system/Facade.php new file mode 100755 index 0000000..0d1ad82 --- /dev/null +++ b/login/app/system/Facade.php @@ -0,0 +1,247 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\System; + +use Illuminate\Support\Facades\Mockery; +use Illuminate\Support\Facades\Mockery\MockInterface; +use Interop\Container\ContainerInterface; +use RuntimeException; + +/** + * Implements base functionality for static "facade" classes. + * + * Adapted from the Laravel Facade class: https://github.com/laravel/framework/blob/5.3/src/Illuminate/Support/Facades/Facade.php + * + * @author Alex Weissman (https://alexanderweissman.com) + * @see https://laravel.com/docs/5.2/facades + * @license https://github.com/laravel/framework/blob/5.3/LICENSE.md (MIT License) + */ +abstract class Facade +{ + /** + * The Pimple container being facaded. + * + * @var \Interop\Container\ContainerInterface + */ + protected static $container; + + /** + * The resolved object instances. + * + * @var array + */ + protected static $resolvedInstance; + + /** + * Hotswap the underlying service instance behind the facade. + * + * @param mixed $instance + * @return void + */ + public static function swap($instance) + { + $name = static::getFacadeAccessor(); + + static::$resolvedInstance[$name] = $instance; + + // Replace service in container + unset(static::$container->$name); + static::$container->$name = $instance; + } + + /** + * Initiate a mock expectation on the facade. + * + * @param mixed + * @return \Mockery\Expectation + */ + public static function shouldReceive() + { + $name = static::getFacadeAccessor(); + + if (static::isMock()) { + $mock = static::$resolvedInstance[$name]; + } else { + $mock = static::createFreshMockInstance($name); + } + + return call_user_func_array([$mock, 'shouldReceive'], func_get_args()); + } + + /** + * Create a fresh mock instance for the given class. + * + * @param string $name + * @return \Mockery\Expectation + */ + protected static function createFreshMockInstance($name) + { + static::$resolvedInstance[$name] = $mock = static::createMockByName($name); + + $mock->shouldAllowMockingProtectedMethods(); + + if (isset(static::$container)) { + static::$container->$name = $mock; + } + + return $mock; + } + + /** + * Create a fresh mock instance for the given class. + * + * @param string $name + * @return \Mockery\Expectation + */ + protected static function createMockByName($name) + { + $class = static::getMockableClass($name); + + return $class ? Mockery::mock($class) : Mockery::mock(); + } + + /** + * Determines whether a mock is set as the instance of the facade. + * + * @return bool + */ + protected static function isMock() + { + $name = static::getFacadeAccessor(); + + return isset(static::$resolvedInstance[$name]) && static::$resolvedInstance[$name] instanceof MockInterface; + } + + /** + * Get the mockable class for the bound instance. + * + * @return string|null + */ + protected static function getMockableClass() + { + if ($root = static::getFacadeRoot()) { + return get_class($root); + } + } + + /** + * Get the root object behind the facade. + * + * @return mixed + */ + public static function getFacadeRoot() + { + return static::resolveFacadeInstance(static::getFacadeAccessor()); + } + + /** + * Get the registered name of the component. + * + * @return string + * + * @throws \RuntimeException + */ + protected static function getFacadeAccessor() + { + throw new RuntimeException('Facade does not implement getFacadeAccessor method.'); + } + + /** + * Resolve the facade root instance from the container. + * + * @param string|object $name + * @return mixed + */ + protected static function resolveFacadeInstance($name) + { + if (is_object($name)) { + return $name; + } + + if (isset(static::$resolvedInstance[$name])) { + return static::$resolvedInstance[$name]; + } + + return static::$resolvedInstance[$name] = static::$container->$name; + } + + /** + * Clear a resolved facade instance. + * + * @param string $name + * @return void + */ + public static function clearResolvedInstance($name) + { + unset(static::$resolvedInstance[$name]); + } + + /** + * Clear all of the resolved instances. + * + * @return void + */ + public static function clearResolvedInstances() + { + static::$resolvedInstance = []; + } + + /** + * Get the container instance behind the facade. + * + * @return \Interop\Container\ContainerInterface + */ + public static function getFacadeContainer() + { + return static::$container; + } + + /** + * Set the container instance. + * + * @param \Interop\Container\ContainerInterface $container + * @return void + */ + public static function setFacadeContainer($container) + { + static::$container = $container; + } + + /** + * Handle dynamic, static calls to the object. + * + * @param string $method + * @param array $args + * @return mixed + * + * @throws \RuntimeException + */ + public static function __callStatic($method, $args) + { + $instance = static::getFacadeRoot(); + + if (! $instance) { + throw new RuntimeException('A facade root has not been set.'); + } + + switch (count($args)) { + case 0: + return $instance->$method(); + case 1: + return $instance->$method($args[0]); + case 2: + return $instance->$method($args[0], $args[1]); + case 3: + return $instance->$method($args[0], $args[1], $args[2]); + case 4: + return $instance->$method($args[0], $args[1], $args[2], $args[3]); + default: + return call_user_func_array([$instance, $method], $args); + } + } +} diff --git a/login/app/system/ServicesProvider.php b/login/app/system/ServicesProvider.php new file mode 100755 index 0000000..6286bc0 --- /dev/null +++ b/login/app/system/ServicesProvider.php @@ -0,0 +1,104 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\System; + +use Interop\Container\ContainerInterface; +use RocketTheme\Toolbox\Event\EventDispatcher; +use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; +use RocketTheme\Toolbox\StreamWrapper\ReadOnlyStream; +use RocketTheme\Toolbox\StreamWrapper\StreamBuilder; +use UserFrosting\System\Sprinkle\SprinkleManager; + +/** + * UserFrosting system services provider. + * + * Registers system services for UserFrosting, such as file locator, event dispatcher, and sprinkle manager. + * @author Alex Weissman (https://alexanderweissman.com) + */ +class ServicesProvider +{ + /** + * Register UserFrosting's system services. + * + * @param ContainerInterface $container A DI container implementing ArrayAccess and container-interop. + */ + public function register(ContainerInterface $container) + { + /** + * Set up the event dispatcher, required by Sprinkles to hook into the UF lifecycle. + */ + $container['eventDispatcher'] = function ($c) { + return new EventDispatcher(); + }; + + /** + * Path/file locator service. + * + * Register custom streams for the application, and add paths for app-level streams. + */ + $container['locator'] = function ($c) { + + $locator = new UniformResourceLocator(\UserFrosting\ROOT_DIR); + + $locator->addPath('build', '', \UserFrosting\BUILD_DIR_NAME); + $locator->addPath('log', '', \UserFrosting\APP_DIR_NAME . '/' . \UserFrosting\LOG_DIR_NAME); + $locator->addPath('cache', '', \UserFrosting\APP_DIR_NAME . '/' . \UserFrosting\CACHE_DIR_NAME); + $locator->addPath('session', '', \UserFrosting\APP_DIR_NAME . '/' . \UserFrosting\SESSION_DIR_NAME); + + // Use locator to initialize streams + ReadOnlyStream::setLocator($locator); + + // Fire up StreamBuilder + $c->streamBuilder; + + return $locator; + }; + + /** + * StreamBuilder, to fire up our custom StreamWrapper defined in the locator service. + */ + $container['streamBuilder'] = function ($c) { + + $streams = [ + 'build' => '\\RocketTheme\\Toolbox\\StreamWrapper\\Stream', + 'log' => '\\RocketTheme\\Toolbox\\StreamWrapper\\Stream', + 'cache' => '\\RocketTheme\\Toolbox\\StreamWrapper\\Stream', + 'session' => '\\RocketTheme\\Toolbox\\StreamWrapper\\Stream', + 'sprinkles' => '\\RocketTheme\\Toolbox\\StreamWrapper\\ReadOnlyStream', + 'assets' => '\\RocketTheme\\Toolbox\\StreamWrapper\\ReadOnlyStream', + 'schema' => '\\RocketTheme\\Toolbox\\StreamWrapper\\ReadOnlyStream', + 'templates' => '\\RocketTheme\\Toolbox\\StreamWrapper\\ReadOnlyStream', + 'extra' => '\\RocketTheme\\Toolbox\\StreamWrapper\\ReadOnlyStream', + 'locale' => '\\RocketTheme\\Toolbox\\StreamWrapper\\ReadOnlyStream', + 'config' => '\\RocketTheme\\Toolbox\\StreamWrapper\\ReadOnlyStream', + 'routes' => '\\RocketTheme\\Toolbox\\StreamWrapper\\ReadOnlyStream', + 'factories' => '\\RocketTheme\\Toolbox\\StreamWrapper\\ReadOnlyStream' + ]; + + // Before registering them, we need to unregister any that where previously registered. + // This will cause error when two scripts are run in succession from the CLI + foreach ($streams as $scheme => $handler) { + if (in_array($scheme, stream_get_wrappers())) { + stream_wrapper_unregister($scheme); + } + } + + $sb = new StreamBuilder($streams); + + return $sb; + }; + + /** + * Set up sprinkle manager service. + */ + $container['sprinkleManager'] = function ($c) { + $sprinkleManager = new SprinkleManager($c); + return $sprinkleManager; + }; + } +} diff --git a/login/app/system/SlimAppEvent.php b/login/app/system/SlimAppEvent.php new file mode 100755 index 0000000..f1217a5 --- /dev/null +++ b/login/app/system/SlimAppEvent.php @@ -0,0 +1,29 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\System; + +use RocketTheme\Toolbox\Event\Event; +use Slim\App; + +/** + * Used for events that need to access the Slim application. + */ +class SlimAppEvent extends Event +{ + protected $app; + + public function __construct(App $app) + { + $this->app = $app; + } + + public function getApp() + { + return $this->app; + } +} diff --git a/login/app/system/Sprinkle/Sprinkle.php b/login/app/system/Sprinkle/Sprinkle.php new file mode 100755 index 0000000..4707025 --- /dev/null +++ b/login/app/system/Sprinkle/Sprinkle.php @@ -0,0 +1,56 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\System\Sprinkle; + +use Interop\Container\ContainerInterface; +use RocketTheme\Toolbox\Event\EventSubscriberInterface; +use Slim\App; + +/** + * Sprinkle class + * + * Represents a sprinkle (plugin, theme, site, etc), and the code required to boot up that sprinkle. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ +class Sprinkle implements EventSubscriberInterface +{ + /** + * @var ContainerInterface The global container object, which holds all your services. + */ + protected $ci; + + /** + * By default assign all methods as listeners using the default priority. + * + * @return array + */ + public static function getSubscribedEvents() + { + $methods = get_class_methods(get_called_class()); + + $list = []; + foreach ($methods as $method) { + if (strpos($method, 'on') === 0) { + $list[$method] = [$method, 0]; + } + } + + return $list; + } + + /** + * Create a new Sprinkle object. + * + * @param ContainerInterface $ci The global container object, which holds all your services. + */ + public function __construct(ContainerInterface $ci) + { + $this->ci = $ci; + } +} diff --git a/login/app/system/Sprinkle/SprinkleManager.php b/login/app/system/Sprinkle/SprinkleManager.php new file mode 100755 index 0000000..c206cea --- /dev/null +++ b/login/app/system/Sprinkle/SprinkleManager.php @@ -0,0 +1,236 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\System\Sprinkle; + +use Illuminate\Support\Str; +use Interop\Container\ContainerInterface; +use RocketTheme\Toolbox\Event\EventDispatcher; +use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; +use UserFrosting\Support\Exception\FileNotFoundException; + +/** + * Sprinkle manager class. + * + * Manages a collection of loaded Sprinkles for the application. + * Handles Sprinkle class creation, event subscription, services registration, and resource stream registration. + * @author Alex Weissman (https://alexanderweissman.com) + */ +class SprinkleManager +{ + /** + * @var ContainerInterface The global container object, which holds all your services. + */ + protected $ci; + + /** + * @var Sprinkle[] An array of sprinkles. + */ + protected $sprinkles = []; + + /** + * @var string The full absolute base path to the sprinkles directory. + */ + protected $sprinklesPath; + + /** + * @var string[] Keeps track of a mapping from resource stream names to relative paths. + */ + protected $resourcePaths; + + /** + * Create a new SprinkleManager object. + * + * @param ContainerInterface $ci The global container object, which holds all your services. + */ + public function __construct(ContainerInterface $ci) + { + $this->ci = $ci; + $this->sprinklesPath = \UserFrosting\APP_DIR_NAME . \UserFrosting\DS . \UserFrosting\SPRINKLES_DIR_NAME . \UserFrosting\DS; + + $this->resourcePaths = [ + 'assets' => \UserFrosting\DS . \UserFrosting\ASSET_DIR_NAME, + 'config' => \UserFrosting\DS . \UserFrosting\CONFIG_DIR_NAME, + 'extra' => \UserFrosting\DS . \UserFrosting\EXTRA_DIR_NAME, + 'factories' => \UserFrosting\DS . \UserFrosting\FACTORY_DIR_NAME, + 'locale' => \UserFrosting\DS . \UserFrosting\LOCALE_DIR_NAME, + 'routes' => \UserFrosting\DS . \UserFrosting\ROUTE_DIR_NAME, + 'schema' => \UserFrosting\DS . \UserFrosting\SCHEMA_DIR_NAME, + 'sprinkles' => '', + 'templates' => \UserFrosting\DS . \UserFrosting\TEMPLATE_DIR_NAME + ]; + } + + /** + * Adds the relative path for a specified resource type in a Sprinkle to the resource's stream. + * + * @param string $resourceName + * @param string $sprinkleName + * @return string|bool The full path to specified resource for the specified Sprinkle (if found). + */ + public function addResource($resourceName, $sprinkleName) + { + $resourcePath = $this->resourcePaths[$resourceName]; + $fullPath = $this->sprinklesPath . $sprinkleName . $resourcePath; + + $this->ci->locator->addPath($resourceName, '', $fullPath); + + return $this->ci->locator->findResource("$resourceName://", true, false); + + /* This would allow a stream to subnavigate to a specific sprinkle (e.g. "templates://core/") + Not sure if we need this. + $locator->addPath('templates', '$name', $sprinklesDirFragment . '/' . \UserFrosting\TEMPLATE_DIR_NAME); + */ + } + + /** + * Register resource streams for all base sprinkles. + */ + public function addResources() + { + // For each sprinkle, register its resources and then run its initializer + foreach ($this->sprinkles as $sprinkleName => $sprinkle) { + $this->addResource('config', $sprinkleName); + $this->addResource('assets', $sprinkleName); + $this->addResource('extra', $sprinkleName); + $this->addResource('factories', $sprinkleName); + $this->addResource('locale', $sprinkleName); + $this->addResource('routes', $sprinkleName); + $this->addResource('schema', $sprinkleName); + $this->addResource('sprinkles', $sprinkleName); + $this->addResource('templates', $sprinkleName); + } + } + + /** + * Takes the name of a Sprinkle, and creates an instance of the initializer object (if defined). + * + * Creates an object of a subclass of UserFrosting\System\Sprinkle\Sprinkle if defined for the sprinkle (converting to StudlyCase). + * Otherwise, returns null. + * @param $name The name of the Sprinkle to initialize. + */ + public function bootSprinkle($name) + { + $className = Str::studly($name); + $fullClassName = "\\UserFrosting\\Sprinkle\\$className\\$className"; + + // Check that class exists. If not, set to null + if (class_exists($fullClassName)) { + $sprinkle = new $fullClassName($this->ci); + return $sprinkle; + } else { + return null; + } + } + + /** + * Returns a list of available sprinkle names. + * + * @return string[] + */ + public function getSprinkleNames() + { + return array_keys($this->sprinkles); + } + + /** + * Returns a list of available sprinkles. + * + * @return Sprinkle[] + */ + public function getSprinkles() + { + return $this->sprinkles; + } + + /** + * Initialize a list of Sprinkles, instantiating their boot classes (if they exist), + * and subscribing them to the event dispatcher. + * + * @param string[] $baseSprinkleNames + */ + public function init($sprinkleNames) + { + foreach ($sprinkleNames as $sprinkleName) { + $sprinkle = $this->bootSprinkle($sprinkleName); + + if ($sprinkle) { + // Subscribe the sprinkle to the event dispatcher + $this->ci->eventDispatcher->addSubscriber($sprinkle); + } + + $this->sprinkles[$sprinkleName] = $sprinkle; + } + } + + /** + * Initialize all base sprinkles in a specified Sprinkles schema file (e.g. 'sprinkles.json'). + * + * @param string $schemaPath + */ + public function initFromSchema($schemaPath) + { + $baseSprinkleNames = $this->loadSchema($schemaPath)->base; + $this->init($baseSprinkleNames); + } + + /** + * Return if a Sprinkle is available + * Can be used by other Sprinkles to test if their dependencies are met + * + * @param $name The name of the Sprinkle + */ + public function isAvailable($name) + { + return in_array($name, $this->getSprinkleNames()); + } + + + /** + * Interate through the list of loaded Sprinkles, and invoke their ServiceProvider classes. + */ + public function registerAllServices() + { + foreach ($this->getSprinkleNames() as $sprinkleName) { + $this->registerServices($sprinkleName); + } + } + + /** + * Register services for a specified Sprinkle. + */ + public function registerServices($name) + { + $className = Str::studly($name); + $fullClassName = "\\UserFrosting\\Sprinkle\\$className\\ServicesProvider\\ServicesProvider"; + + // Check that class exists, and register services + if (class_exists($fullClassName)) { + // Register core services + $serviceProvider = new $fullClassName(); + $serviceProvider->register($this->ci); + } + } + + /** + * Load list of Sprinkles from a JSON schema file (e.g. 'sprinkles.json'). + * + * @param string $schemaPath + * @return string[] + */ + protected function loadSchema($schemaPath) + { + $sprinklesFile = @file_get_contents($schemaPath); + + if ($sprinklesFile === false) { + $errorMessage = "Error: Unable to determine Sprinkle load order. File '$schemaPath' not found or unable to read. Please create a 'sprinkles.json' file and try again."; + throw new FileNotFoundException($errorMessage); + } + + return json_decode($sprinklesFile); + } +} diff --git a/login/app/system/UserFrosting.php b/login/app/system/UserFrosting.php new file mode 100755 index 0000000..4f569ec --- /dev/null +++ b/login/app/system/UserFrosting.php @@ -0,0 +1,187 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\System; + +use RocketTheme\Toolbox\Event\EventDispatcher; +use RocketTheme\Toolbox\Event\Event; +use Slim\App; +use Slim\Container; +use UserFrosting\Support\Exception\FileNotFoundException; +use UserFrosting\System\Facade; + +class UserFrosting +{ + /** + * @var Container The global container object, which holds all your services. + */ + protected $ci; + + /** + * @var App The Slim application instance. + */ + protected $app; + + /** + * Create the UserFrosting application instance. + */ + public function __construct() + { + // First, we create our DI container + $this->ci = new Container; + + // Set up facade reference to container. + Facade::setFacadeContainer($this->ci); + } + + /** + * Fires an event with optional parameters. + * + * @param string $eventName + * @param Event $event + * + * @return Event + */ + public function fireEvent($eventName, Event $event = null) + { + /** @var EventDispatcher $events */ + $eventDispatcher = $this->ci->eventDispatcher; + + return $eventDispatcher->dispatch($eventName, $event); + } + + /** + * Return the underlying Slim App instance, if available. + * + * @return Slim\App + */ + public function getApp() + { + return $this->app; + } + + /** + * Return the DI container. + * + * @return Slim\Container + */ + public function getContainer() + { + return $this->ci; + } + + /** + * Include all defined routes in route stream. + * + * Include them in reverse order to allow higher priority routes to override lower priority. + */ + public function loadRoutes() + { + // Since routes aren't encapsulated in a class yet, we need this workaround :( + global $app; + $app = $this->app; + + $routePaths = array_reverse($this->ci->locator->findResources('routes://', true, true)); + foreach ($routePaths as $path) { + $routeFiles = glob($path . '/*.php'); + foreach ($routeFiles as $routeFile) { + require_once $routeFile; + } + } + } + + /** + * Initialize the application. Set up Sprinkles and the Slim app, define routes, register global middleware, and run Slim. + */ + public function run() + { + $this->setupSprinkles(); + + // Set the configuration settings for Slim in the 'settings' service + $this->ci->settings = $this->ci->config['settings']; + + // Next, we'll instantiate the Slim application. Note that the application is required for the SprinkleManager to set up routes. + $this->app = new App($this->ci); + + $slimAppEvent = new SlimAppEvent($this->app); + + $this->fireEvent('onAppInitialize', $slimAppEvent); + + // Set up all routes + $this->loadRoutes(); + + // Add global middleware + $this->fireEvent('onAddGlobalMiddleware', $slimAppEvent); + + $this->app->run(); + } + + /** + * Register system services, load all sprinkles, and add their resources and services. + * + * @param bool $isWeb Set to true if setting up in an HTTP/web environment, false if setting up for CLI scripts. + */ + public function setupSprinkles($isWeb = true) + { + // Register system services + $serviceProvider = new ServicesProvider(); + $serviceProvider->register($this->ci); + + // Boot the Sprinkle manager, which creates Sprinkle classes and subscribes them to the event dispatcher + $sprinkleManager = $this->ci->sprinkleManager; + + try { + $sprinkleManager->initFromSchema(\UserFrosting\SPRINKLES_SCHEMA_FILE); + } catch (FileNotFoundException $e) { + if ($isWeb) { + $this->renderSprinkleErrorPage($e->getMessage()); + } else { + $this->renderSprinkleErrorCli($e->getMessage()); + } + } + + $this->fireEvent('onSprinklesInitialized'); + + // Add Sprinkle resources (assets, templates, etc) to locator + $sprinkleManager->addResources(); + $this->fireEvent('onSprinklesAddResources'); + + // Register Sprinkle services + $sprinkleManager->registerAllServices(); + $this->fireEvent('onSprinklesRegisterServices'); + } + + /** + * Render a basic error page for problems with loading Sprinkles. + */ + protected function renderSprinkleErrorPage($errorMessage = "") + { + ob_clean(); + $title = "UserFrosting Application Error"; + $errorMessage = "Unable to start site. Contact owner.<br/><br/>" . + "Version: UserFrosting ".\UserFrosting\VERSION."<br/>" . + $errorMessage; + $output = sprintf( + "<html><head><meta http-equiv='Content-Type' content='text/html; charset=utf-8'>" . + "<title>%s</title><style>body{margin:0;padding:30px;font:12px/1.5 Helvetica,Arial,Verdana," . + "sans-serif;}h1{margin:0;font-size:48px;font-weight:normal;line-height:48px;}strong{" . + "display:inline-block;width:65px;}</style></head><body><h1>%s</h1>%s</body></html>", + $title, + $title, + $errorMessage + ); + exit($output); + } + + /** + * Render a CLI error message for problems with loading Sprinkles. + */ + protected function renderSprinkleErrorCli($errorMessage = "") + { + exit($errorMessage . PHP_EOL); + } +} diff --git a/login/app/tests/DatabaseTransactions.php b/login/app/tests/DatabaseTransactions.php new file mode 100755 index 0000000..ed2225b --- /dev/null +++ b/login/app/tests/DatabaseTransactions.php @@ -0,0 +1,48 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Tests; + +/** + * Trait enabling wrapping of each test case in a database transaction + * Based on Laravel `DatabaseTransactions` Traits + * + * @author Louis Charette + */ +trait DatabaseTransactions +{ + /** + * Handle database transactions on the specified connections. + * + * @return void + */ + public function beginDatabaseTransaction() + { + $database = $this->ci['db']; + + foreach ($this->connectionsToTransact() as $name) { + $database->connection($name)->beginTransaction(); + } + + $this->beforeApplicationDestroyed(function () use ($database) { + foreach ($this->connectionsToTransact() as $name) { + $database->connection($name)->rollBack(); + } + }); + } + + /** + * The database connections that should have transactions. + * + * @return array + */ + protected function connectionsToTransact() + { + return property_exists($this, 'connectionsToTransact') + ? $this->connectionsToTransact : [null]; + } +}
\ No newline at end of file diff --git a/login/app/tests/TestCase.php b/login/app/tests/TestCase.php new file mode 100755 index 0000000..1115abe --- /dev/null +++ b/login/app/tests/TestCase.php @@ -0,0 +1,236 @@ +<?php +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ +namespace UserFrosting\Tests; + +use Slim\App; +use PHPUnit\Framework\TestCase as BaseTestCase; +use UserFrosting\System\UserFrosting; + +/** + * Class to handle Test + * + * @author Louis Charette + */ +class TestCase extends BaseTestCase +{ + /** + * The Slim application instance. + * + * @var \Slim\App + */ + protected $app; + + /** + * The global container object, which holds all your services. + * + * @var \Interop\Container\ContainerInterface + */ + protected $ci; + + /** + * The callbacks that should be run after the application is created. + * + * @var array + */ + protected $afterApplicationCreatedCallbacks = []; + + /** + * The callbacks that should be run before the application is destroyed. + * + * @var array + */ + protected $beforeApplicationDestroyedCallbacks = []; + + /** + * Indicates if we have made it through the base setUp function. + * + * @var bool + */ + protected $setUpHasRun = false; + + /** + * Setup the test environment. + * + * @return void + */ + protected function setUp() + { + if (!$this->app) { + $this->refreshApplication(); + } + + $this->setUpTraits(); + + foreach ($this->afterApplicationCreatedCallbacks as $callback) { + call_user_func($callback); + } + + $this->setUpHasRun = true; + } + + /** + * Boot the testing helper traits. + * + * @return void + */ + protected function setUpTraits() + { + $uses = array_flip(class_uses_recursive(static::class)); + + /*if (isset($uses[DatabaseMigrations::class])) { + $this->runDatabaseMigrations(); + }*/ + + if (isset($uses[DatabaseTransactions::class])) { + $this->beginDatabaseTransaction(); + } + } + + /** + * Refresh the application instance. + * + * @return void + */ + protected function refreshApplication() + { + // Force setting UF_MODE. This is needed at the moment because Bakery + // uses passthru to invoke PHPUnit. This means that the value of UF_MODE + // has already been set when Bakery was set up, and PHPUnit < 6.3 has + // no way to override environment vars that have already been set. + putenv('UF_MODE=testing'); + + // Setup the sprinkles + $uf = new UserFrosting(); + + // Set argument as false, we are using the CLI + $uf->setupSprinkles(false); + + // Get the container + $this->ci = $uf->getContainer(); + + // Next, we'll instantiate the application. Note that the application is required for the SprinkleManager to set up routes. + $this->app = new App($this->ci); + } + + /** + * Clean up the testing environment before the next test. + * + * @return void + */ + protected function tearDown() + { + if ($this->app) { + foreach ($this->beforeApplicationDestroyedCallbacks as $callback) { + call_user_func($callback); + } + + $this->app = null; + $this->ci = null; + } + + $this->setUpHasRun = false; + + $this->afterApplicationCreatedCallbacks = []; + $this->beforeApplicationDestroyedCallbacks = []; + } + + /** + * Register a callback to be run after the application is created. + * + * @param callable $callback + * @return void + */ + public function afterApplicationCreated(callable $callback) + { + $this->afterApplicationCreatedCallbacks[] = $callback; + + if ($this->setUpHasRun) { + call_user_func($callback); + } + } + + /** + * Asserts that collections are equivalent. + * + * @param array $expected + * @param array $actual + * @param string $message + * @throws PHPUnit_Framework_AssertionFailedError + */ + public static function assertCollectionsSame($expected, $actual, $key = 'id', $message = '') + { + // Check that they have the same number of items + static::assertEquals(count($expected), count($actual)); + + // Sort by primary key + $expected = collect($expected)->sortBy($key); + $actual = collect($actual)->sortBy($key); + + // Check that the keys match + $expectedKeys = $expected->keys()->all(); + $actualKeys = $actual->keys()->all(); + static::assertEquals(sort($expectedKeys), sort($actualKeys)); + + // Check that the array representations of each collection item match + $expected = $expected->values(); + $actual = $actual->values(); + for ($i = 0; $i < count($expected); $i++) { + static::assertEquals( + static::castToComparable($expected[$i]), + static::castToComparable($actual[$i]) + ); + } + } + + /** + * Register a callback to be run before the application is destroyed. + * + * @param callable $callback + * @return void + */ + protected function beforeApplicationDestroyed(callable $callback) + { + $this->beforeApplicationDestroyedCallbacks[] = $callback; + } + + /** + * Helpers + */ + + /** + * Cast an item to an array if it has a toArray() method. + * + * @param $item Collection|mixed[]|mixed + * @return mixed|mixed[] + */ + protected static function castToComparable($item) + { + return (is_object($item) && method_exists($item, 'toArray')) ? $item->toArray() : $item; + } + + /** + * Remove all relations on a collection of models. + */ + protected static function ignoreRelations($models) + { + foreach ($models as $model) { + $model->setRelations([]); + } + } + + protected function cloneObjectArray($original) + { + $cloned = []; + + foreach ($original as $k => $v) { + $cloned[$k] = clone $v; + } + + return $cloned; + } +}
\ No newline at end of file diff --git a/login/app/tests/Unit/ExampleTest.php b/login/app/tests/Unit/ExampleTest.php new file mode 100755 index 0000000..4ac3e84 --- /dev/null +++ b/login/app/tests/Unit/ExampleTest.php @@ -0,0 +1,19 @@ +<?php + +namespace UserFrosting\Tests\Unit; + +use UserFrosting\Tests\TestCase; +use UserFrosting\Tests\DatabaseTransactions; + +class ExampleTest extends TestCase +{ + /** + * A basic test example. + * + * @return void + */ + public function testBasicTest() + { + $this->assertTrue(true); + } +}
\ No newline at end of file diff --git a/login/bakery b/login/bakery new file mode 100755 index 0000000..4d0348d --- /dev/null +++ b/login/bakery @@ -0,0 +1,34 @@ +#!/usr/bin/env php +<?php + +/** + * _ _ ______ _ _ + * | | | | | ___| | | (_) + * | | | |___ ___ _ __| |_ _ __ ___ ___| |_ _ _ __ __ _ + * | | | / __|/ _ \ '__| _| '__/ _ \/ __| __| | '_ \ / _` | + * | |_| \__ \ __/ | | | | | | (_) \__ \ |_| | | | | (_| | + * \___/|___/\___|_| \_| |_| \___/|___/\__|_|_| |_|\__, | + * __/ | + * |___/ + * http://www.userfrosting.com + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ + +/** + * Require composer autoload file. Not having this file means Composer might not be installed / run + */ +if (!file_exists(__DIR__ . '/app/vendor/autoload.php')) { + die("ERROR :: File `".__DIR__."/app/vendor/autoload.php` not found. This indicate that composer has not yet been run on this install. Install composer and run `composer install` from the project root directory. Check the documentation for more details.\n"); +} else { + require_once __DIR__ . '/app/vendor/autoload.php'; +} + +use UserFrosting\System\Bakery\Bakery; + +/** + * Get and run CLI App + */ +$bakery = new Bakery(); +$bakery->run();
\ No newline at end of file diff --git a/login/build/before_install.sh b/login/build/before_install.sh new file mode 100755 index 0000000..37d7524 --- /dev/null +++ b/login/build/before_install.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# +# Inspired by ownCloud´s before_install.sh script <3 +# + +WORKDIR=$PWD +DB=$1 + +echo "Work directory: $WORKDIR" +echo "Database: $DB" + +# +# set up mysql +# +if [ "$DB" == "mysql" ] ; then + echo "Setting up mysql ..." + mysql -u root -e "CREATE DATABASE userfrosting;" + mysql -u root -e "GRANT ALL ON userfrosting.* TO 'travis'@'localhost';" + printf "UF_MODE=\"dev\"\nDB_DRIVER=\"mysql\"\nDB_HOST=\"localhost\"\nDB_PORT=\"3306\"\nDB_NAME=\"userfrosting\"\nDB_USER=\"travis\"\nDB_PASSWORD=\"\"\n" > app/.env +fi + +# +# set up pgsql +# +if [ "$DB" == "pgsql" ] ; then + echo "Setting up pgsql ..." + psql -c "CREATE DATABASE userfrosting;" -U postgres + psql -c "GRANT ALL PRIVILEGES ON DATABASE userfrosting TO postgres;" -U postgres + printf "UF_MODE=\"dev\"\nDB_DRIVER=\"pgsql\"\nDB_HOST=\"localhost\"\nDB_PORT=\"5432\"\nDB_NAME=\"userfrosting\"\nDB_USER=\"postgres\"\nDB_PASSWORD=\"\"\n" > app/.env +fi + +# +# set up sqlite +# +if [ "$DB" == "sqlite" ] ; then + echo "Setting up sqlite ..." + touch userfrosting.db + printf "UF_MODE=\"dev\"\nDB_DRIVER=\"sqlite\"\nDB_NAME=\"userfrosting.db\"\n" > app/.env +fi diff --git a/login/build/gulpfile.js b/login/build/gulpfile.js new file mode 100755 index 0000000..72cd764 --- /dev/null +++ b/login/build/gulpfile.js @@ -0,0 +1,183 @@ +/* To build bundles... + 1. npm run uf-bundle-build + 2. npm run uf-bundle + 3. npm run uf-bundle-clean + + To get frontend vendor packages via bower + 1. npm run uf-assets-install + + To clean frontend vendor assets + 1. npm run uf-assets-clean +*/ +"use strict"; +const gulp = require('gulp'); +const del = require('del'); +const fs = require('fs'); +const shell = require('shelljs'); +const plugins = require('gulp-load-plugins')(); + +const sprinklesDir = '../app/sprinkles/'; + +const sprinklesSchemaPath = '../app/sprinkles.json'; + +// The Sprinkle load order from sprinkles.json +const sprinkles = ['core'].concat(require(`${sprinklesSchemaPath}`)['base']); + +// The directory where the bundle task should place compiled assets. +// The names of assets in bundle.result.json will be specified relative to this path. +const publicAssetsDir = '../public/assets/'; + +// name of the bundle file +const bundleFile = 'asset-bundles.json'; + +// Compiled bundle config file +const bundleConfigFile = `./${bundleFile}`; + +// Destination directory for vendor assets +const vendorAssetsDir = './assets/vendor'; + +/** + * Vendor asset task + */ + +// Gulp task to install vendor packages via bower +gulp.task('bower-install', () => { + "use strict"; + shell.cd(`${sprinklesDir}`); + sprinkles.forEach((sprinkle) => { + if (fs.existsSync(`${sprinkle}/bower.json`)) { + console.log(`bower.json found in '${sprinkle}' Sprinkle, installing assets.`); + shell.cd(`${sprinkle}`); + shell.exec(`bower install --config.directory=${vendorAssetsDir}`); + shell.cd(`../`); + } + }); + + return true; +}); + +/** + * Bundling tasks + */ + +// Executes bundleing tasks according to bundle.config.json files in each Sprinkle, as per Sprinkle load order. +// Respects bundle collision rules. +gulp.task('bundle-build', () => { + "use strict"; + let copy = require('recursive-copy'); + let merge = require('merge-array-object'); + let cleanup = (e) => { + "use strict"; + // Delete temporary directory if exists + fs.rmdirSync("./temp"); + // Delete created bundle.config.json file + fs.unlinkSync(bundleConfigFile); + // Propagate error + throw e; + }; + let config = { + bundle: {}, + copy: [] + }; + sprinkles.forEach((sprinkle) => { + "use strict"; + let location = `${sprinklesDir}/${sprinkle}/`; + if (fs.existsSync(`${location}${bundleFile}`)) { + let currentConfig = require(`${location}${bundleFile}`); + // Add bundles to config, respecting collision rules. + for (let bundleName in currentConfig.bundle) { + // If bundle already defined, handle as per collision rules. + if (bundleName in config.bundle) { + let onCollision = 'replace'; + try { + onCollision = (typeof currentConfig.bundle[bundleName].options.sprinkle.onCollision !== 'undefined' ? currentConfig.bundle[bundleName].options.sprinkle.onCollision : 'replace'); + } + catch (e) { + + } + switch (onCollision) { + case 'replace': + config.bundle[bundleName] = currentConfig.bundle[bundleName]; + break; + case 'merge': + // If using this collision rule, keep in mind any bundling options will also be merged. + // Inspect the produced 'bundle.config.json' file in the 'build' folder to ensure options are correct. + config.bundle[bundleName] = merge(currentConfig.bundle[bundleName], config.bundle[bundleName]); + break; + case 'ignore': + // Do nothing. This simply exists to prevent falling through to error catchment. + break; + case 'error': + cleanup(`The bundle '${bundleName}' in the Sprinkle '${sprinkle}' has been previously defined, and the bundle's 'onCollision' property is set to 'error'.`); + default: + cleanup(`Unexpected input '${onCollision}' for 'onCollision' for the bundle '${bundleName}' in the Sprinkle '${sprinkle}'.`); + } + } + // Otherwise, just add. + else { + config.bundle[bundleName] = currentConfig.bundle[bundleName]; + } + } + // Add/merge copy files to config + if ('copy' in currentConfig) { + config.copy = new Set(config.copy, currentConfig.copy); + } + } + }); + // Save bundle rules to bundle.config.json + fs.writeFileSync(bundleConfigFile, JSON.stringify(config)); + + // Copy all assets in order of Sprinkle load order. + let sprinkleAssets = [] + sprinkles.forEach((sprinkle) => { + "use strict"; + sprinkleAssets.push(`../app/sprinkles/${sprinkle}/assets/**/*`); + }); + // Gulp v4 src respects order, until it is released, use this alternative. + return plugins.srcOrderedGlobs(sprinkleAssets) + .pipe(plugins.copy('../public/assets/', {prefix: 5}));// And gulp.dest doesn't give us the control needed. +}); + +// Execute gulp-bundle-assets +gulp.task('bundle', () => { + "use strict"; + return gulp.src(bundleConfigFile) + .pipe(plugins.ufBundleAssets({ + base: publicAssetsDir + })) + .pipe(plugins.ufBundleAssets.results({ + dest: './' + })) + .pipe(gulp.dest(publicAssetsDir)); +}); + +/** + * Clean up tasks + */ + +gulp.task('public-clean', () => { + "use strict"; + return del(publicAssetsDir, { force: true }); +}); + +// Clean up temporary bundling files +gulp.task('bundle-clean', () => { + "use strict"; + return del(bundleConfigFile, { force: true }); +}); + +// Deletes assets fetched by bower-install +gulp.task('bower-clean', () => { + "use strict"; + return del(`${sprinklesDir}/*/assets/vendor`, { force: true }); +}); + +// Deletes all generated, or acquired files. +gulp.task('clean', () => { + "use strict"; + del(publicAssetsDir, { force: true}); + del(bundleConfigFile, { force: true }); + del(`${sprinklesDir}/*/assets/vendor`, { force: true }); + + return true; +})
\ No newline at end of file diff --git a/login/build/package-lock.json b/login/build/package-lock.json new file mode 100755 index 0000000..88b110e --- /dev/null +++ b/login/build/package-lock.json @@ -0,0 +1,5512 @@ +{ + "name": "@userfrosting/userfrosting", + "version": "4.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@gulp-sourcemaps/map-sources": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz", + "integrity": "sha1-iQrnxdjId/bThIYCFazp1+yUW9o=", + "dev": true, + "requires": { + "normalize-path": "2.1.1", + "through2": "2.0.3" + } + }, + "accord": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/accord/-/accord-0.28.0.tgz", + "integrity": "sha512-sPF34gqHegaCSryKf5wHJ8wREK1dTZnHmC9hsB7D8xjntRdd30DXDPKf0YVIcSvnXJmcYu5SCvZRz28H++kFhQ==", + "dev": true, + "requires": { + "convert-source-map": "1.5.1", + "glob": "7.1.2", + "indx": "0.2.3", + "lodash.clone": "4.5.0", + "lodash.defaults": "4.2.0", + "lodash.flatten": "4.4.0", + "lodash.merge": "4.6.1", + "lodash.partialright": "4.2.1", + "lodash.pick": "4.4.0", + "lodash.uniq": "4.5.0", + "resolve": "1.7.1", + "semver": "5.5.0", + "uglify-js": "2.8.29", + "when": "3.7.8" + }, + "dependencies": { + "convert-source-map": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", + "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=", + "dev": true + }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=", + "dev": true + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + } + } + }, + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=", + "dev": true + }, + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dev": true, + "optional": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dev": true, + "requires": { + "kind-of": "3.2.2", + "longest": "1.0.1", + "repeat-string": "1.6.1" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "ansi-cyan": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz", + "integrity": "sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-gray": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", + "integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-red": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", + "integrity": "sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "ansi-wrap": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", + "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", + "dev": true + }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-differ": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", + "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=", + "dev": true + }, + "array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", + "dev": true + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", + "dev": true + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "1.0.3" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", + "dev": true + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", + "dev": true, + "optional": true + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", + "dev": true, + "optional": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true, + "optional": true + }, + "atob": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.0.tgz", + "integrity": "sha512-SuiKH8vbsOyCALjA/+EINmt/Kdl+TQPrtFgW7XZZcwtryFu9e5kQoX3bjCW6mIvGH1fbeAZZuvwGR5IlBRznGw==", + "dev": true + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", + "dev": true, + "optional": true + }, + "aws4": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", + "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==", + "dev": true, + "optional": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "1.0.1", + "class-utils": "0.3.6", + "component-emitter": "1.2.1", + "define-property": "1.0.0", + "isobject": "3.0.1", + "mixin-deep": "1.3.1", + "pascalcase": "0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + } + } + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "beeper": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/beeper/-/beeper-1.1.1.tgz", + "integrity": "sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak=", + "dev": true + }, + "bluebird": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==", + "dev": true + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "bower": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/bower/-/bower-1.8.4.tgz", + "integrity": "sha1-54dqB23rgTf30GUl3F6MZtuC8oo=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "1.1.0", + "array-unique": "0.3.2", + "extend-shallow": "2.0.1", + "fill-range": "4.0.0", + "isobject": "3.0.1", + "repeat-element": "1.1.2", + "snapdragon": "0.8.2", + "snapdragon-node": "2.1.1", + "split-string": "3.1.0", + "to-regex": "3.0.2" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "1.0.0", + "component-emitter": "1.2.1", + "get-value": "2.0.6", + "has-value": "1.0.0", + "isobject": "3.0.1", + "set-value": "2.0.0", + "to-object-path": "0.3.0", + "union-value": "1.0.0", + "unset-value": "1.0.0" + } + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "2.1.1", + "map-obj": "1.0.1" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + } + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true, + "optional": true + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dev": true, + "requires": { + "align-text": "0.1.4", + "lazy-cache": "1.0.4" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "3.1.0", + "define-property": "0.2.5", + "isobject": "3.0.1", + "static-extend": "0.1.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + } + } + }, + "clean-css": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.1.11.tgz", + "integrity": "sha1-Ls3xRaujj1R0DybO/Q/z4D4SXWo=", + "dev": true, + "requires": { + "source-map": "0.5.7" + } + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "requires": { + "center-align": "0.1.3", + "right-align": "0.1.3", + "wordwrap": "0.0.2" + } + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true + }, + "clone-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", + "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=", + "dev": true + }, + "clone-stats": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", + "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=", + "dev": true + }, + "cloneable-readable": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.2.tgz", + "integrity": "sha512-Bq6+4t+lbM8vhTs/Bef5c5AdEMtapp/iFb6+s4/Hh9MVTt8OLKH7ZOOZSCT+Ys7hsHvqv0GuMPJ1lnQJVHvxpg==", + "dev": true, + "requires": { + "inherits": "2.0.3", + "process-nextick-args": "2.0.0", + "readable-stream": "2.3.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.1", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + } + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true, + "optional": true + }, + "coffeescript": { + "version": "1.12.7", + "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-1.12.7.tgz", + "integrity": "sha512-pLXHFxQMPklVoEekowk8b3erNynC+DVJzChxS/LCBBgR6/8AJkHivkm//zbowcfc7BTCAjryuhx6gPqPRfsFoA==", + "dev": true + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "1.0.0", + "object-visit": "1.0.1" + } + }, + "color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "dev": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-with-sourcemaps": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.0.5.tgz", + "integrity": "sha512-YtnS0VEY+e2Khzsey/6mra9EoM6h/5gxaC0e3mcHpA5yfDxafhygytNmcJWodvUgyXzSiL5MSkPO6bQGgfliHw==", + "dev": true, + "requires": { + "source-map": "0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "convert-source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-0.3.5.tgz", + "integrity": "sha1-8dgClQr33SYxof6+BZZVDIarMZA=", + "dev": true + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "dev": true, + "optional": true, + "requires": { + "boom": "2.10.1" + } + }, + "css": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css/-/css-2.2.1.tgz", + "integrity": "sha1-c6TIHehdtmTU7mdPfUcIXjstVdw=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "source-map": "0.1.43", + "source-map-resolve": "0.3.1", + "urix": "0.1.0" + }, + "dependencies": { + "atob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/atob/-/atob-1.1.3.tgz", + "integrity": "sha1-lfE2KbEsOlGl0hWr3OKqnzL4B3M=", + "dev": true + }, + "source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + }, + "source-map-resolve": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.3.1.tgz", + "integrity": "sha1-YQ9hIqRFuN1RU1oqcbeD38Ekh2E=", + "dev": true, + "requires": { + "atob": "1.1.3", + "resolve-url": "0.2.1", + "source-map-url": "0.3.0", + "urix": "0.1.0" + } + }, + "source-map-url": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.3.0.tgz", + "integrity": "sha1-fsrxO1e80J2opAxdJp2zN5nUqvk=", + "dev": true + } + } + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "1.0.2" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "dateformat": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz", + "integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI=", + "dev": true + }, + "deap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deap/-/deap-1.0.1.tgz", + "integrity": "sha512-k75KYNZMvwAwes2xIPry/QTffXIchjD8QfABvvfTr80P85jv5ZcKqcoDo+vMe71nNnVnXYe8MA28weyqcf/DKw==", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "debug-fabulous": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/debug-fabulous/-/debug-fabulous-0.0.4.tgz", + "integrity": "sha1-+gccXYdIRoVCSAdCHKSxawsaB2M=", + "dev": true, + "requires": { + "debug": "2.6.9", + "lazy-debug-legacy": "0.0.1", + "object-assign": "4.1.0" + }, + "dependencies": { + "object-assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz", + "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A=", + "dev": true + } + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "dev": true, + "requires": { + "clone": "1.0.4" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "1.0.2", + "isobject": "3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + } + } + }, + "del": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "dev": true, + "requires": { + "globby": "5.0.0", + "is-path-cwd": "1.0.0", + "is-path-in-cwd": "1.0.1", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "rimraf": "2.6.2" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "deprecated": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/deprecated/-/deprecated-0.0.1.tgz", + "integrity": "sha1-+cmvVGSvoeepcUWKi97yqpTVuxk=", + "dev": true + }, + "detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", + "dev": true + }, + "detect-newline": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", + "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", + "dev": true + }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", + "dev": true + }, + "duplexer2": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", + "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=", + "dev": true, + "requires": { + "readable-stream": "1.1.14" + } + }, + "duplexify": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.4.tgz", + "integrity": "sha512-JzYSLYMhoVVBe8+mbHQ4KgpvHpm0DZpJuL8PY93Vyv1fW7jYJ90LoXa1di/CVbJM+TgMs91rbDapE/RNIfnJsA==", + "dev": true, + "requires": { + "end-of-stream": "1.4.1", + "inherits": "2.0.3", + "readable-stream": "2.3.6", + "stream-shift": "1.0.0" + }, + "dependencies": { + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "dev": true, + "requires": { + "once": "1.4.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.1", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + } + } + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "emitter-mixin": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/emitter-mixin/-/emitter-mixin-0.0.3.tgz", + "integrity": "sha1-WUjLKG8uSO3DslGnz8H3iDOW1lw=", + "dev": true + }, + "end-of-stream": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-0.1.5.tgz", + "integrity": "sha1-jhdyBsPICDfYVjLouTWd/osvbq8=", + "dev": true, + "requires": { + "once": "1.3.3" + }, + "dependencies": { + "once": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", + "integrity": "sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + } + } + }, + "errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "dev": true, + "requires": { + "prr": "1.0.1" + } + }, + "error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "dev": true, + "requires": { + "is-arrayish": "0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "posix-character-classes": "0.1.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dev": true, + "requires": { + "fill-range": "2.2.3" + }, + "dependencies": { + "fill-range": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", + "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", + "dev": true, + "requires": { + "is-number": "2.1.0", + "isobject": "2.1.0", + "randomatic": "1.1.7", + "repeat-element": "1.1.2", + "repeat-string": "1.6.1" + } + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "dev": true, + "requires": { + "homedir-polyfill": "1.0.1" + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "1.0.0", + "is-extendable": "1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "0.3.2", + "define-property": "1.0.0", + "expand-brackets": "2.1.4", + "extend-shallow": "2.0.1", + "fragment-cache": "0.2.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fancy-log": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.2.tgz", + "integrity": "sha1-9BEl49hPLn2JpD0G2VjI94vha+E=", + "dev": true, + "requires": { + "ansi-gray": "0.1.1", + "color-support": "1.1.3", + "time-stamp": "1.1.0" + } + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "dev": true + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-number": "3.0.0", + "repeat-string": "1.6.1", + "to-regex-range": "2.1.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "find-index": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/find-index/-/find-index-0.1.1.tgz", + "integrity": "sha1-Z101iyyjiS15Whq0cjL4tuLg3eQ=", + "dev": true + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + }, + "findup-sync": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", + "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", + "dev": true, + "requires": { + "detect-file": "1.0.0", + "is-glob": "3.1.0", + "micromatch": "3.1.10", + "resolve-dir": "1.0.1" + } + }, + "fined": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.1.0.tgz", + "integrity": "sha1-s33IRLdqL15wgeiE98CuNE8VNHY=", + "dev": true, + "requires": { + "expand-tilde": "2.0.2", + "is-plain-object": "2.0.4", + "object.defaults": "1.1.0", + "object.pick": "1.3.0", + "parse-filepath": "1.0.2" + } + }, + "first-chunk-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-1.0.0.tgz", + "integrity": "sha1-Wb+1DNkF9g18OUzT2ayqtOatk04=", + "dev": true + }, + "flagged-respawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.0.tgz", + "integrity": "sha1-Tnmumy6zi/hrO7Vr8+ClaqX8q9c=", + "dev": true + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "dev": true, + "requires": { + "for-in": "1.0.2" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true, + "optional": true + }, + "fork-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/fork-stream/-/fork-stream-0.0.4.tgz", + "integrity": "sha1-24Sfznf2cIpfjzhq5TOgkHtUrnA=", + "dev": true + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "dev": true, + "optional": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.6", + "mime-types": "2.1.18" + } + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "0.2.2" + } + }, + "fs-exists-sync": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", + "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "gaze": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-0.5.2.tgz", + "integrity": "sha1-QLcJU30k0dRXZ9takIaJ3+aaxE8=", + "dev": true, + "requires": { + "globule": "0.1.0" + } + }, + "get-imports": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-imports/-/get-imports-1.0.0.tgz", + "integrity": "sha1-R8C07piTUWQsVJdxk79Pyqv1N48=", + "dev": true, + "requires": { + "array-uniq": "1.0.3", + "import-regex": "1.1.0" + } + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true, + "requires": { + "glob-parent": "2.0.0", + "is-glob": "2.0.1" + }, + "dependencies": { + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + } + } + }, + "glob-manipulate": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/glob-manipulate/-/glob-manipulate-1.1.1.tgz", + "integrity": "sha1-1oYRJd+5QGcBRxlrOeJqy3h2oeE=", + "dev": true + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "2.0.1" + }, + "dependencies": { + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + } + } + }, + "glob-stream": { + "version": "3.1.18", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-3.1.18.tgz", + "integrity": "sha1-kXCl8St5Awb9/lmPMT+PeVT9FDs=", + "dev": true, + "requires": { + "glob": "4.5.3", + "glob2base": "0.0.12", + "minimatch": "2.0.10", + "ordered-read-streams": "0.1.0", + "through2": "0.6.5", + "unique-stream": "1.0.0" + }, + "dependencies": { + "glob": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-4.5.3.tgz", + "integrity": "sha1-xstz0yJsHv7wTePFbQEvAzd+4V8=", + "dev": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "2.0.10", + "once": "1.4.0" + } + }, + "minimatch": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz", + "integrity": "sha1-jQh8OcazjAAbl/ynzm0OHoCvusc=", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true, + "requires": { + "readable-stream": "1.0.34", + "xtend": "4.0.1" + } + } + } + }, + "glob-watcher": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-0.0.6.tgz", + "integrity": "sha1-uVtKjfdLOcgymLDAXJeLTZo7cQs=", + "dev": true, + "requires": { + "gaze": "0.5.2" + } + }, + "glob2base": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/glob2base/-/glob2base-0.0.12.tgz", + "integrity": "sha1-nUGbPijxLoOjYhZKJ3BVkiycDVY=", + "dev": true, + "requires": { + "find-index": "0.1.1" + } + }, + "global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "requires": { + "global-prefix": "1.0.2", + "is-windows": "1.0.2", + "resolve-dir": "1.0.1" + } + }, + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "dev": true, + "requires": { + "expand-tilde": "2.0.2", + "homedir-polyfill": "1.0.1", + "ini": "1.3.5", + "is-windows": "1.0.2", + "which": "1.3.0" + } + }, + "globby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "dev": true, + "requires": { + "array-union": "1.0.2", + "arrify": "1.0.1", + "glob": "7.1.2", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "globule": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globule/-/globule-0.1.0.tgz", + "integrity": "sha1-2cjt3h2nnRJaFRt5UzuXhnY0auU=", + "dev": true, + "requires": { + "glob": "3.1.21", + "lodash": "1.0.2", + "minimatch": "0.2.14" + }, + "dependencies": { + "glob": { + "version": "3.1.21", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", + "integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=", + "dev": true, + "requires": { + "graceful-fs": "1.2.3", + "inherits": "1.0.2", + "minimatch": "0.2.14" + } + }, + "graceful-fs": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", + "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=", + "dev": true + }, + "inherits": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", + "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=", + "dev": true + }, + "minimatch": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "dev": true, + "requires": { + "lru-cache": "2.7.3", + "sigmund": "1.0.1" + } + } + } + }, + "glogg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.1.tgz", + "integrity": "sha512-ynYqXLoluBKf9XGR1gA59yEJisIL7YHEH4xr3ZziHB5/yl4qWfaK8Js9jGe6gBGCSCKVqiyO30WnRZADvemUNw==", + "dev": true, + "requires": { + "sparkles": "1.0.0" + } + }, + "graceful-fs": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.11.tgz", + "integrity": "sha1-dhPHeKGv6mLyXGMKCG1/Osu92Bg=", + "dev": true, + "requires": { + "natives": "1.1.3" + } + }, + "gulp": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/gulp/-/gulp-3.9.1.tgz", + "integrity": "sha1-VxzkWSjdQK9lFPxAEYZgFsE4RbQ=", + "dev": true, + "requires": { + "archy": "1.0.0", + "chalk": "1.1.3", + "deprecated": "0.0.1", + "gulp-util": "3.0.8", + "interpret": "1.1.0", + "liftoff": "2.5.0", + "minimist": "1.2.0", + "orchestrator": "0.3.8", + "pretty-hrtime": "1.0.3", + "semver": "4.3.6", + "tildify": "1.2.0", + "v8flags": "2.1.1", + "vinyl-fs": "0.3.14" + } + }, + "gulp-clean-css": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/gulp-clean-css/-/gulp-clean-css-2.4.0.tgz", + "integrity": "sha1-KuSBCf6DzMln/1rVPARJSaSGOzY=", + "dev": true, + "requires": { + "clean-css": "4.1.11", + "gulp-util": "3.0.8", + "object-assign": "4.1.1", + "through2": "2.0.3", + "vinyl-sourcemaps-apply": "0.2.1" + } + }, + "gulp-coffee": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/gulp-coffee/-/gulp-coffee-2.3.5.tgz", + "integrity": "sha512-PbgPGZVyYFnBTYtfYkVN6jcK8Qsuh3BxycPzvu8y5lZroCw3/x1m25KeyEDX110KsVLDmJxoULjscR21VEN4wA==", + "dev": true, + "requires": { + "coffeescript": "1.12.7", + "gulp-util": "3.0.8", + "merge": "1.2.0", + "through2": "2.0.3", + "vinyl-sourcemaps-apply": "0.2.1" + } + }, + "gulp-concat": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/gulp-concat/-/gulp-concat-2.6.1.tgz", + "integrity": "sha1-Yz0WyV2IUEYorQJmVmPO5aR5M1M=", + "dev": true, + "requires": { + "concat-with-sourcemaps": "1.0.5", + "through2": "2.0.3", + "vinyl": "2.1.0" + }, + "dependencies": { + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "dev": true + }, + "clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", + "dev": true + }, + "replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", + "dev": true + }, + "vinyl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.1.0.tgz", + "integrity": "sha1-Ah+cLPlR1rk5lDyJ617lrdT9kkw=", + "dev": true, + "requires": { + "clone": "2.1.2", + "clone-buffer": "1.0.0", + "clone-stats": "1.0.0", + "cloneable-readable": "1.1.2", + "remove-trailing-separator": "1.1.0", + "replace-ext": "1.0.0" + } + } + } + }, + "gulp-concat-css": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/gulp-concat-css/-/gulp-concat-css-2.3.0.tgz", + "integrity": "sha1-TBWGEhqEEf9LLcRPz6TcdA6P4bY=", + "dev": true, + "requires": { + "gulp-util": "3.0.8", + "lodash.defaults": "3.1.2", + "parse-import": "2.0.0", + "rework": "1.0.1", + "rework-import": "2.1.0", + "rework-plugin-url": "1.1.0", + "through2": "1.1.1" + }, + "dependencies": { + "through2": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-1.1.1.tgz", + "integrity": "sha1-CEfLxESfNAVXTb3M2buEG4OsNUU=", + "dev": true, + "requires": { + "readable-stream": "1.1.14", + "xtend": "4.0.1" + } + } + } + }, + "gulp-copy": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/gulp-copy/-/gulp-copy-1.1.0.tgz", + "integrity": "sha512-OM+6ICtL2nYpzXF/qZb0eLDrIRe28MnN548Dsc60okV/evNbo0552VjtZ317x3MnCh5mGZDcTGK/hX4OwbE7xw==", + "dev": true, + "requires": { + "gulp": "3.9.1", + "plugin-error": "0.1.2", + "through2": "2.0.3" + } + }, + "gulp-if": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/gulp-if/-/gulp-if-2.0.2.tgz", + "integrity": "sha1-pJe351cwBQQcqivIt92jyARE1ik=", + "dev": true, + "requires": { + "gulp-match": "1.0.3", + "ternary-stream": "2.0.1", + "through2": "2.0.3" + } + }, + "gulp-less": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/gulp-less/-/gulp-less-3.5.0.tgz", + "integrity": "sha512-FQLY7unaHdTOXG0jlwxeBQcWoPPrTMQZRA7HfYwSNi9IPVx5l7GJEN72mG4ri2yigp/f/VNGUAJnFMJHBmH3iw==", + "dev": true, + "requires": { + "accord": "0.28.0", + "less": "2.7.3", + "object-assign": "4.1.1", + "plugin-error": "0.1.2", + "replace-ext": "1.0.0", + "through2": "2.0.3", + "vinyl-sourcemaps-apply": "0.2.1" + }, + "dependencies": { + "replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", + "dev": true + } + } + }, + "gulp-load-plugins": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/gulp-load-plugins/-/gulp-load-plugins-1.5.0.tgz", + "integrity": "sha1-TEGffldk2aDjMGG6uWGPgbc9QXE=", + "dev": true, + "requires": { + "array-unique": "0.2.1", + "fancy-log": "1.3.2", + "findup-sync": "0.4.3", + "gulplog": "1.0.0", + "has-gulplog": "0.1.0", + "micromatch": "2.3.11", + "resolve": "1.7.1" + }, + "dependencies": { + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "1.1.0" + } + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.2" + } + }, + "detect-file": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-0.1.0.tgz", + "integrity": "sha1-STXe39lIhkjgBrASlWbpOGcR6mM=", + "dev": true, + "requires": { + "fs-exists-sync": "0.1.0" + } + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "0.1.1" + } + }, + "expand-tilde": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-1.2.2.tgz", + "integrity": "sha1-C4HrqJflo9MdHD0QL48BRB5VlEk=", + "dev": true, + "requires": { + "os-homedir": "1.0.2" + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "findup-sync": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.4.3.tgz", + "integrity": "sha1-QAQ5Kee8YK3wt/SCfExudaDeyhI=", + "dev": true, + "requires": { + "detect-file": "0.1.0", + "is-glob": "2.0.1", + "micromatch": "2.3.11", + "resolve-dir": "0.1.1" + } + }, + "global-modules": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz", + "integrity": "sha1-6lo77ULG1s6ZWk+KEmm12uIjgo0=", + "dev": true, + "requires": { + "global-prefix": "0.1.5", + "is-windows": "0.2.0" + } + }, + "global-prefix": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-0.1.5.tgz", + "integrity": "sha1-jTvGuNo8qBEqFg2NSW/wRiv+948=", + "dev": true, + "requires": { + "homedir-polyfill": "1.0.1", + "ini": "1.3.5", + "is-windows": "0.2.0", + "which": "1.3.0" + } + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "is-windows": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", + "integrity": "sha1-3hqm1j6indJIc3tp8f+LgALSEIw=", + "dev": true + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "2.0.0", + "array-unique": "0.2.1", + "braces": "1.8.5", + "expand-brackets": "0.1.5", + "extglob": "0.3.2", + "filename-regex": "2.0.1", + "is-extglob": "1.0.0", + "is-glob": "2.0.1", + "kind-of": "3.2.2", + "normalize-path": "2.1.1", + "object.omit": "2.0.1", + "parse-glob": "3.0.4", + "regex-cache": "0.4.4" + } + }, + "resolve-dir": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-0.1.1.tgz", + "integrity": "sha1-shklmlYC+sXFxJatiUpujMQwJh4=", + "dev": true, + "requires": { + "expand-tilde": "1.2.2", + "global-modules": "0.2.3" + } + } + } + }, + "gulp-match": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/gulp-match/-/gulp-match-1.0.3.tgz", + "integrity": "sha1-kcfA1/Kb7NZgbVfYCn+Hdqh6uo4=", + "dev": true, + "requires": { + "minimatch": "3.0.4" + } + }, + "gulp-order": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/gulp-order/-/gulp-order-1.1.1.tgz", + "integrity": "sha1-C47wgzI1vtZfHvvHnGrtl7HbQek=", + "dev": true, + "requires": { + "minimatch": "0.2.14", + "through": "2.3.8" + }, + "dependencies": { + "minimatch": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "dev": true, + "requires": { + "lru-cache": "2.7.3", + "sigmund": "1.0.1" + } + } + } + }, + "gulp-rev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/gulp-rev/-/gulp-rev-4.0.0.tgz", + "integrity": "sha1-Zj2RhOdyJHsvJcSk5XE4h0LlXuk=", + "dev": true, + "requires": { + "gulp-util": "3.0.8", + "object-assign": "2.1.1", + "rev-hash": "1.0.0", + "rev-path": "1.0.0", + "through2": "0.6.5", + "vinyl-file": "1.3.0" + }, + "dependencies": { + "object-assign": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-2.1.1.tgz", + "integrity": "sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo=", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true, + "requires": { + "readable-stream": "1.0.34", + "xtend": "4.0.1" + } + } + } + }, + "gulp-sourcemaps": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-1.12.1.tgz", + "integrity": "sha1-tDfR89mAzyboEYSCNxjOFa5ll7Y=", + "dev": true, + "requires": { + "@gulp-sourcemaps/map-sources": "1.0.0", + "acorn": "4.0.13", + "convert-source-map": "1.5.1", + "css": "2.2.1", + "debug-fabulous": "0.0.4", + "detect-newline": "2.1.0", + "graceful-fs": "4.1.11", + "source-map": "0.6.1", + "strip-bom": "2.0.0", + "through2": "2.0.3", + "vinyl": "1.2.0" + }, + "dependencies": { + "convert-source-map": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", + "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=", + "dev": true + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + }, + "vinyl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", + "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", + "dev": true, + "requires": { + "clone": "1.0.4", + "clone-stats": "0.0.1", + "replace-ext": "0.0.1" + } + } + } + }, + "gulp-src-ordered-globs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/gulp-src-ordered-globs/-/gulp-src-ordered-globs-1.0.3.tgz", + "integrity": "sha1-vGQHuw/4/nHABA6wCzJkrbbr2Ko=", + "dev": true, + "requires": { + "glob-manipulate": "1.1.1", + "merge-stream": "0.1.8", + "unique-stream": "2.2.1" + }, + "dependencies": { + "unique-stream": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.2.1.tgz", + "integrity": "sha1-WqADz76Uxf+GbE59ZouxxNuts2k=", + "dev": true, + "requires": { + "json-stable-stringify": "1.0.1", + "through2-filter": "2.0.0" + } + } + } + }, + "gulp-streamify": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/gulp-streamify/-/gulp-streamify-0.0.5.tgz", + "integrity": "sha1-aF0gUSVSrFdlYktBMbSx7K3wDsE=", + "dev": true, + "requires": { + "gulp-util": "2.2.20", + "plexer": "0.0.2", + "readable-stream": "1.1.14" + }, + "dependencies": { + "ansi-regex": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", + "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=", + "dev": true + }, + "ansi-styles": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", + "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=", + "dev": true + }, + "chalk": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", + "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", + "dev": true, + "requires": { + "ansi-styles": "1.1.0", + "escape-string-regexp": "1.0.5", + "has-ansi": "0.1.0", + "strip-ansi": "0.3.0", + "supports-color": "0.2.0" + } + }, + "dateformat": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz", + "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=", + "dev": true, + "requires": { + "get-stdin": "4.0.1", + "meow": "3.7.0" + } + }, + "gulp-util": { + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-2.2.20.tgz", + "integrity": "sha1-1xRuVyiRC9jwR6awseVJvCLb1kw=", + "dev": true, + "requires": { + "chalk": "0.5.1", + "dateformat": "1.0.12", + "lodash._reinterpolate": "2.4.1", + "lodash.template": "2.4.1", + "minimist": "0.2.0", + "multipipe": "0.1.2", + "through2": "0.5.1", + "vinyl": "0.2.3" + } + }, + "has-ansi": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", + "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=", + "dev": true, + "requires": { + "ansi-regex": "0.2.1" + } + }, + "lodash._reinterpolate": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-2.4.1.tgz", + "integrity": "sha1-TxInqlqHEfxjL1sHofRgequLMiI=", + "dev": true + }, + "lodash.defaults": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-2.4.1.tgz", + "integrity": "sha1-p+iIXwXmiFEUS24SqPNngCa8TFQ=", + "dev": true, + "requires": { + "lodash._objecttypes": "2.4.1", + "lodash.keys": "2.4.1" + } + }, + "lodash.escape": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-2.4.1.tgz", + "integrity": "sha1-LOEsXghNsKV92l5dHu659dF1o7Q=", + "dev": true, + "requires": { + "lodash._escapehtmlchar": "2.4.1", + "lodash._reunescapedhtml": "2.4.1", + "lodash.keys": "2.4.1" + } + }, + "lodash.keys": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-2.4.1.tgz", + "integrity": "sha1-SN6kbfj/djKxDXBrissmWR4rNyc=", + "dev": true, + "requires": { + "lodash._isnative": "2.4.1", + "lodash._shimkeys": "2.4.1", + "lodash.isobject": "2.4.1" + } + }, + "lodash.template": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-2.4.1.tgz", + "integrity": "sha1-nmEQB+32KRKal0qzxIuBez4c8g0=", + "dev": true, + "requires": { + "lodash._escapestringchar": "2.4.1", + "lodash._reinterpolate": "2.4.1", + "lodash.defaults": "2.4.1", + "lodash.escape": "2.4.1", + "lodash.keys": "2.4.1", + "lodash.templatesettings": "2.4.1", + "lodash.values": "2.4.1" + } + }, + "lodash.templatesettings": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-2.4.1.tgz", + "integrity": "sha1-6nbHXRHrhtTb6JqDiTu4YZKaxpk=", + "dev": true, + "requires": { + "lodash._reinterpolate": "2.4.1", + "lodash.escape": "2.4.1" + } + }, + "minimist": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.2.0.tgz", + "integrity": "sha1-Tf/lJdriuGTGbC4jxicdev3s784=", + "dev": true + }, + "strip-ansi": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", + "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", + "dev": true, + "requires": { + "ansi-regex": "0.2.1" + } + }, + "supports-color": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", + "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=", + "dev": true + }, + "through2": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.5.1.tgz", + "integrity": "sha1-390BLrnHAOIyP9M084rGIqs3Lac=", + "dev": true, + "requires": { + "readable-stream": "1.0.34", + "xtend": "3.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + } + } + }, + "vinyl": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.2.3.tgz", + "integrity": "sha1-vKk4IJWC7FpJrVOKAPofEl5RMlI=", + "dev": true, + "requires": { + "clone-stats": "0.0.1" + } + }, + "xtend": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", + "integrity": "sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=", + "dev": true + } + } + }, + "gulp-uf-bundle-assets": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/gulp-uf-bundle-assets/-/gulp-uf-bundle-assets-2.28.0.tgz", + "integrity": "sha512-I34CrizllbYy6adFPYcuG7FUynSFHF6KPDcEIgNQ6qYO8BvVtuMV4ndjU/e1nfPGTf8HGv0OCvu1pSB4lhjlMA==", + "dev": true, + "requires": { + "bluebird": "3.5.1", + "duplexer2": "0.0.2", + "graceful-fs": "4.1.11", + "gulp": "3.9.1", + "gulp-clean-css": "2.4.0", + "gulp-coffee": "2.3.5", + "gulp-concat": "2.6.1", + "gulp-concat-css": "2.3.0", + "gulp-if": "2.0.2", + "gulp-less": "3.5.0", + "gulp-order": "1.1.1", + "gulp-rev": "4.0.0", + "gulp-sourcemaps": "1.12.1", + "gulp-streamify": "0.0.5", + "gulp-uglify": "1.5.4", + "gulp-util": "3.0.8", + "lazypipe": "0.2.4", + "lodash": "3.10.1", + "map-stream": "0.0.6", + "merge-stream": "0.1.8", + "mkdirp": "0.5.1", + "pretty-hrtime": "1.0.3", + "readable-stream": "2.3.6", + "through2": "2.0.3", + "vinyl": "1.2.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.1", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "vinyl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", + "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", + "dev": true, + "requires": { + "clone": "1.0.4", + "clone-stats": "0.0.1", + "replace-ext": "0.0.1" + } + } + } + }, + "gulp-uglify": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/gulp-uglify/-/gulp-uglify-1.5.4.tgz", + "integrity": "sha1-UkeI2HZm0J+dDCH7IXf5ADmmWMk=", + "dev": true, + "requires": { + "deap": "1.0.1", + "fancy-log": "1.3.2", + "gulp-util": "3.0.8", + "isobject": "2.1.0", + "through2": "2.0.3", + "uglify-js": "2.6.4", + "uglify-save-license": "0.4.1", + "vinyl-sourcemaps-apply": "0.2.1" + }, + "dependencies": { + "async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + }, + "uglify-js": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.6.4.tgz", + "integrity": "sha1-ZeovswWck5RpLxX+2HwrNsFrmt8=", + "dev": true, + "requires": { + "async": "0.2.10", + "source-map": "0.5.7", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + } + } + } + }, + "gulp-util": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.8.tgz", + "integrity": "sha1-AFTh50RQLifATBh8PsxQXdVLu08=", + "dev": true, + "requires": { + "array-differ": "1.0.0", + "array-uniq": "1.0.3", + "beeper": "1.1.1", + "chalk": "1.1.3", + "dateformat": "2.2.0", + "fancy-log": "1.3.2", + "gulplog": "1.0.0", + "has-gulplog": "0.1.0", + "lodash._reescape": "3.0.0", + "lodash._reevaluate": "3.0.0", + "lodash._reinterpolate": "3.0.0", + "lodash.template": "3.6.2", + "minimist": "1.2.0", + "multipipe": "0.1.2", + "object-assign": "3.0.0", + "replace-ext": "0.0.1", + "through2": "2.0.3", + "vinyl": "0.5.3" + }, + "dependencies": { + "object-assign": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", + "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=", + "dev": true + } + } + }, + "gulplog": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", + "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=", + "dev": true, + "requires": { + "glogg": "1.0.1" + } + }, + "har-schema": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=", + "dev": true, + "optional": true + }, + "har-validator": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", + "dev": true, + "optional": true, + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-gulplog": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/has-gulplog/-/has-gulplog-0.1.0.tgz", + "integrity": "sha1-ZBTIKRNpfaUVkDl9r7EvIpZ4Ec4=", + "dev": true, + "requires": { + "sparkles": "1.0.0" + } + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "2.0.6", + "has-values": "1.0.0", + "isobject": "3.0.1" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "dev": true, + "optional": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", + "dev": true + }, + "homedir-polyfill": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz", + "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=", + "dev": true, + "requires": { + "parse-passwd": "1.0.0" + } + }, + "hosted-git-info": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz", + "integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw==", + "dev": true + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.1", + "sshpk": "1.14.1" + } + }, + "image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", + "dev": true, + "optional": true + }, + "import-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/import-regex/-/import-regex-1.1.0.tgz", + "integrity": "sha1-pVxS5McFx2XKIQ6SQqBrvMiqf2Y=", + "dev": true + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "indx": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/indx/-/indx-0.2.3.tgz", + "integrity": "sha1-Fdz1bunPZcAjTFE8J/vVgOcPvFA=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, + "interpret": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", + "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", + "dev": true + }, + "ip-regex": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-1.0.3.tgz", + "integrity": "sha1-3FiQdvZZ9BnCIgOaMzFvHHOH7/0=", + "dev": true + }, + "is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "dev": true, + "requires": { + "is-relative": "1.0.0", + "is-windows": "1.0.2" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dev": true, + "requires": { + "is-primitive": "2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-odd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", + "integrity": "sha512-OTiixgpZAT1M4NHgS5IguFp/Vz2VI3U7Goh4/HA1adtwyLtSBrxYlcSYkhpAE07s4fKEcjrFxyvtQBND4vFQyQ==", + "dev": true, + "requires": { + "is-number": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true + } + } + }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true + }, + "is-path-in-cwd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", + "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", + "dev": true, + "requires": { + "is-path-inside": "1.0.1" + } + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "requires": { + "path-is-inside": "1.0.2" + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "3.0.1" + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "dev": true + }, + "is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "dev": true, + "requires": { + "is-unc-path": "1.0.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true, + "optional": true + }, + "is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "dev": true, + "requires": { + "unc-path-regex": "0.1.2" + } + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true, + "optional": true + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true, + "optional": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true, + "optional": true + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "junk": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/junk/-/junk-1.0.3.tgz", + "integrity": "sha1-h75jSIZJy9ym9Tqzm+yczSNH9ZI=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true + }, + "lazy-debug-legacy": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/lazy-debug-legacy/-/lazy-debug-legacy-0.0.1.tgz", + "integrity": "sha1-U3cWwHduTPeePtG2IfdljCkRsbE=", + "dev": true + }, + "lazypipe": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/lazypipe/-/lazypipe-0.2.4.tgz", + "integrity": "sha1-bwponbEgf7iKXtSiyeV34l0Xo00=", + "dev": true, + "requires": { + "stream-combiner": "0.2.2" + } + }, + "less": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/less/-/less-2.7.3.tgz", + "integrity": "sha512-KPdIJKWcEAb02TuJtaLrhue0krtRLoRoo7x6BNJIBelO00t/CCdJQUnHW5V34OnHMWzIktSalJxRO+FvytQlCQ==", + "dev": true, + "requires": { + "errno": "0.1.7", + "graceful-fs": "4.1.11", + "image-size": "0.5.5", + "mime": "1.6.0", + "mkdirp": "0.5.1", + "promise": "7.3.1", + "request": "2.81.0", + "source-map": "0.5.7" + }, + "dependencies": { + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true, + "optional": true + } + } + }, + "liftoff": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-2.5.0.tgz", + "integrity": "sha1-IAkpG7Mc6oYbvxCnwVooyvdcMew=", + "dev": true, + "requires": { + "extend": "3.0.1", + "findup-sync": "2.0.0", + "fined": "1.1.0", + "flagged-respawn": "1.0.0", + "is-plain-object": "2.0.4", + "object.map": "1.0.1", + "rechoir": "0.6.2", + "resolve": "1.7.1" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + } + } + }, + "lodash": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-1.0.2.tgz", + "integrity": "sha1-j1dWDIO1n8JwvT1WG2kAQ0MOJVE=", + "dev": true + }, + "lodash._baseassign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "dev": true, + "requires": { + "lodash._basecopy": "3.0.1", + "lodash.keys": "3.1.2" + } + }, + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", + "dev": true + }, + "lodash._basetostring": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz", + "integrity": "sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U=", + "dev": true + }, + "lodash._basevalues": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz", + "integrity": "sha1-W3dXYoAr3j0yl1A+JjAIIP32Ybc=", + "dev": true + }, + "lodash._bindcallback": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", + "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=", + "dev": true + }, + "lodash._createassigner": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz", + "integrity": "sha1-g4pbri/aymOsIt7o4Z+k5taXCxE=", + "dev": true, + "requires": { + "lodash._bindcallback": "3.0.1", + "lodash._isiterateecall": "3.0.9", + "lodash.restparam": "3.6.1" + } + }, + "lodash._escapehtmlchar": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._escapehtmlchar/-/lodash._escapehtmlchar-2.4.1.tgz", + "integrity": "sha1-32fDu2t+jh6DGrSL+geVuSr+iZ0=", + "dev": true, + "requires": { + "lodash._htmlescapes": "2.4.1" + } + }, + "lodash._escapestringchar": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._escapestringchar/-/lodash._escapestringchar-2.4.1.tgz", + "integrity": "sha1-7P4iYYoq3lC/7qQ5N+Ud9m8O23I=", + "dev": true + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "dev": true + }, + "lodash._htmlescapes": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._htmlescapes/-/lodash._htmlescapes-2.4.1.tgz", + "integrity": "sha1-MtFL8IRLbeb4tioFG09nwii2JMs=", + "dev": true + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", + "dev": true + }, + "lodash._isnative": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._isnative/-/lodash._isnative-2.4.1.tgz", + "integrity": "sha1-PqZAS3hKe+g2x7V1gOHN95sUgyw=", + "dev": true + }, + "lodash._objecttypes": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._objecttypes/-/lodash._objecttypes-2.4.1.tgz", + "integrity": "sha1-fAt/admKH3ZSn4kLDNsbTf7BHBE=", + "dev": true + }, + "lodash._reescape": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reescape/-/lodash._reescape-3.0.0.tgz", + "integrity": "sha1-Kx1vXf4HyKNVdT5fJ/rH8c3hYWo=", + "dev": true + }, + "lodash._reevaluate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz", + "integrity": "sha1-WLx0xAZklTrgsSTYBpltrKQx4u0=", + "dev": true + }, + "lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", + "dev": true + }, + "lodash._reunescapedhtml": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._reunescapedhtml/-/lodash._reunescapedhtml-2.4.1.tgz", + "integrity": "sha1-dHxPxAED6zu4oJduVx96JlnpO6c=", + "dev": true, + "requires": { + "lodash._htmlescapes": "2.4.1", + "lodash.keys": "2.4.1" + }, + "dependencies": { + "lodash.keys": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-2.4.1.tgz", + "integrity": "sha1-SN6kbfj/djKxDXBrissmWR4rNyc=", + "dev": true, + "requires": { + "lodash._isnative": "2.4.1", + "lodash._shimkeys": "2.4.1", + "lodash.isobject": "2.4.1" + } + } + } + }, + "lodash._root": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz", + "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=", + "dev": true + }, + "lodash._shimkeys": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._shimkeys/-/lodash._shimkeys-2.4.1.tgz", + "integrity": "sha1-bpzJZm/wgfC1psl4uD4kLmlJ0gM=", + "dev": true, + "requires": { + "lodash._objecttypes": "2.4.1" + } + }, + "lodash.assign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-3.2.0.tgz", + "integrity": "sha1-POnwI0tLIiPilrj6CsH+6OvKZPo=", + "dev": true, + "requires": { + "lodash._baseassign": "3.2.0", + "lodash._createassigner": "3.1.1", + "lodash.keys": "3.1.2" + } + }, + "lodash.clone": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz", + "integrity": "sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y=", + "dev": true + }, + "lodash.defaults": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-3.1.2.tgz", + "integrity": "sha1-xzCLGNv4vJNy1wGnNJPGEZK9Liw=", + "dev": true, + "requires": { + "lodash.assign": "3.2.0", + "lodash.restparam": "3.6.1" + } + }, + "lodash.escape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz", + "integrity": "sha1-mV7g3BjBtIzJLv+ucaEKq1tIdpg=", + "dev": true, + "requires": { + "lodash._root": "3.0.1" + } + }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=", + "dev": true + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "dev": true + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", + "dev": true + }, + "lodash.isobject": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-2.4.1.tgz", + "integrity": "sha1-Wi5H/mmVPx7mMafrof5k0tBlWPU=", + "dev": true, + "requires": { + "lodash._objecttypes": "2.4.1" + } + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "dev": true, + "requires": { + "lodash._getnative": "3.9.1", + "lodash.isarguments": "3.1.0", + "lodash.isarray": "3.0.4" + } + }, + "lodash.merge": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz", + "integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ==", + "dev": true + }, + "lodash.partialright": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.partialright/-/lodash.partialright-4.2.1.tgz", + "integrity": "sha1-ATDYDoM2MmTUAHTzKbij56ihzEs=", + "dev": true + }, + "lodash.pick": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=", + "dev": true + }, + "lodash.restparam": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", + "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=", + "dev": true + }, + "lodash.template": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", + "integrity": "sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8=", + "dev": true, + "requires": { + "lodash._basecopy": "3.0.1", + "lodash._basetostring": "3.0.1", + "lodash._basevalues": "3.0.0", + "lodash._isiterateecall": "3.0.9", + "lodash._reinterpolate": "3.0.0", + "lodash.escape": "3.2.0", + "lodash.keys": "3.1.2", + "lodash.restparam": "3.6.1", + "lodash.templatesettings": "3.1.1" + } + }, + "lodash.templatesettings": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz", + "integrity": "sha1-+zB4RHU7Zrnxr6VOJix0UwfbqOU=", + "dev": true, + "requires": { + "lodash._reinterpolate": "3.0.0", + "lodash.escape": "3.2.0" + } + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "dev": true + }, + "lodash.values": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.values/-/lodash.values-2.4.1.tgz", + "integrity": "sha1-q/UUQ2s8twUAFieXjLzzCxKA7qQ=", + "dev": true, + "requires": { + "lodash.keys": "2.4.1" + }, + "dependencies": { + "lodash.keys": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-2.4.1.tgz", + "integrity": "sha1-SN6kbfj/djKxDXBrissmWR4rNyc=", + "dev": true, + "requires": { + "lodash._isnative": "2.4.1", + "lodash._shimkeys": "2.4.1", + "lodash.isobject": "2.4.1" + } + } + } + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "0.4.1", + "signal-exit": "3.0.2" + } + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", + "dev": true + }, + "make-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", + "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "map-stream": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.6.tgz", + "integrity": "sha1-0u9OuBGihkTHqJiZhcacL91JaCc=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "1.0.1" + } + }, + "maximatch": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/maximatch/-/maximatch-0.1.0.tgz", + "integrity": "sha1-hs2NawTJ8wfAWmuUGZBtA2D7E6I=", + "dev": true, + "requires": { + "array-differ": "1.0.0", + "array-union": "1.0.2", + "arrify": "1.0.1", + "minimatch": "3.0.4" + } + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "2.1.0", + "decamelize": "1.2.0", + "loud-rejection": "1.6.0", + "map-obj": "1.0.1", + "minimist": "1.2.0", + "normalize-package-data": "2.4.0", + "object-assign": "4.1.1", + "read-pkg-up": "1.0.1", + "redent": "1.0.0", + "trim-newlines": "1.0.0" + } + }, + "merge": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.0.tgz", + "integrity": "sha1-dTHjnUlJwoGma4xabgJl6LBYlNo=", + "dev": true + }, + "merge-array-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/merge-array-object/-/merge-array-object-1.0.5.tgz", + "integrity": "sha1-uBfILEGnzgCr9u2CHNscbVM7bRk=", + "dev": true, + "requires": { + "lodash": "4.17.5" + }, + "dependencies": { + "lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==", + "dev": true + } + } + }, + "merge-stream": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-0.1.8.tgz", + "integrity": "sha1-SKB7O0oSHXSj7b/c20sIrb8CQLE=", + "dev": true, + "requires": { + "through2": "0.6.5" + }, + "dependencies": { + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true, + "requires": { + "readable-stream": "1.0.34", + "xtend": "4.0.1" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "braces": "2.3.2", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "extglob": "2.0.4", + "fragment-cache": "0.2.1", + "kind-of": "6.0.2", + "nanomatch": "1.2.9", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "optional": true + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "dev": true + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "dev": true, + "requires": { + "mime-db": "1.33.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "mixin-deep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", + "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "dev": true, + "requires": { + "for-in": "1.0.2", + "is-extendable": "1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "modify-filename": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/modify-filename/-/modify-filename-1.1.0.tgz", + "integrity": "sha1-mi3sg4Bvuy2XXyK+7IWcoms5OqE=", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "multipipe": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz", + "integrity": "sha1-Ko8t33Du1WTf8tV/HhoTfZ8FB4s=", + "dev": true, + "requires": { + "duplexer2": "0.0.2" + } + }, + "nanomatch": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", + "integrity": "sha512-n8R9bS8yQ6eSXaV6jHUpKzD8gLsin02w1HSFiegwrs9E098Ylhw5jdyKPaYqvHknHaSCKTPp7C8dGCQ0q9koXA==", + "dev": true, + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "fragment-cache": "0.2.1", + "is-odd": "2.0.0", + "is-windows": "1.0.2", + "kind-of": "6.0.2", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + } + }, + "natives": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/natives/-/natives-1.1.3.tgz", + "integrity": "sha512-BZGSYV4YOLxzoTK73l0/s/0sH9l8SHs2ocReMH1f8JYSh5FUWu4ZrKCpJdRkWXV6HFR/pZDz7bwWOVAY07q77g==", + "dev": true + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "dev": true, + "requires": { + "hosted-git-info": "2.6.0", + "is-builtin-module": "1.0.0", + "semver": "4.3.6", + "validate-npm-package-license": "3.0.3" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "1.1.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "0.1.1", + "define-property": "0.2.5", + "kind-of": "3.2.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "3.0.1" + } + }, + "object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "dev": true, + "requires": { + "array-each": "1.0.1", + "array-slice": "1.1.0", + "for-own": "1.0.0", + "isobject": "3.0.1" + } + }, + "object.map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", + "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", + "dev": true, + "requires": { + "for-own": "1.0.0", + "make-iterator": "1.0.1" + } + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dev": true, + "requires": { + "for-own": "0.1.5", + "is-extendable": "0.1.1" + }, + "dependencies": { + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true, + "requires": { + "for-in": "1.0.2" + } + } + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "3.0.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "orchestrator": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/orchestrator/-/orchestrator-0.3.8.tgz", + "integrity": "sha1-FOfp4nZPcxX7rBhOUGx6pt+UrX4=", + "dev": true, + "requires": { + "end-of-stream": "0.1.5", + "sequencify": "0.0.7", + "stream-consume": "0.1.1" + } + }, + "ordered-read-streams": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-0.1.0.tgz", + "integrity": "sha1-/VZamvjrRHO6abbtijQ1LLVS8SY=", + "dev": true + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", + "dev": true, + "requires": { + "is-absolute": "1.0.0", + "map-cache": "0.2.2", + "path-root": "0.1.1" + } + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true, + "requires": { + "glob-base": "0.3.0", + "is-dotfile": "1.0.3", + "is-extglob": "1.0.0", + "is-glob": "2.0.1" + }, + "dependencies": { + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + } + } + }, + "parse-import": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-import/-/parse-import-2.0.0.tgz", + "integrity": "sha1-KyR0Aw4AirmNt2xLy/TbWucwb18=", + "dev": true, + "requires": { + "get-imports": "1.0.0" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "1.3.1" + } + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "dev": true + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "2.0.1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", + "dev": true, + "requires": { + "path-root-regex": "0.1.2" + } + }, + "path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", + "dev": true + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + }, + "dependencies": { + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + } + } + }, + "performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=", + "dev": true, + "optional": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "2.0.4" + } + }, + "plexer": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/plexer/-/plexer-0.0.2.tgz", + "integrity": "sha1-Ij1YAGKKBkagCX0wq1doZqLzeE4=", + "dev": true, + "requires": { + "readable-stream": "1.1.14" + } + }, + "plugin-error": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", + "integrity": "sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4=", + "dev": true, + "requires": { + "ansi-cyan": "0.1.1", + "ansi-red": "0.1.1", + "arr-diff": "1.1.0", + "arr-union": "2.1.0", + "extend-shallow": "1.1.4" + }, + "dependencies": { + "arr-diff": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz", + "integrity": "sha1-aHwydYFjWI/vfeezb6vklesaOZo=", + "dev": true, + "requires": { + "arr-flatten": "1.1.0", + "array-slice": "0.2.3" + } + }, + "arr-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz", + "integrity": "sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0=", + "dev": true + }, + "array-slice": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", + "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=", + "dev": true + }, + "extend-shallow": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", + "integrity": "sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE=", + "dev": true, + "requires": { + "kind-of": "1.1.0" + } + }, + "kind-of": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", + "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=", + "dev": true + } + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "dev": true + }, + "pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dev": true, + "requires": { + "asap": "2.0.6" + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true, + "optional": true + }, + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=", + "dev": true, + "optional": true + }, + "randomatic": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", + "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", + "dev": true, + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.4.0", + "path-type": "1.1.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + } + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "requires": { + "resolve": "1.7.1" + } + }, + "recursive-copy": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/recursive-copy/-/recursive-copy-2.0.9.tgz", + "integrity": "sha512-0AkHV+QtfS/1jW01z3m2t/TRTW56Fpc+xYbsoa/bqn8BCYPwmsaNjlYmUU/dyGg9w8MmGoUWihU5W+s+qjxvBQ==", + "dev": true, + "requires": { + "del": "2.2.2", + "emitter-mixin": "0.0.3", + "errno": "0.1.7", + "graceful-fs": "4.1.11", + "junk": "1.0.3", + "maximatch": "0.1.0", + "mkdirp": "0.5.1", + "pify": "2.3.0", + "promise": "7.3.1", + "slash": "1.0.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + } + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "2.1.0", + "strip-indent": "1.0.1" + } + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "dev": true, + "requires": { + "is-equal-shallow": "0.1.3" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "3.0.2", + "safe-regex": "1.1.0" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "1.0.2" + } + }, + "replace-ext": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", + "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=", + "dev": true + }, + "request": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "dev": true, + "optional": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.7.0", + "caseless": "0.12.0", + "combined-stream": "1.0.6", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.18", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.1.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.4", + "tunnel-agent": "0.6.0", + "uuid": "3.2.1" + } + }, + "resolve": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz", + "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==", + "dev": true, + "requires": { + "path-parse": "1.0.5" + } + }, + "resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "dev": true, + "requires": { + "expand-tilde": "2.0.2", + "global-modules": "1.0.0" + } + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "rev-hash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rev-hash/-/rev-hash-1.0.0.tgz", + "integrity": "sha1-lpk5Weqb+xxZsTrfAqwuNLs3NgM=", + "dev": true + }, + "rev-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rev-path/-/rev-path-1.0.0.tgz", + "integrity": "sha1-1My0NqwzcMRgcXXOiOr8XGXF1lM=", + "dev": true, + "requires": { + "modify-filename": "1.1.0" + } + }, + "rework": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rework/-/rework-1.0.1.tgz", + "integrity": "sha1-MIBqhBNCtUUQqkEQhQzUhTQUSqc=", + "dev": true, + "requires": { + "convert-source-map": "0.3.5", + "css": "2.2.1" + } + }, + "rework-import": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/rework-import/-/rework-import-2.1.0.tgz", + "integrity": "sha1-wm7StTFZrHvi7GDaIj74lgPB7x8=", + "dev": true, + "requires": { + "css": "2.2.1", + "globby": "2.1.0", + "parse-import": "2.0.0", + "url-regex": "3.2.0" + }, + "dependencies": { + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "globby": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-2.1.0.tgz", + "integrity": "sha1-npGSvNM/Srak+JTl5+qLcTITxII=", + "dev": true, + "requires": { + "array-union": "1.0.2", + "async": "1.5.2", + "glob": "5.0.15", + "object-assign": "3.0.0" + } + }, + "object-assign": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", + "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=", + "dev": true + } + } + }, + "rework-plugin-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/rework-plugin-function/-/rework-plugin-function-1.0.2.tgz", + "integrity": "sha1-Es5G+1sptdk1FGaD9rmM9J0jc7k=", + "dev": true, + "requires": { + "rework-visit": "1.0.0" + } + }, + "rework-plugin-url": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/rework-plugin-url/-/rework-plugin-url-1.1.0.tgz", + "integrity": "sha1-q1PosQV7nV7MHIJz/32xhgg3XEU=", + "dev": true, + "requires": { + "rework-plugin-function": "1.0.2" + } + }, + "rework-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rework-visit/-/rework-visit-1.0.0.tgz", + "integrity": "sha1-mUWygD8hni96ygCtuLyfZA+ELJo=", + "dev": true + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dev": true, + "requires": { + "align-text": "0.1.4" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "dev": true + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "0.1.15" + } + }, + "semver": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", + "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=", + "dev": true + }, + "sequencify": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/sequencify/-/sequencify-0.0.7.tgz", + "integrity": "sha1-kM/xnQLgcCf9dn9erT57ldHnOAw=", + "dev": true + }, + "set-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "split-string": "3.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "shelljs": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz", + "integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=", + "dev": true, + "requires": { + "glob": "7.1.2", + "interpret": "1.1.0", + "rechoir": "0.6.2" + } + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "0.11.2", + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "map-cache": "0.2.2", + "source-map": "0.5.7", + "source-map-resolve": "0.5.1", + "use": "3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "1.0.0", + "isobject": "3.0.1", + "snapdragon-util": "3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "dev": true, + "optional": true, + "requires": { + "hoek": "2.16.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.1.tgz", + "integrity": "sha512-0KW2wvzfxm8NCTb30z0LMNyPqWCdDGE2viwzUaucqJdkTRXtZiSY3I+2A6nVAjmdOy0I4gU8DwnVVGsk9jvP2A==", + "dev": true, + "requires": { + "atob": "2.1.0", + "decode-uri-component": "0.2.0", + "resolve-url": "0.2.1", + "source-map-url": "0.4.0", + "urix": "0.1.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "sparkles": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.0.tgz", + "integrity": "sha1-Gsu/tZJDbRC76PeFt8xvgoFQEsM=", + "dev": true + }, + "spdx-correct": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", + "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", + "dev": true, + "requires": { + "spdx-expression-parse": "3.0.0", + "spdx-license-ids": "3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", + "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "2.1.0", + "spdx-license-ids": "3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", + "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==", + "dev": true + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "3.0.2" + } + }, + "sshpk": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", + "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", + "dev": true, + "optional": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "0.2.5", + "object-copy": "0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + } + } + }, + "stream-combiner": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz", + "integrity": "sha1-rsjLrBd7Vrb0+kec7YwZEs7lKFg=", + "dev": true, + "requires": { + "duplexer": "0.1.1", + "through": "2.3.8" + } + }, + "stream-consume": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/stream-consume/-/stream-consume-0.1.1.tgz", + "integrity": "sha512-tNa3hzgkjEP7XbCkbRXe1jpg+ievoa0O4SCFlMOYEscGSS4JJsckGL8swUyAa/ApGU3Ae4t6Honor4HhL+tRyg==", + "dev": true + }, + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", + "dev": true + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", + "dev": true, + "optional": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-bom": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-1.0.0.tgz", + "integrity": "sha1-hbiGLzhEtabV7IRnqTWYFzo295Q=", + "dev": true, + "requires": { + "first-chunk-stream": "1.0.0", + "is-utf8": "0.2.1" + } + }, + "strip-bom-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-1.0.0.tgz", + "integrity": "sha1-5xRDmFd9Uaa+0PoZlPoF9D/ZiO4=", + "dev": true, + "requires": { + "first-chunk-stream": "1.0.0", + "strip-bom": "2.0.0" + }, + "dependencies": { + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + } + } + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "4.0.1" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "ternary-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ternary-stream/-/ternary-stream-2.0.1.tgz", + "integrity": "sha1-Bk5Im0tb9gumpre8fy9cJ07Pgmk=", + "dev": true, + "requires": { + "duplexify": "3.5.4", + "fork-stream": "0.0.4", + "merge-stream": "1.0.1", + "through2": "2.0.3" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "merge-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", + "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", + "dev": true, + "requires": { + "readable-stream": "2.3.6" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.1", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + } + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "dev": true, + "requires": { + "readable-stream": "2.3.6", + "xtend": "4.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.1", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + } + } + }, + "through2-filter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-2.0.0.tgz", + "integrity": "sha1-YLxVoNrLdghdsfna6Zq0P4PWIuw=", + "dev": true, + "requires": { + "through2": "2.0.3", + "xtend": "4.0.1" + } + }, + "tildify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tildify/-/tildify-1.2.0.tgz", + "integrity": "sha1-3OwD9V3Km3qj5bBPIYF+tW5jWIo=", + "dev": true, + "requires": { + "os-homedir": "1.0.2" + } + }, + "time-stamp": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", + "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "regex-not": "1.0.2", + "safe-regex": "1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "3.0.0", + "repeat-string": "1.6.1" + } + }, + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "dev": true, + "optional": true, + "requires": { + "punycode": "1.4.1" + } + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true, + "optional": true + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dev": true, + "requires": { + "source-map": "0.5.7", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + } + }, + "uglify-save-license": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/uglify-save-license/-/uglify-save-license-0.4.1.tgz", + "integrity": "sha1-lXJsF8xv0XHDYX479NjYKqjEzOE=", + "dev": true + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "dev": true + }, + "unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", + "dev": true + }, + "union-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "dev": true, + "requires": { + "arr-union": "3.1.0", + "get-value": "2.0.6", + "is-extendable": "0.1.1", + "set-value": "0.4.3" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "to-object-path": "0.3.0" + } + } + } + }, + "unique-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-1.0.0.tgz", + "integrity": "sha1-1ZpKdUJ0R9mqbJHnAmP40mpLEEs=", + "dev": true + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "0.3.1", + "isobject": "3.0.1" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "2.0.6", + "has-values": "0.1.4", + "isobject": "2.1.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + } + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "url-regex": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/url-regex/-/url-regex-3.2.0.tgz", + "integrity": "sha1-260eDJ4p4QXdCx8J9oYvf9tIJyQ=", + "dev": true, + "requires": { + "ip-regex": "1.0.3" + } + }, + "use": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", + "integrity": "sha512-6UJEQM/L+mzC3ZJNM56Q4DFGLX/evKGRg15UJHGB9X5j5Z3AFbgZvjUh2yq/UJUY4U5dh7Fal++XbNg1uzpRAw==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "user-home": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz", + "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", + "dev": true, + "optional": true + }, + "v8flags": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz", + "integrity": "sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ=", + "dev": true, + "requires": { + "user-home": "1.1.1" + } + }, + "validate-npm-package-license": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", + "integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==", + "dev": true, + "requires": { + "spdx-correct": "3.0.0", + "spdx-expression-parse": "3.0.0" + } + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "vinyl": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", + "integrity": "sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4=", + "dev": true, + "requires": { + "clone": "1.0.4", + "clone-stats": "0.0.1", + "replace-ext": "0.0.1" + } + }, + "vinyl-file": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/vinyl-file/-/vinyl-file-1.3.0.tgz", + "integrity": "sha1-qgVjTTqGe6kUR77bs0r8sm9E9uc=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "strip-bom": "2.0.0", + "strip-bom-stream": "1.0.0", + "vinyl": "1.2.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + }, + "vinyl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", + "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", + "dev": true, + "requires": { + "clone": "1.0.4", + "clone-stats": "0.0.1", + "replace-ext": "0.0.1" + } + } + } + }, + "vinyl-fs": { + "version": "0.3.14", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-0.3.14.tgz", + "integrity": "sha1-mmhRzhysHBzqX+hsCTHWIMLPqeY=", + "dev": true, + "requires": { + "defaults": "1.0.3", + "glob-stream": "3.1.18", + "glob-watcher": "0.0.6", + "graceful-fs": "3.0.11", + "mkdirp": "0.5.1", + "strip-bom": "1.0.0", + "through2": "0.6.5", + "vinyl": "0.4.6" + }, + "dependencies": { + "clone": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", + "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true, + "requires": { + "readable-stream": "1.0.34", + "xtend": "4.0.1" + } + }, + "vinyl": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", + "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", + "dev": true, + "requires": { + "clone": "0.2.0", + "clone-stats": "0.0.1" + } + } + } + }, + "vinyl-sourcemaps-apply": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz", + "integrity": "sha1-q2VJ1h0XLCsbh75cUI0jnI74dwU=", + "dev": true, + "requires": { + "source-map": "0.5.7" + } + }, + "when": { + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/when/-/when-3.7.8.tgz", + "integrity": "sha1-xxMLan6gRpPoQs3J56Hyqjmjn4I=", + "dev": true + }, + "which": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", + "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "dev": true, + "requires": { + "isexe": "2.0.0" + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "dev": true + }, + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "requires": { + "camelcase": "1.2.1", + "cliui": "2.1.0", + "decamelize": "1.2.0", + "window-size": "0.1.0" + } + } + } +} diff --git a/login/build/package.json b/login/build/package.json new file mode 100755 index 0000000..d9dc74a --- /dev/null +++ b/login/build/package.json @@ -0,0 +1,33 @@ +{ + "name": "@userfrosting/userfrosting", + "version": "4.1.0", + "description": "Build tool for UF 4.1", + "main": "index.js", + "repository": { + "type": "git", + "url": "https://github.com/userfrosting/userfrosting" + }, + "author": "Alex Weissman", + "license": "MIT", + "devDependencies": { + "gulp": "^3.9.1", + "gulp-uf-bundle-assets": "2.28.0", + "gulp-load-plugins": "^1.4.0", + "merge-array-object": "^1.0.3", + "recursive-copy": "^2.0.5", + "del": "^2.2.2", + "shelljs": "^0.7.6", + "bower": "^1.8.0", + "gulp-src-ordered-globs": "^1.0.3", + "gulp-copy": "^1.0.0" + }, + "scripts": { + "uf-bundle-build": "gulp bundle-build", + "uf-bundle": "gulp bundle", + "uf-bundle-clean": "gulp bundle-clean", + "uf-assets-install": "gulp bower-install", + "uf-assets-clean": "gulp bower-clean", + "uf-public-clean": "gulp public-clean", + "uf-clean": "gulp clean" + } +} diff --git a/login/composer.json b/login/composer.json new file mode 100755 index 0000000..28c3fe1 --- /dev/null +++ b/login/composer.json @@ -0,0 +1,60 @@ +{ + "name": "userfrosting/userfrosting", + "type": "project", + "description": "A secure, modern user management system for PHP.", + "keywords": ["php user management", "usercake", "bootstrap"], + "homepage": "https://github.com/userfrosting/UserFrosting", + "license" : "MIT", + "authors" : [ + { + "name": "Alexander Weissman", + "homepage": "https://alexanderweissman.com" + } + ], + "config": { + "vendor-dir": "app/vendor" + }, + "require": { + "php": ">=5.6", + "ext-gd": "*", + "composer/installers": "^1.4.0", + "symfony/console": "^3.3", + "wikimedia/composer-merge-plugin": "dev-master" + }, + "require-dev": { + "phpunit/phpunit": "^5.7", + "mockery/mockery": "1.0.0-alpha1", + "league/factory-muffin": "^3.0", + "league/factory-muffin-faker": "^2.0" + }, + "extra": { + "merge-plugin": { + "include": [ + "app/sprinkles.json", + "app/sprinkles/*/composer.json" + ], + "recurse": true, + "replace": false, + "merge-dev": true, + "merge-extra": false + } + }, + "minimum-stability": "dev", + "prefer-stable": true, + "autoload": { + "files" : [ + "app/defines.php" + ], + "psr-4": { + "UserFrosting\\System\\": "app/system/" + } + }, + "autoload-dev": { + "psr-4": { + "UserFrosting\\Tests\\": "app/tests/" + } + }, + "scripts": { + "test" : "phpunit --colors=always" + } +} diff --git a/login/docker-compose.yml b/login/docker-compose.yml new file mode 100755 index 0000000..932a1c0 --- /dev/null +++ b/login/docker-compose.yml @@ -0,0 +1,56 @@ +version: '2' +services: + php: + build: + context: ./docker/php + environment: + - DB_DRIVER=mysql + - DB_HOST=mysql + - DB_PORT=3306 + - DB_NAME=userfrosting + - DB_USER=userfrosting + - DB_PASSWORD=password + volumes: + - .:/app + links: + - mysql + nginx: + build: + context: ./docker/nginx + ports: + - 8570:80 + volumes: + - .:/app + links: + - php + mysql: + image: mysql:5.7 + environment: + - MYSQL_DATABASE=userfrosting + - MYSQL_ROOT_PASSWORD=root + - MYSQL_USER=userfrosting + - MYSQL_PASSWORD=password + ports: + - 8571:3306 + volumes: + - userfrosting-db:/var/lib/mysql + + composer: + image: composer/composer + volumes_from: + - php + working_dir: /app + command: -V + + node: + image: node:alpine + build: + context: ./docker/node + volumes_from: + - php + working_dir: /app/build + command: npm run uf-assets-install + +volumes: + userfrosting-db: + diff --git a/login/docker/README.md b/login/docker/README.md new file mode 100755 index 0000000..a2da2e5 --- /dev/null +++ b/login/docker/README.md @@ -0,0 +1,35 @@ +# Docker Development Environment + +First, install [Docker Compose](https://docs.docker.com/compose/install/). + +Second, initialize a new UserFrosting project: + +1. Copy `app/sprinkles/sprinkles.example.json` to `app/sprinkles/sprinkles.json` +2. Run `chmod 777 app/{logs,cache,sessions}` to fix file permissions for web server. (NOTE: File + permissions should be properly secured in a production environment!) +2. Run `docker-compose run composer install` to install all composer modules. +3. Run `docker-compose run node npm install` to install all npm modules. + +Now you can start up the entire Nginx + PHP + MySQL stack using docker with: + + $ docker-compose up + +On the first run you need to init the database (your container name may be different depending on the name of your root directory): + + $ docker exec -it -u www-data userfrosting_php_1 bash -c 'php bakery migrate' + +You also need to setup the first admin user (again, your container name may be different depending on the name of your root directory): + + $ docker exec -it -u www-data userfrosting_php_1 bash -c 'php bakery create-admin' + +Now visit http://localhost:8570/ to see your UserFrosting homepage! + +**This is not (yet) meant for production!!** + +You may be tempted to run with this in production but this setup has not been security-hardened. For example: + +- Database is exposed on port 8571 so you can access MySQL using your favorite client at localhost:8571. However, + the way Docker exposes this actually bypasses common firewalls like `ufw` so this should not be exposed in production. +- Database credentials are hard-coded so obviously not secure. +- File permissions may be more open than necessary. +- It just hasn't been thoroughly tested in the capacity of being a production system. diff --git a/login/docker/nginx/Dockerfile b/login/docker/nginx/Dockerfile new file mode 100755 index 0000000..7d2fbc3 --- /dev/null +++ b/login/docker/nginx/Dockerfile @@ -0,0 +1,2 @@ +FROM nginx:alpine +COPY default.conf /etc/nginx/conf.d/default.conf diff --git a/login/docker/nginx/default.conf b/login/docker/nginx/default.conf new file mode 100755 index 0000000..8ae7bb8 --- /dev/null +++ b/login/docker/nginx/default.conf @@ -0,0 +1,28 @@ +server { + listen 80; + root /app/public; + server_name ""; + + access_log /app/app/logs/nginx-access.log; + + add_header X-Frame-Options SAMEORIGIN; + add_header X-Content-Type-Options nosniff; + + location = /index.php { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_keep_conn on; + fastcgi_pass php:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + } + location ~* \.(png|gif|jpg|jpeg|ico|css|js|woff|ttf|otf|woff2|eot)$ { + include /etc/nginx/mime.types; + expires max; + try_files $uri /index.php?$query_string; + } + location / { + index index.php; + try_files $uri /index.php?$query_string; + } +} diff --git a/login/docker/node/Dockerfile b/login/docker/node/Dockerfile new file mode 100755 index 0000000..a985509 --- /dev/null +++ b/login/docker/node/Dockerfile @@ -0,0 +1,4 @@ +FROM node:alpine + +RUN apk --update add --no-cache git +RUN echo '{ "allow_root": true }' > /root/.bowerrc
\ No newline at end of file diff --git a/login/docker/php/Dockerfile b/login/docker/php/Dockerfile new file mode 100755 index 0000000..165cf4f --- /dev/null +++ b/login/docker/php/Dockerfile @@ -0,0 +1,11 @@ +FROM php:7.0-fpm +RUN apt-get update && apt-get install -y \ + libfreetype6-dev \ + libjpeg62-turbo-dev \ + libpng12-dev \ + && docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \ + && docker-php-ext-install -j$(nproc) gd \ + && docker-php-ext-install -j$(nproc) pdo pdo_mysql +RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer + +WORKDIR /app diff --git a/login/phpunit.xml b/login/phpunit.xml new file mode 100755 index 0000000..8945a71 --- /dev/null +++ b/login/phpunit.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<phpunit backupGlobals="false" + backupStaticAttributes="false" + bootstrap="app/vendor/autoload.php" + colors="true" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" + processIsolation="false" + stopOnFailure="false"> + <testsuites> + <testsuite name="Unit Tests"> + <directory suffix="Test.php">app/tests/Unit</directory> + <directory suffix="Test.php">app/sprinkles/*/tests/Unit</directory> + <directory suffix=".php">app/sprinkles/*/tests/Integration</directory> + </testsuite> + </testsuites> + <php> + <env name="UF_MODE" value="testing"/> + </php> +</phpunit>
\ No newline at end of file diff --git a/login/public/.htaccess b/login/public/.htaccess new file mode 100755 index 0000000..8abe84a --- /dev/null +++ b/login/public/.htaccess @@ -0,0 +1,185 @@ +# Enable this line to temporarily disable the Apache rewrite cache. +#Header set Cache-Control "max-age=0, private, no-store, no-cache, must-revalidate" + +# Tell PHP that we are using Apache +SetEnv SERVER_TYPE Apache + +<IfModule mod_rewrite.c> + + # Tell PHP that the mod_rewrite module is ENABLED. + SetEnv HTTP_MOD_REWRITE On + + RewriteEngine On + + # Uncomment the next two lines to forward all HTTP to HTTPS + #RewriteCond %{HTTPS} !=on + #RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L] + + # Forward all www. to non-www. Remove this rule if you want both available. + RewriteCond %{HTTP_HOST} ^www\.(.*)$ [NC] + RewriteRule ^(.*)$ http://%1/$1 [R=301,L] + + # Remove trailing slash from any non-directory path (canonicalization) - GET requests only + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_METHOD} =GET + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Dynamically rewrite base directory (see http://stackoverflow.com/questions/31062479/301-redirect-urls-that-are-also-being-rewritten) + RewriteCond $0#%{REQUEST_URI} ^([^#]*)#(.*)\1$ + RewriteRule ^.*$ - [E=BASE:%2] + + # Forward any requested URLs that specifically contain index.php (see http://stackoverflow.com/questions/31062479/301-redirect-urls-that-are-also-being-rewritten) + RewriteCond %{THE_REQUEST} /index\.php [NC] + RewriteRule ^index\.php(?:/(.*))?$ %{ENV:BASE}$1 [L,R=301,NC,NE] + + # Send the URI to index.php (Slim routing) + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [QSA,L] + +</IfModule> + +# Deny access to any php.ini files +<Files php.ini> + Order allow,deny + Deny from all +</Files> + +# Gzip Compression +<IfModule mod_deflate.c> + # Force compression for mangled headers. + # http://developer.yahoo.com/blogs/ydn/posts/2010/12/pushing-beyond-gzipping + <IfModule mod_setenvif.c> + <IfModule mod_headers.c> + SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding + RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding + </IfModule> + </IfModule> + + # Compress all output labeled with one of the following MIME-types + # (for Apache versions below 2.3.7, you don't need to enable `mod_filter` + # and can remove the `<IfModule mod_filter.c>` and `</IfModule>` lines + # as `AddOutputFilterByType` is still in the core directives). + + <IfModule mod_filter.c> + AddOutputFilterByType DEFLATE application/atom+xml \ + application/javascript \ + application/json \ + application/rss+xml \ + application/vnd.ms-fontobject \ + application/x-font-ttf \ + application/x-web-app-manifest+json \ + application/xhtml+xml \ + application/xml \ + font/opentype \ + image/svg+xml \ + image/x-icon \ + text/css \ + text/html \ + text/plain \ + text/x-component \ + text/xml + </IfModule> + +</IfModule> + +# ---------------------------------------------------------------------- +# | Expires headers - enable this if you want browsers to cache content | +# ---------------------------------------------------------------------- + +# Serve resources with far-future expires headers. +# +# (!) If you don't control versioning with filename-based +# cache busting, you should consider lowering the cache times +# to something like one week. +# +# https://httpd.apache.org/docs/current/mod/mod_expires.html + +#<IfModule mod_expires.c> +# +# ExpiresActive on +# ExpiresDefault "access plus 1 month" +# +# # CSS +# +# ExpiresByType text/css "access plus 1 hour" +# +# +# # Data interchange +# +# ExpiresByType application/atom+xml "access plus 1 hour" +# ExpiresByType application/rdf+xml "access plus 1 hour" +# ExpiresByType application/rss+xml "access plus 1 hour" +# +# ExpiresByType application/json "access plus 0 seconds" +# ExpiresByType application/ld+json "access plus 0 seconds" +# ExpiresByType application/schema+json "access plus 0 seconds" +# ExpiresByType application/vnd.geo+json "access plus 0 seconds" +# ExpiresByType application/xml "access plus 0 seconds" +# ExpiresByType text/xml "access plus 0 seconds" +# +# +# # Favicon (cannot be renamed!) and cursor images +# +# ExpiresByType image/vnd.microsoft.icon "access plus 1 week" +# ExpiresByType image/x-icon "access plus 1 week" +# +# # HTML +# +# ExpiresByType text/html "access plus 0 seconds" +# +# +# # JavaScript +# +# ExpiresByType application/javascript "access plus 1 hour" +# ExpiresByType application/x-javascript "access plus 1 hour" +# ExpiresByType text/javascript "access plus 1 hour" +# +# +# # Manifest files +# +# ExpiresByType application/manifest+json "access plus 1 week" +# ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds" +# ExpiresByType text/cache-manifest "access plus 0 seconds" +# +# +# # Media files +# +# ExpiresByType audio/ogg "access plus 1 month" +# ExpiresByType image/bmp "access plus 1 month" +# ExpiresByType image/gif "access plus 1 month" +# ExpiresByType image/jpeg "access plus 1 month" +# ExpiresByType image/png "access plus 1 month" +# ExpiresByType image/svg+xml "access plus 1 month" +# ExpiresByType image/webp "access plus 1 month" +# ExpiresByType video/mp4 "access plus 1 month" +# ExpiresByType video/ogg "access plus 1 month" +# ExpiresByType video/webm "access plus 1 month" +# +# +# # Web fonts +# +# # Embedded OpenType (EOT) +# ExpiresByType application/vnd.ms-fontobject "access plus 1 month" +# ExpiresByType font/eot "access plus 1 month" +# +# # OpenType +# ExpiresByType font/opentype "access plus 1 month" +# +# # TrueType +# ExpiresByType application/x-font-ttf "access plus 1 month" +# +# # Web Open Font Format (WOFF) 1.0 +# ExpiresByType application/font-woff "access plus 1 month" +# ExpiresByType application/x-font-woff "access plus 1 month" +# ExpiresByType font/woff "access plus 1 month" +# +# # Web Open Font Format (WOFF) 2.0 +# ExpiresByType application/font-woff2 "access plus 1 month" +# +# +# # Other +# +# ExpiresByType text/x-cross-domain-policy "access plus 1 week" +# +#</IfModule> diff --git a/login/public/index.php b/login/public/index.php new file mode 100755 index 0000000..5cbc048 --- /dev/null +++ b/login/public/index.php @@ -0,0 +1,23 @@ +<?php + +/** + * UserFrosting (http://www.userfrosting.com) + * + * @link https://github.com/userfrosting/UserFrosting + * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License) + */ + +/** + * Entry point for the /public site. + * + * @author Alex Weissman (https://alexanderweissman.com) + */ + +// First off, we'll grab the Composer dependencies +require_once __DIR__ . '/../app/vendor/autoload.php'; + +use UserFrosting\System\UserFrosting; + +$uf = new UserFrosting(); + +$uf->run(); diff --git a/login/screenshots/login.png b/login/screenshots/login.png Binary files differnew file mode 100755 index 0000000..5f95f0c --- /dev/null +++ b/login/screenshots/login.png diff --git a/login/screenshots/permissions.png b/login/screenshots/permissions.png Binary files differnew file mode 100755 index 0000000..ab39d8f --- /dev/null +++ b/login/screenshots/permissions.png diff --git a/login/screenshots/users.png b/login/screenshots/users.png Binary files differnew file mode 100755 index 0000000..d1a001c --- /dev/null +++ b/login/screenshots/users.png diff --git a/login/sponsors/nextgi.png b/login/sponsors/nextgi.png Binary files differnew file mode 100755 index 0000000..4e9f8de --- /dev/null +++ b/login/sponsors/nextgi.png diff --git a/login/sponsors/usor.png b/login/sponsors/usor.png Binary files differnew file mode 100755 index 0000000..74ed24c --- /dev/null +++ b/login/sponsors/usor.png diff --git a/login/webserver-configs/htaccess.txt b/login/webserver-configs/htaccess.txt new file mode 100755 index 0000000..8abe84a --- /dev/null +++ b/login/webserver-configs/htaccess.txt @@ -0,0 +1,185 @@ +# Enable this line to temporarily disable the Apache rewrite cache. +#Header set Cache-Control "max-age=0, private, no-store, no-cache, must-revalidate" + +# Tell PHP that we are using Apache +SetEnv SERVER_TYPE Apache + +<IfModule mod_rewrite.c> + + # Tell PHP that the mod_rewrite module is ENABLED. + SetEnv HTTP_MOD_REWRITE On + + RewriteEngine On + + # Uncomment the next two lines to forward all HTTP to HTTPS + #RewriteCond %{HTTPS} !=on + #RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L] + + # Forward all www. to non-www. Remove this rule if you want both available. + RewriteCond %{HTTP_HOST} ^www\.(.*)$ [NC] + RewriteRule ^(.*)$ http://%1/$1 [R=301,L] + + # Remove trailing slash from any non-directory path (canonicalization) - GET requests only + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_METHOD} =GET + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Dynamically rewrite base directory (see http://stackoverflow.com/questions/31062479/301-redirect-urls-that-are-also-being-rewritten) + RewriteCond $0#%{REQUEST_URI} ^([^#]*)#(.*)\1$ + RewriteRule ^.*$ - [E=BASE:%2] + + # Forward any requested URLs that specifically contain index.php (see http://stackoverflow.com/questions/31062479/301-redirect-urls-that-are-also-being-rewritten) + RewriteCond %{THE_REQUEST} /index\.php [NC] + RewriteRule ^index\.php(?:/(.*))?$ %{ENV:BASE}$1 [L,R=301,NC,NE] + + # Send the URI to index.php (Slim routing) + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [QSA,L] + +</IfModule> + +# Deny access to any php.ini files +<Files php.ini> + Order allow,deny + Deny from all +</Files> + +# Gzip Compression +<IfModule mod_deflate.c> + # Force compression for mangled headers. + # http://developer.yahoo.com/blogs/ydn/posts/2010/12/pushing-beyond-gzipping + <IfModule mod_setenvif.c> + <IfModule mod_headers.c> + SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding + RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding + </IfModule> + </IfModule> + + # Compress all output labeled with one of the following MIME-types + # (for Apache versions below 2.3.7, you don't need to enable `mod_filter` + # and can remove the `<IfModule mod_filter.c>` and `</IfModule>` lines + # as `AddOutputFilterByType` is still in the core directives). + + <IfModule mod_filter.c> + AddOutputFilterByType DEFLATE application/atom+xml \ + application/javascript \ + application/json \ + application/rss+xml \ + application/vnd.ms-fontobject \ + application/x-font-ttf \ + application/x-web-app-manifest+json \ + application/xhtml+xml \ + application/xml \ + font/opentype \ + image/svg+xml \ + image/x-icon \ + text/css \ + text/html \ + text/plain \ + text/x-component \ + text/xml + </IfModule> + +</IfModule> + +# ---------------------------------------------------------------------- +# | Expires headers - enable this if you want browsers to cache content | +# ---------------------------------------------------------------------- + +# Serve resources with far-future expires headers. +# +# (!) If you don't control versioning with filename-based +# cache busting, you should consider lowering the cache times +# to something like one week. +# +# https://httpd.apache.org/docs/current/mod/mod_expires.html + +#<IfModule mod_expires.c> +# +# ExpiresActive on +# ExpiresDefault "access plus 1 month" +# +# # CSS +# +# ExpiresByType text/css "access plus 1 hour" +# +# +# # Data interchange +# +# ExpiresByType application/atom+xml "access plus 1 hour" +# ExpiresByType application/rdf+xml "access plus 1 hour" +# ExpiresByType application/rss+xml "access plus 1 hour" +# +# ExpiresByType application/json "access plus 0 seconds" +# ExpiresByType application/ld+json "access plus 0 seconds" +# ExpiresByType application/schema+json "access plus 0 seconds" +# ExpiresByType application/vnd.geo+json "access plus 0 seconds" +# ExpiresByType application/xml "access plus 0 seconds" +# ExpiresByType text/xml "access plus 0 seconds" +# +# +# # Favicon (cannot be renamed!) and cursor images +# +# ExpiresByType image/vnd.microsoft.icon "access plus 1 week" +# ExpiresByType image/x-icon "access plus 1 week" +# +# # HTML +# +# ExpiresByType text/html "access plus 0 seconds" +# +# +# # JavaScript +# +# ExpiresByType application/javascript "access plus 1 hour" +# ExpiresByType application/x-javascript "access plus 1 hour" +# ExpiresByType text/javascript "access plus 1 hour" +# +# +# # Manifest files +# +# ExpiresByType application/manifest+json "access plus 1 week" +# ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds" +# ExpiresByType text/cache-manifest "access plus 0 seconds" +# +# +# # Media files +# +# ExpiresByType audio/ogg "access plus 1 month" +# ExpiresByType image/bmp "access plus 1 month" +# ExpiresByType image/gif "access plus 1 month" +# ExpiresByType image/jpeg "access plus 1 month" +# ExpiresByType image/png "access plus 1 month" +# ExpiresByType image/svg+xml "access plus 1 month" +# ExpiresByType image/webp "access plus 1 month" +# ExpiresByType video/mp4 "access plus 1 month" +# ExpiresByType video/ogg "access plus 1 month" +# ExpiresByType video/webm "access plus 1 month" +# +# +# # Web fonts +# +# # Embedded OpenType (EOT) +# ExpiresByType application/vnd.ms-fontobject "access plus 1 month" +# ExpiresByType font/eot "access plus 1 month" +# +# # OpenType +# ExpiresByType font/opentype "access plus 1 month" +# +# # TrueType +# ExpiresByType application/x-font-ttf "access plus 1 month" +# +# # Web Open Font Format (WOFF) 1.0 +# ExpiresByType application/font-woff "access plus 1 month" +# ExpiresByType application/x-font-woff "access plus 1 month" +# ExpiresByType font/woff "access plus 1 month" +# +# # Web Open Font Format (WOFF) 2.0 +# ExpiresByType application/font-woff2 "access plus 1 month" +# +# +# # Other +# +# ExpiresByType text/x-cross-domain-policy "access plus 1 week" +# +#</IfModule> diff --git a/login/webserver-configs/nginx.conf b/login/webserver-configs/nginx.conf new file mode 100755 index 0000000..354c683 --- /dev/null +++ b/login/webserver-configs/nginx.conf @@ -0,0 +1,127 @@ +## UserFrosting sample nginx configuration file. +## See https://learn.userfrosting.com/going-live/vps-production-environment/application-setup#configure-the-webserver-nginx- + +## Redirect HTTP to HTTPS +## Enable this block once you've set up SSL. This will redirect all HTTP requests to HTTPS. +#server { +# listen 80; +# server_name example.com; +# return 301 https://$host$request_uri; +#} + +## Main server configuration +server { + ## Non-SSL configuration. Not recommended for production! + listen 80; + ## Defines the script/file to look for when a request is made to the index of your server name. + index index.php index.html index.htm; + + ## Begin - Server Info + ## Document root directory for your project. Should be set to the directory that contains your index.php. + root /usr/share/nginx/project/public; + server_name example.com; + ## End - Server Info + + ## SSL configuration + ## It is STRONGLY RECOMMENDED that you use SSL for all traffic to your UF site. + ## Otherwise, you are potentially leaking your users' sensitive info, including passwords! + ## See https://letsencrypt.org/ to find out how to get a free, trusted SSL cert for your site. + # + #listen 443 ssl http2; + #listen [::]:443 ssl http2; + ## Certificate paths (example for letsencrypt) + #ssl_certificate /etc/letsencrypt/live/<cert name>/fullchain.pem; + #ssl_certificate_key /etc/letsencrypt/live/<cert name>/privkey.pem; + ## Disable SSLv3(enabled by default since nginx 0.8.19) since it's less secure then TLS http://en.wikipedia.org/wiki/Secure_Sockets_Layer#SSL_3.0 + #ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ## Enable session resumption to enable low latency for repeat visitors. + #ssl_session_cache shared:SSL:50m; + #ssl_session_timeout 5m; + ## Enables server-side protection from BEAST attacks + #ssl_prefer_server_ciphers on; + ## Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits + #ssl_dhparam /etc/nginx/dhparam.pem; # google will tell you how to make this + ## Ciphers chosen for forward secrecy and compatibility + #ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'; + ## Enable ocsp stapling (mechanism by which a site can convey certificate revocation information to visitors in a privacy-preserving, scalable manner) + #resolver 8.8.8.8; + #ssl_stapling on; + #ssl_trusted_certificate /etc/letsencrypt/live/<cert name>/fullchain.pem; # same as your ssl_certificate path + ## Config to enable HSTS(HTTP Strict Transport Security) https://developer.mozilla.org/en-US/docs/Security/HTTP_Strict_Transport_Security + #add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;"; + ## End - SSL configuration + + access_log /var/log/nginx/access.log; + + add_header X-Frame-Options SAMEORIGIN; + add_header X-Content-Type-Options nosniff; + ## This header enables the Cross-site scripting (XSS) filter built into most recent web browsers. + add_header X-XSS-Protection "1; mode=block"; #optional + + ## Begin - Pagespeed + ## See https://learn.userfrosting.com/going-live/vps-production-environment/additional-recommendations + ## for information on compiling nginx with the Pagespeed module. + #pagespeed on; + #pagespeed FileCachePath /var/ngx_pagespeed_cache; + #pagespeed Disallow "*.svg*"; + ## Add additional filters here + #pagespeed EnableFilters prioritize_critical_css; + ## Ensure requests for pagespeed optimized resources go to the pagespeed + ## handler and no extraneous headers get set. + #location ~ "\.pagespeed\.([a-z]\.)?[a-z]{2}\.[^.]{10}\.[^.]+" { add_header "" ""; } + #location ~ "^/ngx_pagespeed_static/" { } + #location ~ "^/ngx_pagespeed_beacon" { } + ## End - Pagespeed + + ## Begin - Let's Encrypt + ## Allow URLs for certbot acme challenge + location ~ /.well-known { + allow all; + } + ## End - Let's Encrypt + + ## Begin - Handle PHP requests + location ~ \.(php)$ { + # Throw away any requests to execute PHP scripts in other directories + # See http://cnedelcu.blogspot.com/2010/05/nginx-php-via-fastcgi-important.html for why this is needed + location ~ \..*/.*\.php$ { + return 404; + } + # regex to split $uri to $fastcgi_script_name and $fastcgi_path + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_keep_conn on; + # For FPM (PHP 7) + fastcgi_pass unix:/run/php/php7.0-fpm.sock; + # For FPM (PHP 5.x) + #fastcgi_pass unix:/var/run/php5-fpm.sock; + # For traditional PHP FastCGI (php5-cgi or php7.0-cgi) + #fastcgi_pass 127.0.0.1:9000; + # For HHVM + #fastcgi_pass unix:/var/run/hhvm/hhvm.sock; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + } + ## End - Handle PHP requests + + ## Begin - Caching static files + location ~* \.(png|gif|jpg|jpeg|svg|ico|css|js|woff|ttf|otf|woff2|eot)$ { + include /etc/nginx/mime.types; + expires max; + + index index.php; + try_files $uri $uri/ /index.php?$query_string; + } + ## End - Caching static files + + ## Begin - Index + ## for subfolders, simply adjust: + ## `location /subfolder {` + ## and the rewrite to use `/subfolder/index.php` + location / { + include /etc/nginx/mime.types; + index index.php; + try_files $uri $uri/ /index.php?$query_string; + } + ## End - Index +} diff --git a/login/webserver-configs/web.config b/login/webserver-configs/web.config new file mode 100755 index 0000000..96b3a00 --- /dev/null +++ b/login/webserver-configs/web.config @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Unlike apache and *ngix, IIS will likely require additional configuration that cannot be safely defined, or defined at all in a web.config file. + If odd behaviour occurs: + 1. Ensure web.config settings are being applied, as IIS can be configured to ignore specific web.config settings. + 2. If there is an IIS error page, try the suggested solutions. + 3. See if anyone on the web has encountered the same issue, and try the suggested solutions. + 4. And as a last resort, ask in the UserFrosting chat support channel. http://chat.userfrosting.com --> +<configuration> + <system.webServer> + <!-- Most default installs of PHP in IIS only accept GET and POST HTTP verbs. + If using the account functionality of UserFrosting, additional HTTP verbs will need to be added to the PHP handler. + Specifically, PUT and DELETE are required. + Sample PHP handler definition follows. + <handlers> + <add name="PHP_via_FastCGI" path="*.php" verb="GET,POST,PUT,DELETE" modules="FastCgiModule" scriptProcessor="path/to/php/directory/php-cgi.exe" resourceType="Either" /> + </handlers>--> + <rewrite> + <!-- Rules to clean url, and ensure requests are handled by PHP when appropriate. --> + <rules> + <!-- Clear any inherited rules --> + <clear /> + <!-- Dynamically rewrite base directory to remove index.php from url. --> + <rule name="Remove .../index.php/... from url"> + <match url="^index\.php/(.*)$" /> + <action type="Redirect" redirectType="Permanent" url="{R:1}" /> + </rule> + <!-- Remove trailing slash from any non-directory path (canonicalization) - GET requests only. --> + <rule name="Remove trailing slash"> + <match url="(.*)/$" /> + <conditions> + <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" /> + </conditions> + <action type="Redirect" redirectType="Permanent" url="{R:1}" /> + </rule> + <!-- Send the URI to index.php (Slim routing) if url maps to neither a directory, or file. --> + <rule name="Rewrite to index.php" stopProcessing="true"> + <match url=".*" /> + <conditions> + <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" /> + <add input="{REQUEST_FILEDIR}" matchType="IsDirectory" negate="true" /> + </conditions> + <action type="Rewrite" url="index.php" /> + </rule> + </rules> + </rewrite> + <!-- Set index.php as default document, and clear inherited defaults. --> + <defaultDocument enabled="true"> + <files> + <clear /> + <add value="index.php" /> + </files> + </defaultDocument> + <!-- Whitelist specific static file types --> + <staticContent> + <!-- Clear inherited rules --> + <clear /> + <!-- Set content expiration time (31 days for every static file listed) --> + <!--<clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="31.00:00:00" />--> + <!-- Atom feeds --> + <mimeMap fileExtension=".atom" mimeType="application/atom+xml" /> + <mimeMap fileExtension=".xml" mimeType="application/atom+xml, application/rss+xml, application/xhtml+xml, application/xml, text/xml" /> + <!-- JS --> + <mimeMap fileExtension=".js" mimeType="application/js" /> + <!-- JSON --> + <mimeMap fileExtension=".json" mimeType="application/json" /> + <!-- EOT font --> + <mimeMap fileExtension=".eot" mimeType="application/vnd.ms-fontobject" /> + <!-- TTF fonts --> + <mimeMap fileExtension=".ttf" mimeType="application/x-font-ttf" /> + <!-- Web App Manifest --> + <mimeMap fileExtension=".webapp" mimeType="application/x-web-app-manifest+json" /> + <!-- XHTML --> + <mimeMap fileExtension=".xhtml" mimeType="application/xhtml+xml" /> + <mimeMap fileExtension=".xht" mimeType="application/xhtml+xml" /> + <mimeMap fileExtension=".html" mimeType="application/xhtml+xml" /> + <mimeMap fileExtension=".htm" mimeType="application/xhtml+xml" /> + <!-- Images --> + <mimeMap fileExtension=".svg" mimeType="image/svg+xml" /> + <mimeMap fileExtension=".png" mimeType="image/png" /> + <mimeMap fileExtension=".ico" mimeType="image/x-icon" /> + <!-- WOFF/2 fonts --> + <mimeMap fileExtension=".woff" mimeType="application/x-font-woff" /> + <mimeMap fileExtension=".woff2" mimeType="font/woff2" /> + <!-- CSS --> + <mimeMap fileExtension=".css" mimeType="text/css" /> + <!-- Plain text --> + <mimeMap fileExtension=".txt" mimeType="text/plain" /> + <!-- HTML Component --> + <mimeMap fileExtension=".htc" mimeType="text/x-component" /> + </staticContent> + </system.webServer> +</configuration>
\ No newline at end of file |