first commit

This commit is contained in:
2018-08-01 16:00:05 +02:00
commit 944fd76206
3205 changed files with 356767 additions and 0 deletions
+542
View File
@@ -0,0 +1,542 @@
# v2.7.3
## 06/20/2018
1. [](#bugfix)
* Fixed regression with `redirect_after_login` setting [#164](https://github.com/getgrav/grav-plugin-login/issues/164)
# v2.7.2
## 06/11/2018
1. [](#new)
* Norwegian translation added [#163](https://github.com/getgrav/grav-plugin-login/issues/163)
1. [](#bugfix)
* Fixed issue with `redirect_after_login` being ignored [#164](https://github.com/getgrav/grav-plugin-login/issues/164)
* CLI commands `change-user-state` and `change-password` were ignoring desired username [#161](https://github.com/getgrav/grav-plugin-login/issues/161)
# v2.7.1
## 06/03/2018
1. [](#bugfix)
* Removed extra unnecessary username check [#159](https://github.com/getgrav/grav-plugin-login/issues/159)
* CLI command `add-user` ignores desired username [#157](https://github.com/getgrav/grav-plugin-login/issues/157)
# v2.7.0
## 05/11/2018
1. [](#new)
* Moved support for 2FA authentication into Login plugin (only supported in Admin currently)
* Updated plugin dependencies (Grav >= 1.4.5, Form >=2.13.4, Email >=2.7.0)
1. [](#improved)
* Added cleaner way for 3rd party providers to add twig templates to login form
* Use `Login` class validation methods in CLI
* Added logging of login exceptions
* Show denied message only when authenticated but not authorized
1. [](#bugfix)
* Don't allow Profile saving if a Grav user account doesn't exist (OAuth/LDAP users for example)
* Don't allow PW reset if no current password exists (OAuth/LDAP users for example)
# v2.6.3
## 04/12/2018
1. [](#bugfix)
* Fixed issue with saving profile and stating email has already exists
# v2.6.2
## 04/12/2018
1. [](#new)
* Added custom logout redirect configuration option
* Added support for `Login::login()` and `Login::logout()` to return `UserLoginEvent` instance instead of `User`
* Added support for custom login messages and redirects set in `UserLoginEvent`
1. [](#bugfix)
* Fixed typo in activation email body [#151](https://github.com/getgrav/grav-plugin-login/issues/151)
# v2.6.1
## 03/19/2018
1. [](#improved)
* Fixed undefined index if login form didn't contain username/password
# v2.6.0
## 02/22/2018
1. [](#improved)
* Disabled user registration by default. Enable it manually if you need it.
* Disabled user-login-on-registration by default. Enable it manually if you need it.
* Check for existing email addresses when updating User profile.
# v2.5.0
## 12/05/2017
1. [](#new)
* Added `$grav['login']->login()` and `$grav['login']->logout()` functions with event hooks
* Added `$grav['login']->getRateLimiter($context)` function
* Added events `onUserLoginAuthenticate`, `onUserLoginAuthorize`, `onUserLoginFailure`, `onUserLogin`, `onUserLogout`
* Logout message is now maintained during session destruction
1. [](#improved)
* Remember entered username if login fails
* Improved rate limiter to work without sessions and against distributed attacks
* Removed `partials/messages.html.twig` and rely on new core version
* Moved languages from unified file into dedicated language file structure
* Welcome / Notice / Activation emails now more flushed out and in HTML like Reset Password
1. [](#bugfix)
* Do not send nonce with activation link, email app can open the link in another browser
# v2.4.3
## 10/11/2017
1. [](#bugfix)
* Fix an issue when a user only has `groups` and no `access` defined [#134](https://github.com/getgrav/grav-plugin-login/issues/134)
* Escape untrusted URLs in the template files
# v2.4.2
## 09/29/2017
1. [](#bugfix)
* Fixed issue with protected page media without access [#132](https://github.com/getgrav/grav-plugin-login/issues/132)
* Improved validation of email to support RFC5322 [Grav#1648](https://github.com/getgrav/grav/issues/1648)
# v2.4.1
## 09/12/2017
1. [](#bugfix)
* Fixed an issue with 3rd party login plugins [#130](https://github.com/getgrav/grav-plugin-login/issues/130)
# v2.4.0
## 09/07/2017
1. [](#new)
* Added the ability to have a custom route for login page, but not redirect
* Added a new `unauthorized.md` page that can be customized as needed
1. [](#improved)
* Differentiated between `authenticated` and `authorized`
* Moved rate-limiting logic to the Login class
* Much code cleanup and removing of cruft
* Updated vendor libraries
* Added Russian translation
1. [](#bugfix)
* Fixed login JSON response in case of login failure
* Fixed issue with profile form displaying on login page
* Store referrer page when trying to access Profile page
* Fixed error when logging out with an expired session
# v2.3.2
## 06/22/2017
1. [](#bugfix)
* Grav plugin cli error on password change [#120](https://github.com/getgrav/grav-plugin-login/issues/120)
# v2.3.1
## 05/16/2017
1. [](#improved)
* Added routes to the Admin blueprints
# v2.3.0
## 04/19/2017
1. [](#new)
* Added new built-in profile page support
* Added optional flood protection for password resets and login attempts [#91](https://github.com/getgrav/grav-plugin-login/issues/91)
1. [](#improved)
* Use new system configuration entries for username and password format
* Use initialized form object in Twig templates rather than array from page.header
* Improved alert styling in login templates
* Added `appends` for number field
* Added missing `route` options in admin options (blueprints)
1. [](#bugfix)
* Set cookie path to `/` if `base_url_relative` is empty [#102](https://github.com/getgrav/grav-plugin-login/issues/102)
* Fixed some redirect logic
# v2.2.1
## 01/24/2017
1. [](#bugfix)
* Fix login form/status templates displaying user as logged in even if he's not authenticated
* Use email validation instead of text validation in the forgot password form [https://github.com/gantry/gantry5/issues/1813](https://github.com/gantry/gantry5/issues/1813)
# v2.2.0
## 12/13/2016
1. [](#new)
* RC released as stable
# v2.2.0-rc.5
## 12/07/2016
1. [](#improved)
* Added support for hiding `Remember me` checkbox and and `Forgot` button (for Offline functionality)
1. [](#bugfix)
* Fixed redirect issue in admin plugin
# v2.2.0-rc.4
## 12/04/2016
1. [](#improved)
* Improved logic for redirect after login to not include login-related pages.
# v2.2.0-rc.3
## 11/26/2016
1. [](#improved)
* Added some validity checks in the reset password form
1. [](#bugfix)
* Correctly redirect to the last page visited after login, unless `redirect_after_login` is defined
# v2.2.0-rc.2
## 11/17/2016
1. [](#new)
* Allow to set permissions using nested array syntax [#96](https://github.com/getgrav/grav-plugin-login/issues/96)
1. [](#improved)
* Use the same feedback message when resetting the password if the email exists or not. Remove email in the message as we now recover via email, useless
1. [](#bugfix)
* Fix registration form, fields were not visible [#97](https://github.com/getgrav/grav-plugin-login/issues/97)
* Do not initialize the user session if the user exists but has no `site.login` permission
# v2.2.0-rc.1
## 11/09/2016
1. [](#new)
* Allow login via `username` or `email`
* Only allow password recovery via `email` address
# v2.1.2
## 10/01/2016
1. [](#bugfix)
* Fixed an old reference to `LoginUtils` and replaced with new `EmailUtils`
# v2.1.1
## 09/08/2016
1. [](#improved)
* Use better detection for admin allowing multi-site setup with subfolders
# v2.1.0
## 09/07/2016
1. [](#improved)
* Added support for Grav's autoescape twig setting
* Dropped unused variable reference
* Moved Email Utils to Email plugin
* Updated vendor libraries
* Allow explicitly showing the login page on pages that are not the Login form template [#11](https://github.com/getgrav/grav-plugin-maintenance/issues/11)
# v2.0.1
## 08/10/2016
1. [](#improved)
* Added Romanian
# v2.0.0
## 07/14/2016
1. [](#improved)
* Optimized nonce creation
* Point account path to core's account stream [#85](https://github.com/getgrav/grav-plugin-login/issues/85)
# v2.0.0-rc.2
## 06/21/2016
1. [](#new)
* Add an option to login protect a login-protected page media accessed through the page route [#45](https://github.com/getgrav/grav-plugin-login/issues/45)
1. [](#improved)
* Fixed some language keys
1. [](#bugfix)
* Correctly show an error message when the reset password form does not provide the correct nonce
# v2.0.0-rc.1
## 06/01/2016
1. [](#improved)
* French updated
1. [](#bugfix)
* Enable twig processing in a page #75
* Deny access to registration when user registration is disabled #72
# v2.0.0-beta.3
## 05/23/2016
1. [](#improved)
* Added a redirect after activation
* Changed hardcoded redirect routes to config-based
1. [](#bugfix)
* Fix a redirect issue #74
* Don't error if missing a HTTP_USER_AGENT browser string
# v2.0.0-beta.2
## 05/03/2016
1. [](#improved)
* Improved the login form page once logged in
* Translate welcome and logout strings
1. [](#bugfix)
* Fixed logging out on the homepage
* Fixed an issue in processing user registration
# v2.0.0-beta.1
## 04/20/2016
1. [](#new)
* Introduce a more flexible Login plugin architecture, which allows separate authentication plugins to hook into the Login events. Separated OAuth to its own plugin.
* OAuth has been separated to its own plugin, needs to be installed separately and configured. The users account filename format has changed too, to fix an issue that involved people with the same name on a service.
* The `redirect` option has been changed to `redirect_after_login`. Make sure you update your configuration file.
1. [](#improved)
* Add a proper 'Access levels' config section for Login.
* Various underlying improvements
* Updated french, added german
1. [](#bugfix)
* Make username field autofocus
* Add validation to the password reset form
* Fixed an issue that allowed a user logged in, without access to the actual permissions set to view a page, to see its content, and the login form again even if already logged in.
# v1.3.1
## 02/05/2016
1. [](#new)
* Add translations for Username and Password (placeholders are not translated)
1. [](#improved)
* Improve registration, forgot, reset and login forms accessibility by setting the id attribute
* Improved french translation
* Add the correct message type when raising a form processing error
1. [](#bugfix)
* Show the correct error message when the user is not authorized to view a page
* Fix showing the OAuth links in the login form
# v1.3.0
## 01/06/2016
1. [](#new)
* Added a new CLI command to change a user's password
* Added a new CLI command to edit the user state
1. [](#improved)
* Improved french translation
# v1.2.1
## 12/18/2015
1. [](#new)
* Croatian translation
1. [](#improved)
* Use type `email` in registration form
* Drop manual validation in registration
# v1.2.0
## 12/11/2015
1. [](#new)
* Added account activation email upon registration
* Added forgot password functionality
* Support ACL from parent page
* Allow login immediately after account activation
1. [](#improved)
* Handle admin login page if available
* Example registration form now provided by plugin
* Better error handling of registration
* Tab-based plugin configuration
* Updated translations
1. [](#bugfix)
* Prevent failing when no default values are set
# v1.1.0
## 12/01/2015
1. [](#new)
* Support new **User Registration**
1. [](#improved)
* Use new security salt for newer and fallback otherwise
* Composer update of libraries
* Check for session existence else throw a runtime error
1. [](#bugfix)
* Fix remember-me functionality
* Check page exists so as not to fail hard
* Fix for static Inflector references #17
# v1.0.1
## 11/23/2015
1. [](#improved)
* Hardening cookies with user-agent and system cache key instead of deprecated system hash
* Set a custom route for login only if it's not an admin path
# v1.0.0
## 11/21/2015
1. [](#new)
* Added OAuth login support for _Facebook_, _Google_, _GitHub_ and _Twitter_
* Added **Nonce** form security support
* Added option to "redirect after login"
* Added "remember me" functionality
* Added Hungarian translation
2. [](#improved)
* Added blueprints for Grav Admin plugin (multi-language support!)
# v0.3.3
## 09/11/2015
1. [](#improved)
* Changed authorise to authorize
1. [](#bugfix)
* Fix denied string
# v0.3.2
## 09/01/2015
1. [](#improved)
* Broke out login form into its own partial
# v0.3.1
## 08/31/2015
1. [](#improved)
* Added username field autofocus
# v0.3.0
## 08/24/2015
1. [](#new)
* Added simple CSS styling
* Added simple login status with logout
1. [](#improved)
* Improved README documentation
* More strings translated
* Updated blueprints
# v0.2.0
## 08/11/2015
1. [](#improved)
* Disable `enable` in admin
# v0.1.0
## 08/04/2015
1. [](#new)
* ChangeLog started...
# v1.3.1
## 02/05/2016
1. [](#new)
* Add translations for Username and Password (placeholders are not translated)
1. [](#improved)
* Improve registration, forgot, reset and login forms accessibility by setting the id attribute
* Improved french translation
* Add the correct message type when raising a form processing error
1. [](#bugfix)
* Show the correct error message when the user is not authorized to view a page
* Fix showing the OAuth links in the login form
# v1.3.0
## 01/06/2016
1. [](#new)
* Added a new CLI command to change a user's password
* Added a new CLI command to edit the user state
1. [](#improved)
* Improved french translation
# v1.2.1
## 12/18/2015
1. [](#new)
* Croatian translation
1. [](#improved)
* Use type `email` in registration form
* Drop manual validation in registration
# v1.2.0
## 12/11/2015
1. [](#new)
* Added account activation email upon registration
* Added forgot password functionality
* Support ACL from parent page
* Allow login immediately after account activation
1. [](#improved)
* Handle admin login page if available
* Example registration form now provided by plugin
* Better error handling of registration
* Tab-based plugin configuration
* Updated translations
1. [](#bugfix)
* Prevent failing when no default values are set
# v1.1.0
## 12/01/2015
1. [](#new)
* Support new **User Registration**
1. [](#improved)
* Use new security salt for newer and fallback otherwise
* Composer update of libraries
* Check for session existence else throw a runtime error
1. [](#bugfix)
* Fix remember-me functionality
* Check page exists so as not to fail hard
* Fix for static Inflector references #17
# v1.0.1
## 11/23/2015
1. [](#improved)
* Hardening cookies with user-agent and system cache key instead of deprecated system hash
* Set a custom route for login only if it's not an admin path
# v1.0.0
## 11/21/2015
1. [](#new)
* Added OAuth login support for _Facebook_, _Google_, _GitHub_ and _Twitter_
* Added **Nonce** form security support
* Added option to "redirect after login"
* Added "remember me" functionality
* Added Hungarian translation
2. [](#improved)
* Added blueprints for Grav Admin plugin (multi-language support!)
# v0.3.3
## 09/11/2015
1. [](#improved)
* Changed authorise to authorize
1. [](#bugfix)
* Fix denied string
# v0.3.2
## 09/01/2015
1. [](#improved)
* Broke out login form into its own partial
# v0.3.1
## 08/31/2015
1. [](#improved)
* Added username field autofocus
# v0.3.0
## 08/24/2015
1. [](#new)
* Added simple CSS styling
* Added simple login status with logout
1. [](#improved)
* Improved README documentation
* More strings translated
* Updated blueprints
# v0.2.0
## 08/11/2015
1. [](#improved)
* Disable `enable` in admin
# v0.1.0
## 08/04/2015
1. [](#new)
* ChangeLog started...
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Grav
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.
+490
View File
@@ -0,0 +1,490 @@
# Grav Login Plugin
The **login plugin** for [Grav](http://github.com/getgrav/grav) adds login, basic ACL, and session wide messages to Grav. It is designed to provide a way to secure front-end and admin content throughout Grav.
# Installation
The **login** plugin actually requires the help of the **email** and **form** plugins. The **email** plugin is needed to ensure that you can recover a password via email if required. The **form** plugin is used to generate the forms required.
These are available via GPM, and because the plugin has dependencies you just need to proceed and install the login plugin, and agree when prompted to install the others:
```
$ bin/gpm install login
```
# Changes in version 2.6
* User registration is now disabled by default. If you were relying on it being activated, you need to manually enable it in your `user/config/plugins/login.yaml`:
```
user_registration:
enabled: true
```
* `login_after_registration` has also been changed to a default value of `false` for security purposes.
# Changes in version 2.5
Added new `$grav['login']->login()` and `$grav['login']->logout()` functions for you to use.
They use following events which can be hooked by plugins:
* `onUserLoginAuthenticate` Allows plugins to include their own authentication methods.
* `onUserLoginAuthorize` Allows plugins to block user from being logged in.
* `onUserLoginFailure` Allows plugins to include their own logic when user authentication failed.
* `onUserLogin` Allows plugins to include their own logic when user logs in.
* `onUserLogout` Allows plugins to include their own logic when user logs out.
* `onUserLoginRegisterData` Allows plugins to include their own data to be added to the user object during registration.
* `onUserLoginRegistered` Allows plugins to hook into user registration just before the redirect.
New Plugin options have been added for:
* `dynamic_page_visibility` - Integrate access into page visibility so things can be shown or hidden in the menu
# Changes in version 2.0
* OAuth has been separated to its own plugin, needs to be installed separately and configured. The users account filename format has changed too, to fix an issue that involved people with the same name on a service.
* The `redirect` option has been changed to `redirect_after_login`.
* The Remember Me session minimum length is now 1 week.
* Removed the option to login from oauth without creating the corresponding user file under `user/accounts/`.
# Messages Output
There is not a guaranteed way to display system messages including those added by the Login plugin, so in order to see messages you will need to make sure your theme has a method to output the messages. This is done by adding a simple Twig include, and the best place to do this to ensure it's visible in all your pages, is to add it to the `partials/base.html.twig` (or whatever your base Twig template is called):
```twig
{% block messages %}
{% include 'partials/messages.html.twig' ignore missing %}
{% endblock %}
```
A good location is probably to add this right above where your content is going to be output.
# Creating Users
You can either use the built-in CLI capabilities, or you create a user manually by creating a new YAML file in your `user/acounts` folder.
# CLI Usage
The simplest way to create a new user is to simply run the `bin/plugin login newuser` command. This will take you through a few questions to gather information with which to create your user. You can also use inline arguments to avoid the interactive questions.
### Commands
| Command | Arguments | Explination |
|---------------|--------------------------------------|----------------------------|
|`newuser`|Aliases: `add-user`, `new-user`|Creates a new user (creates file in `user/accounts/`)
|| [ -u, --user=USER ] | The username. |
|| [ -p, --password=PASSWORD ] | The password. Ensure the password respects Grav's password policy. **Note that this option is not recommended because the password will be visible by users listing the processes.** |
|| [ -e, --email=EMAIL ] | The user email address. |
|| [ -P, --permissions=PERMISSIONS ] | The user permissions. It can be either `a` for Admin access only, `s` for Site access only and `b` for both Admin and Site access. |
|| [ -N, --fullname=FULLNAME ] | The user full name |
|| [ -t, --title=TITLE ] | The title of the user. Usually used as a subtext. Example: Admin, Collaborator, Developer |
|| [ -s, --state=STATE ] | The state of the account. Either `enabled` (default) or `disabled` |
|||
|`changepass`|Aliases: `newpass`, `passwd`|Changes password of the specified user (User file must exist)
|| [ -u, --user=USER ] | The username. |
|| [ -p, --password=PASSWORD ] | The new password. Ensure the password respects Grav's password policy. **Note that this option is not recommended because the password will be visible by users listing the processes.** |
### CLI Example
```
> bin/plugin login newuser -u joeuser -p 8c9sRCeBExAiwk -e joeuser@grav.org -P b -N "Joe User" -t "Site Administrator"
Creating new user
Success! User joeuser created.
```
### Interactive Example
```
> bin/plugin login newuser
Creating new user
Enter a username: joeuser
Enter a password: 8c9sRCeBExAiwk
Enter an email: joeuser@grav.org
Please choose a set of permissions:
[a] admin access
[s] site access
[b] admin and site access
> b
Enter a fullname: Joe User
Enter a title: Site Administrator
Please choose the state for the account:
[enabled ] Enabled
[disabled] Disabled
> enabled
Success! User joeuser created.
```
### Manual User Creation
Here is example user defined in `user/accounts/admin.yaml`:
```
password: password
email: youremail@mail.com
fullname: Johnny Appleseed
title: Site Administrator
access:
admin:
login: true
super: true
```
>> Note: the username is based on the name of the YAML file.
# Default Configuration
```yaml
enabled: true # Enable the plugin
built_in_css: true # Use built-in CSS
route: # Specific route for Login page (default is '/login')
redirect_to_login: true # If you try to access a page you don't have access to, should you redirect to login route
redirect_after_login: # Path to redirect to after a successful login (eg '/user_profile')
route_activate: '/activate_user' # Route for the user activation process
route_forgot: '/forgot_password' # Route for the forgot password process
route_reset: '/reset_password' # Route for the reset password process
route_profile: '/user_profile' # Route for the user profile page
route_register: '/user_register' # Route for the user registration page
route_unauthorized: '/user_unauthorized' # Route for a page to display if user is unauthorized
dynamic_page_visibility: false # Integrate access into page visibility so things can be shown or hidden in the menu
parent_acl: false # Look to parent `access` rules for access requirements
protect_protected_page_media: false # Take `access` rules into account when directly accessing a page's media
rememberme:
enabled: true # Enable 'remember me' functionality
timeout: 604800 # Timeout in seconds. Defaults to 1 week
name: grav-rememberme # Name prefix of the session cookie
max_pw_resets_count: 0 # Number of password resets in a specific time frame (0 = unlimited)
max_pw_resets_interval: 60 # Time in minutes to track password resets
max_login_count: 0 # Number of failed login attempts in a specific time frame (0 = unlimited)
max_login_interval: 2 # Time in minutes to track login attempts
```
# Usage
You can add ACL to any page by typing something like below into the page header:
```
access:
site.login: true
admin.login: true
```
Users who have any of the listed ACL roles enabled will have access to the page.
Others will be forwarded to login screen.
Because the admin user contains an `admin.login: true` reference he will be able to login to the secured page because that is one of the conditions defined in the page header. You are free to create any specific set of ACL rules you like. Your user account must simply contain those same rules if you wish the user to have access.
## Create Private Areas
Enabling the setting "Use parent access rules" (`parent_acl` in login.yaml) allows you to create private areas where you set the access level on the parent page, and all the subpages inherit that requirement.
# Login Page
>> Note: the **frontend site** and **admin plugin** use different sessions so you need to explicitly provide a login on the frontend.
The login plugin can **automatically generate** a login page for you when you try to access a page that your user (or guest account) does not have access to.
Alternatively, you can also provide a specific login route if you wish to forward users to a specific login page. To do this you need to create a copy of the `login.yaml` from the plugin in your `user/config/plugins` folder and provide a specific route (or just edit the plugin setttings in the admin plugin).
```
route: /user-login
```
You would then need to provide a suitable login form, probably based on the one that is provided with the plugin.
## Redirection after Login
By default Grav will redirect to the prior page visited before entering the login process. Any page is fair game unless you manually set:
```
login_redirect_here: false
```
In the page's header. If you set this value to `false`, this page will not be a valid redirect page, and the page visited prior to this page will be considered.
You can override this default behavior by forcing a standard location by specifying an explicit option in your Login configuration YAML:
```
redirect_after_login: '/profile'
```
This will always take you to the `/profile` route after a successful login.
# Logout
The login plugin comes with a simple Twig partial to provide a logout link (`login-status.html.twig`). You will need to include it in your theme however. An example of this can be found in the Antimatter theme's `partials/navigation.html.twig` file:
```
{% if config.plugins.login.enabled and grav.user.username %}
<li><i class="fa fa-lock"></i> {% include 'partials/login-status.html.twig' %}</li>
{% endif %}
```
You can also copy this `login-status.html.twig` file into your theme and modify it as you see fit.
# Allow User Registration
The Login plugin handles user registration.
To enable the built-in registration form, in the Login Plugin configuration enable user registration and just add a value to the "Registration path" field.
Then just open your browser on that page, and you'll be presented a registration form.
## Adding the registration page to the menu
Here are two ways you can do it, but of course Grav is flexible and you can come up with other ways too.
The first and easiest way is to add a page with the same slug (route) as the registration form. So for example if in the Login Plugin settings you set /register as the registration form path, then create a `04.register` page (the 04 number is just an example, use your own ordering), with no content.
The Login plugin will "override" that page, serving the registration page form when the user clicks on that menu item.
A second way is to add a custom menu item that points to the registration page, by editing `site.yaml` with this code, that will append a "Register" menu item:
```
menu:
-
url: 'register'
text: Register
```
This works in most themes, Antimatter included, but it's not guaranteed to work in all themes, as it's something that must be added to the navigation twig code.
## Customizing the registration form
The provided registration form is just a quick way to start using it. You might however need different fields on the registration form, or you want to add more content. Here's how to do it.
First, create a registration form page.
Create a folder `04.registration/form.md`. The folder name is just an example. Pick the one that suits you. The important part is the file name: since we're building a form, we need a `form.md` file.
Also, your theme needs to implement forms. Use Antimatter or another form-compatible theme if yours does not work, then once you're setup with the form you can migrate the forms files and make it work on your theme too.
Add the following content to your registration form page:
```yaml
---
form:
fields:
fullname:
type: text
validate:
required: true
username:
type: text
validate:
required: true
message: PLUGIN_LOGIN.USERNAME_NOT_VALID
config-pattern@: system.username_regex
email:
type: email
validate:
required: true
message: PLUGIN_LOGIN.EMAIL_VALIDATION_MESSAGE
password1:
type: password
label: Enter a password
validate:
required: true
message: PLUGIN_LOGIN.PASSWORD_VALIDATION_MESSAGE
config-pattern@: system.pwd_regex
password2:
type: password
label: Enter the password again
validate:
required: true
message: PLUGIN_LOGIN.PASSWORD_VALIDATION_MESSAGE
config-pattern@: system.pwd_regex
buttons:
-
type: submit
value: Submit
-
type: reset
value: Reset
process:
register_user: true
message: "Thanks for registering..."
reset: true
---
```
# Registration of Users
Create a new user account by entering all the required fields below:
This is a normal form. The only thing different from a contact form or another form that you might write on your site is the process field `register_user`, which takes care of processing the user registration.
Once the user is registered, Grav redirects the user to the `display` page with the `message` message.
The only field strictly required by Grav is `username`. Then the other fields can be added as needed.
For example in this case we added
- password1
- password2
to the form. And, in the Login plugin configuration we have by default enable the double password verification with the "Validate double entered password" option. What this does is picking the password1 and password2 fields, validate them, check they are equal and put the content in the `password` field.
You can avoid having 2 fields for the password, which by the way is a recommended option, and just put a single `password` field.
Last important thing before the registration is correctly setup: make sure in the Login plugin settings you have the user registration enabled, otherwise the registration will trigger an error, as by default user registration is DISABLED.
# Registration Options
There are several options that can be configured when registering users via `user/plugins/login.yaml`, they are pretty self-explanatory:
```yaml
user_registration:
enabled: false # Enable User Registration Process
fields: # List of fields to validate and store during user registration
- 'username' # This should match up with your registration form definition
- 'password'
- 'email'
- 'fullname'
- 'title'
- 'level'
default_values: # Any default values for fields you would like to set
level: Newbie # Here the 'level' field will be pre-populated with 'Newbie' text
access: # Default access to set for users created during registration
site:
login: 'true'
redirect_after_registration: '' # Route to redirect to after registration
options:
validate_password1_and_password2: true # Ensure that password1 and password2 match during registration (allows you to have just 1 pw field or 2)
set_user_disabled: false # Set this `true` if you want a user to activate their account via email
login_after_registration: false # Automatically login after registration
send_activation_email: false # Send an email that requires a special link to be clicked in order to activate the account
send_notification_email: false # Send an email to the site administrator to indicate a user has registered
send_welcome_email: false # Send a welcome email to the user (probably should not be used with `send_activation_email`
```
## Sending an activation email
By default the registration process adds a new user, and sets it as enabled.
Grav allows disabled user accounts, so we can take advantage of this functionality and add a new user, but with a disabled state. Then we can send an email to the user, asking to validate the email address.
That validation email will contain a link to set the user account to enabled. To do this, just enable "Set the user as disabled" and "Send activation email" in the Login Plugin options.
## Send a welcome email
Enable "Send welcome email" in the options.
The content of the welcome email is defined in the language file, strings `PLUGIN_LOGIN.WELCOME_EMAIL_SUBJECT` and `PLUGIN_LOGIN.WELCOME_EMAIL_BODY`. Customize them as needed in your language file override.
Note: if the activation email is enabled, the welcome email to be sent upon the account activation action (when the user clicks the link to activate the account)
## Send a notification email to the site owner
Enable "Send notification email" in the options.
The content of the notification email is defined in the language file, strings `PLUGIN_LOGIN.NOTIFICATION_EMAIL_SUBJECT` and `PLUGIN_LOGIN.NOTIFICATION_EMAIL_BODY`. Customize them as needed in your language file override.
Note: if the activation email is enabled, the notification email to be sent upon the account activation action (when the user clicks the link to activate the account)
## Default Access
To control what access your users have upon registering you can edit the `user_registration.access:` attribute in the `user/plugins/login.yaml`. The default is simply `site.login: true`:
```
user_registration:
access:
site:
login: 'true'
```
## Adding your own fields
If you want to add your own custom fields to the registration form, just add fields to the form like you would with any other form.
Then, to let the Login plugin add those fields to the user yaml file, you also need to add it to the "Registration fields" option in the Login Plugin configuration.
By default we have
```
user_registration:
fields:
- 'username'
- 'password'
- 'email'
- 'fullname'
- 'title'
```
Add your own as you prefer, to build any custom registration form you can think of.
## Specifying a default value for a field
If you want to pre-fill a field, without showing it to the user in the form, you could set it as an hidden field. But the user could see it - and modify it via the browser dev tools.
To add a field and make sure the user cannot modify it, add it to "default_values" list:
```
user_registration:
default_values:
title: "Newbie User"
```
## Login users directly after the registration
Just enable "Login the user after registration"
If the user activation email is enabled, the user will be logged in as soon as the activation link is clicked.
## Add captcha to the user registration
Add a captcha like you would with any form:
Add
```
- name: g-recaptcha-response
label: Captcha
type: captcha
recaptcha_site_key: aeio43kdk3idko3k4ikd4
recaptcha_not_validated: 'Captcha not valid!'
validate:
required: true
```
to the form field, and
```
process:
- captcha
```
to validate it server-side. Put this process action before all the other actions, so it's processed first and the user is not created if the captcha is not valid.
## Redirect to another page after login
You can set the "Redirect after registration" option in the Login plugin, or as with any form, use the `process.display` property, and set it to the destination page route:
```
process:
-
display: /welcome
```
# Known issues
When updating from an older version, pre-october 2015, you might have an error `Class 'Grav\Login\Controller' Not Found`. The problem is during the update, since a file name was changed from lowercase to capitalized. Solution: reinstall the Login plugin, or change the file name `user/plugins/login/classes/controller.php` to `user/plugins/login/classes/Controller.php` (notice the capital `C`).
+385
View File
@@ -0,0 +1,385 @@
name: Login
version: 2.7.3
description: Enables user authentication and login screen.
icon: sign-in
author:
name: Team Grav
email: devs@getgrav.org
url: http://getgrav.org
homepage: https://github.com/getgrav/grav-plugin-login
keywords: login, authentication, admin, security
bugs: https://github.com/getgrav/grav-plugin-login/issues
license: MIT
dependencies:
- { name: grav, version: '>=1.4.4' }
- { name: form, version: '>=2.13.4' }
- { name: email, version: '>=2.7.0' }
form:
validation: loose
fields:
tabs:
type: tabs
active: 1
class: subtle
fields:
login:
type: tab
title: PLUGIN_LOGIN.BTN_LOGIN
fields:
enabled:
type: hidden
label: PLUGIN_LOGIN.PLUGIN_STATUS
highlight: 1
default: 1
options:
1: PLUGIN_ADMIN.ENABLED
0: PLUGIN_ADMIN.DISABLED
validate:
type: bool
built_in_css:
type: toggle
label: PLUGIN_LOGIN.BUILTIN_CSS
highlight: 1
default: 1
help: PLUGIN_LOGIN.BUILTIN_CSS_HELP
options:
1: PLUGIN_ADMIN.ENABLED
0: PLUGIN_ADMIN.DISABLED
validate:
type: bool
route:
type: text
size: medium
label: PLUGIN_LOGIN.ROUTE
help: PLUGIN_LOGIN.ROUTE_HELP
placeholder: "/my-custom-login"
redirect_after_login:
type: text
label: PLUGIN_LOGIN.REDIRECT_AFTER_LOGIN
help: PLUGIN_LOGIN.REDIRECT_AFTER_LOGIN_HELP
placeholder: "/my-page"
redirect_after_logout:
type: text
label: PLUGIN_LOGIN.REDIRECT_AFTER_LOGOUT
help: PLUGIN_LOGIN.REDIRECT_AFTER_LOGOUT_HELP
placeholder: "/"
route_forgot:
type: text
size: medium
label: PLUGIN_LOGIN.ROUTE_FORGOT
placeholder: "/forgot_password"
route_reset:
type: text
size: medium
label: PLUGIN_LOGIN.ROUTE_RESET
placeholder: "/reset_password"
route_profile:
type: text
size: medium
label: PLUGIN_LOGIN.ROUTE_PROFILE
placeholder: "/user_profile"
parent_acl:
type: toggle
label: PLUGIN_LOGIN.USE_PARENT_ACL_LABEL
highlight: 1
default: 0
help: PLUGIN_LOGIN.USE_PARENT_ACL_HELP
options:
1: PLUGIN_ADMIN.ENABLED
0: PLUGIN_ADMIN.DISABLED
validate:
type: bool
dynamic_page_visibility:
type: toggle
label: PLUGIN_LOGIN.DYNAMIC_VISIBILITY
highlight: 0
default: 0
help: PLUGIN_LOGIN.DYNAMIC_VISIBILITY_HELP
options:
1: PLUGIN_ADMIN.ENABLED
0: PLUGIN_ADMIN.DISABLED
validate:
type: bool
protect_protected_page_media:
type: toggle
label: PLUGIN_LOGIN.PROTECT_PROTECTED_PAGE_MEDIA_LABEL
highlight: 1
default: 0
help: PLUGIN_LOGIN.PROTECT_PROTECTED_PAGE_MEDIA_HELP
options:
1: PLUGIN_ADMIN.ENABLED
0: PLUGIN_ADMIN.DISABLED
validate:
type: bool
routes:
type: section
title: PLUGIN_LOGIN.ROUTES
fields:
route_activate:
type: text
size: medium
label: PLUGIN_LOGIN.ROUTE_ACTIVATE
placeholder: '/activate_user'
route_forgot:
type: text
size: medium
label: PLUGIN_LOGIN.ROUTE_FORGOT
placeholder: '/forgot_password'
route_reset:
type: text
size: medium
label: PLUGIN_LOGIN.ROUTE_RESET
placeholder: '/reset_password'
route_profile:
type: text
size: medium
label: PLUGIN_LOGIN.ROUTE_PROFILE
placeholder: '/user_profile'
route_register:
type: text
size: medium
label: PLUGIN_LOGIN.ROUTE_REGISTER
help: PLUGIN_LOGIN.ROUTE_REGISTER_HELP
placeholder: "/register"
user_registration.redirect_after_registration:
type: text
label: PLUGIN_LOGIN.REDIRECT_AFTER_REGISTRATION
help: PLUGIN_LOGIN.REDIRECT_AFTER_REGISTRATION_HELP
placeholder: "/page-to-show-after-registration"
user_registration.redirect_after_activation:
type: text
label: PLUGIN_LOGIN.REDIRECT_AFTER_ACTIVATION
help: PLUGIN_LOGIN.REDIRECT_AFTER_ACTIVATION_HELP
placeholder: "/page-to-show-after-activation"
rememberme:
type: section
title: PLUGIN_LOGIN.REMEMBER_ME
fields:
rememberme.enabled:
type: toggle
label: PLUGIN_ADMIN.ENABLED
help: PLUGIN_ADMIN.SESSION_ENABLED_HELP
highlight: 1
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
rememberme.timeout:
type: text
size: small
default: 604800
label: PLUGIN_ADMIN.TIMEOUT
help: PLUGIN_LOGIN.TIMEOUT_HELP
validate:
type: number
min: 1
rememberme.name:
type: text
size: small
label: PLUGIN_ADMIN.NAME
help: PLUGIN_ADMIN.SESSION_NAME_HELP
registration:
type: tab
title: PLUGIN_LOGIN.USER_REGISTRATION
fields:
user_registration.enabled:
type: toggle
label: PLUGIN_ADMIN.ENABLED
help: PLUGIN_LOGIN.USER_REGISTRATION_ENABLED_HELP
highlight: 0
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
registration_fields:
type: section
title: PLUGIN_LOGIN.REGISTRATION_FIELDS
fields:
user_registration.fields:
type: array
value_only: true
label: PLUGIN_LOGIN.REGISTRATION_FIELDS
help: PLUGIN_LOGIN.REGISTRATION_FIELDS_HELP
placeholder_key: PLUGIN_LOGIN.REGISTRATION_FIELD_KEY
placeholder_value: PLUGIN_LOGIN.ADDITIONAL_PARAM_VALUE
user_registration.default_values:
type: array
label: PLUGIN_LOGIN.DEFAULT_VALUES
help: PLUGIN_LOGIN.DEFAULT_VALUES_HELP
placeholder_key: PLUGIN_LOGIN.ADDITIONAL_PARAM_KEY
placeholder_value: PLUGIN_LOGIN.ADDITIONAL_PARAM_VALUE
access_levels:
title: PLUGIN_ADMIN.ACCESS_LEVELS
type: section
security: admin.super
fields:
user_registration.groups:
type: selectize
size: large
label: PLUGIN_ADMIN.GROUPS
'@data-options': '\Grav\User\Groups::groups'
classes: fancy
help: PLUGIN_LOGIN.GROUPS_HELP
validate:
type: commalist
user_registration.access.site:
type: array
label: PLUGIN_ADMIN.SITE_ACCESS
help: PLUGIN_LOGIN.SITE_ACCESS_HELP
multiple: false
validate:
type: array
options:
type: section
title: PLUGIN_LOGIN.OPTIONS
fields:
user_registration.options.validate_password1_and_password2:
type: toggle
label: PLUGIN_LOGIN.VALIDATE_PASSWORD1_AND_PASSWORD2
help: PLUGIN_LOGIN.VALIDATE_PASSWORD1_AND_PASSWORD2_HELP
highlight: 1
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
user_registration.options.set_user_disabled:
type: toggle
label: PLUGIN_LOGIN.SET_USER_DISABLED
help: PLUGIN_LOGIN.SET_USER_DISABLED_HELP
highlight: 1
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
user_registration.options.login_after_registration:
type: toggle
label: PLUGIN_LOGIN.LOGIN_AFTER_REGISTRATION
help: PLUGIN_LOGIN.LOGIN_AFTER_REGISTRATION_HELP
highlight: 0
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
user_registration.options.send_activation_email:
type: toggle
label: PLUGIN_LOGIN.SEND_ACTIVATION_EMAIL
help: PLUGIN_LOGIN.SEND_ACTIVATION_EMAIL_HELP
highlight: 1
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
user_registration.options.send_notification_email:
type: toggle
label: PLUGIN_LOGIN.SEND_NOTIFICATION_EMAIL
help: PLUGIN_LOGIN.SEND_NOTIFICATION_EMAIL_HELP
highlight: 1
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
user_registration.options.send_welcome_email:
type: toggle
label: PLUGIN_LOGIN.SEND_WELCOME_EMAIL
help: PLUGIN_LOGIN.SEND_WELCOME_EMAIL_HELP
highlight: 1
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
Security:
type: tab
title: PLUGIN_LOGIN.SECURITY_TAB
fields:
max_pw_resets_count:
type: number
size: x-small
label: PLUGIN_LOGIN.MAX_RESETS_COUNT
help: PLUGIN_LOGIN.MAX_RESETS_COUNT_HELP
append: PLUGIN_LOGIN.RESETS
validate:
type: number
min: 0
max_pw_resets_interval:
type: number
size: x-small
label: PLUGIN_LOGIN.MAX_RESETS_INTERVAL
help: PLUGIN_LOGIN.MAX_RESETS_INTERVAL_HELP
append: PLUGIN_LOGIN.SECONDS
validate:
type: number
min: 1
max_login_count:
type: number
size: x-small
label: PLUGIN_LOGIN.MAX_LOGINS_COUNT
help: PLUGIN_LOGIN.MAX_LOGINS_COUNT_HELP
append: PLUGIN_LOGIN.ATTEMPTS
validate:
type: number
min: 0
max_login_interval:
type: number
size: x-small
label: PLUGIN_LOGIN.MAX_LOGINS_INTERVAL
help: PLUGIN_LOGIN.MAX_LOGINS_INTERVAL_HELP
append: PLUGIN_LOGIN.SECONDS
validate:
type: number
min: 1
+551
View File
@@ -0,0 +1,551 @@
<?php
/**
* @package Grav\Plugin\Login
*
* @copyright Copyright (C) 2014 - 2017 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Plugin\Login;
use Grav\Common\Grav;
use Grav\Common\Language\Language;
use Grav\Common\Uri;
use Grav\Common\User\User;
use Grav\Common\Utils;
use Grav\Plugin\Email\Utils as EmailUtils;
use Grav\Plugin\Login\TwoFactorAuth\TwoFactorAuth;
use Grav\Plugin\LoginPlugin;
use RocketTheme\Toolbox\Session\Message;
/**
* Class Controller
* @package Grav\Plugin\Login
*/
class Controller
{
/**
* @var Grav
*/
public $grav;
/**
* @var string
*/
public $action;
/**
* @var array
*/
public $post;
/**
* @var string
*/
protected $redirect;
/**
* @var int
*/
protected $redirectCode;
/**
* @var string
*/
protected $prefix = 'task';
/**
* @var RememberMe\RememberMe
* @deprecated 2.0 Use $grav['login']->rememberMe() instead
*/
protected $rememberMe;
/**
* @var Login
*/
protected $login;
/**
* @param Grav $grav
* @param string $action
* @param array $post
*/
public function __construct(Grav $grav, $action, $post = null)
{
$this->grav = $grav;
$this->action = $action;
$this->login = $this->grav['login'];
$this->post = $post ? $this->getPost($post) : [];
$this->rememberMe();
}
/**
* Performs an action.
* @throws \RuntimeException
*/
public function execute()
{
$messages = $this->grav['messages'];
// Set redirect if available.
if (isset($this->post['_redirect'])) {
$redirect = $this->post['_redirect'];
unset($this->post['_redirect']);
}
$success = false;
$method = $this->prefix . ucfirst($this->action);
if (!method_exists($this, $method)) {
throw new \RuntimeException('Page Not Found', 404);
}
try {
$success = call_user_func([$this, $method]);
} catch (\RuntimeException $e) {
$messages->add($e->getMessage(), 'error');
$this->grav['log']->error('plugin.login: '. $e->getMessage());
}
if (!$this->redirect && isset($redirect)) {
$this->setRedirect($redirect, 303);
}
return $success;
}
/**
* Handle login.
*
* @return bool True if the action was performed.
*/
public function taskLogin()
{
/** @var Language $t */
$t = $this->grav['language'];
/** @var Message $messages */
$messages = $this->grav['messages'];
$userKey = isset($this->post['username']) ? (string)$this->post['username'] : '';
$ipKey = Uri::ip();
$rateLimiter = $this->login->getRateLimiter('login_attempts');
// Check if the current IP has been used in failed login attempts.
$attempts = count($rateLimiter->getAttempts($ipKey, 'ip'));
$rateLimiter->registerRateLimitedAction($ipKey, 'ip')->registerRateLimitedAction($userKey);
// Check rate limit for both IP and user, but allow each IP a single try even if user is already rate limited.
if ($rateLimiter->isRateLimited($ipKey, 'ip') || ($attempts && $rateLimiter->isRateLimited($userKey))) {
$messages->add($t->translate(['PLUGIN_LOGIN.TOO_MANY_LOGIN_ATTEMPTS', $rateLimiter->getInterval()]), 'error');
$this->setRedirect($this->grav['config']->get('plugins.login.route', '/'));
return true;
}
// Remove login nonce from the form.
$form = array_diff_key($this->post, ['login-form-nonce' => true]);
// Fire Login process.
$event = $this->login->login($form, ['remember_me' => true], ['return_event' => true]);
$user = $event->getUser();
if ($user->authenticated) {
$rateLimiter->resetRateLimit($ipKey, 'ip')->resetRateLimit($userKey);
if ($user->authorized) {
$event->defMessage('PLUGIN_LOGIN.LOGIN_SUCCESSFUL', 'info');
$event->defRedirect(
$this->grav['session']->redirect_after_login ?: $this->grav['uri']->referrer('/')
);
} else {
$login_route = $this->grav['config']->get('plugins.login.route');
if ($login_route) {
$event->defRedirect($login_route);
}
}
} else {
if ($user->authorized) {
$event->defMessage('PLUGIN_LOGIN.ACCESS_DENIED', 'error');
$event->defRedirect($this->grav['config']->get('plugins.login.route_unauthorized', '/'));
} else {
$event->defMessage('PLUGIN_LOGIN.LOGIN_FAILED', 'error');
}
}
$message = $event->getMessage();
if ($message) {
$messages->add($t->translate($message), $event->getMessageType());
}
$redirect = $event->getRedirect();
if ($redirect) {
$this->setRedirect($redirect, $event->getRedirectCode());
}
return true;
}
public function taskTwoFa()
{
/** @var Language $t */
$t = $this->grav['language'];
/** @var Message $messages */
$messages = $this->grav['messages'];
/** @var TwoFactorAuth $twoFa */
$twoFa = $this->grav['login']->twoFactorAuth();
$user = $this->grav['user'];
$code = isset($this->post['2fa_code']) ? $this->post['2fa_code'] : null;
$secret = isset($user->twofa_secret) ? $user->twofa_secret : null;
if (!$code || !$secret || !$twoFa->verifyCode($secret, $code)) {
$messages->add($t->translate('PLUGIN_LOGIN.2FA_FAILED'), 'error');
$user->authenticated = false;
$login_route = $this->grav['config']->get('plugins.login.route');
if ($login_route) {
$this->setRedirect($login_route, 303);
}
return true;
}
$messages->add($t->translate('PLUGIN_LOGIN.LOGIN_SUCCESSFUL'), 'info');
$user->authorized = true;
$this->setRedirect(
$this->grav['session']->redirect_after_login
?: $this->grav['config']->get('plugins.login.redirect_after_login')
?: $this->grav['uri']->referrer('/')
);
return true;
}
/**
* Handle logout.
*
* @return bool True if the action was performed.
*/
public function taskLogout()
{
$event = $this->login->logout(['remember_me' => true], ['return_event' => true]);
$message = $event->getMessage();
if ($message) {
/** @var Language $t */
$t = $this->grav['language'];
$messages = $this->grav['messages'];
$messages->add($t->translate($message), $event->getMessageType());
}
$redirect = $event->getRedirect() ?: $this->grav['config']->get('plugins.login.redirect_after_logout');
if ($redirect) {
$this->setRedirect($redirect, $event->getRedirectCode());
}
$this->grav['session']->setFlashCookieObject(LoginPlugin::TMP_COOKIE_NAME, ['message' => $this->grav['language']->translate('PLUGIN_LOGIN.LOGGED_OUT'),
'status' => 'info']);
return true;
}
/**
* Handle the email password recovery procedure.
*
* @return bool True if the action was performed.
*/
protected function taskForgot()
{
$param_sep = $this->grav['config']->get('system.param_sep', ':');
$data = $this->post;
$email = isset($data['email']) ? $data['email'] : '';
$user = !empty($email) ? User::find($email, ['email']) : null;
/** @var Language $language */
$language = $this->grav['language'];
$messages = $this->grav['messages'];
if (!isset($this->grav['Email'])) {
$messages->add($language->translate('PLUGIN_LOGIN.FORGOT_EMAIL_NOT_CONFIGURED'), 'error');
$this->setRedirect($this->grav['config']->get('plugins.login.route_forgot', '/'));
return true;
}
if (!$user || !$user->exists()) {
$messages->add($language->translate('PLUGIN_LOGIN.FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL'), 'info');
$this->setRedirect($this->grav['config']->get('plugins.login.route_forgot', '/'));
return true;
}
if (empty($user->email)) {
$messages->add($language->translate(['PLUGIN_LOGIN.FORGOT_CANNOT_RESET_EMAIL_NO_EMAIL', $email]),
'error');
$this->setRedirect($this->grav['config']->get('plugins.login.route_forgot', '/'));
return true;
}
if (empty($user->password) && empty($user->hashed_password)) {
$messages->add($language->translate(['PLUGIN_LOGIN.FORGOT_CANNOT_RESET_EMAIL_NO_PASSWORD', $email]),
'error');
$this->setRedirect($this->grav['config']->get('plugins.login.route_forgot', '/'));
return true;
}
$from = $this->grav['config']->get('plugins.email.from');
if (empty($from)) {
$messages->add($language->translate('PLUGIN_LOGIN.FORGOT_EMAIL_NOT_CONFIGURED'), 'error');
$this->setRedirect($this->grav['config']->get('plugins.login.route_forgot', '/'));
return true;
}
$userKey = $user->username;
$rateLimiter = $this->login->getRateLimiter('pw_resets');
$rateLimiter->registerRateLimitedAction($userKey);
if ($rateLimiter->isRateLimited($userKey)) {
$messages->add($language->translate(['PLUGIN_LOGIN.FORGOT_CANNOT_RESET_IT_IS_BLOCKED', $email, $rateLimiter->getInterval()]), 'error');
$this->setRedirect($this->grav['config']->get('plugins.login.route', '/'));
return true;
}
$token = md5(uniqid(mt_rand(), true));
$expire = time() + 604800; // next week
$user->reset = $token . '::' . $expire;
$user->save();
$author = $this->grav['config']->get('site.author.name', '');
$fullname = $user->fullname ?: $user->username;
$reset_link = $this->grav['base_url_absolute'] . $this->grav['config']->get('plugins.login.route_reset') . '/task:login.reset/token' . $param_sep . $token . '/user' . $param_sep . $user->username . '/nonce' . $param_sep . Utils::getNonce('reset-form');
$sitename = $this->grav['config']->get('site.title', 'Website');
$to = $user->email;
$subject = $language->translate(['PLUGIN_LOGIN.FORGOT_EMAIL_SUBJECT', $sitename]);
$content = $language->translate(['PLUGIN_LOGIN.FORGOT_EMAIL_BODY', $fullname, $reset_link, $author, $sitename]);
$sent = EmailUtils::sendEmail($subject, $content, $to);
if ($sent < 1) {
$messages->add($language->translate('PLUGIN_LOGIN.FORGOT_FAILED_TO_EMAIL'), 'error');
} else {
$messages->add($language->translate('PLUGIN_LOGIN.FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL'), 'info');
}
$this->setRedirect($this->grav['config']->get('plugins.login.route', '/'));
return true;
}
/**
* Handle the reset password action.
*
* @return bool True if the action was performed.
* @throws \Exception
*/
public function taskReset()
{
$data = $this->post;
$language = $this->grav['language'];
$messages = $this->grav['messages'];
if (isset($data['password'])) {
$username = isset($data['username']) ? $data['username'] : null;
$user = !empty($username) ? User::find($username) : null;
$password = isset($data['password']) ? $data['password'] : null;
$token = isset($data['token']) ? $data['token'] : null;
if ($user && !empty($user->reset) && $user->exists()) {
list($good_token, $expire) = explode('::', $user->reset);
if ($good_token === $token) {
if (time() > $expire) {
$messages->add($language->translate('PLUGIN_LOGIN.RESET_LINK_EXPIRED'), 'error');
$this->grav->redirect($this->grav['config']->get('plugins.login.route_forgot', '/'));
return true;
}
unset($user->hashed_password, $user->reset);
$user->password = $password;
$user->validate();
$user->filter();
$user->save();
$messages->add($language->translate('PLUGIN_LOGIN.RESET_PASSWORD_RESET'), 'info');
$this->setRedirect($this->grav['config']->get('plugins.login.route', '/'));
return true;
}
}
$messages->add($language->translate('PLUGIN_LOGIN.RESET_INVALID_LINK'), 'error');
$this->grav->redirect($this->grav['config']->get('plugins.login.route_forgot'));
return true;
}
$user = $this->grav['uri']->param('user');
$token = $this->grav['uri']->param('token');
if (!$user || !$token) {
$messages->add($language->translate('PLUGIN_LOGIN.RESET_INVALID_LINK'), 'error');
$this->grav->redirect($this->grav['config']->get('plugins.login.route_forgot'));
return true;
}
return true;
}
/**
* Redirects an action
*/
public function redirect()
{
if ($this->redirect) {
$this->grav->redirect($this->redirect, $this->redirectCode);
}
}
/**
* Set redirect.
*
* @param $path
* @param int $code
*/
public function setRedirect($path, $code = 303)
{
$this->redirect = $path;
$this->redirectCode = $code;
}
/**
* @return array Array containing [redirect, code].
*/
public function getRedirect()
{
return [$this->redirect, $this->redirectCode];
}
/**
* Prepare and return POST data.
*
* @param array $post
*
* @return array
*/
protected function &getPost(array $post)
{
unset($post[$this->prefix]);
// Decode JSON encoded fields and merge them to data.
if (isset($post['_json'])) {
$post = array_merge_recursive($post, $this->jsonDecode($post['_json']));
unset($post['_json']);
}
return $post;
}
/**
* Recursively JSON decode data.
*
* @param array $data
*
* @return array
*/
protected function jsonDecode(array $data)
{
foreach ($data as &$value) {
if (is_array($value)) {
$value = $this->jsonDecode($value);
} else {
$value = json_decode($value, true);
}
}
return $data;
}
/**
* Gets and sets the RememberMe class
*
* @param mixed $var A rememberMe instance to set
*
* @return RememberMe\RememberMe Returns the current rememberMe instance
* @deprecated 2.5.0 Use $grav['login']->rememberMe() instead
*/
public function rememberMe($var = null)
{
$this->rememberMe = $this->login->rememberMe($var);
return $this->rememberMe;
}
/**
* Check if user may use password reset functionality.
*
* @param User $user
* @param $field
* @param $count
* @param $interval
* @return bool
* @deprecated 2.5.0 Use $grav['login']->getRateLimiter($context) instead. See Grav\Plugin\Login\RateLimiter class.
*/
protected function isUserRateLimited(User $user, $field, $count, $interval)
{
return $this->login->isUserRateLimited($user, $field, $count, $interval);
}
/**
* Reset the rate limit counter
*
* @param User $user
* @param $field
* @deprecated 2.5.0 Use $grav['login']->getRateLimiter($context) instead. See Grav\Plugin\Login\RateLimiter class.
*/
protected function resetRateLimit(User $user, $field)
{
$this->login->resetRateLimit($user, $field);
}
/**
* Authenticate user.
*
* @param array $form Form fields.
*
* @return bool
* @deprecated 2.6.2 Will be removed without replacement.
*/
protected function authenticate($form)
{
// Remove login nonce.
$form = array_diff_key($form, ['login-form-nonce' => true]);
return $this->login->login($form, ['remember_me' => true])->authenticated;
}
}
@@ -0,0 +1,347 @@
<?php
/**
* @package Grav\Plugin\Login
*
* @copyright Copyright (C) 2014 - 2017 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Plugin\Login\Events;
use Grav\Common\Grav;
use Grav\Common\Session;
use Grav\Common\User\User;
use RocketTheme\Toolbox\Event\Event;
/**
* Class UserLoginEvent
* @package Grav\Common\User\Events
*
* @property int $status
* @property array $credentials
* @property string|string[] $authorize
* @property array $options
* @property Session $session
* @property User $user
* @property string $message
*
*/
class UserLoginEvent extends Event
{
/**
* Undefined event state.
*/
const AUTHENTICATION_UNDEFINED = 0;
/**
* onUserAuthenticate success.
*/
const AUTHENTICATION_SUCCESS = 1;
/**
* onUserAuthenticate fails on bad username/password.
*/
const AUTHENTICATION_FAILURE = 2;
/**
* onUserAuthenticate fails on auth cancellation.
*/
const AUTHENTICATION_CANCELLED = 4;
/**
* onUserAuthorizeLogin fails on expired account.
*/
const AUTHORIZATION_EXPIRED = 8;
/**
* onUserAuthorizeLogin is delayed until user has performed extra action(s).
*/
const AUTHORIZATION_DELAYED = 16;
/**
* onUserAuthorizeLogin fails for other reasons.
*/
const AUTHORIZATION_DENIED = 32;
/**
* UserLoginEvent constructor.
* @param array $items
*/
public function __construct(array $items = [])
{
$items += [
'credentials' => [],
'options' => [],
'authorize' => 'site.login',
'status' => static::AUTHENTICATION_UNDEFINED,
'session' => null,
'user' => null,
'message' => null,
'redirect' => null,
'redirect_code' => 303
];
$items['credentials'] += ['username' => '', 'password' => ''];
parent::__construct($items);
if (!$this->offsetExists('session') && isset(Grav::instance()['session'])) {
$this->offsetSet('session', Grav::instance()['session']);
}
if (!$this->offsetExists('user')) {
$this->offsetSet('user', User::load($this['credentials']['username']));
}
}
public function isSuccess()
{
$status = $this->offsetGet('status');
$failure = static::AUTHENTICATION_FAILURE | static::AUTHENTICATION_CANCELLED | static::AUTHORIZATION_EXPIRED
| static::AUTHORIZATION_DENIED;
return ($status & static::AUTHENTICATION_SUCCESS) && !($status & $failure);
}
public function isDelayed()
{
return $this->isSuccess() && ($this->offsetGet('status') & static::AUTHORIZATION_DELAYED);
}
/**
* @return int
*/
public function getStatus()
{
return (int)$this->offsetGet('status');
}
/**
* @param int $status
* @return $this
*/
public function setStatus($status)
{
$this->offsetSet('status', $this->offsetGet('status') | (int)$status);
return $this;
}
/**
* @return array
*/
public function getCredentials()
{
return $this->offsetGet('credentials') + ['username' => '', 'password' => ''];
}
/**
* @param string $name
* @return mixed
*/
public function getCredential($name)
{
return isset($this->items['credentials'][$name]) ? $this->items['credentials'][$name] : null;
}
/**
* @param string $name
* @param mixed $value
* @return $this
*/
public function setCredential($name, $value)
{
$this->items['credentials'][$name] = $value;
return $this;
}
/**
* @return array
*/
public function getOptions()
{
return $this->offsetGet('options');
}
/**
* @param string $name
* @return mixed
*/
public function getOption($name)
{
return isset($this->items['options'][$name]) ? $this->items['options'][$name] : null;
}
/**
* @param string $name
* @param mixed $value
* @return $this
*/
public function setOption($name, $value)
{
$this->items['options'][$name] = $value;
return $this;
}
/**
* @return Session|null
*/
public function getSession()
{
return $this->offsetGet('session');
}
/**
* @return User
*/
public function getUser()
{
return $this->offsetGet('user');
}
/**
* @param User $user
* @return $this
*/
public function setUser(User $user)
{
$this->offsetSet('user', $user);
return $this;
}
/**
* @return array
*/
public function getAuthorize()
{
return (array)$this->offsetGet('authorize');
}
/**
* @return string|null
*/
public function getMessage()
{
return !empty($this->items['message'][0]) ? (string)$this->items['message'][0] : null;
}
/**
* @return string|null
*/
public function getMessageType()
{
return !empty($this->items['message'][1]) ? (string)$this->items['message'][1] : 'info';
}
/**
* @param string $message
* @param string|null $type
* @return $this
*/
public function setMessage($message, $type = null)
{
$this->items['message'] = $message ? [$message, $type] : null;
return $this;
}
/**
* @param string $message
* @param string|null $type
* @return $this
*/
public function defMessage($message, $type = null)
{
if ($message && !isset($this->items['message'])) {
$this->setMessage($message, $type);
}
return $this;
}
/**
* @return string|null
*/
public function getRedirect()
{
return !empty($this->items['redirect']) ? (string)$this->items['redirect'] : null;
}
/**
* @return string|null
*/
public function getRedirectCode()
{
return !empty($this->items['redirect_code']) ? (string)$this->items['redirect_code'] : 303;
}
/**
* @param string $path
* @param int $code
* @return $this
*/
public function setRedirect($path, $code = 303)
{
$this->items['redirect'] = $path ?: null;
$this->items['redirect_code'] = (int)$code;
return $this;
}
/**
* @param string $path
* @param int $code
* @return $this
*/
public function defRedirect($path, $code = 303)
{
if ($path && !isset($this->items['redirect'])) {
$this->setRedirect($path, $code);
}
return $this;
}
/**
* Magic setter method
*
* @param mixed $offset Asset name value
* @param mixed $value Asset value
*/
public function __set($offset, $value)
{
$this->offsetSet($offset, $value);
}
/**
* Magic getter method
*
* @param mixed $offset Asset name value
* @return mixed Asset value
*/
public function __get($offset)
{
return $this->offsetGet($offset);
}
/**
* Magic method to determine if the attribute is set
*
* @param mixed $offset Asset name value
* @return boolean True if the value is set
*/
public function __isset($offset)
{
return $this->offsetExists($offset);
}
/**
* Magic method to unset the attribute
*
* @param mixed $offset The name value to unset
*/
public function __unset($offset)
{
$this->offsetUnset($offset);
}
}
+642
View File
@@ -0,0 +1,642 @@
<?php
/**
* @package Grav\Plugin\Login
*
* @copyright Copyright (C) 2014 - 2017 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Plugin\Login;
use Birke\Rememberme\Cookie;
use Grav\Common\Config\Config;
use Grav\Common\Data\Data;
use Grav\Common\Grav;
use Grav\Common\File\CompiledYamlFile;
use Grav\Common\Language\Language;
use Grav\Common\Page\Page;
use Grav\Common\Session;
use Grav\Common\User\User;
use Grav\Common\Uri;
use Grav\Plugin\Email\Utils as EmailUtils;
use Grav\Plugin\Login\Events\UserLoginEvent;
use Grav\Plugin\Login\RememberMe\RememberMe;
use Grav\Plugin\Login\RememberMe\TokenStorage;
use Grav\Plugin\Login\TwoFactorAuth\TwoFactorAuth;
/**
* Class Login
* @package Grav\Plugin
*/
class Login
{
/** @var Grav */
protected $grav;
/** @var Config */
protected $config;
/** @var Language $language */
protected $language;
/** @var Session */
protected $session;
/** @var Uri */
protected $uri;
/** @var RememberMe */
protected $rememberMe;
/** @var TwoFactorAuth */
protected $twoFa;
/** @var RateLimiter[] */
protected $rateLimiters = [];
/** @var array */
protected $provider_login_templates = [];
/**
* Login constructor.
*
* @param Grav $grav
*/
public function __construct(Grav $grav)
{
$this->grav = $grav;
$this->config = $this->grav['config'];
$this->language = $this->grav['language'];
$this->session = $this->grav['session'];
$this->uri = $this->grav['uri'];
}
/**
* Login user.
*
* @param array $credentials Login credentials, eg: ['username' => '', 'password' => '']
* @param array $options Login options, eg: ['remember_me' => true]
* @param array $extra Example: ['authorize' => 'site.login', 'user' => null], undefined variables get set.
* @return User|UserLoginEvent Returns event if $extra['return_event'] is true.
*/
public function login(array $credentials, array $options = [], array $extra = [])
{
$grav = Grav::instance();
$eventOptions = [
'credentials' => $credentials,
'options' => $options
] + $extra;
// Attempt to authenticate the user.
$event = new UserLoginEvent($eventOptions);
$grav->fireEvent('onUserLoginAuthenticate', $event);
if ($event->isSuccess()) {
// Make sure that event didn't mess up with the user authorization.
$user = $event->getUser();
$user->authenticated = true;
$user->authorized = false;
// Allow plugins to prevent login after successful authentication.
$event = new UserLoginEvent($event->toArray());
$grav->fireEvent('onUserLoginAuthorize', $event);
}
if ($event->isSuccess()) {
// User has been logged in, let plugins know.
$event = new UserLoginEvent($event->toArray());
$grav->fireEvent('onUserLogin', $event);
// Make sure that event didn't mess up with the user authorization.
$user = $event->getUser();
$user->authenticated = true;
$user->authorized = !$event->isDelayed();
} else {
// Allow plugins to log errors or do other tasks on failure.
$event = new UserLoginEvent($event->toArray());
$grav->fireEvent('onUserLoginFailure', $event);
// Make sure that event didn't mess up with the user authorization.
$user = $event->getUser();
$user->authenticated = false;
$user->authorized = false;
}
$user = $event->getUser();
$user->def('language', 'en');
return !empty($event['return_event']) ? $event : $user;
}
/**
* Logout user.
*
* @param array $options
* @param array|User $extra Array of: ['user' => $user, ...] or User object (deprecated).
* @return User|UserLoginEvent Returns event if $extra['return_event'] is true.
*/
public function logout(array $options = [], $extra = [])
{
$grav = Grav::instance();
if ($extra instanceof User) {
$extra = ['user' => $extra];
} elseif (isset($extra['user'])) {
$extra['user'] = $grav['user'];
}
$eventOptions = [
'options' => $options
] + $extra;
$event = new UserLoginEvent($eventOptions);
// Logout the user.
$grav->fireEvent('onUserLogout', $event);
$user = $event->getUser();
$user->authenticated = false;
$user->authorized = false;
return !empty($event['return_event']) ? $event : $user;
}
/**
* Authenticate user.
*
* @param array $credentials Form fields.
* @param array $options
*
* @return bool
*/
public function authenticate($credentials, $options = ['remember_me' => true])
{
$event = $this->login($credentials, $options, ['return_event' => true]);
$user = $event['user'];
$redirect = $event->getRedirect();
$message = $event->getMessage();
$messageType = $event->getMessageType();
if ($user->authenticated && $user->authorized) {
if (!$message) {
$message = 'PLUGIN_LOGIN.LOGIN_SUCCESSFUL';
$messageType = 'info';
}
if (!$redirect) {
$redirect = $this->uri->route();
}
}
if ($message) {
$this->grav['messages']->add($this->language->translate($message, [$user->language]), $messageType);
}
if ($redirect) {
$this->grav->redirect($redirect, $event->getRedirectCode());
}
return $user->authenticated && $user->authorized;
}
/**
* Create a new user file
*
* @param array $data
*
* @return User
*/
public function register($data)
{
if (!isset($data['groups'])) {
//Add new user ACL settings
$groups = (array) $this->config->get('plugins.login.user_registration.groups', []);
if (count($groups) > 0) {
$data['groups'] = $groups;
}
}
if (!isset($data['access'])) {
$access = (array) $this->config->get('plugins.login.user_registration.access.site', []);
if (count($access) > 0) {
$data['access']['site'] = $access;
}
}
$username = $this->validateField('username', $data['username']);
$file = CompiledYamlFile::instance($this->grav['locator']->findResource('account://' . $username . YAML_EXT,
true, true));
// Create user object and save it
$user = new User($data);
$user->file($file);
$user->save();
return $user;
}
/**
* @param string $type
* @param mixed $value
* @param string $extra
*
* @return string
*/
public function validateField($type, $value, $extra = '')
{
switch ($type) {
case 'user':
case 'username':
/** @var Config $config */
$config = Grav::instance()['config'];
$username_regex = '/' . $config->get('system.username_regex') . '/';
if (!is_string($value) || !preg_match($username_regex, $value)) {
throw new \RuntimeException('Username should be between 3 and 16 characters, including lowercase letters, numbers, underscores, and hyphens. Uppercase letters, spaces, and special characters are not allowed');
}
break;
case 'password1':
/** @var Config $config */
$config = Grav::instance()['config'];
$pwd_regex = '/' . $config->get('system.pwd_regex') . '/';
if (!is_string($value) || !preg_match($pwd_regex, $value)) {
throw new \RuntimeException('Password must contain at least one number and one uppercase and lowercase letter, and at least 8 or more characters');
}
break;
case 'password2':
if (!is_string($value) || strcmp($value, $extra)) {
throw new \RuntimeException('Passwords did not match.');
}
break;
case 'email':
if (!is_string($value) || !filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new \RuntimeException('Not a valid email address');
}
break;
case 'permissions':
if (!is_string($value) || !in_array($value, ['a', 's', 'b'])) {
throw new \RuntimeException('Permissions ' . $value . ' are invalid.');
}
break;
case 'fullname':
if (!is_string($value) || trim($value) === '') {
throw new \RuntimeException('Fullname cannot be empty');
}
break;
case 'state':
if ($value !== 'enabled' && $value !== 'disabled') {
throw new \RuntimeException('State is not valid');
}
break;
}
return $value;
}
/**
* Handle the email to notify the user account creation to the site admin.
*
* @param User $user
*
* @return bool True if the action was performed.
* @throws \RuntimeException
*/
public function sendNotificationEmail(User $user)
{
if (empty($user->email)) {
throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.USER_NEEDS_EMAIL_FIELD'));
}
$site_name = $this->config->get('site.title', 'Website');
$subject = $this->language->translate(['PLUGIN_LOGIN.NOTIFICATION_EMAIL_SUBJECT', $site_name]);
$content = $this->language->translate([
'PLUGIN_LOGIN.NOTIFICATION_EMAIL_BODY',
$site_name,
$user->username,
$user->email,
$this->grav['base_url_absolute'],
]);
$to = $this->config->get('plugins.email.from');
if (empty($to)) {
throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.EMAIL_NOT_CONFIGURED'));
}
$sent = EmailUtils::sendEmail($subject, $content, $to);
if ($sent < 1) {
throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.EMAIL_SENDING_FAILURE'));
}
return true;
}
/**
* Handle the email to welcome the new user
*
* @param User $user
*
* @return bool True if the action was performed.
* @throws \RuntimeException
*/
public function sendWelcomeEmail(User $user)
{
if (empty($user->email)) {
throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.USER_NEEDS_EMAIL_FIELD'));
}
$site_name = $this->config->get('site.title', 'Website');
$author = $this->grav['config']->get('site.author.name', '');
$fullname = $user->fullname ?: $user->username;
$subject = $this->language->translate(['PLUGIN_LOGIN.WELCOME_EMAIL_SUBJECT', $site_name]);
$content = $this->language->translate(['PLUGIN_LOGIN.WELCOME_EMAIL_BODY',
$fullname,
$this->grav['base_url_absolute'],
$site_name,
$author
]);
$to = $user->email;
$sent = EmailUtils::sendEmail($subject, $content, $to);
if ($sent < 1) {
throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.EMAIL_SENDING_FAILURE'));
}
return true;
}
/**
* Handle the email to activate the user account.
*
* @param User $user
*
* @return bool True if the action was performed.
* @throws \RuntimeException
*/
public function sendActivationEmail(User $user)
{
if (empty($user->email)) {
throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.USER_NEEDS_EMAIL_FIELD'));
}
$token = md5(uniqid(mt_rand(), true));
$expire = time() + 604800; // next week
$user->activation_token = $token . '::' . $expire;
$user->save();
$param_sep = $this->config->get('system.param_sep', ':');
$activation_link = $this->grav['base_url_absolute'] . $this->config->get('plugins.login.route_activate') . '/token' . $param_sep . $token . '/username' . $param_sep . $user->username;
$site_name = $this->config->get('site.title', 'Website');
$author = $this->grav['config']->get('site.author.name', '');
$fullname = $user->fullname ?: $user->username;
$subject = $this->language->translate(['PLUGIN_LOGIN.ACTIVATION_EMAIL_SUBJECT', $site_name]);
$content = $this->language->translate(['PLUGIN_LOGIN.ACTIVATION_EMAIL_BODY',
$fullname,
$activation_link,
$site_name,
$author
]);
$to = $user->email;
$sent = EmailUtils::sendEmail($subject, $content, $to);
if ($sent < 1) {
throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.EMAIL_SENDING_FAILURE'));
}
return true;
}
/**
* Gets and sets the RememberMe class
*
* @param mixed $var A rememberMe instance to set
*
* @return RememberMe Returns the current rememberMe instance
* @throws \InvalidArgumentException
*/
public function rememberMe($var = null)
{
if ($var !== null) {
$this->rememberMe = $var;
}
if (!$this->rememberMe) {
/** @var Config $config */
$config = $this->grav['config'];
// Setup storage for RememberMe cookies
$storage = new TokenStorage;
$this->rememberMe = new RememberMe($storage);
$this->rememberMe->setCookieName($config->get('plugins.login.rememberme.name'));
$this->rememberMe->setExpireTime($config->get('plugins.login.rememberme.timeout'));
// Hardening cookies with user-agent and random salt or
// fallback to use system based cache key
$server_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : 'unknown';
$data = $server_agent . $config->get('security.salt', $this->grav['cache']->getKey());
$this->rememberMe->setSalt(hash('sha512', $data));
// Set cookie with correct base path of Grav install
$cookie = new Cookie;
$cookie->setPath($this->grav['base_url_relative'] ?: '/');
$this->rememberMe->setCookie($cookie);
}
return $this->rememberMe;
}
/**
* Gets and sets the TwoFactorAuth object
*
* @param TwoFactorAuth $var
* @return TwoFactorAuth
* @throws \RobThree\Auth\TwoFactorAuthException
*/
public function twoFactorAuth($var = null)
{
if ($var !== null) {
$this->twoFa = $var;
}
if (!$this->twoFa) {
$this->twoFa = new TwoFactorAuth;
}
return $this->twoFa;
}
/**
* @param string $context
* @param int $maxCount
* @param int $interval
* @return RateLimiter
*/
public function getRateLimiter($context, $maxCount = null, $interval = null)
{
if (!isset($this->rateLimiters[$context])) {
switch ($context) {
case 'login_attempts':
$maxCount = $this->grav['config']->get('plugins.login.max_login_count', 5);
$interval = $this->grav['config']->get('plugins.login.max_login_interval', 10);
break;
case 'pw_resets':
$maxCount = $this->grav['config']->get('plugins.login.max_pw_resets_count', 0);
$interval = $this->grav['config']->get('plugins.login.max_pw_resets_interval', 2);
break;
}
$this->rateLimiters[$context] = new RateLimiter($context, $maxCount, $interval);
}
return $this->rateLimiters[$context];
}
/**
* @param User $user
* @param Page $page
* @param Data|null $config
* @return bool
*/
public function isUserAuthorizedForPage(User $user, Page $page, $config = null)
{
$header = $page->header();
$rules = isset($header->access) ? (array)$header->access : [];
if ($config !== null && $config->get('parent_acl')) {
// If page has no ACL rules, use its parent's rules
if (!$rules) {
$parent = $page->parent();
while (!$rules and $parent) {
$header = $parent->header();
$rules = isset($header->access) ? (array)$header->access : [];
$parent = $parent->parent();
}
}
}
// Continue to the page if it has no ACL rules.
if (!$rules) {
return true;
}
if (!$user->authorized) {
return false;
}
// Continue to the page if user is authorized to access the page.
foreach ($rules as $rule => $value) {
if (is_array($value)) {
foreach ($value as $nested_rule => $nested_value) {
if ($user->authorize($rule . '.' . $nested_rule) == $nested_value) {
return true;
}
}
} else {
if ($user->authorize($rule) == $value) {
return true;
}
}
}
return false;
}
/**
* Check if user may use password reset functionality.
*
* @param User $user
* @param string $field
* @param int $count
* @param int $interval
* @return bool
* @deprecated 2.5.0 Use $grav['login']->getRateLimiter($context) instead. See Grav\Plugin\Login\RateLimiter class.
*/
public function isUserRateLimited(User $user, $field, $count, $interval)
{
if ($count > 0) {
if (!isset($user->{$field})) {
$user->{$field} = [];
}
//remove older than $interval x minute attempts
$actual_resets = [];
foreach ((array)$user->{$field} as $reset) {
if ($reset > (time() - $interval * 60)) {
$actual_resets[] = $reset;
}
}
if (count($actual_resets) >= $count) {
return true;
}
$actual_resets[] = time(); // current reset
$user->{$field} = $actual_resets;
}
return false;
}
/**
* Reset the rate limit counter.
*
* @param User $user
* @param string $field
* @deprecated 2.5.0 Use $grav['login']->getRateLimiter($context) instead. See Grav\Plugin\Login\RateLimiter class.
*/
public function resetRateLimit(User $user, $field)
{
$user->{$field} = [];
}
/**
* Get Current logged in user
*
* @return User
* @deprecated 2.5.0 Use $grav['user'] instead.
*/
public function getUser()
{
/** @var User $user */
return $this->grav['user'];
}
public function addProviderLoginTemplate($template)
{
$this->provider_login_templates[] = $template;
}
public function getProviderLoginTemplates()
{
$templates = $this->provider_login_templates;
return $templates;
}
}
+103
View File
@@ -0,0 +1,103 @@
<?php
/**
* @package Grav\Plugin\Login
*
* @copyright Copyright (C) 2014 - 2017 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Plugin\Login;
use Doctrine\Common\Cache\FilesystemCache;
use Grav\Common\Grav;
/**
* PSR-16 forward compatible cache.
* @package Grav\Plugin\Login
*/
class LoginCache
{
/**
* @var FilesystemCache
*/
protected $driver;
protected $lifetime;
/**
* @param string $namespace
* @param null|int $defaultLifetime
* @throws \InvalidArgumentException
*/
public function __construct($namespace, $defaultLifetime = null)
{
// Setup cache
$this->lifetime = (int)$defaultLifetime;
$this->driver = new FilesystemCache(Grav::instance()['locator']->findResource('cache://login/' . $namespace, true, true));
}
/**
* Fetches a value from the cache.
*
* @param string $key The unique key of this item in the cache.
* @param mixed $default Default value to return if the key does not exist.
*
* @return mixed The value of the item from the cache, or $default in case of cache miss.
*/
public function get($key, $default = null)
{
$value = $this->driver->fetch($key);
// Doctrine cache does not differentiate between no result and cached 'false'. Make sure that we do.
return $value !== false || $this->driver->contains($key) ? $value : $default;
}
/**
* Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time.
*
* @param string $key The key of the item to store.
* @param mixed $value The value of the item to store, must be serializable.
* @param null|int $ttl Optional. The TTL value of this item.
*
* @return bool True on success and false on failure.
*/
public function set($key, $value, $ttl = null)
{
$ttl = $ttl !== null ? (int)$ttl : $this->lifetime;
return $this->driver->save($key, $value, $ttl);
}
/**
* Determines whether an item is present in the cache.
*
* @param string $key The cache item key.
*
* @return bool
*/
public function has($key)
{
return $this->driver->contains($key);
}
/**
* Delete an item from the cache by its unique key.
*
* @param string $key The unique cache key of the item to delete.
*
* @return bool True if the item was successfully removed. False if there was an error.
*/
public function delete($key)
{
return $this->driver->delete($key);
}
/**
* Wipes clean the entire cache's keys.
*
* @return bool True on success and false on failure.
*/
public function clear()
{
return $this->driver->flushAll();
}
}
+108
View File
@@ -0,0 +1,108 @@
<?php
/**
* @package Grav\Plugin\Login
*
* @copyright Copyright (C) 2014 - 2017 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Plugin\Login;
/**
* Class RateLimiter
* @package Grav\Plugin\Login\RateLimiter
*/
class RateLimiter
{
/** @var LoginCache */
protected $cache;
/** @var int */
protected $maxCount;
/** @var int */
protected $interval;
/**
* RateLimiter constructor.
* @param string $context
* @param int $maxCount
* @param int|null $interval
*/
public function __construct($context, $maxCount, $interval)
{
$this->cache = new LoginCache($context, (int)$interval * 60);
$this->maxCount = (int) $maxCount;
$this->interval = (int) $interval;
}
/**
* @return int
*/
public function getInterval()
{
return $this->interval;
}
/**
* Check if user has hit rate limiter. Remember to use registerRateLimitedAction() before doing the check.
*
* @param string $key
* @param string $type
* @return bool
*/
public function isRateLimited($key, $type = 'username')
{
if (!$key || !$this->interval) {
return false;
}
return $this->maxCount && count($this->getAttempts($key, $type)) > $this->maxCount;
}
/**
*
*
* @param string $key
* @param string $type
* @return array
*/
public function getAttempts($key, $type = 'username')
{
return (array) $this->cache->get($type . $key, []);
}
/**
* Register rate limited action.
*
* @param string $key
* @param string $type
* @return $this
*/
public function registerRateLimitedAction($key, $type = 'username')
{
if ($key && $this->interval) {
$tries = (array)$this->cache->get($type . $key, []);
$tries[] = time();
$this->cache->set($type . $key, $tries);
}
return $this;
}
/**
* Reset the user rate limit counter.
*
* @param string $key
* @param string $type
* @return $this
*/
public function resetRateLimit($key, $type = 'username')
{
if ($key) {
$this->cache->delete($type . $key);
}
return $this;
}
}
@@ -0,0 +1,41 @@
<?php
/**
* @package Grav\Plugin\Login
*
* @copyright Copyright (C) 2014 - 2017 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Plugin\Login\RememberMe;
use Birke\Rememberme\Authenticator;
use Birke\Rememberme\Storage\StorageInterface;
/**
* RememberMe
*
* Handles persistent cookie-storage (Remember Me)
*
* @author Sommerregen <sommerregen@benjamin-regler.de>
*/
class RememberMe extends Authenticator
{
/**
* Gets storage interface
*
* @return StorageInterface
*/
public function getStorage()
{
return $this->storage;
}
/**
* Set storage interface
*
* @param StorageInterface $storage Storage interface
*/
public function setStorage($storage)
{
$this->storage = $storage;
}
}
@@ -0,0 +1,198 @@
<?php
/**
* @package Grav\Plugin\Login
*
* @copyright Copyright (C) 2014 - 2017 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Plugin\Login\RememberMe;
use Grav\Common\Cache;
use Grav\Common\Grav;
use Doctrine\Common\Cache\CacheProvider;
use Doctrine\Common\Cache\FilesystemCache;
use Birke\Rememberme\Storage\StorageInterface;
/**
* Storage wrapper for Doctrine cache
*
* Used for storing the credential/token/persistentToken triplets.
*
* @author Sommerregen <sommerregen@benjamin-regler.de>
*/
class TokenStorage implements StorageInterface
{
/**
* @var CacheProvider
*/
protected $driver;
/**
* @var string
*/
protected $cache_dir;
/**
* Constructor
*
* @param string $path Path to storage directory
* @throws \InvalidArgumentException
*/
public function __construct($path = 'cache://rememberme')
{
/** @var Cache $cache */
$cache = Grav::instance()['cache'];
$this->cache_dir = Grav::instance()['locator']->findResource($path, true, true);
// Setup cache
$this->driver = $cache->getCacheDriver();
if ($this->driver instanceof FilesystemCache) {
$this->driver = new FilesystemCache($this->cache_dir);
}
// Set the cache namespace to our unique key
$this->driver->setNamespace($cache->getKey());
}
/**
* Return Tri-state value constant
*
* @param mixed $credential Unique credential (user id,
* email address, user name)
* @param string $token One-Time Token
* @param string $persistentToken Persistent Token
*
* @return int
*/
public function findTriplet($credential, $token, $persistentToken)
{
// Hash the tokens, because they can contain a salt and can be
// accessed in the file system
$persistentToken = sha1(trim($persistentToken));
$token = sha1(trim($token));
$id = $this->getId($credential);
if (!$this->driver->contains($id)) {
return self::TRIPLET_NOT_FOUND;
}
list($expire, $tokens) = $this->driver->fetch($id);
if (isset($tokens[$persistentToken]) && $tokens[$persistentToken] === $token) {
return self::TRIPLET_FOUND;
}
return self::TRIPLET_INVALID;
}
/**
* Store the new token for the credential and the persistent token.
* Create a new storage entry, if the combination of credential and
* persistent token does not exist.
*
* @param mixed $credential
* @param string $token
* @param string $persistentToken
* @param int $expire Timestamp when this triplet
* will expire (0 = no expiry)
*/
public function storeTriplet($credential, $token, $persistentToken, $expire = null)
{
// Hash the tokens, because they can contain a salt and can be
// accessed in the file system
$persistentToken = sha1(trim($persistentToken));
$token = sha1(trim($token));
$e = null;
$tokens = [];
$id = $this->getId($credential);
if ($this->driver->contains($id)) {
list($e, $tokens) = $this->driver->fetch($id);
}
// Get cache lifetime for tokens
if ($expire === null && $e === null) {
/** @var Cache $cache */
$cache = Grav::instance()['cache'];
$expire = $cache->getLifetime();
} elseif ($expire === null) {
$expire = $e;
}
// Update tokens
$tokens[$persistentToken] = $token;
$this->driver->save($id, [$expire, $tokens], $expire);
return $this;
}
/**
* Replace current token after successful authentication
*
* @param mixed $credential
* @param string $token
* @param string $persistentToken
* @param int $expire
*/
public function replaceTriplet($credential, $token, $persistentToken, $expire = null)
{
$this->cleanTriplet($credential, $persistentToken);
$this->storeTriplet($credential, $token, $persistentToken, $expire);
}
/**
* Remove one triplet of the user from the store
*
* @param mixed $credential
* @param string $persistentToken
*/
public function cleanTriplet($credential, $persistentToken)
{
// Hash the tokens, because they can contain a salt and can be
// accessed in the file system
$persistentToken = sha1(trim($persistentToken));
// Delete token from storage
$id = $this->getId($credential);
if ($this->driver->contains($id)) {
list($expire, $tokens) = $this->driver->fetch($id);
unset($tokens[$persistentToken]);
$this->driver->save($id, [$expire, $tokens], $expire);
}
}
/**
* Remove all triplets of a user, effectively logging him out on all
* machines
*
* @param mixed $credential
*/
public function cleanAllTriplets($credential)
{
$id = $this->getId($credential);
$this->driver->delete($id);
}
/**
* Helper method to clear RememberMe cache
*/
public function clearCache()
{
$this->driver->flushAll();
}
/**
* Get the cache id
*
* @param string $key A key to compute the cache id for
* @return string The cache id
*/
protected function getId($key)
{
/** @var Cache $cache */
$cache = Grav::instance()['cache'];
return 'login' . md5(trim($key) . $cache->getKey());
}
}
@@ -0,0 +1,30 @@
<?php
/**
* @package Grav\Plugin\Login
*
* @copyright Copyright (C) 2014 - 2018 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Plugin\Login\TwoFactorAuth;
use BaconQrCode\Renderer\Image\Png as BaconPng;
use BaconQrCode\Writer as BaconWriter;
use RobThree\Auth\Providers\Qr\IQRCodeProvider;
class BaconQrProvider implements IQRCodeProvider
{
public function getMimeType()
{
return 'image/png';
}
public function getQRCodeImage($qrtext, $size = 256)
{
$renderer = new BaconPng();
$renderer->setHeight($size);
$renderer->setWidth($size);
$writer = new BaconWriter($renderer);
return $writer->writeString($qrtext);
}
}
@@ -0,0 +1,74 @@
<?php
/**
* @package Grav\Plugin\Login
*
* @copyright Copyright (C) 2014 - 2018 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Plugin\Login\TwoFactorAuth;
use Grav\Common\Grav;
use RobThree\Auth\TwoFactorAuth as Auth;
use RobThree\Auth\TwoFactorAuthException;
/**
* Class TwoFactorAuth
* @package Grav\Plugin\Login\RememberMe
*/
class TwoFactorAuth
{
protected $twoFa;
/**
* TwoFactorAuth constructor.
* @throws TwoFactorAuthException
*/
public function __construct()
{
$this->twoFa = new Auth('Grav', 6, 30, 'sha1', new BaconQrProvider);
}
/**
* @return Auth
*/
public function get2FA()
{
return $this->twoFa;
}
/**
* @param int $bits
* @return string
* @throws TwoFactorAuthException
*/
public function createSecret($bits = 160)
{
return $this->twoFa->createSecret($bits);
}
/**
* @param string $secret
* @param string $code
* @return bool
*/
public function verifyCode($secret, $code)
{
$secret = str_replace(' ', '', $secret);
return $this->twoFa->verifyCode($secret, $code);
}
/**
* @param string $username
* @param string $secret
* @return string
* @throws TwoFactorAuthException
*/
public function getQrImageData($username, $secret)
{
$label = $username . ':' . Grav::instance()['config']->get('site.title');
$secret = str_replace(' ', '', $secret);
return $this->twoFa->getQRCodeImageAsDataUri($label, $secret);
}
}
@@ -0,0 +1,183 @@
<?php
/**
* @package Grav\Plugin\Login
*
* @copyright Copyright (C) 2014 - 2017 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Plugin\Console;
use Grav\Common\Config\Config;
use Grav\Console\ConsoleCommand;
use Grav\Common\Grav;
use Grav\Common\File\CompiledYamlFile;
use Grav\Common\User\User;
use Grav\Plugin\Login\Login;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Question\Question;
/**
* Class CleanCommand
*
* @package Grav\Console\Cli
*/
class ChangePasswordCommand extends ConsoleCommand
{
/** @var array */
protected $options = [];
/** @var Login */
protected $login;
/**
* Configure the command
*/
protected function configure()
{
$this
->setName('change-password')
->setAliases(['edit-password', 'newpass', 'changepass', 'passwd'])
->addOption(
'user',
'u',
InputOption::VALUE_REQUIRED,
'The username'
)
->addOption(
'password',
'p',
InputOption::VALUE_REQUIRED,
"The password. Note that this option is not recommended because the password will be visible by users listing the processes. You should also make sure the password respects Grav's password policy."
)
->setDescription('Changes a User Password')
->setHelp('The <info>change-password</info> changes the password of the specified user. (User must exist)')
;
}
/**
* @return int|null|void
*/
protected function serve()
{
include __DIR__ . '/../vendor/autoload.php';
$grav = Grav::instance();
if (!isset($grav['login'])) {
$grav['login'] = new Login($grav);
}
$this->login = $grav['login'];
$this->options = [
'user' => $this->input->getOption('user'),
'password1' => $this->input->getOption('password')
];
$this->validateOptions();
$helper = $this->getHelper('question');
$data = [];
$this->output->writeln('<green>Changing User Password</green>');
$this->output->writeln('');
if (!$this->options['user']) {
// Get username and validate
$question = new Question('Enter a <yellow>username</yellow>: ');
$question->setValidator(function ($value) {
$this->validate('user', $value);
if (!User::find($value, ['username'])->exists()) {
throw new \RuntimeException('Username "' . $value . '" does not exist, please pick another username');
};
return $value;
});
$username = $helper->ask($this->input, $this->output, $question);
} else {
$username = $this->options['user'];
}
if (!$this->options['password1']) {
// Get password and validate
$password = $this->askForPassword($helper, 'Enter a <yellow>new password</yellow>: ', function ($password1) use ($helper) {
$this->validate('password1', $password1);
// Since input is hidden when prompting for passwords, the user is asked to repeat the password
return $this->askForPassword($helper, 'Repeat the <yellow>password</yellow>: ', function ($password2) use ($password1) {
return $this->validate('password2', $password2, $password1);
});
});
$data['password'] = $password;
} else {
$data['password'] = $this->options['password1'];
}
// Lowercase the username for the filename
$username = strtolower($username);
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
// Grab the account file and read in the information before setting the file (prevent setting erase)
$oldUserFile = CompiledYamlFile::instance($locator->findResource('account://' . $username . YAML_EXT, true, true));
$oldData = (array)$oldUserFile->content();
//Set the password field to new password
$oldData['password'] = $data['password'];
// Create user object and save it using oldData (with updated password)
$user = new User($oldData);
$file = CompiledYamlFile::instance($locator->findResource('account://' . $username . YAML_EXT, true, true));
$user->file($file);
$user->save();
$this->output->writeln('');
$this->output->writeln('<green>Success!</green> User <cyan>' . $username . '\'s</cyan> password changed.');
}
/**
*
*/
protected function validateOptions()
{
foreach (array_filter($this->options) as $type => $value) {
$this->validate($type, $value);
}
}
/**
* @param string $type
* @param mixed $value
* @param string $extra
*
* @return string
*/
protected function validate($type, $value, $extra = '')
{
return $this->login->validateField($type, $value, $extra);
}
/**
* Get password and validate.
*
* @param Helper $helper
* @param string $question
* @param callable $validator
*
* @return string
*/
protected function askForPassword(Helper $helper, $question, callable $validator)
{
$question = new Question($question);
$question->setValidator($validator);
$question->setHidden(true);
$question->setHiddenFallback(true);
return $helper->ask($this->input, $this->output, $question);
}
}
@@ -0,0 +1,162 @@
<?php
/**
* @package Grav\Plugin\Login
*
* @copyright Copyright (C) 2014 - 2017 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Plugin\Console;
use Grav\Common\Config\Config;
use Grav\Common\Grav;
use Grav\Console\ConsoleCommand;
use Grav\Common\File\CompiledYamlFile;
use Grav\Common\User\User;
use Grav\Plugin\Login\Login;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\Question;
/**
* Class CleanCommand
*
* @package Grav\Console\Cli
*/
class ChangeUserStateCommand extends ConsoleCommand
{
/** @var array */
protected $options = [];
/** @var Login */
protected $login;
/**
* Configure the command
*/
protected function configure()
{
$this
->setName('toggle-user')
->setAliases(['disableuser', 'enableuser', 'toggleuser', 'change-user-state'])
->addOption(
'user',
'u',
InputOption::VALUE_REQUIRED,
'The username'
)
->addOption(
'state',
's',
InputOption::VALUE_REQUIRED,
'The state of the account. Can be either `enabled` or `disabled`. [default: "enabled"]'
)
->setDescription('Changes whether user can login or not')
->setHelp('The <info>toggle-user</info> sets a user\'s login status to enabled or disabled.')
;
}
/**
* @return int|null|void
*/
protected function serve()
{
include __DIR__ . '/../vendor/autoload.php';
$grav = Grav::instance();
if (!isset($grav['login'])) {
$grav['login'] = new Login($grav);
}
$this->login = $grav['login'];
$this->options = [
'user' => $this->input->getOption('user'),
'state' => $this->input->getOption('state')
];
$this->validateOptions();
$helper = $this->getHelper('question');
$data = [];
$this->output->writeln('<green>Setting User State</green>');
$this->output->writeln('');
if (!$this->options['user']) {
// Get username and validate
$question = new Question('Enter a <yellow>username</yellow>: ');
$question->setValidator(function ($value) {
$this->validate('user', $value);
if (!User::find($value, ['username'])->exists()) {
throw new \RuntimeException('Username "' . $value . '" does not exist, please pick another username');
};
return $value;
});
$username = $helper->ask($this->input, $this->output, $question);
} else {
$username = $this->options['user'];
}
if (!$this->options['state'] && !count(array_filter($this->options))) {
// Choose State
$question = new ChoiceQuestion(
'Please choose the <yellow>state</yellow> for the account:',
array('enabled' => 'Enabled', 'disabled' => 'Disabled'),
'enabled'
);
$question->setErrorMessage('State %s is invalid.');
$data['state'] = $helper->ask($this->input, $this->output, $question);
} else {
$data['state'] = $this->options['state'] ?: 'enabled';
}
// Lowercase the username for the filename
$username = strtolower($username);
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
// Grab the account file and read in the information before setting the file (prevent setting erase)
$oldUserFile = CompiledYamlFile::instance($locator->findResource('account://' . $username . YAML_EXT, true, true));
$oldData = (array)$oldUserFile->content();
//Set the state feild to new state
$oldData['state'] = $data['state'];
// Create user object and save it using oldData (with updated state)
$user = new User($oldData);
$file = CompiledYamlFile::instance($locator->findResource('account://' . $username . YAML_EXT, true, true));
$user->file($file);
$user->save();
$this->output->writeln('');
$this->output->writeln('<green>Success!</green> User <cyan>' . $username . '</cyan> state set to .' . $data['state']);
}
/**
*
*/
protected function validateOptions()
{
foreach (array_filter($this->options) as $type => $value) {
$this->validate($type, $value);
}
}
/**
* @param string $type
* @param mixed $value
* @param string $extra
*
* @return string
*/
protected function validate($type, $value)
{
return $this->login->validateField($type, $value);
}
}
+284
View File
@@ -0,0 +1,284 @@
<?php
/**
* @package Grav\Plugin\Login
*
* @copyright Copyright (C) 2014 - 2017 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Plugin\Console;
use Grav\Common\Config\Config;
use Grav\Console\ConsoleCommand;
use Grav\Common\File\CompiledYamlFile;
use Grav\Common\User\User;
use Grav\Common\Grav;
use Grav\Plugin\Login\Login;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\Question;
/**
* Class CleanCommand
*
* @package Grav\Console\Cli
*/
class NewUserCommand extends ConsoleCommand
{
/** @var array */
protected $options = [];
/** @var Login */
protected $login;
/**
* Configure the command
*/
protected function configure()
{
$this
->setName('new-user')
->setAliases(['add-user', 'newuser'])
->addOption(
'user',
'u',
InputOption::VALUE_REQUIRED,
'The username'
)
->addOption(
'password',
'p',
InputOption::VALUE_REQUIRED,
"The password. Note that this option is not recommended because the password will be visible by users listing the processes. You should also make sure the password respects Grav's password policy."
)
->addOption(
'email',
'e',
InputOption::VALUE_REQUIRED,
'The user email'
)
->addOption(
'permissions',
'P',
InputOption::VALUE_REQUIRED,
'The user permissions. It can be either `a` for Admin access only, `s` for Site access only and `b` for both Admin and Site access.'
)
->addOption(
'fullname',
'N',
InputOption::VALUE_REQUIRED,
'The user full name.'
)
->addOption(
'title',
't',
InputOption::VALUE_REQUIRED,
'The title of the user. Usually used as a subtext. Example: Admin, Collaborator, Developer'
)
->addOption(
'state',
's',
InputOption::VALUE_REQUIRED,
'The state of the account. Can be either `enabled` or `disabled`. [default: "enabled"]'
)
->setDescription('Creates a new user')
->setHelp('The <info>new-user</info> creates a new user file in user/accounts/ folder')
;
}
/**
* @return int|null|void
*/
protected function serve()
{
include __DIR__ . '/../vendor/autoload.php';
$grav = Grav::instance();
if (!isset($grav['login'])) {
$grav['login'] = new Login($grav);
}
$this->login = $grav['login'];
$this->options = [
'user' => $this->input->getOption('user'),
'password1' => $this->input->getOption('password'),
'email' => $this->input->getOption('email'),
'permissions' => $this->input->getOption('permissions'),
'fullname' => $this->input->getOption('fullname'),
'title' => $this->input->getOption('title'),
'state' => $this->input->getOption('state')
];
$this->validateOptions();
$helper = $this->getHelper('question');
$data = [];
$this->output->writeln('<green>Creating new user</green>');
$this->output->writeln('');
if (!$this->options['user']) {
// Get username and validate
$question = new Question('Enter a <yellow>username</yellow>: ', 'admin');
$question->setValidator(function ($value) {
$this->validate('user', $value);
if (User::find($value, ['username'])->exists()) {
throw new \RuntimeException('Username "' . $value . '" already exists, please pick another username');
};
return $value;
});
$username = $helper->ask($this->input, $this->output, $question);
} else {
$username = $this->options['user'];
}
if (!$this->options['password1']) {
// Get password and validate
$password = $this->askForPassword($helper, 'Enter a <yellow>password</yellow>: ', function ($password1) use ($helper) {
$this->validate('password1', $password1);
// Since input is hidden when prompting for passwords, the user is asked to repeat the password
return $this->askForPassword($helper, 'Repeat the <yellow>password</yellow>: ', function ($password2) use ($password1) {
return $this->validate('password2', $password2, $password1);
});
});
$data['password'] = $password;
} else {
$data['password'] = $this->options['password1'];
}
if (!$this->options['email']) {
// Get email and validate
$question = new Question('Enter an <yellow>email</yellow>: ');
$question->setValidator(function ($value) {
return $this->validate('email', $value);
});
$data['email'] = $helper->ask($this->input, $this->output, $question);
} else {
$data['email'] = $this->options['email'];
}
if (!$this->options['permissions']) {
// Choose permissions
$question = new ChoiceQuestion(
'Please choose a set of <yellow>permissions</yellow>:',
array('a' => 'Admin Access', 's' => 'Site Access', 'b' => 'Admin and Site Access'),
'a'
);
$question->setErrorMessage('Permissions %s is invalid.');
$permissions_choice = $helper->ask($this->input, $this->output, $question);
} else {
$permissions_choice = $this->options['permissions'];
}
switch ($permissions_choice) {
case 'a':
$data['access']['admin'] = ['login' => true, 'super' => true];
break;
case 's':
$data['access']['site'] = ['login' => true];
break;
case 'b':
$data['access']['admin'] = ['login' => true, 'super' => true];
$data['access']['site'] = ['login' => true];
}
if (!$this->options['fullname']) {
// Get fullname
$question = new Question('Enter a <yellow>fullname</yellow>: ');
$question->setValidator(function ($value) {
return $this->validate('fullname', $value);
});
$data['fullname'] = $helper->ask($this->input, $this->output, $question);
} else {
$data['fullname'] = $this->options['fullname'];
}
if (!$this->options['title'] && !count(array_filter($this->options))) {
// Get title
$question = new Question('Enter a <yellow>title</yellow>: ');
$data['title'] = $helper->ask($this->input, $this->output, $question);
} else {
$data['title'] = $this->options['title'];
}
if (!$this->options['state'] && !count(array_filter($this->options))) {
// Choose State
$question = new ChoiceQuestion(
'Please choose the <yellow>state</yellow> for the account:',
array('enabled' => 'Enabled', 'disabled' => 'Disabled'),
'enabled'
);
$question->setErrorMessage('State %s is invalid.');
$data['state'] = $helper->ask($this->input, $this->output, $question);
} else {
$data['state'] = $this->options['state'] ?: 'enabled';
}
// Lowercase the username for the filename
$username = strtolower($username);
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
// Create user object and save it
$user = new User($data);
$file = CompiledYamlFile::instance($locator->findResource('account://' . $username . YAML_EXT, true, true));
$user->file($file);
$user->save();
$this->output->writeln('');
$this->output->writeln('<green>Success!</green> User <cyan>' . $username . '</cyan> created.');
}
/**
*
*/
protected function validateOptions()
{
foreach (array_filter($this->options) as $type => $value) {
$this->validate($type, $value);
}
}
/**
* @param string $type
* @param mixed $value
* @param string $extra
*
* @return string
*/
protected function validate($type, $value, $extra = '')
{
return $this->login->validateField($type, $value, $extra);
}
/**
* Get password and validate.
*
* @param Helper $helper
* @param string $question
* @param callable $validator
*
* @return string
*/
protected function askForPassword(Helper $helper, $question, callable $validator)
{
$question = new Question($question);
$question->setValidator($validator);
$question->setHidden(true);
$question->setHiddenFallback(true);
return $helper->ask($this->input, $this->output, $question);
}
}
+39
View File
@@ -0,0 +1,39 @@
{
"name": "grav-plugin-login",
"description": "Enables user authentication and login screen.",
"keywords": ["login", "authentication", "admin", "security"],
"homepage": "https://github.com/getgrav/grav-plugin-login",
"license": "MIT",
"authors": [
{
"name": "Team Grav",
"email": "devs@getgrav.org",
"homepage": "http://getgrav.org",
"role": "Developer"
},
{
"name": "Sommerregen",
"email": "sommerregen@benjamin-regler.de",
"homepage": "http://benjamin-regler.de",
"role": "Developer"
}
],
"support": {
"email": "support@example.org",
"issues": "https://github.com/getgrav/grav-plugin-login/issues",
"irc": "https://gitter.im/getgrav/grav",
"forum": "http://getgrav.org/forum",
"docs": "https://github.com/getgrav/grav-plugin-login/blob/master/README.md"
},
"require": {
"php": ">=5.5.9",
"birke/rememberme": "1.*",
"robthree/twofactorauth": "^1.6",
"bacon/bacon-qr-code": "^1.0"
},
"autoload": {
"psr-4": {
"Grav\\Plugin\\Login\\": "classes/"
}
}
}
+210
View File
@@ -0,0 +1,210 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "bb17626cbf2be970add218a7cab0535f",
"packages": [
{
"name": "bacon/bacon-qr-code",
"version": "1.0.3",
"source": {
"type": "git",
"url": "https://github.com/Bacon/BaconQrCode.git",
"reference": "5a91b62b9d37cee635bbf8d553f4546057250bee"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/5a91b62b9d37cee635bbf8d553f4546057250bee",
"reference": "5a91b62b9d37cee635bbf8d553f4546057250bee",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"php": "^5.4|^7.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8"
},
"suggest": {
"ext-gd": "to generate QR code images"
},
"type": "library",
"autoload": {
"psr-0": {
"BaconQrCode": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "http://www.dasprids.de",
"role": "Developer"
}
],
"description": "BaconQrCode is a QR code generator for PHP.",
"homepage": "https://github.com/Bacon/BaconQrCode",
"time": "2017-10-17T09:59:25+00:00"
},
{
"name": "birke/rememberme",
"version": "1.0.5",
"source": {
"type": "git",
"url": "https://github.com/gbirke/rememberme.git",
"reference": "810473852eb4823098e47e23376a19b77ba0c165"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/gbirke/rememberme/zipball/810473852eb4823098e47e23376a19b77ba0c165",
"reference": "810473852eb4823098e47e23376a19b77ba0c165",
"shasum": ""
},
"require": {
"paragonie/random_compat": "^1.1.4"
},
"require-dev": {
"phpunit/phpunit": "4.*"
},
"type": "library",
"autoload": {
"psr-4": {
"Birke\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gabriel Birke",
"email": "gb@birke-software.de"
}
],
"description": "Secure \"Remember Me\" functionality",
"homepage": "https://github.com/gbirke/rememberme",
"keywords": [
"cookie",
"remember",
"security"
],
"time": "2017-02-12T12:43:00+00:00"
},
{
"name": "paragonie/random_compat",
"version": "v1.4.3",
"source": {
"type": "git",
"url": "https://github.com/paragonie/random_compat.git",
"reference": "9b3899e3c3ddde89016f576edb8c489708ad64cd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/9b3899e3c3ddde89016f576edb8c489708ad64cd",
"reference": "9b3899e3c3ddde89016f576edb8c489708ad64cd",
"shasum": ""
},
"require": {
"php": ">=5.2.0"
},
"require-dev": {
"phpunit/phpunit": "4.*|5.*"
},
"suggest": {
"ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
},
"type": "library",
"autoload": {
"files": [
"lib/random.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paragon Initiative Enterprises",
"email": "security@paragonie.com",
"homepage": "https://paragonie.com"
}
],
"description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
"keywords": [
"csprng",
"pseudorandom",
"random"
],
"time": "2018-04-04T21:48:54+00:00"
},
{
"name": "robthree/twofactorauth",
"version": "1.6.1",
"source": {
"type": "git",
"url": "https://github.com/RobThree/TwoFactorAuth.git",
"reference": "a77e7d822343bb88112baef808839cfae7bc5abb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/RobThree/TwoFactorAuth/zipball/a77e7d822343bb88112baef808839cfae7bc5abb",
"reference": "a77e7d822343bb88112baef808839cfae7bc5abb",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"require-dev": {
"phpunit/phpunit": "@stable"
},
"type": "library",
"autoload": {
"psr-4": {
"RobThree\\Auth\\": "lib"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Rob Janssen",
"homepage": "http://robiii.me",
"role": "Developer"
}
],
"description": "Two Factor Authentication",
"homepage": "https://github.com/RobThree/TwoFactorAuth",
"keywords": [
"Authentication",
"MFA",
"Multi Factor Authentication",
"Two Factor Authentication",
"authenticator",
"authy",
"php",
"tfa"
],
"time": "2017-11-06T17:55:56+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=5.5.9"
},
"platform-dev": []
}
+86
View File
@@ -0,0 +1,86 @@
#grav-login {
max-width: 30rem;
margin: 5rem auto;
background: #fcfcfc;
border: 4px solid #eee;
border-radius: 4px;
padding: 1rem 3rem 3rem 3rem;
text-align: center;
}
#grav-login .form-actions {
text-align: right;
}
#grav-logout {
position: absolute;
bottom: 5px;
right: 5px;
}
.alert.info {
color: #27ae60;
}
.alert.error {
color: #e74c3c;
}
#grav-login p {
font-size: small;
margin: 1rem 0;
padding: 0;
text-align: center;
}
#grav-login .form-actions p {
margin-bottom: 0;
}
#grav-login .button {
vertical-align: middle;
}
#grav-login .delimiter {
display: block;
font-size: 1.6rem;
letter-spacing: 1px;
line-height: 1.6rem;
position: relative;
text-transform: uppercase;
margin: 1rem 0;
}
#grav-login .delimiter:after,
#grav-login .delimiter:before {
background-color: #777777;
content: "";
height: 1px;
position: absolute;
top: 0.8rem;
width: 40%;
}
#grav-login .delimiter:before {
background-image: -moz-linear-gradient(right center , #777777, #ffffff);
left: 0;
}
#grav-login .delimiter:after {
background-image: -moz-linear-gradient(left center , #777777, #ffffff);
right: 0;
}
#grav-login .rememberme {
display: inline-block;
float: left;
padding: 7px 0;
vertical-align: middle;
}
#grav-login .rememberme label {
font-weight: inherit;
display: inline;
}
.login-status {
white-space: nowrap;
vertical-align: middle;
}
+47
View File
@@ -0,0 +1,47 @@
PLUGIN_LOGIN:
USERNAME: "Benutzername"
EMAIL: "Email"
USERNAME_EMAIL: "Benutzername/Email"
PASSWORD: "Passwort"
ACCESS_DENIED: "Zugang verweigert"
LOGIN_FAILED: "Login fehlgeschlagen..."
LOGIN_SUCCESSFUL: "Du wurdest erfolgreich eingeloggt."
BTN_LOGIN: "Anmelden"
BTN_LOGOUT: "Abmelden"
BTN_FORGOT: "Vergessen"
BTN_REGISTER: "Registrieren"
REMEMBER_ME: "Angemeldet bleiben"
REMEMBER_ME_HELP: "Speichert einen Cookie im Browser, welcher eine fortwährende Anmeldung sicherstellt."
BUILTIN_CSS: "Nutze das integrierte CSS"
BUILTIN_CSS_HELP: "Nutze das CSS, welches vom Admin Plugin bereitgestellt werden"
ROUTE: "Anmeldepfad"
ROUTE_REGISTER: "Registrierungspfad"
USERNAME_NOT_AVAILABLE: "Der Nutzername %s existiert bereits, bitte wähle einen Anderen"
USER_NEEDS_EMAIL_FIELD: "Der Nutzer benötigt ein E-Mail Feld"
EMAIL_SENDING_FAILURE: "Ein Fehler ist beim senden der E-Mail aufgetreten"
ACTIVATION_EMAIL_SUBJECT: "Aktiviere dein Account auf %s"
ACTIVATION_EMAIL_BODY: "Hi %s, click %s to activate your account on %s"
WELCOME_EMAIL_SUBJECT: "Willkommen zu %s"
WELCOME_EMAIL_BODY: "Hi %s, willkommen zu %s!"
NOTIFICATION_EMAIL_SUBJECT: "Neuer Nutzer auf %s"
NOTIFICATION_EMAIL_BODY: "Hi, ein neuer Nutzer hat sich auf %s registriert. Nutzername: %s, E-Mail: %s"
EMAIL_FOOTER: "GetGrav.org"
ACTIVATION_LINK_EXPIRED: "Aktivierungslink ist abgelaufen"
USER_ACTIVATED_SUCCESSFULLY: "Nutzer erfolgreich aktiviert"
INVALID_REQUEST: "Ungültige Anfrage"
USER_REGISTRATION: "Nutzer Registrierung"
USER_REGISTRATION_ENABLED_HELP: "Aktiviere die Nutzer Registrierung"
VALIDATE_PASSWORD1_AND_PASSWORD2: "Überprüfe das doppelt eingegebene Passwort"
SEND_ACTIVATION_EMAIL: "Aktivierungs E-Mail senden"
SEND_NOTIFICATION_EMAIL: "Benachtichtigungs E-Mail senden"
SEND_WELCOME_EMAIL: "Sende eine Willkommens E-Mail"
DEFAULT_VALUES: "Standard Werte"
ADDITIONAL_PARAM_KEY: "Parameter"
ADDITIONAL_PARAM_VALUE: "Wert"
REGISTRATION_FIELDS: "Registrierungsfelder"
REGISTRATION_FIELD_KEY: "Feldname"
REDIRECT_AFTER_LOGIN: "Umleitung nach Login"
REDIRECT_AFTER_REGISTRATION: "Umleitung nach Registrierung"
OPTIONS: "Optionen"
EMAIL_VALIDATION_MESSAGE: "Muss eine gültige E-Mail Adresse sein"
WELCOME: "Willkommen"
+129
View File
@@ -0,0 +1,129 @@
PLUGIN_LOGIN:
USERNAME: "Username"
EMAIL: "Email"
USERNAME_EMAIL: "Username/Email"
PASSWORD: "Password"
ACCESS_DENIED: "Access denied..."
LOGIN_FAILED: "Login failed..."
LOGIN_SUCCESSFUL: "You have been successfully logged in."
BTN_LOGIN: "Login"
BTN_LOGOUT: "Logout"
BTN_FORGOT: "Forgot"
BTN_REGISTER: "Register"
BTN_RESET: "Reset Password"
BTN_SEND_INSTRUCTIONS: "Send Reset Instructions"
RESET_LINK_EXPIRED: "Reset link has expired, please try again"
RESET_PASSWORD_RESET: "Password has been reset"
RESET_INVALID_LINK: "Invalid reset link used, please try again"
FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL: "Instructions to reset your password have been sent via email"
FORGOT_FAILED_TO_EMAIL: "Failed to email instructions, please try again later"
FORGOT_CANNOT_RESET_EMAIL_NO_EMAIL: "Cannot reset password for %s, no email address is set"
FORGOT_CANNOT_RESET_EMAIL_NO_PASSWORD: "Cannot reset password for %s, this email is associated with a remote account"
FORGOT_USERNAME_DOES_NOT_EXIST: "User with username <b>%s</b> does not exist"
FORGOT_EMAIL_NOT_CONFIGURED: "Cannot reset password. This site is not configured to send emails"
FORGOT_EMAIL_SUBJECT: "%s Password Reset Request"
FORGOT_EMAIL_BODY: "<h1>Password Reset</h1><p>Dear %1$s,</p><p>A request was made on <b>%4$s</b> to reset your password.</p><p><br /><a href=\"%2$s\" class=\"btn-primary\">Click this to reset your password</a><br /><br /></p><p>Alternatively, copy the following URL into your browser's address bar:</p> <p class=\"word-break\"><a href=\"%2$s\">%2$s</a></p> <p><br />Kind regards,<br /><br />%3$s</p>"
SESSION: "&ldquo;Remember Me&rdquo;-Session"
REMEMBER_ME: "Remember Me"
REMEMBER_ME_HELP: "Sets a persistent cookie on your browser to allow persistent-login authentication between sessions."
REMEMBER_ME_STOLEN_COOKIE: "Someone else has used your login information to access this page! All sessions were logged out. Please log in with your credentials and check your data."
BUILTIN_CSS: "Use built in CSS"
BUILTIN_CSS_HELP: "Include the CSS provided by the admin plugin"
ROUTE: "Login path"
ROUTE_HELP: "Custom route to a custom login page that your theme provides"
ROUTE_REGISTER: "Registration path"
ROUTE_REGISTER_HELP: "Route to the registration page. Set this if you want to use the built-in registration page. Leave it empty if you have your own registration form"
USERNAME_NOT_VALID: "Username should be between 3 and 16 characters, including lowercase letters, numbers, underscores, and hyphens. Uppercase letters, spaces, and special characters are not allowed"
USERNAME_NOT_AVAILABLE: "Username %s already exists, please pick another username"
EMAIL_NOT_AVAILABLE: "Email address %s already exists, please pick another email address"
PASSWORD_NOT_VALID: "Password must contain at least one number and one uppercase and lowercase letter, and at least 8 or more characters"
PASSWORDS_DO_NOT_MATCH: "Passwords do not match. Double-check you entered the same password twice"
USER_NEEDS_EMAIL_FIELD: "The user needs an email field"
EMAIL_SENDING_FAILURE: "An error occurred while sending the email"
ACTIVATION_EMAIL_SUBJECT: "Activate your account on %s"
ACTIVATION_EMAIL_BODY: "<h1>Account Activation</h1><p>Hi %1$s, </p><p>Your account has been successfully created on <b>%3$s</b>, but you cannot login until it is activated.</p><p><br/><a href=\"%2$s\" class=\"btn-primary\">Activate Your Account Now</a><br/><br/></p><p>Alternatively, copy the following URL into your browser's address bar:</p><p class=\"word-break\"><a href=\"%2$s\">%2$s</a></p><p><br/>Kind regards,<br/><br/>%4$s</p>"
ACTIVATION_NOTICE_MSG: "Hi %s, your account is created, please check your email to fully activate it"
WELCOME_EMAIL_SUBJECT: "Welcome to %s"
WELCOME_EMAIL_BODY: "<h1>Account Created</h1><p>Hi %1$s, </p><p>Your account has been successfully created on <b>%3$s</b>.</p><p><br/><a href=\"%2$s\" class=\"btn-primary\">Login Now</a><br/><br/></p><p>Alternatively, copy the following URL into your browser's address bar:</p><p class=\"word-break\"><a href=\"%2$s\">%2$s</a></p><p><br/>Kind regards,<br/><br/>%4$s</p>"
WELCOME_NOTICE_MSG: "Hi %s, your account has been successfully created"
NOTIFICATION_EMAIL_SUBJECT: "New user on %s"
NOTIFICATION_EMAIL_BODY: "<h1>New User</h1><p>Hi, a new user registered on %1$s.</p><p><ul><li>Username: <b>%2$s</b></li><li>Email: <b>%3$s</b></ul><p><p><br/><a href=\"%4$s\" class=\"btn-primary\">Visit %1$s</a><br/><br/></p>"
EMAIL_FOOTER: "GetGrav.org"
ACTIVATION_LINK_EXPIRED: "Activation link expired"
USER_ACTIVATED_SUCCESSFULLY: "User activated successfully"
INVALID_REQUEST: "Invalid request"
USER_REGISTRATION: "User Registration"
USER_REGISTRATION_ENABLED_HELP: "Enable the user registration"
VALIDATE_PASSWORD1_AND_PASSWORD2: "Validate double entered password"
VALIDATE_PASSWORD1_AND_PASSWORD2_HELP: "Validate and compare two different fields for the passwords, named `password1` and `password2`. Enable this if you have two password fields in the registration form"
SET_USER_DISABLED: "Set the user as disabled"
SET_USER_DISABLED_HELP: "Best used along with the `Send activation email` email. Adds the user to Grav, but sets it as disabled"
LOGIN_AFTER_REGISTRATION: "Login the user after registration"
LOGIN_AFTER_REGISTRATION_HELP: "Immediately login the user after the registration. If email activation is required, the user will be logged in immediately after activating the account"
SEND_ACTIVATION_EMAIL: "Send activation email"
SEND_ACTIVATION_EMAIL_HELP: "Sends an email to the user to activate his account. Enable the `Set the user as disabled` option when using this feature, so the user will be set as disabled and an email will be sent to activate the account"
SEND_NOTIFICATION_EMAIL: "Send notification email"
SEND_NOTIFICATION_EMAIL_HELP: "Notifies the site admin that a new user has registered. The email will be sent to the `To` field in the Email Plugin configuration"
SEND_WELCOME_EMAIL: "Send welcome email"
SEND_WELCOME_EMAIL_HELP: "Sends an email to the newly registered user"
DEFAULT_VALUES: "Default values"
DEFAULT_VALUES_HELP: "List of field names and values associated, that will be added to the user profile (yaml file) by default, without being configurable by the user. Separate multiple values with a comma, with no spaces between the values"
ADDITIONAL_PARAM_KEY: "Parameter"
ADDITIONAL_PARAM_VALUE: "Value"
REGISTRATION_FIELDS: "Registration fields"
REGISTRATION_FIELDS_HELP: "Add the fields that will be added to the user Yaml file. Fields not listed here will not be added even if present in the registration form"
REGISTRATION_FIELD_KEY: "Field name"
REDIRECT_AFTER_LOGIN: "Redirect after login"
REDIRECT_AFTER_LOGIN_HELP: "Custom route to redirect after login"
REDIRECT_AFTER_LOGOUT: "Redirect after logout"
REDIRECT_AFTER_LOGOUT_HELP: "Custom route to redirect after logout"
REDIRECT_AFTER_REGISTRATION: "Redirect after registration"
REDIRECT_AFTER_REGISTRATION_HELP: "Custom route to redirect after the registration"
OPTIONS: "Options"
EMAIL_VALIDATION_MESSAGE: "Must be a valid email address"
PASSWORD_VALIDATION_MESSAGE: "Password must contain at least one number and one uppercase and lowercase letter, and at least 8 or more characters"
TIMEOUT_HELP: "Sets the session timeout in seconds when Remember Me is enabled and checked by the user. Minimum is 604800 which means 1 week"
GROUPS_HELP: "List of groups the new registered user will be part of, if any"
SITE_ACCESS_HELP: "List of site access levels the new registered user will have. Example: `login` -> `true` "
WELCOME: "Welcome"
REDIRECT_AFTER_ACTIVATION: "Redirect after the user activation"
REDIRECT_AFTER_ACTIVATION_HELP: "Used if the user is required to activate the account via email. Once activated, this route will be shown"
REGISTRATION_DISABLED: "Registration disabled"
USE_PARENT_ACL_LABEL: "Use parent access rules"
USE_PARENT_ACL_HELP: "Check for parent access rules if no rules are defined"
PROTECT_PROTECTED_PAGE_MEDIA_LABEL: "Protect a login-protected page media"
PROTECT_PROTECTED_PAGE_MEDIA_HELP: "If enabled, media of a login protected page is login protected as well and cannot be seen unless logged in"
SECURITY_TAB: "Security"
MAX_RESETS_COUNT: "Max password resets count"
MAX_RESETS_COUNT_HELP: "Password reset flood protection setting (0 - not limited)"
MAX_RESETS_INTERVAL: "Max password resets interval"
MAX_RESETS_INTERVAL_HELP: "The time interval for the max password resets count value"
FORGOT_CANNOT_RESET_IT_IS_BLOCKED: "Cannot reset password for %s, password reset functionality temporarily blocked, please try later (maximum %s minutes)"
MAX_LOGINS_COUNT: "Max logins count"
MAX_LOGINS_COUNT_HELP: "Flood protection setting (0 - not limited)"
MAX_LOGINS_INTERVAL: "Max logins interval"
MAX_LOGINS_INTERVAL_HELP: "The time interval for the login count value"
TOO_MANY_LOGIN_ATTEMPTS: "Too many failed login attempted in the configured time (%s minutes)"
SECONDS: "seconds"
RESETS: "resets"
ATTEMPTS: "attempts"
ROUTES: "Routes"
ROUTE_FORGOT: "Forgot password route"
ROUTE_RESET: "Reset password route"
ROUTE_PROFILE: "User profile route"
ROUTE_ACTIVATE: "User activation route"
LOGGED_OUT: "You have been successfully logged out..."
PAGE_RESTRICTED: "Access is restricted, please login..."
DYNAMIC_VISIBILITY: "Dynamic Page Visibility"
DYNAMIC_VISIBILITY_HELP: "Allows dynamic processing of page visibility base on access rules if login.visibility_requires_access is set to true on a page"
USER_IS_REMOTE_ONLY: "This user authenticated with a remote service, so profile cannot be saved"
2FA_TITLE: "2-Factor Authentication"
2FA_INSTRUCTIONS: "##### 2-Factor Authentication\nYou have **2FA** enabled on this account. Please use your **2FA** app to enter the current **6-digit code** to complete the login process."
2FA_REGEN_HINT: "Regenerating the secret will require you to update your authenticator app"
2FA_FAILED: "Invalid 2-Factor Authentication code, please try again..."
2FA_ENABLED: "2FA Enabled"
2FA_CODE_INPUT: "000000"
2FA_SECRET: "2FA Secret"
2FA_SECRET_HELP: "Scan this QR code into your [Authenticator App](https://learn.getgrav.org/admin-panel/2fa#apps). Also it's a good idea to backup the secret in a safe location, in case you need to reinstall your app. Check the [Grav docs](https://learn.getgrav.org/admin-panel/2fa) for more information "
2FA_REGENERATE: "Regenerate"
BTN_CANCEL: "Cancel"
+115
View File
@@ -0,0 +1,115 @@
PLUGIN_LOGIN:
USERNAME: "Nombre de usuario"
EMAIL: "Email"
USERNAME_EMAIL: "Nombre de usuario/Email"
PASSWORD: "Contraseña"
ACCESS_DENIED: "Acceso denegado..."
LOGIN_FAILED: "El acceso ha fallado..."
LOGIN_SUCCESSFUL: "Ha accedido correctamente."
BTN_LOGIN: "Acceder"
BTN_LOGOUT: "Salir"
BTN_FORGOT: "Olvidado"
BTN_REGISTER: "Registro"
BTN_RESET: "Restaurar contraseña"
BTN_SEND_INSTRUCTIONS: "Enviar instrucciones para restauración"
RESET_LINK_EXPIRED: "El enlace para la restauración ha caducado, inténtelo de nuevo."
RESET_PASSWORD_RESET: "La contraseña ha sido restaurada"
RESET_INVALID_LINK: "Ha utilizado un enlace de restauración inválido, inténtelo de nuevo."
FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL: "Instructions to reset your password have been sent via email"
FORGOT_FAILED_TO_EMAIL: "No se ha podido enviar el email con instrucciones, inténtelo de nuevo."
FORGOT_CANNOT_RESET_EMAIL_NO_EMAIL: "No se puede restaurar la contraseña para %s, no hay dirección de email."
FORGOT_USERNAME_DOES_NOT_EXIST: "No existe ningún usuario con el nombre <b>%s</b>."
FORGOT_EMAIL_NOT_CONFIGURED: "No se puede restaurar la contraseña. Este sitio no está configurado para enviar emails."
FORGOT_EMAIL_SUBJECT: "%s solicitud de restauración de contraseña"
FORGOT_EMAIL_BODY: "<h1>Restauración de contraseña</h1><p>Estimado/a %1$s,</p><p>Se ha realizado una petición de restauración de contraseña en <b>%4$s</b>.</p><p><br /><a href=\"%2$s\" class=\"btn-primary\">Pulse aquí para restaurar su contraseña</a><br /><br /></p><p>Como alternativa puede copiar la siguiente URL en la barra de direcciones de su navegador:</p> <p class=\"word-break\"><a href=\"%2$s\">%2$s</a></p> <p><br />Saludos cordiales,<br /><br />%3$s</p>"
SESSION: "&ldquo;Recuérdame&rdquo;-Sesión"
REMEMBER_ME: "Recuérdame"
REMEMBER_ME_HELP: "Fija una cookie persistente en su navegador para permitir autenticación persistente entre sesiones."
REMEMBER_ME_STOLEN_COOKIE: "Alguien ha usado su información de acceso para acceder a este sitio. Se han cerrado todas las sesiones. Vuelva a acceder con sus credenciales y compruebe sus datos."
BUILTIN_CSS: "Usar CSS incorporado"
BUILTIN_CSS_HELP: "Incluir CSS proporcionado por el plugin de administración"
ROUTE: "Ruta de acceso"
ROUTE_HELP: "Ruta de acceso personalizada que proporciona su tema"
ROUTE_REGISTER: "Ruta de registro"
ROUTE_REGISTER_HELP: "Ruta a la página de registro. Ajuste esto si desea utilizar la página de registro incorporada. Déjelo vacío so tiene su propio formulario de registro."
USERNAME_NOT_VALID: "El nombre de usuario deberá contener entre 3 y 16 caracteresEl nombre de usuario deberá contener entre 3 y 16 caracteres y puede incluir minúsculas, números, guiones bajos y normales. No se permiten letras mayúsculas, espacios ni caracteres especiales."
USERNAME_NOT_AVAILABLE: "El nombre de usuario %s ya existe, elija otro."
EMAIL_NOT_AVAILABLE: "El email %s ya existe, elija otro."
PASSWORD_NOT_VALID: "La contraseña debe contener por lo menois un número y una letra mayúscula y minúscula, y debe ser de al menos 8 caracteres."
PASSWORDS_DO_NOT_MATCH: "Las contraseñas no coinciden. Compruebe que ha introducido la misma contrasenna dos veces."
USER_NEEDS_EMAIL_FIELD: "El usuario necesita un campo de email."
EMAIL_SENDING_FAILURE: "Ha ocurrido un error al enviar el email."
ACTIVATION_EMAIL_SUBJECT: "Active su cuenta en %s"
ACTIVATION_EMAIL_BODY: "<h1>%Activación de cuenta</h1><p>Hola %1$s, </p><p>Su cuenta en <b>%3$s</b> ha sido creada con éxito, pero no podrá acceder hasta que se active..</p><p><br/><a href=\"%2$s\" class=\"btn-primary\">Active su cuenta ahora</a><br/><br/></p><p>Como alternativa puede copiar la siguiente URL en la barra de direcciones de su navegador:</p><p class=\"word-break\"><a href=\"%2$s\">%2$s</a></p><p><br/>Saludos cordiales,<br/><br/>%4$s</p>"
ACTIVATION_NOTICE_MSG: "Hola %s, su cuenta ha sido creada, compruebe su correo electrónico para acivarla completamente."
WELCOME_EMAIL_SUBJECT: "Bienvenido/a a %s"
WELCOME_EMAIL_BODY: "<h1>Cuenta creada</h1><p>Hola %1$s, </p><p>Su cuenta en <b>%3$s</b> ha sido creada con éxito.</p><p><br/><a href=\"%2$s\" class=\"btn-primary\">Acceda ahora</a><br/><br/></p><p>Como alternativa puede copiar la siguiente URL en la barra de direcciones de su navegador:</p><p class=\"word-break\"><a href=\"%2$s\">%2$s</a></p><p><br/>Saludos cordiales,<br/><br/>%4$s</p>"
WELCOME_NOTICE_MSG: "Hola %s, su cuenta ha sido creada con éxito."
NOTIFICATION_EMAIL_SUBJECT: "Usuario nuevo en %s"
NOTIFICATION_EMAIL_BODY: "<h1>Usuario nuevo</h1><p>Hola, se ha registrado un usuario nuevo en %1$s.</p><p><ul><li>Nombre de usuario: <b>%2$s</b></li><li>Email: <b>%3$s</b></ul><p><p><br/><a href=\"%4$s\" class=\"btn-primary\">Visite %1$s</a><br/><br/></p>"
EMAIL_FOOTER: "GetGrav.org"
ACTIVATION_LINK_EXPIRED: "Enlace de activación caducado"
USER_ACTIVATED_SUCCESSFULLY: "Usiario activado con éxito"
INVALID_REQUEST: "Solicitud inválida"
USER_REGISTRATION: "Registro de usuarios"
USER_REGISTRATION_ENABLED_HELP: "Activar el registro de usuarios"
VALIDATE_PASSWORD1_AND_PASSWORD2: "Validar contraseñas"
VALIDATE_PASSWORD1_AND_PASSWORD2_HELP: "Validar y comparar dos campos diferentes para las contraseñas, llamados `password1` y `password2`. Active esto si tiene dos cambpos de contraseña en el formulario de registro."
SET_USER_DISABLED: "Crear usuario inactivo"
SET_USER_DISABLED_HELP: "Utilizado en combinación con `Enviar email de activación`. Añade el usuario a Grav, pero lo marca inactivo."
LOGIN_AFTER_REGISTRATION: "Acceder con el usuario después del registro."
LOGIN_AFTER_REGISTRATION_HELP: "Hacer que el usuario acceda inmediatamente después del registro. Si se requiere activación mediante email, al usuario se le permitirá entrada justo después de activar la cuenta."
SEND_ACTIVATION_EMAIL: "Enviar email de activación"
SEND_ACTIVATION_EMAIL_HELP: "Envía un email al usuario para activar su cuenta. Active la opción `Crear usuario inactivo` si usa esta característica de modo que el usuario se creará en modo inactivo y se enviará un email para activar la cuenta."
SEND_NOTIFICATION_EMAIL: "Enviar email de notificación"
SEND_NOTIFICATION_EMAIL_HELP: "Notifica al administrador del sitio de que se ha registrado un usuario nuevo. El email se enviará a la dirección del campo `Para` en la configuración del Plugin Email."
SEND_WELCOME_EMAIL: "Enviar email de bienvenida"
SEND_WELCOME_EMAIL_HELP: "Envía un email al usuario recién creado"
DEFAULT_VALUES: "Valores predeterminados"
DEFAULT_VALUES_HELP: "Lista de nombres de campo y sus valores asociados que se añadirán al perfil de usuario (archivo yaml) de forma predeterminada, sin ser configurables por el usuario. Separe los valores con coma, sin espacios entre los valores."
ADDITIONAL_PARAM_KEY: "Parámetro"
ADDITIONAL_PARAM_VALUE: "Valor"
REGISTRATION_FIELDS: "Campos de registro"
REGISTRATION_FIELDS_HELP: "Añada los campos que se incluirán en el archivo Yaml del usuario. Los campos que no se muestren aquí no estarán presentes en el formulario de registro."
REGISTRATION_FIELD_KEY: "Nombre del campo"
REDIRECT_AFTER_LOGIN: "Redireccionar después del acceso"
REDIRECT_AFTER_LOGIN_HELP: "Ruta personalizada para redireccionar al acceder"
REDIRECT_AFTER_REGISTRATION: "Redireccionar después del registro"
REDIRECT_AFTER_REGISTRATION_HELP: "Ruta personalizada para redireccionar después del registro"
OPTIONS: "Opciones"
EMAIL_VALIDATION_MESSAGE: "Debe ser un email válido"
PASSWORD_VALIDATION_MESSAGE: "La contraseña debe contener al menos un número, una mayúscula y una minúscula, y debe ser de al menos 8 caracteres."
TIMEOUT_HELP: "Fija el tiempo de caducidad de la sesión en segundos cuando `Recuérdame` está activado y marcadopor el usuario. El mínimo es 604800, que es una semana."
GROUPS_HELP: "Lista de grupos del que formará parte el usuario reci´n registrado."
SITE_ACCESS_HELP: "Lista de niveles de acceso al sitio que tendrá el usuario nuevo. Por ejemplo: `login` -> `true` "
WELCOME: "Bienvenido/a"
REDIRECT_AFTER_ACTIVATION: "Redireccionar después de activación de usuario"
REDIRECT_AFTER_ACTIVATION_HELP: "Se usará si el usuario debe activar la cuenta vía email. Una vez activado, se mostrará esta ruta"
REGISTRATION_DISABLED: "Registro desactivado"
USE_PARENT_ACL_LABEL: "Usar reglas de acceso del padre"
USE_PARENT_ACL_HELP: "Comprobar reglas de acceso superiores si no hay reglas de acceso definidas."
PROTECT_PROTECTED_PAGE_MEDIA_LABEL: "Proteger medios de una página protegida por acceso"
PROTECT_PROTECTED_PAGE_MEDIA_HELP: "Si está activo, los medios (imágenes, etc.) de una página protegido por acceso también estarán protegidos y solo serán visibles para usuarios que hayan accedido (login)"
SECURITY_TAB: "Seguridad"
MAX_RESETS_COUNT: "Cantidad máxima de restauraciones de contraseña"
MAX_RESETS_COUNT_HELP: "Protección de avalancha de restauración de contraseña (0 - ilimitado)"
MAX_RESETS_INTERVAL: "Intervalo máximo de restauración de contraseña"
MAX_RESETS_INTERVAL_HELP: "El intervalo máximo de tiempo para el valor máximo de restauración"
FORGOT_CANNOT_RESET_IT_IS_BLOCKED: "No se puede restaurar la contraseña de %s, la funcionalidad de restauración ha sido bloqueada temporalmente. Inténtelo de nuevo más tarde (máximo %s minutos)"
MAX_LOGINS_COUNT: "Número máximo de accesos"
MAX_LOGINS_COUNT_HELP: "Protección de avalancha (0 - ilimitado)"
MAX_LOGINS_INTERVAL: "Intervalo máximo de acceso"
MAX_LOGINS_INTERVAL_HELP: "El intervalo de tiempo para el número máximo de accesos"
TOO_MANY_LOGIN_ATTEMPTS: "Ha habido demasiados intentos fallidos de acceso en el tiempo permitido (%s minutos)"
SECONDS: "segundos"
RESETS: "reinicios"
ATTEMPTS: "intentos"
ROUTES: "Rutas"
ROUTE_FORGOT: "Ruta de contraseña olvidada"
ROUTE_RESET: "Ruta de restauración de contraseña"
ROUTE_PROFILE: "Ruta del perfil de usuario"
ROUTE_ACTIVATE: "Ruta de activación de usuario"
LOGGED_OUT: "Su sesión ha finalizado correctamente..."
PAGE_RESTRICTED: "Acceso restringido, acceda con sus credenciales..."
DYNAMIC_VISIBILITY: "Visibilidad de páginas dinámicas"
DYNAMIC_VISIBILITY_HELP: "Permite el procesamiento de la visibilidad de páginas basada en reglas de acceso si login.visibility_requires_access es true en un página"
+89
View File
@@ -0,0 +1,89 @@
PLUGIN_LOGIN:
USERNAME: "Nom dutilisateur"
EMAIL: "E-mail"
USERNAME_EMAIL: "Nom dutilisateur/E-mail"
PASSWORD: "Mot de passe"
ACCESS_DENIED: "Accès refusé..."
LOGIN_FAILED: "Échec de la connexion..."
LOGIN_SUCCESSFUL: "Vous vous êtes connecté avec succès."
BTN_LOGIN: "Connexion"
BTN_LOGOUT: "Déconnexion"
BTN_FORGOT: "Mot de passe oublié"
BTN_REGISTER: "Senregister"
BTN_RESET: "Réinitialiser le mot de passe"
BTN_SEND_INSTRUCTIONS: "Envoyer les instructions de Réinitialisation"
RESET_LINK_EXPIRED: "Le lien de réinitialisation a expiré, veuillez réessayer"
RESET_PASSWORD_RESET: "Le mot de passe a été réinitialisé"
RESET_INVALID_LINK: "Le lien de réinitialisation utilisé nest pas valide, veuillez réessayer"
FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL: "Les instructions pour la réinitialisation de votre mot de passe ont été envoyées par e-mail"
FORGOT_FAILED_TO_EMAIL: "Impossible denvoyer les instructions, veuillez réessayer ultérieurement"
FORGOT_CANNOT_RESET_EMAIL_NO_EMAIL: "Impossible de réinitialiser le mot de passe pour %s, aucune adresse e-mail na été paramétrée"
FORGOT_USERNAME_DOES_NOT_EXIST: "Lutilisateur avec le nom dutilisateur <b>%s</b> nexiste pas"
FORGOT_EMAIL_NOT_CONFIGURED: "Impossible de réinitialiser le mot de passe. Ce site nest pas configuré pour envoyer des e-mails"
FORGOT_EMAIL_SUBJECT: "Demande de réinitialisation de mot de passe %s"
FORGOT_EMAIL_BODY: "<h1>Réinitialisation de mot de passe</h1><p>%1$s,</p><p>Une demande a été faite sur <b>%4$s</b> pour la réinitialisation de votre mot de passe.</p><p><br /><a href=\"%2$s\" class=\"btn-primary\">Cliquez ici pour réinitialiser votre mot de passe</a><br /><br /></p><p>Vous pouvez également copier lURL suivante dans la barre dadresse de votre navigateur :</p> <p class=\"word-break\"><a href=\"%2$s\">%2$s</a></p> <p><br />Cordialement,<br /><br />%3$s</p>"
SESSION: "Session - &ldquo;Se souvenir de moi&rdquo;"
REMEMBER_ME: "Se souvenir de moi"
REMEMBER_ME_HELP: "Définit un cookie persistant sur votre navigateur autorisant lauthentification par connexion persistante entre les sessions."
REMEMBER_ME_STOLEN_COOKIE: "Quelquun dautre a utilisé vos informations de connexion pour accéder à cette page ! Toutes les sessions ont été déconnectées. Veuillez vous connecter avec vos identifiants et vérifier vos données."
BUILTIN_CSS: "Utiliser les CSS intégrés"
BUILTIN_CSS_HELP: "Utiliser les CSS fournis dans le plugin dadministration"
ROUTE: "Chemin de connexion"
ROUTE_HELP: "Chemin personnalisé vers une page de connexion personnalisée proposée par votre thème"
ROUTE_REGISTER: "Chemin vers linscription"
ROUTE_REGISTER_HELP: "Chemin vers la page dinscription. A définir si vous souhaitez utiliser la page dinscription intégrée. Laisser vide si vous disposez de votre propre formulaire dinscription."
USERNAME_NOT_VALID: "Le nom dutilisateur doit comporter entre 3 et 16 caractères et peut être composé de lettres minuscules, de chiffres et de tirets de soulignement (underscores) et des traits dunion. Les lettres majuscules, les espaces et les caractères spéciaux ne sont pas autorisés."
USERNAME_NOT_AVAILABLE: "Le nom dutilisateur %s existe déjà, veuillez en choisir un autre."
PASSWORD_NOT_VALID: "Le mot de passe doit contenir au moins un chiffre, une majuscule et une minuscule et être composé dau moins 8 caractères"
PASSWORDS_DO_NOT_MATCH: "Les mots de passe sont différents. Réessayez de saisir le même mot de passe deux fois."
USER_NEEDS_EMAIL_FIELD: "Lutilisateur a besoin dun champ pour e-mail"
EMAIL_SENDING_FAILURE: "Une erreur est survenue lors de lenvoi de le-mail."
ACTIVATION_EMAIL_SUBJECT: "Activer votre compte sur %s"
ACTIVATION_EMAIL_BODY: "Bonjour %s, <a href=\"%s\">cliquez</a> pour activer votre compte sur %s"
WELCOME_EMAIL_SUBJECT: "Bienvenue sur %s"
WELCOME_EMAIL_BODY: "Bonjour %s, bienvenue sur %s!"
NOTIFICATION_EMAIL_SUBJECT: "Nouvel utilisateur sur %s"
EMAIL_FOOTER: "GetGrav.org"
NOTIFICATION_EMAIL_BODY: "Bonjour, un nouvel utilisateur sest inscrit sur %s. Nom dutilisateur : %s, e-mail : %s"
ACTIVATION_LINK_EXPIRED: "Le lien dactivation a expiré"
USER_ACTIVATED_SUCCESSFULLY: "Utilisateur activé avec succès"
INVALID_REQUEST: "Requête non valide"
USER_REGISTRATION: "Inscription de lutilisateur"
USER_REGISTRATION_ENABLED_HELP: "Activer linscription des utilisateurs"
VALIDATE_PASSWORD1_AND_PASSWORD2: "Valider la double saisie du mot de passe"
VALIDATE_PASSWORD1_AND_PASSWORD2_HELP: "Comparer et valider deux champs pour les mots de passe `Mot de passe 1` et `Mot de passe 2`. Activez cette option si vous avez deux champs de mots de passe dans le formulaire dinscription."
SET_USER_DISABLED: "Définir lutilisateur comme désactivé"
SET_USER_DISABLED_HELP: "La meilleure pratique si vous utilisez loption `Envoyer un e-mail dactivation`. Ajoute lutilisateur à Grav, mais le défini comme étant désactivé."
LOGIN_AFTER_REGISTRATION: "Connecte lutilisateur après son inscription"
LOGIN_AFTER_REGISTRATION_HELP: "Identifier immédiatement lutilisateur après linscription. Si le-mail dactivation est demandé, lutilisateur sera connecté immédiatement après lactivation du compte."
SEND_ACTIVATION_EMAIL: "Envoyer un e-mail dactivation"
SEND_ACTIVATION_EMAIL_HELP: "Envoyer un e-mail à lutilisateur pour lactivation son compte. Lorsque vous utilisez cette fonction, activez loption `Définir lutilisateur comme désactivé` afin que lutilisateur soit désactivé et quun e-mail lui soit envoyé pour activer son compte."
SEND_NOTIFICATION_EMAIL: "Envoyer un e-mail de notification"
SEND_NOTIFICATION_EMAIL_HELP: "Informe ladministrateur du site quun nouvel utilisateur sest enregistré. Le-mail sera envoyé à la personne renseignée dans le champ `À` dans la configuration du plugin e-mail."
SEND_WELCOME_EMAIL: "Envoyer un e-mail de bienvenue"
SEND_WELCOME_EMAIL_HELP: "Envoyer un e-mail à un nouvel utilisateur enregistré."
DEFAULT_VALUES: "Valeurs par défaut"
DEFAULT_VALUES_HELP: "Liste des noms et valeurs associés pour les champs. Ils seront ajoutés au profil utilisateur par défaut (fichier yaml), sans pouvoir être configurables par lutilisateur. Séparez les différentes valeurs par une virgule, sans espaces entre les valeurs."
ADDITIONAL_PARAM_KEY: "Paramètre"
ADDITIONAL_PARAM_VALUE: "Valeur"
REGISTRATION_FIELDS: "Champs dinscription"
REGISTRATION_FIELDS_HELP: "Ajouter les champs qui seront ajoutés au fichier yaml de lutilisateur. Les champs non listés ne seront pas ajoutés même sils sont présent sur le formulaire dinscription"
REGISTRATION_FIELD_KEY: "Nom du champ"
REDIRECT_AFTER_LOGIN: "Redirection après connexion"
REDIRECT_AFTER_LOGIN_HELP: "Chemin personnalisé de redirection après la connexion"
REDIRECT_AFTER_REGISTRATION: "Redirection après inscription"
REDIRECT_AFTER_REGISTRATION_HELP: "Chemin personnalisé de redirection après linscription"
OPTIONS: "Options"
EMAIL_VALIDATION_MESSAGE: "Doit-être une adresse e-mail valide"
PASSWORD_VALIDATION_MESSAGE: "Le mot de passe doit-être composé dau moins un chiffre, une majuscule et une minuscule, et au moins 8 caractères"
TIMEOUT_HELP: "Définit le délai dexpiration de la session en secondes lorsque `Se souvenir de moi` est activé et coché par lutilisateur. Minimum de 604800 ce qui correspond à 1 semaine."
GROUPS_HELP: "Liste des groupes auxquels le nouvel utilisateur enregistré fera partie, le cas échéant."
SITE_ACCESS_HELP: "Liste des niveaux daccès au site attribués au nouvel utilisateur enregistré. Exemple : `login` -> `true` "
WELCOME: "Bienvenue"
REDIRECT_AFTER_ACTIVATION: "Redirection après lactivation de lutilisateur"
REDIRECT_AFTER_ACTIVATION_HELP: "Utilisé sil est nécessaire pour lutilisateur dactiver le compte par e-mail. Une fois activé, ce chemin saffichera"
REGISTRATION_DISABLED: "Inscription désactivée"
USE_PARENT_ACL_LABEL: "Appliquer les règles daccès parentes"
USE_PARENT_ACL_HELP: "Utiliser les règles daccès parentes si aucune règle na été définie"
PROTECT_PROTECTED_PAGE_MEDIA_LABEL: "Protéger le média d'une page par une protection par connexion"
PROTECT_PROTECTED_PAGE_MEDIA_HELP: "Si activé, les médias d'une page protégée par connexion sera également protégé par un système de connexion et ne pourra pas être visible à moins d'être connecté."
+41
View File
@@ -0,0 +1,41 @@
PLUGIN_LOGIN:
ACCESS_DENIED: "Pristup odbijen..."
LOGIN_FAILED: "Prijava nije uspjela..."
BTN_LOGIN: "Prijava"
BTN_LOGOUT: "Odjava"
BTN_FORGOT: "Zaboravih"
BTN_REGISTER: "Registriraj"
REMEMBER_ME: "Zapamti me"
BUILTIN_CSS: "Koristi ugrađeni CSS"
BUILTIN_CSS_HELP: "Uključi CSS koji dolazi sa admin pluginom"
ROUTE: "Putanja prijave"
ROUTE_REGISTER: "Putanja registracije"
USERNAME_NOT_VALID: "Korisničko ime bi trebalo biti između 3 i 16 znakova, uključujući mala slova, brojeve, _, i crtice. VELIKA SLOVA, razmaci, i posebni znakovi nisu dopušteni"
USERNAME_NOT_AVAILABLE: "Korisničko ime %s već postoji, odaberi neko drugo"
PASSWORD_NOT_VALID: "Lozinka mora sadržavati bar jedan broj i jedno veliko i malo slovo, i bar još 8 ili više znakova"
PASSWORDS_DO_NOT_MATCH: "Lozinke se ne slažu. Poonovo provjeri da li si unio istu lozinku dva puta"
USER_NEEDS_EMAIL_FIELD: "Korisnik treba email polje"
EMAIL_SENDING_FAILURE: "Došlo je do greške pri slanju emaila"
ACTIVATION_LINK_EXPIRED: "Aktivacijski link je istekao"
USER_ACTIVATED_SUCCESSFULLY: "Korisnik je uspješno aktiviran"
INVALID_REQUEST: "Nevaljani zahtjev"
USER_REGISTRATION: "Registracija korisnika"
USER_REGISTRATION_ENABLED_HELP: "Omogući registraciju korisnika"
VALIDATE_PASSWORD1_AND_PASSWORD2: "Validiraj duplo unesenu lozinku"
VALIDATE_PASSWORD1_AND_PASSWORD2_HELP: "Validiraj i usporedi dva različčita polja za lozinke, imenovana `lozinka1` i `lozinka2`. Omogući ovo ako imaš dva polja za lozinke u formularu registracije"
LOGIN_AFTER_REGISTRATION: "Ulogiraj korisnika nakon reegistracije"
LOGIN_AFTER_REGISTRATION_HELP: "Ulogiraj korisnika odmah nakon registracije. Ako je potrebna email aktivacija, korisnik će biti ulogiran odmah nakon aktiviranja računa"
SEND_ACTIVATION_EMAIL: "Pošalji aktivacijski email"
SEND_ACTIVATION_EMAIL_HELP: "Šalje email korisniku da aktivira svoja račun."
SEND_NOTIFICATION_EMAIL: "Pošalji email obavijest"
SEND_NOTIFICATION_EMAIL_HELP: "Obavještava administratora da se novi korisnik registrirao. Email će biti poslan na `To` polje u Email Plugin konfiguraciji"
SEND_WELCOME_EMAIL: "Pošalji email dobrodošlice"
SEND_WELCOME_EMAIL_HELP: "Šalje email novoregistriranom korisniku"
DEFAULT_VALUES: "Određene vrijednosti"
DEFAULT_VALUES_HELP: "List of field names and values associated, that will be added to the user profile (yaml file) by default, without being configurable by the user. Separate multiple values with a comma, with no spaces between the values"
ADDITIONAL_PARAM_KEY: "Parametar"
ADDITIONAL_PARAM_VALUE: "Vrijednost"
REGISTRATION_FIELDS: "Registracijska polja"
REGISTRATION_FIELDS_HELP: "Add the fields that will be added to the user yaml file. Fields not listed here will not be added even if present in the registration form"
REGISTRATION_FIELD_KEY: "Ime polja"
OPTIONS: "Opcije"
+22
View File
@@ -0,0 +1,22 @@
PLUGIN_LOGIN:
ACCESS_DENIED: "Hozzáférés megtagadva..."
LOGIN_FAILED: "Sikertelen belépés..."
LOGIN_SUCCESSFUL: "Sikeresen beléptél."
LOGGED_OUT: "Sikeresen kiléptél."
BTN_LOGIN: "Belépés"
BTN_LOGOUT: "Kilépés"
BTN_FORGOT: "Elfelejtettem"
BTN_REGISTER: "Regisztráció"
REMEMBER_ME: "Jegyezz meg"
REMEMBER_ME_HELP: "Elhelyezünk egy hosszú lejáratú sütit a böngésződben, hogy belépve maradhass két munkamenet között."
REMEMBER_ME_STOLEN_COOKIE: "Valaki a belépési adataid felhasználásával látogatta meg ezt az oldalt! Minden munkamenetet kiléptettünk. Kérlek, jelentkezz be ismét és ellenőrizd az adataidat."
BUILTIN_CSS: "Beépített CSS használata"
BUILTIN_CSS_HELP: "Az admin plugin által biztosított CSS beillesztése"
ROUTE: "Belépés útvonala"
ROUTE_HELP: "Egyedi útvonal egy egyedi belépő oldalhoz, melyet az aktuális téma biztosít"
ROUTE_REGISTER: "Regisztráció útvonala"
ROUTE_REGISTER_HELP: "A regisztrációs oldal elérési útja. Akkor állítsd be, ha a beépített regisztrációs oldalt szeretnéd használni"
USERNAME_NOT_VALID: "A felhasználónév 3-16 karakter hosszú legyen, tartalmazhat kisbetűket, számokat, aláhúzást és kötőjelet. Nagybetűk, szóköz és speciális karakterek használata nem megengedett"
USERNAME_NOT_AVAILABLE: "%s nevű felhasználó már létezik, kérlek válassz más felhasználónevet"
PASSWORD_NOT_VALID: "A jelszónak tartalmaznia kell legalább egy számot, egy kisbetűt és egy nagybetűt, valamint legalább 8 karakter hosszú kell, hogy legyen"
PASSWORDS_DO_NOT_MATCH: "A két jelszó nem egyezik meg. Győzödj meg róla, hogy azonos legyen a kettő"
+62
View File
@@ -0,0 +1,62 @@
PLUGIN_LOGIN:
USERNAME: "Brukernavn"
EMAIL: "E-post"
USERNAME_EMAIL: "Brukernavn/E-post"
PASSWORD: "Passord"
ACCESS_DENIED: "Adgang forbudt..."
LOGIN_FAILED: "Innlogging feilet..."
LOGIN_SUCCESSFUL: "Du har logget inn."
BTN_LOGIN: "Logg inn"
BTN_LOGOUT: "Logg ut"
BTN_FORGOT: "Glemt passord"
BTN_REGISTER: "Registrer"
BTN_RESET: "Tilbakestill Passord"
BTN_SEND_INSTRUCTIONS: "Send instruksjoner for tilbakestilling"
RESET_LINK_EXPIRED: "Tilbakestillingslenke har utløpt, vennligst prøv igjen"
RESET_PASSWORD_RESET: "Passord har blitt nullstilt"
RESET_INVALID_LINK: "Ugyldig ilbakestillingslenke, vennligst prøv igjen"
FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL: "Instruksjoner for å tilbakestille passordet ditt er sendt via e-post"
FORGOT_FAILED_TO_EMAIL: "Kunne ikke sende instruksjoner, prøv igjen senere"
FORGOT_CANNOT_RESET_EMAIL_NO_EMAIL: "Kan ikke tilbakestille passord for %s, ingen e-postadresse er angitt"
FORGOT_USERNAME_DOES_NOT_EXIST: "Bruker med brukernavn <b>%s</ b> eksisterer ikke"
FORGOT_EMAIL_NOT_CONFIGURED: "Kan ikke tilbakestille passord. Dette nettstedet er ikke konfigurert til å sende e-post"
FORGOT_EMAIL_SUBJECT: "Forespørsel om tilbakestilling av passord for %s"
FORGOT_EMAIL_BODY: "<h1>Tilbekestilling av passord</h1><p>%1$s,</p><p>En forespørsel om tilbakestilling av passord ble gjort på <b>%4$s</b>.</p><p><br /><a href=\"%2$s\" class=\"btn-primary\">Klikk her for å tilbakestille passordet ditt</a><br /><br /></p><p>Du kan også kopiere følgende nettadresse til nettleserens adressefelt:</p> <p class=\"word-break\"><a href=\"%2$s\">%2$s</a></p> <p><br />Vennlig hilsen,<br /><br />%3$s</p>"
SESSION: "&ldquo;Husk meg&rdquo;-Sesjon"
REMEMBER_ME: "Husk meg"
REMEMBER_ME_HELP: "Setter en informasjonskapsel i nettleseren din for å tillate persistent innlogging."
REMEMBER_ME_STOLEN_COOKIE: "Din innloggingsinformasjon har blitt benyttet et annet sted. Alle sesjoner har blitt logget ut. Vennligst logg inn med brukernavn og passord, og sjekk din brukerinformasjon."
BUILTIN_CSS: "Bruk innebygget stil"
BUILTIN_CSS_HELP: "Bruk stil fra admin-plugin"
ROUTE: "Adresse til innlogging"
ROUTE_HELP: "Adresse til innlogging"
USERNAME_NOT_VALID: "Brukernavn skal være fra 3 til 16 tegn, og kan bestå av små bokstaver, tall, understrek, and bindestrek. Store bokstaver, mellomrom og spesialtegn er ikke tillatt"
USERNAME_NOT_AVAILABLE: "Brukernavnet %s er allerede i bruk, vennligst velg et annet brukernavn"
EMAIL_NOT_AVAILABLE: "E-postadressen %s er i bruk, vennligst velg en annen e-postadresse"
PASSWORD_NOT_VALID: "Passord må inneholde minst ett tall, en listen og en stor bokstav, og må være minst 8 tegn"
PASSWORDS_DO_NOT_MATCH: "Passwordene er ikke like. Sjekk at du skrev inn samme passord to ganger"
USER_NEEDS_EMAIL_FIELD: "Brukeren trenger felt for e-post"
WELCOME_EMAIL_SUBJECT: "Velkommen til %s"
ADDITIONAL_PARAM_VALUE: "Verdi"
REGISTRATION_FIELD_KEY: "Feltnavn"
REDIRECT_AFTER_LOGIN: "Gå til side etter innlogging"
REDIRECT_AFTER_LOGIN_HELP: "Gå til side etter innlogging"
OPTIONS: "Valg"
EMAIL_VALIDATION_MESSAGE: "Må være en gyldig e-postadresse"
PASSWORD_VALIDATION_MESSAGE: "Passordet må bestå av minst ett tall, både store og små bokstaver, og minst 8 tegn"
WELCOME: "Velkommen"
SECURITY_TAB: "Sikkerhet"
MAX_RESETS_COUNT: "Maksimum antall tilbakestilling av passord"
MAX_RESETS_INTERVAL: "Minimum tid mellom tilbakestilling av passord"
MAX_RESETS_INTERVAL_HELP: "Minimum tid mellom hvert forsøk på tilbakestilling av passord"
MAX_LOGINS_COUNT: "Maksimum antall innlogginger"
MAX_LOGINS_INTERVAL: "Maksimum innloggingsinterval"
MAX_LOGINS_INTERVAL_HELP: "Tidsintervall for telling av innlogginsforsøk"
TOO_MANY_LOGIN_ATTEMPTS: "For mange innloggingsforsøk i løpet av %s minutter"
SECONDS: "sekunder"
RESETS: "nullstillinger"
ATTEMPTS: "forsøk"
ROUTE_FORGOT: "Adresse for &rdquo;Glemt passord&rdquo;"
ROUTE_RESET: "Adresse for tilbakestilling av passord"
ROUTE_PROFILE: "Adresse til brukerprofil"
PAGE_RESTRICTED: "Begrenset tilgang, vennligst logg inn..."
+86
View File
@@ -0,0 +1,86 @@
PLUGIN_LOGIN:
USERNAME: "Nume utilizator"
PASSWORD: "Parolă"
ACCESS_DENIED: "Acces refuzat..."
LOGIN_FAILED: "Logare eșuată..."
LOGIN_SUCCESSFUL: "Ați fost logat cu succes."
BTN_LOGIN: "Logare"
BTN_LOGOUT: "Ieșire din cont "
BTN_FORGOT: "Am uitat"
BTN_REGISTER: "Înregistrare"
BTN_RESET: "Resetează parola"
BTN_SEND_INSTRUCTIONS: "Trimite instrucțiuni pentru resetare"
RESET_LINK_EXPIRED: "Link-ul pentru resetarea parolei a expirat, vă rugăm încercați din nou "
RESET_PASSWORD_RESET: "Parola a fost modificată"
RESET_INVALID_LINK: "Link-ul pentru resetare este invalid, Invalid reset link used, vă rugăm încercați din nou "
FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL: "Instrucțiunile pentru resetarea parolei au fst trimise pe email"
FORGOT_FAILED_TO_EMAIL: "Instrucțiunile nu au putut fi trimise pe email, vă rugăm încercați mai târziu "
FORGOT_CANNOT_RESET_EMAIL_NO_EMAIL: "Parola nu poate fi resetată pentru %s, nu este setată nici o adresă de email"
FORGOT_USERNAME_DOES_NOT_EXIST: "Utilizatorul cu numele <b>%s</b> nu există"
FORGOT_EMAIL_NOT_CONFIGURED: "Parola nu poate fi resetată. Acest site nu este configurat pentru a trimite email-uri."
FORGOT_EMAIL_SUBJECT: "%s Cerere de resetare a parolei"
FORGOT_EMAIL_BODY: "<h1>Resetare parolă</h1><p> Dragă %1$s,</p><p>O cerere de resetare a parolei a fost făcută în data de <b>%4$s</b>.</p><p><br /><a href=\"%2$s\" class=\"btn-primary\">Apasă aici pentru a reseta parola</a><br /><br /></p><p>Alternativ, copiază URL de mai jos în bara de adrese a browser-ului favorit:</p> <p class=\"word-break\"><a href=\"%2$s\">%2$s</a></p> <p><br />Cu respect,<br /><br />%3$s</p>"
SESSION: "&ldquo;Ține-mă minte&rdquo;-Sesiune"
REMEMBER_ME: "Ține-mă minte"
REMEMBER_ME_HELP: "Setează o cookie în browserul Dvs. ce permite menținerea datelor de logare între sesiuni."
REMEMBER_ME_STOLEN_COOKIE: "Altcineva a folosit darele Dvs de logare pentru a accesa această pagină! Toate sesiunile au fost deconectate. Vă rugăm să vă logați cu datele Dvs. și să verificați toate detaliile. "
BUILTIN_CSS: "Folosește CSS-ul din modul"
BUILTIN_CSS_HELP: "Include CSS-ul furnizat de către modulul Admin"
ROUTE: "Calea pentru logare"
ROUTE_HELP: "O rută personalizată către pagina de logare pe care o furnizează tema activă "
ROUTE_REGISTER: "Calea pentru înregistrare "
ROUTE_REGISTER_HELP: "Ruta către pagina de înregistrare. Setați această rută dacă doriți folosirea paginei implicite pentru înregistrare. Lăsați gol dacă folosiți o pagină personalizată pentru înregistrare."
USERNAME_NOT_VALID: "Numele de utilizator trebuie să fie între 3-16 caractere, incluzând litere mici, numere, linii de subliniere și cratime. Literele de tipar, spațiile și caracterele speciale nu sunt permise. "
USERNAME_NOT_AVAILABLE: "Utilizatorul %s există deja, vă rugăm alegeți un alt nume de utilizator "
PASSWORD_NOT_VALID: " Parola trebuie să conțină cel puțin 1 număr și o literă de tipar și o literă mică; și să aibă minim 8 caractere"
PASSWORDS_DO_NOT_MATCH: " Parolele nu sunt identice. Vă rugăm verificați că ași scris aceeiași parolă de două ori."
USER_NEEDS_EMAIL_FIELD: "Utilizatorul trebuie să aibă adresa de email completată"
EMAIL_SENDING_FAILURE: "A apărut o eroare la trimirea email-ului"
ACTIVATION_EMAIL_SUBJECT: "Activați-vă contrul pentru %s"
ACTIVATION_EMAIL_BODY: "Salut %s, apasă <a href=\"%s\">aici</a> pentru a-ți activa contul de pe %s"
WELCOME_EMAIL_SUBJECT: "Bine ați venit pe %s"
WELCOME_EMAIL_BODY: "Salut %s, bine ai venit la %s!"
NOTIFICATION_EMAIL_SUBJECT: "Utilizator nou pe %s"
NOTIFICATION_EMAIL_BODY: "Salut, un nou utilizator s-a înregistrat pe %s. Nume de utilizator: %s, adresă de email: %s"
ACTIVATION_LINK_EXPIRED: "Link-ul pentru activare este expirat"
USER_ACTIVATED_SUCCESSFULLY: "Utilizator activat cu succes"
INVALID_REQUEST: "Cerere invalidă "
USER_REGISTRATION: "Înregistrare utilizator "
USER_REGISTRATION_ENABLED_HELP: "Activați înregistrarea utilizatorilor"
VALIDATE_PASSWORD1_AND_PASSWORD2: "Validați parola introdusă de două ori"
VALIDATE_PASSWORD1_AND_PASSWORD2_HELP: "Validați și comparați cele două câmpuri pentru parolă cu numele `password1` și `password2`. Activați această opțiune dacă există două câmpuri pentru parolă în formularul de înregistrare."
SET_USER_DISABLED: "Setați utilizatorul dezactivat"
SET_USER_DISABLED_HELP: "Cel mai bine să fie folosit împreună cu email-ul pentru activare. Adaugă utilizatorul în Grav dar îl setează ca dezactivat"
LOGIN_AFTER_REGISTRATION: "Loghează utilizatorul după înregistrare"
LOGIN_AFTER_REGISTRATION_HELP: "Imediat după înregistrare loghează utilizatorul în cont. Dacă este necesară activarea prin email, utilizatorul va fi logat imediat după activarea contului."
SEND_ACTIVATION_EMAIL: "Trimite email-ul pentru activare"
SEND_ACTIVATION_EMAIL_HELP: "Trimite un email către utilizatorul nou înregistrat pentru activarea contului. Activați opțiunea `Setați utilizatorul dezactivat` când folosiți această opțiune pentru a seta utilizatorul dezactivat și pentru a trimite un email automat pentru activarea contului. "
SEND_NOTIFICATION_EMAIL: "Trimite email cu notificare"
SEND_NOTIFICATION_EMAIL_HELP: "Notifică adminstratorul site-ului când un utilizator nou s-a înregistrat. Email-ul va di trimis către adresa `Către` din configurarea modului de Email"
SEND_WELCOME_EMAIL: "Trimite email de bun venit"
SEND_WELCOME_EMAIL_HELP: "Trimite un email către utilizatorul nou înregistrat."
DEFAULT_VALUES: " Valori implicite"
DEFAULT_VALUES_HELP: "Listă de câmpuri și valorile asociate acestora ce vor fi adăugate profilului utilizatorului (în fișierul yaml) în mod implicit fără a putea fi configurabile de către utilizator. Separați valorile multiple cu virgulă, fără spații între valori."
ADDITIONAL_PARAM_KEY: "Parametru"
ADDITIONAL_PARAM_VALUE: "Valoare"
REGISTRATION_FIELDS: "Câmpuri pentru înregistrare"
REGISTRATION_FIELDS_HELP: "Adaugă câmpurile ce vor fi adăugate fișierului yaml al utilizatorului. Câmpurile care nu sunt listate aici nu vor fi adăugate chiar dacă sunt prezente în formularul de înregistrare."
REGISTRATION_FIELD_KEY: " Numele câmpului"
REDIRECT_AFTER_LOGIN: "Redirecționează după logare"
REDIRECT_AFTER_LOGIN_HELP: "Ruta personalizată pentru redirecționare după logare"
REDIRECT_AFTER_REGISTRATION: "Redirecționează după înregistrare"
REDIRECT_AFTER_REGISTRATION_HELP: "Ruta personalizată pentru redirecționare după înregistrare"
OPTIONS: "Opțiuni"
EMAIL_VALIDATION_MESSAGE: "Trebuie să fie o adresă de email validă"
PASSWORD_VALIDATION_MESSAGE: "Parola trebuie să conțină cel puțin un număr și o literă de tipar și să aibă cel puțin 8 caractere"
TIMEOUT_HELP: "Setează pauza pentru sesiune când este activat 'Ține-mă minte' de către utilizator. Minimul este de 604800 care înseamnă 1 săptămână."
GROUPS_HELP: "Lista grupurilor din care utilizatorii nou înregistrați vor face parte, dacă este necesar"
SITE_ACCESS_HELP: "Lista cu niveluri de acces la care utilizatorul nou înregistrat are acces. De eg: `login` -> `true` "
WELCOME: "Bine ați venit"
REDIRECT_AFTER_ACTIVATION: "Redirecționează după activarea utilizatorului"
REDIRECT_AFTER_ACTIVATION_HELP: "Folosită dacă utilizatorul trebuie să-și activeze contul prin email. Odată activat contul va fi folosită această rută."
REGISTRATION_DISABLED: " Dezactivează înregistrarea "
USE_PARENT_ACL_LABEL: "Folosește regulile de acces ale părintelui"
USE_PARENT_ACL_HELP: "Verifică regulie de acces ale părintelui dacă nu sunt specificate alte reguli de acces"
PROTECT_PROTECTED_PAGE_MEDIA_LABEL: " Protejează media ce aparține paginii de logare "
PROTECT_PROTECTED_PAGE_MEDIA_HELP: "Dacă este activată, media ce aparține unei pagini de logare este protejată și nu poate fi accesată decât după logare."
+108
View File
@@ -0,0 +1,108 @@
PLUGIN_LOGIN:
USERNAME: "Логин"
EMAIL: "Email"
USERNAME_EMAIL: "Логин/Email"
PASSWORD: "Пароль"
ACCESS_DENIED: "Доступ закрыт..."
LOGIN_FAILED: "Ошибка входа..."
LOGIN_SUCCESSFUL: "Вы успешно вошли в систему."
BTN_LOGIN: "Войти"
BTN_LOGOUT: "Выйти"
BTN_FORGOT: "Забыл"
BTN_REGISTER: "Регистрация"
BTN_RESET: "Сброс пароля"
BTN_SEND_INSTRUCTIONS: "Отправить инструкции по сбросу"
RESET_LINK_EXPIRED: "Время ссылки для сброса истекло, повторите попытку"
RESET_PASSWORD_RESET: "Пароль был сброшен"
RESET_INVALID_LINK: "Неверная ссылка сброса, повторите попытку"
FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL: "Инструкции по сбросу пароля были отправлены по электронной почте"
FORGOT_FAILED_TO_EMAIL: "Не удалось отправить инструкции по электронной почте, повторите попытку позже"
FORGOT_CANNOT_RESET_EMAIL_NO_EMAIL: "Не удается сбросить пароль для %s, адресс электронной почты не установлен"
FORGOT_USERNAME_DOES_NOT_EXIST: "Пользователь с именем <b>%s</b> не существует"
FORGOT_EMAIL_NOT_CONFIGURED: "Невозможно сбросить пароль. Этот сайт не настроен для отправки писем"
FORGOT_EMAIL_SUBJECT: "%s Запрос на сброс пароля"
FORGOT_EMAIL_BODY: "<h1>Восстановление пароля</h1><p>Уважаемый %1$s,</p><p>Был сделан запрос для сброса пароля от <b>%4$s</b>.</p><p><br /><a href=\"%2$s\" class=\"btn-primary\">Нажмите, чтобы сбросить пароль</a><br /><br /></p><p>Или скопируйте следующий URL-адрес в адресную строку браузера:</p> <p class=\"word-break\"><a href=\"%2$s\">%2$s</a></p> <p><br />С уважением,<br /><br />%3$s</p>"
SESSION: "&ldquo;Запомнить меня&rdquo;-Сессия"
REMEMBER_ME: "Запомнить меня"
REMEMBER_ME_HELP: "Устанавливает постоянный файл cookie в вашем браузере, чтобы разрешить постоянную аутентификацию входа между сеансами."
REMEMBER_ME_STOLEN_COOKIE: "Кто-то еще использовал вашу регистрационную информацию для доступа к этой странице! Все сеансы были отключены. Войдите в свою учетную запись и проверьте свои данные."
BUILTIN_CSS: "Использовать встроенный CSS"
BUILTIN_CSS_HELP: "Использовать CSS, предоставленный плагином администратора."
ROUTE: "Путь страницы входа"
ROUTE_HELP: "Путь к пользовательской странице входа, которую предоставляет ваша тема"
ROUTE_REGISTER: "Путь регистрации"
ROUTE_REGISTER_HELP: "Путь к пользовательской странице регистрации. Заполните, если вы хотите использовать встроенную страницу регистрации. Оставьте его пустым, если у вас есть собственная регистрационная форма"
USERNAME_NOT_VALID: "Имя пользователя должно быть от 3 до 16 символов, включая строчные буквы, цифры, символы подчеркивания и дефисы. Прописные буквы, пробелы и специальные символы не допускаются"
USERNAME_NOT_AVAILABLE: "Имя пользователя %s уже существует, выберите другое имя пользователя"
PASSWORD_NOT_VALID: "Пароль должен содержать как минимум одно число, одну прописную и строчную букву, и быть не менее 8 символов"
PASSWORDS_DO_NOT_MATCH: "Пароли не совпадают. Дважды проверьте, что вы дважды ввели тот же пароль"
USER_NEEDS_EMAIL_FIELD: "Пользователю требуется поле электронной почты"
EMAIL_SENDING_FAILURE: "Произошла ошибка при отправке письма"
ACTIVATION_EMAIL_SUBJECT: "Активируйте свою учетную запись %s"
ACTIVATION_EMAIL_BODY: "Привет %s, перейдите <a href=\"%s\">сюда</a> для активации вашей учетной записи %s"
WELCOME_EMAIL_SUBJECT: "Добро пожаловать в %s"
WELCOME_EMAIL_BODY: "Привет %s, добро пожаловать в %s!"
NOTIFICATION_EMAIL_SUBJECT: "Новый пользователь %s"
NOTIFICATION_EMAIL_BODY: "Привет, новый пользователь, зарегистрированный на %s. Имя пользователя: %s, email: %s"
EMAIL_FOOTER: "GetGrav.org"
ACTIVATION_LINK_EXPIRED: "Время ссылки для активации истекло"
USER_ACTIVATED_SUCCESSFULLY: "Пользователь успешно активирован"
INVALID_REQUEST: "Неверный запрос"
USER_REGISTRATION: "Регистрация пользователя"
USER_REGISTRATION_ENABLED_HELP: "Включить регистрацию пользователя"
VALIDATE_PASSWORD1_AND_PASSWORD2: "Двойная проверка введенного пароля"
VALIDATE_PASSWORD1_AND_PASSWORD2_HELP: "Подтвердить и сравнить два разных поля для паролей с именами `password1` и` password2`. Включите это, если у вас есть два поля пароля в регистрационной форме"
SET_USER_DISABLED: "Установить пользователя как отключенный"
SET_USER_DISABLED_HELP: "Лучше всего использовать электронную почту «Отправить электронную почту активации». Добавляет пользователя в Grav, но устанавливает его как отключенный"
LOGIN_AFTER_REGISTRATION: "Воход в систему после регистрации"
LOGIN_AFTER_REGISTRATION_HELP: "Автоматический воход в систему после регистрации. Если требуется активация электронной почты, пользователь будет входить в систему сразу после активации учетной записи"
SEND_ACTIVATION_EMAIL: "Отправить письмо активации"
SEND_ACTIVATION_EMAIL_HELP: "Отправляет электронное письмо пользователю для активации своей учетной записи. Включите параметр «Установить пользователя как отключенный» при использовании этой функции, чтобы пользователь был отключен, и для активации учетной записи будет отправлено электронное письмо"
SEND_NOTIFICATION_EMAIL: "Отправить уведомление по электронной почте"
SEND_NOTIFICATION_EMAIL_HELP: "Сообщает администратору сайта о регистрации нового пользователя. Электронная почта будет отправлена в поле «Кому» в конфигурации плагина электронной почты"
SEND_WELCOME_EMAIL: "Отправить приветственное письмо"
SEND_WELCOME_EMAIL_HELP: "Отправляет электронное письмо вновь зарегистрированному пользователю"
DEFAULT_VALUES: "Значения по умолчанию"
DEFAULT_VALUES_HELP: "Список названий полей и связанных значений, которые будут добавлены в профиль пользователя (файл yaml) по умолчанию, без настройки пользователем. Разделите несколько значений запятой, без пробелов между значениями"
ADDITIONAL_PARAM_KEY: "Параметр"
ADDITIONAL_PARAM_VALUE: "Значение"
REGISTRATION_FIELDS: "Регистрационные поля"
REGISTRATION_FIELDS_HELP: "Добавьте поля, которые будут добавлены в файл yaml пользователя. Поля, не перечисленные здесь, не будут добавлены, даже если они присутствуют в регистрационной форме"
REGISTRATION_FIELD_KEY: "Имя поля"
REDIRECT_AFTER_LOGIN: "Перенаправление после входа в систему"
REDIRECT_AFTER_LOGIN_HELP: "Пользовательский маршрут для перенаправления после входа в систему"
REDIRECT_AFTER_REGISTRATION: "Перенаправление после регистрации"
REDIRECT_AFTER_REGISTRATION_HELP: "Пользовательский маршрут для перенаправления после регистрации"
OPTIONS: "Опции"
EMAIL_VALIDATION_MESSAGE: "Адрес эл. почты должен быть действительным"
PASSWORD_VALIDATION_MESSAGE: "Пароль должен содержать как минимум одно число, одну прописную и строчную букву и быть не менее 8 символов"
TIMEOUT_HELP: "Устанавливает тайм-аут сеанса в секундах, когда функция «Запомнить меня» включена и установлена пользователем. Минимум 604800, что означает 1 неделю"
GROUPS_HELP: "Список групп, в которые войдет новый зарегистрированный пользователь"
SITE_ACCESS_HELP: "Список уровней доступа к сайту, зарегистрированных пользователей. Пример: `login` ->` true`"
WELCOME: "Добро пожаловать"
REDIRECT_AFTER_ACTIVATION: "Перенаправление после активации пользователя"
REDIRECT_AFTER_ACTIVATION_HELP: "Используется, если пользователю требуется активировать учетную запись по электронной почте. После активации этот маршрут будет показан"
REGISTRATION_DISABLED: "Регистрация отключена"
USE_PARENT_ACL_LABEL: "Использовать родительские правила доступа"
USE_PARENT_ACL_HELP: "Проверьте правила доступа к родителям, если правила не определены"
PROTECT_PROTECTED_PAGE_MEDIA_LABEL: "Защита защищенных страниц."
PROTECT_PROTECTED_PAGE_MEDIA_HELP: "Если этот параметр включен, то доступ к защищенной странице для входа в систему также защищен паролем, и его нельзя увидеть, если он не зарегистрирован"
SECURITY_TAB: "Безопасность"
MAX_RESETS_COUNT: "Максимальное количество сброса пароля"
MAX_RESETS_COUNT_HELP: "Настройка защиты пароля от флуда (0 - не ограничено)"
MAX_RESETS_INTERVAL: "Максимальный интервал сброса пароля"
MAX_RESETS_INTERVAL_HELP: "Интервал времени для максимального количества сбросов пароля"
FORGOT_CANNOT_RESET_IT_IS_BLOCKED: "Невозможно сбросить пароль для %s, функция сброса пароля временно отключена, попробуйте позже (максимум %s минут)"
MAX_LOGINS_COUNT: "Максимальное количество входов"
MAX_LOGINS_COUNT_HELP: "Настройка защиты от флуда (0 - не ограничено)"
MAX_LOGINS_INTERVAL: "Максимальный интервал входа"
MAX_LOGINS_INTERVAL_HELP: "Временной интервал для значения счетчика входа"
TOO_MANY_LOGIN_ATTEMPTS: "Слишком много неудачных попыток входа в настроенное время (%s минут)"
SECONDS: "секунд"
RESETS: "сбросов"
ATTEMPTS: "попыток"
ROUTES: "Маршруты"
ROUTE_FORGOT: "Забыли пароль"
ROUTE_RESET: "Сброса пароля"
ROUTE_PROFILE: "Профиля пользователя"
ROUTE_ACTIVATE: "Активации пользователя"
+991
View File
@@ -0,0 +1,991 @@
<?php
/**
* @package Grav\Plugin\Login
*
* @copyright Copyright (C) 2014 - 2017 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Plugin;
use Grav\Common\Grav;
use Grav\Common\Language\Language;
use Grav\Common\Page\Page;
use Grav\Common\Page\Pages;
use Grav\Common\Plugin;
use Grav\Common\Twig\Twig;
use Grav\Common\User\User;
use Grav\Common\Utils;
use Grav\Common\Uri;
use Grav\Plugin\Login\Events\UserLoginEvent;
use Grav\Plugin\Login\Login;
use Grav\Plugin\Login\Controller;
use Grav\Plugin\Login\RememberMe\RememberMe;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\Session\Message;
/**
* Class LoginPlugin
* @package Grav\Plugin\Login
*/
class LoginPlugin extends Plugin
{
const TMP_COOKIE_NAME = 'tmp-message';
/** @var string */
protected $route;
/** @var string */
protected $route_register;
/** @var string */
protected $route_forgot;
/** @var bool */
protected $authenticated = true;
/** @var Login */
protected $login;
protected $redirect_to_login;
/**
* @return array
*/
public static function getSubscribedEvents()
{
return [
'onPluginsInitialized' => [['initializeSession', 10000], ['initializeLogin', 1000]],
'onTask.login.login' => ['loginController', 0],
'onTask.login.twofa' => ['loginController', 0],
'onTask.login.forgot' => ['loginController', 0],
'onTask.login.logout' => ['loginController', 0],
'onTask.login.reset' => ['loginController', 0],
'onPagesInitialized' => [['storeReferrerPage', 0], ['pageVisibility', 0]],
'onPageInitialized' => ['authorizePage', 0],
'onPageFallBackUrl' => ['authorizeFallBackUrl', 0],
'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0],
'onTwigSiteVariables' => ['onTwigSiteVariables', -100000],
'onFormProcessed' => ['onFormProcessed', 0],
'onUserLoginAuthenticate' => [['userLoginAuthenticateByRegistration', 10002], ['userLoginAuthenticateByRememberMe', 10001], ['userLoginAuthenticateByEmail', 10000], ['userLoginAuthenticate', 0]],
'onUserLoginAuthorize' => ['userLoginAuthorize', 0],
'onUserLoginFailure' => ['userLoginFailure', 0],
'onUserLogin' => ['userLogin', 0],
'onUserLogout' => ['userLogout', 0],
];
}
/**
* [onPluginsInitialized] Initialize login plugin if path matches.
* @throws \RuntimeException
*/
public function initializeSession()
{
// Check to ensure sessions are enabled.
if (!$this->config->get('system.session.enabled')) {
throw new \RuntimeException('The Login plugin requires "system.session" to be enabled');
}
// Autoload classes
$autoload = __DIR__ . '/vendor/autoload.php';
if (!is_file($autoload)) {
throw new \RuntimeException('Login Plugin failed to load. Composer dependencies not met.');
}
require_once $autoload;
// Define login service.
$this->grav['login'] = function (Grav $c) {
return new Login($c);
};
// Define current user service.
$this->grav['user'] = function (Grav $c) {
$session = $c['session'];
if (empty($session->user)) {
$session->user = $c['login']->login(['username' => ''], ['remember_me' => true, 'remember_me_login' => true]);
}
return $session->user;
};
}
/**
* [onPluginsInitialized] Initialize login plugin if path matches.
* @throws \RuntimeException
*/
public function initializeLogin()
{
$this->login = $this->grav['login'];
/** @var Uri $uri */
$uri = $this->grav['uri'];
// Admin has its own login; make sure we're not in admin.
if (!isset($this->grav['admin'])) {
$this->route = $this->config->get('plugins.login.route');
}
$path = $uri->path();
$this->redirect_to_login = $this->config->get('plugins.login.redirect_to_login');
// Register route to login page if it has been set.
if ($this->route && $this->route === $path) {
$this->enable([
'onPagesInitialized' => ['addLoginPage', 0],
]);
return;
}
if ($path === $this->config->get('plugins.login.route_forgot')) {
$this->enable([
'onPagesInitialized' => ['addForgotPage', 0],
]);
return;
}
if ($path === $this->config->get('plugins.login.route_reset')) {
$this->enable([
'onPagesInitialized' => ['addResetPage', 0],
]);
return;
}
if ($path === $this->config->get('plugins.login.route_register')) {
if ($this->config->get('plugins.login.user_registration.enabled')) {
$this->enable([
'onPagesInitialized' => ['addRegisterPage', 0],
]);
} else {
throw new \RuntimeException($this->grav['language']->translate('PLUGIN_LOGIN.REGISTRATION_DISABLED'), 404);
}
return;
}
if ($path === $this->config->get('plugins.login.route_activate')) {
$this->enable([
'onPagesInitialized' => ['handleUserActivation', 0],
]);
return;
}
if ($path === $this->config->get('plugins.login.route_profile')) {
$this->enable([
'onPagesInitialized' => ['addProfilePage', 0],
]);
return;
}
}
/**
* Optional ability to dynamically set visibility based on page access and page header
* that states `login.visibility_requires_access: true`
*
* Note that this setting may be slow on large sites as it loads all pages into memory for each page load!
*
* @param Event $e
*/
public function pageVisibility(Event $e)
{
if ($this->config->get('plugins.login.dynamic_page_visibility')) {
/** @var Pages $pages */
$pages = $e['pages'];
$user = $this->grav['user'];
foreach ($pages->instances() as $page) {
$header = $page->header();
if (isset($header->login['visibility_requires_access'])) {
$config = $this->mergeConfig($page);
$access = $this->login->isUserAuthorizedForPage($user, $page, $config);
if ($access === false) {
$page->visible(false);
}
}
}
}
}
/**
* [onPagesInitialized]
*/
public function storeReferrerPage()
{
$invalid_redirect_routes = [
$this->config->get('plugins.login.route') ?: '/login',
$this->config->get('plugins.login.route_register') ?: '/register',
$this->config->get('plugins.login.route_activate') ?: '/activate_user',
$this->config->get('plugins.login.route_forgot') ?: '/forgot_password',
$this->config->get('plugins.login.route_reset') ?: '/reset_password',
];
/** @var Uri $uri */
$uri = $this->grav['uri'];
$current_route = $uri->route();
$redirect = $this->grav['config']->get('plugins.login.redirect_after_login');
if (!$redirect && !in_array($current_route, $invalid_redirect_routes, true)) {
// No login redirect set in the configuration; can we redirect to the current page?
$allowed = true;
/** @var Page $page */
$page = $this->grav['pages']->dispatch($current_route);
if ($page) {
$header = $page->header();
if (isset($header->login_redirect_here) && $header->login_redirect_here === false) {
$allowed = false;
}
if ($allowed && $page->routable()) {
$redirect = $page->route() . ($uri->params() ?: '');
}
}
}
$this->grav['session']->redirect_after_login = $redirect;
}
/**
* Add Login page
* @throws \Exception
*/
public function addLoginPage()
{
/** @var Pages $pages */
$pages = $this->grav['pages'];
$page = $pages->dispatch($this->route);
if (!$page) {
// Only add login page if it hasn't already been defined.
$page = new Page;
$page->init(new \SplFileInfo(__DIR__ . '/pages/login.md'));
$page->slug(basename($this->route));
$pages->addPage($page, $this->route);
}
}
/**
* Add Login page
* @throws \Exception
*/
public function addForgotPage()
{
$route = $this->config->get('plugins.login.route_forgot');
/** @var Pages $pages */
$pages = $this->grav['pages'];
$page = $pages->dispatch($route);
if (!$page) {
// Only add forgot page if it hasn't already been defined.
$page = new Page;
$page->init(new \SplFileInfo(__DIR__ . '/pages/forgot.md'));
$page->slug(basename($route));
$pages->addPage($page, $route);
}
}
/**
* Add Reset page
* @throws \Exception
*/
public function addResetPage()
{
$route = $this->config->get('plugins.login.route_reset');
$uri = $this->grav['uri'];
$token = $uri->param('token');
$user = $uri->param('user');
if (!$user || !$token) {
return;
}
/** @var Pages $pages */
$pages = $this->grav['pages'];
$page = $pages->dispatch($route);
if (!$page) {
// Only add login page if it hasn't already been defined.
$page = new Page;
$page->init(new \SplFileInfo(__DIR__ . '/pages/reset.md'));
$page->slug(basename($route));
$pages->addPage($page, $route);
}
}
/**
* Add Register page
* @throws \Exception
*/
public function addRegisterPage()
{
$route = $this->config->get('plugins.login.route_register');
/** @var Pages $pages */
$pages = $this->grav['pages'];
$page = $pages->dispatch($route);
if (!$page) {
$page = new Page;
$page->init(new \SplFileInfo(__DIR__ . '/pages/register.md'));
$page->template('form');
$page->slug(basename($route));
$pages->addPage($page, $route);
}
}
/**
* Handle user activation
* @throws \RuntimeException
*/
public function handleUserActivation()
{
/** @var Uri $uri */
$uri = $this->grav['uri'];
/** @var Message $messages */
$messages = $this->grav['messages'];
$username = $uri->param('username');
$token = $uri->param('token');
$user = User::load($username);
if (empty($user->activation_token)) {
$message = $this->grav['language']->translate('PLUGIN_LOGIN.INVALID_REQUEST');
$messages->add($message, 'error');
} else {
list($good_token, $expire) = explode('::', $user->activation_token, 2);
if ($good_token === $token) {
if (time() > $expire) {
$message = $this->grav['language']->translate('PLUGIN_LOGIN.ACTIVATION_LINK_EXPIRED');
$messages->add($message, 'error');
} else {
$user['state'] = 'enabled';
unset($user['activation_token']);
$user->save();
$message = $this->grav['language']->translate('PLUGIN_LOGIN.USER_ACTIVATED_SUCCESSFULLY');
$messages->add($message, 'info');
if ($this->config->get('plugins.login.user_registration.options.send_welcome_email', false)) {
$this->login->sendWelcomeEmail($user);
}
if ($this->config->get('plugins.login.user_registration.options.send_notification_email', false)) {
$this->login->sendNotificationEmail($user);
}
if ($this->config->get('plugins.login.user_registration.options.login_after_registration', false)) {
$this->login->login(['username' => $username], ['after_registration' => true]);
}
}
} else {
$message = $this->grav['language']->translate('PLUGIN_LOGIN.INVALID_REQUEST');
$messages->add($message, 'error');
}
}
$redirect_route = $this->config->get('plugins.login.user_registration.redirect_after_activation', '/');
$this->grav->redirect($redirect_route);
}
/**
* Add Profile page
*/
public function addProfilePage()
{
$route = $this->config->get('plugins.login.route_profile');
/** @var Pages $pages */
$pages = $this->grav['pages'];
$page = $pages->dispatch($route);
if (!$page) {
// Only add forgot page if it hasn't already been defined.
$page = new Page;
$page->init(new \SplFileInfo(__DIR__ . "/pages/profile.md"));
$page->slug(basename($route));
$pages->addPage($page, $route);
}
$this->storeReferrerPage();
}
/**
* Set Unauthorized page
* @throws \Exception
*/
public function setUnauthorizedPage()
{
$route = $this->config->get('plugins.login.route_unauthorized');
/** @var Pages $pages */
$pages = $this->grav['pages'];
$page = $pages->dispatch($route);
if (!$page) {
$page = new Page;
$page->init(new \SplFileInfo(__DIR__ . '/pages/unauthorized.md'));
$page->template('default');
$page->slug(basename($route));
$pages->addPage($page, $route);
}
unset($this->grav['page']);
$this->grav['page'] = $page;
}
/**
* Initialize login controller
*/
public function loginController()
{
/** @var Uri $uri */
$uri = $this->grav['uri'];
$task = !empty($_POST['task']) ? $_POST['task'] : $uri->param('task');
$task = substr($task, strlen('login.'));
$post = !empty($_POST) ? $_POST : [];
switch ($task) {
case 'login':
if (!isset($post['login-form-nonce']) || !Utils::verifyNonce($post['login-form-nonce'], 'login-form')) {
$this->grav['messages']->add($this->grav['language']->translate('PLUGIN_LOGIN.ACCESS_DENIED'),
'info');
$twig = $this->grav['twig'];
$twig->twig_vars['notAuthorized'] = true;
return;
}
break;
case 'forgot':
if (!isset($post['forgot-form-nonce']) || !Utils::verifyNonce($post['forgot-form-nonce'], 'forgot-form')) {
$this->grav['messages']->add($this->grav['language']->translate('PLUGIN_LOGIN.ACCESS_DENIED'),'info');
return;
}
break;
}
$controller = new Controller($this->grav, $task, $post);
$controller->execute();
$controller->redirect();
}
/**
* Authorize the Page fallback url (page media accessed through the page route)
*/
public function authorizeFallBackUrl()
{
if ($this->config->get('plugins.login.protect_protected_page_media', false)) {
$page_url = dirname($this->grav['uri']->path());
$page = $this->grav['pages']->find($page_url);
unset($this->grav['page']);
$this->grav['page'] = $page;
$this->authorizePage();
}
}
/**
* [onPageInitialized] Authorize Page
*/
public function authorizePage()
{
if (!$this->authenticated) {
return;
}
/** @var User $user */
$user = $this->grav['user'];
/** @var Page $page */
$page = $this->grav['page'];
if (!$page || $this->grav['login']->isUserAuthorizedForPage($user, $page, $this->mergeConfig($page))) {
return;
}
// If this is not an HTML page request, simply throw a 403 error
$uri_extension = $this->grav['uri']->extension('html');
$supported_types = $this->config->get('media.types');
if ($uri_extension !== 'html' && array_key_exists($uri_extension, $supported_types)) {
header('HTTP/1.0 403 Forbidden');
exit;
}
$authorized = $user->authenticated && $user->authorized;
// User is not logged in; redirect to login page.
if ($this->redirect_to_login && $this->route && !$authorized) {
$this->grav->redirect($this->route, 302);
}
/** @var Twig $twig */
$twig = $this->grav['twig'];
// Reset page with login page.
if (!$authorized) {
if ($this->route) {
$page = $this->grav['pages']->dispatch($this->route);
} else {
$page = new Page;
// $this->grav['session']->redirect_after_login = $this->grav['uri']->path() . ($this->grav['uri']->params() ?: '');
// Get the admin Login page is needed, else teh default
if ($this->isAdmin()) {
$login_file = $this->grav['locator']->findResource('plugins://admin/pages/admin/login.md');
$page->init(new \SplFileInfo($login_file));
} else {
$page->init(new \SplFileInfo(__DIR__ . '/pages/login.md'));
}
$page->slug(basename($this->route));
/** @var Pages $pages */
$pages = $this->grav['pages'];
$pages->addPage($page, $this->route);
}
$this->authenticated = false;
unset($this->grav['page']);
$this->grav['page'] = $page;
$twig->twig_vars['form'] = new Form($page);
} else {
/** @var Language $l */
$l = $this->grav['language'];
$this->grav['messages']->add($l->translate('PLUGIN_LOGIN.ACCESS_DENIED'), 'error');
$twig->twig_vars['notAuthorized'] = true;
$this->setUnauthorizedPage();
}
}
/**
* [onTwigTemplatePaths] Add twig paths to plugin templates.
*/
public function onTwigTemplatePaths()
{
$twig = $this->grav['twig'];
$twig->twig_paths[] = __DIR__ . '/templates';
}
/**
* [onTwigSiteVariables] Set all twig variables for generating output.
*/
public function onTwigSiteVariables()
{
/** @var Twig $twig */
$twig = $this->grav['twig'];
$this->grav->fireEvent('onLoginPage');
$extension = $this->grav['uri']->extension();
$extension = $extension ?: 'html';
if (!$this->authenticated) {
$twig->template = "login.{$extension}.twig";
}
// add CSS for frontend if required
if (!$this->isAdmin() && $this->config->get('plugins.login.built_in_css')) {
$this->grav['assets']->add('plugin://login/css/login.css');
}
$task = $this->grav['uri']->param('task') ?: (isset($_POST['task']) ? $_POST['task'] : '');
$task = substr($task, strlen('login.'));
if ($task === 'reset') {
$username = $this->grav['uri']->param('user');
$token = $this->grav['uri']->param('token');
if (!empty($username) && !empty($token)) {
$twig->twig_vars['username'] = $username;
$twig->twig_vars['token'] = $token;
}
} elseif ($task === 'login') {
$twig->twig_vars['username'] = isset($_POST['username']) ? $_POST['username'] : '';
}
$flashData = $this->grav['session']->getFlashCookieObject(self::TMP_COOKIE_NAME);
if (isset($flashData->message)) {
$this->grav['messages']->add($flashData->message, $flashData->status);
}
}
/**
* Process the user registration, triggered by a registration form
*
* @param Form $form
* @throws \RuntimeException
*/
private function processUserRegistration($form, Event $event)
{
$language = $this->grav['language'];
$messages = $this->grav['messages'];
if (!$this->config->get('plugins.login.enabled')) {
throw new \RuntimeException($language->translate('PLUGIN_LOGIN.PLUGIN_LOGIN_DISABLED'));
}
if (!$this->config->get('plugins.login.user_registration.enabled')) {
throw new \RuntimeException($language->translate('PLUGIN_LOGIN.USER_REGISTRATION_DISABLED'));
}
// Check for existing username
$username = $form->value('username');
$existing_username = User::find($username,['username']);
if ($existing_username->exists()) {
$this->grav->fireEvent('onFormValidationError', new Event([
'form' => $form,
'message' => $language->translate([
'PLUGIN_LOGIN.USERNAME_NOT_AVAILABLE',
$username
])
]));
$event->stopPropagation();
return;
}
// Check for existing email
$email = $form->value('email');
$existing_email = User::find($email,['email']);
if ($existing_email->exists()) {
$this->grav->fireEvent('onFormValidationError', new Event([
'form' => $form,
'message' => $language->translate([
'PLUGIN_LOGIN.EMAIL_NOT_AVAILABLE',
$email
])
]));
$event->stopPropagation();
return;
}
$data = [];
$data['username'] = $username;
// if multiple password fields, check they match and set password field from it
if ($this->config->get('plugins.login.user_registration.options.validate_password1_and_password2',
false)
) {
if ($form->value('password1') !== $form->value('password2')) {
$this->grav->fireEvent('onFormValidationError', new Event([
'form' => $form,
'message' => $language->translate('PLUGIN_LOGIN.PASSWORDS_DO_NOT_MATCH')
]));
$event->stopPropagation();
return;
}
$data['password'] = $form->value('password1');
}
$fields = (array)$this->config->get('plugins.login.user_registration.fields', []);
foreach ($fields as $field) {
// Process value of field if set in the page process.register_user
$default_values = (array)$this->config->get('plugins.login.user_registration.default_values');
if ($default_values) {
foreach ($default_values as $key => $param) {
if ($key === $field) {
if (is_array($param)) {
$values = explode(',', $param);
} else {
$values = $param;
}
$data[$field] = $values;
}
}
}
if (!isset($data[$field]) && $form->value($field)) {
$data[$field] = $form->value($field);
}
}
if ($this->config->get('plugins.login.user_registration.options.set_user_disabled', false)) {
$data['state'] = 'disabled';
} else {
$data['state'] = 'enabled';
}
$this->grav->fireEvent('onUserLoginRegisterData', new Event(['data' => &$data]));
$user = $this->login->register($data);
$fullname = $user->fullname ?: $user->username;
if ($this->config->get('plugins.login.user_registration.options.send_activation_email', false)) {
$this->login->sendActivationEmail($user);
$message = $language->translate(['PLUGIN_LOGIN.ACTIVATION_NOTICE_MSG', $fullname]);
$messages->add($message, 'info');
} else {
if ($this->config->get('plugins.login.user_registration.options.send_welcome_email', false)) {
$this->login->sendWelcomeEmail($user);
}
if ($this->config->get('plugins.login.user_registration.options.send_notification_email', false)) {
$this->login->sendNotificationEmail($user);
}
$message = $language->translate(['PLUGIN_LOGIN.WELCOME_NOTICE_MSG', $fullname]);
$messages->add($message, 'info');
}
$this->grav->fireEvent('onUserLoginRegistered', new Event(['user' => $user]));
if (isset($data['state']) && $data['state'] === 'enabled' && $this->config->get('plugins.login.user_registration.options.login_after_registration', false)) {
$this->login->login(['username' => $username], ['after_registration' => true], ['user' => $user]);
}
$redirect = $this->config->get('plugins.login.user_registration.redirect_after_registration', false);
if ($redirect) {
$this->grav->redirect($redirect);
}
}
/**
* Save user profile information
*
* @param Form $form
* @param Event $event
* @return bool
*/
private function processUserProfile($form, Event $event)
{
/** @var User $user */
$user = $this->grav['user'];
$language = $this->grav['language'];
// Don't save if user doesn't exist
if (!$user->exists()) {
$this->grav->fireEvent('onFormValidationError', new Event([
'form' => $form,
'message' => $language->translate('PLUGIN_LOGIN.USER_IS_REMOTE_ONLY')
]));
$event->stopPropagation();
return;
}
// Stop overloading of username
$username = $form->value('username');
if (isset($username)) {
$this->grav->fireEvent('onFormValidationError', new Event([
'form' => $form,
'message' => $language->translate([
'PLUGIN_LOGIN.USERNAME_NOT_AVAILABLE',
$username
])
]));
$event->stopPropagation();
return;
}
// Check for existing email
$email = $form->value('email');
$existing_email = User::find($email,['email']);
if ($user->username != $existing_email->username && $existing_email->exists()) {
$this->grav->fireEvent('onFormValidationError', new Event([
'form' => $form,
'message' => $language->translate([
'PLUGIN_LOGIN.EMAIL_NOT_AVAILABLE',
$email
])
]));
$event->stopPropagation();
return;
}
$user->merge($form->getData()->toArray());
try {
$user->save();
} catch (\Exception $e) {
$form->setMessage($e->getMessage(), 'error');
return false;
}
return true;
}
/**
* [onFormProcessed] Process a registration form. Handles the following actions:
*
* - register_user: registers a user
* - update_user: updates user profile
*
* @param Event $event
* @throws \RuntimeException
*/
public function onFormProcessed(Event $event)
{
$form = $event['form'];
$action = $event['action'];
switch ($action) {
case 'register_user':
$this->processUserRegistration($form, $event);
break;
case 'update_user':
$this->processUserProfile($form, $event);
break;
}
}
/**
* @param UserLoginEvent $event
* @throws \RuntimeException
*/
public function userLoginAuthenticateByRegistration(UserLoginEvent $event)
{
// Check that we're logging in after registration.
if (!$event->getOption('after_registration') || $this->isAdmin()) {
return;
}
$event->setStatus($event::AUTHENTICATION_SUCCESS);
$event->stopPropagation();
}
/**
* @param UserLoginEvent $event
* @throws \RuntimeException
*/
public function userLoginAuthenticateByRememberMe(UserLoginEvent $event)
{
// Check that we're logging in with remember me.
if (!$event->getOption('remember_me_login') || !$event->getOption('remember_me') || $this->isAdmin()) {
return;
}
// Only use remember me if user isn't set and feature is enabled.
if ($this->grav['config']->get('plugins.login.rememberme.enabled') && !$event->getUser()->exists()) {
/** @var RememberMe $rememberMe */
$rememberMe = $this->grav['login']->rememberMe();
$username = $rememberMe->login();
if ($rememberMe->loginTokenWasInvalid()) {
// Token was invalid. We will display error page as this was likely an attack.
throw new \RuntimeException($this->grav['language']->translate('PLUGIN_LOGIN.REMEMBER_ME_STOLEN_COOKIE'), 403);
}
if ($username === false) {
// User has not been remembered, there is no point of continuing.
$event->setStatus($event::AUTHENTICATION_FAILURE);
$event->stopPropagation();
return;
}
// Allow remember me to work with different login methods.
$event->setCredential('username', $username);
$event->setUser(User::load($username, false));
}
}
public function userLoginAuthenticateByEmail(UserLoginEvent $event)
{
if (($username = $event->getCredential('username')) && !$event->getUser()->exists()) {
$event->setUser(User::find($username));
}
}
public function userLoginAuthenticate(UserLoginEvent $event)
{
$user = $event->getUser();
$credentials = $event->getCredentials();
if (!$user->exists()) {
// Never let non-existing users to pass the authentication.
// Higher level plugins may override this behavior by stopping propagation.
$event->setStatus($event::AUTHENTICATION_FAILURE);
$event->stopPropagation();
return;
}
// Never let empty password to pass the authentication.
// Higher level plugins may override this behavior by stopping propagation.
if (empty($credentials['password'])) {
$event->setStatus($event::AUTHENTICATION_FAILURE);
$event->stopPropagation();
return;
}
// Try default user authentication. Stop propagation if authentication succeeds.
if ($user->authenticate($credentials['password'])) {
$event->setStatus($event::AUTHENTICATION_SUCCESS);
$event->stopPropagation();
return;
}
// If authentication status is undefined, lower level event handlers may still be able to authenticate user.
}
public function userLoginAuthorize(UserLoginEvent $event)
{
// Always block access if authorize defaulting to site.login fails.
$user = $event->getUser();
foreach ($event->getAuthorize() as $authorize) {
if (!$user->authorize($authorize)) {
$event->setStatus($event::AUTHORIZATION_DENIED);
$event->stopPropagation();
return;
}
}
if ($event->getOption('twofa') && $user->twofa_enabled && $user->twofa_secret) {
$event->setStatus($event::AUTHORIZATION_DELAYED);
}
}
public function userLoginFailure(UserLoginEvent $event)
{
$this->grav['session']->user = User::load('', false);
}
public function userLogin(UserLoginEvent $event)
{
$session = $this->grav['session'];
$session->user = $event->getUser();
if ($event->getOption('remember_me')) {
/** @var Login $login */
$login = $this->grav['login'];
$session->remember_me = (bool)$event->getOption('remember_me_login');
// If the user wants to be remembered, create Rememberme cookie.
$username = $event->getUser()->get('username');
if ($event->getCredential('rememberme')) {
$login->rememberMe()->createCookie($username);
} else {
$login->rememberMe()->getStorage()->cleanAllTriplets($username);
$login->rememberMe()->clearCookie();
}
}
}
public function userLogout(UserLoginEvent $event)
{
if ($event->getOption('remember_me')) {
/** @var Login $login */
$login = $this->grav['login'];
if (!$login->rememberMe()->login()) {
$login->rememberMe()->getStorage()->cleanAllTriplets($event->getUser()->get('username'));
}
$login->rememberMe()->clearCookie();
}
$this->grav['session']->invalidate()->start();
}
}
+54
View File
@@ -0,0 +1,54 @@
enabled: true # Enable the plugin
built_in_css: true # Use built-in CSS
route: # Specific route for Login page (default is '/login')
redirect_to_login: true # If you try to access a page you don't have access to, should you redirect to login route
redirect_after_login: # Path to redirect to after a successful login (eg '/user_profile')
redirect_after_logout: '/' # Path to redirect to after a successful logout (eg '/')
route_activate: '/activate_user' # Route for the user activation process
route_forgot: '/forgot_password' # Route for the forgot password process
route_reset: '/reset_password' # Route for the reset password process
route_profile: '/user_profile' # Route for the user profile page
route_register: '/user_register' # Route for the user registration page
route_unauthorized: '/user_unauthorized' # Route for a page to display if user is unauthorized
dynamic_page_visibility: false # Integrate access into page visibility so things can be shown or hidden in the menu
parent_acl: false # Look to parent `access` rules for access requirements
protect_protected_page_media: false # Take `access` rules into account when directly accessing a page's media
rememberme:
enabled: true # Enable 'remember me' functionality
timeout: 604800 # Timeout in seconds. Defaults to 1 week
name: grav-rememberme # Name prefix of the session cookie
max_pw_resets_count: 0 # Number of password resets in a specific time frame (0 = unlimited)
max_pw_resets_interval: 60 # Time in minutes to track password resets
max_login_count: 0 # Number of failed login attempts in a specific time frame (0 = unlimited)
max_login_interval: 2 # Time in minutes to track login attempts
user_registration:
enabled: false # Enable User Registration Process
fields: # List of fields to validate and store during user registration
- 'username' # This should match up with your registration form definition
- 'password'
- 'email'
- 'fullname'
- 'title'
- 'level'
default_values: # Any default values for fields you would like to set
level: Newbie # Here the 'level' field will be pre-populated with 'Newbie' text
access: # Default access to set for users created during registration
site:
login: 'true'
redirect_after_registration: '' # Route to redirect to after registration
options:
validate_password1_and_password2: true # Ensure that password1 and password2 match during registration (allows you to have just 1 pw field or 2)
set_user_disabled: false # Set this `true` if you want a user to activate their account via email
login_after_registration: false # Automatically login after registration
send_activation_email: false # Send an email that requires a special link to be clicked in order to activate the account
send_notification_email: false # Send an email to the site administrator to indicate a user has registered
send_welcome_email: false # Send a welcome email to the user (probably should not be used with `send_activation_email`
+21
View File
@@ -0,0 +1,21 @@
---
title: Forgot password
login_redirect_here: false
form:
fields:
- name: email
type: email
label: PLUGIN_LOGIN.EMAIL
autofocus: true
validate:
required: true
type: email
---
# Recover your password
Enter your email to recover your password
+42
View File
@@ -0,0 +1,42 @@
---
title: Login
login_redirect_here: false
forms:
login:
action:
method: post
fields:
username:
type: text
id: username
placeholder: Username
label: PLUGIN_LOGIN.USERNAME_EMAIL
autofocus: true
password:
type: password
id: password
placeholder: Password
label: PLUGIN_LOGIN.PASSWORD
login-twofa:
action:
method: post
fields:
2fa_instructions:
type: display
markdown: true
content: PLUGIN_LOGIN.2FA_INSTRUCTIONS
2fa_code:
type: text
id: twofa-code
autofocus: true
placeholder: PLUGIN_LOGIN.2FA_CODE_INPUT
---
# User Login
+50
View File
@@ -0,0 +1,50 @@
---
title: Profile
access:
site.login: true
form:
fields:
avatar_img:
type: avatar
username:
type: text
readonly: true
disabled: true
email:
type: email
placeholder: "Enter your email"
validate:
required: true
message: PLUGIN_LOGIN.EMAIL_VALIDATION_MESSAGE
fullname:
type: text
title:
type: text
password:
type: password
label: Enter new password
validate:
message: PLUGIN_LOGIN.PASSWORD_VALIDATION_MESSAGE
config-pattern@: system.pwd_regex
buttons:
-
type: submit
value: Submit
-
type: reset
value: Reset
process:
update_user: true
message: "Your profile has been updated"
---
# Profile
+58
View File
@@ -0,0 +1,58 @@
---
login_redirect_here: false
form:
fields:
fullname:
type: text
validate:
required: true
username:
type: text
validate:
required: true
message: PLUGIN_LOGIN.USERNAME_NOT_VALID
config-pattern@: system.username_regex
email:
type: email
validate:
required: true
message: PLUGIN_LOGIN.EMAIL_VALIDATION_MESSAGE
password1:
type: password
label: Enter a password
validate:
required: true
message: PLUGIN_LOGIN.PASSWORD_VALIDATION_MESSAGE
config-pattern@: system.pwd_regex
password2:
type: password
label: Enter the password again
validate:
required: true
message: PLUGIN_LOGIN.PASSWORD_VALIDATION_MESSAGE
config-pattern@: system.pwd_regex
buttons:
-
type: submit
value: Submit
-
type: reset
value: Reset
process:
register_user: true
message: "Thanks for registering..."
reset: true
---
# Register
Create a new user account by entering all the required fields below:
+35
View File
@@ -0,0 +1,35 @@
---
title: Reset password
login_redirect_here: false
form:
fields:
- name: username
type: hidden
id: username
placeholder: Username
readonly: true
- name: password
type: password
id: password
placeholder: Password
autofocus: true
validate:
required: true
message: PLUGIN_LOGIN.PASSWORD_VALIDATION_MESSAGE
config-pattern@: system.pwd_regex
- name: token
type: hidden
process:
twig: true
---
# Password Reset
### Username: {{uri.param('user')}}
+5
View File
@@ -0,0 +1,5 @@
---
title: Unauthorized
---
# You don't have access to this page...
@@ -0,0 +1,6 @@
{% extends 'partials/base.html.twig' %}
{% block content %}
{% include 'partials/messages.html.twig' %}
{% include 'partials/forgot-form.html.twig' %}
{% endblock %}
@@ -0,0 +1,28 @@
{% extends "forms/field.html.twig" %}
{% block input %}
<div class="form-input-wrapper twofa-wrapper">
{% try %}
{% set user = grav.user %}
{% set secret = user.twofa_secret ?: grav.login.twoFactorAuth.createSecret() %}
{% set image = grav.login.twoFactorAuth.getQrImageData(user.username, secret) %}
<img style="border: 1px solid #ddd" data-2fa-image src="{{ image }}" />
<div>
<span>{{ 'PLUGIN_LOGIN.2FA_SECRET'|tu }}: </span><span class="twofa-secret-code" data-2fa-secret>{{ secret|regex_replace('/(\\w{4})/', '\\1 ') }}</span>
</div>
{% if admin %}
<div class="danger twofa-wrapper">
<button data-hint="{{ 'PLUGIN_LOGIN.2FA_REGEN_HINT'|tu }}" class="button button-small hint--bottom" data-2fa-regenerate><i class="fa fa-fw fa-refresh"></i> {{ 'PLUGIN_LOGIN.2FA_REGENERATE'|t }}</button>
</div>
{% endif %}
<input type="text" class="no-form" style="display:none;" name="{{ (scope ~ field.name)|fieldName }}" data-2fa-value value="{{ secret }}" />
{% catch %}
<div class="notice error">
<p>{{ e.message }}</p>
</div>
{% endcatch %}
</div>
{% endblock %}
@@ -0,0 +1,6 @@
{% extends 'partials/base.html.twig' %}
{% block content %}
{% include 'partials/messages.html.twig' %}
{% include 'partials/login-form.html.twig' %}
{% endblock %}
@@ -0,0 +1,5 @@
{%- if not grav.user.authenticated -%}
{"code":401,"status":"unauthenticated","error":{"message":"Authentication required","login":{{ include('partials/login-form.html.twig')|trim|json_encode }}}}
{%- else -%}
{"code":200,"status":"authenticated","message":"You have been authenticated"}
{%- endif -%}
@@ -0,0 +1,18 @@
<section id="grav-login">
{{ content|raw }}
<form method="post" action="{{ (base_url_relative ~ uri.path)|e }}">
{% for field in form.fields %}
{% if field.type %}
<div>
{% include ["forms/fields/#{field.type}/#{field.type}.html.twig", 'forms/fields/text/text.html.twig'] %}
</div>
{% endif %}
{% endfor %}
<div class="form-actions secondary-accent">
<button type="submit" class="button primary" name="task" value="login.forgot"><i class="fa fa-paper-plane"></i> {{ "PLUGIN_LOGIN.BTN_SEND_INSTRUCTIONS"|t }}</button>
</div>
{{ nonce_field('forgot-form', 'forgot-form-nonce')|raw }}
</form>
</section>
+61
View File
@@ -0,0 +1,61 @@
<section id="grav-login">
{% if page.template == 'login' or show_login_form %}
{% set user = grav.user %}
{% if user.authenticated and user.authorized %}
<h4>{{ 'PLUGIN_LOGIN.WELCOME'|t }} <strong>{{ user.fullname ?: user.username }}</strong></h4>
<hr>
<a class="button logout" href="{{ uri.addNonce(base_url_relative ~ uri.path ~ '/task' ~ config.system.param_sep ~ 'login.logout', 'logout-form', 'logout-nonce')|e }}">{{ 'PLUGIN_LOGIN.BTN_LOGOUT'|t }}</a>
{% elseif user.authenticated and user.twofa_enabled %}
{% include 'partials/login-twofa.html.twig' %}
{% else %}
{{ content|raw }}
{# NEW WAY OF INCLUDING 3RD PARTY LOGIN OPTIONS #}
{% for template in grav.login.getProviderLoginTemplates %}
{% include template %}
{% endfor %}
<form method="post" action="{{ (base_url_relative ~ uri.path)|e }}" class="{{ form_outer_classes }}">
{# OLD WAY OF INCLUDING 3RD PARTY LOGIN OPTIONS #}
{% if grav.twig.plugins_hooked_loginPage %}
{% for label in grav.twig.plugins_hooked_loginPage %}
{% include label %}
{% endfor %}
{% endif %}
{% for field in form.fields %}
{% set value = field.name == 'username' ? username : '' %}
{% if field.type %}
{% include ["forms/fields/#{field.type}/#{field.type}.html.twig", 'forms/fields/text/text.html.twig'] %}
{% endif %}
{% endfor %}
<div class="{{ form_button_outer_classes ?: 'form-actions secondary-accent' }}">
{% if config.plugins.login.rememberme.enabled and page.header.form.login.rememberme ?? true %}
<div class="form-data rememberme" data-grav-default="null" data-grav-disabled="true" data-grav-field="checkbox">
<div class="form-input-wrapper">
<input type="checkbox" value="1" name="rememberme" id="#rememberme">
<label class="inline" for="#rememberme" title="{{ 'PLUGIN_LOGIN.REMEMBER_ME_HELP'|t }}">{{ 'PLUGIN_LOGIN.REMEMBER_ME'|t }}</label>
</div>
</div>
{% endif %}
{% if page.header.form.login.forgot_button ?? true %}
<a class="{{ form_button_classes ?: 'button secondary' }}" href="{{ base_url_relative }}{{ config.plugins.login.route_forgot }}"><i class="fa fa-exclamation-circle"></i> {{ 'PLUGIN_LOGIN.BTN_FORGOT'|t }}</a>
{% endif %}
<button class="{{ form_button_classes ?: 'button primary' }}" type="submit" name="task" value="login.login"><i class="fa fa-sign-in"></i> {{ 'PLUGIN_LOGIN.BTN_LOGIN'|t }}</button>
</div>
{{ nonce_field('login-form', 'login-form-nonce')|raw }}
</form>
{% endif %}
{% endif %}
</section>
@@ -0,0 +1,5 @@
<span class="login-status">
{% if grav.user.authenticated %}
<strong>{{ grav.user.fullname ?: grav.user.username }}</strong>, <a class="logout" href="{{ uri.addNonce((base_url_relative ~ uri.path)|trim('/') ~ '/task' ~ config.system.param_sep ~ 'login.logout', 'logout-form', 'logout-nonce')|e }}">{{ 'PLUGIN_LOGIN.BTN_LOGOUT'|t }}</a>
{% endif %}
</span>
@@ -0,0 +1,20 @@
{% set form = forms('login-twofa') %}
<form method="post" action="{{ (base_url_relative ~ uri.path)|e }}" class="{{ form_outer_classes }}">
{% for field in form.fields %}
{% if field.type %}
<div>
{% include ["forms/fields/#{field.type}/#{field.type}.html.twig", 'forms/fields/text/text.html.twig'] %}
</div>
{% endif %}
{% endfor %}
{{ nonce_field('login-form', 'login-form-nonce')|raw }}
<div class="{{ form_button_outer_classes ?: 'form-actions secondary-accent' }}">
<div class="form-actions primary-accent">
<button type="submit" class="button" name="task" value="login.logout">{{ 'PLUGIN_LOGIN.BTN_CANCEL'|t }}</button>
<button type="submit" class="button primary" name="task" value="login.twofa">{{ 'PLUGIN_LOGIN.BTN_LOGIN'|t }}</button>
</div>
</div>
</form>
@@ -0,0 +1,24 @@
{% if uri.param('token') and uri.param('task') %}
<section id="grav-login">
{{ content|raw }}
<form method="post" action="{{ (base_url_relative ~ uri.path)|e }}" classes="{{ form_outer_classes }}">
{% for field in form.fields %}
{% set value = attribute(grav.twig.twig_vars, field.name) is defined ? attribute(grav.twig.twig_vars, field.name) : null %}
{% if field.type %}
<div>
{% include ["forms/fields/#{field.type}/#{field.type}.html.twig", 'forms/fields/text/text.html.twig'] %}
</div>
{% endif %}
{% endfor %}
<div class="form-actions secondary-accent">
<button type="submit" class="button primary" name="task" value="login.reset"><i class="fa fa-key"></i> {{ "PLUGIN_LOGIN.BTN_RESET"|t }}</button>
</div>
{{ nonce_field('reset-form', 'reset-form-nonce')|raw }}
</form>
</section>
{% endif %}
@@ -0,0 +1,9 @@
{% extends 'partials/base.html.twig' %}
{% do form.setAllData(grav.user.toArray) %}
{% block content %}
{% include 'partials/messages.html.twig' %}
{{ page.content }}
{% include 'forms/form.html.twig' %}
{% endblock %}
@@ -0,0 +1 @@
{% extends 'forms/ajax.json.twig' %}
@@ -0,0 +1,8 @@
{% extends 'partials/base.html.twig' %}
{% block content %}
{% include 'partials/messages.html.twig' %}
{% include 'partials/reset-form.html.twig' %}
{% endblock %}
+7
View File
@@ -0,0 +1,7 @@
<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderIniteed5e5cf0aa1e2139f2db7445511e366::getLoader();
@@ -0,0 +1,37 @@
# Changelog
All notable changes to this project will be documented in this file, in reverse chronological order by release.
## 2.0.0 - 2018-04-25
### Added
- [#25](https://github.com/Bacon/BaconQrCode/pull/25) allows for setting a more compact text output
- CHANGELOG.md added (how meta)
- Allows more complex shapes for modules
- Allows setting a gradient for the foreground
- Allows transparent backgrounds and alpha channel on all colors
### Changed
- Minimum PHP version changed to 7.1
- Imagick renderer now allows setting different output formats
- New optimized SVG renderer
### Deprecated
- Nothing.
### Removed
- Legacy ZF module support removed
### Fixed
- Non-release files are excluded from composer packages
+22
View File
@@ -0,0 +1,22 @@
Copyright (c) 2013, Ben 'DASPRiD' Scholzen
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,37 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode;
use Zend\ModuleManager\Feature\AutoloaderProviderInterface;
/**
* Module for generating QR codes.
*/
class Module implements AutoloaderProviderInterface
{
/**
* Get autoloader config.
*
* @return array
*/
public function getAutoloaderConfig()
{
return array(
'Zend\Loader\ClassMapAutoloader' => array(
__DIR__ . '/autoload_classmap.php',
),
'Zend\Loader\StandardAutoloader' => array(
'namespaces' => array(
__NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__,
),
),
);
}
}
+24
View File
@@ -0,0 +1,24 @@
QR Code generator
=================
Master: [![Build Status](https://api.travis-ci.org/Bacon/BaconQrCode.png?branch=master)](http://travis-ci.org/Bacon/BaconQrCode)
Introduction
------------
BaconQrCode is a port of QR code portion of the ZXing library. It currently
only features the encoder part, but could later receive the decoder part as
well.
As the Reed Solomon codec implementation of the ZXing library performs quite
slow in PHP, it was exchanged with the implementation by Phil Karn.
Example usage
-------------
```php
$renderer = new \BaconQrCode\Renderer\Image\Png();
$renderer->setHeight(256);
$renderer->setWidth(256);
$writer = new \BaconQrCode\Writer($renderer);
$writer->writeFile('Hello World!', 'qrcode.png');
```
@@ -0,0 +1,43 @@
<?php
// Generated by ZF2's ./bin/classmap_generator.php
return array(
'BaconQrCode\Common\AbstractEnum' => __DIR__ . '/src/BaconQrCode/Common/AbstractEnum.php',
'BaconQrCode\Common\BitArray' => __DIR__ . '/src/BaconQrCode/Common/BitArray.php',
'BaconQrCode\Common\BitMatrix' => __DIR__ . '/src/BaconQrCode/Common/BitMatrix.php',
'BaconQrCode\Common\BitUtils' => __DIR__ . '/src/BaconQrCode/Common/BitUtils.php',
'BaconQrCode\Common\CharacterSetEci' => __DIR__ . '/src/BaconQrCode/Common/CharacterSetEci.php',
'BaconQrCode\Common\EcBlock' => __DIR__ . '/src/BaconQrCode/Common/EcBlock.php',
'BaconQrCode\Common\EcBlocks' => __DIR__ . '/src/BaconQrCode/Common/EcBlocks.php',
'BaconQrCode\Common\ErrorCorrectionLevel' => __DIR__ . '/src/BaconQrCode/Common/ErrorCorrectionLevel.php',
'BaconQrCode\Common\FormatInformation' => __DIR__ . '/src/BaconQrCode/Common/FormatInformation.php',
'BaconQrCode\Common\Mode' => __DIR__ . '/src/BaconQrCode/Common/Mode.php',
'BaconQrCode\Common\ReedSolomonCodec' => __DIR__ . '/src/BaconQrCode/Common/ReedSolomonCodec.php',
'BaconQrCode\Common\Version' => __DIR__ . '/src/BaconQrCode/Common/Version.php',
'BaconQrCode\Encoder\BlockPair' => __DIR__ . '/src/BaconQrCode/Encoder/BlockPair.php',
'BaconQrCode\Encoder\ByteMatrix' => __DIR__ . '/src/BaconQrCode/Encoder/ByteMatrix.php',
'BaconQrCode\Encoder\Encoder' => __DIR__ . '/src/BaconQrCode/Encoder/Encoder.php',
'BaconQrCode\Encoder\MaskUtil' => __DIR__ . '/src/BaconQrCode/Encoder/MaskUtil.php',
'BaconQrCode\Encoder\MatrixUtil' => __DIR__ . '/src/BaconQrCode/Encoder/MatrixUtil.php',
'BaconQrCode\Encoder\QrCode' => __DIR__ . '/src/BaconQrCode/Encoder/QrCode.php',
'BaconQrCode\Exception\ExceptionInterface' => __DIR__ . '/src/BaconQrCode/Exception/ExceptionInterface.php',
'BaconQrCode\Exception\InvalidArgumentException' => __DIR__ . '/src/BaconQrCode/Exception/InvalidArgumentException.php',
'BaconQrCode\Exception\OutOfBoundsException' => __DIR__ . '/src/BaconQrCode/Exception/OutOfBoundsException.php',
'BaconQrCode\Exception\RuntimeException' => __DIR__ . '/src/BaconQrCode/Exception/RuntimeException.php',
'BaconQrCode\Exception\UnexpectedValueException' => __DIR__ . '/src/BaconQrCode/Exception/UnexpectedValueException.php',
'BaconQrCode\Exception\WriterException' => __DIR__ . '/src/BaconQrCode/Exception/WriterException.php',
'BaconQrCode\Renderer\Color\Cmyk' => __DIR__ . '/src/BaconQrCode/Renderer/Color/Cmyk.php',
'BaconQrCode\Renderer\Color\ColorInterface' => __DIR__ . '/src/BaconQrCode/Renderer/Color/ColorInterface.php',
'BaconQrCode\Renderer\Color\Gray' => __DIR__ . '/src/BaconQrCode/Renderer/Color/Gray.php',
'BaconQrCode\Renderer\Color\Rgb' => __DIR__ . '/src/BaconQrCode/Renderer/Color/Rgb.php',
'BaconQrCode\Renderer\Image\AbstractRenderer' => __DIR__ . '/src/BaconQrCode/Renderer/Image/AbstractRenderer.php',
'BaconQrCode\Renderer\Image\Decorator\DecoratorInterface' => __DIR__ . '/src/BaconQrCode/Renderer/Image/Decorator/DecoratorInterface.php',
'BaconQrCode\Renderer\Image\Decorator\FinderPattern' => __DIR__ . '/src/BaconQrCode/Renderer/Image/Decorator/FinderPattern.php',
'BaconQrCode\Renderer\Image\Eps' => __DIR__ . '/src/BaconQrCode/Renderer/Image/Eps.php',
'BaconQrCode\Renderer\Image\Png' => __DIR__ . '/src/BaconQrCode/Renderer/Image/Png.php',
'BaconQrCode\Renderer\Image\RendererInterface' => __DIR__ . '/src/BaconQrCode/Renderer/Image/RendererInterface.php',
'BaconQrCode\Renderer\Image\Svg' => __DIR__ . '/src/BaconQrCode/Renderer/Image/Svg.php',
'BaconQrCode\Renderer\RendererInterface' => __DIR__ . '/src/BaconQrCode/Renderer/RendererInterface.php',
'BaconQrCode\Renderer\Text\Plain' => __DIR__ . '/src/BaconQrCode/Renderer/Text/Plain.php',
'BaconQrCode\Renderer\Text\Html' => __DIR__ . '/src/BaconQrCode/Renderer/Text/Html.php',
'BaconQrCode\Writer' => __DIR__ . '/src/BaconQrCode/Writer.php',
);
@@ -0,0 +1,12 @@
<?php
return function ($class) {
static $map;
if (!$map) {
$map = include __DIR__ . '/autoload_classmap.php';
}
if (!isset($map[$class])) {
return false;
}
return include $map[$class];
};
@@ -0,0 +1,2 @@
<?php
spl_autoload_register(include __DIR__ . '/autoload_function.php');
@@ -0,0 +1,29 @@
{
"name": "bacon/bacon-qr-code",
"description": "BaconQrCode is a QR code generator for PHP.",
"license" : "BSD-2-Clause",
"homepage": "https://github.com/Bacon/BaconQrCode",
"require": {
"php": "^5.4|^7.0",
"ext-iconv": "*"
},
"suggest": {
"ext-gd": "to generate QR code images"
},
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "http://www.dasprids.de",
"role": "Developer"
}
],
"autoload": {
"psr-0": {
"BaconQrCode": "src/"
}
},
"require-dev": {
"phpunit/phpunit": "^4.8"
}
}
@@ -0,0 +1,115 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Common;
use BaconQrCode\Exception;
use ReflectionClass;
/**
* A general enum implementation until we got SplEnum.
*/
abstract class AbstractEnum
{
/**
* Default value.
*/
const __default = null;
/**
* Current value.
*
* @var mixed
*/
protected $value;
/**
* Cache of constants.
*
* @var array
*/
protected $constants;
/**
* Whether to handle values strict or not.
*
* @var boolean
*/
protected $strict;
/**
* Creates a new enum.
*
* @param mixed $initialValue
* @param boolean $strict
*/
public function __construct($initialValue = null, $strict = false)
{
$this->strict = $strict;
$this->change($initialValue);
}
/**
* Changes the value of the enum.
*
* @param mixed $value
* @return void
*/
public function change($value)
{
if (!in_array($value, $this->getConstList(), $this->strict)) {
throw new Exception\UnexpectedValueException('Value not a const in enum ' . get_class($this));
}
$this->value = $value;
}
/**
* Gets current value.
*
* @return mixed
*/
public function get()
{
return $this->value;
}
/**
* Gets all constants (possible values) as an array.
*
* @param boolean $includeDefault
* @return array
*/
public function getConstList($includeDefault = true)
{
if ($this->constants === null) {
$reflection = new ReflectionClass($this);
$this->constants = $reflection->getConstants();
}
if ($includeDefault) {
return $this->constants;
}
$constants = $this->constants;
unset($constants['__default']);
return $constants;
}
/**
* Gets the name of the enum.
*
* @return string
*/
public function __toString()
{
return array_search($this->value, $this->getConstList());
}
}
@@ -0,0 +1,435 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Common;
use SplFixedArray;
/**
* A simple, fast array of bits.
*/
class BitArray
{
/**
* Bits represented as an array of integers.
*
* @var SplFixedArray
*/
protected $bits;
/**
* Size of the bit array in bits.
*
* @var integer
*/
protected $size;
/**
* Creates a new bit array with a given size.
*
* @param integer $size
*/
public function __construct($size = 0)
{
$this->size = $size;
$this->bits = new SplFixedArray(($this->size + 31) >> 3);
}
/**
* Gets the size in bits.
*
* @return integer
*/
public function getSize()
{
return $this->size;
}
/**
* Gets the size in bytes.
*
* @return integer
*/
public function getSizeInBytes()
{
return ($this->size + 7) >> 3;
}
/**
* Ensures that the array has a minimum capacity.
*
* @param integer $size
* @return void
*/
public function ensureCapacity($size)
{
if ($size > count($this->bits) << 5) {
$this->bits->setSize(($size + 31) >> 5);
}
}
/**
* Gets a specific bit.
*
* @param integer $i
* @return boolean
*/
public function get($i)
{
return ($this->bits[$i >> 5] & (1 << ($i & 0x1f))) !== 0;
}
/**
* Sets a specific bit.
*
* @param integer $i
* @return void
*/
public function set($i)
{
$this->bits[$i >> 5] = $this->bits[$i >> 5] | 1 << ($i & 0x1f);
}
/**
* Flips a specific bit.
*
* @param integer $i
* @return void
*/
public function flip($i)
{
$this->bits[$i >> 5] ^= 1 << ($i & 0x1f);
}
/**
* Gets the next set bit position from a given position.
*
* @param integer $from
* @return integer
*/
public function getNextSet($from)
{
if ($from >= $this->size) {
return $this->size;
}
$bitsOffset = $from >> 5;
$currentBits = $this->bits[$bitsOffset];
$bitsLength = count($this->bits);
$currentBits &= ~((1 << ($from & 0x1f)) - 1);
while ($currentBits === 0) {
if (++$bitsOffset === $bitsLength) {
return $this->size;
}
$currentBits = $this->bits[$bitsOffset];
}
$result = ($bitsOffset << 5) + BitUtils::numberOfTrailingZeros($currentBits);
return $result > $this->size ? $this->size : $result;
}
/**
* Gets the next unset bit position from a given position.
*
* @param integer $from
* @return integer
*/
public function getNextUnset($from)
{
if ($from >= $this->size) {
return $this->size;
}
$bitsOffset = $from >> 5;
$currentBits = ~$this->bits[$bitsOffset];
$bitsLength = count($this->bits);
$currentBits &= ~((1 << ($from & 0x1f)) - 1);
while ($currentBits === 0) {
if (++$bitsOffset === $bitsLength) {
return $this->size;
}
$currentBits = ~$this->bits[$bitsOffset];
}
$result = ($bitsOffset << 5) + BitUtils::numberOfTrailingZeros($currentBits);
return $result > $this->size ? $this->size : $result;
}
/**
* Sets a bulk of bits.
*
* @param integer $i
* @param integer $newBits
* @return void
*/
public function setBulk($i, $newBits)
{
$this->bits[$i >> 5] = $newBits;
}
/**
* Sets a range of bits.
*
* @param integer $start
* @param integer $end
* @return void
* @throws Exception\InvalidArgumentException
*/
public function setRange($start, $end)
{
if ($end < $start) {
throw new Exception\InvalidArgumentException('End must be greater or equal to start');
}
if ($end === $start) {
return;
}
$end--;
$firstInt = $start >> 5;
$lastInt = $end >> 5;
for ($i = $firstInt; $i <= $lastInt; $i++) {
$firstBit = $i > $firstInt ? 0 : $start & 0x1f;
$lastBit = $i < $lastInt ? 31 : $end & 0x1f;
if ($firstBit === 0 && $lastBit === 31) {
$mask = 0x7fffffff;
} else {
$mask = 0;
for ($j = $firstBit; $j < $lastBit; $j++) {
$mask |= 1 << $j;
}
}
$this->bits[$i] = $this->bits[$i] | $mask;
}
}
/**
* Clears the bit array, unsetting every bit.
*
* @return void
*/
public function clear()
{
$bitsLength = count($this->bits);
for ($i = 0; $i < $bitsLength; $i++) {
$this->bits[$i] = 0;
}
}
/**
* Checks if a range of bits is set or not set.
*
* @param integer $start
* @param integer $end
* @param integer $value
* @return boolean
* @throws Exception\InvalidArgumentException
*/
public function isRange($start, $end, $value)
{
if ($end < $start) {
throw new Exception\InvalidArgumentException('End must be greater or equal to start');
}
if ($end === $start) {
return;
}
$end--;
$firstInt = $start >> 5;
$lastInt = $end >> 5;
for ($i = $firstInt; $i <= $lastInt; $i++) {
$firstBit = $i > $firstInt ? 0 : $start & 0x1f;
$lastBit = $i < $lastInt ? 31 : $end & 0x1f;
if ($firstBit === 0 && $lastBit === 31) {
$mask = 0x7fffffff;
} else {
$mask = 0;
for ($j = $firstBit; $j <= $lastBit; $j++) {
$mask |= 1 << $j;
}
}
if (($this->bits[$i] & $mask) !== ($value ? $mask : 0)) {
return false;
}
}
return true;
}
/**
* Appends a bit to the array.
*
* @param boolean $bit
* @return void
*/
public function appendBit($bit)
{
$this->ensureCapacity($this->size + 1);
if ($bit) {
$this->bits[$this->size >> 5] = $this->bits[$this->size >> 5] | (1 << ($this->size & 0x1f));
}
$this->size++;
}
/**
* Appends a number of bits (up to 32) to the array.
*
* @param integer $value
* @param integer $numBits
* @return void
* @throws Exception\InvalidArgumentException
*/
public function appendBits($value, $numBits)
{
if ($numBits < 0 || $numBits > 32) {
throw new Exception\InvalidArgumentException('Num bits must be between 0 and 32');
}
$this->ensureCapacity($this->size + $numBits);
for ($numBitsLeft = $numBits; $numBitsLeft > 0; $numBitsLeft--) {
$this->appendBit((($value >> ($numBitsLeft - 1)) & 0x01) === 1);
}
}
/**
* Appends another bit array to this array.
*
* @param BitArray $other
* @return void
*/
public function appendBitArray(self $other)
{
$otherSize = $other->getSize();
$this->ensureCapacity($this->size + $other->getSize());
for ($i = 0; $i < $otherSize; $i++) {
$this->appendBit($other->get($i));
}
}
/**
* Makes an exclusive-or comparision on the current bit array.
*
* @param BitArray $other
* @return void
* @throws Exception\InvalidArgumentException
*/
public function xorBits(self $other)
{
$bitsLength = count($this->bits);
$otherBits = $other->getBitArray();
if ($bitsLength !== count($otherBits)) {
throw new Exception\InvalidArgumentException('Sizes don\'t match');
}
for ($i = 0; $i < $bitsLength; $i++) {
$this->bits[$i] = $this->bits[$i] ^ $otherBits[$i];
}
}
/**
* Converts the bit array to a byte array.
*
* @param integer $bitOffset
* @param integer $numBytes
* @return SplFixedArray
*/
public function toBytes($bitOffset, $numBytes)
{
$bytes = new SplFixedArray($numBytes);
for ($i = 0; $i < $numBytes; $i++) {
$byte = 0;
for ($j = 0; $j < 8; $j++) {
if ($this->get($bitOffset)) {
$byte |= 1 << (7 - $j);
}
$bitOffset++;
}
$bytes[$i] = $byte;
}
return $bytes;
}
/**
* Gets the internal bit array.
*
* @return SplFixedArray
*/
public function getBitArray()
{
return $this->bits;
}
/**
* Reverses the array.
*
* @return void
*/
public function reverse()
{
$newBits = new SplFixedArray(count($this->bits));
for ($i = 0; $i < $this->size; $i++) {
if ($this->get($this->size - $i - 1)) {
$newBits[$i >> 5] = $newBits[$i >> 5] | (1 << ($i & 0x1f));
}
}
$this->bits = newBits;
}
/**
* Returns a string representation of the bit array.
*
* @return string
*/
public function __toString()
{
$result = '';
for ($i = 0; $i < $this->size; $i++) {
if (($i & 0x07) === 0) {
$result .= ' ';
}
$result .= $this->get($i) ? 'X' : '.';
}
return $result;
}
}
@@ -0,0 +1,350 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Common;
use SplFixedArray;
/**
* Bit matrix.
*
* Represents a 2D matrix of bits. In function arguments below, and throughout
* the common module, x is the column position, and y is the row position. The
* ordering is always x, y. The origin is at the top-left.
*/
class BitMatrix
{
/**
* Width of the bit matrix.
*
* @var integer
*/
protected $width;
/**
* Height of the bit matrix.
*
* @var integer
*/
protected $height;
/**
* Size in bits of each individual row.
*
* @var integer
*/
protected $rowSize;
/**
* Bits representation.
*
* @var SplFixedArray
*/
protected $bits;
/**
* Creates a new bit matrix with given dimensions.
*
* @param integer $width
* @param integer|null $height
* @throws Exception\InvalidArgumentException
*/
public function __construct($width, $height = null)
{
if ($height === null) {
$height = $width;
}
if ($width < 1 || $height < 1) {
throw new Exception\InvalidArgumentException('Both dimensions must be greater than zero');
}
$this->width = $width;
$this->height = $height;
$this->rowSize = ($width + 31) >> 5;
$this->bits = new SplFixedArray($this->rowSize * $height);
}
/**
* Gets the requested bit, where true means black.
*
* @param integer $x
* @param integer $y
* @return boolean
*/
public function get($x, $y)
{
$offset = $y * $this->rowSize + ($x >> 5);
return (BitUtils::unsignedRightShift($this->bits[$offset], ($x & 0x1f)) & 1) !== 0;
}
/**
* Sets the given bit to true.
*
* @param integer $x
* @param integer $y
* @return void
*/
public function set($x, $y)
{
$offset = $y * $this->rowSize + ($x >> 5);
$this->bits[$offset] = $this->bits[$offset] | (1 << ($x & 0x1f));
}
/**
* Flips the given bit.
*
* @param integer $x
* @param integer $y
* @return void
*/
public function flip($x, $y)
{
$offset = $y * $this->rowSize + ($x >> 5);
$this->bits[$offset] = $this->bits[$offset] ^ (1 << ($x & 0x1f));
}
/**
* Clears all bits (set to false).
*
* @return void
*/
public function clear()
{
$max = count($this->bits);
for ($i = 0; $i < $max; $i++) {
$this->bits[$i] = 0;
}
}
/**
* Sets a square region of the bit matrix to true.
*
* @param integer $left
* @param integer $top
* @param integer $width
* @param integer $height
* @return void
*/
public function setRegion($left, $top, $width, $height)
{
if ($top < 0 || $left < 0) {
throw new Exception\InvalidArgumentException('Left and top must be non-negative');
}
if ($height < 1 || $width < 1) {
throw new Exception\InvalidArgumentException('Width and height must be at least 1');
}
$right = $left + $width;
$bottom = $top + $height;
if ($bottom > $this->height || $right > $this->width) {
throw new Exception\InvalidArgumentException('The region must fit inside the matrix');
}
for ($y = $top; $y < $bottom; $y++) {
$offset = $y * $this->rowSize;
for ($x = $left; $x < $right; $x++) {
$index = $offset + ($x >> 5);
$this->bits[$index] = $this->bits[$index] | (1 << ($x & 0x1f));
}
}
}
/**
* A fast method to retrieve one row of data from the matrix as a BitArray.
*
* @param integer $y
* @param BitArray $row
* @return BitArray
*/
public function getRow($y, BitArray $row = null)
{
if ($row === null || $row->getSize() < $this->width) {
$row = new BitArray($this->width);
}
$offset = $y * $this->rowSize;
for ($x = 0; $x < $this->rowSize; $x++) {
$row->setBulk($x << 5, $this->bits[$offset + $x]);
}
return $row;
}
/**
* Sets a row of data from a BitArray.
*
* @param integer $y
* @param BitArray $row
* @return void
*/
public function setRow($y, BitArray $row)
{
$bits = $row->getBitArray();
for ($i = 0; $i < $this->rowSize; $i++) {
$this->bits[$y * $this->rowSize + $i] = $bits[$i];
}
}
/**
* This is useful in detecting the enclosing rectangle of a 'pure' barcode.
*
* @return SplFixedArray
*/
public function getEnclosingRectangle()
{
$left = $this->width;
$top = $this->height;
$right = -1;
$bottom = -1;
for ($y = 0; $y < $this->height; $y++) {
for ($x32 = 0; $x32 < $this->rowSize; $x32++) {
$bits = $this->bits[$y * $this->rowSize + $x32];
if ($bits !== 0) {
if ($y < $top) {
$top = $y;
}
if ($y > $bottom) {
$bottom = $y;
}
if ($x32 * 32 < $left) {
$bit = 0;
while (($bits << (31 - $bit)) === 0) {
$bit++;
}
if (($x32 * 32 + $bit) < $left) {
$left = $x32 * 32 + $bit;
}
}
}
if ($x32 * 32 + 31 > $right) {
$bit = 31;
while (BitUtils::unsignedRightShift($bits, $bit) === 0) {
$bit--;
}
if (($x32 * 32 + $bit) > $right) {
$right = $x32 * 32 + $bit;
}
}
}
}
$width = $right - $left;
$height = $bottom - $top;
if ($width < 0 || $height < 0) {
return null;
}
return SplFixedArray::fromArray(array($left, $top, $width, $height), false);
}
/**
* Gets the most top left set bit.
*
* This is useful in detecting a corner of a 'pure' barcode.
*
* @return SplFixedArray
*/
public function getTopLeftOnBit()
{
$bitsOffset = 0;
while ($bitsOffset < count($this->bits) && $this->bits[$bitsOffset] === 0) {
$bitsOffset++;
}
if ($bitsOffset === count($this->bits)) {
return null;
}
$x = intval($bitsOffset / $this->rowSize);
$y = ($bitsOffset % $this->rowSize) << 5;
$bits = $this->bits[$bitsOffset];
$bit = 0;
while (($bits << (31 - $bit)) === 0) {
$bit++;
}
$x += $bit;
return SplFixedArray::fromArray(array($x, $y), false);
}
/**
* Gets the most bottom right set bit.
*
* This is useful in detecting a corner of a 'pure' barcode.
*
* @return SplFixedArray
*/
public function getBottomRightOnBit()
{
$bitsOffset = count($this->bits) - 1;
while ($bitsOffset >= 0 && $this->bits[$bitsOffset] === 0) {
$bitsOffset--;
}
if ($bitsOffset < 0) {
return null;
}
$x = intval($bitsOffset / $this->rowSize);
$y = ($bitsOffset % $this->rowSize) << 5;
$bits = $this->bits[$bitsOffset];
$bit = 0;
while (BitUtils::unsignedRightShift($bits, $bit) === 0) {
$bit--;
}
$x += $bit;
return SplFixedArray::fromArray(array($x, $y), false);
}
/**
* Gets the width of the matrix,
*
* @return integer
*/
public function getWidth()
{
return $this->width;
}
/**
* Gets the height of the matrix.
*
* @return integer
*/
public function getHeight()
{
return $this->height;
}
}
@@ -0,0 +1,51 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Common;
/**
* General bit utilities.
*
* All utility methods are based on 32-bit integers and also work on 64-bit
* systems.
*/
class BitUtils
{
/**
* Performs an unsigned right shift.
*
* This is the same as the unsigned right shift operator ">>>" in other
* languages.
*
* @param integer $a
* @param integer $b
* @return integer
*/
public static function unsignedRightShift($a, $b)
{
return (
$a >= 0
? $a >> $b
: (($a & 0x7fffffff) >> $b) | (0x40000000 >> ($b - 1))
);
}
/**
* Gets the number of trailing zeros.
*
* @param integer $i
* @return integer
*/
public static function numberOfTrailingZeros($i)
{
$lastPos = strrpos(str_pad(decbin($i), 32, '0', STR_PAD_LEFT), '1');
return $lastPos === false ? 32 : 31 - $lastPos;
}
}
@@ -0,0 +1,134 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Common;
/**
* Encapsulates a Character Set ECI, according to "Extended Channel
* Interpretations" 5.3.1.1 of ISO 18004.
*/
class CharacterSetEci extends AbstractEnum
{
/**#@+
* Character set constants.
*/
const CP437 = 0;
const ISO8859_1 = 1;
const ISO8859_2 = 4;
const ISO8859_3 = 5;
const ISO8859_4 = 6;
const ISO8859_5 = 7;
const ISO8859_6 = 8;
const ISO8859_7 = 9;
const ISO8859_8 = 10;
const ISO8859_9 = 11;
const ISO8859_10 = 12;
const ISO8859_11 = 13;
const ISO8859_12 = 14;
const ISO8859_13 = 15;
const ISO8859_14 = 16;
const ISO8859_15 = 17;
const ISO8859_16 = 18;
const SJIS = 20;
const CP1250 = 21;
const CP1251 = 22;
const CP1252 = 23;
const CP1256 = 24;
const UNICODE_BIG_UNMARKED = 25;
const UTF8 = 26;
const ASCII = 27;
const BIG5 = 28;
const GB18030 = 29;
const EUC_KR = 30;
/**#@-*/
/**
* Map between character names and their ECI values.
*
* @var array
*/
protected static $nameToEci = array(
'ISO-8859-1' => self::ISO8859_1,
'ISO-8859-2' => self::ISO8859_2,
'ISO-8859-3' => self::ISO8859_3,
'ISO-8859-4' => self::ISO8859_4,
'ISO-8859-5' => self::ISO8859_5,
'ISO-8859-6' => self::ISO8859_6,
'ISO-8859-7' => self::ISO8859_7,
'ISO-8859-8' => self::ISO8859_8,
'ISO-8859-9' => self::ISO8859_9,
'ISO-8859-10' => self::ISO8859_10,
'ISO-8859-11' => self::ISO8859_11,
'ISO-8859-12' => self::ISO8859_12,
'ISO-8859-13' => self::ISO8859_13,
'ISO-8859-14' => self::ISO8859_14,
'ISO-8859-15' => self::ISO8859_15,
'ISO-8859-16' => self::ISO8859_16,
'SHIFT-JIS' => self::SJIS,
'WINDOWS-1250' => self::CP1250,
'WINDOWS-1251' => self::CP1251,
'WINDOWS-1252' => self::CP1252,
'WINDOWS-1256' => self::CP1256,
'UTF-16BE' => self::UNICODE_BIG_UNMARKED,
'UTF-8' => self::UTF8,
'ASCII' => self::ASCII,
'GBK' => self::GB18030,
'EUC-KR' => self::EUC_KR,
);
/**
* Additional possible values for character sets.
*
* @var array
*/
protected $additionalValues = array(
self::CP437 => 2,
self::ASCII => 170,
);
/**
* Gets character set ECI by value.
*
* @param string $name
* @return CharacterSetEci|null
*/
public static function getCharacterSetECIByValue($value)
{
if ($value < 0 || $value >= 900) {
throw new Exception\InvalidArgumentException('Value must be between 0 and 900');
}
if (false !== ($key = array_search($value, self::$additionalValues))) {
$value = $key;
}
try {
return new self($value);
} catch (Exception\UnexpectedValueException $e) {
return null;
}
}
/**
* Gets character set ECI by name.
*
* @param string $name
* @return CharacterSetEci|null
*/
public static function getCharacterSetECIByName($name)
{
$name = strtoupper($name);
if (isset(self::$nameToEci[$name])) {
return new self(self::$nameToEci[$name]);
}
return null;
}
}
@@ -0,0 +1,65 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Common;
/**
* Encapsualtes the parameters for one error-correction block in one symbol
* version. This includes the number of data codewords, and the number of times
* a block with these parameters is used consecutively in the QR code version's
* format.
*/
class EcBlock
{
/**
* How many times the block is used.
*
* @var integer
*/
protected $count;
/**
* Number of data codewords.
*
* @var integer
*/
protected $dataCodewords;
/**
* Creates a new EC block.
*
* @param integer $count
* @param integer $dataCodewords
*/
public function __construct($count, $dataCodewords)
{
$this->count = $count;
$this->dataCodewords = $dataCodewords;
}
/**
* Returns how many times the block is used.
*
* @return integer
*/
public function getCount()
{
return $this->count;
}
/**
* Returns the number of data codewords.
*
* @return integer
*/
public function getDataCodewords()
{
return $this->dataCodewords;
}
}
@@ -0,0 +1,101 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Common;
use SplFixedArray;
/**
* Encapsulates a set of error-correction blocks in one symbol version. Most
* versions will use blocks of differing sizes within one version, so, this
* encapsulates the parameters for each set of blocks. It also holds the number
* of error-correction codewords per block since it will be the same across all
* blocks within one version.
*/
class EcBlocks
{
/**
* Number of EC codewords per block.
*
* @var integer
*/
protected $ecCodewordsPerBlock;
/**
* List of EC blocks.
*
* @var SplFixedArray
*/
protected $ecBlocks;
/**
* Creates a new EC blocks instance.
*
* @param integer $ecCodewordsPerBlock
* @param EcBlock $ecb1
* @param EcBlock|null $ecb2
*/
public function __construct($ecCodewordsPerBlock, EcBlock $ecb1, EcBlock $ecb2 = null)
{
$this->ecCodewordsPerBlock = $ecCodewordsPerBlock;
$this->ecBlocks = new SplFixedArray($ecb2 === null ? 1 : 2);
$this->ecBlocks[0] = $ecb1;
if ($ecb2 !== null) {
$this->ecBlocks[1] = $ecb2;
}
}
/**
* Gets the number of EC codewords per block.
*
* @return integer
*/
public function getEcCodewordsPerBlock()
{
return $this->ecCodewordsPerBlock;
}
/**
* Gets the total number of EC block appearances.
*
* @return integer
*/
public function getNumBlocks()
{
$total = 0;
foreach ($this->ecBlocks as $ecBlock) {
$total += $ecBlock->getCount();
}
return $total;
}
/**
* Gets the total count of EC codewords.
*
* @return integer
*/
public function getTotalEcCodewords()
{
return $this->ecCodewordsPerBlock * $this->getNumBlocks();
}
/**
* Gets the EC blocks included in this collection.
*
* @return SplFixedArray
*/
public function getEcBlocks()
{
return $this->ecBlocks;
}
}
@@ -0,0 +1,62 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Common;
/**
* Enum representing the four error correction levels.
*/
class ErrorCorrectionLevel extends AbstractEnum
{
/**
* Level L, ~7% correction.
*/
const L = 0x1;
/**
* Level M, ~15% correction.
*/
const M = 0x0;
/**
* Level Q, ~25% correction.
*/
const Q = 0x3;
/**
* Level H, ~30% correction.
*/
const H = 0x2;
/**
* Gets the ordinal of this enumeration constant.
*
* @return integer
*/
public function getOrdinal()
{
switch ($this->value) {
case self::L:
return 0;
break;
case self::M:
return 1;
break;
case self::Q:
return 2;
break;
case self::H:
return 3;
break;
}
}
}
@@ -0,0 +1,236 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Common;
/**
* Encapsulates a QR Code's format information, including the data mask used and
* error correction level.
*/
class FormatInformation
{
/**
* Mask for format information.
*/
const FORMAT_INFO_MASK_QR = 0x5412;
/**
* Lookup table for decoding format information.
*
* See ISO 18004:2006, Annex C, Table C.1
*
* @var array
*/
protected static $formatInfoDecodeLookup = array(
array(0x5412, 0x00),
array(0x5125, 0x01),
array(0x5e7c, 0x02),
array(0x5b4b, 0x03),
array(0x45f9, 0x04),
array(0x40ce, 0x05),
array(0x4f97, 0x06),
array(0x4aa0, 0x07),
array(0x77c4, 0x08),
array(0x72f3, 0x09),
array(0x7daa, 0x0a),
array(0x789d, 0x0b),
array(0x662f, 0x0c),
array(0x6318, 0x0d),
array(0x6c41, 0x0e),
array(0x6976, 0x0f),
array(0x1689, 0x10),
array(0x13be, 0x11),
array(0x1ce7, 0x12),
array(0x19d0, 0x13),
array(0x0762, 0x14),
array(0x0255, 0x15),
array(0x0d0c, 0x16),
array(0x083b, 0x17),
array(0x355f, 0x18),
array(0x3068, 0x19),
array(0x3f31, 0x1a),
array(0x3a06, 0x1b),
array(0x24b4, 0x1c),
array(0x2183, 0x1d),
array(0x2eda, 0x1e),
array(0x2bed, 0x1f),
);
/**
* Offset i holds the number of 1 bits in the binary representation of i.
*
* @var array
*/
protected static $bitsSetInHalfByte = array(0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4);
/**
* Error correction level.
*
* @var ErrorCorrectionLevel
*/
protected $ecLevel;
/**
* Data mask.
*
* @var integer
*/
protected $dataMask;
/**
* Creates a new format information instance.
*
* @param integer $formatInfo
*/
protected function __construct($formatInfo)
{
$this->ecLevel = new ErrorCorrectionLevel(($formatInfo >> 3) & 0x3);
$this->dataMask = $formatInfo & 0x7;
}
/**
* Checks how many bits are different between two integers.
*
* @param integer $a
* @param integer $b
* @return integer
*/
public static function numBitsDiffering($a, $b)
{
$a ^= $b;
return (
self::$bitsSetInHalfByte[$a & 0xf]
+ self::$bitsSetInHalfByte[(BitUtils::unsignedRightShift($a, 4) & 0xf)]
+ self::$bitsSetInHalfByte[(BitUtils::unsignedRightShift($a, 8) & 0xf)]
+ self::$bitsSetInHalfByte[(BitUtils::unsignedRightShift($a, 12) & 0xf)]
+ self::$bitsSetInHalfByte[(BitUtils::unsignedRightShift($a, 16) & 0xf)]
+ self::$bitsSetInHalfByte[(BitUtils::unsignedRightShift($a, 20) & 0xf)]
+ self::$bitsSetInHalfByte[(BitUtils::unsignedRightShift($a, 24) & 0xf)]
+ self::$bitsSetInHalfByte[(BitUtils::unsignedRightShift($a, 28) & 0xf)]
);
}
/**
* Decodes format information.
*
* @param integer $maskedFormatInfo1
* @param integer $maskedFormatInfo2
* @return FormatInformation|null
*/
public static function decodeFormatInformation($maskedFormatInfo1, $maskedFormatInfo2)
{
$formatInfo = self::doDecodeFormatInformation($maskedFormatInfo1, $maskedFormatInfo2);
if ($formatInfo !== null) {
return $formatInfo;
}
// Should return null, but, some QR codes apparently do not mask this
// info. Try again by actually masking the pattern first.
return self::doDecodeFormatInformation(
$maskedFormatInfo1 ^ self::FORMAT_INFO_MASK_QR,
$maskedFormatInfo2 ^ self::FORMAT_INFO_MASK_QR
);
}
/**
* Internal method for decoding format information.
*
* @param integer $maskedFormatInfo1
* @param integer $maskedFormatInfo2
* @return FormatInformation|null
*/
protected static function doDecodeFormatInformation($maskedFormatInfo1, $maskedFormatInfo2)
{
$bestDifference = PHP_INT_MAX;
$bestFormatInfo = 0;
foreach (self::$formatInfoDecodeLookup as $decodeInfo) {
$targetInfo = $decodeInfo[0];
if ($targetInfo === $maskedFormatInfo1 || $targetInfo === $maskedFormatInfo2) {
// Found an exact match
return new self($decodeInfo[1]);
}
$bitsDifference = self::numBitsDiffering($maskedFormatInfo1, $targetInfo);
if ($bitsDifference < $bestDifference) {
$bestFormatInfo = $decodeInfo[1];
$bestDifference = $bitsDifference;
}
if ($maskedFormatInfo1 !== $maskedFormatInfo2) {
// Also try the other option
$bitsDifference = self::numBitsDiffering($maskedFormatInfo2, $targetInfo);
if ($bitsDifference < $bestDifference) {
$bestFormatInfo = $decodeInfo[1];
$bestDifference = $bitsDifference;
}
}
}
// Hamming distance of the 32 masked codes is 7, by construction, so
// <= 3 bits differing means we found a match.
if ($bestDifference <= 3) {
return new self($bestFormatInfo);
}
return null;
}
/**
* Gets the error correction level.
*
* @return ErrorCorrectionLevel
*/
public function getErrorCorrectionLevel()
{
return $this->ecLevel;
}
/**
* Gets the data mask.
*
* @return integer
*/
public function getDataMask()
{
return $this->dataMask;
}
/**
* Hashes the code of the EC level.
*
* @return integer
*/
public function hashCode()
{
return ($this->ecLevel->get() << 3) | $this->dataMask;
}
/**
* Verifies if this instance equals another one.
*
* @param mixed $other
* @return boolean
*/
public function equals($other) {
if (!$other instanceof self) {
return false;
}
return (
$this->ecLevel->get() === $other->getErrorCorrectionLevel()->get()
&& $this->dataMask === $other->getDataMask()
);
}
}
@@ -0,0 +1,70 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Common;
/**
* Enum representing various modes in which data can be encoded to bits.
*/
class Mode extends AbstractEnum
{
/**#@+
* Mode constants.
*/
const TERMINATOR = 0x0;
const NUMERIC = 0x1;
const ALPHANUMERIC = 0x2;
const STRUCTURED_APPEND = 0x3;
const BYTE = 0x4;
const ECI = 0x7;
const KANJI = 0x8;
const FNC1_FIRST_POSITION = 0x5;
const FNC1_SECOND_POSITION = 0x9;
const HANZI = 0xd;
/**#@-*/
/**
* Character count bits for each version.
*
* @var array
*/
protected static $characterCountBitsForVersions = array(
self::TERMINATOR => array(0, 0, 0),
self::NUMERIC => array(10, 12, 14),
self::ALPHANUMERIC => array(9, 11, 13),
self::STRUCTURED_APPEND => array(0, 0, 0),
self::BYTE => array(8, 16, 16),
self::ECI => array(0, 0, 0),
self::KANJI => array(8, 10, 12),
self::FNC1_FIRST_POSITION => array(0, 0, 0),
self::FNC1_SECOND_POSITION => array(0, 0, 0),
self::HANZI => array(8, 10, 12),
);
/**
* Gets the number of bits used in a specific QR code version.
*
* @param Version $version
* @return integer
*/
public function getCharacterCountBits(Version $version)
{
$number = $version->getVersionNumber();
if ($number <= 9) {
$offset = 0;
} elseif ($number <= 26) {
$offset = 1;
} else {
$offset = 2;
}
return self::$characterCountBitsForVersions[$this->value][$offset];
}
}
@@ -0,0 +1,476 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Common;
use BaconQrCode\Exception;
use SplFixedArray;
/**
* Reed-Solomon codec for 8-bit characters.
*
* Based on libfec by Phil Karn, KA9Q.
*/
class ReedSolomonCodec
{
/**
* Symbol size in bits.
*
* @var integer
*/
protected $symbolSize;
/**
* Block size in symbols.
*
* @var integer
*/
protected $blockSize;
/**
* First root of RS code generator polynomial, index form.
*
* @var integer
*/
protected $firstRoot;
/**
* Primitive element to generate polynomial roots, index form.
*
* @var integer
*/
protected $primitive;
/**
* Prim-th root of 1, index form.
*
* @var integer
*/
protected $iPrimitive;
/**
* RS code generator polynomial degree (number of roots).
*
* @var integer
*/
protected $numRoots;
/**
* Padding bytes at front of shortened block.
*
* @var integer
*/
protected $padding;
/**
* Log lookup table.
*
* @var SplFixedArray
*/
protected $alphaTo;
/**
* Anti-Log lookup table.
*
* @var SplFixedArray
*/
protected $indexOf;
/**
* Generator polynomial.
*
* @var SplFixedArray
*/
protected $generatorPoly;
/**
* Creates a new reed solomon instance.
*
* @param integer $symbolSize
* @param integer $gfPoly
* @param integer $firstRoot
* @param integer $primitive
* @param integer $numRoots
* @param integer $padding
* @throws Exception\InvalidArgumentException
* @throws Exception\RuntimeException
*/
public function __construct($symbolSize, $gfPoly, $firstRoot, $primitive, $numRoots, $padding)
{
if ($symbolSize < 0 || $symbolSize > 8) {
throw new Exception\InvalidArgumentException('Symbol size must be between 0 and 8');
}
if ($firstRoot < 0 || $firstRoot >= (1 << $symbolSize)) {
throw new Exception\InvalidArgumentException('First root must be between 0 and ' . (1 << $symbolSize));
}
if ($numRoots < 0 || $numRoots >= (1 << $symbolSize)) {
throw new Exception\InvalidArgumentException('Num roots must be between 0 and ' . (1 << $symbolSize));
}
if ($padding < 0 || $padding >= ((1 << $symbolSize) - 1 - $numRoots)) {
throw new Exception\InvalidArgumentException('Padding must be between 0 and ' . ((1 << $symbolSize) - 1 - $numRoots));
}
$this->symbolSize = $symbolSize;
$this->blockSize = (1 << $symbolSize) - 1;
$this->padding = $padding;
$this->alphaTo = SplFixedArray::fromArray(array_fill(0, $this->blockSize + 1, 0), false);
$this->indexOf = SplFixedArray::fromArray(array_fill(0, $this->blockSize + 1, 0), false);
// Generate galous field lookup table
$this->indexOf[0] = $this->blockSize;
$this->alphaTo[$this->blockSize] = 0;
$sr = 1;
for ($i = 0; $i < $this->blockSize; $i++) {
$this->indexOf[$sr] = $i;
$this->alphaTo[$i] = $sr;
$sr <<= 1;
if ($sr & (1 << $symbolSize)) {
$sr ^= $gfPoly;
}
$sr &= $this->blockSize;
}
if ($sr !== 1) {
throw new Exception\RuntimeException('Field generator polynomial is not primitive');
}
// Form RS code generator polynomial from its roots
$this->generatorPoly = SplFixedArray::fromArray(array_fill(0, $numRoots + 1, 0), false);
$this->firstRoot = $firstRoot;
$this->primitive = $primitive;
$this->numRoots = $numRoots;
// Find prim-th root of 1, used in decoding
for ($iPrimitive = 1; ($iPrimitive % $primitive) !== 0; $iPrimitive += $this->blockSize);
$this->iPrimitive = intval($iPrimitive / $primitive);
$this->generatorPoly[0] = 1;
for ($i = 0, $root = $firstRoot * $primitive; $i < $numRoots; $i++, $root += $primitive) {
$this->generatorPoly[$i + 1] = 1;
for ($j = $i; $j > 0; $j--) {
if ($this->generatorPoly[$j] !== 0) {
$this->generatorPoly[$j] = $this->generatorPoly[$j - 1] ^ $this->alphaTo[$this->modNn($this->indexOf[$this->generatorPoly[$j]] + $root)];
} else {
$this->generatorPoly[$j] = $this->generatorPoly[$j - 1];
}
}
$this->generatorPoly[$j] = $this->alphaTo[$this->modNn($this->indexOf[$this->generatorPoly[0]] + $root)];
}
// Convert generator poly to index form for quicker encoding
for ($i = 0; $i <= $numRoots; $i++) {
$this->generatorPoly[$i] = $this->indexOf[$this->generatorPoly[$i]];
}
}
/**
* Encodes data and writes result back into parity array.
*
* @param SplFixedArray $data
* @param SplFixedArray $parity
* @return void
*/
public function encode(SplFixedArray $data, SplFixedArray $parity)
{
for ($i = 0; $i < $this->numRoots; $i++) {
$parity[$i] = 0;
}
$iterations = $this->blockSize - $this->numRoots - $this->padding;
for ($i = 0; $i < $iterations; $i++) {
$feedback = $this->indexOf[$data[$i] ^ $parity[0]];
if ($feedback !== $this->blockSize) {
// Feedback term is non-zero
$feedback = $this->modNn($this->blockSize - $this->generatorPoly[$this->numRoots] + $feedback);
for ($j = 1; $j < $this->numRoots; $j++) {
$parity[$j] = $parity[$j] ^ $this->alphaTo[$this->modNn($feedback + $this->generatorPoly[$this->numRoots - $j])];
}
}
for ($j = 0; $j < $this->numRoots - 1; $j++) {
$parity[$j] = $parity[$j + 1];
}
if ($feedback !== $this->blockSize) {
$parity[$this->numRoots - 1] = $this->alphaTo[$this->modNn($feedback + $this->generatorPoly[0])];
} else {
$parity[$this->numRoots - 1] = 0;
}
}
}
/**
* Decodes received data.
*
* @param SplFixedArray $data
* @param SplFixedArray|null $erasures
* @return null|integer
*/
public function decode(SplFixedArray $data, SplFixedArray $erasures = null)
{
// This speeds up the initialization a bit.
$numRootsPlusOne = SplFixedArray::fromArray(array_fill(0, $this->numRoots + 1, 0), false);
$numRoots = SplFixedArray::fromArray(array_fill(0, $this->numRoots, 0), false);
$lambda = clone $numRootsPlusOne;
$b = clone $numRootsPlusOne;
$t = clone $numRootsPlusOne;
$omega = clone $numRootsPlusOne;
$root = clone $numRoots;
$loc = clone $numRoots;
$numErasures = ($erasures !== null ? count($erasures) : 0);
// Form the Syndromes; i.e., evaluate data(x) at roots of g(x)
$syndromes = SplFixedArray::fromArray(array_fill(0, $this->numRoots, $data[0]), false);
for ($i = 1; $i < $this->blockSize - $this->padding; $i++) {
for ($j = 0; $j < $this->numRoots; $j++) {
if ($syndromes[$j] === 0) {
$syndromes[$j] = $data[$i];
} else {
$syndromes[$j] = $data[$i] ^ $this->alphaTo[
$this->modNn($this->indexOf[$syndromes[$j]] + ($this->firstRoot + $j) * $this->primitive)
];
}
}
}
// Convert syndromes to index form, checking for nonzero conditions
$syndromeError = 0;
for ($i = 0; $i < $this->numRoots; $i++) {
$syndromeError |= $syndromes[$i];
$syndromes[$i] = $this->indexOf[$syndromes[$i]];
}
if (!$syndromeError) {
// If syndrome is zero, data[] is a codeword and there are no errors
// to correct, so return data[] unmodified.
return 0;
}
$lambda[0] = 1;
if ($numErasures > 0) {
// Init lambda to be the erasure locator polynomial
$lambda[1] = $this->alphaTo[$this->modNn($this->primitive * ($this->blockSize - 1 - $erasures[0]))];
for ($i = 1; $i < $numErasures; $i++) {
$u = $this->modNn($this->primitive * ($this->blockSize - 1 - $erasures[$i]));
for ($j = $i + 1; $j > 0; $j--) {
$tmp = $this->indexOf[$lambda[$j - 1]];
if ($tmp !== $this->blockSize) {
$lambda[$j] = $lambda[$j] ^ $this->alphaTo[$this->modNn($u + $tmp)];
}
}
}
}
for ($i = 0; $i <= $this->numRoots; $i++) {
$b[$i] = $this->indexOf[$lambda[$i]];
}
// Begin Berlekamp-Massey algorithm to determine error+erasure locator
// polynomial
$r = $numErasures;
$el = $numErasures;
while (++$r <= $this->numRoots) {
// Compute discrepancy at the r-th step in poly form
$discrepancyR = 0;
for ($i = 0; $i < $r; $i++) {
if ($lambda[$i] !== 0 && $syndromes[$r - $i - 1] !== $this->blockSize) {
$discrepancyR ^= $this->alphaTo[$this->modNn($this->indexOf[$lambda[$i]] + $syndromes[$r - $i - 1])];
}
}
$discrepancyR = $this->indexOf[$discrepancyR];
if ($discrepancyR === $this->blockSize) {
$tmp = $b->toArray();
array_unshift($tmp, $this->blockSize);
array_pop($tmp);
$b = SplFixedArray::fromArray($tmp, false);
} else {
$t[0] = $lambda[0];
for ($i = 0; $i < $this->numRoots; $i++) {
if ($b[$i] !== $this->blockSize) {
$t[$i + 1] = $lambda[$i + 1] ^ $this->alphaTo[$this->modNn($discrepancyR + $b[$i])];
} else {
$t[$i + 1] = $lambda[$i + 1];
}
}
if (2 * $el <= $r + $numErasures - 1) {
$el = $r + $numErasures - $el;
for ($i = 0; $i <= $this->numRoots; $i++) {
$b[$i] = (
$lambda[$i] === 0
? $this->blockSize
: $this->modNn($this->indexOf[$lambda[$i]] - $discrepancyR + $this->blockSize)
);
}
} else {
$tmp = $b->toArray();
array_unshift($tmp, $this->blockSize);
array_pop($tmp);
$b = SplFixedArray::fromArray($tmp, false);
}
$lambda = clone $t;
}
}
// Convert lambda to index form and compute deg(lambda(x))
$degLambda = 0;
for ($i = 0; $i <= $this->numRoots; $i++) {
$lambda[$i] = $this->indexOf[$lambda[$i]];
if ($lambda[$i] !== $this->blockSize) {
$degLambda = $i;
}
}
// Find roots of the error+erasure locator polynomial by Chien search.
$reg = clone $lambda;
$reg[0] = 0;
$count = 0;
for ($i = 1, $k = $this->iPrimitive - 1; $i <= $this->blockSize; $i++, $k = $this->modNn($k + $this->iPrimitive)) {
$q = 1;
for ($j = $degLambda; $j > 0; $j--) {
if ($reg[$j] !== $this->blockSize) {
$reg[$j] = $this->modNn($reg[$j] + $j);
$q ^= $this->alphaTo[$reg[$j]];
}
}
if ($q !== 0) {
// Not a root
continue;
}
// Store root (index-form) and error location number
$root[$count] = $i;
$loc[$count] = $k;
if (++$count === $degLambda) {
break;
}
}
if ($degLambda !== $count) {
// deg(lambda) unequal to number of roots: uncorreactable error
// detected
return null;
}
// Compute err+eras evaluate poly omega(x) = s(x)*lambda(x) (modulo
// x**numRoots). In index form. Also find deg(omega).
$degOmega = $degLambda - 1;
for ($i = 0; $i <= $degOmega; $i++) {
$tmp = 0;
for ($j = $i; $j >= 0; $j--) {
if ($syndromes[$i - $j] !== $this->blockSize && $lambda[$j] !== $this->blockSize) {
$tmp ^= $this->alphaTo[$this->modNn($syndromes[$i - $j] + $lambda[$j])];
}
}
$omega[$i] = $this->indexOf[$tmp];
}
// Compute error values in poly-form. num1 = omega(inv(X(l))), num2 =
// inv(X(l))**(firstRoot-1) and den = lambda_pr(inv(X(l))) all in poly
// form.
for ($j = $count - 1; $j >= 0; $j--) {
$num1 = 0;
for ($i = $degOmega; $i >= 0; $i--) {
if ($omega[$i] !== $this->blockSize) {
$num1 ^= $this->alphaTo[$this->modNn($omega[$i] + $i * $root[$j])];
}
}
$num2 = $this->alphaTo[$this->modNn($root[$j] * ($this->firstRoot - 1) + $this->blockSize)];
$den = 0;
// lambda[i+1] for i even is the formal derivativelambda_pr of
// lambda[i]
for ($i = min($degLambda, $this->numRoots - 1) & ~1; $i >= 0; $i -= 2) {
if ($lambda[$i + 1] !== $this->blockSize) {
$den ^= $this->alphaTo[$this->modNn($lambda[$i + 1] + $i * $root[$j])];
}
}
// Apply error to data
if ($num1 !== 0 && $loc[$j] >= $this->padding) {
$data[$loc[$j] - $this->padding] = $data[$loc[$j] - $this->padding] ^ (
$this->alphaTo[
$this->modNn(
$this->indexOf[$num1] + $this->indexOf[$num2] + $this->blockSize - $this->indexOf[$den]
)
]
);
}
}
if ($erasures !== null) {
if (count($erasures) < $count) {
$erasures->setSize($count);
}
for ($i = 0; $i < $count; $i++) {
$erasures[$i] = $loc[$i];
}
}
return $count;
}
/**
* Computes $x % GF_SIZE, where GF_SIZE is 2**GF_BITS - 1, without a slow
* divide.
*
* @param itneger $x
* @return integer
*/
protected function modNn($x)
{
while ($x >= $this->blockSize) {
$x -= $this->blockSize;
$x = ($x >> $this->symbolSize) + ($x & $this->blockSize);
}
return $x;
}
}
@@ -0,0 +1,687 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Common;
use SplFixedArray;
/**
* Version representation.
*/
class Version
{
/**
* Version decode information.
*
* @var array
*/
protected static $versionDecodeInfo = array(
0x07c94, 0x085bc, 0x09a99, 0x0a4d3, 0x0bbf6, 0x0c762, 0x0d847, 0x0e60d,
0x0f928, 0x10b78, 0x1145d, 0x12a17, 0x13532, 0x149a6, 0x15683, 0x168c9,
0x177ec, 0x18ec4, 0x191e1, 0x1afab, 0x1b08e, 0x1cc1a, 0x1d33f, 0x1ed75,
0x1f250, 0x209d5, 0x216f0, 0x228ba, 0x2379f, 0x24b0b, 0x2542e, 0x26a64,
0x27541, 0x28c69
);
/**
* Cached version instances.
*
* @var array
*/
protected static $versions = array();
/**
* Version number of this version.
*
* @var integer
*/
protected $versionNumber;
/**
* Alignment pattern centers.
*
* @var SplFixedArray
*/
protected $alignmentPatternCenters;
/**
* Error correction blocks.
*
* @var SplFixedArray
*/
protected $errorCorrectionBlocks;
/**
* Total number of codewords.
*
* @var integer
*/
protected $totalCodewords;
/**
* Creates a new version.
*
* @param integer $versionNumber
* @param SplFixedArray $alignmentPatternCenters
* @param SplFixedArray $ecBlocks
*/
protected function __construct(
$versionNumber,
SplFixedArray $alignmentPatternCenters,
SplFixedArray $ecBlocks
) {
$this->versionNumber = $versionNumber;
$this->alignmentPatternCenters = $alignmentPatternCenters;
$this->errorCorrectionBlocks = $ecBlocks;
$totalCodewords = 0;
$ecCodewords = $ecBlocks[0]->getEcCodewordsPerBlock();
foreach ($ecBlocks[0]->getEcBlocks() as $ecBlock) {
$totalCodewords += $ecBlock->getCount() * ($ecBlock->getDataCodewords() + $ecCodewords);
}
$this->totalCodewords = $totalCodewords;
}
/**
* Gets the version number.
*
* @return integer
*/
public function getVersionNumber()
{
return $this->versionNumber;
}
/**
* Gets the alignment pattern centers.
*
* @return SplFixedArray
*/
public function getAlignmentPatternCenters()
{
return $this->alignmentPatternCenters;
}
/**
* Gets the total number of codewords.
*
* @return integer
*/
public function getTotalCodewords()
{
return $this->totalCodewords;
}
/**
* Gets the dimension for the current version.
*
* @return integer
*/
public function getDimensionForVersion()
{
return 17 + 4 * $this->versionNumber;
}
/**
* Gets the number of EC blocks for a specific EC level.
*
* @param ErrorCorrectionLevel $ecLevel
* @return integer
*/
public function getEcBlocksForLevel(ErrorCorrectionLevel $ecLevel)
{
return $this->errorCorrectionBlocks[$ecLevel->getOrdinal()];
}
/**
* Gets a provisional version number for a specific dimension.
*
* @param integer $dimension
* @return Version
* @throws Exception\InvalidArgumentException
*/
public static function getProvisionalVersionForDimension($dimension)
{
if ($dimension % 4 !== 1) {
throw new Exception\InvalidArgumentException('Dimension is not 1 mod 4');
}
return self::getVersionForNumber(($dimension - 17) >> 2);
}
/**
* Gets a version instance for a specific version number.
*
* @param integer $versionNumber
* @return Version
* @throws Exception\InvalidArgumentException
*/
public static function getVersionForNumber($versionNumber)
{
if ($versionNumber < 1 || $versionNumber > 40) {
throw new Exception\InvalidArgumentException('Version number must be between 1 and 40');
}
if (!isset(self::$versions[$versionNumber])) {
self::buildVersion($versionNumber);
}
return self::$versions[$versionNumber - 1];
}
/**
* Decodes version information from an integer and returns the version.
*
* @param integer $versionBits
* @return Version|null
*/
public static function decodeVersionInformation($versionBits)
{
$bestDifference = PHP_INT_MAX;
$bestVersion = 0;
foreach (self::$versionDecodeInfo as $i => $targetVersion) {
if ($targetVersion === $versionBits) {
return self::getVersionForNumber($i + 7);
}
$bitsDifference = FormatInformation::numBitsDiffering($versionBits, $targetVersion);
if ($bitsDifference < $bestDifference) {
$bestVersion = $i + 7;
$bestDifference = $bitsDifference;
}
}
if ($bestDifference <= 3) {
return self::getVersionForNumber($bestVersion);
}
return null;
}
/**
* Builds the function pattern for the current version.
*
* @return BitMatrix
*/
public function buildFunctionPattern()
{
$dimension = $this->getDimensionForVersion();
$bitMatrix = new BitMatrix($dimension);
// Top left finder pattern + separator + format
$bitMatrix->setRegion(0, 0, 9, 9);
// Top right finder pattern + separator + format
$bitMatrix->setRegion($dimension - 8, 0, 8, 9);
// Bottom left finder pattern + separator + format
$bitMatrix->setRegion(0, $dimension - 8, 9, 8);
// Alignment patterns
$max = count($this->alignmentPatternCenters);
for ($x = 0; $x < $max; $x++) {
$i = $this->alignmentPatternCenters[$x] - 2;
for ($y = 0; $y < $max; $y++) {
if (($x === 0 && ($y === 0 || $y === $max - 1)) || ($x === $max - 1 && $y === 0)) {
// No alignment patterns near the three finder paterns
continue;
}
$bitMatrix->setRegion($this->alignmentPatternCenters[$y] - 2, $i, 5, 5);
}
}
// Vertical timing pattern
$bitMatrix->setRegion(6, 9, 1, $dimension - 17);
// Horizontal timing pattern
$bitMatrix->setRegion(9, 6, $dimension - 17, 1);
if ($this->versionNumber > 6) {
// Version info, top right
$bitMatrix->setRegion($dimension - 11, 0, 3, 6);
// Version info, bottom left
$bitMatrix->setRegion(0, $dimension - 11, 6, 3);
}
return $bitMatrix;
}
/**
* Returns a string representation for the version.
*
* @return string
*/
public function __toString()
{
return (string) $this->versionNumber;
}
/**
* Build and cache a specific version.
*
* See ISO 18004:2006 6.5.1 Table 9.
*
* @param integer $versionNumber
* @return void
*/
protected static function buildVersion($versionNumber)
{
switch ($versionNumber) {
case 1:
$patterns = array();
$ecBlocks = array(
new EcBlocks(7, new EcBlock(1, 19)),
new EcBlocks(10, new EcBlock(1, 16)),
new EcBlocks(13, new EcBlock(1, 13)),
new EcBlocks(17, new EcBlock(1, 9)),
);
break;
case 2:
$patterns = array(6, 18);
$ecBlocks = array(
new EcBlocks(10, new EcBlock(1, 34)),
new EcBlocks(16, new EcBlock(1, 28)),
new EcBlocks(22, new EcBlock(1, 22)),
new EcBlocks(28, new EcBlock(1, 16)),
);
break;
case 3:
$patterns = array(6, 22);
$ecBlocks = array(
new EcBlocks(15, new EcBlock(1, 55)),
new EcBlocks(26, new EcBlock(1, 44)),
new EcBlocks(18, new EcBlock(2, 17)),
new EcBlocks(22, new EcBlock(2, 13)),
);
break;
case 4:
$patterns = array(6, 26);
$ecBlocks = array(
new EcBlocks(20, new EcBlock(1, 80)),
new EcBlocks(18, new EcBlock(2, 32)),
new EcBlocks(26, new EcBlock(3, 24)),
new EcBlocks(16, new EcBlock(4, 9)),
);
break;
case 5:
$patterns = array(6, 30);
$ecBlocks = array(
new EcBlocks(26, new EcBlock(1, 108)),
new EcBlocks(24, new EcBlock(2, 43)),
new EcBlocks(18, new EcBlock(2, 15), new EcBlock(2, 16)),
new EcBlocks(22, new EcBlock(2, 11), new EcBlock(2, 12)),
);
break;
case 6:
$patterns = array(6, 34);
$ecBlocks = array(
new EcBlocks(18, new EcBlock(2, 68)),
new EcBlocks(16, new EcBlock(4, 27)),
new EcBlocks(24, new EcBlock(4, 19)),
new EcBlocks(28, new EcBlock(4, 15)),
);
break;
case 7:
$patterns = array(6, 22, 38);
$ecBlocks = array(
new EcBlocks(20, new EcBlock(2, 78)),
new EcBlocks(18, new EcBlock(4, 31)),
new EcBlocks(18, new EcBlock(2, 14), new EcBlock(4, 15)),
new EcBlocks(26, new EcBlock(4, 13), new EcBlock(1, 14)),
);
break;
case 8:
$patterns = array(6, 24, 42);
$ecBlocks = array(
new EcBlocks(24, new EcBlock(2, 97)),
new EcBlocks(22, new EcBlock(2, 38), new EcBlock(2, 39)),
new EcBlocks(22, new EcBlock(4, 18), new EcBlock(2, 19)),
new EcBlocks(26, new EcBlock(4, 14), new EcBlock(2, 15)),
);
break;
case 9:
$patterns = array(6, 26, 46);
$ecBlocks = array(
new EcBlocks(30, new EcBlock(2, 116)),
new EcBlocks(22, new EcBlock(3, 36), new EcBlock(2, 37)),
new EcBlocks(20, new EcBlock(4, 16), new EcBlock(4, 17)),
new EcBlocks(24, new EcBlock(4, 12), new EcBlock(4, 13)),
);
break;
case 10:
$patterns = array(6, 28, 50);
$ecBlocks = array(
new EcBlocks(18, new EcBlock(2, 68), new EcBlock(2, 69)),
new EcBlocks(26, new EcBlock(4, 43), new EcBlock(1, 44)),
new EcBlocks(24, new EcBlock(6, 19), new EcBlock(2, 20)),
new EcBlocks(28, new EcBlock(6, 15), new EcBlock(2, 16)),
);
break;
case 11:
$patterns = array(6, 30, 54);
$ecBlocks = array(
new EcBlocks(20, new EcBlock(4, 81)),
new EcBlocks(30, new EcBlock(1, 50), new EcBlock(4, 51)),
new EcBlocks(28, new EcBlock(4, 22), new EcBlock(4, 23)),
new EcBlocks(24, new EcBlock(3, 12), new EcBlock(8, 13)),
);
break;
case 12:
$patterns = array(6, 32, 58);
$ecBlocks = array(
new EcBlocks(24, new EcBlock(2, 92), new EcBlock(2, 93)),
new EcBlocks(22, new EcBlock(6, 36), new EcBlock(2, 37)),
new EcBlocks(26, new EcBlock(4, 20), new EcBlock(6, 21)),
new EcBlocks(28, new EcBlock(7, 14), new EcBlock(4, 15)),
);
break;
case 13:
$patterns = array(6, 34, 62);
$ecBlocks = array(
new EcBlocks(26, new EcBlock(4, 107)),
new EcBlocks(22, new EcBlock(8, 37), new EcBlock(1, 38)),
new EcBlocks(24, new EcBlock(8, 20), new EcBlock(4, 21)),
new EcBlocks(22, new EcBlock(12, 11), new EcBlock(4, 12)),
);
break;
case 14:
$patterns = array(6, 26, 46, 66);
$ecBlocks = array(
new EcBlocks(30, new EcBlock(3, 115), new EcBlock(1, 116)),
new EcBlocks(24, new EcBlock(4, 40), new EcBlock(5, 41)),
new EcBlocks(20, new EcBlock(11, 16), new EcBlock(5, 17)),
new EcBlocks(24, new EcBlock(11, 12), new EcBlock(5, 13)),
);
break;
case 15:
$patterns = array(6, 26, 48, 70);
$ecBlocks = array(
new EcBlocks(22, new EcBlock(5, 87), new EcBlock(1, 88)),
new EcBlocks(24, new EcBlock(5, 41), new EcBlock(5, 42)),
new EcBlocks(30, new EcBlock(5, 24), new EcBlock(7, 25)),
new EcBlocks(24, new EcBlock(11, 12), new EcBlock(7, 13)),
);
break;
case 16:
$patterns = array(6, 26, 50, 74);
$ecBlocks = array(
new EcBlocks(24, new EcBlock(5, 98), new EcBlock(1, 99)),
new EcBlocks(28, new EcBlock(7, 45), new EcBlock(3, 46)),
new EcBlocks(24, new EcBlock(15, 19), new EcBlock(2, 20)),
new EcBlocks(30, new EcBlock(3, 15), new EcBlock(13, 16)),
);
break;
case 17:
$patterns = array(6, 30, 54, 78);
$ecBlocks = array(
new EcBlocks(28, new EcBlock(1, 107), new EcBlock(5, 108)),
new EcBlocks(28, new EcBlock(10, 46), new EcBlock(1, 47)),
new EcBlocks(28, new EcBlock(1, 22), new EcBlock(15, 23)),
new EcBlocks(28, new EcBlock(2, 14), new EcBlock(17, 15)),
);
break;
case 18:
$patterns = array(6, 30, 56, 82);
$ecBlocks = array(
new EcBlocks(30, new EcBlock(5, 120), new EcBlock(1, 121)),
new EcBlocks(26, new EcBlock(9, 43), new EcBlock(4, 44)),
new EcBlocks(28, new EcBlock(17, 22), new EcBlock(1, 23)),
new EcBlocks(28, new EcBlock(2, 14), new EcBlock(19, 15)),
);
break;
case 19:
$patterns = array(6, 30, 58, 86);
$ecBlocks = array(
new EcBlocks(28, new EcBlock(3, 113), new EcBlock(4, 114)),
new EcBlocks(26, new EcBlock(3, 44), new EcBlock(11, 45)),
new EcBlocks(26, new EcBlock(17, 21), new EcBlock(4, 22)),
new EcBlocks(26, new EcBlock(9, 13), new EcBlock(16, 14)),
);
break;
case 20:
$patterns = array(6, 34, 62, 90);
$ecBlocks = array(
new EcBlocks(28, new EcBlock(3, 107), new EcBlock(5, 108)),
new EcBlocks(26, new EcBlock(3, 41), new EcBlock(13, 42)),
new EcBlocks(30, new EcBlock(15, 24), new EcBlock(5, 25)),
new EcBlocks(28, new EcBlock(15, 15), new EcBlock(10, 16)),
);
break;
case 21:
$patterns = array(6, 28, 50, 72, 94);
$ecBlocks = array(
new EcBlocks(28, new EcBlock(4, 116), new EcBlock(4, 117)),
new EcBlocks(26, new EcBlock(17, 42)),
new EcBlocks(28, new EcBlock(17, 22), new EcBlock(6, 23)),
new EcBlocks(30, new EcBlock(19, 16), new EcBlock(6, 17)),
);
break;
case 22:
$patterns = array(6, 26, 50, 74, 98);
$ecBlocks = array(
new EcBlocks(28, new EcBlock(2, 111), new EcBlock(7, 112)),
new EcBlocks(28, new EcBlock(17, 46)),
new EcBlocks(30, new EcBlock(7, 24), new EcBlock(16, 25)),
new EcBlocks(24, new EcBlock(34, 13)),
);
break;
case 23:
$patterns = array(6, 30, 54, 78, 102);
$ecBlocks = array(
new EcBlocks(30, new EcBlock(4, 121), new EcBlock(5, 122)),
new EcBlocks(28, new EcBlock(4, 47), new EcBlock(14, 48)),
new EcBlocks(30, new EcBlock(11, 24), new EcBlock(14, 25)),
new EcBlocks(30, new EcBlock(16, 15), new EcBlock(14, 16)),
);
break;
case 24:
$patterns = array(6, 28, 54, 80, 106);
$ecBlocks = array(
new EcBlocks(30, new EcBlock(6, 117), new EcBlock(4, 118)),
new EcBlocks(28, new EcBlock(6, 45), new EcBlock(14, 46)),
new EcBlocks(30, new EcBlock(11, 24), new EcBlock(16, 25)),
new EcBlocks(30, new EcBlock(30, 16), new EcBlock(2, 17)),
);
break;
case 25:
$patterns = array(6, 32, 58, 84, 110);
$ecBlocks = array(
new EcBlocks(26, new EcBlock(8, 106), new EcBlock(4, 107)),
new EcBlocks(28, new EcBlock(8, 47), new EcBlock(13, 48)),
new EcBlocks(30, new EcBlock(7, 24), new EcBlock(22, 25)),
new EcBlocks(30, new EcBlock(22, 15), new EcBlock(13, 16)),
);
break;
case 26:
$patterns = array(6, 30, 58, 86, 114);
$ecBlocks = array(
new EcBlocks(28, new EcBlock(10, 114), new EcBlock(2, 115)),
new EcBlocks(28, new EcBlock(19, 46), new EcBlock(4, 47)),
new EcBlocks(28, new EcBlock(28, 22), new EcBlock(6, 23)),
new EcBlocks(30, new EcBlock(33, 16), new EcBlock(4, 17)),
);
break;
case 27:
$patterns = array(6, 34, 62, 90, 118);
$ecBlocks = array(
new EcBlocks(30, new EcBlock(8, 122), new EcBlock(4, 123)),
new EcBlocks(28, new EcBlock(22, 45), new EcBlock(3, 46)),
new EcBlocks(30, new EcBlock(8, 23), new EcBlock(26, 24)),
new EcBlocks(30, new EcBlock(12, 15), new EcBlock(28, 16)),
);
break;
case 28:
$patterns = array(6, 26, 50, 74, 98, 122);
$ecBlocks = array(
new EcBlocks(30, new EcBlock(3, 117), new EcBlock(10, 118)),
new EcBlocks(28, new EcBlock(3, 45), new EcBlock(23, 46)),
new EcBlocks(30, new EcBlock(4, 24), new EcBlock(31, 25)),
new EcBlocks(30, new EcBlock(11, 15), new EcBlock(31, 16)),
);
break;
case 29:
$patterns = array(6, 30, 54, 78, 102, 126);
$ecBlocks = array(
new EcBlocks(30, new EcBlock(7, 116), new EcBlock(7, 117)),
new EcBlocks(28, new EcBlock(21, 45), new EcBlock(7, 46)),
new EcBlocks(30, new EcBlock(1, 23), new EcBlock(37, 24)),
new EcBlocks(30, new EcBlock(19, 15), new EcBlock(26, 16)),
);
break;
case 30:
$patterns = array(6, 26, 52, 78, 104, 130);
$ecBlocks = array(
new EcBlocks(30, new EcBlock(5, 115), new EcBlock(10, 116)),
new EcBlocks(28, new EcBlock(19, 47), new EcBlock(10, 48)),
new EcBlocks(30, new EcBlock(15, 24), new EcBlock(25, 25)),
new EcBlocks(30, new EcBlock(23, 15), new EcBlock(25, 16)),
);
break;
case 31:
$patterns = array(6, 30, 56, 82, 108, 134);
$ecBlocks = array(
new EcBlocks(30, new EcBlock(13, 115), new EcBlock(3, 116)),
new EcBlocks(28, new EcBlock(2, 46), new EcBlock(29, 47)),
new EcBlocks(30, new EcBlock(42, 24), new EcBlock(1, 25)),
new EcBlocks(30, new EcBlock(23, 15), new EcBlock(28, 16)),
);
break;
case 32:
$patterns = array(6, 34, 60, 86, 112, 138);
$ecBlocks = array(
new EcBlocks(30, new EcBlock(17, 115)),
new EcBlocks(28, new EcBlock(10, 46), new EcBlock(23, 47)),
new EcBlocks(30, new EcBlock(10, 24), new EcBlock(35, 25)),
new EcBlocks(30, new EcBlock(19, 15), new EcBlock(35, 16)),
);
break;
case 33:
$patterns = array(6, 30, 58, 86, 114, 142);
$ecBlocks = array(
new EcBlocks(30, new EcBlock(17, 115), new EcBlock(1, 116)),
new EcBlocks(28, new EcBlock(14, 46), new EcBlock(21, 47)),
new EcBlocks(30, new EcBlock(29, 24), new EcBlock(19, 25)),
new EcBlocks(30, new EcBlock(11, 15), new EcBlock(46, 16)),
);
break;
case 34:
$patterns = array(6, 34, 62, 90, 118, 146);
$ecBlocks = array(
new EcBlocks(30, new EcBlock(13, 115), new EcBlock(6, 116)),
new EcBlocks(28, new EcBlock(14, 46), new EcBlock(23, 47)),
new EcBlocks(30, new EcBlock(44, 24), new EcBlock(7, 25)),
new EcBlocks(30, new EcBlock(59, 16), new EcBlock(1, 17)),
);
break;
case 35:
$patterns = array(6, 30, 54, 78, 102, 126, 150);
$ecBlocks = array(
new EcBlocks(30, new EcBlock(12, 121), new EcBlock(7, 122)),
new EcBlocks(28, new EcBlock(12, 47), new EcBlock(26, 48)),
new EcBlocks(30, new EcBlock(39, 24), new EcBlock(14, 25)),
new EcBlocks(30, new EcBlock(22, 15), new EcBlock(41, 16)),
);
break;
case 36:
$patterns = array(6, 24, 50, 76, 102, 128, 154);
$ecBlocks = array(
new EcBlocks(30, new EcBlock(6, 121), new EcBlock(14, 122)),
new EcBlocks(28, new EcBlock(6, 47), new EcBlock(34, 48)),
new EcBlocks(30, new EcBlock(46, 24), new EcBlock(10, 25)),
new EcBlocks(30, new EcBlock(2, 15), new EcBlock(64, 16)),
);
break;
case 37:
$patterns = array(6, 28, 54, 80, 106, 132, 158);
$ecBlocks = array(
new EcBlocks(30, new EcBlock(17, 122), new EcBlock(4, 123)),
new EcBlocks(28, new EcBlock(29, 46), new EcBlock(14, 47)),
new EcBlocks(30, new EcBlock(49, 24), new EcBlock(10, 25)),
new EcBlocks(30, new EcBlock(24, 15), new EcBlock(46, 16)),
);
break;
case 38:
$patterns = array(6, 32, 58, 84, 110, 136, 162);
$ecBlocks = array(
new EcBlocks(30, new EcBlock(4, 122), new EcBlock(18, 123)),
new EcBlocks(28, new EcBlock(13, 46), new EcBlock(32, 47)),
new EcBlocks(30, new EcBlock(48, 24), new EcBlock(14, 25)),
new EcBlocks(30, new EcBlock(42, 15), new EcBlock(32, 16)),
);
break;
case 39:
$patterns = array(6, 26, 54, 82, 110, 138, 166);
$ecBlocks = array(
new EcBlocks(30, new EcBlock(20, 117), new EcBlock(4, 118)),
new EcBlocks(28, new EcBlock(40, 47), new EcBlock(7, 48)),
new EcBlocks(30, new EcBlock(43, 24), new EcBlock(22, 25)),
new EcBlocks(30, new EcBlock(10, 15), new EcBlock(67, 16)),
);
break;
case 40:
$patterns = array(6, 30, 58, 86, 114, 142, 170);
$ecBlocks = array(
new EcBlocks(30, new EcBlock(19, 118), new EcBlock(6, 119)),
new EcBlocks(28, new EcBlock(18, 47), new EcBlock(31, 48)),
new EcBlocks(30, new EcBlock(34, 24), new EcBlock(34, 25)),
new EcBlocks(30, new EcBlock(20, 15), new EcBlock(61, 16)),
);
break;
}
self::$versions[$versionNumber - 1] = new self(
$versionNumber,
SplFixedArray::fromArray($patterns, false),
SplFixedArray::fromArray($ecBlocks, false)
);
}
}
@@ -0,0 +1,64 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Encoder;
use SplFixedArray;
/**
* Block pair.
*/
class BlockPair
{
/**
* Data bytes in the block.
*
* @var SplFixedArray
*/
protected $dataBytes;
/**
* Error correction bytes in the block.
*
* @var SplFixedArray
*/
protected $errorCorrectionBytes;
/**
* Creates a new block pair.
*
* @param SplFixedArray $data
* @param SplFixedArray $errorCorrection
*/
public function __construct(SplFixedArray $data, SplFixedArray $errorCorrection)
{
$this->dataBytes = $data;
$this->errorCorrectionBytes = $errorCorrection;
}
/**
* Gets the data bytes.
*
* @return SplFixedArray
*/
public function getDataBytes()
{
return $this->dataBytes;
}
/**
* Gets the error correction bytes.
*
* @return SplFixedArray
*/
public function getErrorCorrectionBytes()
{
return $this->errorCorrectionBytes;
}
}
@@ -0,0 +1,158 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Encoder;
use SplFixedArray;
/**
* Byte matrix.
*/
class ByteMatrix
{
/**
* Bytes in the matrix, represented as array.
*
* @var SplFixedArray
*/
protected $bytes;
/**
* Width of the matrix.
*
* @var integer
*/
protected $width;
/**
* Height of the matrix.
*
* @var integer
*/
protected $height;
/**
* Creates a new byte matrix.
*
* @param integer $width
* @param integer $height
*/
public function __construct($width, $height)
{
$this->height = $height;
$this->width = $width;
$this->bytes = new SplFixedArray($height);
for ($y = 0; $y < $height; $y++) {
$this->bytes[$y] = new SplFixedArray($width);
}
}
/**
* Gets the width of the matrix.
*
* @return integer
*/
public function getWidth()
{
return $this->width;
}
/**
* Gets the height of the matrix.
*
* @return integer
*/
public function getHeight()
{
return $this->height;
}
/**
* Gets the internal representation of the matrix.
*
* @return SplFixedArray
*/
public function getArray()
{
return $this->bytes;
}
/**
* Gets the byte for a specific position.
*
* @param integer $x
* @param integer $y
* @return integer
*/
public function get($x, $y)
{
return $this->bytes[$y][$x];
}
/**
* Sets the byte for a specific position.
*
* @param integer $x
* @param integer $y
* @param integer $value
* @return void
*/
public function set($x, $y, $value)
{
$this->bytes[$y][$x] = (int) $value;
}
/**
* Clears the matrix with a specific value.
*
* @param integer $value
* @return void
*/
public function clear($value)
{
for ($y = 0; $y < $this->height; $y++) {
for ($x = 0; $x < $this->width; $x++) {
$this->bytes[$y][$x] = $value;
}
}
}
/**
* Returns a string representation of the matrix.
*
* @return string
*/
public function __toString()
{
$result = '';
for ($y = 0; $y < $this->height; $y++) {
for ($x = 0; $x < $this->width; $x++) {
switch ($this->bytes[$y][$x]) {
case 0:
$result .= ' 0';
break;
case 1:
$result .= ' 1';
break;
default:
$result .= ' ';
break;
}
}
$result .= "\n";
}
return $result;
}
}
@@ -0,0 +1,687 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Encoder;
use BaconQrCode\Common\BitArray;
use BaconQrCode\Common\CharacterSetEci;
use BaconQrCode\Common\ErrorCorrectionLevel;
use BaconQrCode\Common\Mode;
use BaconQrCode\Common\ReedSolomonCodec;
use BaconQrCode\Common\Version;
use BaconQrCode\Exception;
use SplFixedArray;
/**
* Encoder.
*/
class Encoder
{
/**
* Default byte encoding.
*/
const DEFAULT_BYTE_MODE_ECODING = 'ISO-8859-1';
/**
* The original table is defined in the table 5 of JISX0510:2004 (p.19).
*
* @var array
*/
protected static $alphanumericTable = array(
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0x00-0x0f
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0x10-0x1f
36, -1, -1, -1, 37, 38, -1, -1, -1, -1, 39, 40, -1, 41, 42, 43, // 0x20-0x2f
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 44, -1, -1, -1, -1, -1, // 0x30-0x3f
-1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, // 0x40-0x4f
25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1, // 0x50-0x5f
);
/**
* Codec cache.
*
* @var array
*/
protected static $codecs = array();
/**
* Encodes "content" with the error correction level "ecLevel".
*
* @param string $content
* @param ErrorCorrectionLevel $ecLevel
* @param ? $hints
* @return QrCode
*/
public static function encode($content, ErrorCorrectionLevel $ecLevel, $encoding = self::DEFAULT_BYTE_MODE_ECODING)
{
// Pick an encoding mode appropriate for the content. Note that this
// will not attempt to use multiple modes / segments even if that were
// more efficient. Would be nice.
$mode = self::chooseMode($content, $encoding);
// This will store the header information, like mode and length, as well
// as "header" segments like an ECI segment.
$headerBits = new BitArray();
// Append ECI segment if applicable
if ($mode->get() === Mode::BYTE && $encoding !== self::DEFAULT_BYTE_MODE_ECODING) {
$eci = CharacterSetEci::getCharacterSetEciByName($encoding);
if ($eci !== null) {
self::appendEci($eci, $headerBits);
}
}
// (With ECI in place,) Write the mode marker
self::appendModeInfo($mode, $headerBits);
// Collect data within the main segment, separately, to count its size
// if needed. Don't add it to main payload yet.
$dataBits = new BitArray();
self::appendBytes($content, $mode, $dataBits, $encoding);
// Hard part: need to know version to know how many bits length takes.
// But need to know how many bits it takes to know version. First we
// take a guess at version by assuming version will be the minimum, 1:
$provisionalBitsNeeded = $headerBits->getSize()
+ $mode->getCharacterCountBits(Version::getVersionForNumber(1))
+ $dataBits->getSize();
$provisionalVersion = self::chooseVersion($provisionalBitsNeeded, $ecLevel);
// Use that guess to calculate the right version. I am still not sure
// this works in 100% of cases.
$bitsNeeded = $headerBits->getSize()
+ $mode->getCharacterCountBits($provisionalVersion)
+ $dataBits->getSize();
$version = self::chooseVersion($bitsNeeded, $ecLevel);
$headerAndDataBits = new BitArray();
$headerAndDataBits->appendBitArray($headerBits);
// Find "length" of main segment and write it.
$numLetters = ($mode->get() === Mode::BYTE ? $dataBits->getSizeInBytes() : strlen($content));
self::appendLengthInfo($numLetters, $version, $mode, $headerAndDataBits);
// Put data together into the overall payload.
$headerAndDataBits->appendBitArray($dataBits);
$ecBlocks = $version->getEcBlocksForLevel($ecLevel);
$numDataBytes = $version->getTotalCodewords() - $ecBlocks->getTotalEcCodewords();
// Terminate the bits properly.
self::terminateBits($numDataBytes, $headerAndDataBits);
// Interleave data bits with error correction code.
$finalBits = self::interleaveWithEcBytes(
$headerAndDataBits,
$version->getTotalCodewords(),
$numDataBytes,
$ecBlocks->getNumBlocks()
);
$qrCode = new QrCode();
$qrCode->setErrorCorrectionLevel($ecLevel);
$qrCode->setMode($mode);
$qrCode->setVersion($version);
// Choose the mask pattern and set to "qrCode".
$dimension = $version->getDimensionForVersion();
$matrix = new ByteMatrix($dimension, $dimension);
$maskPattern = self::chooseMaskPattern($finalBits, $ecLevel, $version, $matrix);
$qrCode->setMaskPattern($maskPattern);
// Build the matrix and set it to "qrCode".
MatrixUtil::buildMatrix($finalBits, $ecLevel, $version, $maskPattern, $matrix);
$qrCode->setMatrix($matrix);
return $qrCode;
}
/**
* Gets the alphanumeric code for a byte.
*
* @param string|integer $code
* @return integer
*/
protected static function getAlphanumericCode($code)
{
$code = (is_string($code) ? ord($code) : $code);
if (isset(self::$alphanumericTable[$code])) {
return self::$alphanumericTable[$code];
}
return -1;
}
/**
* Chooses the best mode for a given content.
*
* @param string $content
* @param string $encoding
* @return Mode
*/
protected static function chooseMode($content, $encoding = null)
{
if (strcasecmp($encoding, 'SHIFT-JIS') === 0) {
return self::isOnlyDoubleByteKanji($content) ? new Mode(Mode::KANJI) : new Mode(Mode::BYTE);
}
$hasNumeric = false;
$hasAlphanumeric = false;
$contentLength = strlen($content);
for ($i = 0; $i < $contentLength; $i++) {
$char = $content[$i];
if (ctype_digit($char)) {
$hasNumeric = true;
} elseif (self::getAlphanumericCode($char) !== -1) {
$hasAlphanumeric = true;
} else {
return new Mode(Mode::BYTE);
}
}
if ($hasAlphanumeric) {
return new Mode(Mode::ALPHANUMERIC);
} elseif ($hasNumeric) {
return new Mode(Mode::NUMERIC);
}
return new Mode(Mode::BYTE);
}
/**
* Calculates the mask penalty for a matrix.
*
* @param ByteMatrix $matrix
* @return integer
*/
protected static function calculateMaskPenalty(ByteMatrix $matrix)
{
return (
MaskUtil::applyMaskPenaltyRule1($matrix)
+ MaskUtil::applyMaskPenaltyRule2($matrix)
+ MaskUtil::applyMaskPenaltyRule3($matrix)
+ MaskUtil::applyMaskPenaltyRule4($matrix)
);
}
/**
* Chooses the best mask pattern for a matrix.
*
* @param BitArray $bits
* @param ErrorCorrectionLevel $ecLevel
* @param Version $version
* @param ByteMatrix $matrix
* @return integer
*/
protected static function chooseMaskPattern(
BitArray $bits,
ErrorCorrectionLevel $ecLevel,
Version $version,
ByteMatrix $matrix
) {
$minPenality = PHP_INT_MAX;
$bestMaskPattern = -1;
for ($maskPattern = 0; $maskPattern < QrCode::NUM_MASK_PATTERNS; $maskPattern++) {
MatrixUtil::buildMatrix($bits, $ecLevel, $version, $maskPattern, $matrix);
$penalty = self::calculateMaskPenalty($matrix);
if ($penalty < $minPenality) {
$minPenality = $penalty;
$bestMaskPattern = $maskPattern;
}
}
return $bestMaskPattern;
}
/**
* Chooses the best version for the input.
*
* @param integer $numInputBits
* @param ErrorCorrectionLevel $ecLevel
* @return Version
* @throws Exception\WriterException
*/
protected static function chooseVersion($numInputBits, ErrorCorrectionLevel $ecLevel)
{
for ($versionNum = 1; $versionNum <= 40; $versionNum++) {
$version = Version::getVersionForNumber($versionNum);
$numBytes = $version->getTotalCodewords();
$ecBlocks = $version->getEcBlocksForLevel($ecLevel);
$numEcBytes = $ecBlocks->getTotalEcCodewords();
$numDataBytes = $numBytes - $numEcBytes;
$totalInputBytes = intval(($numInputBits + 8) / 8);
if ($numDataBytes >= $totalInputBytes) {
return $version;
}
}
throw new Exception\WriterException('Data too big');
}
/**
* Terminates the bits in a bit array.
*
* @param integer $numDataBytes
* @param BitArray $bits
* @throws Exception\WriterException
*/
protected static function terminateBits($numDataBytes, BitArray $bits)
{
$capacity = $numDataBytes << 3;
if ($bits->getSize() > $capacity) {
throw new Exception\WriterException('Data bits cannot fit in the QR code');
}
for ($i = 0; $i < 4 && $bits->getSize() < $capacity; $i++) {
$bits->appendBit(false);
}
$numBitsInLastByte = $bits->getSize() & 0x7;
if ($numBitsInLastByte > 0) {
for ($i = $numBitsInLastByte; $i < 8; $i++) {
$bits->appendBit(false);
}
}
$numPaddingBytes = $numDataBytes - $bits->getSizeInBytes();
for ($i = 0; $i < $numPaddingBytes; $i++) {
$bits->appendBits(($i & 0x1) === 0 ? 0xec : 0x11, 8);
}
if ($bits->getSize() !== $capacity) {
throw new Exception\WriterException('Bits size does not equal capacity');
}
}
/**
* Gets number of data- and EC bytes for a block ID.
*
* @param integer $numTotalBytes
* @param integer $numDataBytes
* @param integer $numRsBlocks
* @param integer $blockId
* @return array
* @throws Exception\WriterException
*/
protected static function getNumDataBytesAndNumEcBytesForBlockId(
$numTotalBytes,
$numDataBytes,
$numRsBlocks,
$blockId
) {
if ($blockId >= $numRsBlocks) {
throw new Exception\WriterException('Block ID too large');
}
$numRsBlocksInGroup2 = $numTotalBytes % $numRsBlocks;
$numRsBlocksInGroup1 = $numRsBlocks - $numRsBlocksInGroup2;
$numTotalBytesInGroup1 = intval($numTotalBytes / $numRsBlocks);
$numTotalBytesInGroup2 = $numTotalBytesInGroup1 + 1;
$numDataBytesInGroup1 = intval($numDataBytes / $numRsBlocks);
$numDataBytesInGroup2 = $numDataBytesInGroup1 + 1;
$numEcBytesInGroup1 = $numTotalBytesInGroup1 - $numDataBytesInGroup1;
$numEcBytesInGroup2 = $numTotalBytesInGroup2 - $numDataBytesInGroup2;
if ($numEcBytesInGroup1 !== $numEcBytesInGroup2) {
throw new Exception\WriterException('EC bytes mismatch');
}
if ($numRsBlocks !== $numRsBlocksInGroup1 + $numRsBlocksInGroup2) {
throw new Exception\WriterException('RS blocks mismatch');
}
if ($numTotalBytes !==
(($numDataBytesInGroup1 + $numEcBytesInGroup1) * $numRsBlocksInGroup1)
+ (($numDataBytesInGroup2 + $numEcBytesInGroup2) * $numRsBlocksInGroup2)
) {
throw new Exception\WriterException('Total bytes mismatch');
}
if ($blockId < $numRsBlocksInGroup1) {
return array($numDataBytesInGroup1, $numEcBytesInGroup1);
} else {
return array($numDataBytesInGroup2, $numEcBytesInGroup2);
}
}
/**
* Interleaves data with EC bytes.
*
* @param BitArray $bits
* @param integer $numTotalBytes
* @param integer $numDataBytes
* @param integer $numRsBlocks
* @return BitArray
* @throws Exception\WriterException
*/
protected static function interleaveWithEcBytes(BitArray $bits, $numTotalBytes, $numDataBytes, $numRsBlocks)
{
if ($bits->getSizeInBytes() !== $numDataBytes) {
throw new Exception\WriterException('Number of bits and data bytes does not match');
}
$dataBytesOffset = 0;
$maxNumDataBytes = 0;
$maxNumEcBytes = 0;
$blocks = new SplFixedArray($numRsBlocks);
for ($i = 0; $i < $numRsBlocks; $i++) {
list($numDataBytesInBlock, $numEcBytesInBlock) = self::getNumDataBytesAndNumEcBytesForBlockId(
$numTotalBytes,
$numDataBytes,
$numRsBlocks,
$i
);
$size = $numDataBytesInBlock;
$dataBytes = $bits->toBytes(8 * $dataBytesOffset, $size);
$ecBytes = self::generateEcBytes($dataBytes, $numEcBytesInBlock);
$blocks[$i] = new BlockPair($dataBytes, $ecBytes);
$maxNumDataBytes = max($maxNumDataBytes, $size);
$maxNumEcBytes = max($maxNumEcBytes, count($ecBytes));
$dataBytesOffset += $numDataBytesInBlock;
}
if ($numDataBytes !== $dataBytesOffset) {
throw new Exception\WriterException('Data bytes does not match offset');
}
$result = new BitArray();
for ($i = 0; $i < $maxNumDataBytes; $i++) {
foreach ($blocks as $block) {
$dataBytes = $block->getDataBytes();
if ($i < count($dataBytes)) {
$result->appendBits($dataBytes[$i], 8);
}
}
}
for ($i = 0; $i < $maxNumEcBytes; $i++) {
foreach ($blocks as $block) {
$ecBytes = $block->getErrorCorrectionBytes();
if ($i < count($ecBytes)) {
$result->appendBits($ecBytes[$i], 8);
}
}
}
if ($numTotalBytes !== $result->getSizeInBytes()) {
throw new Exception\WriterException('Interleaving error: ' . $numTotalBytes . ' and ' . $result->getSizeInBytes() . ' differ');
}
return $result;
}
/**
* Generates EC bytes for given data.
*
* @param SplFixedArray $dataBytes
* @param integer $numEcBytesInBlock
* @return SplFixedArray
*/
protected static function generateEcBytes(SplFixedArray $dataBytes, $numEcBytesInBlock)
{
$numDataBytes = count($dataBytes);
$toEncode = new SplFixedArray($numDataBytes + $numEcBytesInBlock);
for ($i = 0; $i < $numDataBytes; $i++) {
$toEncode[$i] = $dataBytes[$i] & 0xff;
}
$ecBytes = new SplFixedArray($numEcBytesInBlock);
$codec = self::getCodec($numDataBytes, $numEcBytesInBlock);
$codec->encode($toEncode, $ecBytes);
return $ecBytes;
}
/**
* Gets an RS codec and caches it.
*
* @param integer $numDataBytes
* @param integer $numEcBytesInBlock
* @return ReedSolomonCodec
*/
protected static function getCodec($numDataBytes, $numEcBytesInBlock)
{
$cacheId = $numDataBytes . '-' . $numEcBytesInBlock;
if (!isset(self::$codecs[$cacheId])) {
self::$codecs[$cacheId] = new ReedSolomonCodec(
8,
0x11d,
0,
1,
$numEcBytesInBlock,
255 - $numDataBytes - $numEcBytesInBlock
);
}
return self::$codecs[$cacheId];
}
/**
* Appends mode information to a bit array.
*
* @param Mode $mode
* @param BitArray $bits
* @return void
*/
protected static function appendModeInfo(Mode $mode, BitArray $bits)
{
$bits->appendBits($mode->get(), 4);
}
/**
* Appends length information to a bit array.
*
* @param integer $numLetters
* @param Version $version
* @param Mode $mode
* @param BitArray $bits
* @return void
* @throws Exception\WriterException
*/
protected static function appendLengthInfo($numLetters, Version $version, Mode $mode, BitArray $bits)
{
$numBits = $mode->getCharacterCountBits($version);
if ($numLetters >= (1 << $numBits)) {
throw new Exception\WriterException($numLetters . ' is bigger than ' . ((1 << $numBits) - 1));
}
$bits->appendBits($numLetters, $numBits);
}
/**
* Appends bytes to a bit array in a specific mode.
*
* @param stirng $content
* @param Mode $mode
* @param BitArray $bits
* @param string $encoding
* @return void
* @throws Exception\WriterException
*/
protected static function appendBytes($content, Mode $mode, BitArray $bits, $encoding)
{
switch ($mode->get()) {
case Mode::NUMERIC:
self::appendNumericBytes($content, $bits);
break;
case Mode::ALPHANUMERIC:
self::appendAlphanumericBytes($content, $bits);
break;
case Mode::BYTE:
self::append8BitBytes($content, $bits, $encoding);
break;
case Mode::KANJI:
self::appendKanjiBytes($content, $bits);
break;
default:
throw new Exception\WriterException('Invalid mode: ' . $mode->get());
}
}
/**
* Appends numeric bytes to a bit array.
*
* @param string $content
* @param BitArray $bits
* @return void
*/
protected static function appendNumericBytes($content, BitArray $bits)
{
$length = strlen($content);
$i = 0;
while ($i < $length) {
$num1 = (int) $content[$i];
if ($i + 2 < $length) {
// Encode three numeric letters in ten bits.
$num2 = (int) $content[$i + 1];
$num3 = (int) $content[$i + 2];
$bits->appendBits($num1 * 100 + $num2 * 10 + $num3, 10);
$i += 3;
} elseif ($i + 1 < $length) {
// Encode two numeric letters in seven bits.
$num2 = (int) $content[$i + 1];
$bits->appendBits($num1 * 10 + $num2, 7);
$i += 2;
} else {
// Encode one numeric letter in four bits.
$bits->appendBits($num1, 4);
$i++;
}
}
}
/**
* Appends alpha-numeric bytes to a bit array.
*
* @param string $content
* @param BitArray $bits
* @return void
*/
protected static function appendAlphanumericBytes($content, BitArray $bits)
{
$length = strlen($content);
$i = 0;
while ($i < $length) {
if (-1 === ($code1 = self::getAlphanumericCode($content[$i]))) {
throw new Exception\WriterException('Invalid alphanumeric code');
}
if ($i + 1 < $length) {
if (-1 === ($code2 = self::getAlphanumericCode($content[$i + 1]))) {
throw new Exception\WriterException('Invalid alphanumeric code');
}
// Encode two alphanumeric letters in 11 bits.
$bits->appendBits($code1 * 45 + $code2, 11);
$i += 2;
} else {
// Encode one alphanumeric letter in six bits.
$bits->appendBits($code1, 6);
$i++;
}
}
}
/**
* Appends regular 8-bit bytes to a bit array.
*
* @param string $content
* @param BitArray $bits
* @return void
*/
protected static function append8BitBytes($content, BitArray $bits, $encoding)
{
if (false === ($bytes = @iconv('utf-8', $encoding, $content))) {
throw new Exception\WriterException('Could not encode content to ' . $encoding);
}
$length = strlen($bytes);
for ($i = 0; $i < $length; $i++) {
$bits->appendBits(ord($bytes[$i]), 8);
}
}
/**
* Appends KANJI bytes to a bit array.
*
* @param string $content
* @param BitArray $bits
* @return void
*/
protected static function appendKanjiBytes($content, BitArray $bits)
{
if (strlen($content) % 2 > 0) {
// We just do a simple length check here. The for loop will check
// individual characters.
throw new Exception\WriterException('Content does not seem to be encoded in SHIFT-JIS');
}
$length = strlen($content);
for ($i = 0; $i < $length; $i += 2) {
$byte1 = ord($content[$i]) & 0xff;
$byte2 = ord($content[$i + 1]) & 0xff;
$code = ($byte1 << 8) | $byte2;
if ($code >= 0x8140 && $code <= 0x9ffc) {
$subtracted = $code - 0x8140;
} elseif ($code >= 0xe040 && $code <= 0xebbf) {
$subtracted = $code - 0xc140;
} else {
throw new Exception\WriterException('Invalid byte sequence');
}
$encoded = (($subtracted >> 8) * 0xc0) + ($subtracted & 0xff);
$bits->appendBits($encoded, 13);
}
}
/**
* Appends ECI information to a bit array.
*
* @param CharacterSetEci $eci
* @param BitArray $bits
* @return void
*/
protected static function appendEci(CharacterSetEci $eci, BitArray $bits)
{
$mode = new Mode(Mode::ECI);
$bits->appendBits($mode->get(), 4);
$bits->appendBits($eci->get(), 8);
}
}
@@ -0,0 +1,291 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Encoder;
use BaconQrCode\Common\BitUtils;
/**
* Mask utility.
*/
class MaskUtil
{
/**#@+
* Penalty weights from section 6.8.2.1
*/
const N1 = 3;
const N2 = 3;
const N3 = 40;
const N4 = 10;
/**#@-*/
/**
* Applies mask penalty rule 1 and returns the penalty.
*
* Finds repetitive cells with the same color and gives penalty to them.
* Example: 00000 or 11111.
*
* @param ByteMatrix $matrix
* @return integer
*/
public static function applyMaskPenaltyRule1(ByteMatrix $matrix)
{
return (
self::applyMaskPenaltyRule1Internal($matrix, true)
+ self::applyMaskPenaltyRule1Internal($matrix, false)
);
}
/**
* Applies mask penalty rule 2 and returns the penalty.
*
* Finds 2x2 blocks with the same color and gives penalty to them. This is
* actually equivalent to the spec's rule, which is to find MxN blocks and
* give a penalty proportional to (M-1)x(N-1), because this is the number of
* 2x2 blocks inside such a block.
*
* @param ByteMatrix $matrix
* @return integer
*/
public static function applyMaskPenaltyRule2(ByteMatrix $matrix)
{
$penalty = 0;
$array = $matrix->getArray();
$width = $matrix->getWidth();
$height = $matrix->getHeight();
for ($y = 0; $y < $height - 1; $y++) {
for ($x = 0; $x < $width - 1; $x++) {
$value = $array[$y][$x];
if ($value === $array[$y][$x + 1] && $value === $array[$y + 1][$x] && $value === $array[$y + 1][$x + 1]) {
$penalty++;
}
}
}
return self::N2 * $penalty;
}
/**
* Applies mask penalty rule 3 and returns the penalty.
*
* Finds consecutive cells of 00001011101 or 10111010000, and gives penalty
* to them. If we find patterns like 000010111010000, we give penalties
* twice (i.e. 40 * 2).
*
* @param ByteMatrix $matrix
* @return integer
*/
public static function applyMaskPenaltyRule3(ByteMatrix $matrix)
{
$penalty = 0;
$array = $matrix->getArray();
$width = $matrix->getWidth();
$height = $matrix->getHeight();
for ($y = 0; $y < $height; $y++) {
for ($x = 0; $x < $width; $x++) {
if (
$x + 6 < $width
&& $array[$y][$x] === 1
&& $array[$y][$x + 1] === 0
&& $array[$y][$x + 2] === 1
&& $array[$y][$x + 3] === 1
&& $array[$y][$x + 4] === 1
&& $array[$y][$x + 5] === 0
&& $array[$y][$x + 6] === 1
&& (
(
$x + 10 < $width
&& $array[$y][$x + 7] === 0
&& $array[$y][$x + 8] === 0
&& $array[$y][$x + 9] === 0
&& $array[$y][$x + 10] === 0
)
|| (
$x - 4 >= 0
&& $array[$y][$x - 1] === 0
&& $array[$y][$x - 2] === 0
&& $array[$y][$x - 3] === 0
&& $array[$y][$x - 4] === 0
)
)
) {
$penalty += self::N3;
}
if (
$y + 6 < $height
&& $array[$y][$x] === 1
&& $array[$y + 1][$x] === 0
&& $array[$y + 2][$x] === 1
&& $array[$y + 3][$x] === 1
&& $array[$y + 4][$x] === 1
&& $array[$y + 5][$x] === 0
&& $array[$y + 6][$x] === 1
&& (
(
$y + 10 < $height
&& $array[$y + 7][$x] === 0
&& $array[$y + 8][$x] === 0
&& $array[$y + 9][$x] === 0
&& $array[$y + 10][$x] === 0
)
|| (
$y - 4 >= 0
&& $array[$y - 1][$x] === 0
&& $array[$y - 2][$x] === 0
&& $array[$y - 3][$x] === 0
&& $array[$y - 4][$x] === 0
)
)
) {
$penalty += self::N3;
}
}
}
return $penalty;
}
/**
* Applies mask penalty rule 4 and returns the penalty.
*
* Calculates the ratio of dark cells and gives penalty if the ratio is far
* from 50%. It gives 10 penalty for 5% distance.
*
* @param ByteMatrix $matrix
* @return integer
*/
public static function applyMaskPenaltyRule4(ByteMatrix $matrix)
{
$numDarkCells = 0;
$array = $matrix->getArray();
$width = $matrix->getWidth();
$height = $matrix->getHeight();
for ($y = 0; $y < $height; $y++) {
$arrayY = $array[$y];
for ($x = 0; $x < $width; $x++) {
if ($arrayY[$x] === 1) {
$numDarkCells++;
}
}
}
$numTotalCells = $height * $width;
$darkRatio = $numDarkCells / $numTotalCells;
$fixedPercentVariances = (int) (abs($darkRatio - 0.5) * 20);
return $fixedPercentVariances * self::N4;
}
/**
* Returns the mask bit for "getMaskPattern" at "x" and "y".
*
* See 8.8 of JISX0510:2004 for mask pattern conditions.
*
* @param integer $maskPattern
* @param integer $x
* @param integer $y
* @return integer
* @throws Exception\InvalidArgumentException
*/
public static function getDataMaskBit($maskPattern, $x, $y)
{
switch ($maskPattern) {
case 0:
$intermediate = ($y + $x) & 0x1;
break;
case 1:
$intermediate = $y & 0x1;
break;
case 2:
$intermediate = $x % 3;
break;
case 3:
$intermediate = ($y + $x) % 3;
break;
case 4:
$intermediate = (BitUtils::unsignedRightShift($y, 1) + ($x / 3)) & 0x1;
break;
case 5:
$temp = $y * $x;
$intermediate = ($temp & 0x1) + ($temp % 3);
break;
case 6:
$temp = $y * $x;
$intermediate = (($temp & 0x1) + ($temp % 3)) & 0x1;
break;
case 7:
$temp = $y * $x;
$intermediate = (($temp % 3) + (($y + $x) & 0x1)) & 0x1;
break;
default:
throw new Exception\InvalidArgumentException('Invalid mask pattern: ' . $maskPattern);
}
return $intermediate === 0;
}
/**
* Helper function for applyMaskPenaltyRule1.
*
* We need this for doing this calculation in both vertical and horizontal
* orders respectively.
*
* @param ByteMatrix $matrix
* @param boolean $isHorizontal
* @return integer
*/
protected static function applyMaskPenaltyRule1Internal(ByteMatrix $matrix, $isHorizontal)
{
$penalty = 0;
$iLimit = $isHorizontal ? $matrix->getHeight() : $matrix->getWidth();
$jLimit = $isHorizontal ? $matrix->getWidth() : $matrix->getHeight();
$array = $matrix->getArray();
for ($i = 0; $i < $iLimit; $i++) {
$numSameBitCells = 0;
$prevBit = -1;
for ($j = 0; $j < $jLimit; $j++) {
$bit = $isHorizontal ? $array[$i][$j] : $array[$j][$i];
if ($bit === $prevBit) {
$numSameBitCells++;
} else {
if ($numSameBitCells >= 5) {
$penalty += self::N1 + ($numSameBitCells - 5);
}
$numSameBitCells = 1;
$prevBit = $bit;
}
}
if ($numSameBitCells >= 5) {
$penalty += self::N1 + ($numSameBitCells - 5);
}
}
return $penalty;
}
}
@@ -0,0 +1,580 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Encoder;
use BaconQrCode\Common\BitArray;
use BaconQrCode\Common\ErrorCorrectionLevel;
use BaconQrCode\Common\Version;
use BaconQrCode\Exception;
/**
* Matrix utility.
*/
class MatrixUtil
{
/**
* Position detection pattern.
*
* @var array
*/
protected static $positionDetectionPattern = array(
array(1, 1, 1, 1, 1, 1, 1),
array(1, 0, 0, 0, 0, 0, 1),
array(1, 0, 1, 1, 1, 0, 1),
array(1, 0, 1, 1, 1, 0, 1),
array(1, 0, 1, 1, 1, 0, 1),
array(1, 0, 0, 0, 0, 0, 1),
array(1, 1, 1, 1, 1, 1, 1),
);
/**
* Position adjustment pattern.
*
* @var array
*/
protected static $positionAdjustmentPattern = array(
array(1, 1, 1, 1, 1),
array(1, 0, 0, 0, 1),
array(1, 0, 1, 0, 1),
array(1, 0, 0, 0, 1),
array(1, 1, 1, 1, 1),
);
/**
* Coordinates for position adjustment patterns for each version.
*
* @var array
*/
protected static $positionAdjustmentPatternCoordinateTable = array(
array(null, null, null, null, null, null, null), // Version 1
array( 6, 18, null, null, null, null, null), // Version 2
array( 6, 22, null, null, null, null, null), // Version 3
array( 6, 26, null, null, null, null, null), // Version 4
array( 6, 30, null, null, null, null, null), // Version 5
array( 6, 34, null, null, null, null, null), // Version 6
array( 6, 22, 38, null, null, null, null), // Version 7
array( 6, 24, 42, null, null, null, null), // Version 8
array( 6, 26, 46, null, null, null, null), // Version 9
array( 6, 28, 50, null, null, null, null), // Version 10
array( 6, 30, 54, null, null, null, null), // Version 11
array( 6, 32, 58, null, null, null, null), // Version 12
array( 6, 34, 62, null, null, null, null), // Version 13
array( 6, 26, 46, 66, null, null, null), // Version 14
array( 6, 26, 48, 70, null, null, null), // Version 15
array( 6, 26, 50, 74, null, null, null), // Version 16
array( 6, 30, 54, 78, null, null, null), // Version 17
array( 6, 30, 56, 82, null, null, null), // Version 18
array( 6, 30, 58, 86, null, null, null), // Version 19
array( 6, 34, 62, 90, null, null, null), // Version 20
array( 6, 28, 50, 72, 94, null, null), // Version 21
array( 6, 26, 50, 74, 98, null, null), // Version 22
array( 6, 30, 54, 78, 102, null, null), // Version 23
array( 6, 28, 54, 80, 106, null, null), // Version 24
array( 6, 32, 58, 84, 110, null, null), // Version 25
array( 6, 30, 58, 86, 114, null, null), // Version 26
array( 6, 34, 62, 90, 118, null, null), // Version 27
array( 6, 26, 50, 74, 98, 122, null), // Version 28
array( 6, 30, 54, 78, 102, 126, null), // Version 29
array( 6, 26, 52, 78, 104, 130, null), // Version 30
array( 6, 30, 56, 82, 108, 134, null), // Version 31
array( 6, 34, 60, 86, 112, 138, null), // Version 32
array( 6, 30, 58, 86, 114, 142, null), // Version 33
array( 6, 34, 62, 90, 118, 146, null), // Version 34
array( 6, 30, 54, 78, 102, 126, 150), // Version 35
array( 6, 24, 50, 76, 102, 128, 154), // Version 36
array( 6, 28, 54, 80, 106, 132, 158), // Version 37
array( 6, 32, 58, 84, 110, 136, 162), // Version 38
array( 6, 26, 54, 82, 110, 138, 166), // Version 39
array( 6, 30, 58, 86, 114, 142, 170), // Version 40
);
/**
* Type information coordinates.
*
* @var array
*/
protected static $typeInfoCoordinates = array(
array(8, 0),
array(8, 1),
array(8, 2),
array(8, 3),
array(8, 4),
array(8, 5),
array(8, 7),
array(8, 8),
array(7, 8),
array(5, 8),
array(4, 8),
array(3, 8),
array(2, 8),
array(1, 8),
array(0, 8),
);
/**
* Version information polynomial.
*
* @var integer
*/
protected static $versionInfoPoly = 0x1f25;
/**
* Type information polynomial.
*
* @var integer
*/
protected static $typeInfoPoly = 0x537;
/**
* Type information mask pattern.
*
* @var integer
*/
protected static $typeInfoMaskPattern = 0x5412;
/**
* Clears a given matrix.
*
* @param ByteMatrix $matrix
* @return void
*/
public static function clearMatrix(ByteMatrix $matrix)
{
$matrix->clear(-1);
}
/**
* Builds a complete matrix.
*
* @param BitArray $dataBits
* @param ErrorCorrectionLevel $level
* @param Version $version
* @param integer $maskPattern
* @param ByteMatrix $matrix
* @return void
*/
public static function buildMatrix(
BitArray $dataBits,
ErrorCorrectionLevel $level,
Version $version,
$maskPattern,
ByteMatrix $matrix
) {
self::clearMatrix($matrix);
self::embedBasicPatterns($version, $matrix);
self::embedTypeInfo($level, $maskPattern, $matrix);
self::maybeEmbedVersionInfo($version, $matrix);
self::embedDataBits($dataBits, $maskPattern, $matrix);
}
/**
* Embeds type information into a matrix.
*
* @param ErrorCorrectionLevel $level
* @param integer $maskPattern
* @param ByteMatrix $matrix
* @return void
*/
protected static function embedTypeInfo(ErrorCorrectionLevel $level, $maskPattern, ByteMatrix $matrix)
{
$typeInfoBits = new BitArray();
self::makeTypeInfoBits($level, $maskPattern, $typeInfoBits);
$typeInfoBitsSize = $typeInfoBits->getSize();
for ($i = 0; $i < $typeInfoBitsSize; $i++) {
$bit = $typeInfoBits->get($typeInfoBitsSize - 1 - $i);
$x1 = self::$typeInfoCoordinates[$i][0];
$y1 = self::$typeInfoCoordinates[$i][1];
$matrix->set($x1, $y1, $bit);
if ($i < 8) {
$x2 = $matrix->getWidth() - $i - 1;
$y2 = 8;
} else {
$x2 = 8;
$y2 = $matrix->getHeight() - 7 + ($i - 8);
}
$matrix->set($x2, $y2, $bit);
}
}
/**
* Generates type information bits and appends them to a bit array.
*
* @param ErrorCorrectionLevel $level
* @param integer $maskPattern
* @param BitArray $bits
* @return void
* @throws Exception\RuntimeException
*/
protected static function makeTypeInfoBits(ErrorCorrectionLevel $level, $maskPattern, BitArray $bits)
{
$typeInfo = ($level->get() << 3) | $maskPattern;
$bits->appendBits($typeInfo, 5);
$bchCode = self::calculateBchCode($typeInfo, self::$typeInfoPoly);
$bits->appendBits($bchCode, 10);
$maskBits = new BitArray();
$maskBits->appendBits(self::$typeInfoMaskPattern, 15);
$bits->xorBits($maskBits);
if ($bits->getSize() !== 15) {
throw new Exception\RuntimeException('Bit array resulted in invalid size: ' . $bits->getSize());
}
}
/**
* Embeds version information if required.
*
* @param Version $version
* @param ByteMatrix $matrix
* @return void
*/
protected static function maybeEmbedVersionInfo(Version $version, ByteMatrix $matrix)
{
if ($version->getVersionNumber() < 7) {
return;
}
$versionInfoBits = new BitArray();
self::makeVersionInfoBits($version, $versionInfoBits);
$bitIndex = 6 * 3 - 1;
for ($i = 0; $i < 6; $i++) {
for ($j = 0; $j < 3; $j++) {
$bit = $versionInfoBits->get($bitIndex);
$bitIndex--;
$matrix->set($i, $matrix->getHeight() - 11 + $j, $bit);
$matrix->set($matrix->getHeight() - 11 + $j, $i, $bit);
}
}
}
/**
* Generates version information bits and appends them to a bit array.
*
* @param Version $version
* @param BitArray $bits
* @return void
* @throws Exception\RuntimeException
*/
protected static function makeVersionInfoBits(Version $version, BitArray $bits)
{
$bits->appendBits($version->getVersionNumber(), 6);
$bchCode = self::calculateBchCode($version->getVersionNumber(), self::$versionInfoPoly);
$bits->appendBits($bchCode, 12);
if ($bits->getSize() !== 18) {
throw new Exception\RuntimeException('Bit array resulted in invalid size: ' . $bits->getSize());
}
}
/**
* Calculates the BHC code for a value and a polynomial.
*
* @param integer $value
* @param integer $poly
* @return integer
*/
protected static function calculateBchCode($value, $poly)
{
$msbSetInPoly = self::findMsbSet($poly);
$value <<= $msbSetInPoly - 1;
while (self::findMsbSet($value) >= $msbSetInPoly) {
$value ^= $poly << (self::findMsbSet($value) - $msbSetInPoly);
}
return $value;
}
/**
* Finds and MSB set.
*
* @param integer $value
* @return integer
*/
protected static function findMsbSet($value)
{
$numDigits = 0;
while ($value !== 0) {
$value >>= 1;
$numDigits++;
}
return $numDigits;
}
/**
* Embeds basic patterns into a matrix.
*
* @param Version $version
* @param ByteMatrix $matrix
* @return void
*/
protected static function embedBasicPatterns(Version $version, ByteMatrix $matrix)
{
self::embedPositionDetectionPatternsAndSeparators($matrix);
self::embedDarkDotAtLeftBottomCorner($matrix);
self::maybeEmbedPositionAdjustmentPatterns($version, $matrix);
self::embedTimingPatterns($matrix);
}
/**
* Embeds position detection patterns and separators into a byte matrix.
*
* @param ByteMatrix $matrix
* @return void
*/
protected static function embedPositionDetectionPatternsAndSeparators(ByteMatrix $matrix)
{
$pdpWidth = count(self::$positionDetectionPattern[0]);
self::embedPositionDetectionPattern(0, 0, $matrix);
self::embedPositionDetectionPattern($matrix->getWidth() - $pdpWidth, 0, $matrix);
self::embedPositionDetectionPattern(0, $matrix->getWidth() - $pdpWidth, $matrix);
$hspWidth = 8;
self::embedHorizontalSeparationPattern(0, $hspWidth - 1, $matrix);
self::embedHorizontalSeparationPattern($matrix->getWidth() - $hspWidth, $hspWidth - 1, $matrix);
self::embedHorizontalSeparationPattern(0, $matrix->getWidth() - $hspWidth, $matrix);
$vspSize = 7;
self::embedVerticalSeparationPattern($vspSize, 0, $matrix);
self::embedVerticalSeparationPattern($matrix->getHeight() - $vspSize - 1, 0, $matrix);
self::embedVerticalSeparationPattern($vspSize, $matrix->getHeight() - $vspSize, $matrix);
}
/**
* Embeds a single position detection pattern into a byte matrix.
*
* @param integer $xStart
* @param integer $yStart
* @param ByteMatrix $matrix
* @return void
*/
protected static function embedPositionDetectionPattern($xStart, $yStart, ByteMatrix $matrix)
{
for ($y = 0; $y < 7; $y++) {
for ($x = 0; $x < 7; $x++) {
$matrix->set($xStart + $x, $yStart + $y, self::$positionDetectionPattern[$y][$x]);
}
}
}
/**
* Embeds a single horizontal separation pattern.
*
* @param integer $xStart
* @param integer $yStart
* @param ByteMatrix $matrix
* @return void
* @throws Exception\RuntimeException
*/
protected static function embedHorizontalSeparationPattern($xStart, $yStart, ByteMatrix $matrix)
{
for ($x = 0; $x < 8; $x++) {
if ($matrix->get($xStart + $x, $yStart) !== -1) {
throw new Exception\RuntimeException('Byte already set');
}
$matrix->set($xStart + $x, $yStart, 0);
}
}
/**
* Embeds a single vertical separation pattern.
*
* @param integer $xStart
* @param integer $yStart
* @param ByteMatrix $matrix
* @return void
* @throws Exception\RuntimeException
*/
protected static function embedVerticalSeparationPattern($xStart, $yStart, ByteMatrix $matrix)
{
for ($y = 0; $y < 7; $y++) {
if ($matrix->get($xStart, $yStart + $y) !== -1) {
throw new Exception\RuntimeException('Byte already set');
}
$matrix->set($xStart, $yStart + $y, 0);
}
}
/**
* Embeds a dot at the left bottom corner.
*
* @param ByteMatrix $matrix
* @return void
* @throws Exception\RuntimeException
*/
protected static function embedDarkDotAtLeftBottomCorner(ByteMatrix $matrix)
{
if ($matrix->get(8, $matrix->getHeight() - 8) === 0) {
throw new Exception\RuntimeException('Byte already set to 0');
}
$matrix->set(8, $matrix->getHeight() - 8, 1);
}
/**
* Embeds position adjustment patterns if required.
*
* @param Version $version
* @param ByteMatrix $matrix
* @return void
*/
protected static function maybeEmbedPositionAdjustmentPatterns(Version $version, ByteMatrix $matrix)
{
if ($version->getVersionNumber() < 2) {
return;
}
$index = $version->getVersionNumber() - 1;
$coordinates = self::$positionAdjustmentPatternCoordinateTable[$index];
$numCoordinates = count($coordinates);
for ($i = 0; $i < $numCoordinates; $i++) {
for ($j = 0; $j < $numCoordinates; $j++) {
$y = $coordinates[$i];
$x = $coordinates[$j];
if ($x === null || $y === null) {
continue;
}
if ($matrix->get($x, $y) === -1) {
self::embedPositionAdjustmentPattern($x - 2, $y - 2, $matrix);
}
}
}
}
/**
* Embeds a single position adjustment pattern.
*
* @param integer $xStart
* @param intger $yStart
* @param ByteMatrix $matrix
* @return void
*/
protected static function embedPositionAdjustmentPattern($xStart, $yStart, ByteMatrix $matrix)
{
for ($y = 0; $y < 5; $y++) {
for ($x = 0; $x < 5; $x++) {
$matrix->set($xStart + $x, $yStart + $y, self::$positionAdjustmentPattern[$y][$x]);
}
}
}
/**
* Embeds timing patterns into a matrix.
*
* @param ByteMatrix $matrix
* @return void
*/
protected static function embedTimingPatterns(ByteMatrix $matrix)
{
$matrixWidth = $matrix->getWidth();
for ($i = 8; $i < $matrixWidth - 8; $i++) {
$bit = ($i + 1) % 2;
if ($matrix->get($i, 6) === -1) {
$matrix->set($i, 6, $bit);
}
if ($matrix->get(6, $i) === -1) {
$matrix->set(6, $i, $bit);
}
}
}
/**
* Embeds "dataBits" using "getMaskPattern".
*
* For debugging purposes, it skips masking process if "getMaskPattern" is
* -1. See 8.7 of JISX0510:2004 (p.38) for how to embed data bits.
*
* @param BitArray $dataBits
* @param integer $maskPattern
* @param ByteMatrix $matrix
* @return void
* @throws Exception\WriterException
*/
protected static function embedDataBits(BitArray $dataBits, $maskPattern, ByteMatrix $matrix)
{
$bitIndex = 0;
$direction = -1;
// Start from the right bottom cell.
$x = $matrix->getWidth() - 1;
$y = $matrix->getHeight() - 1;
while ($x > 0) {
// Skip vertical timing pattern.
if ($x === 6) {
$x--;
}
while ($y >= 0 && $y < $matrix->getHeight()) {
for ($i = 0; $i < 2; $i++) {
$xx = $x - $i;
// Skip the cell if it's not empty.
if ($matrix->get($xx, $y) !== -1) {
continue;
}
if ($bitIndex < $dataBits->getSize()) {
$bit = $dataBits->get($bitIndex);
$bitIndex++;
} else {
// Padding bit. If there is no bit left, we'll fill the
// left cells with 0, as described in 8.4.9 of
// JISX0510:2004 (p. 24).
$bit = false;
}
// Skip masking if maskPattern is -1.
if ($maskPattern !== -1 && MaskUtil::getDataMaskBit($maskPattern, $xx, $y)) {
$bit = !$bit;
}
$matrix->set($xx, $y, $bit);
}
$y += $direction;
}
$direction = -$direction;
$y += $direction;
$x -= 2;
}
// All bits should be consumed
if ($bitIndex !== $dataBits->getSize()) {
throw new Exception\WriterException('Not all bits consumed (' . $bitIndex . ' out of ' . $dataBits->getSize() .')');
}
}
}
@@ -0,0 +1,201 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Encoder;
use BaconQrCode\Common\ErrorCorrectionLevel;
use BaconQrCode\Common\Mode;
use BaconQrCode\Common\Version;
/**
* QR code.
*/
class QrCode
{
/**
* Number of possible mask patterns.
*/
const NUM_MASK_PATTERNS = 8;
/**
* Mode of the QR code.
*
* @var Mode
*/
protected $mode;
/**
* EC level of the QR code.
*
* @var ErrorCorrectionLevel
*/
protected $errorCorrectionLevel;
/**
* Version of the QR code.
*
* @var Version
*/
protected $version;
/**
* Mask pattern of the QR code.
*
* @var integer
*/
protected $maskPattern = -1;
/**
* Matrix of the QR code.
*
* @var ByteMatrix
*/
protected $matrix;
/**
* Gets the mode.
*
* @return Mode
*/
public function getMode()
{
return $this->mode;
}
/**
* Sets the mode.
*
* @param Mode $mode
* @return void
*/
public function setMode(Mode $mode)
{
$this->mode = $mode;
}
/**
* Gets the EC level.
*
* @return ErrorCorrectionLevel
*/
public function getErrorCorrectionLevel()
{
return $this->errorCorrectionLevel;
}
/**
* Sets the EC level.
*
* @param ErrorCorrectionLevel $errorCorrectionLevel
* @return void
*/
public function setErrorCorrectionLevel(ErrorCorrectionLevel $errorCorrectionLevel)
{
$this->errorCorrectionLevel = $errorCorrectionLevel;
}
/**
* Gets the version.
*
* @return Version
*/
public function getVersion()
{
return $this->version;
}
/**
* Sets the version.
*
* @param Version $version
* @return void
*/
public function setVersion(Version $version)
{
$this->version = $version;
}
/**
* Gets the mask pattern.
*
* @return integer
*/
public function getMaskPattern()
{
return $this->maskPattern;
}
/**
* Sets the mask pattern.
*
* @param integer $maskPattern
* @return void
*/
public function setMaskPattern($maskPattern)
{
$this->maskPattern = $maskPattern;
}
/**
* Gets the matrix.
*
* @return ByteMatrix
*/
public function getMatrix()
{
return $this->matrix;
}
/**
* Sets the matrix.
*
* @param ByteMatrix $matrix
* @return void
*/
public function setMatrix(ByteMatrix $matrix)
{
$this->matrix = $matrix;
}
/**
* Validates whether a mask pattern is valid.
*
* @param integer $maskPattern
* @return boolean
*/
public static function isValidMaskPattern($maskPattern)
{
return $maskPattern > 0 && $maskPattern < self::NUM_MASK_PATTERNS;
}
/**
* Returns a string representation of the QR code.
*
* @return string
*/
public function __toString()
{
$result = "<<\n"
. " mode: " . $this->mode . "\n"
. " ecLevel: " . $this->errorCorrectionLevel . "\n"
. " version: " . $this->version . "\n"
. " maskPattern: " . $this->maskPattern . "\n";
if ($this->matrix === null) {
$result .= " matrix: null\n";
} else {
$result .= " matrix:\n";
$result .= $this->matrix;
}
$result .= ">>\n";
return $result;
}
}
@@ -0,0 +1,14 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Exception;
interface ExceptionInterface
{
}
@@ -0,0 +1,14 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Exception;
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}
@@ -0,0 +1,14 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Exception;
class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface
{
}
@@ -0,0 +1,14 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Exception;
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}
@@ -0,0 +1,14 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Exception;
class UnexpectedValueException extends \UnexpectedValueException implements ExceptionInterface
{
}
@@ -0,0 +1,14 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Exception;
class WriterException extends \RuntimeException implements ExceptionInterface
{
}
@@ -0,0 +1,160 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Renderer\Color;
use BaconQrCode\Exception;
/**
* CMYK color.
*/
class Cmyk implements ColorInterface
{
/**
* Cyan value.
*
* @var integer
*/
protected $cyan;
/**
* Magenta value.
*
* @var integer
*/
protected $magenta;
/**
* Yellow value.
*
* @var integer
*/
protected $yellow;
/**
* Black value.
*
* @var integer
*/
protected $black;
/**
* Creates a new CMYK color.
*
* @param integer $cyan
* @param integer $magenta
* @param integer $yellow
* @param integer $black
*/
public function __construct($cyan, $magenta, $yellow, $black)
{
if ($cyan < 0 || $cyan > 100) {
throw new Exception\InvalidArgumentException('Cyan must be between 0 and 100');
}
if ($magenta < 0 || $magenta > 100) {
throw new Exception\InvalidArgumentException('Magenta must be between 0 and 100');
}
if ($yellow < 0 || $yellow > 100) {
throw new Exception\InvalidArgumentException('Yellow must be between 0 and 100');
}
if ($black < 0 || $black > 100) {
throw new Exception\InvalidArgumentException('Black must be between 0 and 100');
}
$this->cyan = (int) $cyan;
$this->magenta = (int) $magenta;
$this->yellow = (int) $yellow;
$this->black = (int) $black;
}
/**
* Returns the cyan value.
*
* @return integer
*/
public function getCyan()
{
return $this->cyan;
}
/**
* Returns the magenta value.
*
* @return integer
*/
public function getMagenta()
{
return $this->magenta;
}
/**
* Returns the yellow value.
*
* @return integer
*/
public function getYellow()
{
return $this->yellow;
}
/**
* Returns the black value.
*
* @return integer
*/
public function getBlack()
{
return $this->black;
}
/**
* toRgb(): defined by ColorInterface.
*
* @see ColorInterface::toRgb()
* @return Rgb
*/
public function toRgb()
{
$k = $this->black / 100;
$c = (-$k * $this->cyan + $k * 100 + $this->cyan) / 100;
$m = (-$k * $this->magenta + $k * 100 + $this->magenta) / 100;
$y = (-$k * $this->yellow + $k * 100 + $this->yellow) / 100;
return new Rgb(
-$c * 255 + 255,
-$m * 255 + 255,
-$y * 255 + 255
);
}
/**
* toCmyk(): defined by ColorInterface.
*
* @see ColorInterface::toCmyk()
* @return Cmyk
*/
public function toCmyk()
{
return $this;
}
/**
* toGray(): defined by ColorInterface.
*
* @see ColorInterface::toGray()
* @return Gray
*/
public function toGray()
{
return $this->toRgb()->toGray();
}
}
@@ -0,0 +1,37 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Renderer\Color;
/**
* Color interface.
*/
interface ColorInterface
{
/**
* Converts the color to RGB.
*
* @return Rgb
*/
public function toRgb();
/**
* Converts the color to CMYK.
*
* @return Cmyk
*/
public function toCmyk();
/**
* Converts the color to gray.
*
* @return Gray
*/
public function toGray();
}
@@ -0,0 +1,84 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Renderer\Color;
use BaconQrCode\Exception;
/**
* Gray color.
*/
class Gray implements ColorInterface
{
/**
* Gray value.
*
* @var integer
*/
protected $gray;
/**
* Creates a new gray color.
*
* A low gray value means black, while a high value means white.
*
* @param integer $gray
*/
public function __construct($gray)
{
if ($gray < 0 || $gray > 100) {
throw new Exception\InvalidArgumentException('Gray must be between 0 and 100');
}
$this->gray = (int) $gray;
}
/**
* Returns the gray value.
*
* @return integer
*/
public function getGray()
{
return $this->gray;
}
/**
* toRgb(): defined by ColorInterface.
*
* @see ColorInterface::toRgb()
* @return Rgb
*/
public function toRgb()
{
return new Rgb($this->gray * 2.55, $this->gray * 2.55, $this->gray * 2.55);
}
/**
* toCmyk(): defined by ColorInterface.
*
* @see ColorInterface::toCmyk()
* @return Cmyk
*/
public function toCmyk()
{
return new Cmyk(0, 0, 0, 100 - $this->gray);
}
/**
* toGray(): defined by ColorInterface.
*
* @see ColorInterface::toGray()
* @return Gray
*/
public function toGray()
{
return $this;
}
}
@@ -0,0 +1,148 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Renderer\Color;
use BaconQrCode\Exception;
/**
* RGB color.
*/
class Rgb implements ColorInterface
{
/**
* Red value.
*
* @var integer
*/
protected $red;
/**
* Green value.
*
* @var integer
*/
protected $green;
/**
* Blue value.
*
* @var integer
*/
protected $blue;
/**
* Creates a new RGB color.
*
* @param integer $red
* @param integer $green
* @param integer $blue
*/
public function __construct($red, $green, $blue)
{
if ($red < 0 || $red > 255) {
throw new Exception\InvalidArgumentException('Red must be between 0 and 255');
}
if ($green < 0 || $green > 255) {
throw new Exception\InvalidArgumentException('Green must be between 0 and 255');
}
if ($blue < 0 || $blue > 255) {
throw new Exception\InvalidArgumentException('Blue must be between 0 and 255');
}
$this->red = (int) $red;
$this->green = (int) $green;
$this->blue = (int) $blue;
}
/**
* Returns the red value.
*
* @return integer
*/
public function getRed()
{
return $this->red;
}
/**
* Returns the green value.
*
* @return integer
*/
public function getGreen()
{
return $this->green;
}
/**
* Returns the blue value.
*
* @return integer
*/
public function getBlue()
{
return $this->blue;
}
/**
* Returns a hexadecimal string representation of the RGB value.
*
* @return string
*/
public function __toString()
{
return sprintf('%02x%02x%02x', $this->red, $this->green, $this->blue);
}
/**
* toRgb(): defined by ColorInterface.
*
* @see ColorInterface::toRgb()
* @return Rgb
*/
public function toRgb()
{
return $this;
}
/**
* toCmyk(): defined by ColorInterface.
*
* @see ColorInterface::toCmyk()
* @return Cmyk
*/
public function toCmyk()
{
$c = 1 - ($this->red / 255);
$m = 1 - ($this->green / 255);
$y = 1 - ($this->blue / 255);
$k = min($c, $m, $y);
return new Cmyk(
100 * ($c - $k) / (1 - $k),
100 * ($m - $k) / (1 - $k),
100 * ($y - $k) / (1 - $k),
100 * $k
);
}
/**
* toGray(): defined by ColorInterface.
*
* @see ColorInterface::toGray()
* @return Gray
*/
public function toGray()
{
return new Gray(($this->red * 0.21 + $this->green * 0.71 + $this->blue * 0.07) / 2.55);
}
}
@@ -0,0 +1,338 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Renderer\Image;
use BaconQrCode\Encoder\QrCode;
use BaconQrCode\Renderer\Color;
use BaconQrCode\Renderer\Image\Decorator\DecoratorInterface;
use BaconQrCode\Exception;
/**
* Image renderer, supporting multiple backends.
*/
abstract class AbstractRenderer implements RendererInterface
{
/**
* Margin around the QR code, also known as quiet zone.
*
* @var integer
*/
protected $margin = 4;
/**
* Requested width of the rendered image.
*
* @var integer
*/
protected $width = 0;
/**
* Requested height of the rendered image.
*
* @var integer
*/
protected $height = 0;
/**
* Whether dimensions should be rounded down.
*
* @var boolean
*/
protected $roundDimensions = true;
/**
* Final width of the image.
*
* @var integer
*/
protected $finalWidth;
/**
* Final height of the image.
*
* @var integer
*/
protected $finalHeight;
/**
* Size of each individual block.
*
* @var integer
*/
protected $blockSize;
/**
* Background color.
*
* @var Color\ColorInterface
*/
protected $backgroundColor;
/**
* Whether dimensions should be rounded down
*
* @var boolean
*/
protected $floorToClosestDimension;
/**
* Foreground color.
*
* @var Color\ColorInterface
*/
protected $foregroundColor;
/**
* Decorators used on QR codes.
*
* @var array
*/
protected $decorators = array();
/**
* Sets the margin around the QR code.
*
* @param integer $margin
* @return AbstractRenderer
* @throws Exception\InvalidArgumentException
*/
public function setMargin($margin)
{
if ($margin < 0) {
throw new Exception\InvalidArgumentException('Margin must be equal to greater than 0');
}
$this->margin = (int) $margin;
return $this;
}
/**
* Gets the margin around the QR code.
*
* @return integer
*/
public function getMargin()
{
return $this->margin;
}
/**
* Sets the height around the renderd image.
*
* If the width is smaller than the matrix width plus padding, the renderer
* will automatically use that as the width instead of the specified one.
*
* @param integer $width
* @return AbstractRenderer
*/
public function setWidth($width)
{
$this->width = (int) $width;
return $this;
}
/**
* Gets the width of the rendered image.
*
* @return integer
*/
public function getWidth()
{
return $this->width;
}
/**
* Sets the height around the renderd image.
*
* If the height is smaller than the matrix height plus padding, the
* renderer will automatically use that as the height instead of the
* specified one.
*
* @param integer $height
* @return AbstractRenderer
*/
public function setHeight($height)
{
$this->height = (int) $height;
return $this;
}
/**
* Gets the height around the rendered image.
*
* @return integer
*/
public function getHeight()
{
return $this->height;
}
/**
* Sets whether dimensions should be rounded down.
*
* @param boolean $flag
* @return AbstractRenderer
*/
public function setRoundDimensions($flag)
{
$this->floorToClosestDimension = $flag;
return $this;
}
/**
* Gets whether dimensions should be rounded down.
*
* @return boolean
*/
public function shouldRoundDimensions()
{
return $this->floorToClosestDimension;
}
/**
* Sets background color.
*
* @param Color\ColorInterface $color
* @return AbstractRenderer
*/
public function setBackgroundColor(Color\ColorInterface $color)
{
$this->backgroundColor = $color;
return $this;
}
/**
* Gets background color.
*
* @return Color\ColorInterface
*/
public function getBackgroundColor()
{
if ($this->backgroundColor === null) {
$this->backgroundColor = new Color\Gray(100);
}
return $this->backgroundColor;
}
/**
* Sets foreground color.
*
* @param Color\ColorInterface $color
* @return AbstractRenderer
*/
public function setForegroundColor(Color\ColorInterface $color)
{
$this->foregroundColor = $color;
return $this;
}
/**
* Gets foreground color.
*
* @return Color\ColorInterface
*/
public function getForegroundColor()
{
if ($this->foregroundColor === null) {
$this->foregroundColor = new Color\Gray(0);
}
return $this->foregroundColor;
}
/**
* Adds a decorator to the renderer.
*
* @param DecoratorInterface $decorator
* @return AbstractRenderer
*/
public function addDecorator(DecoratorInterface $decorator)
{
$this->decorators[] = $decorator;
return $this;
}
/**
* render(): defined by RendererInterface.
*
* @see RendererInterface::render()
* @param QrCode $qrCode
* @return string
*/
public function render(QrCode $qrCode)
{
$input = $qrCode->getMatrix();
$inputWidth = $input->getWidth();
$inputHeight = $input->getHeight();
$qrWidth = $inputWidth + ($this->getMargin() << 1);
$qrHeight = $inputHeight + ($this->getMargin() << 1);
$outputWidth = max($this->getWidth(), $qrWidth);
$outputHeight = max($this->getHeight(), $qrHeight);
$multiple = (int) min($outputWidth / $qrWidth, $outputHeight / $qrHeight);
if ($this->shouldRoundDimensions()) {
$outputWidth -= $outputWidth % $multiple;
$outputHeight -= $outputHeight % $multiple;
}
// Padding includes both the quiet zone and the extra white pixels to
// accommodate the requested dimensions. For example, if input is 25x25
// the QR will be 33x33 including the quiet zone. If the requested size
// is 200x160, the multiple will be 4, for a QR of 132x132. These will
// handle all the padding from 100x100 (the actual QR) up to 200x160.
$leftPadding = (int) (($outputWidth - ($inputWidth * $multiple)) / 2);
$topPadding = (int) (($outputHeight - ($inputHeight * $multiple)) / 2);
// Store calculated parameters
$this->finalWidth = $outputWidth;
$this->finalHeight = $outputHeight;
$this->blockSize = $multiple;
$this->init();
$this->addColor('background', $this->getBackgroundColor());
$this->addColor('foreground', $this->getForegroundColor());
$this->drawBackground('background');
foreach ($this->decorators as $decorator) {
$decorator->preProcess(
$qrCode,
$this,
$outputWidth,
$outputHeight,
$leftPadding,
$topPadding,
$multiple
);
}
for ($inputY = 0, $outputY = $topPadding; $inputY < $inputHeight; $inputY++, $outputY += $multiple) {
for ($inputX = 0, $outputX = $leftPadding; $inputX < $inputWidth; $inputX++, $outputX += $multiple) {
if ($input->get($inputX, $inputY) === 1) {
$this->drawBlock($outputX, $outputY, 'foreground');
}
}
}
foreach ($this->decorators as $decorator) {
$decorator->postProcess(
$qrCode,
$this,
$outputWidth,
$outputHeight,
$leftPadding,
$topPadding,
$multiple
);
}
return $this->getByteStream();
}
}
@@ -0,0 +1,63 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Renderer\Image\Decorator;
use BaconQrCode\Encoder\QrCode;
use BaconQrCode\Renderer\Image\RendererInterface;
/**
* Decorator interface.
*/
interface DecoratorInterface
{
/**
* Pre-process a QR code.
*
* @param QrCode $qrCode
* @param RendererInterface $renderer
* @param integer $outputWidth
* @param integer $outputHeight
* @param integer $leftPadding
* @param integer $topPadding
* @param integer $multiple
* @return void
*/
public function preProcess(
QrCode $qrCode,
RendererInterface $renderer,
$outputWidth,
$outputHeight,
$leftPadding,
$topPadding,
$multiple
);
/**
* Post-process a QR code.
*
* @param QrCode $qrCode
* @param RendererInterface $renderer
* @param integer $outputWidth
* @param integer $outputHeight
* @param integer $leftPadding
* @param integer $topPadding
* @param integer $multiple
* @return void
*/
public function postProcess(
QrCode $qrCode,
RendererInterface $renderer,
$outputWidth,
$outputHeight,
$leftPadding,
$topPadding,
$multiple
);
}
@@ -0,0 +1,210 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Renderer\Image\Decorator;
use BaconQrCode\Encoder\QrCode;
use BaconQrCode\Renderer\Image\RendererInterface;
use BaconQrCode\Renderer\Color;
/**
* Finder pattern decorator.
*/
class FinderPattern implements DecoratorInterface
{
/**
* @var Color\ColorInterface
*/
protected $innerColor;
/**
* @varColor\ColorInterface
*/
protected $outerColor;
/**
* Outer position detection pattern.
*
* @var array
*/
protected static $outerPositionDetectionPattern = array(
array(1, 1, 1, 1, 1, 1, 1),
array(1, 0, 0, 0, 0, 0, 1),
array(1, 0, 0, 0, 0, 0, 1),
array(1, 0, 0, 0, 0, 0, 1),
array(1, 0, 0, 0, 0, 0, 1),
array(1, 0, 0, 0, 0, 0, 1),
array(1, 1, 1, 1, 1, 1, 1),
);
/**
* Inner position detection pattern.
*
* @var array
*/
protected static $innerPositionDetectionPattern = array(
array(0, 0, 0, 0, 0, 0, 0),
array(0, 0, 0, 0, 0, 0, 0),
array(0, 0, 1, 1, 1, 0, 0),
array(0, 0, 1, 1, 1, 0, 0),
array(0, 0, 1, 1, 1, 0, 0),
array(0, 0, 0, 0, 0, 0, 0),
array(0, 0, 0, 0, 0, 0, 0),
);
/**
* Sets outer color.
*
* @param Color\ColorInterface $color
* @return FinderPattern
*/
public function setOuterColor(Color\ColorInterface $color)
{
$this->outerColor = $color;
return $this;
}
/**
* Gets outer color.
*
* @return Color\ColorInterface
*/
public function getOuterColor()
{
if ($this->outerColor === null) {
$this->outerColor = new Color\Gray(100);
}
return $this->outerColor;
}
/**
* Sets inner color.
*
* @param Color\ColorInterface $color
* @return FinderPattern
*/
public function setInnerColor(Color\ColorInterface $color)
{
$this->innerColor = $color;
return $this;
}
/**
* Gets inner color.
*
* @return Color\ColorInterface
*/
public function getInnerColor()
{
if ($this->innerColor === null) {
$this->innerColor = new Color\Gray(0);
}
return $this->innerColor;
}
/**
* preProcess(): defined by DecoratorInterface.
*
* @see DecoratorInterface::preProcess()
* @param QrCode $qrCode
* @param RendererInterface $renderer
* @param integer $outputWidth
* @param integer $outputHeight
* @param integer $leftPadding
* @param integer $topPadding
* @param integer $multiple
* @return void
*/
public function preProcess(
QrCode $qrCode,
RendererInterface $renderer,
$outputWidth,
$outputHeight,
$leftPadding,
$topPadding,
$multiple
) {
$matrix = $qrCode->getMatrix();
$positions = array(
array(0, 0),
array($matrix->getWidth() - 7, 0),
array(0, $matrix->getHeight() - 7),
);
foreach (self::$outerPositionDetectionPattern as $y => $row) {
foreach ($row as $x => $isSet) {
foreach ($positions as $position) {
$matrix->set($x + $position[0], $y + $position[1], 0);
}
}
}
}
/**
* postProcess(): defined by DecoratorInterface.
*
* @see DecoratorInterface::postProcess()
*
* @param QrCode $qrCode
* @param RendererInterface $renderer
* @param integer $outputWidth
* @param integer $outputHeight
* @param integer $leftPadding
* @param integer $topPadding
* @param integer $multiple
* @return void
*/
public function postProcess(
QrCode $qrCode,
RendererInterface $renderer,
$outputWidth,
$outputHeight,
$leftPadding,
$topPadding,
$multiple
) {
$matrix = $qrCode->getMatrix();
$positions = array(
array(0, 0),
array($matrix->getWidth() - 7, 0),
array(0, $matrix->getHeight() - 7),
);
$renderer->addColor('finder-outer', $this->getOuterColor());
$renderer->addColor('finder-inner', $this->getInnerColor());
foreach (self::$outerPositionDetectionPattern as $y => $row) {
foreach ($row as $x => $isOuterSet) {
$isInnerSet = self::$innerPositionDetectionPattern[$y][$x];
if ($isOuterSet) {
foreach ($positions as $position) {
$renderer->drawBlock(
$leftPadding + $x * $multiple + $position[0] * $multiple,
$topPadding + $y * $multiple + $position[1] * $multiple,
'finder-outer'
);
}
}
if ($isInnerSet) {
foreach ($positions as $position) {
$renderer->drawBlock(
$leftPadding + $x * $multiple + $position[0] * $multiple,
$topPadding + $y * $multiple + $position[1] * $multiple,
'finder-inner'
);
}
}
}
}
}
}
@@ -0,0 +1,152 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Renderer\Image;
use BaconQrCode\Renderer\Color\ColorInterface;
use BaconQrCode\Renderer\Color\Rgb;
use BaconQrCode\Renderer\Color\Cmyk;
use BaconQrCode\Renderer\Color\Gray;
/**
* EPS backend.
*/
class Eps extends AbstractRenderer
{
/**
* EPS string.
*
* @var string
*/
protected $eps;
/**
* Colors used for drawing.
*
* @var array
*/
protected $colors = array();
/**
* Current color.
*
* @var string
*/
protected $currentColor;
/**
* init(): defined by RendererInterface.
*
* @see ImageRendererInterface::init()
* @return void
*/
public function init()
{
$this->eps = "%!PS-Adobe-3.0 EPSF-3.0\n"
. "%%BoundingBox: 0 0 " . $this->finalWidth . " " . $this->finalHeight . "\n"
. "/F { rectfill } def\n";
}
/**
* addColor(): defined by RendererInterface.
*
* @see ImageRendererInterface::addColor()
* @param string $id
* @param ColorInterface $color
* @return void
*/
public function addColor($id, ColorInterface $color)
{
if (
!$color instanceof Rgb
&& !$color instanceof Cmyk
&& !$color instanceof Gray
) {
$color = $color->toCmyk();
}
$this->colors[$id] = $color;
}
/**
* drawBackground(): defined by RendererInterface.
*
* @see ImageRendererInterface::drawBackground()
* @param string $colorId
* @return void
*/
public function drawBackground($colorId)
{
$this->setColor($colorId);
$this->eps .= "0 0 " . $this->finalWidth . " " . $this->finalHeight . " F\n";
}
/**
* drawBlock(): defined by RendererInterface.
*
* @see ImageRendererInterface::drawBlock()
* @param integer $x
* @param integer $y
* @param string $colorId
* @return void
*/
public function drawBlock($x, $y, $colorId)
{
$this->setColor($colorId);
$this->eps .= $x . " " . ($this->finalHeight - $y - $this->blockSize) . " " . $this->blockSize . " " . $this->blockSize . " F\n";
}
/**
* getByteStream(): defined by RendererInterface.
*
* @see ImageRendererInterface::getByteStream()
* @return string
*/
public function getByteStream()
{
return $this->eps;
}
/**
* Sets color to use.
*
* @param string $colorId
* @return void
*/
protected function setColor($colorId)
{
if ($colorId !== $this->currentColor) {
$color = $this->colors[$colorId];
if ($color instanceof Rgb) {
$this->eps .= sprintf(
"%F %F %F setrgbcolor\n",
$color->getRed() / 100,
$color->getGreen() / 100,
$color->getBlue() / 100
);
} elseif ($color instanceof Cmyk) {
$this->eps .= sprintf(
"%F %F %F %F setcmykcolor\n",
$color->getCyan() / 100,
$color->getMagenta() / 100,
$color->getYellow() / 100,
$color->getBlack() / 100
);
} elseif ($color instanceof Gray) {
$this->eps .= sprintf(
"%F setgray\n",
$color->getGray() / 100
);
}
$this->currentColor = $colorId;
}
}
}
@@ -0,0 +1,115 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Renderer\Image;
use BaconQrCode\Exception;
use BaconQrCode\Renderer\Color\ColorInterface;
/**
* PNG backend.
*/
class Png extends AbstractRenderer
{
/**
* Image resource used when drawing.
*
* @var resource
*/
protected $image;
/**
* Colors used for drawing.
*
* @var array
*/
protected $colors = array();
/**
* init(): defined by RendererInterface.
*
* @see ImageRendererInterface::init()
* @return void
*/
public function init()
{
$this->image = imagecreatetruecolor($this->finalWidth, $this->finalHeight);
}
/**
* addColor(): defined by RendererInterface.
*
* @see ImageRendererInterface::addColor()
* @param string $id
* @param ColorInterface $color
* @return void
* @throws Exception\RuntimeException
*/
public function addColor($id, ColorInterface $color)
{
if ($this->image === null) {
throw new Exception\RuntimeException('Colors can only be added after init');
}
$color = $color->toRgb();
$this->colors[$id] = imagecolorallocate(
$this->image,
$color->getRed(),
$color->getGreen(),
$color->getBlue()
);
}
/**
* drawBackground(): defined by RendererInterface.
*
* @see ImageRendererInterface::drawBackground()
* @param string $colorId
* @return void
*/
public function drawBackground($colorId)
{
imagefill($this->image, 0, 0, $this->colors[$colorId]);
}
/**
* drawBlock(): defined by RendererInterface.
*
* @see ImageRendererInterface::drawBlock()
* @param integer $x
* @param integer $y
* @param string $colorId
* @return void
*/
public function drawBlock($x, $y, $colorId)
{
imagefilledrectangle(
$this->image,
$x,
$y,
$x + $this->blockSize - 1,
$y + $this->blockSize - 1,
$this->colors[$colorId]
);
}
/**
* getByteStream(): defined by RendererInterface.
*
* @see ImageRendererInterface::getByteStream()
* @return string
*/
public function getByteStream()
{
ob_start();
imagepng($this->image);
return ob_get_clean();
}
}
@@ -0,0 +1,61 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Renderer\Image;
use BaconQrCode\Renderer\Color\ColorInterface;
use BaconQrCode\Renderer\RendererInterface as GeneralRendererInterface;
/**
* Renderer interface.
*/
interface RendererInterface extends GeneralRendererInterface
{
/**
* Initiates the drawing area.
*
* @return void
*/
public function init();
/**
* Adds a color to the drawing area.
*
* @param string $id
* @param ColorInterface $color
* @return void
*/
public function addColor($id, ColorInterface $color);
/**
* Draws the background.
*
* @param string $colorId
* @return void
*/
public function drawBackground($colorId);
/**
* Draws a block at a specified position.
*
* @param integer $x
* @param integer $y
* @param string $colorId
* @return void
*/
public function drawBlock($x, $y, $colorId);
/**
* Returns the byte stream representing the QR code.
*
* @return string
*/
public function getByteStream();
}
@@ -0,0 +1,146 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Renderer\Image;
use BaconQrCode\Exception;
use BaconQrCode\Renderer\Color\ColorInterface;
use SimpleXMLElement;
/**
* SVG backend.
*/
class Svg extends AbstractRenderer
{
/**
* SVG resource.
*
* @var SimpleXMLElement
*/
protected $svg;
/**
* Colors used for drawing.
*
* @var array
*/
protected $colors = array();
/**
* Prototype IDs.
*
* @var array
*/
protected $prototypeIds = array();
/**
* init(): defined by RendererInterface.
*
* @see ImageRendererInterface::init()
* @return void
*/
public function init()
{
$this->svg = new SimpleXMLElement(
'<?xml version="1.0" encoding="UTF-8"?>'
. '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"/>'
);
$this->svg->addAttribute('version', '1.1');
$this->svg->addAttribute('width', $this->finalWidth . 'px');
$this->svg->addAttribute('height', $this->finalHeight . 'px');
$this->svg->addAttribute('viewBox', '0 0 ' . $this->finalWidth . ' ' . $this->finalHeight);
$this->svg->addChild('defs');
}
/**
* addColor(): defined by RendererInterface.
*
* @see ImageRendererInterface::addColor()
* @param string $id
* @param ColorInterface $color
* @return void
* @throws Exception\InvalidArgumentException
*/
public function addColor($id, ColorInterface $color)
{
$this->colors[$id] = (string) $color->toRgb();
}
/**
* drawBackground(): defined by RendererInterface.
*
* @see ImageRendererInterface::drawBackground()
* @param string $colorId
* @return void
*/
public function drawBackground($colorId)
{
$rect = $this->svg->addChild('rect');
$rect->addAttribute('x', 0);
$rect->addAttribute('y', 0);
$rect->addAttribute('width', $this->finalWidth);
$rect->addAttribute('height', $this->finalHeight);
$rect->addAttribute('fill', '#' . $this->colors[$colorId]);
}
/**
* drawBlock(): defined by RendererInterface.
*
* @see ImageRendererInterface::drawBlock()
* @param integer $x
* @param integer $y
* @param string $colorId
* @return void
*/
public function drawBlock($x, $y, $colorId)
{
$use = $this->svg->addChild('use');
$use->addAttribute('x', $x);
$use->addAttribute('y', $y);
$use->addAttribute(
'xlink:href',
$this->getRectPrototypeId($colorId),
'http://www.w3.org/1999/xlink'
);
}
/**
* getByteStream(): defined by RendererInterface.
*
* @see ImageRendererInterface::getByteStream()
* @return string
*/
public function getByteStream()
{
return $this->svg->asXML();
}
/**
* Get the prototype ID for a color.
*
* @param integer $colorId
* @return string
*/
protected function getRectPrototypeId($colorId)
{
if (!isset($this->prototypeIds[$colorId])) {
$id = 'r' . dechex(count($this->prototypeIds));
$rect = $this->svg->defs->addChild('rect');
$rect->addAttribute('id', $id);
$rect->addAttribute('width', $this->blockSize);
$rect->addAttribute('height', $this->blockSize);
$rect->addAttribute('fill', '#' . $this->colors[$colorId]);
$this->prototypeIds[$colorId] = '#' . $id;
}
return $this->prototypeIds[$colorId];
}
}
@@ -0,0 +1,26 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Renderer;
use BaconQrCode\Encoder\QrCode;
/**
* Renderer interface.
*/
interface RendererInterface
{
/**
* Renders a QR code.
*
* @param QrCode $qrCode
* @return string
*/
public function render(QrCode $qrCode);
}
@@ -0,0 +1,91 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Renderer\Text;
use BaconQrCode\Encoder\QrCode;
/**
* Html renderer.
*/
class Html extends Plain
{
/**
* HTML CSS class attribute value.
*
* @var string
*/
protected $class = '';
/**
* HTML CSS style definition for the code element.
*
* @var string
*/
protected $style = 'font-family: monospace; line-height: 0.65em; letter-spacing: -1px';
/**
* Set CSS class name.
*
* @param string $class
*/
public function setClass($class)
{
$this->class = $class;
}
/**
* Get CSS class name.
*
* @return string
*/
public function getClass()
{
return $this->class;
}
/**
* Set CSS style value.
*
* @param string $style
*/
public function setStyle($style)
{
$this->style = $style;
}
/**
* Get CSS style value.
*
* @return string
*/
public function getStyle()
{
return $this->style;
}
/**
* render(): defined by RendererInterface.
*
* @see RendererInterface::render()
* @param QrCode $qrCode
* @return string
*/
public function render(QrCode $qrCode)
{
$textCode = parent::render($qrCode);
$result = '<pre'
. ' style="' . htmlspecialchars($this->style, ENT_QUOTES, 'utf-8') . '"'
. ' class="' . htmlspecialchars($this->class, ENT_QUOTES, 'utf-8') . '"'
. '>' . $textCode . '</pre>';
return $result;
}
}
@@ -0,0 +1,150 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Renderer\Text;
use BaconQrCode\Exception;
use BaconQrCode\Encoder\QrCode;
use BaconQrCode\Renderer\RendererInterface;
/**
* Plaintext renderer.
*/
class Plain implements RendererInterface
{
/**
* Margin around the QR code, also known as quiet zone.
*
* @var integer
*/
protected $margin = 1;
/**
* Char used for full block.
*
* UTF-8 FULL BLOCK (U+2588)
*
* @var string
* @link http://www.fileformat.info/info/unicode/char/2588/index.htm
*/
protected $fullBlock = "\xE2\x96\x88";
/**
* Char used for empty space
*
* @var string
*/
protected $emptyBlock = ' ';
/**
* Set char used as full block (occupied space, "black").
*
* @param string $fullBlock
*/
public function setFullBlock($fullBlock)
{
$this->fullBlock = $fullBlock;
}
/**
* Get char used as full block (occupied space, "black").
*
* @return string
*/
public function getFullBlock()
{
return $this->fullBlock;
}
/**
* Set char used as empty block (empty space, "white").
*
* @param string $emptyBlock
*/
public function setEmptyBlock($emptyBlock)
{
$this->emptyBlock = $emptyBlock;
}
/**
* Get char used as empty block (empty space, "white").
*
* @return string
*/
public function getEmptyBlock()
{
return $this->emptyBlock;
}
/**
* Sets the margin around the QR code.
*
* @param integer $margin
* @return AbstractRenderer
* @throws Exception\InvalidArgumentException
*/
public function setMargin($margin)
{
if ($margin < 0) {
throw new Exception\InvalidArgumentException('Margin must be equal to greater than 0');
}
$this->margin = (int) $margin;
return $this;
}
/**
* Gets the margin around the QR code.
*
* @return integer
*/
public function getMargin()
{
return $this->margin;
}
/**
* render(): defined by RendererInterface.
*
* @see RendererInterface::render()
* @param QrCode $qrCode
* @return string
*/
public function render(QrCode $qrCode)
{
$result = '';
$matrix = $qrCode->getMatrix();
$width = $matrix->getWidth();
// Top margin
for ($x = 0; $x < $this->margin; $x++) {
$result .= str_repeat($this->emptyBlock, $width + 2 * $this->margin)."\n";
}
// Body
$array = $matrix->getArray();
foreach ($array as $row) {
$result .= str_repeat($this->emptyBlock, $this->margin); // left margin
foreach ($row as $byte) {
$result .= $byte ? $this->fullBlock : $this->emptyBlock;
}
$result .= str_repeat($this->emptyBlock, $this->margin); // right margin
$result .= "\n";
}
// Bottom margin
for ($x = 0; $x < $this->margin; $x++) {
$result .= str_repeat($this->emptyBlock, $width + 2 * $this->margin)."\n";
}
return $result;
}
}
@@ -0,0 +1,105 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode;
use BaconQrCode\Common\ErrorCorrectionLevel;
use BaconQrCode\Encoder\Encoder;
use BaconQrCode\Exception;
use BaconQrCode\Renderer\RendererInterface;
/**
* QR code writer.
*/
class Writer
{
/**
* Renderer instance.
*
* @var RendererInterface
*/
protected $renderer;
/**
* Creates a new writer with a specific renderer.
*
* @param RendererInterface $renderer
*/
public function __construct(RendererInterface $renderer)
{
$this->renderer = $renderer;
}
/**
* Sets the renderer used to create a byte stream.
*
* @param RendererInterface $renderer
* @return Writer
*/
public function setRenderer(RendererInterface $renderer)
{
$this->renderer = $renderer;
return $this;
}
/**
* Gets the renderer used to create a byte stream.
*
* @return RendererInterface
*/
public function getRenderer()
{
return $this->renderer;
}
/**
* Writes QR code and returns it as string.
*
* Content is a string which *should* be encoded in UTF-8, in case there are
* non ASCII-characters present.
*
* @param string $content
* @param string $encoding
* @param integer $ecLevel
* @return string
* @throws Exception\InvalidArgumentException
*/
public function writeString(
$content,
$encoding = Encoder::DEFAULT_BYTE_MODE_ECODING,
$ecLevel = ErrorCorrectionLevel::L
) {
if (strlen($content) === 0) {
throw new Exception\InvalidArgumentException('Found empty contents');
}
$qrCode = Encoder::encode($content, new ErrorCorrectionLevel($ecLevel), $encoding);
return $this->getRenderer()->render($qrCode);
}
/**
* Writes QR code to a file.
*
* @see Writer::writeString()
* @param string $content
* @param string $filename
* @param string $encoding
* @param integer $ecLevel
* @return void
*/
public function writeFile(
$content,
$filename,
$encoding = Encoder::DEFAULT_BYTE_MODE_ECODING,
$ecLevel = ErrorCorrectionLevel::L
) {
file_put_contents($filename, $this->writeString($content, $encoding, $ecLevel));
}
}
@@ -0,0 +1,372 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
use BaconQrCode\Exception\InvalidArgumentException;
use SplFixedArray;
/**
* A simple, fast array of bits.
*/
final class BitArray
{
/**
* Bits represented as an array of integers.
*
* @var SplFixedArray<int>
*/
private $bits;
/**
* Size of the bit array in bits.
*
* @var int
*/
private $size;
/**
* Creates a new bit array with a given size.
*/
public function __construct(int $size = 0)
{
$this->size = $size;
$this->bits = SplFixedArray::fromArray(array_fill(0, ($this->size + 31) >> 3, 0));
}
/**
* Gets the size in bits.
*/
public function getSize() : int
{
return $this->size;
}
/**
* Gets the size in bytes.
*/
public function getSizeInBytes() : int
{
return ($this->size + 7) >> 3;
}
/**
* Ensures that the array has a minimum capacity.
*/
public function ensureCapacity(int $size) : void
{
if ($size > count($this->bits) << 5) {
$this->bits->setSize(($size + 31) >> 5);
}
}
/**
* Gets a specific bit.
*/
public function get(int $i) : bool
{
return 0 !== ($this->bits[$i >> 5] & (1 << ($i & 0x1f)));
}
/**
* Sets a specific bit.
*/
public function set(int $i) : void
{
$this->bits[$i >> 5] = $this->bits[$i >> 5] | 1 << ($i & 0x1f);
}
/**
* Flips a specific bit.
*/
public function flip(int $i) : void
{
$this->bits[$i >> 5] ^= 1 << ($i & 0x1f);
}
/**
* Gets the next set bit position from a given position.
*/
public function getNextSet(int $from) : int
{
if ($from >= $this->size) {
return $this->size;
}
$bitsOffset = $from >> 5;
$currentBits = $this->bits[$bitsOffset];
$bitsLength = count($this->bits);
$currentBits &= ~((1 << ($from & 0x1f)) - 1);
while (0 === $currentBits) {
if (++$bitsOffset === $bitsLength) {
return $this->size;
}
$currentBits = $this->bits[$bitsOffset];
}
$result = ($bitsOffset << 5) + BitUtils::numberOfTrailingZeros($currentBits);
return $result > $this->size ? $this->size : $result;
}
/**
* Gets the next unset bit position from a given position.
*/
public function getNextUnset(int $from) : int
{
if ($from >= $this->size) {
return $this->size;
}
$bitsOffset = $from >> 5;
$currentBits = ~$this->bits[$bitsOffset];
$bitsLength = count($this->bits);
$currentBits &= ~((1 << ($from & 0x1f)) - 1);
while (0 === $currentBits) {
if (++$bitsOffset === $bitsLength) {
return $this->size;
}
$currentBits = ~$this->bits[$bitsOffset];
}
$result = ($bitsOffset << 5) + BitUtils::numberOfTrailingZeros($currentBits);
return $result > $this->size ? $this->size : $result;
}
/**
* Sets a bulk of bits.
*/
public function setBulk(int $i, int $newBits) : void
{
$this->bits[$i >> 5] = $newBits;
}
/**
* Sets a range of bits.
*
* @throws InvalidArgumentException if end is smaller than start
*/
public function setRange(int $start, int $end) : void
{
if ($end < $start) {
throw new InvalidArgumentException('End must be greater or equal to start');
}
if ($end === $start) {
return;
}
--$end;
$firstInt = $start >> 5;
$lastInt = $end >> 5;
for ($i = $firstInt; $i <= $lastInt; ++$i) {
$firstBit = $i > $firstInt ? 0 : $start & 0x1f;
$lastBit = $i < $lastInt ? 31 : $end & 0x1f;
if (0 === $firstBit && 31 === $lastBit) {
$mask = 0x7fffffff;
} else {
$mask = 0;
for ($j = $firstBit; $j < $lastBit; ++$j) {
$mask |= 1 << $j;
}
}
$this->bits[$i] = $this->bits[$i] | $mask;
}
}
/**
* Clears the bit array, unsetting every bit.
*/
public function clear() : void
{
$bitsLength = count($this->bits);
for ($i = 0; $i < $bitsLength; ++$i) {
$this->bits[$i] = 0;
}
}
/**
* Checks if a range of bits is set or not set.
* @throws InvalidArgumentException if end is smaller than start
*/
public function isRange(int $start, int $end, bool $value) : bool
{
if ($end < $start) {
throw new InvalidArgumentException('End must be greater or equal to start');
}
if ($end === $start) {
return true;
}
--$end;
$firstInt = $start >> 5;
$lastInt = $end >> 5;
for ($i = $firstInt; $i <= $lastInt; ++$i) {
$firstBit = $i > $firstInt ? 0 : $start & 0x1f;
$lastBit = $i < $lastInt ? 31 : $end & 0x1f;
if (0 === $firstBit && 31 === $lastBit) {
$mask = 0x7fffffff;
} else {
$mask = 0;
for ($j = $firstBit; $j <= $lastBit; ++$j) {
$mask |= 1 << $j;
}
}
if (($this->bits[$i] & $mask) !== ($value ? $mask : 0)) {
return false;
}
}
return true;
}
/**
* Appends a bit to the array.
*/
public function appendBit(bool $bit) : void
{
$this->ensureCapacity($this->size + 1);
if ($bit) {
$this->bits[$this->size >> 5] = $this->bits[$this->size >> 5] | (1 << ($this->size & 0x1f));
}
++$this->size;
}
/**
* Appends a number of bits (up to 32) to the array.
* @throws InvalidArgumentException if num bits is not between 0 and 32
*/
public function appendBits(int $value, int $numBits) : void
{
if ($numBits < 0 || $numBits > 32) {
throw new InvalidArgumentException('Num bits must be between 0 and 32');
}
$this->ensureCapacity($this->size + $numBits);
for ($numBitsLeft = $numBits; $numBitsLeft > 0; $numBitsLeft--) {
$this->appendBit((($value >> ($numBitsLeft - 1)) & 0x01) === 1);
}
}
/**
* Appends another bit array to this array.
*/
public function appendBitArray(self $other) : void
{
$otherSize = $other->getSize();
$this->ensureCapacity($this->size + $other->getSize());
for ($i = 0; $i < $otherSize; ++$i) {
$this->appendBit($other->get($i));
}
}
/**
* Makes an exclusive-or comparision on the current bit array.
*
* @throws InvalidArgumentException if sizes don't match
*/
public function xorBits(self $other) : void
{
$bitsLength = count($this->bits);
$otherBits = $other->getBitArray();
if ($bitsLength !== count($otherBits)) {
throw new InvalidArgumentException('Sizes don\'t match');
}
for ($i = 0; $i < $bitsLength; ++$i) {
$this->bits[$i] = $this->bits[$i] ^ $otherBits[$i];
}
}
/**
* Converts the bit array to a byte array.
*
* @return SplFixedArray<int>
*/
public function toBytes(int $bitOffset, int $numBytes) : SplFixedArray
{
$bytes = new SplFixedArray($numBytes);
for ($i = 0; $i < $numBytes; ++$i) {
$byte = 0;
for ($j = 0; $j < 8; ++$j) {
if ($this->get($bitOffset)) {
$byte |= 1 << (7 - $j);
}
++$bitOffset;
}
$bytes[$i] = $byte;
}
return $bytes;
}
/**
* Gets the internal bit array.
*
* @return SplFixedArray<int>
*/
public function getBitArray() : SplFixedArray
{
return $this->bits;
}
/**
* Reverses the array.
*/
public function reverse() : void
{
$newBits = new SplFixedArray(count($this->bits));
for ($i = 0; $i < $this->size; ++$i) {
if ($this->get($this->size - $i - 1)) {
$newBits[$i >> 5] = $newBits[$i >> 5] | (1 << ($i & 0x1f));
}
}
$this->bits = $newBits;
}
/**
* Returns a string representation of the bit array.
*/
public function __toString() : string
{
$result = '';
for ($i = 0; $i < $this->size; ++$i) {
if (0 === ($i & 0x07)) {
$result .= ' ';
}
$result .= $this->get($i) ? 'X' : '.';
}
return $result;
}
}
@@ -0,0 +1,313 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
use BaconQrCode\Exception\InvalidArgumentException;
use SplFixedArray;
/**
* Bit matrix.
*
* Represents a 2D matrix of bits. In function arguments below, and throughout
* the common module, x is the column position, and y is the row position. The
* ordering is always x, y. The origin is at the top-left.
*/
class BitMatrix
{
/**
* Width of the bit matrix.
*
* @var int
*/
private $width;
/**
* Height of the bit matrix.
*
* @var int
*/
private $height;
/**
* Size in bits of each individual row.
*
* @var int
*/
private $rowSize;
/**
* Bits representation.
*
* @var SplFixedArray<int>
*/
private $bits;
/**
* @throws InvalidArgumentException if a dimension is smaller than zero
*/
public function __construct(int $width, int $height = null)
{
if (null === $height) {
$height = $width;
}
if ($width < 1 || $height < 1) {
throw new InvalidArgumentException('Both dimensions must be greater than zero');
}
$this->width = $width;
$this->height = $height;
$this->rowSize = ($width + 31) >> 5;
$this->bits = SplFixedArray::fromArray(array_fill(0, $this->rowSize * $height, 0));
}
/**
* Gets the requested bit, where true means black.
*/
public function get(int $x, int $y) : bool
{
$offset = $y * $this->rowSize + ($x >> 5);
return 0 !== (BitUtils::unsignedRightShift($this->bits[$offset], ($x & 0x1f)) & 1);
}
/**
* Sets the given bit to true.
*/
public function set(int $x, int $y) : void
{
$offset = $y * $this->rowSize + ($x >> 5);
$this->bits[$offset] = $this->bits[$offset] | (1 << ($x & 0x1f));
}
/**
* Flips the given bit.
*/
public function flip(int $x, int $y) : void
{
$offset = $y * $this->rowSize + ($x >> 5);
$this->bits[$offset] = $this->bits[$offset] ^ (1 << ($x & 0x1f));
}
/**
* Clears all bits (set to false).
*/
public function clear() : void
{
$max = count($this->bits);
for ($i = 0; $i < $max; ++$i) {
$this->bits[$i] = 0;
}
}
/**
* Sets a square region of the bit matrix to true.
*
* @throws InvalidArgumentException if left or top are negative
* @throws InvalidArgumentException if width or height are smaller than 1
* @throws InvalidArgumentException if region does not fit into the matix
*/
public function setRegion(int $left, int $top, int $width, int $height) : void
{
if ($top < 0 || $left < 0) {
throw new InvalidArgumentException('Left and top must be non-negative');
}
if ($height < 1 || $width < 1) {
throw new InvalidArgumentException('Width and height must be at least 1');
}
$right = $left + $width;
$bottom = $top + $height;
if ($bottom > $this->height || $right > $this->width) {
throw new InvalidArgumentException('The region must fit inside the matrix');
}
for ($y = $top; $y < $bottom; ++$y) {
$offset = $y * $this->rowSize;
for ($x = $left; $x < $right; ++$x) {
$index = $offset + ($x >> 5);
$this->bits[$index] = $this->bits[$index] | (1 << ($x & 0x1f));
}
}
}
/**
* A fast method to retrieve one row of data from the matrix as a BitArray.
*/
public function getRow(int $y, BitArray $row = null) : BitArray
{
if (null === $row || $row->getSize() < $this->width) {
$row = new BitArray($this->width);
}
$offset = $y * $this->rowSize;
for ($x = 0; $x < $this->rowSize; ++$x) {
$row->setBulk($x << 5, $this->bits[$offset + $x]);
}
return $row;
}
/**
* Sets a row of data from a BitArray.
*/
public function setRow(int $y, BitArray $row) : void
{
$bits = $row->getBitArray();
for ($i = 0; $i < $this->rowSize; ++$i) {
$this->bits[$y * $this->rowSize + $i] = $bits[$i];
}
}
/**
* This is useful in detecting the enclosing rectangle of a 'pure' barcode.
*
* @return int[]|null
*/
public function getEnclosingRectangle() : ?array
{
$left = $this->width;
$top = $this->height;
$right = -1;
$bottom = -1;
for ($y = 0; $y < $this->height; ++$y) {
for ($x32 = 0; $x32 < $this->rowSize; ++$x32) {
$bits = $this->bits[$y * $this->rowSize + $x32];
if (0 !== $bits) {
if ($y < $top) {
$top = $y;
}
if ($y > $bottom) {
$bottom = $y;
}
if ($x32 * 32 < $left) {
$bit = 0;
while (($bits << (31 - $bit)) === 0) {
$bit++;
}
if (($x32 * 32 + $bit) < $left) {
$left = $x32 * 32 + $bit;
}
}
}
if ($x32 * 32 + 31 > $right) {
$bit = 31;
while (0 === BitUtils::unsignedRightShift($bits, $bit)) {
--$bit;
}
if (($x32 * 32 + $bit) > $right) {
$right = $x32 * 32 + $bit;
}
}
}
}
$width = $right - $left;
$height = $bottom - $top;
if ($width < 0 || $height < 0) {
return null;
}
return [$left, $top, $width, $height];
}
/**
* Gets the most top left set bit.
*
* This is useful in detecting a corner of a 'pure' barcode.
*
* @return int[]|null
*/
public function getTopLeftOnBit() : ?array
{
$bitsOffset = 0;
while ($bitsOffset < count($this->bits) && 0 === $this->bits[$bitsOffset]) {
++$bitsOffset;
}
if (count($this->bits) === $bitsOffset) {
return null;
}
$x = intdiv($bitsOffset, $this->rowSize);
$y = ($bitsOffset % $this->rowSize) << 5;
$bits = $this->bits[$bitsOffset];
$bit = 0;
while (0 === ($bits << (31 - $bit))) {
++$bit;
}
$x += $bit;
return [$x, $y];
}
/**
* Gets the most bottom right set bit.
*
* This is useful in detecting a corner of a 'pure' barcode.
*
* @return int[]|null
*/
public function getBottomRightOnBit() : ?array
{
$bitsOffset = count($this->bits) - 1;
while ($bitsOffset >= 0 && 0 === $this->bits[$bitsOffset]) {
--$bitsOffset;
}
if ($bitsOffset < 0) {
return null;
}
$x = intdiv($bitsOffset, $this->rowSize);
$y = ($bitsOffset % $this->rowSize) << 5;
$bits = $this->bits[$bitsOffset];
$bit = 0;
while (0 === BitUtils::unsignedRightShift($bits, $bit)) {
--$bit;
}
$x += $bit;
return [$x, $y];
}
/**
* Gets the width of the matrix,
*/
public function getWidth() : int
{
return $this->width;
}
/**
* Gets the height of the matrix.
*/
public function getHeight() : int
{
return $this->height;
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
/**
* General bit utilities.
*
* All utility methods are based on 32-bit integers and also work on 64-bit
* systems.
*/
final class BitUtils
{
private function __construct()
{
}
/**
* Performs an unsigned right shift.
*
* This is the same as the unsigned right shift operator ">>>" in other
* languages.
*/
public static function unsignedRightShift(int $a, int $b) : int
{
return (
$a >= 0
? $a >> $b
: (($a & 0x7fffffff) >> $b) | (0x40000000 >> ($b - 1))
);
}
/**
* Gets the number of trailing zeros.
*/
public static function numberOfTrailingZeros(int $i) : int
{
$lastPos = strrpos(str_pad(decbin($i), 32, '0', STR_PAD_LEFT), '1');
return $lastPos === false ? 32 : 31 - $lastPos;
}
}
@@ -0,0 +1,180 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
use BaconQrCode\Exception\InvalidArgumentException;
use DASPRiD\Enum\AbstractEnum;
/**
* Encapsulates a Character Set ECI, according to "Extended Channel Interpretations" 5.3.1.1 of ISO 18004.
*
* @method static self CP437()
* @method static self ISO8859_1()
* @method static self ISO8859_2()
* @method static self ISO8859_3()
* @method static self ISO8859_4()
* @method static self ISO8859_5()
* @method static self ISO8859_6()
* @method static self ISO8859_7()
* @method static self ISO8859_8()
* @method static self ISO8859_9()
* @method static self ISO8859_10()
* @method static self ISO8859_11()
* @method static self ISO8859_12()
* @method static self ISO8859_13()
* @method static self ISO8859_14()
* @method static self ISO8859_15()
* @method static self ISO8859_16()
* @method static self SJIS()
* @method static self CP1250()
* @method static self CP1251()
* @method static self CP1252()
* @method static self CP1256()
* @method static self UNICODE_BIG_UNMARKED()
* @method static self UTF8()
* @method static self ASCII()
* @method static self BIG5()
* @method static self GB18030()
* @method static self EUC_KR()
*/
final class CharacterSetEci extends AbstractEnum
{
protected const CP437 = [[0, 2]];
protected const ISO8859_1 = [[1, 3], 'ISO-8859-1'];
protected const ISO8859_2 = [[4], 'ISO-8859-2'];
protected const ISO8859_3 = [[5], 'ISO-8859-3'];
protected const ISO8859_4 = [[6], 'ISO-8859-4'];
protected const ISO8859_5 = [[7], 'ISO-8859-5'];
protected const ISO8859_6 = [[8], 'ISO-8859-6'];
protected const ISO8859_7 = [[9], 'ISO-8859-7'];
protected const ISO8859_8 = [[10], 'ISO-8859-8'];
protected const ISO8859_9 = [[11], 'ISO-8859-9'];
protected const ISO8859_10 = [[12], 'ISO-8859-10'];
protected const ISO8859_11 = [[13], 'ISO-8859-11'];
protected const ISO8859_12 = [[14], 'ISO-8859-12'];
protected const ISO8859_13 = [[15], 'ISO-8859-13'];
protected const ISO8859_14 = [[16], 'ISO-8859-14'];
protected const ISO8859_15 = [[17], 'ISO-8859-15'];
protected const ISO8859_16 = [[18], 'ISO-8859-16'];
protected const SJIS = [[20], 'Shift_JIS'];
protected const CP1250 = [[21], 'windows-1250'];
protected const CP1251 = [[22], 'windows-1251'];
protected const CP1252 = [[23], 'windows-1252'];
protected const CP1256 = [[24], 'windows-1256'];
protected const UNICODE_BIG_UNMARKED = [[25], 'UTF-16BE', 'UnicodeBig'];
protected const UTF8 = [[26], 'UTF-8'];
protected const ASCII = [[27, 170], 'US-ASCII'];
protected const BIG5 = [[28]];
protected const GB18030 = [[29], 'GB2312', 'EUC_CN', 'GBK'];
protected const EUC_KR = [[30], 'EUC-KR'];
/**
* @var int[]
*/
private $values;
/**
* @var string[]
*/
private $otherEncodingNames;
/**
* @var array<int, self>|null
*/
private static $valueToEci;
/**
* @var array<string, self>|null
*/
private static $nameToEci;
public function __construct(array $values, string ...$otherEncodingNames)
{
$this->values = $values;
$this->otherEncodingNames = $otherEncodingNames;
}
/**
* Returns the primary value.
*/
public function getValue() : int
{
return $this->values[0];
}
/**
* Gets character set ECI by value.
*
* Returns the representing ECI of a given value, or null if it is legal but unsupported.
*
* @throws InvalidArgumentException if value is not between 0 and 900
*/
public static function getCharacterSetEciByValue(int $value) : ?self
{
if ($value < 0 || $value >= 900) {
throw new InvalidArgumentException('Value must be between 0 and 900');
}
$valueToEci = self::valueToEci();
if (! array_key_exists($value, $valueToEci)) {
return null;
}
return $valueToEci[$value];
}
/**
* Returns character set ECI by name.
*
* Returns the representing ECI of a given name, or null if it is legal but unsupported
*/
public static function getCharacterSetEciByName(string $name) : ?self
{
$nameToEci = self::nameToEci();
$name = strtolower($name);
if (! array_key_exists($name, $nameToEci)) {
return null;
}
return $nameToEci[$name];
}
private static function valueToEci() : array
{
if (null !== self::$valueToEci) {
return self::$valueToEci;
}
self::$valueToEci = [];
foreach (self::values() as $eci) {
foreach ($eci->values as $value) {
self::$valueToEci[$value] = $eci;
}
}
return self::$valueToEci;
}
private static function nameToEci() : array
{
if (null !== self::$nameToEci) {
return self::$nameToEci;
}
self::$nameToEci = [];
foreach (self::values() as $eci) {
self::$nameToEci[strtolower($eci->name())] = $eci;
foreach ($eci->otherEncodingNames as $name) {
self::$nameToEci[strtolower($name)] = $eci;
}
}
return self::$nameToEci;
}
}

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