Shablon verstka

This commit is contained in:
merdan 2021-06-04 15:06:44 +05:00
parent 2741331dbc
commit 00af87206c
189 changed files with 22719 additions and 2 deletions

View File

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

View File

@ -0,0 +1,213 @@
<?php
namespace JanVince\SmallContactForm;
use \Illuminate\Support\Facades\Event;
use System\Classes\PluginBase;
use System\Classes\PluginManager;
use Config;
use Backend;
use Validator;
use Log;
use JanVince\SmallContactForm\Models\Settings;
class Plugin extends PluginBase {
/**
* @var array Plugin dependencies
*/
public $require = [];
/**
* Returns information about this plugin.
*
* @return array
*/
public function pluginDetails() {
return [
'name' => 'janvince.smallcontactform::lang.plugin.name',
'description' => 'janvince.smallcontactform::lang.plugin.description',
'author' => 'Jan Vince',
'icon' => 'icon-inbox'
];
}
public function boot() {
/**
* Custom Validator rules
*/
Validator::extend('custom_not_regex', function ($attribute, $value, $parameters) {
if (is_array($parameters)) {
$param = $parameters[0];
} else {
$param = $parameters;
}
try {
$result = preg_match($param, $value);
if ($result === 1) {
return false;
} else {
return true;
}
} catch (\Exception $e) {
Log::error('Error in Small Contact Form custom_not_regex validation rule! ' . $e->getMessage());
}
return false;
});
}
public function registerSettings() {
return [
'settings' => [
'label' => 'janvince.smallcontactform::lang.plugin.name',
'description' => 'janvince.smallcontactform::lang.plugin.description',
'category' => 'Small plugins',
'icon' => 'icon-inbox',
'class' => 'JanVince\SmallContactForm\Models\Settings',
'keywords' => 'small contact form message recaptcha antispam',
'order' => 990,
'permissions' => ['janvince.smallcontactform.access_settings'],
]
];
}
public function registerNavigation(){
return [
'smallcontactform' => [
'label' => 'janvince.smallcontactform::lang.navigation.main_label',
'url' => Backend::url('janvince/smallcontactform/messages'),
'icon' => 'icon-inbox',
'permissions' => ['janvince.smallcontactform.access_messages'],
'order' => 990,
'sideMenu' => [
'messages' => [
'label' => 'janvince.smallcontactform::lang.navigation.messages',
'icon' => 'icon-envelope-o',
'url' => Backend::url('janvince/smallcontactform/messages'),
'permissions' => ['janvince.smallcontactform.access_messages']
],
],
],
];
}
public function registerPermissions(){
return [
'janvince.smallcontactform.access_messages' => [
'label' => 'janvince.smallcontactform::lang.permissions.access_messages',
'tab' => 'janvince.smallcontactform::lang.plugin.name',
],
'janvince.smallcontactform.access_settings' => [
'label' => 'janvince.smallcontactform::lang.permissions.access_settings',
'tab' => 'janvince.smallcontactform::lang.plugin.name',
],
'janvince.smallcontactform.delete_messages' => [
'label' => 'janvince.smallcontactform::lang.permissions.delete_messages',
'tab' => 'janvince.smallcontactform::lang.plugin.name',
],
'janvince.smallcontactform.export_messages' => [
'label' => 'janvince.smallcontactform::lang.permissions.export_messages',
'tab' => 'janvince.smallcontactform::lang.plugin.name',
],
];
}
public function registerComponents()
{
return [
'JanVince\SmallContactForm\Components\SmallContactForm' => 'contactForm',
];
}
public function registerPageSnippets()
{
return [
'JanVince\SmallContactForm\Components\SmallContactForm' => 'contactForm',
];
}
public function registerMailTemplates()
{
return Settings::getTranslatedTemplates();
}
public function registerMarkupTags()
{
return [
'filters' => [
],
'functions' => [
'trans' => function($value) { return e(trans($value)); },
'html_entity_decode' => function($value) { return html_entity_decode($value); },
'settingsGet' => function($value, $default = NULL) { return Settings::get($value, $default); }
]
];
}
/**
* Custom list types
*/
public function registerListColumnTypes()
{
return [
'strong' => function($value) { return '<strong>'. $value . '</strong>'; },
'text_preview' => function($value) { $content = mb_substr(strip_tags($value), 0, 150); if(mb_strlen($content) > 150) { return ($content . '...'); } else { return $content; } },
'array_preview' => function($value) { $content = mb_substr(strip_tags( implode(' --- ', $value) ), 0, 150); if(mb_strlen($content) > 150) { return ($content . '...'); } else { return $content; } },
'switch_icon_star' => function($value) { return '<div class="text-center"><span class="'. ($value==1 ? 'oc-icon-circle text-success' : 'text-muted oc-icon-circle text-draft') .'">' . ($value==1 ? e(trans('janvince.smallcontactform::lang.models.message.columns.new')) : e(trans('janvince.smallcontactform::lang.models.message.columns.read')) ) . '</span></div>'; },
'switch_extended_input' => function($value) { if($value){return '<span class="list-badge badge-success"><span class="icon-check"></span></span>';} else { return '<span class="list-badge badge-danger"><span class="icon-minus"></span></span>';} },
'switch_extended' => function($value) { if($value){return '<span class="list-badge badge-success"><span class="icon-check"></span></span>';} else { return '<span class="list-badge badge-danger"><span class="icon-minus"></span></span>';} },
'attached_images_count' => function($value){ return (count($value) ? count($value) : NULL); },
'image_preview' => function($value) {
$width = Settings::get('records_list_preview_width') ? Settings::get('records_list_preview_width') : 50;
$height = Settings::get('records_list_preview_height') ? Settings::get('records_list_preview_height') : 50;
if($value){ return "<img src='".$value->getThumb($width, $height)."' style='width: auto; height: auto; max-width: ".$width."px; max-height: ".$height."px'>"; }
},
'scf_files_link' => function($value){
if(!empty($value)) {
$output = [];
foreach($value as $file) {
$output[] = "<div><a class='btn btn-primary' href='".$file->getPath()."'>Open file</a></div>";
}
return implode('', $output);
}
},
];
}
public function registerReportWidgets()
{
return [
'JanVince\SmallContactForm\ReportWidgets\Messages' => [
'label' => 'janvince.smallcontactform::lang.reportwidget.partials.messages.label',
'context' => 'dashboard'
],
'JanVince\SmallContactForm\ReportWidgets\NewMessage' => [
'label' => 'janvince.smallcontactform::lang.reportwidget.partials.new_message.label',
'context' => 'dashboard'
],
];
}
}

View File

@ -0,0 +1,440 @@
# Small Contact form
> Simple but flexible contact form builder with custom fields, validation and passive antispam.
## Installation
**GitHub** clone into `/plugins` dir:
```sh
git clone https://github.com/jan-vince/smallcontactform
```
**OctoberCMS backend**
Just look for 'Small Contact Form' in search field in:
> Settings > Updates & Plugins > Install plugins
### Permissions
> Settings > Administrators
You can set permissions to restrict access to *Settings > Small plugins > Contact form* and to messages list.
### Installation with composer
* Edit composer.json by adding new repository
```
"repositories": [
{
"type": "vcs",
"url": "https://github.com/jan-vince/smallcontactform"
}
]
```
* run in command line
```sh
composer require janvince/smallcontactform
```
## Setup new Contact form
> Settings > Small Contact form
### FORM
* You can set your own CSS class name and general success/error messages.
* If you need it, placeholders can be used instead of labels
* Form can be hidden after successful submit.
#### Enable AJAX
By default, sending form will trigger page reload. With AJAX, everything can be done without page reloading which will be more user friendly.
*If user's browser doesn't support (or has disabled) JavaScript, form will still work with page reloads after send.*
* For AJAX enabled form, before send confirmation dialog can be required.
#### Add Assets
If you want to start quickly, you can enable Add assets checkbox - and then Add CSS and JS assets.
This will include necessary styles (Bootstrap, AJAX, October AJAX) and scripts (jQuery, Bootstrap, October AJAX framework and extras).
But you have to include Twig tags ````{% styles %}```` and ````{% scripts %}```` into your layout or page like this:
````
<html>
<head>
{% styles %}
</head>
<body>
{% page %}
{% scripts %}
</body>
</html>
````
If you want to insert assets by hand, you can do it this way (or similar):
````
<html>
<head>
<link href="{{['~/modules/system/assets/css/framework.extras.css']|theme }}.css" rel="stylesheet">
</head>
<body>
{% page %}
<script type="text/javascript" src="{{ [
'@jquery',
'@framework',
'@framework.extras']|theme}}.js">
</script>
</body>
</html>
````
### SEND BUTTON
* You can set button class and text.
#### Redirection after the form is sent
You have some options to control redirection after form is successfully sent:
* In main form settings you can allow redirection and set fixed URL (internal or external)
* In component properties (on CMS Page or Layout) you can override main redirection settings for a specific form
* You can add a dynamic redirect URL as a markup parameter eg. `{% component 'contactForm' redirect_url = ('/success#'~this.page.id) %}`
> If you use markup parameter do not forget to allow form redirection in form main settings or (rather) in component parameters ! There is no markup parametr to allow redirection.
### FIELDS
Here you can add fields to build your contact (or other) form.
The idea is simple (and solution is so I hope):
* Click to add new field
* Set it's name (this is used for ````<input name="{{name}}" id="{{name}}">````), so it should be lowercase without special characters.
* Set Label if you need one (it is used for descriptive text above input field)
* Set autofocus if you want cursor to automatically jump to this field (if checked more than one field, cursor jumps to first one)
When dropdown is selected there will be values/options repeater shown. You can add as many values you need.
> Hint: you can add dropdown empty option by adding a value with empty ID.
You can also use **Custom code** and have complete control of generated code.
There is also a **Custom content** field to add formated content in place of a field.
#### Field data validation
You can select from predefined rules or add custom Validator rules (read [documentation](https://octobercms.com/docs/services/validation#available-validation-rules)).
Some rules require additional validation pattern some of them not.
* You can add one or more validation rules and error messages for them
* Error messages will be shown above input field
* You can reorder fields by drag and drop left circle (all fields can be collapsed by pressing Ctrl+click (Cmd+click on MacOS) on arrow in right top corners)
> Hint: For dropdown validation you can use `custom` validation type with rule `in` and list of IDs in `pattern` field (eg: 1,2,3).
> Note: There is a `custom_not_regex` validation rule as an inverse to built in `regex`.
### COLUMNS MAPPING
System writes all form data in database, but for quick overview Name, Email and Message columns are visible separately in Messages list.
But you have to help system to identify these columns by mapping to your form fields.
These mappings are also used for autoreply emails where at least Email field mapping is important.
### ANTISPAM
#### Passive antispam
Very simple implementation of passive antispam (inspired by [Nette AntiSpam Control](https://gist.github.com/Michal-Mikolas/2388131)).
The idea behind this is to check how fast is form send and if robots-catching field is filled.
* When allowed, you can set form delay (in seconds) to prevent too fast form sending (mostly by robots). You can add custom error message (will be shown in general error message box above form).
* You can add antispam field label and error message for non JavaScript enabled browsers.
* If JavaScript is working, antispam field is automatically hidden and cleared.
#### Google reCaptcha
Implementation of Google reCaptcha antispam protection.
##### Setup
First you have to create new API keys pair in reCaptcha admin panel.
Hit **Get reCAPTCHA** button on [reCaptcha wellcome page](https://www.google.com/recaptcha). Set label and check reCAPTCHA v2 option and hit button Register.
Copy Site key and Secret key to Contact Form's settings fields.
If you want Contact Form to automatically include server scripts in your layout, check the button in Form settings.
#### Check sender's IP
You can add an extra form protection with limit submits from one IP address.
This check has own error message and custom field to set maximum submits.
### EMAIL
Mails can be sent directly or queued ([OctoberCMS queue](https://octobercms.com/docs/services/queues) must be configured!).
Don't forget to configure mail preferences in *Settings > Mail > Mail configuration*!
#### Data in email templates
There are variables available in all email templates:
* **fields** is array of [ 'field name' => 'post value' ]
* **fieldsDetails** is array of [ 'field name' => ['name', 'value', 'type', ...] ]
* **uploads** is array of uploads (of class `System\Models\File`)
* **messageObject** is a model instance of a selected message
#### Allow autoreply
Email can be send to form sender as confirmation.
* You have to enter email address and name - it will be used as FROM field
* Email subject can be manually added here (or edited in *Settings > Mail > Mail templates (code: janvince.smallcontactform::mail.autoreply)*)
* Email TO address and name have to be assigned to form fields (in selections only corresponding field types are shown - if you don't see one, try to check it's type in Fields tab)
* Email REPLY TO address can be set
* Message field can be also assigned (and will be saved separately into database)
#### Allow notifications
Once a Contact form is sent a notification can be immediately send to a provided email address (or comma-separated list of addresses).
*A **Reply to** address of notification email will be set to an email address from Contact form (if this field is used).*
You can also force **From** address to be set to the one entered in Contact form - but not all email systems support this!
## TRANSLATION
You can allow translation with [RainLab Translate](https://octobercms.com/plugin/rainlab-translate) plugin.
> After installation of Translate plugin, please add at least two languages in *Settings > Translate > Manage languages*.
> For translations to work there must be a localePicker component included in your layout/page.
#### Form texts
Most of Small Contact form texts can be edited right in *Settings > Small plugins > Contact form*.
#### Custom form fields
Translate plugin doesn't supports translation of individual repeater fields yet, so form field texts (label, validation error messages) have to be - for now - translated in a dictionary: *Settings > Translate > Translate messages*
> Please note that form fields labels will be shown in dictionary after first form render (on your frontend page) and validation error messages after first send.
#### Email templates
You can create your own email templates in *Settings > Mail > Mail templates* (for hint look inside of default templates starting with *janvince.smallcontactform::*).
Remember your email templates CODE and put in in Small Contact form email settings in *Settings > Small plugins > Contact form > Email tab*. For each language there can be specific template.
There are `{{fields}}` and `{{fieldsDetails}}` arrays available inside of email templates.
You can also use `{{url}}` variable to get original request URL.
*If your custom form field has name eg. 'email', you use it in template with ````{{fields.email}}````.*
You can itterate over uploaded files with:
```
{% for item in uploads %}
<a href="{{ item.getPath }}">Uploaded file</a>
{% endfor %}
```
You can access model data with eg. `{{ messageObject.id }}`.
## GOOGLE ANALYTICS
> if you want to use these settings, be sure to have Google Analytics scripts included on your site. You can use [Rainlab Google Analytics plugin](https://octobercms.com/plugin/rainlab-googleanalytics).
### Events
You can allow events to be send to your GA account when the form is successfully sent.
There are (translatable) fields for category, action and label.
*All event settings can be overriden in component property so if you use more then one form, you can custommize events for each of them.*
## MESSAGES LIST
All sent data from Contact form are saved and listed in backend Messages list.
If email, name and message fields are assigned on *Settings > Small plugins > contact form > Columns mapping tab*, they will be saved and shown in separate columns.
You can click on a record to see all form data. The message will be marked as read.
## DASHBOARD REPORT WIDGETS
There are available report widgets to be used on OctoberCMS dashboard.
#### Messages stats
Shows basic messages statistics.
#### New messages
Shows number of new messages. The color changes to green if there are any.
You can simply click widget to open Messages list.
## Overriding form settings
You can override some of the form settings in component dropdown (on page or layout) or by passing them in component call.
#### Form settings
*There is also an Alias column that contain component's alias of the used form and is saved in messages log (this field is invisible by default in messages table).*
````
[contactForm myForm]
form_description = 'Form used in home page'
disable_fields = 'name|message'
send_btn_label = 'Go'
form_success_msg = 'Ok, sent :)'
form_error_msg = 'Houston, we have a problem'
````
You can override form's property in Twig component tag, eg:
````
{% component 'myForm' form_description = 'My other description' send_btn_label = 'Stay in touch' %}
````
This can be even more complex:
````
{% set myVar = 12345 %}
{% component 'myForm' form_description = ('Current value: ' ~ myVar) %}
````
In email template you can access some of these variables like this:
````
Form alias: {{fields.form_alias}}
Form description: {{fields.form_description}}
````
> When you override form description in ````{% component form_description = 'My description' %}````, description will be added as a **hidden field** into a form. Do not use this to store private data as this is easily visible in page HTML code!
#### Override notification email options
You can set different email address to which notification about sent form will be delivered and also change a notification template.
*Template must exist in Settings > Mail > Mail configuration*.
If you add a locale string to ````notification_template```` property (like ````notification_template_en````) than that one has priority and will be used if ````App::getLocale()```` returns ````en````.
````
[contactForm salesForm]
disable_notifications = true
notification_address_to = 'sales@domain.com'
notification_address_from = 'contactform@domain.com'
notification_template = 'notification-sales'
notification_template_en = 'notification-sales-en'
notification_template_cs = 'notification-sales-cs'
notification_subject = 'Notification sent by form {{ fields.form_alias }} on {{ "now"|date }}'
````
> Local strings in `notification_template` canot be used in Twig!
#### Override autoreply email options
You can set different email address and name for autoreply message and also use different autoreply template.
*Template must exist in Settings > Mail > Mail configuration*.
If you add a locale string to ````autoreply_template```` or ````autoreply_address_from_name```` property (like ````autoreply_template_en```` or ````autoreply_address_from_name_en````) than that one has priority and will be used if ````App::getLocale()```` returns ````en````.
````
[contactForm orderForm]
autoreply_address_from = 'order@domain.com'
autoreply_address_from_name = 'Orders'
autoreply_address_from_name_en = 'Orders'
autoreply_address_from_name_cs = 'Objednávky'
autoreply_template = 'autoreply-order'
autoreply_template_en = 'autoreply-order-en'
autoreply_template_cs = 'autoreply-order-cs'
autoreply_subject = 'Autoreply sent by form {{ fields.form_alias }} on {{ "now"|date }}'
````
> Do you know that you can use form variables in an email template subject. In Settings > Mail templates create new template and set the Subject field to eg: `My form {{ fields.form_alias }}`.
#### Disable some form fields
You can disable some of defined form fields by passing their names in ````disable_fields```` component property.
Several fields can be added while separated with pipe ````|````.
````
[contactForm]
disable_fields = 'phone|name|confirmation'
````
Or you can disable some of functions:
````
[contactForm]
disable_notifications = true
disable_autoreply = true
````
----
## HOWTO
### Fight SPAM
#### Prohibit sending URLs in a (message) field.
* Use Custom rule
* Add your validation error text
* Use validation rule: `custom_not_regex`
* Use validation: `/(http|https|ftp|ftps)\:\/\/?/`
![Custom regex to prevent sending URLs](https://www.vince.cz/storage/app/media/OctoberCMS/scf-custom-regex-urls.png)
### Add an empty option to dropdown field
You can easily add an empty option with empty ID and some value.
![Dropdown empty field](https://www.vince.cz/storage/app/media/OctoberCMS/scf-settings-dropdown.png)
#### Validate dropdown field
If you want to validate dropdown options, you can use custom validation rule `in` with list of IDs as a validation pattern.
![Dropdown validation](https://www.vince.cz/storage/app/media/OctoberCMS/scf-settings-dropdown-validation.png)
----
> My thanks goes to:
> [OctoberCMS](http://www.octobercms.com) team members and supporters for this great system.
> [Andrew Measham](https://unsplash.com/@andrewmeasham) for his photo.
> [Font Awesome](http://fontawesome.io/icons/) for nice icons.
Created by [Jan Vince](http://www.vince.cz), freelance web designer from Czech Republic.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
<div id="scf-{{ __SELF__ }}">
<div id="scf-message-{{ __SELF__ }}">
{% partial __SELF__ ~ '::scf-message' %}
</div>
<div id="scf-form-{{ __SELF__ }}">
{% partial __SELF__ ~ '::scf-form' %}
</div>
</div>

View File

@ -0,0 +1,61 @@
{% if formSentAlias == __SELF__.alias and formError is empty and settingsGet('form_hide_after_success', 0) %}
{# no errors and set to hide after send #}
{% else %}
{{ form_open(__SELF__.getFormAttributes) }}
{% for field in __SELF__.fields %}
{{ __SELF__.getFieldHtmlCode(field)|raw }}
{% endfor %}
{{ __SELF__.getAntispamFieldHtmlCode({})|raw }}
{{ __SELF__.getDescriptionFieldHtmlCode({})|raw }}
{{ __SELF__.getRedirectFieldHtmlCode({})|raw }}
{% if (settingsGet('google_recaptcha_version') is null or settingsGet('google_recaptcha_version') == 'v2checkbox') and settingsGet('add_google_recaptcha') and settingsGet('google_recaptcha_site_key') %}
<div class="{{__SELF__.getReCaptchaWrapperClass()}}">
<div class="g-recaptcha" data-sitekey="{{ settingsGet('google_recaptcha_site_key') }}"></div>
</div>
{% endif %}
{{ __SELF__.getSubmitButtonHtmlCode({})|raw }}
{{ form_close() }}
{% if settingsGet('add_google_recaptcha') and settingsGet('google_recaptcha_scripts_allow') %}
<script src='https://www.google.com/recaptcha/api.js{{ settingsGet("google_recaptcha_locale_allow") and currentLocale ? ("?hl="~currentLocale) }}' async defer></script>
{% if settingsGet('google_recaptcha_version') == 'v2invisible' %}
<script>
function onSubmit_{{ __SELF__.alias }}(token) {
return new Promise(function(resolve, reject) {
//Your code logic goes here
document.getElementById("{{'scf-form-id-'~__SELF__.alias}}").submit();
resolve();
}); //end promise
}
</script>
{% endif %}
{% endif %}
{% endif %}

View File

@ -0,0 +1,28 @@
{% if formSentAlias == __SELF__.alias %}
{% if formSuccess %}
{{ __SELF__.getGaSuccessEventHtmlCode(true)|raw }}
{% flash success %}
<div class="alert alert-{{ type == 'error' ? 'danger' : type }}">
{{ html_entity_decode(message)|nl2br }}
</div>
{% endflash %}
{% elseif formError %}
{% flash error %}
<div class="alert alert-{{ type == 'error' ? 'danger' : type }}">
{{ html_entity_decode(message)|nl2br }}
</div>
{% endflash %}
{% endif %}
{% endif %}

View File

@ -0,0 +1,8 @@
{
"name": "janvince/smallcontactform-plugin",
"type": "october-plugin",
"description": "None",
"require": {
"composer/installers": "~1.0"
}
}

View File

@ -0,0 +1,158 @@
<?php namespace Janvince\SmallContactform\Controllers;
use BackendMenu;
use Backend\Classes\Controller;
use JanVince\SmallContactForm\Models\Settings;
use JanVince\SmallContactForm\Models\Message;
use Flash;
use App;
use Carbon\Carbon;
use Redirect;
use Backend;
/**
* Messages Back-end Controller
*/
class Messages extends Controller
{
public $requiredPermissions = ['janvince.smallcontactform.access_messages'];
public $implement = [
'Backend.Behaviors.ListController',
'Backend.Behaviors.ImportExportController',
];
public $listConfig = 'config_list.yaml';
public $importExportConfig = 'config_export.yaml';
public function __construct()
{
parent::__construct();
BackendMenu::setContext('JanVince.SmallContactForm', 'smallcontactform', 'messages');
}
/**
* Generate messages statsitics
* @param $part
*/
public function getRecordsStats( $part ){
switch( $part ){
case 'all_count':
return Message::count();
break;
case 'read_count':
return Message::isRead()->count();
break;
case 'new_count':
return Message::isNew()->count();
break;
case 'latest_message_date':
$data = Message::orderBy( 'created_at', 'DESC' )->first();
if ( !empty( $data->created_at ) ) {
Carbon::setLocale( App::getLocale() );
return Carbon::createFromFormat( 'Y-m-d H:i:s', $data->created_at )->diffForHumans();
}
return NULL;
break;
case 'latest_message_name':
$data = Message::orderBy( 'created_at', 'DESC' )->first();
if( !empty( $data->name ) ) {
return $data->name;
}
return NULL;
break;
default:
return NULL;
break;
}
}
/**
* Preview page view
* @param $id
*/
public function preview( $id ){
$message = Message::find( $id );
if ( $message ) {
$this->vars['message'] = $message;
$message->new_message = 0;
$message->save();
} else{
Flash::error( e( trans( 'janvince.smallcontactform::lang.controller.preview.record_not_found') ) );
return Redirect::to( Backend::url( 'janvince/smallcontactform/messages' ) );
}
}
/**
* Index page view
*/
public function index(){
parent::index();
if (!$this->user->hasAccess('janvince.smallcontactform.access_messages')) {
Flash::error( e(trans('janvince.smallcontactform::lang.controllers.index.unauthorized')) );
return Redirect::to( Backend::url('/') );
}
}
/**
* Mark messages as read
* @param $record
*/
public function onMarkRead(){
if (!$this->user->hasAccess('janvince.smallcontactform.access_messages')) {
Flash::error( e(trans('janvince.smallcontactform::lang.controllers.index.unauthorized')) );
return;
}
if ( ($checkedIds = post('checked')) && is_array($checkedIds) && count($checkedIds) ) {
foreach ($checkedIds as $item) {
if (!$record = Message::find($item)) {
continue;
}
$record->new_message = 0;
$record->save();
}
Flash::success( e(trans('janvince.smallcontactform::lang.controller.scoreboard.mark_read_success')) );
return $this->listRefresh();
}
}
}

View File

@ -0,0 +1,72 @@
<div class="scoreboard">
<div data-control="toolbar">
<div class="scoreboard-item control-chart" data-control="chart-pie">
<ul>
<li data-color="#95b753"><?= e(trans('janvince.smallcontactform::lang.controller.scoreboard.new_count')) ?> <span><?= $this->getRecordsStats('new_count'); ?></span></li>
<li data-color="#d1d1d1"><?= e(trans('janvince.smallcontactform::lang.controller.scoreboard.read_count')) ?> <span><?= $this->getRecordsStats('read_count'); ?></span></li>
</ul>
</div>
<div class="scoreboard-item title-value">
<h4><?= e(trans('janvince.smallcontactform::lang.controller.scoreboard.new_count')) ?></h4>
<p><?= $this->getRecordsStats('new_count'); ?></p>
<p class="description"><?= e(trans('janvince.smallcontactform::lang.controller.scoreboard.new_description')) ?></p>
</div>
<div class="scoreboard-item title-value">
<h4><?= e(trans('janvince.smallcontactform::lang.controller.scoreboard.latest_record')) ?></h4>
<p class="oc-icon-user"><?= $this->getRecordsStats('latest_message_name'); ?></p>
<p class="description"><?= $this->getRecordsStats('latest_message_date'); ?></p>
</div>
</div>
</div>
<div data-control="toolbar">
<?php if ($this->user->hasAccess('janvince.smallcontactform.delete_messages')): ?>
<button
class="btn btn-default oc-icon-trash-o"
disabled="disabled"
onclick="$(this).data('request-data', {
checked: $('.control-list').listWidget('getChecked')
})"
data-request="onDelete"
data-request-confirm="<?= e(trans('backend::lang.list.delete_selected_confirm')) ?>"
data-trigger-action="enable"
data-trigger=".control-list input[type=checkbox]"
data-trigger-condition="checked"
data-request-success="$(this).prop('disabled', true)"
data-stripe-load-indicator>
<?= e(trans('backend::lang.list.delete_selected')) ?>
</button>
<?php endif ?>
<button
class="btn btn-default oc-icon-check-square"
disabled="disabled"
onclick="$(this).data('request-data', {
checked: $('.control-list').listWidget('getChecked')
})"
data-request="onMarkRead"
data-request-confirm="<?= e(trans('janvince.smallcontactform::lang.controller.scoreboard.mark_read_confirm')) ?>"
data-trigger-action="enable"
data-trigger=".control-list input[type=checkbox]"
data-trigger-condition="checked"
data-request-success="$(this).prop('disabled', true)"
data-stripe-load-indicator>
<?= e(trans('janvince.smallcontactform::lang.controller.scoreboard.mark_read')) ?>
</button>
<a href="<?= Backend::url('system/settings/update/janvince/smallcontactform/settings'); ?>" class="btn btn-info oc-icon-cogs"><?= e(trans('janvince.smallcontactform::lang.controller.scoreboard.settings_btn')) ?></a>
<?php if ($this->user->hasAccess('janvince.smallcontactform.export_messages')): ?>
<a
href="<?= Backend::url('janvince/smallcontactform/messages/export/') ?>"
class="btn btn-default oc-icon-download">
<?= e(trans('janvince.smallcontactform::lang.controllers.messages.export')) ?>
</a>
<?php endif ?>
</div>

View File

@ -0,0 +1,10 @@
# ===================================
# Import/Export Behavior Config
# ===================================
export:
title: janvince.smallcontactform::lang.controllers.messages.export
modelClass: JanVince\SmallContactForm\Models\MessageExport
list: $/janvince/smallcontactform/models/message/columns_export.yaml
redirect: /janvince/smallcontactform/messages
fileName: scf-export.csv

View File

@ -0,0 +1,10 @@
# ===================================
# Filter Scope Definitions
# ===================================
scopes:
date:
label: janvince.smallcontactform::lang.controller.filter.columns.date
type: daterange
conditions: date >= ':after' AND date <= ':before'

View File

@ -0,0 +1,43 @@
# ===================================
# List Behavior Config
# ===================================
# Model List Column configuration
list: $/janvince/smallcontactform/models/message/columns.yaml
# Model Class name
modelClass: JanVince\SmallContactForm\Models\Message
# List Title
title: "janvince.smallcontactform::lang.controllers.messages.list_title"
# Message to display if the list is empty
noRecordsMessage: backend::lang.list.no_records
recordUrl: 'janvince/smallcontactform/messages/preview/:id'
# Records to display per page
recordsPerPage: 20
# Displays the list column set up button
showSetup: true
# Displays the sorting link on each column
showSorting: true
# Default sorting column
defaultSort:
column: created_at
direction: desc
# Display checkboxes next to each record
showCheckboxes: true
# Toolbar widget configuration
toolbar:
# Partial for toolbar buttons
buttons: list_toolbar
# Search widget configuration
search:
prompt: backend::lang.list.search_prompt

View File

@ -0,0 +1,25 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?= Backend::url('janvince/smallcontactform/messages') ?>"><?= e(trans('janvince.smallcontactform::lang.controllers.messages.list_title')) ?></a></li>
<li><?= e(trans($this->pageTitle)) ?></li>
</ul>
<?php Block::endPut() ?>
<?= Form::open(['class' => 'layout']) ?>
<div class="layout-row">
<?= $this->exportRender() ?>
</div>
<div class="form-buttons">
<button
type="submit"
data-control="popup"
data-handler="onExportLoadForm"
data-keyboard="false"
class="btn btn-primary">
<?= e(trans('janvince.smallcontactform::lang.controllers.messages.export')) ?>
</button>
</div>
<?= Form::close() ?>

View File

@ -0,0 +1,2 @@
<?= $this->listRender() ?>

View File

@ -0,0 +1,71 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?= Backend::url('janvince/smallcontactform/messages') ?>"><?= e(trans('janvince.smallcontactform::lang.controllers.messages.list_title')); ?></a></li>
<li><?= e(trans('janvince.smallcontactform::lang.controllers.messages.preview')); ?></li>
</ul>
<?php Block::endPut() ?>
<?php if (!$this->fatalError): ?>
<div class="preview">
<h3><?php echo(e(trans('janvince.smallcontactform::lang.controllers.messages.preview_title')) ); ?></h3>
<p><strong><?php echo(e(trans('janvince.smallcontactform::lang.controllers.messages.preview_date')) ); ?></strong> <?php echo($message->created_at->format('j.n.Y H:i:s')); ?></p>
<br>
<p><strong><?php echo(e(trans('janvince.smallcontactform::lang.controllers.messages.preview_content_title')) ); ?></strong></p>
<table>
<?php foreach($message->form_data as $key => $field) : ?>
<tr>
<th class="p-r-md" style="vertical-align: top;"><?php echo($key); ?></th>
<td><?php echo( nl2br(html_entity_decode($field)) ) ?></td>
</tr>
<?php endforeach ?>
<?php if ($message->uploads): ?>
<tr>
<th>Uploads</th>
<td>
<?php foreach($message->uploads as $upload) : ?>
<a href="<?php echo( $upload->getPath() ) ?>" target="blank">
<img src="<?php echo( $upload->getThumb(300,300) ) ?>">
</a>
<?php endforeach ?>
</td>
</tr>
<?php endif ?>
<tr>
<th class="p-r-md"><?php echo(e(trans('janvince.smallcontactform::lang.controllers.messages.remote_ip')) ); ?></th>
<td><?php if(!empty($message->remote_ip)) { echo($message->remote_ip); } ?></td>
<tr>
<th class="p-r-md"><?php echo(e(trans('janvince.smallcontactform::lang.components.properties.form_description')) ); ?></th>
<td><?php if(!empty($message->form_description)) { echo($message->form_description); } ?></td>
</table>
<br>
<br>
</div>
<?php else: ?>
<p class="flash-message static error"><?= e($this->fatalError) ?></p>
<?php endif ?>
<p>
<a href="<?= Backend::url('janvince/smallcontactform/messages') ?>" class="btn btn-default oc-icon-chevron-left">
<?= e(trans('backend::lang.form.return_to_list')) ?>
</a>
</p>

View File

@ -0,0 +1,523 @@
<?php
return [
'plugin' => [
'name' => 'Kontaktní formulář',
'description' => 'Jednoduchý kontaktní formulář',
'category' => 'Small plugins',
],
'permissions' => [
'access_messages' => 'Přístup k seznamu zpráv',
'access_settings' => 'Přístup k nastavení',
'delete_messages' => 'Smazat vybrané zprávy',
'export_messages' => 'Exportovat zprávy',
],
'navigation' => [
'main_label' => 'Kontaktní formulář',
'messages' => 'Zprávy',
],
'controller' => [
'contact_form' => [
'name' => 'Kontaktní formulář',
'description' => 'Přidá do stránky kontaktní formulář',
'no_fields' => 'Přidejte prosím nějaká formulářová pole v administraci systému (Nastavení > Kontaktní formulář > Pole)...',
],
'filter' => [
'date' => 'Rozmezí data',
],
'scoreboard' => [
'records_count' => 'Zprávy',
'latest_record' => 'nejnovější od',
'new_count' => 'Nové',
'new_description' => 'Zpráv',
'read_count' => 'Přečtené',
'all_count' => 'Celkem',
'all_description' => 'Zpráv',
'settings_btn' => 'Nastavení formuláře',
'mark_read' => 'Označit jako přečtené',
'mark_read_confirm' => 'Opravdu chcete vybrané zprávy označit jako přečtené?',
'mark_read_success' => 'Zprávy byly označeny jako přečtené.',
],
'preview' => [
'record_not_found' => 'Zpráva nebyla nalezena!',
],
],
'models' => [
'message' => [
'columns' => [
'id' => 'ID',
'datetime' => 'Datum a čas',
'form_data' => 'Data formuláře',
'name' => 'Jméno',
'email' => 'Email',
'message' => 'Zpráva',
'new_message' => 'Stav',
'new' => 'Nová',
'read' => 'Přečtená',
'remote_ip' => 'IP odesílatele',
'created_at' => 'Datum vytvoření',
'updated_at' => 'Datum aktualizace',
]
],
],
'controllers' => [
'messages' => [
'list_title' => 'Zprávy',
'preview' => 'Náhled',
'preview_title' => 'Zpráva z kontaktního formuláře',
'preview_date' => 'Ze dne:',
'preview_content_title' => 'Obsah:',
'remote_ip' => 'odesláno z ip',
'form_alias' => 'Alias',
'form_description' => 'Popisek',
'export' => 'Export',
],
'index' => [
'unauthorized' => 'Neoprávněný přístup!',
],
],
'mail' => [
'templates' => [
'autoreply' => 'Zpráva automatické odpovědi z kontaktního formuláře (Anglicky)',
'notification' => 'Notifikace z kontaktního formuláře (Anglicky)',
'autoreply_cs' => 'Zpráva automatické odpovědi z kontaktního formuláře (Česky)',
'notification_cs' => 'Notifikace z kontaktního formuláře (Česky)',
]
],
'reportwidget' => [
'partials' => [
'messages' => [
'label' => 'Kontaktní formulář - Přehled zpráv',
'title' => 'Přehled zpráv',
'messages_all' => 'Vše',
'messages_new' => 'Nové',
'messages_read' => 'Přečtené',
],
'new_message' => [
'label' => 'Kontaktní formulář - Nové zprávy',
'title' => 'Nové zprávy',
'link_text' => 'Klikněte pro zobrazení přehledu zpráv',
],
],
],
'settings' => [
'form' => [
'css_class' => 'CSS třída formuláře',
'use_placeholders' => 'Používat zástupný text (placeholder)',
'use_placeholders_comment' => 'Místo popisků nad formulářovými poli bude použitý zástupný text',
'disable_browser_validation' => 'Zakázat validaci prohlížečem',
'disable_browser_validation_comment' => 'Nepovolit prohlížeči použít vlastní validaci a zobrazovat výstrahy.',
'success_msg' => 'Zpráva po úspěšném odeslání',
'success_msg_placeholder' => 'Formulář byl v pořádku odeslán.',
'error_msg' => 'Chybová zpráva',
'error_msg_placeholder' => 'Při odesílání formuláře došlo k chybě!',
'allow_ajax' => 'Povolit AJAX',
'allow_ajax_comment' => 'Povolí AJAX, ale umožní fungování formuláře i na prohlížečích s vypnutým JavaScriptem',
'allow_confirm_msg' => 'Požadovat potvrzení před odesláním',
'allow_confirm_msg_comment' => 'Zobrazí potvrzovací okno před odesláním formuláře',
'send_confirm_msg' => 'Text potvrzení',
'send_confirm_msg_placeholder' => 'Opravdu chcete odeslat formulář?',
'hide_after_success' => 'Skrýt formulář po úspěšném odeslání',
'hide_after_success_comment' => 'Po odeslání zobrazí pouze zprávu z potvrzením bez formuláře',
'add_assets' => 'Přidat doplňky',
'add_assets_comment' => 'Automaticky vloží potřebné CSS styly a JS skripty (Více informací je v souboru README.md)',
'add_css_assets' => 'Přidat CSS styly',
'add_css_assets_comment' => 'Vloží všechny potřebné styly',
'add_js_assets' => 'Přidat JS skripty',
'add_js_assets_comment' => 'Vloží všechny potřebné skripty',
'form_ga_event_success' => 'Událost po úspěšném odeslání',
],
'sections' => [
'ga_events' => 'Události'
],
'ga' => [
'ga_success_event_allow' => 'Zaznamenat událost po úspěšném odeslání formuláře',
],
'buttons' => [
'send_btn_text' => 'Text odesílacího tlačítka',
'send_btn_text_placeholder' => 'Odeslat',
'send_btn_css_class' => 'CSS třída odesílacího tlačítka',
'send_btn_css_class_placeholder' => 'btn btn-primary',
'send_btn_wrapper_css' => 'CSS třída kontejneru',
'send_btn_wrapper_css_placeholder' => 'form-group',
],
'redirect' => [
'allow_redirect' => 'Přesměrovat po úspěšném odeslání',
'allow_redirect_comment' => 'Přesměrovat na jinou stránku po úspěšném odeslání formuláře',
'redirect_url' => 'URL stránky pro přesměrování',
'redirect_url_comment' => 'Vložte URL adresu stránky, kam bude přesměrováno (např. /kontakt/diky)',
'redirect_url_placeholder' => '/kontakt/diky',
'redirect_url_external' => 'Externí URL',
'redirect_url_external_comment' => 'Toto je adresa externí stránky (např. http://www.domain.com)',
],
'form_fields' => [
'prompt' => 'Přidat nové pole formuláře',
'name' => 'NÁZEV POLE',
'name_comment' => 'Malými písmeny bez diakritiky (např. jmeno, email, vase_poznamka, ...)',
'type' => 'Typ pole',
'label' => 'Popisek (label)',
'label_placeholder' => 'Pole formuláře',
'field_styling' => 'Vlastní CSS třídy',
'field_styling_comment' => 'Můžete přidat vlastní styly',
'autofocus' => 'Automaticky zvýraznit (autofocus)',
'autofocus_comment' => 'Po zobrazení nastavit na poli kurzor',
'wrapper_css' => 'CSS třída kontejneru',
'wrapper_css_placeholder' => 'form-group',
'field_css' => 'CSS třida pole',
'field_css_placeholder' => 'form-control',
'label_css' => 'CSS třída popisku (label)',
'label_css_placeholder' => '',
'field_validation' => 'Validační pravidla pole',
'field_validation_comment' => 'Povolí nastavení vlastních validačních pravidel',
'validation' => 'Pravidlo',
'validation_prompt' => 'Přidat pravidlo',
'validation_type' => 'Typ',
'validation_error' => 'Chybová zpráva',
'validation_error_placeholder' => 'Prosím vložte správná data.',
'validation_error_comment' => 'Chybová hláška, která se zobrazí u pole',
'validation_custom_type' => 'Název validačního pravidla',
'validation_custom_type_comment' => 'Vložte název pravidla třídy Validator (např. regex, boolean, ...).<br>Přehled <a href="https://octobercms.com/docs/services/validation#available-validation-rules" target="_blank">validačních pravidel</a>.',
'validation_custom_type_placeholder' => 'regex',
'validation_custom_pattern' => 'Podmínka validačního pravidla',
'validation_custom_pattern_comment' => 'Nechte prázdné nebo doplňte podmínku pravidla (toto je pravá část zápisu validačního pravidla za dvojtečkou - např. [abc] pro pravidlo regex).',
'validation_custom_pattern_placeholder' => "/^[0-9]+$/",
'custom' => 'Vlastní pole',
'custom_description' => 'Vlastní pole s validačními pravidly',
'add_values_prompt' => 'Přidat hodnoty',
'field_value_id' => 'ID hodnoty',
'field_value_content' => 'Obsah',
'hit_type' => 'Hit type',
'event_category' => 'Kategorie události (event category)',
'event_action' => 'Akce události (event action)',
'event_label' => 'Štítek události (event label)',
'custom_code' => 'Vlastní kód',
'custom_code_comment' => 'Vložený kód přepíše automaticky generovaný kód políčka formuláře. Používejte opatrně!',
'custom_code_twig' => 'Povolit Twig',
'custom_code_twig_comment' => 'Můžete povolit parset syntaxe jazyka Twig.',
'custom_content' => 'Vlastní obsah',
'custom_content_comment' => 'Obsah bude přidaný k políčku formuláře.',
],
'form_field_types' => [
'text' => 'Text',
'email' => 'Email',
'textarea' => 'Textarea',
'checkbox' => 'Checkbox',
'dropdown' => 'Výběr (dropdown)',
'file' => 'Soubor',
'custom_code' => 'Vlastní kód',
'custom_content' => 'Vlastní obsah',
],
'form_field_validation' => [
'select' => '--- Vyberte pravidlo ---',
'required' => 'Vyžadováno',
'email' => 'Email',
'numeric' => 'Číslo',
'custom' => 'Vlastní pravidlo',
],
'email' => [
'address_from' => 'Adresa OD',
'address_from_placeholder' => 'john.doe@domain.com',
'address_from_name' => 'Jméno odesílatele',
'address_from_name_placeholder' => 'John Doe',
'address_replyto' => 'Adresa Odpovědět na',
'address_replyto_comment' => 'Odpověď na mail půjde na tuto adresu (REPLY-TO).',
'subject' => 'Předmět emailu',
'subject_comment' => 'Nastavte pouze pokud chcete přepsat předmět definovaný v šabloně (Nastavení > E-mailové šablony).',
'template' => 'Šablona emailu',
'template_comment' => 'Kód emailové šablony vytvořené v Nastavení > E-mailové šablony. Nechte prázdné pro výchozí šablonu: janvince.smallcontactform::mail.autoreply.',
'allow_email_queue' => 'Řadit do fronty',
'allow_email_queue_comment' => 'Přidat emaily do fronty místo okamžitého odeslání. Musíte ale nejdříve správně nakonfigurovat frontu systému OctoberCMS!',
'allow_notifications' => 'Povolit odesílání upozornění',
'allow_notifications_comment' => 'Odesílat upozornění, pokud někdo odešle formulář.',
'notification_address_to' => 'Upozornění posílat na adresu:',
'notification_address_to_comment' => 'Jedna emailová adresa nebo seznam adres oddělených čárkami',
'notification_address_to_placeholder' => 'notifications@domain.com',
'notification_address_from_form' => 'Nastavit adresu Od na email z formuláře (NEMUSÍ PODPOROVAT váš emailový systém!)',
'notification_address_from_form_comment' => 'Nastaví u odesílaného upozornění adresu Od (From) na tu, která byla zadána ve formuláři (sloupec email musí mít nastavenou vazbu).',
'allow_autoreply' => 'Povolit automatickou odpověď',
'allow_autoreply_comment' => 'Poslat automatickou odpověď odesílateli formuláře',
'autoreply_name_field' => 'Pole formuláře, které obsahuje JMÉNO odesílatele',
'autoreply_name_field_empty_option' => '-- Vyberte --',
'autoreply_name_field_comment' => 'Pole typu Text.',
'autoreply_email_field' => 'Pole formuláře, které obsahuje ADRESU odesílatele',
'autoreply_email_field_empty_option' => '-- Vyberte --',
'autoreply_email_field_comment' => 'Pole typu Email.',
'autoreply_message_field' => 'Pole formuláře, které obsahuje ZPRÁVU',
'autoreply_message_field_empty_option' => '-- vyberte --',
'autoreply_message_field_comment' => 'Pole typu Textarea nebo Text.',
'notification_template' => 'Šablona notifikačního emailu',
'notification_template_comment' => 'Kód emailové šablony vytvořené v Nastavení > E-mailové šablony. Nechte prázdné pro výchozí šablonu: janvince.smallcontactform::mail.notification.',
],
'antispam' => [
'add_antispam' => 'Přidat pasivní ochranu proti spamu',
'add_antispam_comment' => 'Přidá jednoduchou ale efektivní pasivní ochranu proti robotům (více informací v souboru README.md)',
'antispam_delay' => 'Zpoždění formuláře (s)',
'antispam_delay_comment' => 'Test na příliš rychlé odeslání formuláře (většinou roboty)',
'antispam_delay_placeholder' => '3',
'antispam_label' => 'Popisek (label) antispamového pole',
'antispam_label_comment' => 'Popisek bude viditelný pouze na prohlížečích bez podpory JavaScriptu',
'antispam_label_placeholder' => 'Prosím vymažte toto pole',
'antispam_error_msg' => 'Chybová zprávy',
'antispam_error_msg_comment' => 'Zpráva, která se zobrazí, pokud se aktivuje pasivní antispam',
'antispam_error_msg_placeholder' => 'Prosím vymažte obsah tohoto pole!',
'antispam_delay_error_msg' => 'Chybová zprávy při rychlém odeslání',
'antispam_delay_error_msg_comment' => 'Zpráva, která se zobrazí při příliš rychlém odeslání formuláře',
'antispam_delay_error_msg_placeholder' => 'Příliš rychlé odeslání formuláře! Prosím zkuste to za pár vteřin znovu!',
'add_google_recaptcha' => 'Přidat Google reCaptcha',
'add_google_recaptcha_comment' => 'Přidá reCaptcha do kontaktního formuláře (více informací v souboru README.md). <br>API klíče můžete získat na <a href="https://www.google.com/recaptcha/admin#list" target="_blank">stránce Google reCaptcha</a>.',
'google_recaptcha_version' => 'Verze Google reCaptcha',
'google_recaptcha_version_comment' => 'Zvolte verzi reCaptcha widgetu.<br>Více informací naleznete na <a href="https://developers.google.com/recaptcha/docs/versions" target="_blank">webu Google reCaptcha</a>.',
'google_recaptcha_versions' => [
'v2checkbox' => 'reCaptcha V2 zaškrtávací pole',
'v2invisible' => 'reCaptcha V2 neviditelná',
],
'google_recaptcha_site_key' => 'Site key',
'google_recaptcha_site_key_comment' => 'Vložte svůj "site key"',
'google_recaptcha_secret_key' => 'Secret key',
'google_recaptcha_secret_key_comment' => 'Vložte svůj "secret key"',
'google_recaptcha_wrapper_css' => 'CSS třída kontejneru reCaptcha boxu',
'google_recaptcha_wrapper_css_comment' => 'CSS třída kontejneru, ve kterém je vložený box reCaptcha',
'google_recaptcha_wrapper_css_placeholder' => 'form-group',
'google_recaptcha_error_msg' => 'Chybová zpráva',
'google_recaptcha_error_msg_comment' => 'Zpráva, která se zobrazí, pokud dojde chybě při ověření reCAPTCHA.',
'google_recaptcha_error_msg_placeholder' => 'Chyba při ověření pomocí Google reCAPTCHA!',
'google_recaptcha_scripts_allow' => 'Automaticky přidat Google reCAPTCHA sckript',
'google_recaptcha_scripts_allow_comment' => 'Vloží odkaz na JavaScriptový soubor potřebný pro fungování reCAPTCHA.',
'google_recaptcha_locale_allow' => 'Povolit detekci jazyka',
'google_recaptcha_locale_allow_comment' => 'Přidá k reCAPTCHA skriptu kód jazyka stránky, takže ověřovací box bude mluvit jazykem návštěvníka webu.',
'add_ip_protection' => 'Testovat IP adresu odesílatele',
'add_ip_protection_comment' => 'Nepovolí příliš mnoho odeslání formuláře z jedné IP adresy',
'add_ip_protection_count' => 'Maximální počet odeslání během jednoho dne',
'add_ip_protection_count_comment' => 'Počet povolených odeslání formuláře z jedné IP adresy během jednoho dne',
'add_ip_protection_count_placeholder' => '3',
'add_ip_protection_error_get_ip' => 'Nepodařilo se určit vaši IP adresu!',
'add_ip_protection_error_too_many_submits' => 'Chybová zpráva při překročení počtu odeslání',
'add_ip_protection_error_too_many_submits_comment' => 'Zpráva, kterou obdrží uživatel při překročení limitu počtu odeslání formuláře',
'add_ip_protection_error_too_many_submits_placeholder' => 'Byl překročen limit odeslání formuláře během jednoho dne!',
'disabled_extensions' => 'Zakázaná rozšíření',
'disabled_extensions_comment' => 'Nastavení ze záložky Soukromí zkusobila vypnutí těchto rozšíření',
],
'mapping' => [
'hint' => [
'title' => 'Proč vazby na sloupce?',
'content' => '
<p>Můžete vytvořit libovolný formulář s vlastními poli a jejich typy.</p>
<p>Systém zapíše do databáze všechna odeslaná data formuláře, ale pro Přehled zpráv jsou zvlášť ukládána pole Jméno, Email a Zpráva.</p>
<p>Proto je nutné identifikovat pro tyto sloupce odpovídající pole ve vašem formuláři.</p>
<p><em>Vytvořené vazby jsou použité i při odesílání automatických odpovědí, kde je nutné vazba alespoň na pole Email.</em></p>
',
],
'warning' => [
'title' => 'Nevidíte vaše formulářová pole?',
'content' => '
<p>Pokud zde nevidíte svá formulářová pole, klikněte dole na tlačítko Uložit a pak obnovte stránku (F5 nebo Ctr+R / Cmd+R).</p>
',
],
],
'privacy' => [
'disable_messages_saving' => 'Zakázat ukládání zpráv',
'disable_messages_saving_comment' => 'Pokud je zaškrtnuto, odeslané zprávy se nebudou ukládat do databáze.<br><strong>Tato volba zároveň zakáže použití IP ochrany!</strong>',
'disable_messages_saving_comment_section' => '<div class="callout fade in callout-danger no-subheader"><div class="header"><i class="icon-warning"></i><h3>Ujistěte se, že máte povoleny notifikační emaily, jinak nebudete mít žádná data z odeslaných formulářů!</h3></div></div>',
],
'tabs' => [
'form' => 'Formulář',
'buttons' => 'Odesílací tlačítko',
'form_fields' => 'Pole formuláře',
'mapping' => 'Vazby sloupců',
'email' => 'Email',
'antispam' => 'Antispam',
'privacy' => 'Soukromí'
],
],
'components' => [
'groups' => [
'hacks' => 'Hacks',
'override_form' => 'Přepsat nastavení formuláře',
'override_notifications' => 'Přepsat nastavení notifikací',
'override_autoreply' => 'Přepsat nastavení automatických odpovědí',
'override' => 'Přepsat nastavení',
'override_redirect' => 'Přepsat nastavení přesměrování',
'override_ga' => 'Přepsat nastavení Google Analytics',
],
'properties' => [
'disable_notifications' => 'Zakázat odesílání notifikačních emailů',
'disable_notifications_comment' => 'Zakáže odeslání notifikáčních emailů (bez ohledu na systémová nastavení formuláře)',
'form_description' => 'Popisek formuláře',
'form_description_comment' => 'Volitelně můžete přidat popisek formuláře, který se uloží společně s odeslanými daty do seznamu zpráv. Můžete použít i {{ :slug }}.',
'disable_fields' => 'Zakázat pole',
'disable_fields_comment' => 'Vložte názvy polí oddělené trubkou (např. name|message|phone)',
'send_btn_label' => 'Popisek odesílacího tlačítka',
'send_btn_label_comment' => 'Přepíše výchozí text odesílacího tlačítka',
'form_success_msg' => 'Zpráva po úspěšném odeslání',
'form_success_msg_comment' => 'Přepíše výchozí zprávu zobrazenou po úspěšném odeslání formuláře',
'form_error_msg' => 'Zpráva po chybě při odeslání',
'form_error_msg_comment' => 'Přepíše výchozí zprávu zobrazenou po neúspěšném odeslání formuláře',
'notification_address_to' => 'Adresa KOMU',
'notification_address_to_comment' => 'Přepíše adresu KOMU v notifikačním emailu',
'notification_address_from' => 'Adresa OD',
'notification_address_from_comment' => 'Přepíše adresu OD v notifikačním emailu',
'notification_address_from_name' => 'Jméno pro adresu OD',
'notification_address_from_name_comment' => 'Přepíše jméno zobrazené spolu s emailem OD',
'notification_template' => 'Šablona notifikace',
'notification_template_comment' => 'Přepíše šablonu notifikačního emailu',
'notification_subject' => 'Předmět notifikace',
'notification_template_comment' => 'Přepíše předmět emailu',
'disable_autoreply' => 'Zakázat notifikace',
'disable_autoreply_comment' => 'Zakáže odesílání notifikací',
'autoreply_address_from' => 'Adresa OD',
'autoreply_address_from_comment' => 'Přepíše adresu od v automatické odpovědi po odeslání formuláře',
'autoreply_address_from_name' => 'Jméno pro adresu OD',
'autoreply_address_from_name_comment' => 'Přepíše jméno zobrazené spolu s emailem OD',
'autoreply_address_replyto' => 'Adresa ODPOVĚDĚT NA',
'autoreply_address_replyto_comment' => 'Přepíše adresu REPLY TO v automatické odpovědi po odeslání formuláře.',
'autoreply_template' => 'Šablona automatické odpovědi',
'autoreply_template_comment' => 'Přepíše šablonu emailu automatické odpovědi',
'autoreply_subject' => 'Předmět automatické odpovědi',
'autoreply_template_comment' => 'Přepíše předmět emailu',
]
],
];

View File

@ -0,0 +1,491 @@
<?php
return [
'plugin' => [
'name' => 'Kontaktformular',
'description' => 'Kontaktformular',
'category' => 'Kontaktformular',
],
'permissions' => [
'access_messages' => 'Auf Nachrichtenliste zugreifen',
'access_settings' => 'Backendeinstellungen bearbeiten',
'delete_messages' => 'gespeicherte Nachrichten löschen',
'export_messages' => 'gespeicherte Nachrichten exportieren',
],
'navigation' => [
'main_label' => 'Kontaktformular',
'messages' => 'Nachrichten',
],
'controller' => [
'contact_form' => [
'name' => 'Kontaktformular',
'description' => 'Kontaktformular in die Seite einfügen',
'no_fields' => 'Bitte fügen Sie zuerst einige Formularfelder in der Backend-Administration hinzu (in Einstellungen > Kontaktformular > Felder)...',
],
'filter' => [
'date' => 'Datumsbereich',
],
'scoreboard' => [
'records_count' => 'Nachrichten',
'latest_record' => 'Letzte von',
'new_count' => 'Neu',
'new_description' => 'Nachrichten',
'read_count' => 'Gelesen',
'all_count' => 'Insgesamt',
'all_description' => 'Nachrichten',
'settings_btn' => 'Formular Einstellungen',
'mark_read' => 'Als Gelesen markieren',
'mark_read_confirm' => 'Möchten Sie wirklich die ausgewählten Nachrichten als gelesen markeiren?',
'mark_read_success' => 'Erfolgreich als gelesen markiert.',
],
'preview' => [
'record_not_found' => 'Nachricht nicht gefunden!',
],
],
'models' => [
'message' => [
'columns' => [
'id' => 'ID',
'datetime' => 'Datum und Zeit',
'form_data' => 'Daten',
'name' => 'Name',
'email' => 'Email',
'message' => 'Nachricht',
'new_message' => 'Status',
'new' => 'Neu',
'read' => 'Gelesen',
'remote_ip' => 'IP des Absenders',
'form_alias' => 'Alias',
'form_description' => 'Beschreibung',
'created_at' => 'Erstellt am',
'updated_at' => 'Geupdated am',
]
],
],
'controllers' => [
'messages' => [
'list_title' => 'Nachrichten',
'preview' => 'Vorschau',
'preview_title' => 'Nachricht',
'preview_date' => 'Datum:',
'preview_content_title' => 'Inhalt:',
'remote_ip' => 'Von IP gesendet:',
'export' => 'Exportieren',
],
'index' => [
'unauthorized' => 'Unerlaubter Zugriff',
],
],
'mail' => [
'templates' => [
'autoreply' => 'Form autoreply message (English)',
'autoreply_cs' => 'Form autoreply message (Czech)',
'notification' => 'Form notification message (English)',
'notification_cs' => 'Form notification message (Czech)',
]
],
'reportwidget' => [
'partials' => [
'messages' => [
'label' => 'Kontaktformular - Nachrichtenstatistik',
'title' => 'Nachrichtenstatistik',
'messages_all' => 'Alle',
'messages_new' => 'Neu',
'messages_read' => 'Gelesen',
],
'new_message' => [
'label' => 'Kontaktformular - Neue Nachrichten',
'title' => 'Neue Nachrichten',
'link_text' => 'Hier klicken, um die Liste aller Nachrichten anzuzeigen',
],
],
],
'settings' => [
'form' => [
'css_class' => 'Formular CSS Klasse',
'use_placeholders' => 'Platzhalter benutzen',
'use_placeholders_comment' => 'Platzhalter werden anstelle von Labels verwendet',
'disable_browser_validation' => 'Browservalidierung deaktivieren',
'disable_browser_validation_comment' => 'Integrierte Validierung und Popups im Browser nicht zulassen.',
'success_msg' => 'Form success message',
'success_msg_placeholder' => 'Wir haben Ihre Nachricht erhalten.',
'error_msg' => 'Form error message',
'error_msg_placeholder' => 'Es gab einen Fehler beim Senden Ihrer Daten!',
'allow_ajax' => 'Enable AJAX',
'allow_ajax_comment' => 'AJAX mit Fallback für Browser ohne JavaScript verwenden',
'allow_confirm_msg' => 'Vor dem Absenden des Formulars um Bestätigung bitten',
'allow_confirm_msg_comment' => 'Bestätigungsdialog vor dem Senden hinzufügen',
'send_confirm_msg' => 'Bestätigungs-Text',
'send_confirm_msg_placeholder' => 'Sind Sie sicher?',
'hide_after_success' => 'Formular nach erfolgreichem Senden ausblenden',
'hide_after_success_comment' => 'Nur Erfolgsmeldung ohne Formular anzeigen',
'add_assets' => 'Assets hinzufügen',
'add_assets_comment' => 'Automatisches Hinzufügen notwendiger CSS- und JS-Assets (mehr über Assets in der Datei README.md)',
'add_css_assets' => 'CSS Assets hinzufügen',
'add_css_assets_comment' => 'Alle benötigten CSS Dateien werden hinzugefügt',
'add_js_assets' => 'JavaScript Assets hinzufügen',
'add_js_assets_comment' => 'Alle benötigten JavaScript Dateien werden hinzugefügt',
],
'buttons' => [
'send_btn_text' => 'Text senden Button',
'send_btn_text_placeholder' => 'Absenden',
'send_btn_css_class' => 'CSS Klasse senden Button',
'send_btn_css_class_placeholder' => 'btn btn-primary',
'send_btn_wrapper_css' => 'CSS Klasse des senden Button wrappers',
'send_btn_wrapper_css_placeholder' => 'form-group',
],
'redirect' => [
'allow_redirect' => 'Nach dem Einreichen umleiten',
'allow_redirect_comment' => 'Nach erfolgreicher Übermittlung auf eine andere Seite umleiten',
'redirect_url' => 'URL der Seite, auf die umgeleitet werden soll',
'redirect_url_comment' => 'Geben Sie die URL Ihrer Seite ein (bspw. /kontact/danke)',
'redirect_url_placeholder' => '/kontakt/danke',
'redirect_url_external' => 'Externe URL',
'redirect_url_external_comment' => 'Dies ist ein externer URL-Pfad (bspw. http://www.domain.com',
],
'form_fields' => [
'prompt' => 'Neues Formularfeld hinzufügen',
'name' => 'FELDNAME',
'name_comment' => 'Kleinbuchstaben ohne Sonderzeichen (bspw. name, email, adresse, ...)',
'type' => 'Feldtyp',
'label' => 'Label',
'label_placeholder' => 'Vollständiger Name',
'field_styling' => 'Eigene CSS Klasse',
'field_styling_comment' => 'Ändern der Standard-Bootstrap-Stile',
'autofocus' => 'Autofokus-Feld',
'autofocus_comment' => 'Autofokus für dieses Formularfeld',
'wrapper_css' => 'Wrapper CSS-Klasse',
'wrapper_css_placeholder' => 'form-group',
'field_css' => 'Feld CSS-Klasse',
'field_css_placeholder' => 'form-control',
'label_css' => 'Label CSS-Klasse',
'label_css_placeholder' => '',
'field_validation' => 'Feldüberprüfung',
'field_validation_comment' => 'Feldüberprüfungsregeln hinzufügen',
'validation' => 'Validierung',
'validation_prompt' => 'Validierung hinzufügen',
'validation_type' => 'Validierungsregel',
'validation_error' => 'Validierungs-Fehlermeldung',
'validation_error_placeholder' => 'Bitte gültige Daten eingeben.',
'validation_error_comment' => 'Fehlermeldung, die zu verwenden ist, wenn die Validierung fehlschlägt',
'validation_custom_type' => 'Name der Validierungsregel',
'validation_custom_type_comment' => 'Validator-Regelnamen eingeben (bspw. regex, boolean, ...).<br>See <a href="https://octobercms.com/docs/services/validation#available-validation-rules" target="_blank">validation rules</a>.',
'validation_custom_type_placeholder' => 'regex',
'validation_custom_pattern' => 'Muster für Validierungsregeln',
'validation_custom_pattern_comment' => 'Left empty or enter custom rule pattern (this is a right part of Validator rule after colon - eg. [abc] for regex).',
'validation_custom_pattern_placeholder' => "/^[0-9]+$/",
'custom' => 'Benutzerdefiniertes Feld',
'custom_description' => 'Benutzerdefiniertes Feld mit Validierungsoption',
'add_values_prompt' => 'Werte hinzufügen',
'field_value_id' => 'Feld ID',
'field_value_content' => 'Feld Inhalt',
],
'form_field_types' => [
'text' => 'Text',
'email' => 'Email',
'textarea' => 'Textbereich',
'checkbox' => 'Checkbox',
'dropdown' => 'Dropdown-Menü',
'file' => 'File',
'custom_code' => 'Custom code',
'custom_content' => 'Custom content',
],
'form_field_validation' => [
'select' => '--- Validierung auswählen ---',
'required' => 'erforderlich',
'email' => 'Email',
'numeric' => 'Numerisch',
'custom' => 'Benutzerdefinierte Regel',
],
'email' => [
'address_from' => 'Absenderaddresse',
'address_from_placeholder' => 'max.mustermann@domain.de',
'address_from_name' => 'Absendername',
'address_from_name_placeholder' => 'Max Mustermann',
'subject' => 'Email subject',
'subject_comment' => 'Nur einstellen, wenn Sie andere als die unter Einstellungen > Mail-Vorlagen definierten Einstellungen wünschen.',
'template' => 'Email template',
'template_comment' => 'Code der E-Mail-Vorlage, die unter Einstellungen > E-Mail-Vorlagen erstellt wurde. Bei Standardvorlage leer lassen: janvince.smallcontactform::mail.autoreply.',
'allow_email_queue' => 'E-Mail in Warteschlange setzen',
'allow_email_queue_comment' => 'Fügen Sie E-Mails in die Warteschlange ein, anstatt sie sofort zu senden. Sie müssen zuerst Ihre OctoberCMS-Warteschlange konfigurieren!',
'allow_notifications' => 'Benachrichtigungen zulassen',
'allow_notifications_comment' => 'Benachrichtigung senden, nachdem das Formular gesendet wurde',
'notification_address_to' => 'Benachrichtigung an E-Mail senden',
'notification_address_to_comment' => 'Eine E-Mail-Adresse oder eine durch Komma getrennte Liste von Adressen',
'notification_address_to_placeholder' => 'kontakt@domain.de',
'notification_address_from_form' => 'Benachrichtigung von der Adresse des Absenders erzwingen (NICHT von allen E-Mail-Systemen UNTERSTÜTZT!)',
'notification_address_from_form_comment' => 'Benachrichtigung von Adresse auf eine im Kontaktformular eingegebene E-Mail setzen (das Feld muss in der Spaltenzuordnung gesetzt werden).',
'allow_autoreply' => 'Autoreply zulassen',
'allow_autoreply_comment' => 'Senden Sie eine Kopie des Formularinhalts an den Autor',
'autoreply_name_field' => 'NAME form field',
'autoreply_name_field_empty_option' => '-- Select --',
'autoreply_name_field_comment' => 'Must be type of Text.<br><em>Save and refresh this page if you can\'t see your fields.</em>',
'autoreply_email_field' => 'EMAIL address form field',
'autoreply_email_field_empty_option' => '-- Select --',
'autoreply_email_field_comment' => 'Must be type of Email.<br><em>Save and refresh this page if you can\'t see your fields.</em>',
'autoreply_message_field' => 'MESSAGE form field',
'autoreply_message_field_empty_option' => '-- Select --',
'autoreply_message_field_comment' => 'Must be type of Textarea or Text.<br><em>Save and refresh this page if you can\'t see your fields.</em>',
'notification_template' => 'Notification email template',
'notification_template_comment' => 'Code of email template created in Settings > Email templates. Left empty for default template: janvince.smallcontactform::mail.autoreply.',
],
'antispam' => [
'add_antispam' => 'Add passive antispam protection',
'add_antispam_comment' => 'Add simple but effective passive antispam control (more info in README.md file)',
'antispam_delay' => 'Antispam delay (s)',
'antispam_delay_comment' => 'Delay protection for too fast form sending (usually by robots)',
'antispam_delay_placeholder' => '3',
'antispam_label' => 'Antispam field label',
'antispam_label_comment' => 'Label will be visible for non JavaScript enabled browsers',
'antispam_label_placeholder' => 'Please clear this field',
'antispam_error_msg' => 'Error message',
'antispam_error_msg_comment' => 'Message to show to user when antispam protection is triggered',
'antispam_error_msg_placeholder' => 'Please empty this field!',
'antispam_delay_error_msg' => 'Delay error message',
'antispam_delay_error_msg_comment' => 'Message to show to user when form was sent too fast',
'antispam_delay_error_msg_placeholder' => 'Form sent too fast! Please wait few seconds and try again!',
'add_google_recaptcha' => 'Add Google reCaptcha',
'add_google_recaptcha_comment' => 'Add reCaptcha to Contact Form (more info in README.md file).<br>You can get API keys on <a href="https://www.google.com/recaptcha/admin#list" target="_blank">Google reCaptcha site</a>.',
'google_recaptcha_version' => 'Google reCaptcha version',
'google_recaptcha_version_comment' => 'Choose a version of reCaptcha widget.<br>More info on <a href="https://developers.google.com/recaptcha/docs/versions" target="_blank">Google reCaptcha site</a>.',
'google_recaptcha_versions' => [
'v2checkbox' => 'reCaptcha V2 checkbox',
'v2invisible' => 'reCaptcha V2 invisible',
],
'google_recaptcha_site_key' => 'Site key',
'google_recaptcha_site_key_comment' => 'Put your site key',
'google_recaptcha_secret_key' => 'Secret key',
'google_recaptcha_secret_key_comment' => 'Put your secret key',
'google_recaptcha_wrapper_css' => 'reCaptcha box wrapper CSS class',
'google_recaptcha_wrapper_css_comment' => 'CSS class of wrapper box around reCaptcha box',
'google_recaptcha_wrapper_css_placeholder' => 'form-group',
'google_recaptcha_error_msg' => 'Error message',
'google_recaptcha_error_msg_comment' => 'Message to show to user when reCAPTCHA is not validated.',
'google_recaptcha_error_msg_placeholder' => 'Google reCAPTCHA validation error!',
'google_recaptcha_scripts_allow' => 'Automatically add necessary JS scripts',
'google_recaptcha_scripts_allow_comment' => 'This will add link to JS scripts to your site.',
'google_recaptcha_locale_allow' => 'Allow locale detection',
'google_recaptcha_locale_allow_comment' => 'This will add curent web page locale to reCAPTCHA script, so it will translated.',
'add_ip_protection' => 'Check sender\'s IP',
'add_ip_protection_comment' => 'Do not allow too many form submits from one IP address',
'add_ip_protection_count' => 'Maximum form submits during a day',
'add_ip_protection_count_comment' => 'Number of allowed submits from one IP address during a single day',
'add_ip_protection_count_placeholder' => '3',
'add_ip_protection_error_get_ip' => 'We wasn\'t able to determine your IP address!',
'add_ip_protection_error_too_many_submits' => 'Too many submits error message',
'add_ip_protection_error_too_many_submits_comment' => 'Error message to show to the user',
'add_ip_protection_error_too_many_submits_placeholder' => 'Too many form submits from one address today!',
'disabled_extensions' => 'Disabled extensions',
'disabled_extensions_comment' => 'Settings set on Privacy tab disabled these extensions',
],
'mapping' => [
'hint' => [
'title' => 'Why fields mapping?',
'content' => '
<p>You can build a custom form with own field names and types.</p>
<p>System writes all form data in database, but for quick overview Name, Email and Message columns are visible separately in Messages list.</p>
<p>So you have to help system to identify these columns by mapping to your form fields.</p>
<p><em>These mappings are also used for autoreply emails where at least Email field mapping is important.</em></p>
',
],
'warning' => [
'title' => 'Can\'t select your form fields?',
'content' => '
<p>If you don\'t see your form fields, click on button Save at the bottom of this page and then reload page (F5 or Ctr+R / Cmd+R).</p>
',
],
],
'privacy' => [
'disable_messages_saving' => 'Disable messages saving',
'disable_messages_saving_comment' => 'When checked, no data will saved in Messages list.<br><strong>This will also disable IP protection!</strong>',
'disable_messages_saving_comment_section' => '<div class="callout fade in callout-danger no-subheader"><div class="header"><i class="icon-warning"></i><h3>Be sure to allow notification emails or you will have no data from sent forms!</h3></div></div>',
],
'tabs' => [
'form' => 'Form',
'buttons' => 'Send button',
'form_fields' => 'Fields',
'mapping' => 'Columns mapping',
'email' => 'Email',
'antispam' => 'Antispam',
'privacy' => 'Privacy'
],
],
'components' => [
'groups' => [
'hacks' => 'Hacks',
'override_form' => 'Override form settings',
'override_notifications' => 'Override notification settings',
'override_autoreply' => 'Override autoreply settings',
'override' => 'Override form settings',
],
'properties' => [
'form_description' => 'Form description',
'form_description_comment' => 'You can add optional form description, that will be saved with other sent data in the messages list. You can also use {{ :slug }} here.',
'disable_fields' => 'Disable fields',
'disable_fields_comment' => 'This will disable listed fields. Add field names separated by pipe (eg. name|message|phone)',
'send_btn_label' => 'Send button label',
'send_btn_label_comment' => 'Override send button label',
'form_success_msg' => 'Success message',
'form_success_msg_comment' => 'Override success message shown after successful sent',
'form_error_msg' => 'Error message',
'form_error_msg_comment' => 'Override error message shown after unsuccessful sent',
'disable_notifications' => 'Disable notification',
'disable_notifications_comment' => 'This will disable notification emails (overrides form settings)',
'notification_address_to' => 'Address TO',
'notification_address_to_comment' => 'This will override email address where notification email will be sent (if enabled in form settings)',
'notification_address_from' => 'Address FROM',
'notification_address_from_comment' => 'This will override email address from where notification email will be sent',
'notification_address_from_name' => 'Address FROM name',
'notification_address_from_name_comment' => 'This will override email address name from where notification email will be sent',
'notification_template' => 'Notification template',
'notification_template_comment' => 'This will override notification email template (eg. janvince.smallcontactform::mail.notification)',
'disable_autoreply' => 'Disable notification',
'disable_autoreply_comment' => 'This will disable notification emails (overrides form settings)',
'autoreply_address_from' => 'Address FROM',
'autoreply_address_from_comment' => 'This will override email address in autoreply email (if enabled in form settings)',
'autoreply_address_from_name' => 'Address (FROM) name',
'autoreply_address_from_name_comment' => 'This will override email address name in autoreply email (if enabled in form settings)',
'autoreply_template' => 'Autoreply template',
'autoreply_template_comment' => 'This will override autoreply email template (eg. janvince.smallcontactform::mail.autoreply)',
]
],
];

View File

@ -0,0 +1,529 @@
<?php
return [
'plugin' => [
'name' => 'Contact form',
'description' => 'Simple contact form builder',
'category' => 'Small plugins',
],
'permissions' => [
'access_messages' => 'Access messages list',
'access_settings' => 'Manage backend preferences',
'delete_messages' => 'Delete stored messages',
'export_messages' => 'Export messages',
],
'navigation' => [
'main_label' => 'Contact form',
'messages' => 'Messages',
],
'controller' => [
'contact_form' => [
'name' => 'Contact form',
'description' => 'Insert contact form to the page',
'no_fields' => 'Please add some form fields in backend administration first (in Settings > Small Contact form > Fields)...',
],
'filter' => [
'date' => 'Date range',
],
'scoreboard' => [
'records_count' => 'Messages',
'latest_record' => 'Latest from',
'new_count' => 'New',
'new_description' => 'Messages',
'read_count' => 'Read',
'all_count' => 'Total',
'all_description' => 'Messages',
'settings_btn' => 'Form settings',
'mark_read' => 'Mark as read',
'mark_read_confirm' => 'Really set selected messages as read?',
'mark_read_success' => 'Successfully marked as read.',
],
'preview' => [
'record_not_found' => 'Message not found!',
],
],
'models' => [
'message' => [
'columns' => [
'id' => 'ID',
'datetime' => 'Date and time',
'form_data' => 'Form data',
'name' => 'Name',
'email' => 'Email',
'message' => 'Message',
'new_message' => 'Status',
'new' => 'New',
'read' => 'Read',
'remote_ip' => 'Sender\'s IP',
'form_alias' => 'Alias',
'form_description' => 'Description',
'created_at' => 'Created at',
'updated_at' => 'Updated at',
'url' => 'URL',
'files' => 'Files',
]
],
],
'controllers' => [
'messages' => [
'list_title' => 'Messages',
'preview' => 'Preview',
'preview_title' => 'Contact form message',
'preview_date' => 'From date:',
'preview_content_title' => 'Content:',
'remote_ip' => 'Sent from ip',
'export' => 'Export',
],
'index' => [
'unauthorized' => 'Unauthorized access',
],
],
'mail' => [
'templates' => [
'autoreply' => 'Form autoreply message (English)',
'autoreply_cs' => 'Form autoreply message (Czech)',
'notification' => 'Form notification message (English)',
'notification_cs' => 'Form notification message (Czech)',
]
],
'reportwidget' => [
'partials' => [
'messages' => [
'label' => 'Contact form - Messages stats',
'title' => 'Messages stats',
'messages_all' => 'All',
'messages_new' => 'New',
'messages_read' => 'Read',
],
'new_message' => [
'label' => 'Contact form - New messages',
'title' => 'New messages',
'link_text' => 'Click to show Messages list',
],
],
],
'settings' => [
'form' => [
'css_class' => 'Form CSS class',
'use_placeholders' => 'Use placeholders',
'use_placeholders_comment' => 'Placeholders will be shown instead of field labels',
'disable_browser_validation' => 'Disable browser validation',
'disable_browser_validation_comment' => 'Do not allow browser built-in validation and popups.',
'success_msg' => 'Form success message',
'success_msg_placeholder' => 'Your data was sent.',
'error_msg' => 'Form error message',
'error_msg_placeholder' => 'There was an error sending your data!',
'allow_ajax' => 'Enable AJAX',
'allow_ajax_comment' => 'Allow AJAX with fallback for non JavaScript browsers',
'allow_confirm_msg' => 'Ask confirmation before form send',
'allow_confirm_msg_comment' => 'Add confirm dialog before sending',
'send_confirm_msg' => 'Confirmation text',
'send_confirm_msg_placeholder' => 'Are you sure?',
'hide_after_success' => 'Hide form after successful send',
'hide_after_success_comment' => 'Show only success message without form',
'add_assets' => 'Add assets',
'add_assets_comment' => 'Automatically add necessary CSS and JS assets (more about assets in README.md file)',
'add_css_assets' => 'Add CSS assets',
'add_css_assets_comment' => 'All necesssary styles will be included',
'add_js_assets' => 'Add JavaScript assets',
'add_js_assets_comment' => 'All necesssary JavaScripts will be included',
'form_ga_event_success' => 'GA event after successful sent',
],
'sections' => [
'ga_events' => 'Events'
],
'ga' => [
'ga_success_event_allow' => 'Send event after successful sent',
],
'buttons' => [
'send_btn_text' => 'Send button text',
'send_btn_text_placeholder' => 'Send',
'send_btn_css_class' => 'Send button CSS class',
'send_btn_css_class_placeholder' => 'btn btn-primary',
'send_btn_wrapper_css' => 'Send button wrapper CSS class',
'send_btn_wrapper_css_placeholder' => 'form-group',
],
'redirect' => [
'allow_redirect' => 'Redirect after submit',
'allow_redirect_comment' => 'Redirect to another page after successfull submit',
'redirect_url' => 'Page URL to redirect to',
'redirect_url_comment' => 'Enter your page URL (eg. /contact/thank-you)',
'redirect_url_placeholder' => '/contact/thank-you',
'redirect_url_external' => 'External URL',
'redirect_url_external_comment' => 'This is external URL path (eg. http://www.domain.com)',
],
'form_fields' => [
'prompt' => 'Add new form field',
'name' => 'FIELD NAME',
'name_comment' => 'Lower case without special characters (eg. name, email, home_address, ...)',
'type' => 'Field type',
'label' => 'Label',
'label_placeholder' => 'Full name',
'field_styling' => 'Custom CSS class',
'field_styling_comment' => 'Change default Bootstrap styles',
'autofocus' => 'Autofocus field',
'autofocus_comment' => 'Autofocus this form field',
'wrapper_css' => 'Wrapper CSS class',
'wrapper_css_placeholder' => 'form-group',
'field_css' => 'Field CSS class',
'field_css_placeholder' => 'form-control',
'label_css' => 'Label CSS class',
'label_css_placeholder' => '',
'field_validation' => 'Field validation',
'field_validation_comment' => 'Add field validation rules',
'validation' => 'Validation',
'validation_prompt' => 'Add validation',
'validation_type' => 'Validation rule',
'validation_error' => 'Validation error message',
'validation_error_placeholder' => 'Please enter valid data.',
'validation_error_comment' => 'Error message to use when validation fails',
'validation_custom_type' => 'Validation rule name',
'validation_custom_type_comment' => 'Enter Validator rule name (eg. regex, boolean, ...).<br>See <a href="https://octobercms.com/docs/services/validation#available-validation-rules" target="_blank">validation rules</a>.',
'validation_custom_type_placeholder' => 'regex',
'validation_custom_pattern' => 'Validation rule pattern',
'validation_custom_pattern_comment' => 'Left empty or enter custom rule pattern (this is a right part of Validator rule after colon - eg. [abc] for regex).',
'validation_custom_pattern_placeholder' => "/^[0-9]+$/",
'custom' => 'Custom field',
'custom_description' => 'Custom field with validation option',
'add_values_prompt' => 'Add values',
'field_value_id' => 'Field value ID',
'field_value_content' => 'Field value content',
'hit_type' => 'Hit type',
'event_category' => 'Event category',
'event_action' => 'Event action',
'event_label' => 'Event label',
'custom_code' => 'Custom code',
'custom_code_comment' => 'This code will override built in field code. Use carefully!',
'custom_code_twig' => 'Allow Twig',
'custom_code_twig_comment' => 'If checked, Twig markup will be parsed.',
'custom_content' => 'Custom content',
'custom_content_comment' => 'This content will be added to field.',
],
'form_field_types' => [
'text' => 'Text',
'email' => 'Email',
'textarea' => 'Textarea',
'checkbox' => 'Checkbox',
'dropdown' => 'Dropdown',
'file' => 'File',
'custom_code' => 'Custom code',
'custom_content' => 'Custom content',
],
'form_field_validation' => [
'select' => '--- Select validation ---',
'required' => 'Required',
'email' => 'Email',
'numeric' => 'Numeric',
'custom' => 'Custom rule',
],
'email' => [
'address_from' => 'From address',
'address_from_placeholder' => 'john.doe@domain.com',
'address_from_name' => 'From address name',
'address_from_name_placeholder' => 'John Doe',
'address_replyto' => 'Reply To address',
'address_replyto_comment' => 'Reply to mail will be send to this address.',
'subject' => 'Email subject',
'subject_comment' => 'Set only if you want other than defined in Settings > Mail templates.',
'template' => 'Email template',
'template_comment' => 'Code of email template created in Settings > Email templates. Left empty for default template: janvince.smallcontactform::mail.autoreply.',
'allow_email_queue' => 'Queueing mail',
'allow_email_queue_comment' => 'Add email to queue instead of immediately send. You have to configure your OctoberCMS queue first!',
'allow_notifications' => 'Allow notifications',
'allow_notifications_comment' => 'Send notification after form has been sent',
'notification_address_to' => 'Send notification to email',
'notification_address_to_comment' => 'One email address or comma-separated list of addresses',
'notification_address_to_placeholder' => 'notifications@domain.com',
'notification_address_from_form' => 'Force notification From address (NOT SUPPORTED by all email systems!)',
'notification_address_from_form_comment' => 'Set notification From address to an email entered in contact form (the field must be set in column mapping).',
'allow_autoreply' => 'Allow autoreply',
'allow_autoreply_comment' => 'Send a form content copy to author',
'autoreply_name_field' => 'NAME form field',
'autoreply_name_field_empty_option' => '-- Select --',
'autoreply_name_field_comment' => 'Must be type of Text.<br><em>Save and refresh this page if you can\'t see your fields.</em>',
'autoreply_email_field' => 'EMAIL address form field',
'autoreply_email_field_empty_option' => '-- Select --',
'autoreply_email_field_comment' => 'Must be type of Email.<br><em>Save and refresh this page if you can\'t see your fields.</em>',
'autoreply_message_field' => 'MESSAGE form field',
'autoreply_message_field_empty_option' => '-- Select --',
'autoreply_message_field_comment' => 'Must be type of Textarea or Text.<br><em>Save and refresh this page if you can\'t see your fields.</em>',
'notification_template' => 'Notification email template',
'notification_template_comment' => 'Code of email template created in Settings > Email templates. Left empty for default template: janvince.smallcontactform::mail.autoreply.',
],
'antispam' => [
'add_antispam' => 'Add passive antispam protection',
'add_antispam_comment' => 'Add simple but effective passive antispam control (more info in README.md file)',
'antispam_delay' => 'Antispam delay (s)',
'antispam_delay_comment' => 'Delay protection for too fast form sending (usually by robots)',
'antispam_delay_placeholder' => '3',
'antispam_label' => 'Antispam field label',
'antispam_label_comment' => 'Label will be visible for non JavaScript enabled browsers',
'antispam_label_placeholder' => 'Please clear this field',
'antispam_error_msg' => 'Error message',
'antispam_error_msg_comment' => 'Message to show to user when antispam protection is triggered',
'antispam_error_msg_placeholder' => 'Please empty this field!',
'antispam_delay_error_msg' => 'Delay error message',
'antispam_delay_error_msg_comment' => 'Message to show to user when form was sent too fast',
'antispam_delay_error_msg_placeholder' => 'Form sent too fast! Please wait few seconds and try again!',
'add_google_recaptcha' => 'Add Google reCaptcha',
'add_google_recaptcha_comment' => 'Add reCaptcha to Contact Form (more info in README.md file).<br>You can get API keys on <a href="https://www.google.com/recaptcha/admin#list" target="_blank">Google reCaptcha site</a>.',
'google_recaptcha_version' => 'Google reCaptcha version',
'google_recaptcha_version_comment' => 'Choose a version of reCaptcha widget.<br>More info on <a href="https://developers.google.com/recaptcha/docs/versions" target="_blank">Google reCaptcha site</a>.',
'google_recaptcha_versions' => [
'v2checkbox' => 'reCaptcha V2 checkbox',
'v2invisible' => 'reCaptcha V2 invisible',
],
'google_recaptcha_site_key' => 'Site key',
'google_recaptcha_site_key_comment' => 'Put your site key',
'google_recaptcha_secret_key' => 'Secret key',
'google_recaptcha_secret_key_comment' => 'Put your secret key',
'google_recaptcha_wrapper_css' => 'reCaptcha box wrapper CSS class',
'google_recaptcha_wrapper_css_comment' => 'CSS class of wrapper box around reCaptcha box',
'google_recaptcha_wrapper_css_placeholder' => 'form-group',
'google_recaptcha_error_msg' => 'Error message',
'google_recaptcha_error_msg_comment' => 'Message to show to user when reCAPTCHA is not validated.',
'google_recaptcha_error_msg_placeholder' => 'Google reCAPTCHA validation error!',
'google_recaptcha_scripts_allow' => 'Automatically add necessary JS scripts',
'google_recaptcha_scripts_allow_comment' => 'This will add link to JS scripts to your site.',
'google_recaptcha_locale_allow' => 'Allow locale detection',
'google_recaptcha_locale_allow_comment' => 'This will add curent web page locale to reCAPTCHA script, so it will translated.',
'add_ip_protection' => 'Check sender\'s IP',
'add_ip_protection_comment' => 'Do not allow too many form submits from one IP address',
'add_ip_protection_count' => 'Maximum form submits during a day',
'add_ip_protection_count_comment' => 'Number of allowed submits from one IP address during a single day',
'add_ip_protection_count_placeholder' => '3',
'add_ip_protection_error_get_ip' => 'We wasn\'t able to determine your IP address!',
'add_ip_protection_error_too_many_submits' => 'Too many submits error message',
'add_ip_protection_error_too_many_submits_comment' => 'Error message to show to the user',
'add_ip_protection_error_too_many_submits_placeholder' => 'Too many form submits from one address today!',
'disabled_extensions' => 'Disabled extensions',
'disabled_extensions_comment' => 'Settings set on Privacy tab disabled these extensions',
],
'mapping' => [
'hint' => [
'title' => 'Why fields mapping?',
'content' => '
<p>You can build a custom form with own field names and types.</p>
<p>System writes all form data in database, but for quick overview Name, Email and Message columns are visible separately in Messages list.</p>
<p>So you have to help system to identify these columns by mapping to your form fields.</p>
<p><em>These mappings are also used for autoreply emails where at least Email field mapping is important.</em></p>
',
],
'warning' => [
'title' => 'Can\'t select your form fields?',
'content' => '
<p>If you don\'t see your form fields, click on button Save at the bottom of this page and then reload page (F5 or Ctr+R / Cmd+R).</p>
',
],
],
'privacy' => [
'disable_messages_saving' => 'Disable messages saving',
'disable_messages_saving_comment' => 'When checked, no data will saved in Messages list.<br><strong>This will also disable IP protection!</strong>',
'disable_messages_saving_comment_section' => '<div class="callout fade in callout-danger no-subheader"><div class="header"><i class="icon-warning"></i><h3>Be sure to allow notification emails or you will have no data from sent forms!</h3></div></div>',
],
'tabs' => [
'form' => 'Form',
'buttons' => 'Send button',
'form_fields' => 'Fields',
'mapping' => 'Columns mapping',
'email' => 'Email',
'antispam' => 'Antispam',
'privacy' => 'Privacy',
'ga' => 'Google Analytics',
],
],
'components' => [
'groups' => [
'hacks' => 'Hacks',
'override_form' => 'Override form settings',
'override_notifications' => 'Override notification settings',
'override_autoreply' => 'Override autoreply settings',
'override' => 'Override form settings',
'override_redirect' => 'Override redirect settings',
'override_ga' => 'Override Google Analytics settings',
],
'properties' => [
'form_description' => 'Form description',
'form_description_comment' => 'You can add optional form description, that will be saved with other sent data in the messages list. You can also use {{ :slug }} here.',
'disable_fields' => 'Disable fields',
'disable_fields_comment' => 'This will disable listed fields. Add field names separated by pipe (eg. name|message|phone)',
'send_btn_label' => 'Send button label',
'send_btn_label_comment' => 'Override send button label',
'form_success_msg' => 'Success message',
'form_success_msg_comment' => 'Override success message shown after successful sent',
'form_error_msg' => 'Error message',
'form_error_msg_comment' => 'Override error message shown after unsuccessful sent',
'disable_notifications' => 'Disable notification',
'disable_notifications_comment' => 'This will disable notification emails (overrides form settings)',
'notification_address_to' => 'Address TO',
'notification_address_to_comment' => 'This will override email address where notification email will be sent (if enabled in form settings)',
'notification_address_from' => 'Address FROM',
'notification_address_from_comment' => 'This will override email address from where notification email will be sent',
'notification_address_from_name' => 'Address FROM name',
'notification_address_from_name_comment' => 'This will override email address name from where notification email will be sent',
'notification_template' => 'Notification template',
'notification_template_comment' => 'This will override notification email template (eg. janvince.smallcontactform::mail.notification)',
'notification_subject' => 'Notification subject',
'notification_template_comment' => 'Override email subject',
'disable_autoreply' => 'Disable notification',
'disable_autoreply_comment' => 'This will disable notification emails (overrides form settings)',
'autoreply_address_from' => 'Address FROM',
'autoreply_address_from_comment' => 'This will override email address in autoreply email (if enabled in form settings)',
'autoreply_address_from_name' => 'Address (FROM) name',
'autoreply_address_from_name_comment' => 'This will override email address name in autoreply email (if enabled in form settings)',
'autoreply_address_replyto' => 'Address REPLY TO',
'autoreply_address_replyto_comment' => 'This will override REPLY TO email address in autoreply email (if enabled in form settings)',
'autoreply_template' => 'Autoreply template',
'autoreply_template_comment' => 'This will override autoreply email template (eg. janvince.smallcontactform::mail.autoreply)',
'autoreply_subject' => 'Autoreply email subject',
'autoreply_template_comment' => 'Override email subject',
]
],
];

View File

@ -0,0 +1,426 @@
<?php
return [
'plugin' => [
'name' => 'Formulaire de contact',
'description' => 'Générateur simple de formulaire de contact',
'category' => 'Petits plugins',
],
'permissions' => [
'access_messages' => 'Accéder à la liste des messages',
'access_settings' => 'Gérer les préférences d\'administration',
'delete_messages' => 'Supprimer les messages stockés',
'export_messages' => 'Exporter des messages',
],
'navigation' => [
'main_label' => 'Formulaire de contact',
'messages' => 'Messages',
],
'controller' => [
'contact_form' => [
'name' => 'Formulaire de contact',
'description' => 'Insérer un formulaire de contact sur la page',
'no_fields' => 'Veuillez ajouter d\abord des champs de formulaire dans l\administration dorsale (dans Paramètres > Formulaire de contact > Champs) ...',
],
'filter' => [
'date' => 'Plage de dates',
],
'scoreboard' => [
'records_count' => 'Messages',
'latest_record' => 'Dernier message de',
'new_count' => 'Nouveau',
'new_description' => 'Messages',
'read_count' => 'Lus',
'all_count' => 'Total',
'all_description' => 'Messages',
'settings_btn' => 'Paramètres du formulaire',
'mark_read' => 'Marquer comme lu',
'mark_read_confirm' => 'Voulez-vous vraiment définir les messages sélectionnés comme lus?',
'mark_read_success' => 'Les messages ont été marqués comme lu avec succès.',
],
'preview' => [
'record_not_found' => 'Message non trouvé!',
],
],
'models' => [
'message' => [
'columns' => [
'id' => 'ID',
'datetime' => 'Date et heure',
'form_data' => 'Données du formulaire',
'name' => 'Nom',
'email' => 'E-mail',
'message' => 'Message',
'new_message' => 'Statut',
'new' => 'Nouveau',
'read' => 'Lu',
'remote_ip' => 'IP de l\'expéditeur',
'form_alias' => 'Alias',
'form_description' => 'Description',
'created_at' => 'Créé à',
'updated_at' => 'Modifié à',
]
],
],
'controllers' => [
'messages' => [
'list_title' => 'Messages',
'preview' => 'Aperçu',
'preview_title' => 'Message du formulaire de contact',
'preview_date' => 'Date : ',
'preview_content_title' => 'Contenu :',
'remote_ip' => 'Envoyé depuis l\'IP :',
'export' => 'Exporter',
],
'index' => [
'unauthorized' => 'Accès non autorisé',
],
],
'mail' => [
'templates' => [
'autoreply' => 'Réponse automatique du formulaire (français)',
'autoreply_cs' => 'Réponse automatique du formulaire (tchèque)',
'notification' => 'Message de notification du formulaire (français)',
'notification_cs' => 'Message de notification du formulaire (tchèque)',
]
],
'reportwidget' => [
'partials' => [
'messages' => [
'label' => 'Formulaire de contact - Statistiques des messages',
'title' => 'Statistiques des messages',
'messages_all' => 'Tout',
'messages_new' => 'Nouveaux',
'messages_read' => 'Lus',
],
'new_message' => [
'label' => 'Formulaire de contact - Nouveaux messages',
'title' => 'Nouveaux messages',
'link_text' => 'Cliquez pour afficher la liste des messages',
],
],
],
'settings' => [
'form' => [
'css_class' => 'Classe CSS du formulaire',
'use_placeholders' => 'Utiliser des "placeholder"',
'use_placeholders_comment' => 'Des "placeholder" seront affichés à la plage des étiquettes de champs.',
'disable_browser_validation' => 'Désactiver la validation du navigateur',
'disable_browser_validation_comment' => 'Ne pas autoriser la validation intégrée du navigateur et ses fenêtres contextuelles.',
'success_msg' => 'Message de réussite du formulaire',
'success_msg_placeholder' => 'Vos données ont été envoyées.',
'error_msg' => 'Message d\'erreur du formulaire',
'error_msg_placeholder' => 'Une erreur s\'est produite lors de l\'envoi de vos données!',
'allow_ajax' => 'Activer AJAX',
'allow_ajax_comment' => 'Autoriser AJAX avec rechange pour les navigateurs sans JavaScript.',
'allow_confirm_msg' => 'Demander une confirmation avant l\'envoi du formulaire',
'allow_confirm_msg_comment' => 'Ajouter une boîte de dialogue de confirmation avant l\'envoi',
'send_confirm_msg' => 'Texte de confirmation',
'send_confirm_msg_placeholder' => 'Êtes-vous sûr?',
'hide_after_success' => 'Masquer le formulaire après l\'envoi réussi',
'hide_after_success_comment' => 'Afficher uniquement le message de réussite sans formulaire',
'add_assets' => 'Ajouter des assets',
'add_assets_comment' => 'Ajouter automatiquement les assets CSS et JS nécessaires (plus d\'informations sur les assets dans le fichier README.md - en anglais)',
'add_css_assets' => 'Ajouter des assets CSS',
'add_css_assets_comment' => 'Tous les styles nécessaires seront inclus',
'add_js_assets' => 'Ajouter des assets JavaScript',
'add_js_assets_comment' => 'Tous les scripts JavaScript nécessaires seront inclus',
],
'buttons' => [
'send_btn_text' => 'Texte du bouton envoyer',
'send_btn_text_placeholder' => 'Envoyer',
'send_btn_css_class' => 'Classe(s) CSS du bouton envoyer',
'send_btn_css_class_placeholder' => 'btn btn-primary',
'send_btn_wrapper_css' => 'Classe(s) CSS du wrapper du bouton envoyer',
'send_btn_wrapper_css_placeholder' => 'form-group',
],
'redirect' => [
'allow_redirect' => 'Rediriger après soumission',
'allow_redirect_comment' => 'Rediriger vers une autre page après un envoie réussie',
'redirect_url' => 'URL de la page vers laquelle rediriger',
'redirect_url_comment' => 'Entrez l\'URL de votre page (par exemple, /contact/thank-you)',
'redirect_url_placeholder' => '/contact/thank-you',
'redirect_url_external' => 'URL externe',
'redirect_url_external_comment' => 'Ceci est un chemin d\'URL externe (ex. http://www.domain.com)',
],
'form_fields' => [
'prompt' => 'Ajouter un nouveau champ de formulaire',
'name' => 'Nom du champ',
'name_comment' => 'Minuscules sans caractères spéciaux (ex. nom, email, adresse_personnelle, ...)',
'type' => 'Type de champ',
'label' => 'Étiquette',
'label_placeholder' => 'Nom complet',
'field_styling' => 'Classe CSS personnalisée',
'field_styling_comment' => 'Changer les styles de Bootstrap par défaut',
'autofocus' => 'Champ autofocus',
'autofocus_comment' => 'Autofocus ce champ du formulaire',
'wrapper_css' => 'Classe CSS du wrapper',
'wrapper_css_placeholder' => 'form-group',
'field_css' => 'Classe CSS du champ',
'field_css_placeholder' => 'form-control',
'label_css' => 'Classe CSS de l\'étiquette',
'label_css_placeholder' => '',
'field_validation' => 'Validation du champ',
'field_validation_comment' => 'Ajouter des règles de validation du champ',
'validation' => 'Validation',
'validation_prompt' => 'Ajouter une validation',
'validation_type' => 'Règle de validation',
'validation_error' => 'Message d\'erreur de validation',
'validation_error_placeholder' => 'S\'il vous plaît entrer des données valides.',
'validation_error_comment' => 'Message d\'erreur à utiliser lorsque la validation échoue',
'custom' => 'Champ personnalisé',
'custom_description' => 'Champ personnalisé avec option de validation',
],
'form_field_types' => [
'text' => 'Texte',
'email' => 'Email',
'textarea' => 'Aire de texte',
'checkbox' => 'Case à cocher',
'dropdown' => 'Dropdown',
'file' => 'File',
'custom_code' => 'Custom code',
'custom_content' => 'Custom content',
],
'form_field_validation' => [
'select' => '--- Sélectionnez la validation ---',
'required' => 'Requis',
'email' => 'Email',
'numeric' => 'Numérique',
],
'email' => [
'address_from' => 'Adresse de l\'expéditeur',
'address_from_placeholder' => 'john.doe@domain.com',
'address_from_name' => 'Nom de l\'expéditeur',
'address_from_name_placeholder' => 'John Doe',
'subject' => 'Sujet de l\'e-mail',
'subject_comment' => 'Définissez uniquement si vous souhaitez une définition autre que celle définie dans Paramètres > Modèles des e-mails.',
'template' => 'Modèle e-mail',
'template_comment' => 'Code du modèle e-mail créé dans Paramètres > Modèles des e-mails. Laissez vide pour le modèle par défaut: janvince.smallcontactform::mail.autoreply.',
'allow_email_queue' => 'E-mail en fille d\'attente',
'allow_email_queue_comment' => 'Ajouter un email à la file d\'attente au lieu de l\'envoyer immédiatement. Vous devez d\'abord configurer votre file d\'attente OctoberCMS!',
'allow_notifications' => 'Autoriser les notifications',
'allow_notifications_comment' => 'Envoyer une notification après l\'envoi du formulaire',
'notification_address_to' => 'Envoyer une notification à l\'e-mail',
'notification_address_to_comment' => 'Une adresse électronique ou une liste d\'adresses séparées par des virgules',
'notification_address_to_placeholder' => 'notifications@domain.com',
'notification_address_from_form' => 'Forcer l\'adresse e-mail de la notification (NON PRIS EN CHARGE par tous les systèmes de messagerie!)',
'notification_address_from_form_comment' => 'Définir l\'adresse e-mail entré dans le formulaire de contact comme adresse de l\'expéditeur (le champ doit être défini dans le mappage de colonnes).',
'allow_autoreply' => 'Autoriser une copie à l\'auteur',
'allow_autoreply_comment' => 'Envoyer une copie du contenu du formulaire à l\'auteur',
'autoreply_name_field' => 'Champ du formulaire avec le NOM',
'autoreply_name_field_empty_option' => '-- Sélectionnez --',
'autoreply_name_field_comment' => 'Doit être du type Texte.<br><em>Enregistrez et actualisez cette page si vous ne pouvez pas voir vos champs.</em>',
'autoreply_email_field' => 'Champ du formulaire avec l\'adresse E-MAIL',
'autoreply_email_field_empty_option' => '-- Sélectionnez --',
'autoreply_email_field_comment' => 'Doit être du type email.<br><em>Enregistrez et actualisez cette page si vous ne pouvez pas voir vos champs.</em>',
'autoreply_message_field' => 'Champ du formulaire avec le contenu du MESSAGE',
'autoreply_message_field_empty_option' => '-- Sélectionnez --',
'autoreply_message_field_comment' => 'Doit être du type Aire de texte ou Texte.<br><em>Enregistrez et actualisez cette page si vous ne pouvez pas voir vos champs.</em>',
'notification_template' => 'Modèle e-mail de la notification',
'notification_template_comment' => 'Code du modèle de courrier électronique créé dans Paramètres > Modèless des e-mails. Laisser vide pour le modèle par défaut : janvince.smallcontactform::mail.autoreply.',
],
'antispam' => [
'add_antispam' => 'Ajouter une protection antispam passive',
'add_antispam_comment' => 'Ajouter un contrôle antispam passif simple mais efficace (plus dinformations dans le fichier README.md - en anglais)',
'antispam_delay' => 'Délai Antispam (en secondes)',
'antispam_delay_comment' => 'Protection différée pour l\'envoi de formulaires trop rapide (généralement par des robots)',
'antispam_delay_placeholder' => '3',
'antispam_label' => 'Étiquette de champ antispam',
'antispam_label_comment' => 'L\'étiquette sera visible pour les navigateurs non activés par JavaScript',
'antispam_label_placeholder' => 'Veuillez effacer ce champ',
'antispam_error_msg' => 'Message d\'erreur',
'antispam_error_msg_comment' => 'Message à afficher à l\'utilisateur lorsque la protection antispam est déclenchée',
'antispam_error_msg_placeholder' => 'Veuillez vider ce champ!',
'antispam_delay_error_msg' => 'Message d\'erreur de délai',
'antispam_delay_error_msg_comment' => 'Message à montrer à l\'utilisateur lorsque le formulaire a été envoyé trop rapidement',
'antispam_delay_error_msg_placeholder' => 'Formulaire envoyé trop vite! S\'il vous plaît attendre quelques secondes et essayez à nouveau!',
'add_google_recaptcha' => 'Ajouter Google reCaptcha',
'add_google_recaptcha_comment' => 'Ajoutez reCaptcha au formulaire de contact (plus d\informations dans le fichier README.md - en anglais). <br> Vous pouvez obtenir les clés dAPI sur le <a href="https://www.google.com/recaptcha/admin#list" target="_blank">Google reCaptcha</a>.',
'google_recaptcha_site_key' => 'Clé du site',
'google_recaptcha_site_key_comment' => 'Mettez votre clé du site',
'google_recaptcha_secret_key' => 'Clé secrète',
'google_recaptcha_secret_key_comment' => 'Mettez votre clé secrète',
'google_recaptcha_error_msg' => 'Message d\'erreur',
'google_recaptcha_error_msg_comment' => 'Message à afficher à l\'utilisateur lorsque le reCAPTCHA n\'est pas validé.',
'google_recaptcha_error_msg_placeholder' => 'Erreur de validation du reCAPTCHA de Google!',
'google_recaptcha_scripts_allow' => 'Ajouter automatiquement les scripts JS nécessaires',
'google_recaptcha_scripts_allow_comment' => 'Cela ajoutera un lien vers les scripts JS sur votre site.',
'google_recaptcha_locale_allow' => 'Autoriser la détection des paramètres de langues',
'google_recaptcha_locale_allow_comment' => 'Cela ajoutera les paramètres de langues actuels de la page Web au script reCAPTCHA, de sorte qu\'il sera traduit.',
'add_ip_protection' => 'Vérifier l\'adresse IP de l\'expéditeur',
'add_ip_protection_comment' => 'Ne pas autoriser trop de formulaires soumis à partir d\'une même adresse IP',
'add_ip_protection_count' => 'Formulaire maximum soumis au cours d\'une journée',
'add_ip_protection_count_comment' => 'Nombre de soumissions autorisées à partir d\'une même adresse IP au cours d\'une seule journée',
'add_ip_protection_count_placeholder' => '3',
'add_ip_protection_error_get_ip' => 'Nous n\'avons pas pu déterminer votre adresse IP!',
'add_ip_protection_error_too_many_submits' => 'Message d\'erreur lorsque trop de messages sont envoyés',
'add_ip_protection_error_too_many_submits_comment' => 'Message d\'erreur à montrer à l\'utilisateur',
'add_ip_protection_error_too_many_submits_placeholder' => 'Trop de messages ont été envoyés par votre adresse aujourd\'hui!',
'disabled_extensions' => 'Extensions désactivées',
'disabled_extensions_comment' => 'Les paramètres définis dans l\'onglet Confidentialité ont désactivé ces extensions.',
],
'mapping' => [
'hint' => [
'title' => 'Pourquoi le mappage des champs?',
'content' => '
<p>Vous pouvez créer un formulaire personnalisé avec vos propres noms et types de champs.</p>
<p>Le système écrit toutes les données de formulaire dans la base de données, mais pour une présentation rapide, les colonnes Nom, E-mail et Message sont visibles séparément dans la listedes messages.</p>
<p>Vous devez donc aider le système à identifier ces colonnes en les associant à vos champs de formulaire.</p>
<p><em>Ces mappages sont également utilisés pour les e-mails à réponse automatique dans lesquels au moins le mappage du champ e-mail est important.</em></p>
',
],
'warning' => [
'title' => 'Vous ne pouvez pas sélectionner vos champs de formulaire?',
'content' => '
<p>Si vous ne voyez pas vos champs de formulaire, cliquez sur le bouton Enregistrer en bas de cette page, puis rechargez la page (F5 ou Ctrl+R / Cmd+R).</p>
',
],
],
'privacy' => [
'disable_messages_saving' => 'Désactiver l\'enregistrement des messages',
'disable_messages_saving_comment' => 'Lorsque cette case est cochée, aucune donnée ne sera enregistrée dans la liste de messages.<br><strong>La protection IP sera également désactivée!</strong>',
'disable_messages_saving_comment_section' => '<div class="callout fade in callout-danger no-subheader"><div class="header"><i class="icon-warning"></i><h3>Assurez-vous d\'autoriser les courriels de notification, sinon vous ne recevrez aucune donnée des formulaires envoyés!</h3></div></div>',
],
'tabs' => [
'form' => 'Formulaire',
'buttons' => 'Bouton envoyer',
'form_fields' => 'Champs',
'mapping' => 'Mappage des colonnes',
'email' => 'E-mail',
'antispam' => 'Antispam',
'privacy' => 'Confidentialité'
],
],
'components' => [
'groups' => [
'hacks' => 'Hacks',
],
'properties' => [
'disable_notifications' => 'Désactiver les e-mails de notification',
'disable_notifications_comment' => 'Ceci désactivera les courriels de notification (remplace les paramètres du formulaire)',
'form_description' => 'Description du formulaire',
'form_description_comment' => 'Vous pouvez ajouter une description de formulaire facultative, qui sera enregistrée avec les autres données envoyées dans la liste de messages. Vous pouvez également utiliser {{:slug}} ici.',
]
],
];

View File

@ -0,0 +1,146 @@
<?php
return [
'plugin' => [
'name' => 'Kapcsolat űrlap',
'description' => 'Űrlap generálása kapcsolat felvételhez.',
'category' => 'Small plugins',
],
'permissions' => [
'access_messages' => 'Üzenetek megtekintése',
'access_settings' => 'Beállítások módosítása',
'delete_messages' => 'Üzenetek törlése',
'export_messages' => 'Üzenetek exportálása',
],
'navigation' => [
'main_label' => 'Kapcsolat űrlap',
'messages' => 'Üzenetek',
],
'controller' => [
'contact_form' => [
'name' => 'Kapcsolat űrlap',
'description' => 'Űrlap generálása kapcsolat felvételhez.',
'no_fields' => 'Elsőként hozzon létre legalább egy mezőt itt: Beállítások > Small plugins > Kapcsolat űrlap > Fields',
],
'filter' => [
'date' => 'Időintervallum',
],
'scoreboard' => [
'records_count' => 'Üzenetek',
'latest_record' => 'Legutóbbi',
'new_count' => 'Új levél',
'new_description' => 'üzenet',
'read_count' => 'Olvasott',
'all_count' => 'Összes',
'all_description' => 'üzenet',
'settings_btn' => 'Testreszabás',
'mark_read' => 'Olvasottnak jelöl',
'mark_read_confirm' => 'Valóban olvasottnak szeretné jelölni az üzeneteket?',
'mark_read_success' => 'Az üzenetek sikeresen olvasottnak lettek jelölve.',
],
'preview' => [
'record_not_found' => 'Üzenet nem található!',
],
],
'models' => [
'message' => [
'columns' => [
'id' => 'ID',
'datetime' => 'Elküldve',
'form_data' => 'Adatok',
'name' => 'Név',
'email' => 'Email',
'message' => 'Üzenet',
'new_message' => 'Státusz',
'new' => 'Új levél',
'read' => 'Olvasott',
'remote_ip' => 'IP cím',
'form_alias' => 'Űrlap',
'form_description' => 'Leírás',
'created_at' => 'Létrehozva',
'updated_at' => 'Módosítva',
],
],
],
'controllers' => [
'messages' => [
'list_title' => 'Üzenetek',
'preview' => 'Előnézet',
'preview_title' => 'Üzenet',
'preview_date' => 'Elküldve:',
'preview_content_title' => 'Tartalom:',
'remote_ip' => 'IP cím',
'export' => 'Exportálás',
],
'index' => [
'unauthorized' => 'Illetéktelen hozzáférés',
],
],
'mail' => [
'templates' => [
'autoreply' => 'Automatikus üzenet (angol)',
'autoreply_cs' => 'Automatikus üzenet (cseh)',
'notification' => 'Értesítő üzenet (angol)',
'notification_cs' => 'Értesítő üzenet (cseh)',
]
],
'reportwidget' => [
'partials' => [
'messages' => [
'label' => 'Kapcsolat űrlap - Statisztika',
'title' => 'Üzenet statisztika',
'messages_all' => 'Összes',
'messages_new' => 'Új levél',
'messages_read' => 'Olvasott',
],
'new_message' => [
'label' => 'Kapcsolat űrlap - Új üzenetek',
'title' => 'Új üzenetek',
'link_text' => 'Összes üzenet megtekintése',
],
],
],
'components' => [
'groups' => [
'hacks' => 'Hackelés',
],
'properties' => [
'disable_notifications' => 'Értesítő e-mailek letiltása',
'disable_notifications_comment' => 'Felül fogja írni az űrlap központi beállítását erre vonatkozóan.',
'form_description' => 'Űrlap leírás',
'form_description_comment' => 'Amennyiben megad adatot, azok mentésre kerülnek az űrlap beküldésekor. Például az aktuális oldal címének megjegyzéséhez használhatja a {{ :slug }} kódot.',
],
],
];

View File

@ -0,0 +1,436 @@
<?php
return [
'plugin' => [
'name' => 'Formularz kontaktowy',
'description' => 'Kreator prostego formularza kontaktowego',
'category' => 'Small plugins',
],
'permissions' => [
'access_messages' => 'Przeglądaj wiadomości',
'access_settings' => 'Zarządzaj ustawieniami',
'delete_messages' => 'Usuwaj zapisane wiadomości',
'export_messages' => 'Eksportuj wiadomości',
],
'navigation' => [
'main_label' => 'Formularz kontaktowy',
'messages' => 'Wiadomości',
],
'controller' => [
'contact_form' => [
'name' => 'Formularz kontaktowy',
'description' => 'Dodaj formularz kontaktowy do strony',
'no_fields' => 'Najpierw dodaj pola formularza w panelu administracyjnym (Ustawienia -> Small plugins -> Formularz kontktowy)...',
],
'filter' => [
'date' => 'Zakres dat',
],
'scoreboard' => [
'records_count' => 'Wiadomości',
'latest_record' => 'Ostatnia wiadomość od',
'new_count' => 'Nowe',
'new_description' => 'Wiadomości',
'read_count' => 'Przeczytane',
'all_count' => 'Wszystkie',
'all_description' => 'Wiadomości',
'settings_btn' => 'Ustawienia formularza',
'mark_read' => 'Oznacz jako przeczytane',
'mark_read_confirm' => 'Czy na pewno oznaczyć wybrane wiadomości jako przeczytane?',
'mark_read_success' => 'Oznaczono jako przeczytane.',
],
'preview' => [
'record_not_found' => 'Nie znaleziono wiadomości!',
],
],
'models' => [
'message' => [
'columns' => [
'id' => 'ID',
'datetime' => 'Data i godzina',
'form_data' => 'Dane formularza',
'name' => 'Imię i nazwisko',
'email' => 'Email',
'message' => 'Wiadomość',
'new_message' => 'Status',
'new' => 'Nowa',
'read' => 'Przeczytana',
'remote_ip' => 'Adres IP wysyłającego',
'form_alias' => 'Alias',
'form_description' => 'Opis',
'created_at' => 'Utworzono',
'updated_at' => 'Zaktualizowano',
]
],
],
'controllers' => [
'messages' => [
'list_title' => 'Wiadomości',
'preview' => 'Podgląd',
'preview_title' => 'Wiadomość z formularza kontaktowego',
'preview_date' => 'Data otrzymania:',
'preview_content_title' => 'Zawartość:',
'remote_ip' => 'Wysłano z adresu IP',
'export' => 'Eksportuj',
],
'index' => [
'unauthorized' => 'Nie masz uprawnień aby wyświetlić tą stroną',
],
],
'mail' => [
'templates' => [
'autoreply' => 'Wiadomość automatycznej odpowiedzi (Angielski)',
'autoreply_cs' => 'Wiadomość automatycznej odpowiedzi (Czeski)',
'notification' => 'Wiadomość powiadamiająca (Angielski)',
'notification_cs' => 'Wiadomość powiadamiająca (Czeski)',
]
],
'reportwidget' => [
'partials' => [
'messages' => [
'label' => 'Formularz kontaktowy - Statystyki wiadomości',
'title' => 'Statystyki wiadomości',
'messages_all' => 'Wszystkie',
'messages_new' => 'Nowe',
'messages_read' => 'Przeczytane',
],
'new_message' => [
'label' => 'Formularz Kontaktowy - Nowe wiadomości',
'title' => 'Nowe wiadomości',
'link_text' => 'Kliknij, aby pokazać listę wiadomości',
],
],
],
'settings' => [
'form' => [
'css_class' => 'Klasa CSS formularza',
'use_placeholders' => 'Użyj tekstów zastępczych (Placeholder)',
'use_placeholders_comment' => 'Teksty zastępcze będą wyświetlane zamiast etykiet pól. (Etykiety otrzymają styl display: none)',
'disable_browser_validation' => 'Wyłącz domyślną walidację przeglądarki',
'disable_browser_validation_comment' => 'Nie zezwalaj na wbudowane w przeglądarkę sprawdzanie poprawności i wyskakujące okienka.',
'success_msg' => 'Wiadomość o poprawnym wysłaniu formularza',
'success_msg_placeholder' => 'Twoja wiadomość została wysłana.',
'error_msg' => 'Wiadomość o niepoprawnym wysłaniu formularza',
'error_msg_placeholder' => 'Wystąpił błąd podczas wysyłania Twojej wiadomości!',
'allow_ajax' => 'Włącz AJAX',
'allow_ajax_comment' => 'Zezwól na asynchroniczny formularz AJAX razem ze wsparciem dla przeglądarek nieobsługujących JavaScript.',
'allow_confirm_msg' => 'Zapytaj o potwierdzenie przed wysłaniem formularza',
'allow_confirm_msg_comment' => 'Dodaje okno dialogowe potwierdzenia przed wysłaniem',
'send_confirm_msg' => 'Tekst potwierdzenia',
'send_confirm_msg_placeholder' => 'Czy jesteś pewny?',
'hide_after_success' => 'Ukryj formularz po pomyślnym wysłaniu',
'hide_after_success_comment' => 'Pokaż tylko wiadomość o pomyślnym wysłaniu wiadomości bez formularza',
'add_assets' => 'Dodaj pliki',
'add_assets_comment' => 'Automatycznie dodaje niezbędne pliki CSS i JS (więcej na ten temat w pliku README.md)',
'add_css_assets' => 'Dodaj pliki CSS',
'add_css_assets_comment' => 'Dodaje wszystkie niezbędne pliki CSS',
'add_js_assets' => 'Dodaj pliki JS',
'add_js_assets_comment' => 'Dodaje wszystkie niezbędne pliki JS',
],
'buttons' => [
'send_btn_text' => 'Tekst przycisku wyślij',
'send_btn_text_placeholder' => 'Wyślij',
'send_btn_css_class' => 'Klasa CSS przycisku wyślij',
'send_btn_css_class_placeholder' => 'btn btn-primary',
'send_btn_wrapper_css' => 'Klasa CSS kontenera przycisku wyślij',
'send_btn_wrapper_css_placeholder' => 'form-group',
],
'redirect' => [
'allow_redirect' => 'Przekieruj po wysłaniu',
'allow_redirect_comment' => 'Przekierowuje na inną stronę po wysłaniu formularza',
'redirect_url' => 'Adres URL strony przekierowania',
'redirect_url_comment' => 'Wprowadź adres URL strony (np. /contact/thank-you)',
'redirect_url_placeholder' => '/contact/thank-you',
'redirect_url_external' => 'Zewnętrzny adres URL',
'redirect_url_external_comment' => 'Ten adres URL wskazuje na zewnętrzną domenę (np. http://www.domain.com)',
],
'form_fields' => [
'prompt' => 'Dodaj nowe pole formularza',
'name' => 'NAZWA POLA',
'name_comment' => 'Małe litery bez znaków specjalnych (np. name, email, home_address, ...)',
'type' => 'Rodzaj pola',
'label' => 'Etykieta',
'label_placeholder' => 'Imię i nazwisko',
'field_styling' => 'Niestandardowa klasa CSS',
'field_styling_comment' => 'Zmień domyślne style (Bootstrap)',
'autofocus' => 'Automatyczny fokus',
'autofocus_comment' => 'Automatycznie ustaw fokus na to pole',
'wrapper_css' => 'Klasa CSS kontenera',
'wrapper_css_placeholder' => 'form-group',
'field_css' => 'Klasa CSS pola',
'field_css_placeholder' => 'form-control',
'label_css' => 'Klasa CSS etykiety',
'label_css_placeholder' => 'control-label',
'field_validation' => 'Walidacja',
'field_validation_comment' => 'Dodaj reguły walidacji',
'validation' => 'Walidacja',
'validation_prompt' => 'Dodaj regułę walidacji',
'validation_type' => 'Reguła walidacji',
'validation_error' => 'Komunikat błędu walidacji',
'validation_error_placeholder' => 'Wprowadź poprawne dane.',
'validation_error_comment' => 'Komunikat wyświetlany, gdy pole nie przejdzie walidacji',
'validation_custom_type' => 'Validation rule name',
'validation_custom_type_comment' => 'Wprowadź nazwę reguły walidatora (eg. regex, boolean, ...).<br>See <a href="https://octobercms.com/docs/services/validation#available-validation-rules" target="_blank">reguły walidacji</a>.',
'validation_custom_type_placeholder' => 'regex',
'validation_custom_pattern' => 'Wzorzec reguły walidacji',
'validation_custom_pattern_comment' => 'Pozostaw puste lub wprowadź niestandardowy wzorzec reguły (prawa część reguły znajdująca się po dwukropku - np. [abc] dla reguły regex)',
'validation_custom_pattern_placeholder' => "/^[0-9]+$/",
'custom' => 'Pole niestandardowe',
'custom_description' => 'Niestandardowe pole z opcją walidacji',
'add_values_prompt' => 'Dodaj opcje',
'field_value_id' => 'Identyfikator opcji',
'field_value_content' => 'Tekst opcji',
],
'form_field_types' => [
'text' => 'Tekst',
'email' => 'Email',
'textarea' => 'Pole tekstowy',
'checkbox' => 'Pole wyboru',
'dropdown' => 'Lista rozwijalna',
'file' => 'File',
'custom_code' => 'Custom code',
'custom_content' => 'Custom content',
],
'form_field_validation' => [
'select' => '--- Wybierz regułę walidacji ---',
'required' => 'Wymagane',
'email' => 'Email',
'numeric' => 'Numeryczne',
'custom' => 'Reguła niestandardowa',
],
'email' => [
'address_from' => 'Z adresu',
'address_from_placeholder' => 'example@domain.com',
'address_from_name' => 'Nazwa wyświetlana w polu "Od"',
'address_from_name_placeholder' => 'Jan Nowak',
'subject' => 'Temat wiadomości',
'subject_comment' => 'Ustaw tylko wtedy, gdy chcesz aby był inny niż zdefiniowano w Ustawienia > Mail > Szablony wiadomości email',
'template' => 'Szablon wiadomości',
'template_comment' => 'Kod szablonu wiadomości email utworzony w Ustawienia > Mail > Szablony wiadomości email. Pozostaw puste dla domyślnego szablonu: janvince.smallcontactform::mail.autoreply.',
'allow_email_queue' => 'Kolejkowanie wiadomości',
'allow_email_queue_comment' => 'Dodawaj wiadomości do kolejki zamiast wysyłać je natychmiastowo. Pamiętaj, najpierw musisz skonfigurować kolejkę OctoberCMS!',
'allow_notifications' => 'Wysyłaj powiadomienia',
'allow_notifications_comment' => 'Wyślij powiadomienie zaraz po wysłaniu formularza',
'notification_address_to' => 'Wyślij powiadomienie na adres/adresy',
'notification_address_to_comment' => 'Jeden adres email, lub lista oddzielona przecinkami',
'notification_address_to_placeholder' => 'notifications@domain.com',
'notification_address_from_form' => 'Wymuś zawartość pola "Od" powiadomienia (NIE WSPIERANE przez wszystkie systemy email)',
'notification_address_from_form_comment' => 'Ustaw adres, z którego wysyłane jest powiadomienie na adres email podany w formularzu kontaktowym (pole musi być ustawione w odwzorowaniu pól).',
'allow_autoreply' => 'Automatyczna odpowiedź',
'allow_autoreply_comment' => 'Wyślij kopię wiadomości do autora',
'autoreply_name_field' => 'Pole NAZWA',
'autoreply_name_field_empty_option' => '-- Wybierz --',
'autoreply_name_field_comment' => 'Musi być typu Tekst.<br><em>Zapisz i odśwież tą stronę jeśli nie widzisz pól.</em>',
'autoreply_email_field' => 'Pole EMAIL',
'autoreply_email_field_empty_option' => '-- Wybierz --',
'autoreply_email_field_comment' => 'Musi być typu Email.<br><em>Zapisz i odśwież tą stronę jeśli nie widzisz pól.</em>',
'autoreply_message_field' => 'Pole WIADOMOŚĆ',
'autoreply_message_field_empty_option' => '-- Wybierz --',
'autoreply_message_field_comment' => 'Musi być typu Pole tekstowe lub Tekst.<br><em>Zapisz i odśwież tą stronę jeśli nie widzisz pól.</em>',
'notification_template' => 'Szablon wiadomości powiadomienia',
'notification_template_comment' => 'Kod szablonu wiadomości email utworzony w Ustawienia > Mail > Szablony wiadomości email. Pozostaw puste dla domyślnego szablonu: janvince.smallcontactform::mail.notification.',
],
'antispam' => [
'add_antispam' => 'Dodaj pasywną ochronę antyspamową',
'add_antispam_comment' => 'Dodaj prostą, ale skuteczną pasywną kontrolę antyspamową (więcej informacji w pliku README.md)',
'antispam_delay' => 'Opóźnienie ochrony antyspamowej (s)',
'antispam_delay_comment' => 'Zabezpieczenia w postaci opóźnienia podczas wysyłania dużej ilości wiadomości (zwykle przez roboty)',
'antispam_delay_placeholder' => '3',
'antispam_label' => 'Etykieta pola ochrony antyspamowej',
'antispam_label_comment' => 'Etykieta będzie widoczna tylko dla przeglądarek, które nie obsługują JavaScript',
'antispam_label_placeholder' => 'Wyczyść to pole w celu wysłania wiadomości',
'antispam_error_msg' => 'Komunikat o błędzie - ochrona antyspamowa',
'antispam_error_msg_comment' => 'Komunikat, który zostanie wyświetlony, gdy uruchomi się ochrona antyspamowa',
'antispam_error_msg_placeholder' => 'Musisz wyczyścić to pole, aby wysłać wiadomość!',
'antispam_delay_error_msg' => 'Komunikat o błędzie - opóźnienie',
'antispam_delay_error_msg_comment' => 'Komunikat, który zostanie wyświetlony, gdy użytkownik wyśle wiadomość zbyt szybko',
'antispam_delay_error_msg_placeholder' => 'Wiadomość wysłana zbyt szybko! Poczekaj chwilę, a następnie spróbuj ponownie.',
'add_google_recaptcha' => 'Dodaj Google reCaptcha',
'add_google_recaptcha_comment' => 'Dodaj reCaptcha do formularza kontaktowego (więcej informacji w pliku README.md).<br>Możesz uzyskać klucze API na <a href="https://www.google.com/recaptcha/admin#list" target="_blank">stronie Google reCaptcha</a>.',
'google_recaptcha_site_key' => 'Klucz strony',
'google_recaptcha_site_key_comment' => 'Wpisz swój kod strony',
'google_recaptcha_secret_key' => 'Sekretny klucz',
'google_recaptcha_secret_key_comment' => 'Wpisz swój sekretny klucz',
'google_recaptcha_wrapper_css' => 'Klasa CSS kontenera reCaptcha',
'google_recaptcha_wrapper_css_comment' => 'Klasa CSS kontenera wokół pola reCaptcha',
'google_recaptcha_wrapper_css_placeholder' => 'form-group',
'google_recaptcha_error_msg' => 'Komunikat błędu',
'google_recaptcha_error_msg_comment' => 'Komunikat, który pokaże się, gdy reCAPTCHA zwróci błąd.',
'google_recaptcha_error_msg_placeholder' => 'Google reCAPTCHA validation error!',
'google_recaptcha_scripts_allow' => 'Automatycznie dodaj niezbędne skrypt JS',
'google_recaptcha_scripts_allow_comment' => 'Dodaj skrypty JS do twojej strony',
'google_recaptcha_locale_allow' => 'Zezwól na wykrywanie ustawień regionalnych',
'google_recaptcha_locale_allow_comment' => 'Spowoduje to dodanie bieżących ustawień regionalnych do skryptu reCAPTCHA, tak że będzie ona przetłumaczona.',
'add_ip_protection' => 'Sprawdzaj adres IP wysyłającego',
'add_ip_protection_comment' => 'Nie zezwalaj na zbyt dużo wiadomości z jednego adresu IP',
'add_ip_protection_count' => 'Maksymalna liczba wiadomości na dzień',
'add_ip_protection_count_comment' => 'Maksymalna liczba wiadomości możliwa do wysłania z jednego adresu IP na dzień',
'add_ip_protection_count_placeholder' => '3',
'add_ip_protection_error_get_ip' => 'Nie udało się ustalić Twojego adresu IP',
'add_ip_protection_error_too_many_submits' => 'Komunikat błędu - za dużo wiadomości',
'add_ip_protection_error_too_many_submits_comment' => 'Komunikat, który zostanie pokazany użytkownikowi',
'add_ip_protection_error_too_many_submits_placeholder' => 'Otrzymaliśmy dzisiaj zbyt wiele wiadomości z jednego adresu IP. Spróbuj ponownie jutro.',
'disabled_extensions' => 'Wyłączone rozszerzenia',
'disabled_extensions_comment' => 'Ustawienia na zakładce Prywatność wyłączyły te rozszerzenia',
],
'mapping' => [
'hint' => [
'title' => 'Dlaczego odwzorowywanie pól?',
'content' => '
<p>Możesz zbudować niestandardowy formularz z własnymi nazwami i typami pól.</p>
<p>System zapisuje wszystkie dane formularze w bazie danych. W celu szybkiego podglądu kolumny NAZWA, EMAIL i WIADOMOŚĆ widoczne osobno na liście wiadomości.</p>
<p>Musisz więc pomóc systemowi w identyfikacji tych kolumn poprzez odwzorowanie pól formularza.</p>
<p><em>Odwzorowania również używane przez system automatycznej odpowiedzi na wiadomości, w których ważne jest przynajmniej pole EMAIL.</em></p>
',
],
'warning' => [
'title' => 'Nie możesz wybrać pól?',
'content' => '<p>Jeśli nie widzisz pól formularza wciśnij przycisk zapisz na dole strony a następnie odśwież stronę (F5 lub Ctrl + R / Cmd + R)</p>',
],
],
'privacy' => [
'disable_messages_saving' => 'Wyłącz zapisywanie wiadomości',
'disable_messages_saving_comment' => 'Żadne dane nie zostaną zapisane w liście wiadomości. <strong>To ustawienie wyłączy również ochronę na podstawie adresu IP!</strong>',
'disable_messages_saving_comment_section' => '<div class="callout fade in callout-danger no-subheader"><div class="header"><i class="icon-warning"></i><h3>Pamiętaj, aby włączyć powiadomienia email! W przeciwnym wypadku nie otrzymasz żadnych danych z formularza.</h3></div></div>',
],
'tabs' => [
'form' => 'Formularz',
'buttons' => 'Przycisk wyślij',
'form_fields' => 'Pola',
'mapping' => 'Odwzorowanie pól',
'email' => 'Email',
'antispam' => 'Ochrona antyspamowa',
'privacy' => 'Prywatność'
],
],
'components' => [
'groups' => [
'hacks' => 'Hacki',
],
'properties' => [
'disable_notifications' => 'Wyłącz powiadomienia e-mail',
'disable_notifications_comment' => 'Spowoduje wyłączenie powiadomień e-mail (nadpisuje ustawienia globalne)',
'form_description' => 'Opis formularza',
'form_description_comment' => 'Możesz dodać opcjonalny opis formularza, który zostanie zapisany wraz z danymi wysłanymi przez formularz. Może także użyć {{ :slug }}.',
]
],
];

View File

@ -0,0 +1,322 @@
<?php
return [
'plugin' => [
'name' => 'Контактная форма',
'description' => 'Простой конструктор контактной формы',
],
'permissions' => [
'access_messages' => 'Доступ к списку сообщений',
'access_settings' => 'Управление настройками бэкенд',
'delete_messages' => 'Удалить сохраненные сообщения',
],
'navigation' => [
'main_label' => 'Контакт форма',
'messages' => 'Сообщения',
],
'controller' => [
'contact_form' => [
'name' => 'Контактная форма',
'description' => 'Вставьте контактную форму на страницу',
'no_fields' => 'Для работы плагина следует добавить поля формы в бэкенд (Настройки > Контактная форма > Поля)...',
],
'filter' => [
'date' => 'Диапазон дат',
],
'scoreboard' => [
'records_count' => 'Сообщения',
'latest_record' => 'Последнее от',
'new_count' => 'Новые',
'new_description' => 'Сообщения',
'read_count' => 'Прочитанные',
'all_count' => 'Всего',
'all_description' => 'Сообщения',
'settings_btn' => 'Настройки формы',
'mark_read' => 'Отметить как прочитанное',
'mark_read_confirm' => 'Вы действительно хотите отметить выбранные сообщения как прочитанные?',
'mark_read_success' => 'Успешно отмечено как прочитанное',
],
'preview' => [
'record_not_found' => 'Сообщение не найдено!',
],
],
'models' => [
'message' => [
'columns' => [
'datetime' => 'Дата и время',
'form_data' => 'Данные формы',
'name' => 'Имя',
'message' => 'Сообщение',
'new_message' => 'Статус',
'new' => 'Новое',
'read' => 'Прочитанное',
'remote_ip' => 'IP отправителя',
'form_alias' => 'Псевдоним',
'form_description' => 'Описание',
]
],
],
'controllers' => [
'messages' => [
'list_title' => 'Сообщения',
'preview' => 'Предварительный просмотр',
'preview_title' => 'Сообщение контактной формы',
'preview_date' => 'От даты:',
'preview_content_title' => 'Содержание:',
'remote_ip' => 'Отправлено с IP',
],
'index' => [
'unauthorized' => 'Несанкционированный доступ',
],
],
'reportwidget' => [
'partials' => [
'messages' => [
'label' => 'Контактная форма - Статистика сообщений',
'title' => 'Статистика сообщений',
'messages_all' => 'Все',
'messages_new' => 'Новые',
'messages_read' => 'Прочитанные',
],
'new_message' => [
'label' => 'Контактная форма - Новое сообщение',
'title' => 'Новое сообщение',
'link_text' => 'Нажмите, чтобы показать список сообщений',
],
],
],
'settings' => [
'form' => [
'css_class' => 'CSS классы формы',
'use_placeholders' => 'Использовать заполнители формы',
'use_placeholders_comment' => 'Будет скрыт <label>, а текст метки будет показан внутри поля.',
'disable_browser_validation' => 'Отключить валидацию браузера',
'disable_browser_validation_comment' => 'Запретить встроенные проверки и всплывающие окна браузера.',
'success_msg' => 'Текст сообщения об успешной отправке',
'success_msg_placeholder' => 'Ваше сообщение доставлено',
'error_msg' => 'Текст сообщения о ошибке',
'error_msg_placeholder' => 'При отправке сообщения произошла ошибка',
'allow_ajax' => 'Включить AJAX',
'allow_ajax_comment' => 'Разрешить AJAX с откатом для браузеров без JavaScript.',
'allow_confirm_msg' => 'Запрашивать подтверждение перед отправкой формы',
'allow_confirm_msg_comment' => 'Добавить текст диалога перед отправкой.',
'send_confirm_msg' => 'Текст подтверждения',
'send_confirm_msg_placeholder' => 'Отправить сообщение?',
'hide_after_success' => 'Скрыть форму после успешной отправки',
'hide_after_success_comment' => 'Показать только сообщение об успешной отправке.',
],
'buttons' => [
'send_btn_text' => 'Текст на кнопке',
'send_btn_text_placeholder' => 'Отправить',
'send_btn_css_class' => 'CSS классы кнопки',
'send_btn_wrapper_css' => 'CSS классы обертки (wrapper) кнопки',
],
'redirect' => [
'allow_redirect' => 'Редирект после отправки',
'allow_redirect_comment' => 'Перенаправить пользователя на другую страницу после успешного отправки формы.',
'redirect_url' => 'URL страницы для перенаправления',
'redirect_url_comment' => 'Введите URL-адрес страницы (например: /contact/thank-you)',
'redirect_url_external' => 'Внешний URL',
'redirect_url_external_comment' => 'Отметьте, если это внешний адрес (например: http://www.domain.com).',
],
'form_fields' => [
'prompt' => 'Добавить новое поле формы',
'name' => 'ИМЯ ПОЛЯ',
'name_comment' => 'Нижний регистр без специальных символов (например: name, email, home_address, ...).',
'type' => 'Тип поля',
'label' => 'Метка поля',
'label_placeholder' => 'Ваше имя',
'field_styling' => 'Пользовательские CSS классы',
'field_styling_comment' => 'Изменение стандартных Bootstrap стилей.',
'autofocus' => 'Автофокус',
'autofocus_comment' => 'Автофокусировка на этом поле формы.',
'wrapper_css' => 'CSS классы обертки (wrapper)',
'field_css' => 'CSS классы полей <input>',
'label_css' => 'CSS классы метки <label>',
'label_css_placeholder' => '',
'field_validation' => 'Валидация поля',
'field_validation_comment' => 'Добавить правила для проверки поля.',
'validation' => 'Валидация',
'validation_prompt' => 'Добавить валидацию',
'validation_error' => 'Сообщение при ошибке валидации',
'validation_error_placeholder' => 'Данные введены не верно',
'validation_error_comment' => 'Сообщение об ошибке, если валидация не пройдена.',
],
'email' => [
'address_from' => 'От адреса',
'address_from_name' => 'От имени',
'subject' => 'Тема письма',
'subject_comment' => 'Установите если хотите использовать отличное от шаблона, Настройки > Шаблоны почты.',
'template' => 'Шаблон почты',
'template_comment' => 'Код шаблона почты, созданный в Настройки > Шаблоны почты. Если оставить пустым будет использован по умолчанию: janvince.smallcontactform::mail.autoreply.',
'allow_notifications' => 'Разрешить уведомления',
'allow_notifications_comment' => 'Отправлять уведомление на email после отправки формы.',
'notification_address_to' => 'Отправить уведомление на email',
'allow_autoreply' => 'Автоматический ответ',
'allow_autoreply_comment' => 'Отправлять копию содержимого формы автору.',
'autoreply_name_field' => 'Поле формы NAME',
'autoreply_name_field_comment' => 'Должен быть тип поля: Text.<br><em>Сохраните и обновите эту страницу, если вы не видите свои поля.</em>',
'autoreply_email_field' => 'Поле формы EMAIL',
'autoreply_email_field_comment' => 'Должен быть тип поля: Email.<br><em>Сохраните и обновите эту страницу, если вы не видите свои поля.</em>',
'autoreply_message_field' => 'Поле формы MESSAGE',
'autoreply_message_field_comment' => 'Должен быть тип поля: Textarea или Text.<br><em>Сохраните и обновите эту страницу, если вы не видите свои поля.</em>',
'notification_template' => 'Шаблон почты для уведомления',
'notification_template_comment' => 'Код шаблона почты, созданный в Настройки > Шаблоны почты. Если оставить пустым будет использован по умолчанию: janvince.smallcontactform::mail.autoreply.',
],
'antispam' => [
'add_antispam' => 'Добавить пассивную антиспам защиту',
'add_antispam_comment' => 'Простая, но эффективная антиспам защита (подробнее в README.md файле).',
'antispam_delay' => 'Антиспам задержка в секундах',
'antispam_delay_comment' => 'Защищает от слишком быстрой отправки формы (против роботов).',
'antispam_delay_placeholder' => '3',
'antispam_label' => 'Метка поля антиспама',
'antispam_label_comment' => 'Будет отображаться в браузерах с выключенным JavaScript.',
'antispam_label_placeholder' => 'Пожалуйста очистите это поле',
'antispam_error_msg' => 'Текст сообщения при ошибке',
'antispam_error_msg_comment' => 'Сообщение будет показано пользователю, при срабатывании защиты от спама',
'antispam_error_msg_placeholder' => 'Антиспам - Пожалуйста очистите это поле!',
'antispam_delay_error_msg' => 'Текст сообщения при ошибке',
'antispam_delay_error_msg_comment' => 'Сообщение для пользователя, чтобы показать, что форма была отправлена слишком быстро.',
'antispam_delay_error_msg_placeholder' => 'Форма отправлена слишком быстро, пожалуйста подождите 5 секунд.',
'add_google_recaptcha' => 'Добавить Google reCaptcha',
'add_google_recaptcha_comment' => 'Добавить reCaptcha в контактную форму (подробнее в README.md файле).<br>Вы можете получить API ключ на <a href="https://www.google.com/recaptcha/admin#list" target="_blank">Google reCaptcha site</a>.',
'google_recaptcha_site_key_comment' => 'Введите ключ своего сайта',
'google_recaptcha_secret_key_comment' => 'Введите секретный ключ',
'google_recaptcha_error_msg' => 'Текст сообщения при ошибке',
'google_recaptcha_error_msg_comment' => 'Сообщение для пользователя, когда он не прошел проверку reCAPTCHA.',
'google_recaptcha_error_msg_placeholder' => 'Google reCAPTCHA ошибка валидации!',
'google_recaptcha_scripts_allow' => 'Автоматический добавить JavaScript',
'google_recaptcha_scripts_allow_comment' => 'Добавить загрузку необходимых JavaScript на ваш сайт.',
'google_recaptcha_locale_allow' => 'Разрешить локализацию',
'google_recaptcha_locale_allow_comment' => 'Переведет reCAPTCHA, в соответствии с языком веб-страницы.',
'add_ip_protection' => 'Проверять IP-адрес отправителя',
'add_ip_protection_comment' => 'Ограничивает число отправки форм с одного IP-адреса.',
'add_ip_protection_count' => 'Максимальное число отправки форм за день',
'add_ip_protection_count_comment' => 'Количество разрешенных отправлений формы с одного IP-адреса в течение одного дня.',
'add_ip_protection_count_placeholder' => '3',
'add_ip_protection_error_too_many_submits' => 'Сообщение об ошибке',
'add_ip_protection_error_too_many_submits_comment' => 'Текст ошибки при привышении максимального числа отправок, для показа пользователю.',
'add_ip_protection_error_too_many_submits_placeholder' => 'С вашего IP отправлено слишком много форм!',
],
'mapping' => [
'hint' => [
'title' => 'Почему отображение полей?',
'content' => '
<p>Вы можете создать пользовательскую форму с собственными именами и типами полей.</p>
<p>Система записывает все данные формы в базу данных. Для быстрого обзора в бэкенд Контакт форма > Сообщения, нужно привязать поля: Имя, Email, Сообщения.</p>
<p>Поэтому вам необходимо помочь системе идентифицировать эти столбцы, сопоставляя их с полями формы.</p>
<p><em>Эти сопоставления также используются для атоматической отправки писем, где важно, по крайней мере, сопоставление поля Email.</em></p>
',
],
'warning' => [
'title' => 'Не можете выбрать поля формы?',
'content' => '
<p>Если вы не видите свои поля, нажмите кнопку «Сохранить» в нижней части этой страницы, а затем перезагрузите страницу (F5 или Ctr+R / Cmd+R).</p>
',
],
],
'tabs' => [
'form' => 'Форма',
'buttons' => 'Кнопка отправки',
'form_fields' => 'Редактор полей',
'mapping' => 'Отображение',
'email' => 'Email',
'antispam' => 'АнтиСпам',
],
],
];

View File

@ -0,0 +1,305 @@
<?php
return [
'plugin' => [
'name' => 'Kontaktný formulár',
'description' => 'Jednoduchý kontaktný formulár',
'category' => 'Small plugins',
],
'permissions' => [
'access_messages' => 'Prístup k zoznamu správ',
'access_settings' => 'Prístup k nastaveniam',
'delete_messages' => 'Zmazať vybrané správy',
'export_messages' => 'Exportovať správy',
],
'navigation' => [
'main_label' => 'Kontaktný formulár',
'messages' => 'Správy',
],
'controller' => [
'contact_form' => [
'name' => 'Kontaktný formulár',
'description' => 'Pridá na stránku kontaktný formulár',
'no_fields' => 'Pridajte prosím nejaké formulárové polia v administrácií systému (Nastavenia > Kontaktný formulár > Polia)...',
],
'filter' => [
'date' => 'Rozmedzie dátumu',
],
'scoreboard' => [
'records_count' => 'Správy',
'latest_record' => 'najnovšie od',
'new_count' => 'Nové',
'new_description' => 'Správ',
'read_count' => 'Prečítané',
'all_count' => 'Celkom',
'all_description' => 'Správ',
'settings_btn' => 'Nastavenie formulára',
'mark_read' => 'Označiť ako prečítané',
'mark_read_confirm' => 'Naozaj chcete vybrané správy označiť ako prečítané?',
'mark_read_success' => 'Správy boli označené ako prečítané.',
],
'preview' => [
'record_not_found' => 'Správa nebola nájdená!',
],
],
'models' => [
'message' => [
'columns' => [
'id' => 'ID',
'datetime' => 'Dátum a čas',
'form_data' => 'Dáta formulára',
'name' => 'Meno',
'email' => 'Email',
'message' => 'Správa',
'new_message' => 'Stav',
'new' => 'Nová',
'read' => 'Prečítaná',
'remote_ip' => 'IP odosielateľa',
'created_at' => 'Dátum vytvorenia',
'updated_at' => 'Dátum aktualizácie',
]
],
],
'controllers' => [
'messages' => [
'list_title' => 'Správy',
'preview' => 'Náhľad',
'preview_title' => 'Správa z kontaktného formulára',
'preview_date' => 'Z dňa:',
'preview_content_title' => 'Obsah:',
'remote_ip' => 'odoslané z ip',
'form_alias' => 'Alias',
'form_description' => 'Popis',
'export' => 'Export',
],
'index' => [
'unauthorized' => 'Neoprávnený prístup!',
],
],
'mail' => [
'templates' => [
'autoreply' => 'Správa automatickej odpovede z kontaktného formulára (Anglicky)',
'notification' => 'Notifikácia z kontaktného formulára (Anglicky)',
'autoreply_cs' => 'Správa automatickej odpovede z kontaktného formulára (Česky)',
'notification_cs' => 'Notifikácia z kontaktného formulára (Česky)',
]
],
'reportwidget' => [
'partials' => [
'messages' => [
'label' => 'Kontaktný formulár - Prehľad správ',
'title' => 'Prehľad správ',
'messages_all' => 'Všetky',
'messages_new' => 'Nové',
'messages_read' => 'Prečítané',
],
'new_message' => [
'label' => 'Kontaktný formulár - Nové správy',
'title' => 'Nové správy',
'link_text' => 'Kliknite pre zobrazenie prehľadu správ',
],
],
],
'settings' => [
'form' => [
'css_class' => 'CSS trieda formulára',
'use_placeholders' => 'Používať zástupný text (placeholder)',
'use_placeholders_comment' => 'Miesto popiskov nad formulárovými poliami bude použitý zástupný text',
'disable_browser_validation' => 'Zakázať validáciu prehliadačom',
'disable_browser_validation_comment' => 'Nepovoliť prehliadaču použiť vlastnú validáciu a zobrazovať výstrahy.',
'success_msg' => 'Správa po úspešnom odoslaní',
'success_msg_placeholder' => 'Formulár bol v poriadku odoslaný.',
'error_msg' => 'Chybová správa',
'error_msg_placeholder' => 'Pri odosielaní formulára došlo k chybe!',
'allow_ajax' => 'Povoliť AJAX',
'allow_ajax_comment' => 'Povolí AJAX, ale umožní fungovanie formulára aj na prehliadačoch s vypnutým JavaScriptom',
'allow_confirm_msg' => 'Požadovať potvrdenie pred odoslaním',
'allow_confirm_msg_comment' => 'Zobrazí potvrdzovacie okno pred odoslaním formulára',
'send_confirm_msg' => 'Text potvrdenia',
'send_confirm_msg_placeholder' => 'Naozaj chcete odoslať formulár?',
'hide_after_success' => 'Skryť formulár po úspešnom odoslaní',
'hide_after_success_comment' => 'Po odoslaní zobrazí iba správu s potvrdením bez formulára',
'add_assets' => 'Pridať doplnky',
'add_assets_comment' => 'Automaticky vloží potrebné CSS štýly a JS skripty (Viac informácií je v súbore README.md)',
'add_css_assets' => 'Pridať CSS štýly',
'add_css_assets_comment' => 'Vloží všetky potrebné štýly',
'add_js_assets' => 'Pridať JS skripty',
'add_js_assets_comment' => 'Vloží všetky potrebné skripty',
],
'buttons' => [
'send_btn_text' => 'Text odosielacieho tlačidla',
'send_btn_text_placeholder' => 'Odoslať',
'send_btn_css_class' => 'CSS trieda odosielacieho tlačidla',
'send_btn_css_class_placeholder' => 'btn btn-primary',
'send_btn_wrapper_css' => 'CSS trieda kontajneru',
'send_btn_wrapper_css_placeholder' => 'form-group',
],
'redirect' => [
'allow_redirect' => 'Presmerovať po úspěšnom odoslaní',
'allow_redirect_comment' => 'Presmerovať na inú stránku po úspešnom odoslaní formulára',
'redirect_url' => 'URL stránky pre presmerovanie',
'redirect_url_comment' => 'Vložte URL adresu stránky, kam bude presmerované (např. /kontakt/dakujeme)',
'redirect_url_placeholder' => '/kontakt/dakujeme',
'redirect_url_external' => 'Externá URL',
'redirect_url_external_comment' => 'Toto je adresa externej stránky (napr. http://www.domain.com)',
],
'form_fields' => [
'prempt' => 'Pridať nové pole formulára',
'name' => 'NÁZOV POĽA',
'name_comment' => 'Malými písmenami bez diakritiky (napr. meno, email, vasa_poznamka, ...)',
'type' => 'Typ poľa',
'label' => 'Popisok (label)',
'label_placeholder' => 'Pole formulára',
'field_styling' => 'Vlastné CSS triedy',
'field_styling_comment' => 'Môžete pridať vlastné štýly',
'autofocus' => 'Automaticky zvýrazniť (autofocus)',
'autofocus_comment' => 'Po zobrazení nastaviť na pole kurzor',
'wrapper_css' => 'CSS trieda kontajneru',
'wrapper_css_placeholder' => 'form-group',
'field_css' => 'CSS trieda poľa',
'field_css_placeholder' => 'form-control',
'label_css' => 'CSS trieda popisku (label)',
'label_css_placeholder' => '',
'field_validation' => 'Validačné pravidlá poľa',
'field_validation_comment' => 'Povolí nastavenie vlastných validačných pravidiel',
'validation' => 'Pravidlo',
'validation_prempt' => 'Pridať pravidlo',
'validation_type' => 'Typ',
'validation_error' => 'Chybová správa',
'validation_error_placeholder' => 'prosím vložte správne dáta.',
'validation_error_comment' => 'Chybová hláška, ktorá sa zobrazí pri poli',
'custom' => 'vlastné pole',
'custom_description' => 'vlastné pole s validačnými pravidlami',
],
'form_field_types' => [
'text' => 'Text',
'email' => 'Email',
'textarea' => 'Textarea',
'checkbox' => 'Checkbox',
'dropdown' => 'Dropdown',
'file' => 'File',
'custom_code' => 'Custom code',
'custom_content' => 'Custom content',
],
'form_field_validation' => [
'select' => '--- Vyberte pravidlo ---',
'required' => 'Vyžadované',
'email' => 'Email',
'numeric' => 'Číslo',
],
'email' => [
'address_from' => 'Adresa OD',
'address_from_placeholder' => 'john.doe@domain.com',
'address_from_name' => 'Meno odesielateľa',
'address_from_name_placeholder' => 'John Doe',
'subject' => 'Peedmet emailu',
'subject_comment' => 'Nastavte iba pokiaľ chcete prepísať predmet definovaný v šablóne (Nastavenia > E-mailové šablóny).',
'template' => 'Šablóna emailu',
'template_comment' => 'Kód emailovej šablóny vytvorenej v Nastavenia > E-mailové šablóny. Nechajte prázdne pre predvolenú šablónu: janvince.smallcontactform::mail.autoreply.',
'allow_email_queue' => 'Řadit do fronty',
'allow_email_queue_comment' => 'Pridať emaily do fronty místo okamžitého odoslaní. Musíte ale nejdříve správně nakonfigurovat frontu systému OctoberCMS!',
'allow_notifications' => 'Povoliť odosielanie upozornění',
'allow_notifications_comment' => 'Odesílat upozornení, pokiaľ niekto odošle formulár.',
'notification_address_to' => 'Upozornenia posielať na adresu:',
'notification_address_to_comment' => 'Jedna emailová adresa alebo zoznam adries oddelených čiarkami',
'notification_address_to_placeholder' => 'notifications@domain.com',
'notification_address_from_form' => 'nastaviť adresu Od na email z formulára (NEMUSÍ PODPOROVAŤ váš emailový systém!)',
'notification_address_from_form_comment' => 'Nastaví u odosielaného upozornenia adresu Od (From) na tú, ktorá bola zadaná vo formulári (stĺpec email musí mať nastavenú väzbu).',
'allow_autoreply' => 'Povoliť automatickú odpoveď',
'allow_autoreply_comment' => 'Poslať automatickú odpoveď odosielateľovi formulára',
'autoreply_name_field' => 'Pole formulára, ktoré obsahuje MENO odosielateľa',
'autoreply_name_field_empty_option' => '-- Vyberte --',
'autoreply_name_field_comment' => 'Pole typu Text.',
'autoreply_email_field' => 'Pole formulára, ktoré obsahuje EMAIL odosielateľa',
'autoreply_email_field_empty_option' => '-- Vyberte --',
'autoreply_email_field_comment' => 'Pole typu Email.',
'autoreply_message_field' => 'Pole formulára, ktoré obsahuje SPRÁVU',
'autoreply_message_field_empty_option' => '-- vyberte --',
'autoreply_message_field_comment' => 'Pole typu Textarea alebo Text.',
'notification_template' => 'Šablóna notifikačného emailu',
'notification_template_comment' => 'Kód emailovej šablóny vytvorenej v Nastavenia > E-mailové šablóny. Nechajte prázdne pre predvolenú šablónu: janvince.smallcontactform::mail.notification.',
],
'antispam' => [
'add_antispam' => 'Pridať pasívnu ochranu proti spamu',
'add_antispam_comment' => 'Pridá jednoduchú ale efektívnu pasívnu ochranu proti robotom (viac informácií v súbore README.md)',
'antispam_delay' => 'Meškanie formulára (s)',
'antispam_delay_comment' => 'Test na príliš rýchle odoslanie formulára (väčšinou robotmi)',
'antispam_delay_placeholder' => '3',
'antispam_label' => 'Popisok (label) antispamového poľa',
'antispam_label_comment' => 'Popisok bude viditeľný iba na prehliadačoch bez podpory JavaScriptu',
'antispam_label_placeholder' => 'prosím vymažte toto pole',
'antispam_error_msg' => 'Chybová správa',
'antispam_error_msg_comment' => 'Správa, ktorá se zobrazí, pokiaľ sa aktivuje pasívny antispam',
'antispam_error_msg_placeholder' => 'prosím vymažte obsah tohoto poľa!',
'antispam_delay_error_msg' => 'Chybová správa pri rýchlom odoslaní',
'antispam_delay_error_msg_comment' => 'Správa, ktorá se zobrazí pri príliš rýchlom odoslaní formulára',
'antispam_delay_error_msg_placeholder' => 'Príliš rýchle odoslanie formulára! prosím skúste to za pár sékúnd znovu!',
'add_google_recaptcha' => 'Pridať Google reCaptcha',
'add_google_recaptcha_comment' => 'Pridá reCaptcha do kontaktného formulára (viac informácií v súbore README.md). <br>API kľúče môžete získať na <a href="https://www.google.com/recaptcha/admin#list" target="_blank">stránke Google reCaptcha</a>.',
'google_recaptcha_site_key' => 'Site key',
'google_recaptcha_site_key_comment' => 'Vložte svoj "site key"',
'google_recaptcha_secret_key' => 'Secret key',
'google_recaptcha_secret_key_comment' => 'Vložte svoj "secret key"',
'google_recaptcha_error_msg' => 'Chybová správa',
'google_recaptcha_error_msg_comment' => 'Správa, ktorá sa zobrazí, pokiaľ dôjde k chybe pri overení reCAPTCHA.',
'google_recaptcha_error_msg_placeholder' => 'Chyba pri overení pomocou Google reCAPTCHA!',
'google_recaptcha_scripts_allow' => 'Automaticky pridať Google reCAPTCHA skript',
'google_recaptcha_scripts_allow_comment' => 'Vloží odkaz na JavaScriptový súbor potrebný pre fungovanie reCAPTCHA.',
'google_recaptcha_locale_allow' => 'Povoliť detekciu jazyka',
'google_recaptcha_locale_allow_comment' => 'Pridá k reCAPTCHA skriptu kód jazyka stránky, takže overovací box bude preložený do jazyka návštevníka webu.',
'add_ip_pretection' => 'Testovať IP adresu odosielateľa',
'add_ip_pretection_comment' => 'Nepovolí príliš mnoho odoslaní formulára z jednej IP adresy',
'add_ip_pretection_count' => 'Maximálny počet odoslaní behom jedného dňa',
'add_ip_pretection_count_comment' => 'Počet povolených odoslaní formulára z jednej IP adresy behom jedného dňa',
'add_ip_pretection_count_placeholder' => '3',
'add_ip_pretection_error_get_ip' => 'Nepodarilo sa určiť vašu IP adresu!',
'add_ip_pretection_error_too_many_submits' => 'Chybová správa pri prekročení počtu odoslaní',
'add_ip_pretection_error_too_many_submits_comment' => 'Správa, ktorú obdrží uživateľ pri prekročení limitu počtu odoslaní formulára',
'add_ip_pretection_error_too_many_submits_placeholder' => 'Bol prekročný limit odoslaní formulára behom jedného dňa!',
'disabled_extensions' => 'Zakázané rozšírenia',
'disabled_extensions_comment' => 'Nastavenia zo záložky Súkromie spôsibili vypnutie týchto rozšírení',
],
'mapping' => [
'hint' => [
'title' => 'zrušiť vazby na stĺpce?',
'content' => '
<p>Môžete vytvoriť ľubovolný formulár s vlastnými poliami a ich typmi.</p>
<p>Systém zapíše do databázy všetky odoslané dáta formulára, ale pre Prehľad správ zvlášť ukladané polia Meno, Email a Správa.</p>
<p>preto je nutné identifikovať pre tieot stĺpce odpovedajúce polia vo vašom formulári.</p>
<p><em>Vytvorené väzby použité aj pri odosielaní automatických odpovedí, kde je nutná väzba aspoň na pole Email.</em></p>
',
],
'warning' => [
'title' => 'Nevidíte vaše formulárové polia?',
'content' => '
<p>Pokiaľ tu nevidíte svoje formulárové polia, kliknite dole na tlačidlo Uložiť a potom obnovte stránku (F5 alebo Ctrl+R / Cmd+R).</p>
',
],
],
'privacy' => [
'disable_messages_saving' => 'Zakázať ukladanie správ',
'disable_messages_saving_comment' => 'Pokiaľ je zaškrtnuté, odoslané správy sa nebudú ukladať do databázy.<br><strong>Táto voľba zárovaň zakáže použitie IP ochrany!</strong>',
'disable_messages_saving_comment_section' => '<div class="callout fade in callout-danger no-subheader"><div class="header"><i class="icon-warning"></i><h3>Uistite sa, že máte povolené notifikačné emaily, inak nebudete mať žiadne dáta z odoslaných formulárov!</h3></div></div>',
],
'tabs' => [
'form' => 'Formulár',
'buttons' => 'Odosielacie tlačidlo',
'form_fields' => 'Pole formulára',
'mapping' => 'Väzby stĺpcov',
'email' => 'Email',
'antispam' => 'Antispam',
'privacy' => 'Súkromie'
],
],
'components' => [
'groups' => [
'hacks' => 'Hacks',
],
'preperties' => [
'disable_notifications' => 'Zakázať odosielanie notifikačných emailov',
'disable_notifications_comment' => 'Zakáže odoslanie notifikačných emailov (bez ohľadu na systémové nastavenia formulára)',
'form_description' => 'Popisok formulára',
'form_description_comment' => 'Volitelne môžete pridať popisok formulára, ktorý sa uloží spoločne s odoslanými dátami zoznamu správ. Môžete použiť aj {{ :slug }}.',
]
],
];

View File

@ -0,0 +1,540 @@
<?php namespace JanVince\SmallContactForm\Models;
use Str;
use Model;
use URL;
use October\Rain\Router\Helper as RouterHelper;
use Cms\Classes\Page as CmsPage;
use Cms\Classes\Theme;
use JanVince\SmallContactForm\Models\Settings;
use Log;
use Validator;
use Mail;
use Request;
use Carbon\Carbon;
use View;
use App;
use System\Models\MailTemplate;
use System\Classes\MailManager;
use Twig;
use Input;
use System\Models\File;
class Message extends Model
{
use \October\Rain\Database\Traits\Validation;
public $table = 'janvince_smallcontactform_messages';
public $implement = ['@RainLab.Translate.Behaviors.TranslatableModel'];
public $timestamps = true;
public $rules = [];
public $translatable = [];
protected $guarded = [];
protected $jsonable = ['form_data'];
public $attachMany = [
'uploads' => 'System\Models\File',
];
/**
* Scope new messages only
*/
public function scopeIsNew($query)
{
return $query->where('new_message', '=', 1);
}
/**
* Scope read messages only
*/
public function scopeIsRead($query)
{
return $query->where('new_message', '=', 0);
}
public function storeFormData($data, $formAlias, $formDescription){
if(Settings::getTranslated('privacy_disable_messages_saving')) {
return;
}
$output = [];
$name_field_value = NULL;
$email_field_value = NULL;
$message_field_value = NULL;
$formFields = Settings::getTranslated('form_fields');
$uploads = [];
foreach($data as $key => $value) {
// skip helpers
if(substr($key, 0, 1) == '_'){
continue;
}
// skip reCaptcha
if($key == 'g-recaptcha-response'){
continue;
}
// skip non-defined fields
// store uploaded file -- currently only one file is supported
$fieldDefined = null;
$fieldUpload = null;
foreach( $formFields as $field) {
if( $field['name'] == $key) {
$fieldDefined = true;
if($field['type'] == 'file') {
$fieldUpload = true;
if(!empty(Input::file($key))) {
$file = new File;
$file->data = (Input::file($key));
$file->is_public = true;
$file->save();
$uploads[] = $file;
}
}
}
}
// skip uploads
if($fieldUpload){
continue;
}
if( !$fieldDefined ) {
Log::warning('SMALL CONTACT FORM WARNING: Found a non-defined field in sent data! Field name: ' . e($key) . ' and value: ' . e($value['value']) );
continue;
}
$output[$key] = e($value['value']);
// if email field is assigned in autoreply, save it separatelly
if(empty($email_field_value) and $key == Settings::getTranslated('autoreply_email_field')){
$email_field_value = e($value['value']);
}
// if name field is assigned in autoreply, save it separatelly
if(empty($name_field_value) and $key == Settings::getTranslated('autoreply_name_field')){
$name_field_value = e($value['value']);
}
// if message is assigned in autoreply, save it separatelly
if(empty($message_field_value) and $key == Settings::getTranslated('autoreply_message_field')){
$message_field_value = e($value['value']);
}
}
$this->form_data = $output;
$this->name = $name_field_value;
$this->email = $email_field_value;
$this->message = $message_field_value;
$this->remote_ip = Request::ip();
$this->form_alias = $formAlias;
$this->form_description = $formDescription;
$this->url = url()->full();
$this->save();
// Add files
if($uploads) {
foreach($uploads as $upload) {
$this->uploads()->add($upload);
}
}
return $this;
}
/**
* Build and send autoreply message
*/
public function sendAutoreplyEmail($postData, $componentProperties = [], $formAlias, $formDescription, $messageObject){
if(!Settings::getTranslated('allow_autoreply')) {
return;
}
if (!empty($componentProperties['disable_autoreply'])) {
return;
}
if(!Settings::getTranslated('autoreply_email_field')) {
Log::error('SMALL CONTACT FORM ERROR: Contact form data have no email to send autoreply message!');
return;
}
/**
* Extract and test email field value
*/
$sendTo = '';
foreach($postData as $key => $field) {
if($key == Settings::getTranslated('autoreply_email_field')){
$sendTo = $field['value'];
}
}
$validator = Validator::make(['email' => $sendTo], ['email' => 'required|email']);
if($validator->fails()){
Log::error('SMALL CONTACT FORM ERROR: Email to send autoreply is not valid!' . PHP_EOL . ' Data: '. json_encode($postData) );
return;
}
if( Settings::getTranslated('allow_email_queue') ){
$method = 'queue';
} else {
$method = 'send';
}
$output = [];
$outputFull = [];
$formFields = Settings::getTranslated('form_fields');
foreach($formFields as $field) {
if(!empty($field['type'] and $field['type'] == 'file')) {
continue;
}
$fieldValue = null;
if( !empty( $postData[ $field['name'] ]['value'] ) ) {
$fieldValue = e( html_entity_decode( $postData[ $field['name'] ]['value'] ) );
} else {
$fieldValue = null;
}
if( !empty( $field['name'] ) ) {
$outputFull[ $field['name'] ] = array_merge( $field, [ 'value' => $fieldValue ] );
}
$output[ $field['name'] ] = $fieldValue;
}
$output['form_description'] = $formDescription;
$output['form_alias'] = $formAlias;
$template = Settings::getTranslatedTemplates('en', App::getLocale(), 'autoreply');
if( Settings::getTranslated('email_template') ){
if(View::exists(Settings::getTranslated('email_template')) OR !empty( MailTemplate::listAllTemplates()[Settings::getTranslated('email_template')] ) ) {
$template = Settings::getTranslated('email_template');
} else {
Log::error('SMALL CONTACT FORM: Missing defined email template: ' . Settings::getTranslated('email_template') . '. Default template will be used!');
}
}
/**
* Override email template by component property
* Language specific template has priority (override non language specific)
*/
if ( !empty($componentProperties['autoreply_template']) and !empty( MailTemplate::listAllTemplates()[ $componentProperties['autoreply_template'] ] ) ) {
$template = $componentProperties['autoreply_template'];
} elseif ( !empty($componentProperties['autoreply_template']) and empty( MailTemplate::listAllTemplates()[ $componentProperties['autoreply_template'] ] ) ) {
Log::error('SMALL CONTACT FORM: Missing defined email template: ' . $componentProperties['autoreply_template'] . '. ' . $template . ' template will be used!');
}
if ( !empty($componentProperties[ ('autoreply_template_'.App::getLocale())]) and !empty( MailTemplate::listAllTemplates()[ $componentProperties[ ('autoreply_template_'.App::getLocale())] ] ) ) {
$template = $componentProperties[('autoreply_template_'.App::getLocale())];
} elseif ( !empty($componentProperties[ ('autoreply_template_'.App::getLocale())]) and empty( MailTemplate::listAllTemplates()[ $componentProperties[ ('autoreply_template_'.App::getLocale())] ] ) ) {
Log::error('SMALL CONTACT FORM: Missing defined email template: ' . $componentProperties[ ('autoreply_template_'.App::getLocale())] . '. ' . $template . ' template will be used!');
}
if(!empty($messageObject->uploads))
{
$uploads = $messageObject->uploads;
}
else
{
$uploads = [];
}
Mail::{$method}($template, ['messageObject' => $messageObject, 'uploads' => $uploads, 'fields' => $output, 'fieldsDetails' => $outputFull, 'url' => url()->full()], function($message) use($sendTo, $componentProperties, $output){
$message->to($sendTo);
if (!empty($componentProperties['autoreply_subject'])) {
$message->subject(Twig::parse($componentProperties['autoreply_subject'],['fields' => $output]));
\App::forgetInstance('parse.twig');
\App::forgetInstance('twig.environment');
} elseif (Settings::getTranslated('email_subject')) {
$message->subject(Twig::parse(Settings::getTranslated('email_subject'), ['fields' => $output]));
\App::forgetInstance('parse.twig');
\App::forgetInstance('twig.environment');
}
/**
* From address
* Component's property can override this
*/
$fromAddress = null;
$fromAddressName = null;
if( Settings::getTranslated('email_address_from') ) {
$fromAddress = Settings::getTranslated('email_address_from');
$fromAddressName = Settings::getTranslated('email_address_from_name');
}
if( !empty($componentProperties['autoreply_address_from']) ) {
$fromAddress = $componentProperties['autoreply_address_from'];
}
if( !empty($componentProperties['autoreply_address_from_name']) ) {
$fromAddressName = $componentProperties['autoreply_address_from_name'];
}
if( !empty($componentProperties[ ('autoreply_address_from_name_'.App::getLocale()) ]) ) {
$fromAddressName = $componentProperties[ ('autoreply_address_from_name_'.App::getLocale()) ];
}
$validator = Validator::make(['email' => $fromAddress], ['email' => 'required|email']);
if($validator->fails()){
Log::error('SMALL CONTACT FORM ERROR: Autoreply email address is invalid (' .$fromAddress. ')! System email address and name will be used.');
} else {
$message->from($fromAddress, $fromAddressName);
}
/**
* Reply To address
* Component's property can override this
*/
$replyToAddress = null;
if( Settings::getTranslated('email_address_replyto') ) {
$replyToAddress = Settings::getTranslated('email_address_replyto');
}
if( !empty($componentProperties['autoreply_address_replyto']) ) {
$replyToAddress = $componentProperties['autoreply_address_replyto'];
}
if( $replyToAddress ) {
$validator = Validator::make(['email' => $replyToAddress], ['email' => 'required|email']);
if($validator->fails()){
Log::error('SMALL CONTACT FORM ERROR: Autoreply Reply To email address is invalid (' .$replyToAddress. ')! No Reply To header will be added.');
} else {
$message->replyTo($replyToAddress);
}
}
});
}
/**
* Build and send notification message
*/
public function sendNotificationEmail($postData, $componentProperties = [], $formAlias, $formDescription, $messageObject){
if(!Settings::getTranslated('allow_notifications')) {
return;
}
if(!empty($componentProperties['disable_notifications'])) {
return;
}
$sendTo = (!empty($componentProperties['notification_address_to']) ? $componentProperties['notification_address_to'] : Settings::getTranslated('notification_address_to') );
$sendToAddresses = explode(',', $sendTo);
$sendToAddressesValidated = [];
foreach($sendToAddresses as $sendToAddress) {
$validator = Validator::make(['email' => trim($sendToAddress)], ['email' => 'required|email']);
if($validator->fails()){
Log::error('SMALL CONTACT FORM ERROR: Notification email address (' .trim($sendToAddress). ') is invalid! No notification will be delivered!');
} else {
$sendToAddressesValidated[] = trim($sendToAddress);
}
}
if( !count($sendToAddressesValidated) ) {
return;
}
if( Settings::getTranslated('allow_email_queue') ){
$method = 'queue';
} else {
$method = 'send';
}
$output = [];
$outputFull = [];
$formFields = Settings::getTranslated('form_fields');
$replyToAddress = null;
$replyToName = null;
foreach($formFields as $field) {
if(!empty($field['type'] and $field['type'] == 'file')) {
continue;
}
$fieldValue = null;
if( !empty( $postData[ $field['name'] ]['value'] ) ) {
$fieldValue = e( html_entity_decode( $postData[ $field['name'] ]['value'] ) );
} else {
$fieldValue = null;
}
if( !empty( $field['name'] ) ) {
$outputFull[ $field['name'] ] = array_merge( $field, [ 'value' => $fieldValue ] );
}
// If email field is assigned, prepare for replyTo
if(empty($replyToAddress) and $field['name'] == Settings::getTranslated('autoreply_email_field') and isset($postData[$field['name']]['value'])){
$replyToAddress = e( $postData[$field['name']]['value'] );
}
// If name field is assigned, prepare for fromAddress
if(empty($replyToName) and $field['name'] == Settings::getTranslated('autoreply_name_field') and isset($postData[$field['name']]['value'])){
$replyToName = e( $postData[$field['name']]['value'] );
}
$output[ $field['name'] ] = $fieldValue;
}
$output['form_description'] = $formDescription;
$output['form_alias'] = $formAlias;
$template = Settings::getTranslatedTemplates('en', App::getLocale(), 'notification');
if( Settings::getTranslated('notification_template') ){
if(View::exists(Settings::getTranslated('notification_template')) OR !empty( MailTemplate::listAllTemplates()[Settings::getTranslated('notification_template')] ) ) {
$template = Settings::getTranslated('notification_template');
} else {
Log::error('SMALL CONTACT FORM: Missing defined email template: ' . Settings::getTranslated('notification_template') . '. Default template will be used!');
}
}
/**
* Override email template by component property
* Language specific template has priority (override non language specific)
*/
if ( !empty($componentProperties['notification_template']) and !empty( MailTemplate::listAllTemplates()[ $componentProperties['notification_template'] ] ) ) {
$template = $componentProperties['notification_template'];
} elseif ( !empty($componentProperties['notification_template']) and empty( MailTemplate::listAllTemplates()[ $componentProperties['notification_template'] ] ) ) {
Log::error('SMALL CONTACT FORM: Missing defined email template: ' . $componentProperties['notification_template'] . '. ' . $template . ' template will be used!');
}
if ( !empty($componentProperties[ ('notification_template_'.App::getLocale())]) and !empty( MailTemplate::listAllTemplates()[ $componentProperties[ ('notification_template_'.App::getLocale())] ] ) ) {
$template = $componentProperties[('notification_template_'.App::getLocale())];
} elseif ( !empty($componentProperties[ ('notification_template_'.App::getLocale())]) and empty( MailTemplate::listAllTemplates()[ $componentProperties[ ('notification_template_'.App::getLocale())] ] ) ) {
Log::error('SMALL CONTACT FORM: Missing defined email template: ' . $componentProperties[ ('notification_template_'.App::getLocale())] . '. ' . $template . ' template will be used!');
}
if(!empty($messageObject->uploads))
{
$uploads = $messageObject->uploads;
}
else
{
$uploads = [];
}
Mail::{$method}($template, ['messageObject' => $messageObject, 'uploads' => $uploads, 'fields' => $output, 'fieldsDetails' => $outputFull, 'url' => url()->full()], function($message) use($sendToAddressesValidated, $replyToAddress, $replyToName, $componentProperties, $output){
if( count($sendToAddressesValidated)>1 ) {
foreach($sendToAddressesValidated as $address) {
$message->bcc($address);
}
} elseif( !empty($sendToAddressesValidated[0]) ) {
$message->to($sendToAddressesValidated[0]);
} else {
return;
}
if (!empty($componentProperties['notification_subject'])) {
$message->subject(Twig::parse($componentProperties['notification_subject'], ['fields' => $output]));
\App::forgetInstance('parse.twig');
\App::forgetInstance('twig.environment');
}
/**
* Set Reply to address and also set From address if requested
*/
if ( !empty($replyToAddress) ) {
$validator = Validator::make(['email' => $replyToAddress], ['email' => 'required|email']);
if($validator->fails()){
Log::error('SMALL CONTACT FORM ERROR: Notification replyTo address is not valid (' .$replyToAddress. ')! Reply to address will not be used.');
return;
}
$message->replyTo($replyToAddress, $replyToName);
// Force From field if requested
if ( Settings::getTranslated('notification_address_from_form') ) {
$message->from($replyToAddress, $replyToName);
}
}
/**
* Set From address if defined in component property
*/
if ( !empty($componentProperties['notification_address_from'])) {
if (!empty($componentProperties['notification_address_from_name'])) {
$fromAddressName = $componentProperties['notification_address_from_name'];
} else {
$fromAddressName = null;
}
$message->from($componentProperties['notification_address_from'], $fromAddressName);
}
});
}
/**
* Test how many times was given IP address used this day
* @return int
*/
public function testIPAddress($ip){
$today = Carbon::today();
$count = Message::whereDate('created_at', '=', $today->toDateString())
->where('remote_ip', '=', $ip)
->count();
return $count;
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace JanVince\SmallContactForm\Models;
use Db;
use \Backend\Models\ExportModel;
use \October\Rain\Support\Collection;
use \JanVince\SmallContactForm\Models\Message;
class MessageExport extends ExportModel {
/**
* @var array Fillable fields
*/
// protected $fillable = [];
public function exportData($columns, $sessionKey = null)
{
$records = Message::all();
$records->each(function($record) use ($columns) {
$record->addVisible($columns);
});
return $records->toArray();
}
}

View File

@ -0,0 +1,438 @@
<?php
namespace JanVince\SmallContactForm\Models;
use Model;
use App;
use System\Classes\PluginManager;
class Settings extends Model
{
public $implement = [
'System.Behaviors.SettingsModel',
'@RainLab.Translate.Behaviors.TranslatableModel',
];
public $translatable = [
'form_success_msg',
'form_error_msg',
'form_send_confirm_msg',
'send_btn_text',
'form_fields',
'antispam_delay_error_msg',
'antispam_label',
'antispam_error_msg',
'add_ip_protection_error_too_many_submits',
'email_address_from_name',
'email_subject',
'email_template',
'notification_template',
'google_recaptcha_error_msg',
'ga_success_event_category',
'ga_success_event_action',
'ga_success_event_label',
];
public $requiredPermissions = ['janvince.smallcontactform.access_settings'];
public $settingsCode = 'janvince_smallcontactform_settings';
public $settingsFields = 'fields.yaml';
protected $jsonable = ['form_fields'];
/**
* Try to use Rainlab Tranlaste plugin to get translated content or falls back to default settings value
*/
public static function getTranslated($value, $defaultValue = false){
// Check for Rainlab.Translate plugin
$pluginManager = PluginManager::instance()->findByIdentifier('Rainlab.Translate');
if ($pluginManager && !$pluginManager->disabled) {
$settings = Settings::instance();
$valueTranslated = $settings->getAttributeTranslated($value);
if(!empty($valueTranslated)){
return $valueTranslated;
} else {
return $defaultValue;
}
} else {
return Settings::get($value, $defaultValue);
}
}
/**
* Try to use Rainlab Tranlaste plugin to get translated content for given key
*/
public static function getDictionaryTranslated($value){
// Check for Rainlab.Translate plugin
$translatePlugin = PluginManager::instance()->findByIdentifier('Rainlab.Translate');
if ($translatePlugin && !$translatePlugin->disabled) {
$params = [];
$message = \RainLab\Translate\Models\Message::trans($value, $params);
return $message;
} else {
return $value;
}
}
/**
* Generate form fields types list
* @return array
*/
public function getTypeOptions($value, $formData)
{
$fieldTypes = $this->getFieldTypes();
$types = [];
if(!$fieldTypes) {
return [];
}
foreach ($fieldTypes as $key => $value) {
$types[$key] = 'janvince.smallcontactform::lang.settings.form_field_types.'.$key;
}
return $types;
}
/**
* Generate form fields types list
* @return array
*/
public function getValidationTypeOptions($value, $formData)
{
return [
'required' => 'janvince.smallcontactform::lang.settings.form_field_validation.required',
'email' => 'janvince.smallcontactform::lang.settings.form_field_validation.email',
'numeric' => 'janvince.smallcontactform::lang.settings.form_field_validation.numeric',
'custom' => 'janvince.smallcontactform::lang.settings.form_field_validation.custom',
];
}
/**
* Generate list of existing fields for email name
* @return array
*/
public function getAutoreplyNameFieldOptions($value, $formData)
{
return $this->getFieldsList('text');
}
/**
* Generate list of existing fields for email name
* @return array
*/
public function getAutoreplyEmailFieldOptions($value, $formData)
{
return $this->getFieldsList('email');
}
/**
* Generate list of existing message fields
* @return array
*/
public function getAutoreplyMessageFieldOptions($value, $formData)
{
return $this->getFieldsList('textarea');
}
/**
* Generate fields list
* @return arry
*/
private function getFieldsList($type = false, $forceType = false){
$output = [];
$outputAll = [];
foreach (Settings::getTranslated('form_fields', []) as $field) {
$fieldName = $field['name'] . ' ['. $field ['type'] . ']';
$outputAll[$field['name']] = $fieldName;
if($type && !empty($field['type']) && $field['type'] <> $type) {
continue;
} else {
$output[$field['name']] = $fieldName;
}
}
if($forceType) {
return $output;
} else {
return $outputAll;
}
}
/**
* HTML field types mapping array
* @return array
*/
public static function getFieldTypes($type = false) {
$types = [
'text' => [
'html_open' => 'input',
'label' => true,
'wrapper_class' => 'form-group',
'field_class' => 'form-control',
'use_name_attribute' => true,
'attributes' => [
'type' => 'text',
],
'html_close' => null,
],
'email' => [
'html_open' => 'input',
'label' => true,
'wrapper_class' => 'form-group',
'field_class' => 'form-control',
'use_name_attribute' => true,
'attributes' => [
'type' => 'email',
],
'html_close' => null,
],
'textarea' => [
'html_open' => 'textarea',
'label' => true,
'wrapper_class' => 'form-group',
'field_class' => 'form-control',
'use_name_attribute' => true,
'attributes' => [
'rows' => 5,
],
'html_close' => 'textarea',
],
'checkbox' => [
'html_open' => 'input',
'label' => false,
'wrapper_class' => null,
'field_class' => null,
'inner_label' => true,
'use_name_attribute' => true,
'attributes' => [
'type' => 'checkbox',
],
'html_close' => null,
],
'dropdown' => [
'html_open' => 'select',
'label' => true,
'wrapper_class' => 'form-group',
'field_class' => 'form-control',
'inner_label' => false,
'use_name_attribute' => true,
'attributes' => [
],
'html_close' => 'select',
],
'file' => [
'html_open' => 'input',
'label' => true,
'wrapper_class' => 'form-group',
'field_class' => 'form-control',
'inner_label' => false,
'use_name_attribute' => true,
'attributes' => [
'type' => 'file',
],
'html_close' => null,
],
'custom_code' => [
'html_open' => "div",
'label' => true,
'wrapper_class' => null,
'field_class' => null,
'inner_label' => false,
'use_name_attribute' => false,
'attributes' => [
],
'html_close' => "div",
],
'custom_content' => [
'html_open' => "div",
'label' => true,
'wrapper_class' => null,
'field_class' => null,
'inner_label' => false,
'use_name_attribute' => false,
'attributes' => [
],
'html_close' => "div",
],
];
if($type){
if(!empty($types[$type])){
return $types[$type];
}
}
return $types;
}
/**
* Get non English locales from Translate plugin
* @return array
*/
public static function getEnabledLocales() {
// Check for Rainlab.Translate plugin
$pluginManager = PluginManager::instance()->findByIdentifier('Rainlab.Translate');
if ($pluginManager && !$pluginManager->disabled) {
$locales = \RainLab\Translate\Models\Locale::listEnabled();
return $locales;
} elseif( App::getLocale() ) {
// Backend locale
return [
'en' => 'English',
App::getLocale() => App::getLocale(),
];
}
// English fallback
return [
'en' => 'English',
];
}
/**
* Get non English locales from Translate plugin
* @return array
*/
public static function getTranslatedTemplates($defaultLocale = 'en', $locale = NULL, $templateType = NULL) {
$enabledLocales = Settings::getEnabledLocales();
/**
* Templates map
* [locale] => [templateType] => [template]
*/
$translatedTemplates = [
'en' => [
'autoreply' => [
'janvince.smallcontactform::mail.autoreply' => 'janvince.smallcontactform::lang.mail.templates.autoreply',
],
'notification' => [
'janvince.smallcontactform::mail.notification' => 'janvince.smallcontactform::lang.mail.templates.notification',
],
],
'cs' => [
'autoreply' => [
'janvince.smallcontactform::mail.autoreply_cs' => 'janvince.smallcontactform::lang.mail.templates.autoreply_cs',
],
'notification' => [
'janvince.smallcontactform::mail.notification_cs' => 'janvince.smallcontactform::lang.mail.templates.notification_cs',
],
],
'es' => [
'autoreply' => [
'janvince.smallcontactform::mail.autoreply_es' => 'janvince.smallcontactform::lang.mail.templates.autoreply_es',
],
'notification' => [
'janvince.smallcontactform::mail.notification_es' => 'janvince.smallcontactform::lang.mail.templates.notification_es',
],
],
];
if( $locale and $templateType ) {
if( !empty($translatedTemplates[$locale]) and !empty($translatedTemplates[$locale][$templateType]) ) {
return key($translatedTemplates[$locale][$templateType]);
} elseif ( $defaultLocale and !empty($translatedTemplates[$defaultLocale]) and !empty($translatedTemplates[$defaultLocale][$templateType]) ) {
return key($translatedTemplates[$defaultLocale][$templateType]);
} else {
return NULL;
}
} else {
$allEnabledTemplates = [];
foreach( $enabledLocales as $enabledLocaleKey => $enabledLocaleName ) {
if( !empty($translatedTemplates[$enabledLocaleKey]) ) {
foreach( $translatedTemplates[$enabledLocaleKey] as $type ) {
foreach( $type as $key => $value ) {
$allEnabledTemplates[$key] = e(trans($value));
}
}
}
}
return $allEnabledTemplates;
}
return [];
}
}

View File

@ -0,0 +1,67 @@
# ===================================
# Column Definitions
# ===================================
columns:
# id:
# label: janvince.smallgallery::lang.categories.columns.id
# searchable: true
new_message:
label: janvince.smallcontactform::lang.models.message.columns.new_message
type: switch_icon_star
created_at:
label: janvince.smallcontactform::lang.models.message.columns.datetime
type: datetime
searchable: true
name:
label: janvince.smallcontactform::lang.models.message.columns.name
searchable: true
email:
label: janvince.smallcontactform::lang.models.message.columns.email
searchable: true
message:
label: janvince.smallcontactform::lang.models.message.columns.message
searchable: true
type: text_preview
width: 50%
remote_ip:
label: janvince.smallcontactform::lang.models.message.columns.remote_ip
searchable: true
type: text
invisible: true
form_alias:
label: janvince.smallcontactform::lang.models.message.columns.form_alias
searchable: true
type: text
invisible: true
form_description:
label: janvince.smallcontactform::lang.models.message.columns.form_description
searchable: true
type: text_preview
invisible: true
form_data:
label: janvince.smallcontactform::lang.models.message.columns.form_data
searchable: true
type: text
invisible: true
url:
label: janvince.smallcontactform::lang.models.message.columns.url
searchable: true
type: text
invisible: true
uploads:
label: janvince.smallcontactform::lang.models.message.columns.files
type: scf_files_link
invisible: true

View File

@ -0,0 +1,49 @@
# ===================================
# Column Definitions
# ===================================
columns:
id:
label: janvince.smallcontactform::lang.models.message.columns.id
type: number
new_message:
label: janvince.smallcontactform::lang.models.message.columns.new
type: number
created_at:
label: janvince.smallcontactform::lang.models.message.columns.created_at
type: datetime
updated_at:
label: janvince.smallcontactform::lang.models.message.columns.updated_at
type: datetime
name:
label: janvince.smallcontactform::lang.models.message.columns.name
type: text
email:
label: janvince.smallcontactform::lang.models.message.columns.email
type: text
message:
label: janvince.smallcontactform::lang.models.message.columns.message
type: text
remote_ip:
label: janvince.smallcontactform::lang.models.message.columns.remote_ip
type: text
form_alias:
label: janvince.smallcontactform::lang.models.message.columns.form_alias
type: text
form_description:
label: janvince.smallcontactform::lang.models.message.columns.form_description
type: text
form_description:
label: janvince.smallcontactform::lang.models.message.columns.url
type: text

View File

@ -0,0 +1,9 @@
<div class="callout fade in callout-info no-subheader">
<div class="header">
<i class="icon-warning"></i>
<h3><?= e(trans('janvince.smallcontactform::lang.settings.mapping.hint.title')); ?></h3>
</div>
<div class="content">
<p><?= trans('janvince.smallcontactform::lang.settings.mapping.hint.content'); ?></p>
</div>
</div>

View File

@ -0,0 +1,9 @@
<div class="callout fade in callout-warning no-subheader">
<div class="header">
<i class="icon-warning"></i>
<h3><?= e(trans('janvince.smallcontactform::lang.settings.mapping.warning.title')); ?></h3>
</div>
<div class="content">
<p><?= trans('janvince.smallcontactform::lang.settings.mapping.warning.content'); ?></p>
</div>
</div>

View File

@ -0,0 +1,870 @@
fields: { }
tabs:
fields:
## FORM
form_css_class:
label: 'janvince.smallcontactform::lang.settings.form.css_class'
span: left
type: text
tab: 'janvince.smallcontactform::lang.settings.tabs.form'
form_success_msg:
label: 'janvince.smallcontactform::lang.settings.form.success_msg'
placeholder: 'janvince.smallcontactform::lang.settings.form.success_msg_placeholder'
span: left
type: text
tab: 'janvince.smallcontactform::lang.settings.tabs.form'
form_error_msg:
label: 'janvince.smallcontactform::lang.settings.form.error_msg'
placeholder: 'janvince.smallcontactform::lang.settings.form.error_msg_placeholder'
span: right
type: text
tab: 'janvince.smallcontactform::lang.settings.tabs.form'
form_hide_after_success:
label: 'janvince.smallcontactform::lang.settings.form.hide_after_success'
comment: 'janvince.smallcontactform::lang.settings.form.hide_after_success_comment'
span: left
type: checkbox
tab: 'janvince.smallcontactform::lang.settings.tabs.form'
form_use_placeholders:
label: 'janvince.smallcontactform::lang.settings.form.use_placeholders'
comment: 'janvince.smallcontactform::lang.settings.form.use_placeholders_comment'
span: left
type: checkbox
tab: 'janvince.smallcontactform::lang.settings.tabs.form'
form_disable_browser_validation:
label: 'janvince.smallcontactform::lang.settings.form.disable_browser_validation'
comment: 'janvince.smallcontactform::lang.settings.form.disable_browser_validation_comment'
span: left
type: checkbox
tab: 'janvince.smallcontactform::lang.settings.tabs.form'
section_ajax:
type: section
tab: 'janvince.smallcontactform::lang.settings.tabs.form'
form_allow_ajax:
label: 'janvince.smallcontactform::lang.settings.form.allow_ajax'
comment: 'janvince.smallcontactform::lang.settings.form.allow_ajax_comment'
span: full
type: checkbox
tab: 'janvince.smallcontactform::lang.settings.tabs.form'
form_allow_confirm_msg:
label: 'janvince.smallcontactform::lang.settings.form.allow_confirm_msg'
comment: 'janvince.smallcontactform::lang.settings.form.allow_confirm_msg_comment'
span: left
type: checkbox
tab: 'janvince.smallcontactform::lang.settings.tabs.form'
cssClass: field-indent
trigger:
action: show
field: form_allow_ajax
condition: checked
form_send_confirm_msg:
label: 'janvince.smallcontactform::lang.settings.form.send_confirm_msg'
placeholder: 'janvince.smallcontactform::lang.settings.form.send_confirm_msg_placeholder'
span: right
type: text
tab: 'janvince.smallcontactform::lang.settings.tabs.form'
cssClass: field-indent
trigger:
action: show
field: form_allow_ajax
condition: checked
section_assets:
type: section
tab: 'janvince.smallcontactform::lang.settings.tabs.form'
trigger:
action: show
field: add_assets
condition: checked
add_assets:
label: 'janvince.smallcontactform::lang.settings.form.add_assets'
comment: 'janvince.smallcontactform::lang.settings.form.add_assets_comment'
span: full
type: checkbox
tab: 'janvince.smallcontactform::lang.settings.tabs.form'
add_css_assets:
label: 'janvince.smallcontactform::lang.settings.form.add_css_assets'
comment: 'janvince.smallcontactform::lang.settings.form.add_css_assets_comment'
span: full
type: checkbox
tab: 'janvince.smallcontactform::lang.settings.tabs.form'
cssClass: field-indent
trigger:
action: show
field: add_assets
condition: checked
add_js_assets:
label: 'janvince.smallcontactform::lang.settings.form.add_js_assets'
comment: 'janvince.smallcontactform::lang.settings.form.add_js_assets_comment'
span: full
type: checkbox
tab: 'janvince.smallcontactform::lang.settings.tabs.form'
cssClass: field-indent
trigger:
action: show
field: add_assets
condition: checked
## BUTTONS
send_btn_wrapper_css:
span: left
label: 'janvince.smallcontactform::lang.settings.buttons.send_btn_wrapper_css'
placeholder: 'janvince.smallcontactform::lang.settings.buttons.send_btn_wrapper_css_placeholder'
type: text
tab: 'janvince.smallcontactform::lang.settings.tabs.buttons'
send_btn_css_class:
label: 'janvince.smallcontactform::lang.settings.buttons.send_btn_css_class'
placeholder: 'janvince.smallcontactform::lang.settings.buttons.send_btn_css_class_placeholder'
span: left
type: text
tab: 'janvince.smallcontactform::lang.settings.tabs.buttons'
send_btn_text:
label: 'janvince.smallcontactform::lang.settings.buttons.send_btn_text'
placeholder: 'janvince.smallcontactform::lang.settings.buttons.send_btn_text_placeholder'
span: left
type: text
tab: 'janvince.smallcontactform::lang.settings.tabs.buttons'
section_redirect:
type: section
tab: 'janvince.smallcontactform::lang.settings.tabs.buttons'
allow_redirect:
label: 'janvince.smallcontactform::lang.settings.redirect.allow_redirect'
comment: 'janvince.smallcontactform::lang.settings.redirect.allow_redirect_comment'
span: full
type: checkbox
tab: 'janvince.smallcontactform::lang.settings.tabs.buttons'
redirect_url:
label: 'janvince.smallcontactform::lang.settings.redirect.redirect_url'
comment: 'janvince.smallcontactform::lang.settings.redirect.redirect_url_comment'
placeholder: 'janvince.smallcontactform::lang.settings.redirect.redirect_url_placeholder'
span: full
type: text
tab: 'janvince.smallcontactform::lang.settings.tabs.buttons'
cssClass: field-indent
trigger:
action: show
field: allow_redirect
condition: checked
redirect_url_external:
label: 'janvince.smallcontactform::lang.settings.redirect.redirect_url_external'
comment: 'janvince.smallcontactform::lang.settings.redirect.redirect_url_external_comment'
span: full
type: checkbox
tab: 'janvince.smallcontactform::lang.settings.tabs.buttons'
cssClass: field-indent
trigger:
action: show
field: allow_redirect
condition: checked
## FORM FIELDS
form_fields:
prompt: 'janvince.smallcontactform::lang.settings.form_fields.prompt'
span: full
type: repeater
style: collapsed
tab: 'janvince.smallcontactform::lang.settings.tabs.form_fields'
form:
fields:
name:
span: full
label: 'janvince.smallcontactform::lang.settings.form_fields.name'
comment: 'janvince.smallcontactform::lang.settings.form_fields.name_comment'
type: text
required: true
# cssClass: 'bg-info'
type:
span: left
label: 'janvince.smallcontactform::lang.settings.form_fields.type'
type: dropdown
required: true
cssClass: field-indent
label:
span: right
label: 'janvince.smallcontactform::lang.settings.form_fields.label'
placeholder: 'janvince.smallcontactform::lang.settings.form_fields.label_placeholder'
type: text
cssClass: field-indent
field_values:
# label: 'janvince.smallcontactform::lang.settings.form_fields.validation'
prompt: 'janvince.smallcontactform::lang.settings.form_fields.add_values_prompt'
span: full
type: repeater
style: collapsed
cssClass: field-indent
trigger:
action: show
field: type
condition: value[dropdown]
form:
fields:
field_value_id:
span: left
label: 'janvince.smallcontactform::lang.settings.form_fields.field_value_id'
type: text
field_value_content:
span: right
label: 'janvince.smallcontactform::lang.settings.form_fields.field_value_content'
type: text
field_custom_code:
span: full
label: 'janvince.smallcontactform::lang.settings.form_fields.custom_code'
comment: 'janvince.smallcontactform::lang.settings.form_fields.custom_code_comment'
type: codeeditor
size: large
cssClass: field-indent
trigger:
action: show
field: type
condition: value[custom_code]
field_custom_code_twig:
span: left
label: 'janvince.smallcontactform::lang.settings.form_fields.custom_code_twig'
comment: 'janvince.smallcontactform::lang.settings.form_fields.custom_code_twig_comment'
type: checkbox
cssClass: field-indent
trigger:
action: show
field: type
condition: value[custom_code]
field_custom_code_line:
span: full
type: section
cssClass: field-indent
trigger:
action: show
field: type
condition: value[custom_code]
field_custom_content:
span: full
label: 'janvince.smallcontactform::lang.settings.form_fields.custom_content'
comment: 'janvince.smallcontactform::lang.settings.form_fields.custom_content_comment'
type: richeditor
size: large
cssClass: field-indent
trigger:
action: show
field: type
condition: value[custom_content]
field_custom_content_section:
span: full
type: section
cssClass: field-indent
trigger:
action: show
field: type
condition: value[custom_content]
field_styling:
label: 'janvince.smallcontactform::lang.settings.form_fields.field_styling'
comment: 'janvince.smallcontactform::lang.settings.form_fields.field_styling_comment'
span: left
type: checkbox
tab: 'janvince.smallcontactform::lang.settings.tabs.form_fields'
cssClass: field-indent
autofocus:
label: 'janvince.smallcontactform::lang.settings.form_fields.autofocus'
comment: 'janvince.smallcontactform::lang.settings.form_fields.autofocus_comment'
span: right
type: checkbox
tab: 'janvince.smallcontactform::lang.settings.tabs.form_fields'
cssClass: field-indent
wrapper_css:
span: left
label: 'janvince.smallcontactform::lang.settings.form_fields.wrapper_css'
placeholder: 'janvince.smallcontactform::lang.settings.form_fields.wrapper_css_placeholder'
type: text
cssClass: field-indent
trigger:
action: show
field: field_styling
condition: checked
label_css:
span: left
label: 'janvince.smallcontactform::lang.settings.form_fields.label_css'
placeholder: 'janvince.smallcontactform::lang.settings.form_fields.label_css_placeholder'
type: text
cssClass: field-indent
trigger:
action: show
field: field_styling
condition: checked
field_css:
span: right
label: 'janvince.smallcontactform::lang.settings.form_fields.field_css'
placeholder: 'janvince.smallcontactform::lang.settings.form_fields.field_css_placeholder'
type: text
trigger:
action: show
field: field_styling
condition: checked
section_validation:
type: section
cssClass: field-indent
trigger:
action: show
field: field_styling
condition: checked
field_validation:
label: 'janvince.smallcontactform::lang.settings.form_fields.field_validation'
comment: 'janvince.smallcontactform::lang.settings.form_fields.field_validation_comment'
span: full
type: checkbox
cssClass: field-indent
validation:
# label: 'janvince.smallcontactform::lang.settings.form_fields.validation'
prompt: 'janvince.smallcontactform::lang.settings.form_fields.validation_prompt'
span: full
type: repeater
style: collapsed
cssClass: field-indent
trigger:
action: show
field: field_validation
condition: checked
form:
fields:
validation_type:
span: left
label: 'janvince.smallcontactform::lang.settings.form_fields.validation'
type: dropdown
emptyOption: 'janvince.smallcontactform::lang.settings.form_field_validation.select'
validation_error:
span: right
label: 'janvince.smallcontactform::lang.settings.form_fields.validation_error'
placeholder: 'janvince.smallcontactform::lang.settings.form_fields.validation_error_placeholder'
type: text
validation_custom_type:
span: left
label: 'janvince.smallcontactform::lang.settings.form_fields.validation_custom_type'
placeholder: 'janvince.smallcontactform::lang.settings.form_fields.validation_custom_type_placeholder'
comment: 'janvince.smallcontactform::lang.settings.form_fields.validation_custom_type_comment'
commentHtml: true
type: text
trigger:
action: show
field: validation_type
condition: value[custom]
validation_custom_pattern:
span: right
label: 'janvince.smallcontactform::lang.settings.form_fields.validation_custom_pattern'
placeholder: 'janvince.smallcontactform::lang.settings.form_fields.validation_custom_pattern_placeholder'
comment: 'janvince.smallcontactform::lang.settings.form_fields.validation_custom_pattern_comment'
type: text
trigger:
action: show
field: validation_type
condition: value[custom]
## MAPPING
autoreply_email_field:
label: 'janvince.smallcontactform::lang.settings.email.autoreply_email_field'
commentHtml: true
comment: 'janvince.smallcontactform::lang.settings.email.autoreply_email_field_comment'
span: left
type: dropdown
tab: 'janvince.smallcontactform::lang.settings.tabs.mapping'
emptyOption: 'janvince.smallcontactform::lang.settings.email.autoreply_email_field_empty_option'
partial_mapping:
type: partial
path: $/janvince/smallcontactform/models/settings/_mapping_help.htm
span: right
tab: 'janvince.smallcontactform::lang.settings.tabs.mapping'
autoreply_name_field:
label: 'janvince.smallcontactform::lang.settings.email.autoreply_name_field'
commentHtml: true
comment: 'janvince.smallcontactform::lang.settings.email.autoreply_name_field_comment'
span: left
type: dropdown
tab: 'janvince.smallcontactform::lang.settings.tabs.mapping'
emptyOption: 'janvince.smallcontactform::lang.settings.email.autoreply_name_field_empty_option'
autoreply_message_field:
label: 'janvince.smallcontactform::lang.settings.email.autoreply_message_field'
commentHtml: true
comment: 'janvince.smallcontactform::lang.settings.email.autoreply_message_field_comment'
span: left
type: dropdown
tab: 'janvince.smallcontactform::lang.settings.tabs.mapping'
emptyOption: 'janvince.smallcontactform::lang.settings.email.autoreply_message_field_empty_option'
partial_mapping_warning:
type: partial
path: $/janvince/smallcontactform/models/settings/_mapping_warning.htm
span: right
tab: 'janvince.smallcontactform::lang.settings.tabs.mapping'
## ANTISPAM
add_google_recaptcha:
label: 'janvince.smallcontactform::lang.settings.antispam.add_google_recaptcha'
comment: 'janvince.smallcontactform::lang.settings.antispam.add_google_recaptcha_comment'
commentHtml: true
span: full
type: checkbox
tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
google_recaptcha_version:
label: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_version'
comment: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_version_comment'
commentHtml: true
span: left
type: dropdown
tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
cssClass: field-indent
options:
v2checkbox: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_versions.v2checkbox'
v2invisible: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_versions.v2invisible'
trigger:
action: show
field: add_google_recaptcha
condition: checked
google_recaptcha_site_key:
label: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_site_key'
comment: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_site_key_comment'
commentHtml: true
span: left
type: text
tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
cssClass: field-indent
trigger:
action: show
field: add_google_recaptcha
condition: checked
google_recaptcha_secret_key:
label: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_secret_key'
comment: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_secret_key_comment'
span: right
type: text
tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
cssClass: field-indent
trigger:
action: show
field: add_google_recaptcha
condition: checked
google_recaptcha_error_msg:
label: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_error_msg'
comment: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_error_msg_comment'
placeholder: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_error_msg_placeholder'
span: left
type: text
tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
cssClass: field-indent
trigger:
action: show
field: add_google_recaptcha
condition: checked
google_recaptcha_wrapper_css:
label: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_wrapper_css'
comment: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_wrapper_css_comment'
placeholder: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_wrapper_css_placeholder'
span: left
type: text
tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
cssClass: field-indent
trigger:
action: show
field: add_google_recaptcha
condition: checked
google_recaptcha_scripts_allow:
label: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_scripts_allow'
comment: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_scripts_allow_comment'
span: left
type: checkbox
default: false
tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
cssClass: field-indent
trigger:
action: show
field: add_google_recaptcha
condition: checked
google_recaptcha_locale_allow:
label: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_locale_allow'
comment: 'janvince.smallcontactform::lang.settings.antispam.google_recaptcha_locale_allow_comment'
span: left
type: checkbox
default: false
tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
cssClass: field-indent
trigger:
action: show
field: add_google_recaptcha
condition: checked
section_google_recaptcha:
type: section
tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
trigger:
action: show
field: add_google_recaptcha
condition: checked
section_disabled_extensions:
type: section
label: 'janvince.smallcontactform::lang.settings.antispam.disabled_extensions'
comment: 'janvince.smallcontactform::lang.settings.antispam.disabled_extensions_comment'
tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
trigger:
action: show
field: privacy_disable_messages_saving
condition: checked
add_antispam:
label: 'janvince.smallcontactform::lang.settings.antispam.add_antispam'
comment: 'janvince.smallcontactform::lang.settings.antispam.add_antispam_comment'
span: full
type: checkbox
tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
antispam_delay:
label: 'janvince.smallcontactform::lang.settings.antispam.antispam_delay'
comment: 'janvince.smallcontactform::lang.settings.antispam.antispam_delay_comment'
placeholder: 'janvince.smallcontactform::lang.settings.antispam.antispam_delay_placeholder'
span: left
type: number
tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
cssClass: field-indent
trigger:
action: show
field: add_antispam
condition: checked
antispam_delay_error_msg:
label: 'janvince.smallcontactform::lang.settings.antispam.antispam_delay_error_msg'
comment: 'janvince.smallcontactform::lang.settings.antispam.antispam_delay_error_msg_comment'
placeholder: 'janvince.smallcontactform::lang.settings.antispam.antispam_delay_error_msg_placeholder'
span: right
type: text
tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
cssClass: field-indent
trigger:
action: show
field: add_antispam
condition: checked
antispam_label:
label: 'janvince.smallcontactform::lang.settings.antispam.antispam_label'
comment: 'janvince.smallcontactform::lang.settings.antispam.antispam_label_comment'
placeholder: 'janvince.smallcontactform::lang.settings.antispam.antispam_label_placeholder'
span: left
type: text
tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
cssClass: field-indent
trigger:
action: show
field: add_antispam
condition: checked
antispam_error_msg:
label: 'janvince.smallcontactform::lang.settings.antispam.antispam_error_msg'
comment: 'janvince.smallcontactform::lang.settings.antispam.antispam_error_msg_comment'
placeholder: 'janvince.smallcontactform::lang.settings.antispam.antispam_error_msg_placeholder'
span: right
type: text
tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
cssClass: field-indent
trigger:
action: show
field: add_antispam
condition: checked
section_ip_protection:
type: section
tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
trigger:
action: show
field: add_antispam
condition: checked
add_ip_protection:
label: 'janvince.smallcontactform::lang.settings.antispam.add_ip_protection'
comment: 'janvince.smallcontactform::lang.settings.antispam.add_ip_protection_comment'
span: full
type: checkbox
tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
trigger:
action: disable
field: privacy_disable_messages_saving
condition: checked
add_ip_protection_count:
label: 'janvince.smallcontactform::lang.settings.antispam.add_ip_protection_count'
comment: 'janvince.smallcontactform::lang.settings.antispam.add_ip_protection_count_comment'
placeholder: 'janvince.smallcontactform::lang.settings.antispam.add_ip_protection_count_placeholder'
span: left
type: number
tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
cssClass: field-indent
trigger:
action: show
field: add_ip_protection
condition: checked
add_ip_protection_error_too_many_submits:
label: 'janvince.smallcontactform::lang.settings.antispam.add_ip_protection_error_too_many_submits'
comment: 'janvince.smallcontactform::lang.settings.antispam.add_ip_protection_error_too_many_submits_comment'
placeholder: 'janvince.smallcontactform::lang.settings.antispam.add_ip_protection_error_too_many_submits_placeholder'
span: right
type: text
tab: 'janvince.smallcontactform::lang.settings.tabs.antispam'
cssClass: field-indent
trigger:
action: show
field: add_ip_protection
condition: checked
## EMAILS
allow_email_queue:
label: 'janvince.smallcontactform::lang.settings.email.allow_email_queue'
comment: 'janvince.smallcontactform::lang.settings.email.allow_email_queue_comment'
span: full
type: checkbox
tab: 'janvince.smallcontactform::lang.settings.tabs.email'
section_autoreply:
type: section
tab: 'janvince.smallcontactform::lang.settings.tabs.email'
trigger:
action: show
field: allow_autoreply
condition: checked
allow_autoreply:
label: 'janvince.smallcontactform::lang.settings.email.allow_autoreply'
comment: 'janvince.smallcontactform::lang.settings.email.allow_autoreply_comment'
span: left
type: checkbox
tab: 'janvince.smallcontactform::lang.settings.tabs.email'
email_address_from:
label: 'janvince.smallcontactform::lang.settings.email.address_from'
span: left
type: text
placeholder: 'janvince.smallcontactform::lang.settings.email.address_from_placeholder'
tab: 'janvince.smallcontactform::lang.settings.tabs.email'
cssClass: field-indent
trigger:
action: show
field: allow_autoreply
condition: checked
email_address_from_name:
label: 'janvince.smallcontactform::lang.settings.email.address_from_name'
span: right
type: text
placeholder: 'janvince.smallcontactform::lang.settings.email.address_from_name_placeholder'
tab: 'janvince.smallcontactform::lang.settings.tabs.email'
cssClass: field-indent
trigger:
action: show
field: allow_autoreply
condition: checked
email_address_replyto:
label: 'janvince.smallcontactform::lang.settings.email.address_replyto'
span: left
type: text
placeholder: 'janvince.smallcontactform::lang.settings.email.address_from_placeholder'
tab: 'janvince.smallcontactform::lang.settings.tabs.email'
cssClass: field-indent
trigger:
action: show
field: allow_autoreply
condition: checked
email_subject:
label: 'janvince.smallcontactform::lang.settings.email.subject'
comment: 'janvince.smallcontactform::lang.settings.email.subject_comment'
span: full
type: text
tab: 'janvince.smallcontactform::lang.settings.tabs.email'
cssClass: field-indent
trigger:
action: show
field: allow_autoreply
condition: checked
email_template:
label: 'janvince.smallcontactform::lang.settings.email.template'
comment: 'janvince.smallcontactform::lang.settings.email.template_comment'
span: left
type: text
tab: 'janvince.smallcontactform::lang.settings.tabs.email'
cssClass: field-indent
trigger:
action: show
field: allow_autoreply
condition: checked
section_notify:
type: section
tab: 'janvince.smallcontactform::lang.settings.tabs.email'
trigger:
action: show
field: allow_notifications
condition: checked
allow_notifications:
label: 'janvince.smallcontactform::lang.settings.email.allow_notifications'
comment: 'janvince.smallcontactform::lang.settings.email.allow_notifications_comment'
span: left
type: checkbox
tab: 'janvince.smallcontactform::lang.settings.tabs.email'
notification_address_from_form:
label: 'janvince.smallcontactform::lang.settings.email.notification_address_from_form'
comment: 'janvince.smallcontactform::lang.settings.email.notification_address_from_form_comment'
span: full
type: checkbox
tab: 'janvince.smallcontactform::lang.settings.tabs.email'
cssClass: field-indent
trigger:
action: show
field: allow_notifications
condition: checked
notification_address_to:
label: 'janvince.smallcontactform::lang.settings.email.notification_address_to'
comment: 'janvince.smallcontactform::lang.settings.email.notification_address_to_comment'
span: left
type: text
placeholder: 'janvince.smallcontactform::lang.settings.email.notification_address_to_placeholder'
tab: 'janvince.smallcontactform::lang.settings.tabs.email'
cssClass: field-indent
trigger:
action: show
field: allow_notifications
condition: checked
notification_template:
label: 'janvince.smallcontactform::lang.settings.email.notification_template'
comment: 'janvince.smallcontactform::lang.settings.email.notification_template_comment'
span: left
type: text
tab: 'janvince.smallcontactform::lang.settings.tabs.email'
cssClass: field-indent
trigger:
action: show
field: allow_notifications
condition: checked
## Google Analytics
section_ga_events:
type: section
label: 'janvince.smallcontactform::lang.settings.sections.ga_events'
tab: 'janvince.smallcontactform::lang.settings.tabs.ga'
ga_success_event_allow:
label: 'janvince.smallcontactform::lang.settings.ga.ga_success_event_allow'
span: left
type: checkbox
tab: 'janvince.smallcontactform::lang.settings.tabs.ga'
ga_success_event_category:
label: 'janvince.smallcontactform::lang.settings.form_fields.event_category'
type: text
tab: 'janvince.smallcontactform::lang.settings.tabs.ga'
cssClass: field-indent
trigger:
action: show
field: ga_success_event_allow
condition: checked
ga_success_event_action:
label: 'janvince.smallcontactform::lang.settings.form_fields.event_action'
type: text
tab: 'janvince.smallcontactform::lang.settings.tabs.ga'
cssClass: field-indent
trigger:
action: show
field: ga_success_event_allow
condition: checked
ga_success_event_label:
label: 'janvince.smallcontactform::lang.settings.form_fields.event_label'
type: text
tab: 'janvince.smallcontactform::lang.settings.tabs.ga'
cssClass: field-indent
trigger:
action: show
field: ga_success_event_allow
condition: checked
## PRIVACY
privacy_disable_messages_saving:
label: 'janvince.smallcontactform::lang.settings.privacy.disable_messages_saving'
comment: 'janvince.smallcontactform::lang.settings.privacy.disable_messages_saving_comment'
commentHtml: true
span: full
type: checkbox
tab: 'janvince.smallcontactform::lang.settings.tabs.privacy'
privacy_disable_messages_saving_section:
comment: 'janvince.smallcontactform::lang.settings.privacy.disable_messages_saving_comment_section'
commentHtml: true
span: full
type: section
cssClass: field-indent
tab: 'janvince.smallcontactform::lang.settings.tabs.privacy'
trigger:
action: show
field: privacy_disable_messages_saving
condition: checked

View File

@ -0,0 +1,27 @@
<?php
namespace JanVince\SmallContactForm\ReportWidgets;
use Backend\Classes\ReportWidgetBase;
use JanVince\SmallContactForm\Controllers\Messages as MessagesController;
/**
* Contact form sent messages report widget
*/
class Messages extends ReportWidgetBase
{
public function render()
{
return $this->makePartial('messages');
}
public function getRecordsStats($value){
$controller = new MessagesController;
return $controller->getRecordsStats($value);
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace JanVince\SmallContactForm\ReportWidgets;
use Backend\Classes\ReportWidgetBase;
use JanVince\SmallContactForm\Controllers\Messages as MessagesController;
/**
* Contact form sent messages report widget
*/
class NewMessage extends ReportWidgetBase
{
public function render()
{
return $this->makePartial('newmessage');
}
public function getRecordsStats($value){
$controller = new MessagesController;
return $controller->getRecordsStats($value);
}
}

View File

@ -0,0 +1,17 @@
<div class="report-widget">
<h3><?= e(trans('janvince.smallcontactform::lang.reportwidget.partials.messages.title')) ?></h3>
<a href="<?= Backend::url('janvince/smallcontactform/messages');?>">
<div
class="control-chart"
data-control="chart-bar"
data-size="200"
data-center-text="180">
<ul>
<li data-color="orange"><?= e(trans('janvince.smallcontactform::lang.reportwidget.partials.messages.messages_all')) ?> <span><?php echo($this->getRecordsStats('all_count')); ?></span></li>
<li data-color="#95b753"><?= e(trans('janvince.smallcontactform::lang.reportwidget.partials.messages.messages_new')) ?> <span><?php echo($this->getRecordsStats('new_count')); ?></span></li>
<li data-color="#d1d1d1"><?= e(trans('janvince.smallcontactform::lang.reportwidget.partials.messages.messages_read')) ?> <span><?php echo($this->getRecordsStats('read_count')); ?></span></li>
</ul>
</div>
</a>
</div>

View File

@ -0,0 +1,13 @@
<div class="report-widget">
<h3><?= e(trans('janvince.smallcontactform::lang.reportwidget.partials.new_message.title')) ?></h3>
<a href="<?= Backend::url('janvince/smallcontactform/messages');?>">
<div class="scoreboard-item title-value">
<p <?= ($this->getRecordsStats('new_count') > 0 ? 'class="positive"' : 'style="color: #d1d1d1;"'); ?>">
<span class="oc-icon-inbox oc-icon-2x"></span><strong><?= $this->getRecordsStats('new_count'); ?></strong>
</p>
<p class="description"><?= e(trans('janvince.smallcontactform::lang.reportwidget.partials.new_message.link_text')) ?></p>
</div>
</a>
</div>

View File

@ -0,0 +1,31 @@
<?php
namespace JanVince\SmallContactForm\Updates;
use Schema;
use October\Rain\Database\Updates\Migration;
class SmallContactFormTables_01 extends Migration
{
public function up()
{
Schema::create('janvince_smallcontactform_messages', function($table)
{
$table->engine = 'InnoDB';
$table->increments('id');
$table->text('name')->nullable();
$table->text('email')->nullable();
$table->text('message')->nullable();
$table->text('form_data')->nullable();
$table->boolean('new_message')->default(1);
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('janvince_smallcontactform_messages');
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace JanVince\SmallContactForm\Updates;
use Schema;
use October\Rain\Database\Updates\Migration;
class SmallContactFormTables_02 extends Migration
{
public function up()
{
Schema::table('janvince_smallcontactform_messages', function($table)
{
$table->string('remote_ip')->nullable();
$table->index('remote_ip');
});
}
public function down()
{
if (Schema::hasColumn('janvince_smallcontactform_messages', 'remote_ip')) {
Schema::table('janvince_smallcontactform_messages', function($table)
{
$table->dropColumn('remote_ip');
});
}
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace JanVince\SmallContactForm\Updates;
use Schema;
use October\Rain\Database\Updates\Migration;
class SmallContactFormTables_03 extends Migration
{
public function up()
{
Schema::table('janvince_smallcontactform_messages', function($table)
{
$table->text('form_description')->nullable();
$table->string('form_alias')->nullable();
});
}
public function down()
{
if (Schema::hasColumn('janvince_smallcontactform_messages', 'form_description')) {
Schema::table('janvince_smallcontactform_messages', function($table)
{
$table->dropColumn('form_description');
});
}
if (Schema::hasColumn('janvince_smallcontactform_messages', 'form_alias')) {
Schema::table('janvince_smallcontactform_messages', function($table)
{
$table->dropColumn('form_alias');
});
}
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace JanVince\SmallContactForm\Updates;
use Schema;
use October\Rain\Database\Updates\Migration;
class SmallContactFormTables_04 extends Migration
{
public function up()
{
Schema::table('janvince_smallcontactform_messages', function($table)
{
$table->text('url')->nullable();
});
}
public function down()
{
if (Schema::hasColumn('janvince_smallcontactform_messages', 'form_description')) {
Schema::table('janvince_smallcontactform_messages', function($table)
{
$table->dropColumn('url');
});
}
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace JanVince\SmallContactForm\Updates;
use Schema;
use October\Rain\Database\Updates\Migration;
class SmallContactFormTables_05 extends Migration
{
public function up()
{
if (Schema::hasColumn('janvince_smallcontactform_messages', 'form_description'))
{
Schema::table('janvince_smallcontactform_messages', function($table)
{
$table->text('url', 2000)->nullable()->change();
});
}
}
public function down()
{
if (Schema::hasColumn('janvince_smallcontactform_messages', 'form_description'))
{
Schema::table('janvince_smallcontactform_messages', function($table)
{
$table->text('url')->nullable()->change();
});
}
}
}

View File

@ -0,0 +1,244 @@
1.0.0:
- "First version of Small Contact Form plugin"
- scf_tables.php
1.0.1:
- Fix form hiding after successful send
- Fix in README.md
1.0.2:
- Fix some typos and add LICENCE file (thanks Szabó Gergő)
1.1.0:
- Added function to delete records in Messages list
- Added permission to delete records
1.2.0:
- Added dashboard report widgets (Stats and New messages)
1.2.1:
- Mail templates now render values with {{ values|raw }}
1.2.2:
- Mail templates convert new lines to <br> with {{ values|raw|nl2br }}
1.2.3:
- Fields mapping moved to separate tab *Columns mapping*
1.2.4:
- Updated README.md with assets usage example
1.2.5:
- Added IP protection function (limit too many submits from one IP address)
- And Messages list column to show senders IP address (invisible by default)
- scf_tables_02.php
1.2.6:
- Fixed IP protection error message
1.2.7:
- Changed remote_ip column type to string
1.2.8:
- Added option to use placeholders instead of labels
1.3.0:
- Added translation support for Rainlab Translate plugin
- Fixed some typos
1.3.1:
- Added default value for getTranslated() method
1.3.2:
- Added custom send button wrapper class
1.4.0:
- Added redirect option after successful submit (internal and external URL)
1.4.1:
- Minor UI fix (thanks Szabó Gergő)
1.4.2:
- Added support for default translated mail templates (Czech and English for now)
1.4.3:
- Fixed translation of mail templates description in Settings > Mail templates
1.4.4:
- Fixed array of enabledLocales
1.4.5:
- Fixed email template check
- Added default EN locale to enabled locales array
1.4.6:
- Removed field type restriction for Fields mapping
1.4.7:
- Removed hardcoded date format for created_at column in messages list, updated README and added hungarian language (thanks Szabó Gergő for all this)
1.4.8:
- Changes to allow multiple use of contact form (form and message blocks has now unique IDs)
- Added checkbox field type
- Scoreboard last message time format (thanks Szabó Gergő)
1.4.9:
- Added scoreboard button to quickly open form settings
1.4.10:
- Fixed typo in lang filename
1.4.11:
- Added "fieldsDetails" array to all email templates to have access to field labels, types and more
- Updated default autoreply mail templates to include fieldsDetail array
- Added function to detect non-defined fields in sent data
- Updated README.md file
1.5.0:
- Added some component hacking options (override autoreply and notification emails and template, disable fields)
- Fixed some typos
- Updated README.md file
1.5.1:
- Fixed flash message visibility when where are some errors
1.5.2:
- Fixed flash error for IP protection visibility
1.5.3:
- Added option for notification emails to have FROM address set from contact form email field
1.5.4:
- Added option to mark selected messages as read
1.5.5:
- Changed JSON type for repeater DB column
1.5.6:
- Removed value attribute in textarea field
1.5.7:
- Added component alias to id attributes for multi-form usage
1.5.8:
- Fixed typo in lang files
1.5.9:
- Added direct link to messages list from dashboard widget
1.6.0:
- Added Google reCAPTCHA validation
1.6.1:
- Changed All messages large indicator to New messages in scoreboard
1.6.2:
- Removed reCAPTCHA hard coded locale string (thx kuzyk). Added settings option to allow locale detection.
1.7.0:
- Added option to specify <label> custom CSS class
1.7.1:
- Fixed 'text_preview' list field type truncate function (thx kuzyk)
1.7.2:
- Changed count() to mb_strlen() function in custom list type definition
1.8.0:
- Added option to disable built-in browser form validation. Added class 'is-invalid' for fields with error (as used Bootstrap 4).
1.9.0:
- Form registered as Page snippet to be used in Rainlab.Page content (thx BtzLeon)
1.9.1:
- REPLY TO address is now used for notification email by default. You can still force FROM address to be used (but this is not supported by all email systems!).
1.9.2:
- Fix problem when ReCaptcha field was logged as an undefined field to system log (thx LukeTowers)
1.9.3:
- Fixed label 'for' attribute to point to input ID (as required by specification)
1.10.0:
- Added form component hacks group (now only for disabling notification emails, more will come)
1.11.0:
- Added form fields alias and description (can be used to distinquish between more forms or to save extra data). More info in README file.
- scf_tables_03.php
1.11.1:
- Added form description field to message preview
1.12.0:
- Added Russian translation (thank Dinver)
1.12.1:
- Chanded input custom list column type for switch to prevent interaction with toolbar JS
1.13.1:
- Added form_alias and form_description variables to email (notification and autoreply) templates
1.13.2:
- Disabled placeholder attribute for checkbox
1.14.0:
- Added option to export messages list
1.14.1:
- Added permissions to export messages list
1.15.0:
- Added Privacy tab and new option to disable sent messages saving
1.15.1:
- Fixed settings fields trigger
1.15.2:
- Fixed default values for recaptcha settings to false
1.15.3:
- Allowed combination of disabled messages saving and allowed passive antispam
1.16.0:
- Added option to have more than one notification email address
1.16.1:
- Fixed missing form data in autoreply templates. Updated default autoreply messages.
1.16.2:
- Updated hungarian translation (thx gergo85)
1.16.3:
- Fixed checkbox validation and validation state
1.17.0:
- Added Slovak translation (thx vosco88)
1.18.0:
- Added French translations (thx FelixINX)
1.19.0:
- Added custom validation fields (thanks petr-vytlacil for help)
1.20.0:
- Added dropdown field type
1.21.0:
- Form fields repeater is now translatable
1.22.0:
- Fixed multiple flash messages shown
1.23.0:
- When placeholders are used, labels are now still present and only hidden by style attribute
1.24.0:
- Added option to set custom reCaptcha wrapper CSS class
1.25.0:
- Added polish (thanks Magiczne) and spanish (thanks codibit) translations
1.30.0:
- Added invisible reCaptcha
1.30.1:
- Fixed reCaptcha scripts load
1.31.2:
- Fixed AJAX redirect when validation error (thanks zlobec)
1.31.3:
- Fixed reCaptcha checkbox version not showing up on older installations
1.31.4:
- Fixed unnecessary refresh after Ajax send (thanks cregx)
1.32.0:
- Added all settings overrides as regular component properties (can be used to override some form settings in multi-form setup)
- Updated documentation
1.32.1:
- Fixed test on empty values if some fields are disabled
1.40.0:
- Added validation rule custom_not_regex (inverse of default regex validation rule)
- Added form container with ID and action attribute with this hash URL (to automatically jump to form after nonAJAX send or refresh)
1.40.1:
- Fixed notification From name to be correctly set from component properties (thanks @pavsid)
1.41.0:
- Added component redirect properties and allow dynamic redirect URL as a component markup parameter. More info in README file.
- Removed hard coded form hash URL (#scf-[form-alias]) as this can be now easily added with redirect options.
1.41.1:
- Set redirect URL property default value to null.
1.42.0:
- Added Google Analytics events after form is successfully sent
- Do not populate redirection field code when redirection is not allowed
1.43.0: !!! Rewritten component partials. No action is needed if you use plugin as is. But if you override component partials test them before update!
1.44.0:
- Added component properties to override notification and autoreply email subject (with support for Twig variables)
1.45.0:
- Added German translation (thanks NiklasDah)
1.46.0:
- Fixed backend validation for reCaptcha invisible
1.47.0:
- Added Custom code and Custom content field types
- Updated README
1.47.1:
- Fixed typo in README (wrong component redirection parameter name)
1.47.2:
- Fixed checkbox validation (thanks Chocofede)
1.47.3:
- Removed unnecessarry name attribute from custom_content field
1.47.4:
- Fixed typo in field attributes
1.48.0:
- Added option to set ReplyTo email address for autoreply emails
1.48.1:
- Fixed autoreply email addresses checks
1.48.2:
- Fixed empty ReplyTo field error
1.48.3:
- Fixed getFieldHtmlCode method (thanks sdlb)
1.49.0:
- Fixed missing description and redirect url after AJAX calls
- Fields forms in backend are now collapsed by default for better visibility
1.50.0:
- Auto store form request URL in DB (is available also in Mail templates)
- scf_tables_04.php
1.50.1:
- Removed unnecessarry debug log
1.51.0:
- Added support for (one or many) file uploads
- Fixed AJAX validation
- Updated README
1.51.1:
- Removed uploads array from default fields sent to autoreply template
1.51.2:
- Fixed uploads field in email messages
1.51.3:
- Changed size of database column url (thanks zlobec)
- scf_tables_05.php
1.51.4:
- Fixed passive antispam delay validation
1.52.0:
- Changed reCaptcha validation to work with allow_url_fopen disabled
1.52.1:
- Fixed project git files

View File

@ -0,0 +1,7 @@
<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit27d394c7526e9a74451bd4dc314fca57::getLoader();

View File

@ -0,0 +1,445 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see http://www.php-fig.org/psr/psr-0/
* @see http://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
// PSR-4
private $prefixLengthsPsr4 = array();
private $prefixDirsPsr4 = array();
private $fallbackDirsPsr4 = array();
// PSR-0
private $prefixesPsr0 = array();
private $fallbackDirsPsr0 = array();
private $useIncludePath = false;
private $classMap = array();
private $classMapAuthoritative = false;
private $missingClasses = array();
private $apcuPrefix;
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', $this->prefixesPsr0);
}
return array();
}
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array $classMap Class to filename map
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*/
public function add($prefix, $paths, $prepend = false)
{
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
(array) $paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
(array) $paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = (array) $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
(array) $paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
(array) $paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
(array) $paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
(array) $paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 base directories
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}
/**
* Unregisters this instance as an autoloader.
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return bool|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*/
function includeFile($file)
{
include $file;
}

View File

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

View File

@ -0,0 +1,9 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
);

View File

@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
);

View File

@ -0,0 +1,11 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'ReCaptcha\\' => array($vendorDir . '/google/recaptcha/src/ReCaptcha'),
'Composer\\Installers\\' => array($vendorDir . '/composer/installers/src/Composer/Installers'),
);

View File

@ -0,0 +1,52 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit27d394c7526e9a74451bd4dc314fca57
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInit27d394c7526e9a74451bd4dc314fca57', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInit27d394c7526e9a74451bd4dc314fca57', 'loadClassLoader'));
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
if ($useStaticLoader) {
require_once __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit27d394c7526e9a74451bd4dc314fca57::getInitializer($loader));
} else {
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
$loader->set($namespace, $path);
}
$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
$loader->setPsr4($namespace, $path);
}
$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
$loader->addClassMap($classMap);
}
}
$loader->register(true);
return $loader;
}
}

View File

@ -0,0 +1,39 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit27d394c7526e9a74451bd4dc314fca57
{
public static $prefixLengthsPsr4 = array (
'R' =>
array (
'ReCaptcha\\' => 10,
),
'C' =>
array (
'Composer\\Installers\\' => 20,
),
);
public static $prefixDirsPsr4 = array (
'ReCaptcha\\' =>
array (
0 => __DIR__ . '/..' . '/google/recaptcha/src/ReCaptcha',
),
'Composer\\Installers\\' =>
array (
0 => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers',
),
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit27d394c7526e9a74451bd4dc314fca57::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit27d394c7526e9a74451bd4dc314fca57::$prefixDirsPsr4;
}, null, ClassLoader::class);
}
}

View File

@ -0,0 +1,184 @@
[
{
"name": "composer/installers",
"version": "dev-main",
"version_normalized": "dev-main",
"source": {
"type": "git",
"url": "https://github.com/composer/installers.git",
"reference": "1b94b414035400a8be5c694048a0e711a86b1080"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/installers/zipball/1b94b414035400a8be5c694048a0e711a86b1080",
"reference": "1b94b414035400a8be5c694048a0e711a86b1080",
"shasum": ""
},
"require": {
"composer-plugin-api": "^1.0 || ^2.0"
},
"replace": {
"roundcube/plugin-installer": "*",
"shama/baton": "*"
},
"require-dev": {
"composer/composer": "1.6.* || ^2.0",
"composer/semver": "^1 || ^3",
"phpstan/phpstan": "^0.12.55",
"phpstan/phpstan-phpunit": "^0.12.16",
"symfony/phpunit-bridge": "^4.2 || ^5",
"symfony/process": "^2.3"
},
"time": "2021-03-08T12:02:54+00:00",
"type": "composer-plugin",
"extra": {
"class": "Composer\\Installers\\Plugin",
"branch-alias": {
"dev-main": "1.x-dev"
}
},
"installation-source": "source",
"autoload": {
"psr-4": {
"Composer\\Installers\\": "src/Composer/Installers"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Kyle Robinson Young",
"email": "kyle@dontkry.com",
"homepage": "https://github.com/shama"
}
],
"description": "A multi-framework Composer library installer",
"homepage": "https://composer.github.io/installers/",
"keywords": [
"Craft",
"Dolibarr",
"Eliasis",
"Hurad",
"ImageCMS",
"Kanboard",
"Lan Management System",
"MODX Evo",
"MantisBT",
"Mautic",
"Maya",
"OXID",
"Plentymarkets",
"Porto",
"RadPHP",
"SMF",
"Starbug",
"Thelia",
"Whmcs",
"WolfCMS",
"agl",
"aimeos",
"annotatecms",
"attogram",
"bitrix",
"cakephp",
"chef",
"cockpit",
"codeigniter",
"concrete5",
"croogo",
"dokuwiki",
"drupal",
"eZ Platform",
"elgg",
"expressionengine",
"fuelphp",
"grav",
"installer",
"itop",
"joomla",
"known",
"kohana",
"laravel",
"lavalite",
"lithium",
"magento",
"majima",
"mako",
"mediawiki",
"modulework",
"modx",
"moodle",
"osclass",
"phpbb",
"piwik",
"ppi",
"processwire",
"puppet",
"pxcms",
"reindex",
"roundcube",
"shopware",
"silverstripe",
"sydes",
"sylius",
"symfony",
"tastyigniter",
"typo3",
"wordpress",
"yawik",
"zend",
"zikula"
]
},
{
"name": "google/recaptcha",
"version": "dev-master",
"version_normalized": "9999999-dev",
"source": {
"type": "git",
"url": "https://github.com/google/recaptcha.git",
"reference": "f911286ad361c9fba1b422c07f040852c0c193a3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/google/recaptcha/zipball/f911286ad361c9fba1b422c07f040852c0c193a3",
"reference": "f911286ad361c9fba1b422c07f040852c0c193a3",
"shasum": ""
},
"require": {
"php": ">=5.5"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.2.20|^2.15",
"php-coveralls/php-coveralls": "^2.1",
"phpunit/phpunit": "^4.8.36|^5.7.27|^6.59|^7.5.11"
},
"time": "2020-10-01T15:14:41+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.2.x-dev"
}
},
"installation-source": "source",
"autoload": {
"psr-4": {
"ReCaptcha\\": "src/ReCaptcha"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"description": "Client library for reCAPTCHA, a free service that protects websites from spam and abuse.",
"homepage": "https://www.google.com/recaptcha/",
"keywords": [
"Abuse",
"captcha",
"recaptcha",
"spam"
]
}
]

View File

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2019, Google Inc.
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.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
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 HOLDER 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.

View File

@ -0,0 +1,140 @@
# reCAPTCHA PHP client library
[![Build Status](https://travis-ci.org/google/recaptcha.svg)](https://travis-ci.org/google/recaptcha)
[![Coverage Status](https://coveralls.io/repos/github/google/recaptcha/badge.svg)](https://coveralls.io/github/google/recaptcha)
[![Latest Stable Version](https://poser.pugx.org/google/recaptcha/v/stable.svg)](https://packagist.org/packages/google/recaptcha)
[![Total Downloads](https://poser.pugx.org/google/recaptcha/downloads.svg)](https://packagist.org/packages/google/recaptcha)
reCAPTCHA is a free CAPTCHA service that protects websites from spam and abuse.
This is a PHP library that wraps up the server-side verification step required
to process responses from the reCAPTCHA service. This client supports both v2
and v3.
- reCAPTCHA: https://www.google.com/recaptcha
- This repo: https://github.com/google/recaptcha
- Hosted demo: https://recaptcha-demo.appspot.com/
- Version: 1.2.4
- License: BSD, see [LICENSE](LICENSE)
## Installation
### Composer (recommended)
Use [Composer](https://getcomposer.org) to install this library from Packagist:
[`google/recaptcha`](https://packagist.org/packages/google/recaptcha)
Run the following command from your project directory to add the dependency:
```sh
composer require google/recaptcha "^1.2"
```
Alternatively, add the dependency directly to your `composer.json` file:
```json
"require": {
"google/recaptcha": "^1.2"
}
```
### Direct download
Download the [ZIP file](https://github.com/google/recaptcha/archive/master.zip)
and extract into your project. An autoloader script is provided in
`src/autoload.php` which you can require into your script. For example:
```php
require_once '/path/to/recaptcha/src/autoload.php';
$recaptcha = new \ReCaptcha\ReCaptcha($secret);
```
The classes in the project are structured according to the
[PSR-4](http://www.php-fig.org/psr/psr-4/) standard, so you can also use your
own autoloader or require the needed files directly in your code.
## Usage
First obtain the appropriate keys for the type of reCAPTCHA you wish to
integrate for v2 at https://www.google.com/recaptcha/admin or v3 at
https://g.co/recaptcha/v3.
Then follow the [integration guide on the developer
site](https://developers.google.com/recaptcha/intro) to add the reCAPTCHA
functionality into your frontend.
This library comes in when you need to verify the user's response. On the PHP
side you need the response from the reCAPTCHA service and secret key from your
credentials. Instantiate the `ReCaptcha` class with your secret key, specify any
additional validation rules, and then call `verify()` with the reCAPTCHA
response and user's IP address. For example:
```php
<?php
$recaptcha = new \ReCaptcha\ReCaptcha($secret);
$resp = $recaptcha->setExpectedHostname('recaptcha-demo.appspot.com')
->verify($gRecaptchaResponse, $remoteIp);
if ($resp->isSuccess()) {
// Verified!
} else {
$errors = $resp->getErrorCodes();
}
```
The following methods are available:
- `setExpectedHostname($hostname)`: ensures the hostname matches. You must do
this if you have disabled "Domain/Package Name Validation" for your
credentials.
- `setExpectedApkPackageName($apkPackageName)`: if you're verifying a response
from an Android app. Again, you must do this if you have disabled
"Domain/Package Name Validation" for your credentials.
- `setExpectedAction($action)`: ensures the action matches for the v3 API.
- `setScoreThreshold($threshold)`: set a score threshold for responses from the
v3 API
- `setChallengeTimeout($timeoutSeconds)`: set a timeout between the user passing
the reCAPTCHA and your server processing it.
Each of the `set`\*`()` methods return the `ReCaptcha` instance so you can chain
them together. For example:
```php
<?php
$recaptcha = new \ReCaptcha\ReCaptcha($secret);
$resp = $recaptcha->setExpectedHostname('recaptcha-demo.appspot.com')
->setExpectedAction('homepage')
->setScoreThreshold(0.5)
->verify($gRecaptchaResponse, $remoteIp);
if ($resp->isSuccess()) {
// Verified!
} else {
$errors = $resp->getErrorCodes();
}
```
You can find the constants for the libraries error codes in the `ReCaptcha`
class constants, e.g. `ReCaptcha::E_HOSTNAME_MISMATCH`
For more details on usage and structure, see [ARCHITECTURE](ARCHITECTURE.md).
### Examples
You can see examples of each reCAPTCHA type in [examples/](examples/). You can
run the examples locally by using the Composer script:
```sh
composer run-script serve-examples
```
This makes use of the in-built PHP dev server to host the examples at
http://localhost:8080/
These are also hosted on Google AppEngine Flexible environment at
https://recaptcha-demo.appspot.com/. This is configured by
[`app.yaml`](./app.yaml) which you can also use to [deploy to your own AppEngine
project](https://cloud.google.com/appengine/docs/flexible/php/download).
## Contributing
No one ever has enough engineers, so we're very happy to accept contributions
via Pull Requests. For details, see [CONTRIBUTING](CONTRIBUTING.md)

View File

@ -0,0 +1,39 @@
{
"name": "google/recaptcha",
"description": "Client library for reCAPTCHA, a free service that protects websites from spam and abuse.",
"type": "library",
"keywords": ["recaptcha", "captcha", "spam", "abuse"],
"homepage": "https://www.google.com/recaptcha/",
"license": "BSD-3-Clause",
"support": {
"forum": "https://groups.google.com/forum/#!forum/recaptcha",
"source": "https://github.com/google/recaptcha"
},
"require": {
"php": ">=5.5"
},
"require-dev": {
"phpunit/phpunit": "^4.8.36|^5.7.27|^6.59|^7.5.11",
"friendsofphp/php-cs-fixer": "^2.2.20|^2.15",
"php-coveralls/php-coveralls": "^2.1"
},
"autoload": {
"psr-4": {
"ReCaptcha\\": "src/ReCaptcha"
}
},
"extra": {
"branch-alias": {
"dev-master": "1.2.x-dev"
}
},
"scripts": {
"lint": "vendor/bin/php-cs-fixer -vvv fix --using-cache=no --dry-run .",
"lint-fix": "vendor/bin/php-cs-fixer -vvv fix --using-cache=no .",
"test": "vendor/bin/phpunit --colors=always",
"serve-examples": "@php -S localhost:8080 -t examples"
},
"config": {
"process-timeout": 0
}
}

View File

@ -0,0 +1,269 @@
<?php
/**
* This is a PHP library that handles calling reCAPTCHA.
*
* BSD 3-Clause License
* @copyright (c) 2019, Google Inc.
* @link https://www.google.com/recaptcha
* 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.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* 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 HOLDER 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.
*/
namespace ReCaptcha;
/**
* reCAPTCHA client.
*/
class ReCaptcha
{
/**
* Version of this client library.
* @const string
*/
const VERSION = 'php_1.2.4';
/**
* URL for reCAPTCHA siteverify API
* @const string
*/
const SITE_VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify';
/**
* Invalid JSON received
* @const string
*/
const E_INVALID_JSON = 'invalid-json';
/**
* Could not connect to service
* @const string
*/
const E_CONNECTION_FAILED = 'connection-failed';
/**
* Did not receive a 200 from the service
* @const string
*/
const E_BAD_RESPONSE = 'bad-response';
/**
* Not a success, but no error codes received!
* @const string
*/
const E_UNKNOWN_ERROR = 'unknown-error';
/**
* ReCAPTCHA response not provided
* @const string
*/
const E_MISSING_INPUT_RESPONSE = 'missing-input-response';
/**
* Expected hostname did not match
* @const string
*/
const E_HOSTNAME_MISMATCH = 'hostname-mismatch';
/**
* Expected APK package name did not match
* @const string
*/
const E_APK_PACKAGE_NAME_MISMATCH = 'apk_package_name-mismatch';
/**
* Expected action did not match
* @const string
*/
const E_ACTION_MISMATCH = 'action-mismatch';
/**
* Score threshold not met
* @const string
*/
const E_SCORE_THRESHOLD_NOT_MET = 'score-threshold-not-met';
/**
* Challenge timeout
* @const string
*/
const E_CHALLENGE_TIMEOUT = 'challenge-timeout';
/**
* Shared secret for the site.
* @var string
*/
private $secret;
/**
* Method used to communicate with service. Defaults to POST request.
* @var RequestMethod
*/
private $requestMethod;
/**
* Create a configured instance to use the reCAPTCHA service.
*
* @param string $secret The shared key between your site and reCAPTCHA.
* @param RequestMethod $requestMethod method used to send the request. Defaults to POST.
* @throws \RuntimeException if $secret is invalid
*/
public function __construct($secret, RequestMethod $requestMethod = null)
{
if (empty($secret)) {
throw new \RuntimeException('No secret provided');
}
if (!is_string($secret)) {
throw new \RuntimeException('The provided secret must be a string');
}
$this->secret = $secret;
$this->requestMethod = (is_null($requestMethod)) ? new RequestMethod\Post() : $requestMethod;
}
/**
* Calls the reCAPTCHA siteverify API to verify whether the user passes
* CAPTCHA test and additionally runs any specified additional checks
*
* @param string $response The user response token provided by reCAPTCHA, verifying the user on your site.
* @param string $remoteIp The end user's IP address.
* @return Response Response from the service.
*/
public function verify($response, $remoteIp = null)
{
// Discard empty solution submissions
if (empty($response)) {
$recaptchaResponse = new Response(false, array(self::E_MISSING_INPUT_RESPONSE));
return $recaptchaResponse;
}
$params = new RequestParameters($this->secret, $response, $remoteIp, self::VERSION);
$rawResponse = $this->requestMethod->submit($params);
$initialResponse = Response::fromJson($rawResponse);
$validationErrors = array();
if (isset($this->hostname) && strcasecmp($this->hostname, $initialResponse->getHostname()) !== 0) {
$validationErrors[] = self::E_HOSTNAME_MISMATCH;
}
if (isset($this->apkPackageName) && strcasecmp($this->apkPackageName, $initialResponse->getApkPackageName()) !== 0) {
$validationErrors[] = self::E_APK_PACKAGE_NAME_MISMATCH;
}
if (isset($this->action) && strcasecmp($this->action, $initialResponse->getAction()) !== 0) {
$validationErrors[] = self::E_ACTION_MISMATCH;
}
if (isset($this->threshold) && $this->threshold > $initialResponse->getScore()) {
$validationErrors[] = self::E_SCORE_THRESHOLD_NOT_MET;
}
if (isset($this->timeoutSeconds)) {
$challengeTs = strtotime($initialResponse->getChallengeTs());
if ($challengeTs > 0 && time() - $challengeTs > $this->timeoutSeconds) {
$validationErrors[] = self::E_CHALLENGE_TIMEOUT;
}
}
if (empty($validationErrors)) {
return $initialResponse;
}
return new Response(
false,
array_merge($initialResponse->getErrorCodes(), $validationErrors),
$initialResponse->getHostname(),
$initialResponse->getChallengeTs(),
$initialResponse->getApkPackageName(),
$initialResponse->getScore(),
$initialResponse->getAction()
);
}
/**
* Provide a hostname to match against in verify()
* This should be without a protocol or trailing slash, e.g. www.google.com
*
* @param string $hostname Expected hostname
* @return ReCaptcha Current instance for fluent interface
*/
public function setExpectedHostname($hostname)
{
$this->hostname = $hostname;
return $this;
}
/**
* Provide an APK package name to match against in verify()
*
* @param string $apkPackageName Expected APK package name
* @return ReCaptcha Current instance for fluent interface
*/
public function setExpectedApkPackageName($apkPackageName)
{
$this->apkPackageName = $apkPackageName;
return $this;
}
/**
* Provide an action to match against in verify()
* This should be set per page.
*
* @param string $action Expected action
* @return ReCaptcha Current instance for fluent interface
*/
public function setExpectedAction($action)
{
$this->action = $action;
return $this;
}
/**
* Provide a threshold to meet or exceed in verify()
* Threshold should be a float between 0 and 1 which will be tested as response >= threshold.
*
* @param float $threshold Expected threshold
* @return ReCaptcha Current instance for fluent interface
*/
public function setScoreThreshold($threshold)
{
$this->threshold = floatval($threshold);
return $this;
}
/**
* Provide a timeout in seconds to test against the challenge timestamp in verify()
*
* @param int $timeoutSeconds Expected hostname
* @return ReCaptcha Current instance for fluent interface
*/
public function setChallengeTimeout($timeoutSeconds)
{
$this->timeoutSeconds = $timeoutSeconds;
return $this;
}
}

View File

@ -0,0 +1,50 @@
<?php
/**
* This is a PHP library that handles calling reCAPTCHA.
*
* BSD 3-Clause License
* @copyright (c) 2019, Google Inc.
* @link https://www.google.com/recaptcha
* 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.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* 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 HOLDER 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.
*/
namespace ReCaptcha;
/**
* Method used to send the request to the service.
*/
interface RequestMethod
{
/**
* Submit the request with the specified parameters.
*
* @param RequestParameters $params Request parameters
* @return string Body of the reCAPTCHA response
*/
public function submit(RequestParameters $params);
}

View File

@ -0,0 +1,82 @@
<?php
/**
* This is a PHP library that handles calling reCAPTCHA.
*
* BSD 3-Clause License
* @copyright (c) 2019, Google Inc.
* @link https://www.google.com/recaptcha
* 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.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* 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 HOLDER 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.
*/
namespace ReCaptcha\RequestMethod;
/**
* Convenience wrapper around the cURL functions to allow mocking.
*/
class Curl
{
/**
* @see http://php.net/curl_init
* @param string $url
* @return resource cURL handle
*/
public function init($url = null)
{
return curl_init($url);
}
/**
* @see http://php.net/curl_setopt_array
* @param resource $ch
* @param array $options
* @return bool
*/
public function setoptArray($ch, array $options)
{
return curl_setopt_array($ch, $options);
}
/**
* @see http://php.net/curl_exec
* @param resource $ch
* @return mixed
*/
public function exec($ch)
{
return curl_exec($ch);
}
/**
* @see http://php.net/curl_close
* @param resource $ch
*/
public function close($ch)
{
curl_close($ch);
}
}

View File

@ -0,0 +1,104 @@
<?php
/**
* This is a PHP library that handles calling reCAPTCHA.
*
* BSD 3-Clause License
* @copyright (c) 2019, Google Inc.
* @link https://www.google.com/recaptcha
* 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.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* 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 HOLDER 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.
*/
namespace ReCaptcha\RequestMethod;
use ReCaptcha\ReCaptcha;
use ReCaptcha\RequestMethod;
use ReCaptcha\RequestParameters;
/**
* Sends cURL request to the reCAPTCHA service.
* Note: this requires the cURL extension to be enabled in PHP
* @see http://php.net/manual/en/book.curl.php
*/
class CurlPost implements RequestMethod
{
/**
* Curl connection to the reCAPTCHA service
* @var Curl
*/
private $curl;
/**
* URL for reCAPTCHA siteverify API
* @var string
*/
private $siteVerifyUrl;
/**
* Only needed if you want to override the defaults
*
* @param Curl $curl Curl resource
* @param string $siteVerifyUrl URL for reCAPTCHA siteverify API
*/
public function __construct(Curl $curl = null, $siteVerifyUrl = null)
{
$this->curl = (is_null($curl)) ? new Curl() : $curl;
$this->siteVerifyUrl = (is_null($siteVerifyUrl)) ? ReCaptcha::SITE_VERIFY_URL : $siteVerifyUrl;
}
/**
* Submit the cURL request with the specified parameters.
*
* @param RequestParameters $params Request parameters
* @return string Body of the reCAPTCHA response
*/
public function submit(RequestParameters $params)
{
$handle = $this->curl->init($this->siteVerifyUrl);
$options = array(
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $params->toQueryString(),
CURLOPT_HTTPHEADER => array(
'Content-Type: application/x-www-form-urlencoded'
),
CURLINFO_HEADER_OUT => false,
CURLOPT_HEADER => false,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => true
);
$this->curl->setoptArray($handle, $options);
$response = $this->curl->exec($handle);
$this->curl->close($handle);
if ($response !== false) {
return $response;
}
return '{"success": false, "error-codes": ["'.ReCaptcha::E_CONNECTION_FAILED.'"]}';
}
}

View File

@ -0,0 +1,88 @@
<?php
/**
* This is a PHP library that handles calling reCAPTCHA.
*
* BSD 3-Clause License
* @copyright (c) 2019, Google Inc.
* @link https://www.google.com/recaptcha
* 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.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* 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 HOLDER 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.
*/
namespace ReCaptcha\RequestMethod;
use ReCaptcha\ReCaptcha;
use ReCaptcha\RequestMethod;
use ReCaptcha\RequestParameters;
/**
* Sends POST requests to the reCAPTCHA service.
*/
class Post implements RequestMethod
{
/**
* URL for reCAPTCHA siteverify API
* @var string
*/
private $siteVerifyUrl;
/**
* Only needed if you want to override the defaults
*
* @param string $siteVerifyUrl URL for reCAPTCHA siteverify API
*/
public function __construct($siteVerifyUrl = null)
{
$this->siteVerifyUrl = (is_null($siteVerifyUrl)) ? ReCaptcha::SITE_VERIFY_URL : $siteVerifyUrl;
}
/**
* Submit the POST request with the specified parameters.
*
* @param RequestParameters $params Request parameters
* @return string Body of the reCAPTCHA response
*/
public function submit(RequestParameters $params)
{
$options = array(
'http' => array(
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
'method' => 'POST',
'content' => $params->toQueryString(),
// Force the peer to validate (not needed in 5.6.0+, but still works)
'verify_peer' => true,
),
);
$context = stream_context_create($options);
$response = file_get_contents($this->siteVerifyUrl, false, $context);
if ($response !== false) {
return $response;
}
return '{"success": false, "error-codes": ["'.ReCaptcha::E_CONNECTION_FAILED.'"]}';
}
}

View File

@ -0,0 +1,112 @@
<?php
/**
* This is a PHP library that handles calling reCAPTCHA.
*
* BSD 3-Clause License
* @copyright (c) 2019, Google Inc.
* @link https://www.google.com/recaptcha
* 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.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* 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 HOLDER 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.
*/
namespace ReCaptcha\RequestMethod;
/**
* Convenience wrapper around native socket and file functions to allow for
* mocking.
*/
class Socket
{
private $handle = null;
/**
* fsockopen
*
* @see http://php.net/fsockopen
* @param string $hostname
* @param int $port
* @param int $errno
* @param string $errstr
* @param float $timeout
* @return resource
*/
public function fsockopen($hostname, $port = -1, &$errno = 0, &$errstr = '', $timeout = null)
{
$this->handle = fsockopen($hostname, $port, $errno, $errstr, (is_null($timeout) ? ini_get("default_socket_timeout") : $timeout));
if ($this->handle != false && $errno === 0 && $errstr === '') {
return $this->handle;
}
return false;
}
/**
* fwrite
*
* @see http://php.net/fwrite
* @param string $string
* @param int $length
* @return int | bool
*/
public function fwrite($string, $length = null)
{
return fwrite($this->handle, $string, (is_null($length) ? strlen($string) : $length));
}
/**
* fgets
*
* @see http://php.net/fgets
* @param int $length
* @return string
*/
public function fgets($length = null)
{
return fgets($this->handle, $length);
}
/**
* feof
*
* @see http://php.net/feof
* @return bool
*/
public function feof()
{
return feof($this->handle);
}
/**
* fclose
*
* @see http://php.net/fclose
* @return bool
*/
public function fclose()
{
return fclose($this->handle);
}
}

View File

@ -0,0 +1,108 @@
<?php
/**
* This is a PHP library that handles calling reCAPTCHA.
*
* BSD 3-Clause License
* @copyright (c) 2019, Google Inc.
* @link https://www.google.com/recaptcha
* 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.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* 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 HOLDER 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.
*/
namespace ReCaptcha\RequestMethod;
use ReCaptcha\ReCaptcha;
use ReCaptcha\RequestMethod;
use ReCaptcha\RequestParameters;
/**
* Sends a POST request to the reCAPTCHA service, but makes use of fsockopen()
* instead of get_file_contents(). This is to account for people who may be on
* servers where allow_url_open is disabled.
*/
class SocketPost implements RequestMethod
{
/**
* Socket to the reCAPTCHA service
* @var Socket
*/
private $socket;
/**
* Only needed if you want to override the defaults
*
* @param \ReCaptcha\RequestMethod\Socket $socket optional socket, injectable for testing
* @param string $siteVerifyUrl URL for reCAPTCHA siteverify API
*/
public function __construct(Socket $socket = null, $siteVerifyUrl = null)
{
$this->socket = (is_null($socket)) ? new Socket() : $socket;
$this->siteVerifyUrl = (is_null($siteVerifyUrl)) ? ReCaptcha::SITE_VERIFY_URL : $siteVerifyUrl;
}
/**
* Submit the POST request with the specified parameters.
*
* @param RequestParameters $params Request parameters
* @return string Body of the reCAPTCHA response
*/
public function submit(RequestParameters $params)
{
$errno = 0;
$errstr = '';
$urlParsed = parse_url($this->siteVerifyUrl);
if (false === $this->socket->fsockopen('ssl://' . $urlParsed['host'], 443, $errno, $errstr, 30)) {
return '{"success": false, "error-codes": ["'.ReCaptcha::E_CONNECTION_FAILED.'"]}';
}
$content = $params->toQueryString();
$request = "POST " . $urlParsed['path'] . " HTTP/1.0\r\n";
$request .= "Host: " . $urlParsed['host'] . "\r\n";
$request .= "Content-Type: application/x-www-form-urlencoded\r\n";
$request .= "Content-length: " . strlen($content) . "\r\n";
$request .= "Connection: close\r\n\r\n";
$request .= $content . "\r\n\r\n";
$this->socket->fwrite($request);
$response = '';
while (!$this->socket->feof()) {
$response .= $this->socket->fgets(4096);
}
$this->socket->fclose();
if (0 !== strpos($response, 'HTTP/1.0 200 OK')) {
return '{"success": false, "error-codes": ["'.ReCaptcha::E_BAD_RESPONSE.'"]}';
}
$parts = preg_split("#\n\s*\n#Uis", $response);
return $parts[1];
}
}

View File

@ -0,0 +1,111 @@
<?php
/**
* This is a PHP library that handles calling reCAPTCHA.
*
* BSD 3-Clause License
* @copyright (c) 2019, Google Inc.
* @link https://www.google.com/recaptcha
* 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.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* 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 HOLDER 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.
*/
namespace ReCaptcha;
/**
* Stores and formats the parameters for the request to the reCAPTCHA service.
*/
class RequestParameters
{
/**
* The shared key between your site and reCAPTCHA.
* @var string
*/
private $secret;
/**
* The user response token provided by reCAPTCHA, verifying the user on your site.
* @var string
*/
private $response;
/**
* Remote user's IP address.
* @var string
*/
private $remoteIp;
/**
* Client version.
* @var string
*/
private $version;
/**
* Initialise parameters.
*
* @param string $secret Site secret.
* @param string $response Value from g-captcha-response form field.
* @param string $remoteIp User's IP address.
* @param string $version Version of this client library.
*/
public function __construct($secret, $response, $remoteIp = null, $version = null)
{
$this->secret = $secret;
$this->response = $response;
$this->remoteIp = $remoteIp;
$this->version = $version;
}
/**
* Array representation.
*
* @return array Array formatted parameters.
*/
public function toArray()
{
$params = array('secret' => $this->secret, 'response' => $this->response);
if (!is_null($this->remoteIp)) {
$params['remoteip'] = $this->remoteIp;
}
if (!is_null($this->version)) {
$params['version'] = $this->version;
}
return $params;
}
/**
* Query string representation for HTTP request.
*
* @return string Query string formatted parameters.
*/
public function toQueryString()
{
return http_build_query($this->toArray(), '', '&');
}
}

View File

@ -0,0 +1,218 @@
<?php
/**
* This is a PHP library that handles calling reCAPTCHA.
*
* BSD 3-Clause License
* @copyright (c) 2019, Google Inc.
* @link https://www.google.com/recaptcha
* 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.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* 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 HOLDER 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.
*/
namespace ReCaptcha;
/**
* The response returned from the service.
*/
class Response
{
/**
* Success or failure.
* @var boolean
*/
private $success = false;
/**
* Error code strings.
* @var array
*/
private $errorCodes = array();
/**
* The hostname of the site where the reCAPTCHA was solved.
* @var string
*/
private $hostname;
/**
* Timestamp of the challenge load (ISO format yyyy-MM-dd'T'HH:mm:ssZZ)
* @var string
*/
private $challengeTs;
/**
* APK package name
* @var string
*/
private $apkPackageName;
/**
* Score assigned to the request
* @var float
*/
private $score;
/**
* Action as specified by the page
* @var string
*/
private $action;
/**
* Build the response from the expected JSON returned by the service.
*
* @param string $json
* @return \ReCaptcha\Response
*/
public static function fromJson($json)
{
$responseData = json_decode($json, true);
if (!$responseData) {
return new Response(false, array(ReCaptcha::E_INVALID_JSON));
}
$hostname = isset($responseData['hostname']) ? $responseData['hostname'] : null;
$challengeTs = isset($responseData['challenge_ts']) ? $responseData['challenge_ts'] : null;
$apkPackageName = isset($responseData['apk_package_name']) ? $responseData['apk_package_name'] : null;
$score = isset($responseData['score']) ? floatval($responseData['score']) : null;
$action = isset($responseData['action']) ? $responseData['action'] : null;
if (isset($responseData['success']) && $responseData['success'] == true) {
return new Response(true, array(), $hostname, $challengeTs, $apkPackageName, $score, $action);
}
if (isset($responseData['error-codes']) && is_array($responseData['error-codes'])) {
return new Response(false, $responseData['error-codes'], $hostname, $challengeTs, $apkPackageName, $score, $action);
}
return new Response(false, array(ReCaptcha::E_UNKNOWN_ERROR), $hostname, $challengeTs, $apkPackageName, $score, $action);
}
/**
* Constructor.
*
* @param boolean $success
* @param string $hostname
* @param string $challengeTs
* @param string $apkPackageName
* @param float $score
* @param string $action
* @param array $errorCodes
*/
public function __construct($success, array $errorCodes = array(), $hostname = null, $challengeTs = null, $apkPackageName = null, $score = null, $action = null)
{
$this->success = $success;
$this->hostname = $hostname;
$this->challengeTs = $challengeTs;
$this->apkPackageName = $apkPackageName;
$this->score = $score;
$this->action = $action;
$this->errorCodes = $errorCodes;
}
/**
* Is success?
*
* @return boolean
*/
public function isSuccess()
{
return $this->success;
}
/**
* Get error codes.
*
* @return array
*/
public function getErrorCodes()
{
return $this->errorCodes;
}
/**
* Get hostname.
*
* @return string
*/
public function getHostname()
{
return $this->hostname;
}
/**
* Get challenge timestamp
*
* @return string
*/
public function getChallengeTs()
{
return $this->challengeTs;
}
/**
* Get APK package name
*
* @return string
*/
public function getApkPackageName()
{
return $this->apkPackageName;
}
/**
* Get score
*
* @return float
*/
public function getScore()
{
return $this->score;
}
/**
* Get action
*
* @return string
*/
public function getAction()
{
return $this->action;
}
public function toArray()
{
return array(
'success' => $this->isSuccess(),
'hostname' => $this->getHostname(),
'challenge_ts' => $this->getChallengeTs(),
'apk_package_name' => $this->getApkPackageName(),
'score' => $this->getScore(),
'action' => $this->getAction(),
'error-codes' => $this->getErrorCodes(),
);
}
}

View File

@ -0,0 +1,69 @@
<?php
/* An autoloader for ReCaptcha\Foo classes. This should be required()
* by the user before attempting to instantiate any of the ReCaptcha
* classes.
*
* BSD 3-Clause License
* @copyright (c) 2019, Google Inc.
* @link https://www.google.com/recaptcha
* 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.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* 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 HOLDER 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.
*/
spl_autoload_register(function ($class) {
if (substr($class, 0, 10) !== 'ReCaptcha\\') {
/* If the class does not lie under the "ReCaptcha" namespace,
* then we can exit immediately.
*/
return;
}
/* All of the classes have names like "ReCaptcha\Foo", so we need
* to replace the backslashes with frontslashes if we want the
* name to map directly to a location in the filesystem.
*/
$class = str_replace('\\', '/', $class);
/* First, check under the current directory. It is important that
* we look here first, so that we don't waste time searching for
* test classes in the common case.
*/
$path = dirname(__FILE__).'/'.$class.'.php';
if (is_readable($path)) {
require_once $path;
return;
}
/* If we didn't find what we're looking for already, maybe it's
* a test class?
*/
$path = dirname(__FILE__).'/../tests/'.$class.'.php';
if (is_readable($path)) {
require_once $path;
}
});

View File

@ -0,0 +1,77 @@
subject = "Contact form confirmation"
==
Hello,
this is a confirmation from Contact form.
{% if fieldsDetails|length %}
You have sent this:
<table border="0">
<tbody>
{% for field in fieldsDetails %}
{% if field.label == 'form_description' or field.label == 'form_alias' %}
{% else %}
<tr>
<th style="vertical-align: top; text-align: left; padding-right: 10px;">{{ field.label }}</th>
<td style="text-align: left;">{{ field.value|raw|nl2br }}</td>
<th>{{field.label}}</th>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
{% endif %}
Best regards,
Contact form
==
<p>Hello,</p>
<p>this is a confirmation from Contact form.</p>
{% if fieldsDetails|length %}
<br>
<p>You have sent this:</p>
<table border="0">
<tbody>
{% for field in fieldsDetails %}
<tr>
<th style="vertical-align: top; text-align: left; padding-right: 10px;">{{ field.label }}</th>
<td style="text-align: left;">{{ field.value|raw|nl2br }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<br>
<p>Best regards,<br>
Contact form</p>

View File

@ -0,0 +1,76 @@
subject = "Potvrzení doručení zprávy z kontaktního formuláře"
==
Dobrý den,
posíláme potvrzení přijetí vaší zprávy odeslané z kontaktního formuláře.
{% if fields|length %}
Vyplnil/a jste toto:
<table border="0">
<tbody>
{% for field in fieldsDetails %}
{% if field.label == 'form_description' or field.label == 'form_alias' %}
{% else %}
<tr>
<th style="vertical-align: top; text-align: left; padding-right: 10px;">{{ field.label }}</th>
<td style="text-align: left;">{{ field.value|raw|nl2br }}</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
{% endif %}
S pozdravem,
Kontaktní formulář
==
<p>Dobrý den,</p>
<p>posíláme potvrzení přijetí vaší zprávy odeslané z kontaktního formuláře.</p>
{% if fields|length %}
<br>
<p>Vyplnil/a jste toto:</p>
<table border="0">
<tbody>
{% for field in fieldsDetails %}
<tr>
<th style="vertical-align: top; text-align: left; padding-right: 10px;">{{ field.label }}</th>
<td style="text-align: left;">{{ field.value|raw|nl2br }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<br>
<p>S pozdravem,<br>
Kontaktní formulář</p>

View File

@ -0,0 +1,77 @@
subject = "Confirmación del formulario de contacto"
==
Hola,
esto es una confirmación del formulario de contacto.
{% if fieldsDetails|length %}
Ha enviado lo siguiente:
<table border="0">
<tbody>
{% for field in fieldsDetails %}
{% if field.label == 'form_description' or field.label == 'form_alias' %}
{% else %}
<tr>
<th style="vertical-align: top; text-align: left; padding-right: 10px;">{{ field.label }}</th>
<td style="text-align: left;">{{ field.value|raw|nl2br }}</td>
<th>{{field.label}}</th>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
{% endif %}
Le saluda atentamente,
El formulario de contacto
==
<p>Hola,</p>
<p>esto es una confirmación del formulario de contacto.</p>
{% if fieldsDetails|length %}
<br>
<p>Ha enviado lo siguiente:</p>
<table border="0">
<tbody>
{% for field in fieldsDetails %}
<tr>
<th style="vertical-align: top; text-align: left; padding-right: 10px;">{{ field.label }}</th>
<td style="text-align: left;">{{ field.value|raw|nl2br }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<br>
<p>Le saluda atentamente,<br>
El formulario de contacto</p>

View File

@ -0,0 +1,77 @@
subject = "Potwierdzenie wysłania formularza"
==
Dzień dobry,
to jest potwierdzenie wysłane z formularza kontaktowego.
{% if fieldsDetails|length %}
Wiadomość, którą wysłałeś:
<table border="0">
<tbody>
{% for field in fieldsDetails %}
{% if field.label == 'form_description' or field.label == 'form_alias' %}
{% else %}
<tr>
<th style="vertical-align: top; text-align: left; padding-right: 10px;">{{ field.label }}</th>
<td style="text-align: left;">{{ field.value|raw|nl2br }}</td>
<th>{{field.label}}</th>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
{% endif %}
Z poważaniem,
Formularz kontaktowy
==
<p>Dzień dobry,</p>
<p>to jest potwierdzenie wysłane z formularza kontaktowego.</p>
{% if fieldsDetails|length %}
<br>
<p>Wiadomość, którą wysłałeś:</p>
<table border="0">
<tbody>
{% for field in fieldsDetails %}
<tr>
<th style="vertical-align: top; text-align: left; padding-right: 10px;">{{ field.label }}</th>
<td style="text-align: left;">{{ field.value|raw|nl2br }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<br>
<p>Z poważaniem,<br>
Formularz kontaktowy</p>

View File

@ -0,0 +1,71 @@
subject = "Contact form notification"
==
Hello,
this is a notification from a Contact form.
{% if fields|length %}
Sent form content:
<table border="0">
<tbody>
{% for key,value in fields %}
<tr>
<th style="vertical-align: top; text-align: left; padding-right: 10px;">{{ key|upper }}</th>
<td style="text-align: left;">{{ value|raw|nl2br }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
Best regards,
Contact form
==
<p>Hello,</p>
<p>this is a notification from a Contact form.</p>
{% if fields|length %}
<br>
<p>Sent form content:</p>
<table border="0">
<tbody>
{% for key,value in fields %}
<tr>
<th style="vertical-align: top; text-align: left; padding-right: 10px;">{{ key|upper }}</th>
<td style="text-align: left;">{{ value|raw|nl2br }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<br>
<p>Best regards,<br>
Contact form</p>

View File

@ -0,0 +1,71 @@
subject = "Oznámení o odeslání kontaktního formuláře"
==
Dobrý den,
toto je oznámení o odeslání kontaktního formuláře.
{% if fields|length %}
Odeslaný obsah:
<table border="0">
<tbody>
{% for key,value in fields %}
<tr>
<th style="vertical-align: top; text-align: left; padding-right: 10px;">{{ key|upper }}</th>
<td style="text-align: left;">{{ value|raw|nl2br }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
S pozdravem,
Kontaktní formulář
==
<p>Dobrý den,</p>
<p>toto je oznámení o odeslání kontaktního formuláře.</p>
{% if fields|length %}
<br>
<p>Odeslaný obsah:</p>
<table border="0">
<tbody>
{% for key,value in fields %}
<tr>
<th style="vertical-align: top; text-align: left; padding-right: 10px;">{{ key|upper }}</th>
<td style="text-align: left;">{{ value|raw|nl2br }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<br>
<p>S pozdravem,<br>
Kontaktní formulář</p>

View File

@ -0,0 +1,70 @@
subject = "Notificación del formulario de contacto"
==
Hola,
esto es una notificación de un formulario de contacto.
{% if fields|length %}
Contenido del formulario enviado:
<table border="0">
<tbody>
{% for key,value in fields %}
<tr>
<th style="vertical-align: top; text-align: left; padding-right: 10px;">{{ key|upper }}</th>
<td style="text-align: left;">{{ value|raw|nl2br }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
Le saluda atentamente,
El formulario de contacto
==
<p>Hola,</p>
<p>esto es una notificación de un formulario de contacto..</p>
{% if fields|length %}
<br>
<p>Sent form content:</p>
<table border="0">
<tbody>
{% for key,value in fields %}
<tr>
<th style="vertical-align: top; text-align: left; padding-right: 10px;">{{ key|upper }}</th>
<td style="text-align: left;">{{ value|raw|nl2br }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<br>
<p>Le saluda atentamente,<br>
El formulario de contacto</p>

View File

@ -0,0 +1,77 @@
subject = "Powiadomienie o nowej wiadomości"
==
Dzień dobry,
to jest powiadomienie wysłane z formularza kontaktowego.
{% if fieldsDetails|length %}
Otrzymałeś nową wiadomość:
<table border="0">
<tbody>
{% for field in fieldsDetails %}
{% if field.label == 'form_description' or field.label == 'form_alias' %}
{% else %}
<tr>
<th style="vertical-align: top; text-align: left; padding-right: 10px;">{{ field.label }}</th>
<td style="text-align: left;">{{ field.value|raw|nl2br }}</td>
<th>{{field.label}}</th>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
{% endif %}
Z poważaniem,
Formularz kontaktowy
==
<p>Dzień dobry,</p>
<p>to jest powiadomienie wysłane z formularza kontaktowego.</p>
{% if fieldsDetails|length %}
<br>
<p>Otrzymałeś nową wiadomość:</p>
<table border="0">
<tbody>
{% for field in fieldsDetails %}
<tr>
<th style="vertical-align: top; text-align: left; padding-right: 10px;">{{ field.label }}</th>
<td style="text-align: left;">{{ field.value|raw|nl2br }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<br>
<p>Z poważaniem,<br>
Formularz kontaktowy</p>

View File

@ -10,4 +10,6 @@ class Settings extends CommonSettings
const SETTINGS_CODE = 'lovata_toolbox_settings';
public $settingsCode = 'lovata_toolbox_settings';
public $translatable = ['address','site_name'];
}

View File

@ -1,10 +1,43 @@
tabs:
fields:
site_name:
tab: lovata.toolbox::lang.tab.settings
label: Site name
dollar:
tab: lovata.toolbox::lang.tab.settings
span: left
label: Dollar kurs
euro:
tab: lovata.toolbox::lang.tab.settings
span: right
label: Euro kurs
gbp:
tab: lovata.toolbox::lang.tab.settings
span: left
label: GBP kurs
phone:
tab: lovata.toolbox::lang.tab.settings
span: left
label: Telefon
email:
tab: lovata.toolbox::lang.tab.settings
span: right
label: Email
address:
tab: lovata.toolbox::lang.tab.settings
label: Adres
slug_is_translatable:
tab: lovata.toolbox::lang.tab.settings
span: left
label: lovata.toolbox::lang.settings.slug_is_translatable
type: checkbox
queue_on:
tab: lovata.toolbox::lang.tab.mail
span: left

View File

@ -0,0 +1,15 @@
---
name: "🐛 Bug Report"
about: 'Report a general Pages Plugin issue'
labels: 'Status: Review Needed, Type: Unconfirmed Bug'
---
- OctoberCMS Build: ### <!-- Or Commit hash if using composer -->
- RainLab Pages Plugin Version: ###
- PHP Version:
### Description:
<!-- Describe the issue encountered and what should actually be happening instead in as much detail as possible-->
### Steps To Reproduce:
<!-- (Describe the steps to reproduce the problem here) -->

View File

@ -0,0 +1,13 @@
---
name: "⚠️ General Support"
about: 'This repository is only for reporting bugs or problems. If you need help using RainLab Pages, see: https://octobercms.com/support'
---
This repository is only for reporting bugs or problems with the RainLab Pages plugin. If you need support, please use
the following options:
- Forum: https://octobercms.com/forum
- Discord: https://discord.com/invite/gEKgwSZ
- Stack Overflow: https://stackoverflow.com/questions/tagged/octobercms
Thanks!

2
plugins/rainlab/pages/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.DS_Store
.idea

View File

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

View File

@ -0,0 +1,252 @@
<?php namespace RainLab\Pages;
use Event;
use Backend;
use RainLab\Pages\Classes\Controller;
use RainLab\Pages\Classes\Page as StaticPage;
use RainLab\Pages\Classes\Router;
use RainLab\Pages\Classes\Snippet;
use RainLab\Pages\Classes\SnippetManager;
use Cms\Classes\Theme;
use Cms\Classes\Controller as CmsController;
use System\Classes\PluginBase;
class Plugin extends PluginBase
{
public function pluginDetails()
{
return [
'name' => 'rainlab.pages::lang.plugin.name',
'description' => 'rainlab.pages::lang.plugin.description',
'author' => 'Alexey Bobkov, Samuel Georges',
'icon' => 'icon-files-o',
'homepage' => 'https://github.com/rainlab/pages-plugin'
];
}
public function registerComponents()
{
return [
'\RainLab\Pages\Components\ChildPages' => 'childPages',
'\RainLab\Pages\Components\StaticPage' => 'staticPage',
'\RainLab\Pages\Components\StaticMenu' => 'staticMenu',
'\RainLab\Pages\Components\StaticBreadcrumbs' => 'staticBreadcrumbs'
];
}
public function registerPermissions()
{
return [
'rainlab.pages.manage_pages' => [
'tab' => 'rainlab.pages::lang.page.tab',
'order' => 200,
'label' => 'rainlab.pages::lang.page.manage_pages'
],
'rainlab.pages.manage_menus' => [
'tab' => 'rainlab.pages::lang.page.tab',
'order' => 200,
'label' => 'rainlab.pages::lang.page.manage_menus'
],
'rainlab.pages.manage_content' => [
'tab' => 'rainlab.pages::lang.page.tab',
'order' => 200,
'label' => 'rainlab.pages::lang.page.manage_content'
],
'rainlab.pages.access_snippets' => [
'tab' => 'rainlab.pages::lang.page.tab',
'order' => 200,
'label' => 'rainlab.pages::lang.page.access_snippets'
]
];
}
public function registerNavigation()
{
return [
'pages' => [
'label' => 'rainlab.pages::lang.plugin.name',
'url' => Backend::url('rainlab/pages'),
'icon' => 'icon-files-o',
'iconSvg' => 'plugins/rainlab/pages/assets/images/pages-icon.svg',
'permissions' => ['rainlab.pages.*'],
'order' => 200,
'sideMenu' => [
'pages' => [
'label' => 'rainlab.pages::lang.page.menu_label',
'icon' => 'icon-files-o',
'url' => 'javascript:;',
'attributes' => ['data-menu-item'=>'pages'],
'permissions' => ['rainlab.pages.manage_pages']
],
'menus' => [
'label' => 'rainlab.pages::lang.menu.menu_label',
'icon' => 'icon-sitemap',
'url' => 'javascript:;',
'attributes' => ['data-menu-item'=>'menus'],
'permissions' => ['rainlab.pages.manage_menus']
],
'content' => [
'label' => 'rainlab.pages::lang.content.menu_label',
'icon' => 'icon-file-text-o',
'url' => 'javascript:;',
'attributes' => ['data-menu-item'=>'content'],
'permissions' => ['rainlab.pages.manage_content']
],
'snippets' => [
'label' => 'rainlab.pages::lang.snippet.menu_label',
'icon' => 'icon-newspaper-o',
'url' => 'javascript:;',
'attributes' => ['data-menu-item'=>'snippet'],
'permissions' => ['rainlab.pages.access_snippets']
]
]
]
];
}
public function registerFormWidgets()
{
return [
FormWidgets\PagePicker::class => 'staticpagepicker',
FormWidgets\MenuPicker::class => 'staticmenupicker',
];
}
public function boot()
{
Event::listen('cms.router.beforeRoute', function($url) {
return Controller::instance()->initCmsPage($url);
});
Event::listen('cms.page.beforeRenderPage', function($controller, $page) {
/*
* Before twig renders
*/
$twig = $controller->getTwig();
$loader = $controller->getLoader();
Controller::instance()->injectPageTwig($page, $loader, $twig);
/*
* Get rendered content
*/
$contents = Controller::instance()->getPageContents($page);
if (strlen($contents)) {
return $contents;
}
});
Event::listen('cms.page.initComponents', function($controller, $page) {
Controller::instance()->initPageComponents($controller, $page);
});
Event::listen('cms.block.render', function($blockName, $blockContents) {
$page = CmsController::getController()->getPage();
if (!isset($page->apiBag['staticPage'])) {
return;
}
$contents = Controller::instance()->getPlaceholderContents($page, $blockName, $blockContents);
if (strlen($contents)) {
return $contents;
}
});
Event::listen('pages.menuitem.listTypes', function() {
return [
'static-page' => 'rainlab.pages::lang.menuitem.static_page',
'all-static-pages' => 'rainlab.pages::lang.menuitem.all_static_pages'
];
});
Event::listen('pages.menuitem.getTypeInfo', function($type) {
if ($type == 'url') {
return [];
}
if ($type == 'static-page'|| $type == 'all-static-pages') {
return StaticPage::getMenuTypeInfo($type);
}
});
Event::listen('pages.menuitem.resolveItem', function($type, $item, $url, $theme) {
if ($type == 'static-page' || $type == 'all-static-pages') {
return StaticPage::resolveMenuItem($item, $url, $theme);
}
});
Event::listen('backend.form.extendFieldsBefore', function($formWidget) {
if ($formWidget->model instanceof \Cms\Classes\Partial) {
Snippet::extendPartialForm($formWidget);
}
});
Event::listen('cms.template.getTemplateToolbarSettingsButtons', function($extension, $dataHolder) {
if ($dataHolder->templateType === 'partial') {
Snippet::extendEditorPartialToolbar($dataHolder);
}
});
Event::listen('cms.template.save', function($controller, $template, $type) {
Plugin::clearCache();
});
Event::listen('cms.template.processSettingsBeforeSave', function($controller, $dataHolder) {
$dataHolder->settings = Snippet::processTemplateSettingsArray($dataHolder->settings);
});
Event::listen('cms.template.processSettingsAfterLoad', function($controller, $template, $context = null) {
Snippet::processTemplateSettings($template, $context);
});
Event::listen('cms.template.processTwigContent', function($template, $dataHolder) {
if ($template instanceof \Cms\Classes\Layout) {
$dataHolder->content = Controller::instance()->parseSyntaxFields($dataHolder->content);
}
});
Event::listen('backend.richeditor.listTypes', function () {
return [
'static-page' => 'rainlab.pages::lang.menuitem.static_page',
];
});
Event::listen('backend.richeditor.getTypeInfo', function ($type) {
if ($type === 'static-page') {
return StaticPage::getRichEditorTypeInfo($type);
}
});
Event::listen('system.console.theme.sync.getAvailableModelClasses', function () {
return [
Classes\Menu::class,
Classes\Page::class,
];
});
}
/**
* Register new Twig variables
* @return array
*/
public function registerMarkupTags()
{
return [
'filters' => [
'staticPage' => ['RainLab\Pages\Classes\Page', 'url']
]
];
}
public static function clearCache()
{
$theme = Theme::getEditTheme();
$router = new Router($theme);
$router->clearCache();
StaticPage::clearMenuCache($theme);
SnippetManager::clearCache($theme);
}
}

View File

@ -0,0 +1,91 @@
# Pages plugin
This plugin allows end users to create and edit static pages and menus with a simple WYSIWYG user interface.
## Managing static pages
Static pages are managed on the Pages tab of the Static Pages plugin. Static pages have three required parameters - **Title**, **URL** and **Layout**. The URL is generated automatically when the Title is entered, but it could be changed manually. URLs must start with the forward slash character. The Layout drop-down allows to select a layout created with the CMS. Only layouts that include the `staticPage` component are displayed in the drop-down.
![image](https://raw.githubusercontent.com/rainlab/pages-plugin/master/docs/images/static-page.png) {.img-responsive .frame}
Pages are hierarchical. The page hierarchy is used when a new page URL is generated, but as URLs can be changed manually, the hierarchy doesn't really affect the routing process. The only place where the page hierarchy matters is the generated Menus. The generated menus reflect the page hierarchy. You can manage the page hierarchy by dragging pages in the page tree. The page drag handle appears when you move the mouse cursor over page item in the tree.
Optional properties of static pages are **Hidden** and **Hide in navigation**. The **Hidden** checkbox allows to hide a page from the front-end. Hidden pages are still visible for administrators who are logged into the back-end. The **Hide in navigation** checkbox allows to hide a page from generated menus and breadcrumbs.
## Placeholders
If a static layout contains [placeholders](https://octobercms.com/docs/cms/layouts#placeholders), the static page editor will show tabs for editing the placeholder contents. The plugin automatically detects text and HTML placeholders and displays a corresponding editor for them - the WYSIWYG editor for HTML placeholders and a text editor for text placeholders.
## Snippets
Snippets are elements that can be added by a Static Page, in the rich text editor. They allow to inject complex (and interactive) areas to pages. There are many possible applications and examples of using Snippets:
* Google Maps snippet - outputs a map centered on specific coordinates with predefined zoom factor. That snippet would be great for static pages that explain directions.
* Universal commenting system - allows visitors to post comments to any static page.
* Third-party integrations - for example with Yelp or TripAdvisor for displaying extra information on a static page.
Snippets are displayed in the sidebar list on the Static Pages and can be added into a rich editor with a mouse click. Snippets are configurable and have properties that users can manage with the Inspector.
![image](https://raw.githubusercontent.com/rainlab/pages-plugin/master/docs/images/snippets-backend.png)
![image](https://raw.githubusercontent.com/rainlab/pages-plugin/master/docs/images/snippets-frontend.png)
## Managing menus
You can manage menus on the Menus tab of the Static Pages plugin. A website can contain multiple menus, for example the main menu, footer menu, sidebar menu, etc. A theme developer can include menus on a page layout with the `staticMenu` component.
Menus have two required properties - the menu **Name** and menu **Code**. The menu name is displayed in the menu list in the back-end. The menu code is required for referring menus in the layout code, it's the API parameter.
![image](https://raw.githubusercontent.com/rainlab/pages-plugin/master/docs/images/menu-management.png) {.img-responsive .frame}
Menus can contain multiple **menu items**, and menu items can be nested. Each menu item has a number of properties. There are properties common for all menu item types, and some properties depend on the item type. The common menu item properties are **Title** and **Type**. The Title defines the menu item text. The Type is a drop-down list which displays all menu item types available in your OctoberCMS copy.
![image](https://raw.githubusercontent.com/rainlab/pages-plugin/master/docs/images/menu-item.png) {.img-responsive .frame}
#### Standard menu item types
The available menu item types depend on the installed plugins, but there are three basic item types that are supported out of the box.
###### Header {.subheader}
Items of this type are used for displaying text and don't link to anything. The text could be used as a category heading for other menu items. This type will only show a title property.
###### URL {.subheader}
Items of this type are links to a specific fixed URL. That could be an URL of an or internal page. Items of this type don't have any other properties - just the title and URL.
###### Static Page {.subheader}
Items of this type refer to static pages. The static page should be selected in the **Reference** drop-down list described below.
###### All Static Pages {.subheader}
Items of this type expand to create links to all static pages defined in the theme. Nested pages are represented with nested menu items.
#### Custom menu item types
Other plugins can supply new menu item types. For example, the [Blog plugin](https://octobercms.com/plugin/rainlab-blog) by [RainLab](https://octobercms.com/author/RainLab) supplies two more types:
###### Blog Category {.subheader}
An item of this type represents a link to a specific blog category. The category should be selected in the **Reference** drop-down. This menu type also requires selecting a **CMS page** that outputs a blog category.
###### All Blog Categories {.subheader}
An item of this time expands into multiple items representing all blog existing categories. This menu type also requires selecting a **CMS page**.
#### Menu item properties
Depending on the selected menu item time you might need to provide other properties of the menu item. The available properties are described below.
###### Reference {.subheader}
A drop-down list of objects the menu item should refer to. The list content depends on the menu item type. For the **Static page** item type the list displays all static pages defined in the system. For the **Blog category** item type the list displays a list of blog categories.
###### Allow nested items {.subheader}
This checkbox is available only for menu item types that suppose nested objects. For example, static pages are hierarchical, and this property is available for the **Static page** item type. On the other hand, blog categories are not hierarchical, and the checkbox is hidden.
###### Replace this item with its generated children {.subheader}
A checkbox determining whether the menu item should be replaced with generated menu items. This property is available only for menu item types that suppose automatic item generating, for example for the **Static page** menu item type. The **Blog category** menu item type doesn't have this property because blog categories cannot be nested and menu items of this type always point to a specific blog category. This property is very handy when you want to include generated menu items to the root of the menu. For example, you can create the **All blog categories** menu item and enable the replacing. As a result you will get a menu that lists all blog categories on the first level of the menu. If you didn't enable the replacing, there would be a root menu item, with blog categories listed under it.
###### CMS Page {.subheader}
This drop-down is available for menu item types that require a special CMS page to refer to. For example, the **Blog category** menu item type requires a CMS page that hosts the `blogPosts` component. The CMS Page drop-down for this item type will only display pages that include this component.
###### Code {.subheader}
The Code field allows to assign the API code that you can use to set the active menu item explicitly in the page's `onInit()` handler described in the documentation.
## See also
Read the [Getting started with Static Pages](https://octobercms.com/blog/post/getting-started-static-pages) tutorial in the Blog.
Read the [documentation](/docs/documentation.md).

View File

@ -0,0 +1,133 @@
.control-treeview ol > li.dragged div,
.control-treeview ol > li > div:hover {
background-color: #4ea5e0 !important;
}
.control-treeview ol >li > div:active {
background-color: #3498db !important;
}
.control-filelist.menu-list li > a {
position: relative;
}
.control-filelist.menu-list li > a:before {
position: absolute;
width: 18px;
height: 18px;
left: 17px;
top: 18px;
content: ' ';
background-image: url(../images/menu-icons.png);
background-position: 0 0;
background-repeat: no-repeat;
background-size: 36px auto;
}
.control-filelist.menu-list li > a:hover:before {
background-position: 0 -60px;
}
.control-filelist.content li > a {
position: relative;
}
.control-filelist.content li > a:before {
position: absolute;
width: 18px;
height: 22px;
left: 18px;
top: 10px;
content: ' ';
background-image: url(../images/content-icons.png);
background-position: 0 0;
background-repeat: no-repeat;
background-size: 34px auto;
}
.control-filelist.content li > a:hover:before {
background-position: 0 -27px;
}
.control-filelist.content li.group ul li > a:before {
left: 34px;
}
.control-filelist.snippet-list li > a {
position: relative;
color: #808c8d;
}
.control-filelist.snippet-list li > a:before {
position: absolute;
width: 17px;
height: 19px;
content: ' ';
background-image: url(../images/snippet-icons.png);
background-position: 0 0;
background-repeat: no-repeat;
background-size: 34px auto;
left: 18px;
top: 13px;
}
.control-filelist.snippet-list li > a:hover:before {
background-position: 0 -21px;
}
.control-filelist.snippet-list li.group ul li > a:before {
left: 34px;
}
@media only screen and (-moz-min-device-pixel-ratio: 1.5), only screen and (-o-min-device-pixel-ratio: 3/2), only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-devicepixel-ratio: 1.5), only screen and (min-resolution: 1.5dppx) {
.control-filelist.menu-list li > a:before {
background-position: 0px -11px;
background-size: 18px auto;
}
.control-filelist.menu-list li > a:hover:before {
background-position: 0px -40px;
}
.control-filelist.content li a:before {
background-position: 0px -27px;
background-size: 17px auto;
}
.control-filelist.content li a:hover:before {
background-position: 0px -52px;
}
.control-filelist.snippet-list li a:before {
background-position: 0px -21px;
background-size: 17px auto;
}
.control-filelist.snippet-list li a:hover:before {
background-position: 0px -41px;
}
}
.fancy-layout .pagesTextEditor {
border-left: 1px solid #bdc3c7 !important;
}
.control-richeditor [data-snippet]:before {
content: attr(data-name);
}
.control-richeditor [data-snippet]:after {
position: absolute;
width: 17px;
height: 19px;
left: 18px;
top: 17px;
content: ' ';
background-image: url(../images/snippet-icons.png);
background-position: 0 0;
background-repeat: no-repeat;
background-size: 34px auto;
left: 11px;
top: 12px;
}
.control-richeditor [data-snippet].loading:after {
background-image: url(../../../../../modules/system/assets/ui/images/loader-transparent.svg);
background-size: 15px 15px;
background-position: 50% 50%;
position: absolute;
width: 15px;
height: 15px;
top: 13px;
content: ' ';
-webkit-animation: spin 1s linear infinite;
animation: spin 1s linear infinite;
}
@media only screen and (-moz-min-device-pixel-ratio: 1.5), only screen and (-o-min-device-pixel-ratio: 3/2), only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-devicepixel-ratio: 1.5), only screen and (min-resolution: 1.5dppx) {
.control-richeditor [data-snippet]:after {
background-position: 0px -21px;
background-size: 17px auto;
}
}
.form-group.secondary-tab {
padding: 10px 20px;
background: #fff;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="65px" height="64px" viewBox="0 0 65 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.4.4 (17249) - http://www.bohemiancoding.com/sketch -->
<title>pages-icon</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="Group" sketch:type="MSLayerGroup" transform="translate(1.000000, 0.000000)">
<path d="M-0.246,3.84 C-0.246,1.719 1.473,0 3.594,0 L31.754,0 L47.754,13.44 L47.754,60.16 C47.754,62.281 46.035,64 43.914,64 L3.594,64 C1.473,64 -0.246,62.281 -0.246,60.16 L-0.246,3.84 Z" id="Fill-392" fill="#FFFFFF" sketch:type="MSShapeGroup"></path>
<path d="M31.754,0 L31.754,9.6 C31.754,11.721 33.473,13.44 35.594,13.44 L47.754,13.44 L31.754,0 Z" id="Fill-393" fill="#F0F1F1" sketch:type="MSShapeGroup"></path>
<path d="M53.754,18.5 L58.754,8 L63.754,18.5 L63.754,57 L53.754,57 L53.754,18.5 Z" id="Fill-394" fill="#F4D0A1" sketch:type="MSShapeGroup"></path>
<path d="M63.754,24 L63.754,18.5 L58.754,8 L58.754,24 L63.754,24 Z" id="Fill-395" fill="#EDBC7C" sketch:type="MSShapeGroup"></path>
<path d="M63.754,61 C63.754,62.657 62.411,64 60.754,64 L56.754,64 C55.097,64 53.754,62.657 53.754,61 L53.754,59 L63.754,59 L63.754,61 Z" id="Fill-396" fill="#F89392" sketch:type="MSShapeGroup"></path>
<path d="M38.754,22 C38.754,22.55 38.304,23 37.754,23 L22.754,23 C22.204,23 21.754,22.55 21.754,22 C21.754,21.45 22.204,21 22.754,21 L37.754,21 C38.304,21 38.754,21.45 38.754,22" id="Fill-397" fill="#E2E4E5" sketch:type="MSShapeGroup"></path>
<path d="M32.754,27 C32.754,27.55 32.304,28 31.754,28 L22.754,28 C22.204,28 21.754,27.55 21.754,27 C21.754,26.45 22.204,26 22.754,26 L31.754,26 C32.304,26 32.754,26.45 32.754,27" id="Fill-398" fill="#E2E4E5" sketch:type="MSShapeGroup"></path>
<path d="M38.754,35 C38.754,35.55 38.304,36 37.754,36 L22.754,36 C22.204,36 21.754,35.55 21.754,35 C21.754,34.45 22.204,34 22.754,34 L37.754,34 C38.304,34 38.754,34.45 38.754,35" id="Fill-399" fill="#E2E4E5" sketch:type="MSShapeGroup"></path>
<path d="M32.754,40 C32.754,40.55 32.304,41 31.754,41 L22.754,41 C22.204,41 21.754,40.55 21.754,40 C21.754,39.45 22.204,39 22.754,39 L31.754,39 C32.304,39 32.754,39.45 32.754,40" id="Fill-400" fill="#E2E4E5" sketch:type="MSShapeGroup"></path>
<path d="M38.754,48 C38.754,48.55 38.304,49 37.754,49 L22.754,49 C22.204,49 21.754,48.55 21.754,48 C21.754,47.45 22.204,47 22.754,47 L37.754,47 C38.304,47 38.754,47.45 38.754,48" id="Fill-401" fill="#E2E4E5" sketch:type="MSShapeGroup"></path>
<path d="M32.754,53 C32.754,53.55 32.304,54 31.754,54 L22.754,54 C22.204,54 21.754,53.55 21.754,53 C21.754,52.45 22.204,52 22.754,52 L31.754,52 C32.304,52 32.754,52.45 32.754,53" id="Fill-402" fill="#E2E4E5" sketch:type="MSShapeGroup"></path>
<path d="M11.8213,29 L8.0473,25.226 C7.6563,24.835 7.6563,24.202 8.0473,23.812 C8.4373,23.421 9.0703,23.421 9.4613,23.812 L11.6873,26.037 L15.9853,20.878 C16.3393,20.454 16.9703,20.397 17.3943,20.75 C17.8183,21.104 17.8763,21.734 17.5223,22.159 L11.8213,29 Z" id="Fill-403" fill="#11B3A0" sketch:type="MSShapeGroup"></path>
<path d="M11.8213,42 L8.0473,38.226 C7.6563,37.835 7.6563,37.202 8.0473,36.812 C8.4373,36.421 9.0703,36.421 9.4613,36.812 L11.6873,39.037 L15.9853,33.878 C16.3393,33.454 16.9703,33.397 17.3943,33.75 C17.8183,34.104 17.8763,34.734 17.5223,35.159 L11.8213,42 Z" id="Fill-404" fill="#11B3A0" sketch:type="MSShapeGroup"></path>
<path d="M11.8213,55 L8.0473,51.226 C7.6563,50.835 7.6563,50.202 8.0473,49.812 C8.4373,49.421 9.0703,49.421 9.4613,49.812 L11.6873,52.037 L15.9853,46.878 C16.3393,46.454 16.9703,46.397 17.3943,46.75 C17.8183,47.104 17.8763,47.734 17.5223,48.159 L11.8213,55 Z" id="Fill-405" fill="#11B3A0" sketch:type="MSShapeGroup"></path>
<path d="M63.754,59 L53.754,59 L53.754,56 L63.754,56 L63.754,59 Z" id="Fill-406" fill="#FACB1B" sketch:type="MSShapeGroup"></path>
<path d="M58.754,59 L53.754,59 L53.754,57 L58.754,57 L58.754,59 Z" id="Fill-407" fill="#FBE158" sketch:type="MSShapeGroup"></path>
<path d="M58.7672,7.9844 L56.9102,11.8834 C57.4222,12.5624 58.0622,12.9844 58.7672,12.9844 C59.4722,12.9844 60.1122,12.5624 60.6242,11.8834 L58.7672,7.9844 Z" id="Fill-408" fill="#3E3E3F" sketch:type="MSShapeGroup"></path>
<path d="M61.254,20 C60.1,20 59.128,19.218 58.841,18.155 C58.814,18.058 58.694,18.058 58.667,18.155 C58.38,19.218 57.408,20 56.254,20 C55.202,20 54.301,19.35 53.932,18.429 C53.893,18.332 53.754,18.359 53.754,18.464 L53.754,57 L63.754,57 L63.754,18.464 C63.754,18.359 63.615,18.332 63.576,18.429 C63.207,19.35 62.306,20 61.254,20" id="Fill-409" fill="#0484AB" sketch:type="MSShapeGroup"></path>
<path d="M53.754,57 L53.754,18.493 C53.754,18.382 53.901,18.354 53.944,18.457 C54.319,19.363 55.212,20 56.254,20 C57.413,20 58.388,19.211 58.671,18.141 C58.685,18.089 58.754,18.095 58.754,18.15 L58.754,57 L53.754,57 Z" id="Fill-410" fill="#21B2D1" sketch:type="MSShapeGroup"></path>
<path d="M53.754,59 L53.754,61 C53.754,62.657 55.097,64 56.754,64 L58.754,64 L58.754,59 L53.754,59 Z" id="Fill-411" fill="#F8B4B4" sketch:type="MSShapeGroup"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,644 @@
/*
* Handles the Pages main page.
*/
+function ($) { "use strict";
var Base = $.oc.foundation.base,
BaseProto = Base.prototype
var PagesPage = function () {
Base.call(this)
this.init()
}
PagesPage.prototype = Object.create(BaseProto)
PagesPage.prototype.constructor = PagesPage
PagesPage.prototype.init = function() {
this.$masterTabs = $('#pages-master-tabs')
this.$sidePanel = $('#pages-side-panel')
this.$pageTree = $('[data-control=treeview]', this.$sidePanel)
this.masterTabsObj = this.$masterTabs.data('oc.tab')
this.snippetManager = new $.oc.pages.snippetManager(this.$masterTabs)
this.registerHandlers()
}
PagesPage.prototype.registerHandlers = function() {
// Item is clicked in the sidebar
$(document).on('open.oc.treeview', 'form.layout[data-content-id=pages]', this.proxy(this.onSidebarItemClick))
$(document).on('open.oc.list', this.$sidePanel, this.proxy(this.onSidebarItemClick))
// A tab is shown / switched
this.$masterTabs.on('shown.bs.tab', this.proxy(this.onTabShown))
// All master tabs are closed
this.$masterTabs.on('afterAllClosed.oc.tab', this.proxy(this.onAllTabsClosed))
// A master tab is closed
this.$masterTabs.on('closed.oc.tab', this.proxy(this.onTabClosed))
// AJAX errors in the master tabs area
$(document).on('ajaxError', '#pages-master-tabs form', this.proxy(this.onAjaxError))
// AJAX success in the master tabs area
$(document).on('ajaxSuccess', '#pages-master-tabs form', this.proxy(this.onAjaxSuccess))
// Before save a content block
$(document).on('oc.beforeRequest', '#pages-master-tabs form[data-object-type=content]', this.proxy(this.onBeforeSaveContent))
// Layout changed
$(document).on('change', '#pages-master-tabs form[data-object-type=page] select[name="viewBag[layout]"]', this.proxy(this.onLayoutChanged))
// Create object button click
$(document).on(
'click',
'#pages-side-panel form [data-control=create-object], #pages-side-panel form [data-control=create-template]',
this.proxy(this.onCreateObject)
)
// Submenu item is clicked in the sidebar
$(document).on('submenu.oc.treeview', 'form.layout[data-content-id=pages]', this.proxy(this.onSidebarSubmenuItemClick))
// The Delete Object button click
$(document).on('click', '#pages-side-panel form button[data-control=delete-object], #pages-side-panel form button[data-control=delete-template]',
this.proxy(this.onDeleteObject))
// A new tab is added to the editor
this.$masterTabs.on('initTab.oc.tab', this.proxy(this.onInitTab))
// Handle the menu saving
$(document).on('oc.beforeRequest', '#pages-master-tabs form[data-object-type=menu]', this.proxy(this.onSaveMenu))
}
/*
* Displays the concurrency resolution form.
*/
PagesPage.prototype.handleMtimeMismatch = function (form) {
var $form = $(form)
$form.popup({ handler: 'onOpenConcurrencyResolveForm' })
var popup = $form.data('oc.popup'),
self = this
$(popup.$container).on('click', 'button[data-action=reload]', function(){
popup.hide()
self.reloadForm($form)
})
$(popup.$container).on('click', 'button[data-action=save]', function(){
popup.hide()
$('input[name=objectForceSave]', $form).val(1)
$('a[data-request=onSave]', $form).trigger('click')
$('input[name=objectForceSave]', $form).val(0)
})
}
/*
* Reloads the Editor form.
*/
PagesPage.prototype.reloadForm = function($form) {
var data = {
type: $('[name=objectType]', $form).val(),
theme: $('[name=theme]', $form).val(),
path: $('[name=objectPath]', $form).val(),
},
tabId = data.type + '-' + data.theme + '-' + data.path,
tab = this.masterTabsObj.findByIdentifier(tabId),
self = this
/*
* Update tab
*/
$.oc.stripeLoadIndicator.show()
$form.request('onOpen', {
data: data
}).done(function(data) {
$.oc.stripeLoadIndicator.hide()
self.$masterTabs.ocTab('updateTab', tab, data.tabTitle, data.tab)
self.$masterTabs.ocTab('unmodifyTab', tab)
self.updateModifiedCounter()
}).always(function(){
$.oc.stripeLoadIndicator.hide()
})
}
/*
* Updates the sidebar counter
*/
PagesPage.prototype.updateModifiedCounter = function() {
var counters = {
page: {menu: 'pages', count: 0},
menu: {menu: 'menus', count: 0},
content: {menu: 'content', count: 0},
}
$('> div.tab-content > div.tab-pane[data-modified]', this.$masterTabs).each(function(){
var inputType = $('> form > input[name=objectType]', this).val()
counters[inputType].count++
})
$.each(counters, function(type, data){
$.oc.sideNav.setCounter('pages/' + data.menu, data.count);
})
}
/*
* Triggered when a tab is displayed. Updated the current selection in the sidebar and sets focus on an editor.
*/
PagesPage.prototype.onTabShown = function(e) {
var $tabControl = $(e.target).closest('[data-control=tab]')
if ($tabControl.attr('id') == 'pages-master-tabs') {
var dataId = $(e.target).closest('li').attr('data-tab-id'),
title = $(e.target).attr('title')
if (title)
this.setPageTitle(title)
this.$pageTree.treeView('markActive', dataId)
$('[data-control=filelist]', this.$sidePanel).fileList('markActive', dataId)
$(window).trigger('resize')
} else if ($tabControl.hasClass('secondary')) {
// TODO: Focus the code or rich editor here
}
}
/*
* Triggered when all master tabs are closed.
*/
PagesPage.prototype.onAllTabsClosed = function() {
this.$pageTree.treeView('markActive', null)
$('[data-control=filelist]', this.$sidePanel).fileList('markActive', null)
this.setPageTitle('')
}
/*
* Triggered when a master tab is closed.
*/
PagesPage.prototype.onTabClosed = function() {
this.updateModifiedCounter()
}
/*
* Handles AJAX errors in the master tab forms. Processes the mtime mismatch condition (concurrency).
*/
PagesPage.prototype.onAjaxError = function(event, context, message, data, jqXHR) {
if (context.handler != 'onSave')
return
if (jqXHR.responseText == 'mtime-mismatch') {
event.preventDefault()
this.handleMtimeMismatch(event.target)
}
}
/*
* Handles successful AJAX request in the master tab forms. Updates the UI elements and resets the mtime value.
*/
PagesPage.prototype.onAjaxSuccess = function(event, context, data) {
var $form = $(event.currentTarget),
$tabPane = $form.closest('.tab-pane')
// Update the visibilities of the commit & reset buttons
$('[data-control=commit-button]', $form).toggleClass('hide', !data.canCommit)
$('[data-control=reset-button]', $form).toggleClass('hide', !data.canReset)
if (data.objectPath !== undefined) {
$('input[name=objectPath]', $form).val(data.objectPath)
$('input[name=objectMtime]', $form).val(data.objectMtime)
$('[data-control=delete-button]', $form).removeClass('hide')
$('[data-control=preview-button]', $form).removeClass('hide')
if (data.pageUrl !== undefined)
$('[data-control=preview-button]', $form).attr('href', data.pageUrl)
}
if (data.tabTitle !== undefined) {
this.$masterTabs.ocTab('updateTitle', $tabPane, data.tabTitle)
this.setPageTitle(data.tabTitle)
}
var tabId = $('input[name=objectType]', $form).val() + '-'
+ $('input[name=theme]', $form).val() + '-'
+ $('input[name=objectPath]', $form).val();
this.$masterTabs.ocTab('updateIdentifier', $tabPane, tabId)
this.$pageTree.treeView('markActive', tabId)
$('[data-control=filelist]', this.$sidePanel).fileList('markActive', tabId)
var objectType = $('input[name=objectType]', $form).val()
if (objectType.length > 0 &&
(context.handler == 'onSave' || context.handler == 'onCommit' || context.handler == 'onReset')
)
this.updateObjectList(objectType)
if (context.handler == 'onSave' && (!data['X_OCTOBER_ERROR_FIELDS'] && !data['X_OCTOBER_ERROR_MESSAGE']))
$form.trigger('unchange.oc.changeMonitor')
// Reload the form if the server has requested it
if (data.forceReload) {
this.reloadForm($form)
}
}
PagesPage.prototype.onBeforeSaveContent = function(e, data) {
var form = e.currentTarget,
$tabPane = $(form).closest('.tab-pane')
this.updateContentEditorMode($tabPane, false)
}
PagesPage.prototype.onDeletePageSingle = function(el) {
var $el = $(el);
$el.request('onDelete', {
success: function(data) {
$.oc.pagesPage.closeTabs(data, 'page');
$.oc.pagesPage.updateObjectList('page');
$(this).trigger('close.oc.tab', [{force: true}]);
}
});
}
/*
* Updates the browser title when an object is saved.
*/
PagesPage.prototype.setPageTitle = function(title) {
$.oc.layout.setPageTitle(title.length ? (title + ' | ') : title)
}
/*
* Updates the sidebar object list.
*/
PagesPage.prototype.updateObjectList = function(objectType) {
var $form = $('form[data-object-type='+objectType+']', this.$sidePanel),
objectList = objectType + 'List',
self = this
$.oc.stripeLoadIndicator.show()
$form.request(objectList + '::onUpdate', {
complete: function(data) {
$('button[data-control=delete-object], button[data-control=delete-template]', $form).trigger('oc.triggerOn.update')
}
}).always(function(){
$.oc.stripeLoadIndicator.hide()
});
}
/*
* Closes deleted page tabs in the editor area.
*/
PagesPage.prototype.closeTabs = function(data, type) {
var self = this
$.each(data.deletedObjects, function(){
var tabId = type + '-' + data.theme + '-' + this,
tab = self.masterTabsObj.findByIdentifier(tabId)
$(tab).trigger('close.oc.tab', [{force: true}])
})
}
/*
* Triggered when an item is clicked in the sidebar. Opens the item in the editor.
* If the item is already opened, activate its tab in the editor.
*/
PagesPage.prototype.onSidebarItemClick = function(e) {
var self = this,
$item = $(e.relatedTarget),
$form = $item.closest('form'),
theme = $('input[name=theme]', $form).val(),
data = {
type: $form.data('object-type'),
theme: theme,
path: $item.data('item-path')
},
tabId = data.type + '-' + data.theme + '-' + data.path
if ($item.data('type') == 'snippet') {
this.snippetManager.onSidebarSnippetClick($item)
return
}
/*
* Find if the tab is already opened
*/
if (this.masterTabsObj.goTo(tabId))
return false
/*
* Open a new tab
*/
$.oc.stripeLoadIndicator.show()
$form.request('onOpen', {
data: data
}).done(function(data) {
self.$masterTabs.ocTab('addTab', data.tabTitle, data.tab, tabId, $form.data('type-icon'))
}).always(function() {
$.oc.stripeLoadIndicator.hide()
})
return false
}
/*
* Triggered when the Add button is clicked on the sidebar
*/
PagesPage.prototype.onCreateObject = function(e) {
var self = this,
$button = $(e.target),
$form = $button.closest('form'),
parent = $button.data('parent') !== undefined ? $button.data('parent') : null,
type = $form.data('object-type') ? $form.data('object-type') : $form.data('template-type'),
tabId = type + Math.random()
$.oc.stripeLoadIndicator.show()
$form.request('onCreateObject', {
data: {
type: type,
parent: parent
}
}).done(function(data){
self.$masterTabs.ocTab('addTab', data.tabTitle, data.tab, tabId, $form.data('type-icon') + ' new-template')
$('#layout-side-panel').trigger('close.oc.sidePanel')
self.setPageTitle(data.tabTitle)
}).always(function(){
$.oc.stripeLoadIndicator.hide()
})
e.stopPropagation()
return false
}
/*
* Triggered when an item is clicked in the sidebar submenu
*/
PagesPage.prototype.onSidebarSubmenuItemClick = function(e) {
if ($(e.clickEvent.target).data('control') == 'create-object')
this.onCreateObject(e.clickEvent)
return false
}
/*
* Triggered when the Delete button is clicked on the sidebar
*/
PagesPage.prototype.onDeleteObject = function(e) {
var $el = $(e.target),
$form = $el.closest('form'),
objectType = $form.data('object-type'),
self = this
if (!confirm($el.data('confirmation')))
return
$form.request('onDeleteObjects', {
data: {
type: objectType
},
success: function(data) {
$.each(data.deleted, function(index, path){
var tabId = objectType + '-' + data.theme + '-' + path,
tab = self.masterTabsObj.findByIdentifier(tabId)
self.$masterTabs.ocTab('closeTab', tab, true)
})
if (data.error !== undefined && $.type(data.error) === 'string' && data.error.length)
$.oc.flashMsg({text: data.error, 'class': 'error'})
},
complete: function() {
self.updateObjectList(objectType)
}
})
return false
}
/*
* Triggered when a static page layout changes
*/
PagesPage.prototype.onLayoutChanged = function(e) {
var
self = this,
$el = $(e.target),
$form = $el.closest('form'),
$pane = $form.closest('.tab-pane'),
data = {
type: $('[name=objectType]', $form).val(),
theme: $('[name=theme]', $form).val(),
path: $('[name=objectPath]', $form).val()
},
tab = $pane.data('tab')
// $form.trigger('unchange.oc.changeMonitor')
$form.changeMonitor('dispose')
$.oc.stripeLoadIndicator.show()
$form
.request('onUpdatePageLayout', {
data: data
})
.done(function(data){
self.$masterTabs.ocTab('updateTab', tab, data.tabTitle, data.tab)
})
.always(function(){
$.oc.stripeLoadIndicator.hide()
$('form:first', $pane).changeMonitor().trigger('change')
})
}
/*
* Triggered when a new tab is added to the Editor
*/
PagesPage.prototype.onInitTab = function(e, data) {
if ($(e.target).attr('id') != 'pages-master-tabs')
return
var $collapseIcon = $('<a href="javascript:;" class="tab-collapse-icon tabless"><i class="icon-chevron-up"></i></a>'),
$panel = $('.form-tabless-fields', data.pane),
$secondaryPanel = $('.control-tabs.secondary-tabs', data.pane),
$primaryPanel = $('.control-tabs.primary-tabs', data.pane),
hasSecondaryTabs = $secondaryPanel.length > 0
$secondaryPanel.addClass('secondary-content-tabs')
$panel.append($collapseIcon)
if (!hasSecondaryTabs) {
$('.primary-tabs').parent().removeClass('min-size')
}
$collapseIcon.click(function(){
$panel.toggleClass('collapsed')
if (typeof(localStorage) !== 'undefined')
localStorage.ocPagesTablessCollapsed = $panel.hasClass('collapsed') ? 1 : 0
window.setTimeout(function(){
$(window).trigger('oc.updateUi')
}, 500)
return false
})
var $primaryCollapseIcon = $('<a href="javascript:;" class="tab-collapse-icon primary"><i class="icon-chevron-down"></i></a>')
if ($primaryPanel.length > 0) {
$secondaryPanel.append($primaryCollapseIcon)
$primaryCollapseIcon.click(function(){
$primaryPanel.toggleClass('collapsed')
$secondaryPanel.toggleClass('primary-collapsed')
$(window).trigger('oc.updateUi')
if (typeof(localStorage) !== 'undefined')
localStorage.ocPagesPrimaryCollapsed = $primaryPanel.hasClass('collapsed') ? 1 : 0
return false
})
} else {
$secondaryPanel.addClass('primary-collapsed')
}
if (typeof(localStorage) !== 'undefined') {
if (!$('a', data.tab).hasClass('new-template') && localStorage.ocPagesTablessCollapsed == 1)
$panel.addClass('collapsed')
if (localStorage.ocPagesPrimaryCollapsed == 1 && hasSecondaryTabs) {
$primaryPanel.addClass('collapsed')
$secondaryPanel.addClass('primary-collapsed')
}
}
var $form = $('form', data.pane),
self = this,
$panel = $('.form-tabless-fields', data.pane)
this.updateContentEditorMode(data.pane, true)
$form.on('changed.oc.changeMonitor', function() {
$panel.trigger('modified.oc.tab')
$panel.find('[data-control=commit-button]').addClass('hide');
$panel.find('[data-control=reset-button]').addClass('hide');
self.updateModifiedCounter()
})
$form.on('unchanged.oc.changeMonitor', function() {
$panel.trigger('unmodified.oc.tab')
self.updateModifiedCounter()
})
}
/*
* Triggered before a menu is saved
*/
PagesPage.prototype.onSaveMenu = function(e, data) {
var form = e.currentTarget,
items = [],
$items = $('div[data-control=treeview] > ol > li', form)
var iterator = function(items) {
var result = []
$.each(items, function() {
var item = $(this).data('menu-item')
var $subitems = $('> ol >li', this)
if ($subitems.length)
item['items'] = iterator($subitems)
result.push(item)
})
return result
}
data.options.data['itemData'] = JSON.stringify(iterator($items))
}
/*
* Updates the content editor to correspond to the content file extension
*/
PagesPage.prototype.updateContentEditorMode = function(pane, initialization) {
if ($('[data-toolbar-type]', pane).data('toolbar-type') !== 'content')
return
var extension = this.getContentExtension(pane),
mode = 'html',
editor = $('[data-control=codeeditor]', pane)
if (extension == 'html')
extension = 'htm'
if (initialization)
$(pane).data('prev-extension', extension)
if (extension == 'htm') {
$('[data-field-name=markup]', pane).hide()
$('[data-field-name=markup_html]', pane).show()
if (!initialization && $(pane).data('prev-extension') != 'htm') {
var val = editor.codeEditor('getContent')
$('div[data-control=richeditor]', pane).richEditor('setContent', val)
}
}
else {
$('[data-field-name=markup]', pane).show()
$('[data-field-name=markup_html]', pane).hide()
if (!initialization && $(pane).data('prev-extension') == 'htm') {
var val = $('div[data-control=richeditor]', pane).richEditor('getContent')
editor.codeEditor('setContent', val)
}
var modes = $.oc.codeEditorExtensionModes
if (modes[extension] !== undefined)
mode = modes[extension];
var setEditorMode = function() {
window.setTimeout(function(){
editor.codeEditor('getEditorObject')
.getSession()
.setMode({ path: 'ace/mode/'+mode })
}, 200)
}
if (initialization)
editor.on('oc.codeEditorReady', setEditorMode)
else
setEditorMode()
}
if (!initialization)
$(pane).data('prev-extension', extension)
}
/*
* Returns the content file extension
*/
PagesPage.prototype.getContentExtension = function(form) {
var $input = $('input[name=fileName]', form),
fileName = $input.length ? $input.val() : '',
parts = fileName.split('.')
if (parts.length >= 2)
return parts.pop().toLowerCase()
return 'htm'
}
$(document).ready(function(){
$.oc.pagesPage = new PagesPage()
})
}(window.jQuery);

View File

@ -0,0 +1,213 @@
/*
* Handles snippet operations on the Pages main page
*/
+function ($) { "use strict";
if ($.oc.pages === undefined)
$.oc.pages = {}
var SnippetManager = function ($masterTabs) {
this.$masterTabs = $masterTabs
var self = this
$(document).on('hidden.oc.inspector', '.field-richeditor [data-snippet]', function() {
self.syncEditorCode(this)
})
$(document).on('init.oc.richeditor', '.field-richeditor textarea', function(ev, richeditor) {
self.initSnippets(richeditor.getElement())
})
$(document).on('setContent.oc.richeditor', '.field-richeditor textarea', function(ev, richeditor) {
if (!richeditor.isCodeViewActive()) {
self.initSnippets(richeditor.getElement())
}
})
$(document).on('syncContent.oc.richeditor', '.field-richeditor textarea', function(ev, richeditor, container) {
self.syncPageMarkup(ev, container)
})
$(document).on('figureKeydown.oc.richeditor', '.field-richeditor textarea', function(ev, originalEv, richeditor) {
self.editorKeyDown(ev, originalEv, richeditor)
})
$(document).on('click', '[data-snippet]', function() {
if ($(this).hasClass('inspector-open')) {
return
}
$.oc.inspector.manager.createInspector(this)
return false
})
}
SnippetManager.prototype.onSidebarSnippetClick = function($sidebarItem) {
var $pageForm = $('div.tab-content > .tab-pane.active form[data-object-type=page]', this.$masterTabs)
if (!$pageForm.length) {
$.oc.alert('Snippets can only be added to Pages. Please open or create a Page first.')
return
}
var $activeEditorTab = $('.control-tabs.secondary-tabs .tab-pane.active', $pageForm),
$textarea = $activeEditorTab.find('[data-control="richeditor"] textarea'),
$richeditorNode = $textarea.closest('[data-control="richeditor"]'),
$snippetNode = $('<figure contenteditable="false" data-inspector-css-class="hero">&nbsp;</figure>'),
componentClass = $sidebarItem.attr('data-component-class'),
snippetCode = $sidebarItem.data('snippet')
if (!$textarea.length) {
$.oc.alert('Snippets can only be added to page Content or HTML placeholders.')
return
}
if (componentClass) {
$snippetNode.attr({
'data-component': componentClass,
'data-inspector-class': componentClass
})
// If a component-based snippet was added, make sure that
// its code is unique, as it will be used as a component
// alias.
snippetCode = this.generateUniqueComponentSnippetCode(componentClass, snippetCode, $pageForm)
}
$snippetNode.attr({
'data-snippet': snippetCode,
'data-name': $sidebarItem.data('snippet-name'),
'tabindex': '0',
'draggable': 'true',
'data-ui-block': 'true'
})
$snippetNode.addClass('fr-draggable')
$richeditorNode.richEditor('insertUiBlock', $snippetNode)
}
SnippetManager.prototype.generateUniqueComponentSnippetCode = function(componentClass, originalCode, $pageForm) {
var updatedCode = originalCode,
counter = 1,
snippetFound = false
do {
snippetFound = false
$('[data-control="richeditor"] textarea', $pageForm).each(function(){
var $textarea = $(this),
$codeDom = $('<div>' + $textarea.val() + '</div>')
if ($codeDom.find('[data-snippet="'+updatedCode+'"][data-component]').length > 0) {
snippetFound = true
updatedCode = originalCode + counter
counter++
return false
}
})
} while (snippetFound)
return updatedCode
}
SnippetManager.prototype.syncEditorCode = function(inspectable) {
// Race condition
setTimeout(function() {
var $richeditor = $(inspectable).closest('[data-control=richeditor]')
$richeditor.richEditor('syncContent')
inspectable.focus()
}, 0)
}
SnippetManager.prototype.initSnippets = function($editor) {
var snippetCodes = []
$('.fr-view [data-snippet]', $editor).each(function(){
var $snippet = $(this),
snippetCode = $snippet.attr('data-snippet'),
componentClass = $snippet.attr('data-component')
if (componentClass)
snippetCode += '|' + componentClass
snippetCodes.push(snippetCode)
$snippet
.addClass('loading')
.addClass('fr-draggable')
.attr({
'data-inspector-css-class': 'hero',
'data-name': 'Loading...',
'data-ui-block': 'true',
'draggable': 'true',
'tabindex': '0'
})
.html('&nbsp;')
if (componentClass) {
$snippet.attr('data-inspector-class', componentClass)
}
this.contentEditable = false
})
if (snippetCodes.length > 0) {
var request = $editor.request('onGetSnippetNames', {
data: {
codes: snippetCodes
}
}).done(function(data) {
if (data.names !== undefined) {
$.each(data.names, function(code){
$('[data-snippet="'+code+'"]', $editor)
.attr('data-name', this)
.removeClass('loading')
})
}
})
}
}
SnippetManager.prototype.syncPageMarkup = function(ev, container) {
var $domTree = $('<div>'+container.html+'</div>')
$('[data-snippet]', $domTree).each(function(){
var $snippet = $(this)
$snippet.removeAttr('contenteditable data-name tabindex data-inspector-css-class data-inspector-class data-property-inspectorclassname data-property-inspectorproperty data-ui-block draggable')
$snippet.removeClass('fr-draggable fr-dragging')
if (!$snippet.attr('class')) {
$snippet.removeAttr('class')
}
})
container.html = $domTree.html()
}
SnippetManager.prototype.editorKeyDown = function(ev, originalEv, richeditor) {
if (richeditor.getTextarea() === undefined)
return
if (originalEv.target && $(originalEv.target).attr('data-snippet') !== undefined) {
this.snippetKeyDown(originalEv, originalEv.target)
}
}
SnippetManager.prototype.snippetKeyDown = function(ev, snippet) {
if (ev.which == 32) {
switch (ev.which) {
case 32:
// Space key
$.oc.inspector.manager.createInspector(snippet)
break
}
}
}
$.oc.pages.snippetManager = SnippetManager
}(window.jQuery);

View File

@ -0,0 +1,204 @@
@import "../../../../../modules/backend/assets/less/core/boot.less";
.control-treeview {
ol > li.dragged div,
ol >li > div:hover {
background-color: #4ea5e0 !important
}
ol >li > div:active {
background-color: #3498db !important;
}
}
.control-filelist.menu-list {
li {
> a {
position: relative;
&:before {
position: absolute;
width: 18px;
height: 18px;
left: 17px;
top: 18px;
content: ' ';
background-image: url(../images/menu-icons.png);
background-position: 0 0;
background-repeat: no-repeat;
background-size: 36px auto;
}
&:hover {
&:before {
background-position: 0 -60px;
}
}
}
}
}
.control-filelist.content {
li > a {
position: relative;
&:before {
position: absolute;
width: 18px;
height: 22px;
left: 18px;
top: 10px;
content: ' ';
background-image: url(../images/content-icons.png);
background-position: 0 0;
background-repeat: no-repeat;
background-size: 34px auto;
}
&:hover {
&:before {
background-position: 0 -27px;
}
}
}
li.group ul li > a:before {
left: 34px;
}
}
.page-snippet-icon() {
position: absolute;
width: 17px;
height: 19px;
left: 18px;
top: 13px;
content: ' ';
background-image: url(../images/snippet-icons.png);
background-position: 0 0;
background-repeat: no-repeat;
background-size: 34px auto;
}
.control-filelist.snippet-list {
li > a {
position: relative;
color: #808c8d;
&:before {
.page-snippet-icon();
left: 18px;
top: 17px;
}
&:hover {
&:before {
background-position: 0 -21px;
}
}
}
li.group ul li > a:before {
left: 34px;
}
}
@media only screen and (-moz-min-device-pixel-ratio: 1.5), only screen and (-o-min-device-pixel-ratio: 3/2), only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-devicepixel-ratio: 1.5), only screen and (min-resolution: 1.5dppx) {
.control-filelist.menu-list {
li {
> a {
&:before {
background-position: 0px -11px;
background-size: 18px auto;
}
&:hover {
&:before {
background-position: 0px -40px;
}
}
}
}
}
.control-filelist.content {
li {
a {
&:before {
background-position: 0px -27px;
background-size: 17px auto;
}
&:hover {
&:before {
background-position: 0px -52px;
}
}
}
}
}
.control-filelist.snippet-list {
li {
a {
&:before {
background-position: 0px -21px;
background-size: 17px auto;
}
&:hover {
&:before {
background-position: 0px -41px;
}
}
}
}
}
}
.fancy-layout {
.pagesTextEditor {
border-left: 1px solid @color-form-field-border!important;
}
}
.control-richeditor {
[data-snippet] {
&:before {
content: attr(data-name);
}
&:after {
.page-snippet-icon();
left: 11px;
top: 12px;
}
&.loading:after {
background-image:url(../../../../../modules/system/assets/ui/images/loader-transparent.svg);
background-size: 15px 15px;
background-position: 50% 50%;
position: absolute;
width: 15px;
height: 15px;
top: 13px;
content: ' ';
.animation(spin 1s linear infinite);
}
}
}
@media only screen and (-moz-min-device-pixel-ratio: 1.5), only screen and (-o-min-device-pixel-ratio: 3/2), only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-devicepixel-ratio: 1.5), only screen and (min-resolution: 1.5dppx) {
.control-richeditor [data-snippet]:after {
background-position: 0px -21px;
background-size: 17px auto;
}
}

View File

@ -0,0 +1,35 @@
<?php namespace RainLab\Pages\Classes;
use Cms\Classes\Content as ContentBase;
/**
* Represents a content template.
*
* @package rainlab\pages
* @author Alexey Bobkov, Samuel Georges
*/
class Content extends ContentBase
{
public $implement = ['@RainLab.Translate.Behaviors.TranslatableCmsObject'];
/**
* @var array Attributes that support translation, if available.
*/
public $translatable = [
'markup'
];
public $translatableModel = 'RainLab\Translate\Classes\MLContent';
/**
* Converts the content object file name in to something nicer
* for humans to read.
* @return string
*/
public function getNiceTitleAttribute()
{
$title = basename($this->getBaseFileName());
$title = ucwords(str_replace(['-', '_'], ' ', $title));
return $title;
}
}

View File

@ -0,0 +1,123 @@
<?php namespace RainLab\Pages\Classes;
use Lang;
use Cms\Classes\Page as CmsPage;
use Cms\Classes\Theme;
use Cms\Classes\Layout;
use Cms\Classes\CmsException;
use October\Rain\Parse\Syntax\Parser as SyntaxParser;
use Exception;
/**
* Represents a static page controller.
*
* @package rainlab\pages
* @author Alexey Bobkov, Samuel Georges
*/
class Controller
{
use \October\Rain\Support\Traits\Singleton;
protected $theme;
/**
* Initialize this singleton.
*/
protected function init()
{
$this->theme = Theme::getActiveTheme();
if (!$this->theme) {
throw new CmsException(Lang::get('cms::lang.theme.active.not_found'));
}
}
/**
* Creates a CMS page from a static page and configures it.
* @param string $url Specifies the static page URL.
* @return \Cms\Classes\Page Returns the CMS page object or NULL of the requested page was not found.
*/
public function initCmsPage($url)
{
$router = new Router($this->theme);
$page = $router->findByUrl($url);
if (!$page) {
return null;
}
$viewBag = $page->viewBag;
$cmsPage = CmsPage::inTheme($this->theme);
$cmsPage->url = $url;
$cmsPage->apiBag['staticPage'] = $page;
/*
* Transfer specific values from the content view bag to the page settings object.
*/
$viewBagToSettings = ['title', 'layout', 'meta_title', 'meta_description', 'is_hidden'];
foreach ($viewBagToSettings as $property) {
$cmsPage->settings[$property] = array_get($viewBag, $property);
}
// Transer page ID to CMS page
$cmsPage->settings['id'] = $page->getId();
return $cmsPage;
}
public function injectPageTwig($page, $loader, $twig)
{
if (!isset($page->apiBag['staticPage'])) {
return;
}
$staticPage = $page->apiBag['staticPage'];
CmsException::mask($staticPage, 400);
$loader->setObject($staticPage);
$template = $twig->loadTemplate($staticPage->getFilePath());
$template->render([]);
CmsException::unmask();
}
public function getPageContents($page)
{
if (!isset($page->apiBag['staticPage'])) {
return;
}
return $page->apiBag['staticPage']->getProcessedMarkup();
}
public function getPlaceholderContents($page, $placeholderName, $placeholderContents)
{
if (!isset($page->apiBag['staticPage'])) {
return;
}
return $page->apiBag['staticPage']->getProcessedPlaceholderMarkup($placeholderName, $placeholderContents);
}
public function initPageComponents($cmsController, $page)
{
if (!isset($page->apiBag['staticPage'])) {
return;
}
$page->apiBag['staticPage']->initCmsComponents($cmsController);
}
public function parseSyntaxFields($content)
{
try {
return SyntaxParser::parse($content, [
'varPrefix' => 'extraData.',
'tagPrefix' => 'page:'
])->toTwig();
}
catch (Exception $ex) {
return $content;
}
}
}

View File

@ -0,0 +1,300 @@
<?php namespace RainLab\Pages\Classes;
use Url;
use Event;
use Request;
use SystemException;
use Cms\Classes\Meta;
use October\Rain\Support\Str;
/**
* Represents a front-end menu.
*
* @package rainlab\pages
* @author Alexey Bobkov, Samuel Georges
*/
class Menu extends Meta
{
/**
* @var string The container name associated with the model, eg: pages.
*/
protected $dirName = 'meta/menus';
/**
* @var array The attributes that are mass assignable.
*/
protected $fillable = [
'content',
'code',
'name',
'itemData',
];
/**
* @var array List of attribute names which are not considered "settings".
*/
protected $purgeable = [
'code',
];
/**
* @var array The rules to be applied to the data.
*/
public $rules = [
'code' => 'required|regex:/^[0-9a-z\-\_]+$/i',
];
/**
* @var array The array of custom error messages.
*/
public $customMessages = [
'required' => 'rainlab.pages::lang.menu.code_required',
'regex' => 'rainlab.pages::lang.menu.invalid_code',
];
/**
* Returns the menu code.
* @return string
*/
public function getCodeAttribute()
{
return $this->getBaseFileName();
}
/**
* Sets the menu code.
* @param string $code Specifies the file code.
* @return \Cms\Classes\CmsObject Returns the object instance.
*/
public function setCodeAttribute($code)
{
$code = trim($code);
if (strlen($code)) {
$this->fileName = $code.'.yaml';
$this->attributes = array_merge($this->attributes, ['code' => $code]);
}
return $this;
}
/**
* Returns a default value for items attribute.
* Items are objects of the \RainLab\Pages\Classes\MenuItem class.
* @return array
*/
public function getItemsAttribute()
{
$items = [];
if (!empty($this->attributes['items'])) {
$items = MenuItem::initFromArray($this->attributes['items']);
}
return $items;
}
/**
* Store the itemData in the items attribute
*
* @param array $data
* @return void
*/
public function setItemDataAttribute($data)
{
$this->items = $data;
return $this;
}
/**
* Processes the content attribute to an array of menu data.
* @return array|null
*/
protected function parseContent()
{
$parsedData = parent::parseContent();
if (!array_key_exists('name', $parsedData)) {
throw new SystemException(sprintf('The content of the %s file is invalid: the name element is not found.', $this->fileName));
}
return $parsedData;
}
/**
* Initializes a cache item.
* @param array &$item The cached item array.
*/
public static function initCacheItem(&$item)
{
$obj = new static($item);
$item['name'] = $obj->name;
$item['items'] = $obj->items;
}
/**
* Returns the menu item references.
* This function is used on the front-end.
* @param Cms\Classes\Page $page The current page object.
* @return array Returns an array of the \RainLab\Pages\Classes\MenuItemReference objects.
*/
public function generateReferences($page)
{
$currentUrl = Request::path();
if (!strlen($currentUrl)) {
$currentUrl = '/';
}
$currentUrl = Str::lower(Url::to($currentUrl));
$activeMenuItem = $page->activeMenuItem ?: false;
$iterator = function($items) use ($currentUrl, &$iterator, $activeMenuItem) {
$result = [];
foreach ($items as $item) {
$parentReference = new MenuItemReference;
$parentReference->type = $item->type;
$parentReference->title = $item->title;
$parentReference->code = $item->code;
$parentReference->viewBag = $item->viewBag;
/*
* If the item type is URL, assign the reference the item's URL and compare the current URL with the item URL
* to determine whether the item is active.
*/
if ($item->type == 'url') {
$parentReference->url = $item->url;
$parentReference->isActive = $currentUrl == Str::lower(Url::to($item->url)) || $activeMenuItem === $item->code;
}
else {
/*
* If the item type is not URL, use the API to request the item type's provider to
* return the item URL, subitems and determine whether the item is active.
*/
$apiResult = Event::fire('pages.menuitem.resolveItem', [$item->type, $item, $currentUrl, $this->theme]);
if (is_array($apiResult)) {
foreach ($apiResult as $itemInfo) {
if (!is_array($itemInfo)) {
continue;
}
if (!$item->replace && isset($itemInfo['url'])) {
$parentReference->url = $itemInfo['url'];
$parentReference->isActive = $itemInfo['isActive'] || $activeMenuItem === $item->code;
}
if (isset($itemInfo['items'])) {
$itemIterator = function($items) use (&$itemIterator, $parentReference) {
$result = [];
foreach ($items as $item) {
$reference = new MenuItemReference;
$reference->type = isset($item['type']) ? $item['type'] : null;
$reference->title = isset($item['title']) ? $item['title'] : '--no title--';
$reference->url = isset($item['url']) ? $item['url'] : '#';
$reference->isActive = isset($item['isActive']) ? $item['isActive'] : false;
$reference->viewBag = isset($item['viewBag']) ? $item['viewBag'] : [];
$reference->code = isset($item['code']) ? $item['code'] : null;
if (!strlen($parentReference->url)) {
$parentReference->url = $reference->url;
$parentReference->isActive = $reference->isActive;
}
if (isset($item['items'])) {
$reference->items = $itemIterator($item['items']);
}
$result[] = $reference;
}
return $result;
};
$parentReference->items = $itemIterator($itemInfo['items']);
}
}
}
}
if ($item->items) {
$parentReference->items = $iterator($item->items);
}
if (!$item->replace) {
$result[] = $parentReference;
}
else {
foreach ($parentReference->items as $subItem) {
$result[] = $subItem;
}
}
}
return $result;
};
$items = $iterator($this->items);
/*
* Populate the isChildActive property
*/
$hasActiveChild = function($items) use (&$hasActiveChild) {
foreach ($items as $item) {
if ($item->isActive) {
return true;
}
$result = $hasActiveChild($item->items);
if ($result) {
return $result;
}
}
};
$iterator = function($items) use (&$iterator, &$hasActiveChild) {
foreach ($items as $item) {
$item->isChildActive = $hasActiveChild($item->items);
$iterator($item->items);
}
};
$iterator($items);
/*
* @event pages.menu.referencesGenerated
* Provides opportunity to dynamically change menu entries right after reference generation.
*
* For example you can use it to filter menu entries for user groups from RainLab.User
* Before doing so you have to add custom field 'group' to menu viewBag using backend.form.extendFields event
* where the group can be selected by the user. See how to do this here:
* https://octobercms.com/docs/backend/forms#extend-form-fields
*
* Parameter provided is `$items` - a collection of the MenuItemReference objects passed by reference
*
* For example to hide entries where group is not 'registered' you can use the following code. It can
* be used to show different menus for different user groups.
*
* Event::listen('pages.menu.referencesGenerated', function (&$items) {
* $iterator = function ($menuItems) use (&$iterator, $clusterRepository) {
* $result = [];
* foreach ($menuItems as $item) {
* if (isset($item->viewBag['group']) && $item->viewBag['group'] !== "registered") {
* $item->viewBag['isHidden'] = "1";
* }
* if ($item->items) {
* $item->items = $iterator($item->items);
* }
* $result[] = $item;
* }
* return $result;
* };
* $items = $iterator($items);
* });
*/
Event::fire('pages.menu.referencesGenerated', [&$items]);
return $items;
}
}

View File

@ -0,0 +1,214 @@
<?php namespace RainLab\Pages\Classes;
use ApplicationException;
use Validator;
use Lang;
use Event;
/**
* Represents a menu item.
* This class is used in the back-end for managing the menu items.
* On the front-end items are represented with the
* \RainLab\Pages\Classes\MenuItemReference objects.
*
* @package rainlab\pages
* @author Alexey Bobkov, Samuel Georges
*/
class MenuItem
{
/**
* @var string Specifies the menu title
*/
public $title;
/**
* @var array Specifies the item subitems
*/
public $items = [];
/**
* @var string Specifies the parent menu item.
* An object of the RainLab\Pages\Classes\MenuItem class or null.
*/
public $parent;
/**
* @var boolean Determines whether the auto-generated menu items could have subitems.
*/
public $nesting;
/**
* @var string Specifies the menu item type - URL, static page, etc.
*/
public $type = 'url';
/**
* @var string Specifies the URL for URL-type items.
*/
public $url;
/**
* @var string Specifies the menu item code.
*/
public $code;
/**
* @var string Specifies the object identifier the item refers to.
* The identifier could be the database identifier or an object code.
*/
public $reference;
/**
* @var boolean Indicates that generated items should replace this item.
*/
public $replace;
/**
* @var string Specifies the CMS page path to resolve dynamic menu items to.
*/
public $cmsPage;
/**
* @var boolean Used by the system internally.
*/
public $exists = false;
public $fillable = [
'title',
'nesting',
'type',
'url',
'code',
'reference',
'cmsPage',
'replace',
'viewBag'
];
/**
* @var array Contains the view bag properties.
* This property is used by the menu editor internally.
*/
public $viewBag = [];
/**
* Initializes a menu item from a data array.
* @param array $items Specifies the menu item data.
* @return Returns an array of the MenuItem objects.
*/
public static function initFromArray($items)
{
$result = [];
foreach ($items as $itemData) {
$obj = new self;
foreach ($itemData as $name => $value) {
if ($name != 'items') {
if (property_exists($obj, $name)) {
$obj->$name = $value;
}
}
else {
$obj->items = self::initFromArray($value);
}
}
$result[] = $obj;
}
return $result;
}
/**
* Returns a list of registered menu item types
* @return array Returns an array of registered item types
*/
public function getTypeOptions($keyValue = null)
{
/*
* Baked in types
*/
$result = [
'url' => 'URL',
'header' => 'Header',
];
$apiResult = Event::fire('pages.menuitem.listTypes');
if (is_array($apiResult)) {
foreach ($apiResult as $typeList) {
if (!is_array($typeList)) {
continue;
}
foreach ($typeList as $typeCode => $typeName) {
$result[$typeCode] = $typeName;
}
}
}
return $result;
}
public function getCmsPageOptions($keyValue = null)
{
return []; // CMS Pages are loaded client-side
}
public function getReferenceOptions($keyValue = null)
{
return []; // References are loaded client-side
}
public static function getTypeInfo($type)
{
$result = [];
$apiResult = Event::fire('pages.menuitem.getTypeInfo', [$type]);
if (is_array($apiResult)) {
foreach ($apiResult as $typeInfo) {
if (!is_array($typeInfo)) {
continue;
}
foreach ($typeInfo as $name => $value) {
if ($name == 'cmsPages') {
$cmsPages = [];
foreach ($value as $page) {
$baseName = $page->getBaseFileName();
$pos = strrpos($baseName, '/');
$dir = $pos !== false ? substr($baseName, 0, $pos).' / ' : null;
$cmsPages[$baseName] = strlen($page->title)
? $dir.$page->title
: $baseName;
}
$value = $cmsPages;
}
$result[$name] = $value;
}
}
}
return $result;
}
/**
* Converts the menu item data to an array
* @return array Returns the menu item data as array
*/
public function toArray()
{
$result = [];
foreach ($this->fillable as $property) {
$result[$property] = $this->$property;
}
return $result;
}
}

View File

@ -0,0 +1,58 @@
<?php namespace RainLab\Pages\Classes;
use ApplicationException;
use Validator;
use Lang;
use Event;
/**
* Represents a front-end menu item.
* This class is used on the front-end.
* In the back-end items are represented with the
* \RainLab\Pages\Classes\MenuItem objects.
*
* @package rainlab\pages
* @author Alexey Bobkov, Samuel Georges
*/
class MenuItemReference
{
/**
* @var string Specifies the menu item type.
*/
public $type;
/**
* @var string Specifies the item title
*/
public $title;
/**
* @var string Specifies the item URL
*/
public $url;
/**
* @var string Specifies the menu item code.
*/
public $code;
/**
* @var string Indicates whether the item corresponds the currently viewed page.
*/
public $isActive = false;
/**
* @var string Indicates whether an item subitem corresponds the currently viewed page.
*/
public $isChildActive = false;
/**
* @var array Specifies the item subitems
*/
public $items = [];
/**
* @var array Specifies the item custom view bag properties.
*/
public $viewBag = [];
}

View File

@ -0,0 +1,962 @@
<?php namespace RainLab\Pages\Classes;
use Cms;
use File;
use Lang;
use Cache;
use Event;
use Route;
use Config;
use Validator;
use RainLab\Pages\Classes\Router;
use RainLab\Pages\Classes\Snippet;
use RainLab\Pages\Classes\PageList;
use Cms\Classes\Theme;
use Cms\Classes\Layout;
use Cms\Classes\Content as ContentBase;
use Cms\Classes\ComponentManager;
use System\Helpers\View as ViewHelper;
use October\Rain\Support\Str;
use October\Rain\Router\Helper as RouterHelper;
use October\Rain\Parse\Bracket as TextParser;
use October\Rain\Parse\Syntax\Parser as SyntaxParser;
use ApplicationException;
use Twig\Node\Node as TwigNode;
/**
* Represents a static page.
*
* @package rainlab\pages
* @author Alexey Bobkov, Samuel Georges
*/
class Page extends ContentBase
{
public $implement = [
'@RainLab.Translate.Behaviors.TranslatablePageUrl',
'@RainLab.Translate.Behaviors.TranslatableCmsObject'
];
/**
* @var string The container name associated with the model, eg: pages.
*/
protected $dirName = 'content/static-pages';
/**
* @var bool Wrap code section in PHP tags.
*/
protected $wrapCode = false;
/**
* @var array Properties that can be set with fill()
*/
protected $fillable = [
'markup',
'settings',
'placeholders',
];
/**
* @var array List of attribute names which are not considered "settings".
*/
protected $purgeable = ['parsedMarkup', 'placeholders'];
/**
* @var array The rules to be applied to the data.
*/
public $rules = [
'title' => 'required',
'url' => ['required', 'regex:/^\/[a-z0-9\/_\-\.]*$/i', 'uniqueUrl']
];
/**
* @var array The array of custom attribute names.
*/
public $attributeNames = [
'title' => 'title',
'url' => 'url',
];
/**
* @var array Attributes that support translation, if available.
*/
public $translatable = [
'code',
'markup',
'viewBag[title]',
'viewBag[meta_title]',
'viewBag[meta_description]',
];
/**
* @var string Translation model used for translation, if available.
*/
public $translatableModel = 'RainLab\Translate\Classes\MLStaticPage';
/**
* @var string Contains the page parent file name.
* This property is used by the page editor internally.
*/
public $parentFileName;
protected static $menuTreeCache = null;
protected $parentCache = null;
protected $childrenCache = null;
protected $processedMarkupCache = false;
protected $processedBlockMarkupCache = [];
/**
* Creates an instance of the object and associates it with a CMS theme.
* @param array $attributes
*/
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
$this->customMessages = [
'url.regex' => 'rainlab.pages::lang.page.invalid_url',
'url.unique_url' => 'rainlab.pages::lang.page.url_not_unique',
];
}
//
// CMS Object
//
/**
* Sets the object attributes.
* @param array $attributes A list of attributes to set.
*/
public function fill(array $attributes)
{
parent::fill($attributes);
/*
* When the page is saved, copy setting properties to the view bag.
* This is required for the back-end editors.
*/
if (array_key_exists('settings', $attributes) && array_key_exists('viewBag', $attributes['settings'])) {
$this->getViewBag()->setProperties($attributes['settings']['viewBag']);
$this->fillViewBagArray();
}
}
/**
* Returns the attributes used for validation.
* @return array
*/
protected function getValidationAttributes()
{
return $this->getAttributes() + $this->viewBag;
}
/**
* Validates the object properties.
* Throws a ValidationException in case of an error.
*/
public function beforeValidate()
{
$pages = Page::listInTheme($this->theme, true);
Validator::extend('uniqueUrl', function($attribute, $value, $parameters) use ($pages) {
$value = trim(strtolower($value));
foreach ($pages as $existingPage) {
if (
$existingPage->getBaseFileName() !== $this->getBaseFileName() &&
strtolower($existingPage->getViewBag()->property('url')) == $value
) {
return false;
}
}
return true;
});
}
/**
* Triggered before a new object is saved.
*/
public function beforeCreate()
{
$this->fileName = $this->generateFilenameFromCode();
}
/**
* Triggered after a new object is saved.
*/
public function afterCreate()
{
$this->appendToMeta();
}
/**
* Adds this page to the meta index.
*/
protected function appendToMeta()
{
$pageList = new PageList($this->theme);
$pageList->appendPage($this);
}
/*
* Generate a file name based on the URL
*/
protected function generateFilenameFromCode()
{
$dir = rtrim($this->getFilePath(''), '/');
$fileName = trim(str_slug(str_replace('/', '-', $this->getViewBag()->property('url')), '-'));
if (strlen($fileName) > 200) {
$fileName = substr($fileName, 0, 200);
}
if (!strlen($fileName)) {
$fileName = 'index';
}
$curName = trim($fileName).'.htm';
$counter = 2;
while (File::exists($dir.'/'.$curName)) {
$curName = $fileName.'-'.$counter.'.htm';
$counter++;
}
return $curName;
}
/**
* Deletes the object from the disk.
* Recursively deletes subpages. Returns a list of file names of deleted pages.
* @return array
*/
public function delete()
{
$result = [];
/*
* Delete subpages
*/
foreach ($this->getChildren() as $subPage) {
$result = array_merge($result, $subPage->delete());
}
/*
* Delete the object
*/
$result = array_merge($result, [$this->getBaseFileName()]);
parent::delete();
/*
* Remove from meta
*/
$this->removeFromMeta();
return $result;
}
/**
* Removes this page to the meta index.
*/
protected function removeFromMeta()
{
$pageList = new PageList($this->theme);
$pageList->removeSubtree($this);
}
//
// Public API
//
/**
* Helper that makes a URL for a static page in the active theme.
*
* Guide for the page reference:
* - chairs -> content/static-pages/chairs.htm
*
* @param mixed $page Specifies the Content file name.
* @return string
*/
public static function url($name)
{
if (empty($name) || !$page = static::find($name)) {
return null;
}
$url = array_get($page->attributes, 'viewBag.url');
return Cms::url($url);
}
/**
* Determine the default layout for a new page
* @param \RainLab\Pages\Classes\Page $parentPage
*/
public function setDefaultLayout($parentPage)
{
// Check parent page for a defined child layout
if ($parentPage && $parentPage->layout) {
$layout = Layout::load($this->theme, $parentPage->layout);
$component = $layout ? $layout->getComponent('staticPage') : null;
$childLayoutName = $component ? $component->property('childLayout', null) : null;
if ($childLayoutName) {
$this->getViewBag()->setProperty('layout', $childLayoutName);
$this->fillViewBagArray();
return;
}
}
// Check theme layouts for one marked as the default
foreach (Layout::listInTheme($this->theme) as $layout) {
$component = $layout->getComponent('staticPage');
if ($component && $component->property('default', false)) {
$this->getViewBag()->setProperty('layout', $layout->getBaseFileName());
$this->fillViewBagArray();
return;
}
}
}
//
// Getters
//
/**
* Returns the parent page that belongs to this one, or null.
* @return mixed
*/
public function getParent()
{
if ($this->parentCache !== null) {
return $this->parentCache;
}
$pageList = new PageList($this->theme);
$parent = null;
if ($fileName = $pageList->getPageParent($this)) {
$parent = static::load($this->theme, $fileName);
}
return $this->parentCache = $parent;
}
/**
* Returns all the child pages that belong to this one.
* @return array
*/
public function getChildren()
{
if ($this->childrenCache !== null) {
return $this->childrenCache;
}
$children = [];
$pageList = new PageList($this->theme);
$subtree = $pageList->getPageSubTree($this);
foreach ($subtree as $fileName => $subPages) {
$subPage = static::load($this->theme, $fileName);
if ($subPage) {
$children[] = $subPage;
}
}
return $this->childrenCache = $children;
}
/**
* Returns a list of layouts available in the theme.
* This method is used by the form widget.
* @return array Returns an array of strings.
*/
public function getLayoutOptions()
{
$result = [];
$layouts = Layout::listInTheme($this->theme, true);
foreach ($layouts as $layout) {
if (!$layout->hasComponent('staticPage')) {
continue;
}
$baseName = $layout->getBaseFileName();
$result[$baseName] = strlen($layout->description) ? $layout->description : $baseName;
}
if (!$result) {
$result[null] = Lang::get('rainlab.pages::lang.page.layouts_not_found');
}
return $result;
}
/**
* Looks up the Layout Cms object for this page.
* @return Cms\Classes\Layout
*/
public function getLayoutObject()
{
$viewBag = $this->getViewBag();
$layout = $viewBag->property('layout');
if (!$layout) {
$layouts = $this->getLayoutOptions();
$layout = count($layouts) ? array_keys($layouts)[0] : null;
}
if (!$layout) {
return null;
}
$layout = Layout::load($this->theme, $layout);
if (!$layout) {
return null;
}
return $layout;
}
/**
* Returns the Twig content string
*/
public function getTwigContent()
{
return $this->code;
}
//
// Syntax field processing
//
public function listLayoutSyntaxFields()
{
if (!$layout = $this->getLayoutObject()) {
return [];
}
$syntax = SyntaxParser::parse($layout->markup, ['tagPrefix' => 'page:']);
$result = $syntax->toEditor();
return $result;
}
//
// Placeholder processing
//
/**
* Returns information about placeholders defined in the page layout.
* @return array Returns an associative array of the placeholder name and codes.
*/
public function listLayoutPlaceholders()
{
if (!$layout = $this->getLayoutObject()) {
return [];
}
$result = [];
$bodyNode = $layout->getTwigNodeTree()->getNode('body')->getNode(0);
$nodes = $this->flattenTwigNode($bodyNode);
foreach ($nodes as $node) {
if (!$node instanceof \Cms\Twig\PlaceholderNode) {
continue;
}
$title = $node->hasAttribute('title') ? trim($node->getAttribute('title')) : null;
if (!strlen($title)) {
$title = $node->getAttribute('name');
}
$type = $node->hasAttribute('type') ? trim($node->getAttribute('type')) : null;
$ignore = $node->hasAttribute('ignore') ? trim($node->getAttribute('ignore')) : false;
$placeholderInfo = [
'title' => $title,
'type' => $type ?: 'html',
'ignore' => $ignore
];
$result[$node->getAttribute('name')] = $placeholderInfo;
}
return $result;
}
/**
* Recursively flattens a twig node and children
* @param $node
* @return array A flat array of twig nodes
*/
protected function flattenTwigNode($node)
{
$result = [];
if (!$node instanceof TwigNode) {
return $result;
}
foreach ($node as $subNode) {
$flatNodes = $this->flattenTwigNode($subNode);
$result = array_merge($result, [$subNode], $flatNodes);
}
return $result;
}
/**
* Parses the page placeholder {% put %} tags and extracts the placeholder values.
* @return array Returns an associative array of the placeholder names and values.
*/
public function getPlaceholdersAttribute()
{
if (!strlen($this->code)) {
return [];
}
if ($placeholders = array_get($this->attributes, 'placeholders')) {
return $placeholders;
}
$bodyNode = $this->getTwigNodeTree($this->code)->getNode('body')->getNode(0);
if ($bodyNode instanceof \Cms\Twig\PutNode) {
$bodyNode = [$bodyNode];
}
$result = [];
foreach ($bodyNode as $node) {
if (!$node instanceof \Cms\Twig\PutNode) {
continue;
}
$bodyNode = $node->getNode('body');
$result[$node->getAttribute('name')] = trim($bodyNode->getAttribute('data'));
}
$this->attributes['placeholders'] = $result;
return $result;
}
/**
* Takes an array of placeholder data (key: code, value: content) and renders
* it as a single string of Twig markup against the "code" attribute.
* @param array $value
* @return void
*/
public function setPlaceholdersAttribute($value)
{
if (!is_array($value)) {
return;
}
// Prune any attempt at setting a placeholder that
// is not actually defined by this pages layout.
$placeholders = array_intersect_key($value, $this->listLayoutPlaceholders());
$result = '';
foreach ($placeholders as $code => $content) {
if (!strlen(trim($content))) {
continue;
}
$result .= '{% put '.$code.' %}'.PHP_EOL;
$result .= $content.PHP_EOL;
$result .= '{% endput %}'.PHP_EOL;
$result .= PHP_EOL;
}
$this->attributes['code'] = trim($result);
$this->attributes['placeholders'] = $placeholders;
}
public function getProcessedMarkup()
{
if ($this->processedMarkupCache !== false) {
return $this->processedMarkupCache;
}
/*
* Process snippets
*/
$markup = Snippet::processPageMarkup(
$this->getFileName(),
$this->theme,
$this->markup
);
/*
* Inject global view variables
*/
$globalVars = ViewHelper::getGlobalVars();
if (!empty($globalVars)) {
$markup = TextParser::parse($markup, $globalVars);
}
return $this->processedMarkupCache = $markup;
}
public function getProcessedPlaceholderMarkup($placeholderName, $placeholderContents)
{
if (array_key_exists($placeholderName, $this->processedBlockMarkupCache)) {
return $this->processedBlockMarkupCache[$placeholderName];
}
/*
* Process snippets
*/
$markup = Snippet::processPageMarkup(
$this->getFileName().md5($placeholderName),
$this->theme,
$placeholderContents
);
/*
* Inject global view variables
*/
$globalVars = ViewHelper::getGlobalVars();
if (!empty($globalVars)) {
$markup = TextParser::parse($markup, $globalVars);
}
return $this->processedBlockMarkupCache[$placeholderName] = $markup;
}
//
// Snippets
//
/**
* Initializes CMS components associated with the page.
*/
public function initCmsComponents($cmsController)
{
$snippetComponents = Snippet::listPageComponents(
$this->getFileName(),
$this->theme,
$this->markup.$this->code
);
$componentManager = ComponentManager::instance();
foreach ($snippetComponents as $componentInfo) {
// Register components for snippet-based components
// if they're not defined yet. This is required because
// not all snippet components are registered as components,
// but it's safe to register them in render-time.
if (!$componentManager->hasComponent($componentInfo['class'])) {
$componentManager->registerComponent($componentInfo['class'], $componentInfo['alias']);
}
$cmsController->addComponent(
$componentInfo['class'],
$componentInfo['alias'],
$componentInfo['properties']
);
}
}
//
// Static Menu API
//
/**
* Returns a cache key for this record.
*/
protected static function getMenuCacheKey($theme)
{
$key = crc32($theme->getPath()).'static-page-menu';
/**
* @event pages.page.getMenuCacheKey
* Enables modifying the key used to reference cached RainLab.Pages menu trees
*
* Example usage:
*
* Event::listen('pages.page.getMenuCacheKey', function (&$key) {
* $key = $key . '-' . App::getLocale();
* });
*
*/
Event::fire('pages.page.getMenuCacheKey', [&$key]);
return $key;
}
/**
* Returns whether the specified URLs are equal.
*/
protected static function urlsAreEqual($url, $other)
{
return rawurldecode($url) === rawurldecode($other);
}
/**
* Clears the menu item cache
* @param \Cms\Classes\Theme $theme Specifies the current theme.
*/
public static function clearMenuCache($theme)
{
Cache::forget(self::getMenuCacheKey($theme));
}
/**
* Handler for the pages.menuitem.getTypeInfo event.
* Returns a menu item type information. The type information is returned as array
* with the following elements:
* - references - a list of the item type reference options. The options are returned in the
* ["key"] => "title" format for options that don't have sub-options, and in the format
* ["key"] => ["title"=>"Option title", "items"=>[...]] for options that have sub-options. Optional,
* required only if the menu item type requires references.
* - nesting - Boolean value indicating whether the item type supports nested items. Optional,
* false if omitted.
* - dynamicItems - Boolean value indicating whether the item type could generate new menu items.
* Optional, false if omitted.
* - cmsPages - a list of CMS pages (objects of the Cms\Classes\Page class), if the item type requires a CMS page reference to
* resolve the item URL.
* @param string $type Specifies the menu item type
* @return array Returns an array
*/
public static function getMenuTypeInfo($type)
{
if ($type == 'all-static-pages') {
return [
'dynamicItems' => true
];
}
if ($type == 'static-page') {
return [
'references' => self::listStaticPageMenuOptions(),
'nesting' => true,
'dynamicItems' => true
];
}
}
/**
* Handler for the pages.menuitem.resolveItem event.
* Returns information about a menu item. The result is an array
* with the following keys:
* - url - the menu item URL. Not required for menu item types that return all available records.
* The URL should be returned relative to the website root and include the subdirectory, if any.
* Use the Cms::url() helper to generate the URLs.
* - isActive - determines whether the menu item is active. Not required for menu item types that
* return all available records.
* - items - an array of arrays with the same keys (url, isActive, items) + the title key.
* The items array should be added only if the $item's $nesting property value is TRUE.
* @param \RainLab\Pages\Classes\MenuItem $item Specifies the menu item.
* @param \Cms\Classes\Theme $theme Specifies the current theme.
* @param string $url Specifies the current page URL, normalized, in lower case
* The URL is specified relative to the website root, it includes the subdirectory name, if any.
* @return mixed Returns an array. Returns null if the item cannot be resolved.
*/
public static function resolveMenuItem($item, $url, $theme)
{
$tree = self::buildMenuTree($theme);
if ($item->type == 'static-page' && !isset($tree[$item->reference])) {
return;
}
$result = [];
if ($item->type == 'static-page') {
$pageInfo = $tree[$item->reference];
$result['url'] = Cms::url($pageInfo['url']);
$result['mtime'] = $pageInfo['mtime'];
$result['isActive'] = self::urlsAreEqual($result['url'], $url);
}
if ($item->nesting || $item->type == 'all-static-pages') {
$iterator = function($items) use (&$iterator, &$tree, $url) {
$branch = [];
foreach ($items as $itemName) {
if (!isset($tree[$itemName])) {
continue;
}
$itemInfo = $tree[$itemName];
if ($itemInfo['navigation_hidden']) {
continue;
}
$branchItem = [];
$branchItem['url'] = Cms::url($itemInfo['url']);
$branchItem['isActive'] = self::urlsAreEqual($branchItem['url'], $url);
$branchItem['title'] = $itemInfo['title'];
$branchItem['mtime'] = $itemInfo['mtime'];
if ($itemInfo['items']) {
$branchItem['items'] = $iterator($itemInfo['items']);
}
$branch[] = $branchItem;
}
return $branch;
};
$result['items'] = $iterator($item->type == 'static-page' ? $pageInfo['items'] : $tree['--root-pages--']);
}
return $result;
}
/**
* Handler for the backend.richeditor.getTypeInfo event.
* Returns a menu item type information. The type information is returned as array
*
* @param string $type Specifies the page link type
* @return array Array of available link targets keyed by URL ['https://example.com/' => 'Homepage]
*/
public static function getRichEditorTypeInfo($type)
{
if ($type == 'static-page') {
$pages = self::listStaticPageMenuOptions();
$iterator = function($pages) use (&$iterator) {
$result = [];
foreach ($pages as $pageFile => $page) {
$url = self::url($pageFile);
if (is_array($page)) {
$result[$url] = [
'title' => array_get($page, 'title', []),
'links' => $iterator(array_get($page, 'items', []))
];
}
else {
$result[$url] = $page;
}
}
return $result;
};
return $iterator($pages);
}
return [];
}
/**
* Builds and caches a menu item tree.
* This method is used internally for menu items and breadcrumbs.
* @param \Cms\Classes\Theme $theme Specifies the current theme.
* @return array Returns an array containing the page information
*/
public static function buildMenuTree($theme)
{
if (self::$menuTreeCache !== null) {
return self::$menuTreeCache;
}
$key = self::getMenuCacheKey($theme);
$cached = Cache::get($key, false);
$unserialized = $cached ? @unserialize($cached) : false;
if ($unserialized !== false) {
return self::$menuTreeCache = $unserialized;
}
$menuTree = [
'--root-pages--' => []
];
$iterator = function($items, $parent, $level) use (&$menuTree, &$iterator) {
$result = [];
foreach ($items as $item) {
$viewBag = $item->page->viewBag;
$pageCode = $item->page->getBaseFileName();
$pageUrl = Str::lower(RouterHelper::normalizeUrl(array_get($viewBag, 'url')));
$itemData = [
'url' => $pageUrl,
'title' => array_get($viewBag, 'title'),
'mtime' => $item->page->mtime,
'items' => $iterator($item->subpages, $pageCode, $level+1),
'parent' => $parent,
'navigation_hidden' => array_get($viewBag, 'navigation_hidden')
];
if ($level == 0) {
$menuTree['--root-pages--'][] = $pageCode;
}
$result[] = $pageCode;
$menuTree[$pageCode] = $itemData;
}
return $result;
};
$pageList = new PageList($theme);
$iterator($pageList->getPageTree(), null, 0);
self::$menuTreeCache = $menuTree;
$comboConfig = Config::get('cms.parsedPageCacheTTL', Config::get('cms.template_cache_ttl', 10));
$expiresAt = now()->addMinutes($comboConfig);
Cache::put($key, serialize($menuTree), $expiresAt);
return self::$menuTreeCache;
}
/**
* Returns a list of options for the Reference drop-down menu in the
* menu item configuration form, when the Static Page item type is selected.
* @return array Returns an array
*/
protected static function listStaticPageMenuOptions()
{
$theme = Theme::getEditTheme();
$pageList = new PageList($theme);
$pageTree = $pageList->getPageTree(true);
$iterator = function($pages) use (&$iterator) {
$result = [];
foreach ($pages as $pageInfo) {
$pageName = $pageInfo->page->getViewBag()->property('title');
$fileName = $pageInfo->page->getBaseFileName();
if (!$pageInfo->subpages) {
$result[$fileName] = $pageName;
}
else {
$result[$fileName] = [
'title' => $pageName,
'items' => $iterator($pageInfo->subpages)
];
}
}
return $result;
};
return $iterator($pageTree);
}
/**
* Disables safe mode check for static pages.
*
* This allows developers to use placeholders in layouts even if safe mode is enabled.
*
* @return void
*/
protected function checkSafeMode()
{
}
}

View File

@ -0,0 +1,236 @@
<?php namespace RainLab\Pages\Classes;
use Cms\Classes\Meta;
use RainLab\Pages\Classes\Page;
/**
* The page list class reads and manages the static page hierarchy.
*
* @package rainlab\pages
* @author Alexey Bobkov, Samuel Georges
*/
class PageList
{
protected $theme;
protected static $configCache = false;
/**
* Creates the page list object.
* @param \Cms\Classes\Theme $theme Specifies a parent theme.
*/
public function __construct($theme)
{
$this->theme = $theme;
}
/**
* Returns a list of static pages in the specified theme.
* This method is used internally by the system.
* @param boolean $skipCache Indicates if objects should be reloaded from the disk bypassing the cache.
* @return array Returns an array of static pages.
*/
public function listPages($skipCache = false)
{
return Page::listInTheme($this->theme, $skipCache);
}
/**
* Returns a list of top-level pages with subpages.
* The method uses the theme's meta/static-pages.yaml file to build the hierarchy. The pages are returned
* in the order defined in the YAML file. The result of the method is used for building the back-end UI
* and for generating the menus.
* @param boolean $skipCache Indicates if objects should be reloaded from the disk bypassing the cache.
* @return array Returns a nested array of objects: object('page': $pageObj, 'subpages'=>[...])
*/
public function getPageTree($skipCache = false)
{
$pages = $this->listPages($skipCache);
$config = $this->getPagesConfig();
// Make the $pages collection an associative array for performance
$pagesArray = $pages->keyBy(function ($page) {
return $page->getBaseFileName();
})->all();
$iterator = function($configPages) use (&$iterator, $pagesArray) {
$result = [];
foreach ($configPages as $fileName => $subpages) {
if (isset($pagesArray[$fileName])) {
$result[] = (object) [
'page' => $pagesArray[$fileName],
'subpages' => $iterator($subpages),
];
}
}
return $result;
};
return $iterator($config['static-pages']);
}
/**
* Returns the parent name of the specified page.
* @param \Cms\Classes\Page $page Specifies a page object.
* @param string Returns the parent page name.
*/
public function getPageParent($page)
{
$pagesConfig = $this->getPagesConfig();
$requestedFileName = $page->getBaseFileName();
$parent = null;
$iterator = function($configPages) use (&$iterator, &$parent, $requestedFileName) {
foreach ($configPages as $fileName => $subpages) {
if ($fileName == $requestedFileName) {
return true;
}
if ($iterator($subpages) == true && is_null($parent)) {
$parent = $fileName;
return true;
}
}
};
$iterator($pagesConfig['static-pages']);
return $parent;
}
/**
* Returns a part of the page hierarchy starting from the specified page.
* @param \Cms\Classes\Page $page Specifies a page object.
* @param array Returns a nested array of page names.
*/
public function getPageSubTree($page)
{
$pagesConfig = $this->getPagesConfig();
$requestedFileName = $page->getBaseFileName();
$subTree = [];
$iterator = function($configPages) use (&$iterator, &$subTree, $requestedFileName) {
if (is_array($configPages)) {
foreach ($configPages as $fileName => $subpages) {
if ($fileName == $requestedFileName) {
$subTree = $subpages;
return true;
}
if ($iterator($subpages) === true) {
return true;
}
}
}
};
$iterator($pagesConfig['static-pages']);
return $subTree;
}
/**
* Appends page to the page hierarchy.
* The page can be added to the end of the hierarchy or as a subpage to any existing page.
*/
public function appendPage($page)
{
$parent = $page->parentFileName;
$originalData = $this->getPagesConfig();
$structure = $originalData['static-pages'];
if (!strlen($parent)) {
$structure[$page->getBaseFileName()] = [];
}
else {
$iterator = function(&$configPages) use (&$iterator, $parent, $page) {
foreach ($configPages as $fileName => &$subpages) {
if ($fileName == $parent) {
$subpages[$page->getBaseFileName()] = [];
return true;
}
if ($iterator($subpages) == true)
return true;
}
};
$iterator($structure);
}
$this->updateStructure($structure);
}
/**
* Removes a part of the page hierarchy starting from the specified page.
* @param \Cms\Classes\Page $page Specifies a page object.
*/
public function removeSubtree($page)
{
$pagesConfig = $this->getPagesConfig();
$requestedFileName = $page->getBaseFileName();
$tree = [];
$iterator = function($configPages) use (&$iterator, &$pages, $requestedFileName) {
$result = [];
foreach ($configPages as $fileName => $subpages) {
if ($requestedFileName != $fileName) {
$result[$fileName] = $iterator($subpages);
}
}
return $result;
};
$updatedStructure = $iterator($pagesConfig['static-pages']);
$this->updateStructure($updatedStructure);
}
/**
* Returns the parsed meta/static-pages.yaml file contents.
* @return mixed
*/
protected function getPagesConfig()
{
if (self::$configCache !== false) {
return self::$configCache;
}
$config = Meta::loadCached($this->theme, 'static-pages.yaml');
if (!$config) {
$config = new Meta();
$config->fileName = 'static-pages.yaml';
$config['static-pages'] = [];
$config->save();
}
if (!isset($config->attributes['static-pages'])) {
$config['static-pages'] = [];
}
return self::$configCache = $config;
}
/**
* Updates the page hierarchy structure in the theme's meta/static-pages.yaml file.
* @param array $structure A nested associative array representing the page structure
*/
public function updateStructure($structure)
{
$config = $this->getPagesConfig();
$config['static-pages'] = $structure;
$config->save();
}
}

View File

@ -0,0 +1,178 @@
<?php namespace RainLab\Pages\Classes;
use Lang;
use Cache;
use Event;
use Config;
use Cms\Classes\Theme;
use RainLab\Pages\Classes\Page;
use October\Rain\Support\Str;
use October\Rain\Router\Helper as RouterHelper;
/**
* A router for static pages.
*
* @package rainlab\pages
* @author Alexey Bobkov, Samuel Georges
*/
class Router
{
/**
* @var \Cms\Classes\Theme A reference to the CMS theme containing the object.
*/
protected $theme;
/**
* @var array Contains the URL map - the list of page file names and corresponding URL patterns.
*/
private static $urlMap = [];
/**
* @var array Request-level cache
*/
private static $cache = [];
/**
* Creates the router instance.
* @param \Cms\Classes\Theme $theme Specifies the theme being processed.
*/
public function __construct(Theme $theme)
{
$this->theme = $theme;
}
/**
* Finds a static page by its URL.
* @param string $url The requested URL string.
* @return \RainLab\Pages\Classes\Page Returns \RainLab\Pages\Classes\Page object or null if the page cannot be found.
*/
public function findByUrl($url)
{
$url = Str::lower(RouterHelper::normalizeUrl($url));
if (array_key_exists($url, self::$cache)) {
return self::$cache[$url];
}
$urlMap = $this->getUrlMap();
$urlMap = array_key_exists('urls', $urlMap) ? $urlMap['urls'] : [];
if (!array_key_exists($url, $urlMap)) {
return null;
}
$fileName = $urlMap[$url];
if (($page = Page::loadCached($this->theme, $fileName)) === null) {
/*
* If the page was not found on the disk, clear the URL cache
* and try again.
*/
$this->clearCache();
return self::$cache[$url] = Page::loadCached($this->theme, $fileName);
}
return self::$cache[$url] = $page;
}
/**
* Autoloads the URL map only allowing a single execution.
* @return array Returns the URL map.
*/
protected function getUrlMap()
{
if (!count(self::$urlMap)) {
$this->loadUrlMap();
}
return self::$urlMap;
}
/**
* Loads the URL map - a list of page file names and corresponding URL patterns.
* The URL map can is cached. The clearUrlMap() method resets the cache. By default
* the map is updated every time when a page is saved in the back-end, or
* when the interval defined with the cms.urlCacheTtl expires.
* @return boolean Returns true if the URL map was loaded from the cache. Otherwise returns false.
*/
protected function loadUrlMap()
{
$key = $this->getCacheKey('static-page-url-map');
$cacheable = Config::get('cms.enableRoutesCache', Config::get('cms.enable_route_cache', false));
$cached = $cacheable ? Cache::get($key, false) : false;
if (!$cached || ($unserialized = @unserialize($cached)) === false) {
/*
* The item doesn't exist in the cache, create the map
*/
$pageList = new PageList($this->theme);
$pages = $pageList->listPages();
$map = [
'urls' => [],
'files' => [],
'titles' => []
];
foreach ($pages as $page) {
$url = $page->getViewBag()->property('url');
if (!$url) {
continue;
}
$url = Str::lower(RouterHelper::normalizeUrl($url));
$file = $page->getBaseFileName();
$map['urls'][$url] = $file;
$map['files'][$file] = $url;
$map['titles'][$file] = $page->getViewBag()->property('title');
}
self::$urlMap = $map;
if ($cacheable) {
$comboConfig = Config::get('cms.urlCacheTtl', Config::get('cms.url_cache_ttl', 10));
$expiresAt = now()->addMinutes($comboConfig);
Cache::put($key, serialize($map), $expiresAt);
}
return false;
}
self::$urlMap = $unserialized;
return true;
}
/**
* Returns the caching URL key depending on the theme.
* @param string $keyName Specifies the base key name.
* @return string Returns the theme-specific key name.
*/
protected function getCacheKey($keyName)
{
$key = crc32($this->theme->getPath()).$keyName;
/**
* @event pages.router.getCacheKey
* Enables modifying the key used to reference cached RainLab.Pages routes
*
* Example usage:
*
* Event::listen('pages.router.getCacheKey', function (&$key) {
* $key = $key . '-' . App::getLocale();
* });
*
*/
Event::fire('pages.router.getCacheKey', [&$key]);
return $key;
}
/**
* Clears the router cache.
*/
public function clearCache()
{
Cache::forget($this->getCacheKey('static-page-url-map'));
}
}

View File

@ -0,0 +1,687 @@
<?php namespace RainLab\Pages\Classes;
use Event;
use Lang;
use Cache;
use Config;
use ApplicationException;
use Cms\Classes\Partial;
use Cms\Classes\ComponentHelpers;
use Cms\Classes\Controller as CmsController;
use ValidationException;
use DOMDocument;
/**
* Represents a static page snippet.
*
* @package rainlab\pages
* @author Alexey Bobkov, Samuel Georges
*/
class Snippet
{
/**
* @var string Specifies the snippet code.
*/
public $code;
/**
* @var string Specifies the snippet description.
*/
protected $description = null;
/**
* @var string Specifies the snippet name.
*/
protected $name = null;
/**
* @var string Snippet properties.
*/
protected $properties;
/**
* @var string Snippet component class name.
*/
protected $componentClass = null;
/**
* @var array Internal cache of snippet declarations defined on a page.
*/
protected static $pageSnippetMap = [];
/**
* @var \Cms\Classes\ComponentBase Snippet component object.
*/
protected $componentObj = null;
public function __construct()
{
}
/**
* Initializes the snippet from a CMS partial.
* @param \Cms\Classes\Partial $parital A partial to load the configuration from.
*/
public function initFromPartial($partial)
{
$viewBag = $partial->getViewBag();
$this->code = $viewBag->property('snippetCode');
$this->description = $partial->description;
$this->name = $viewBag->property('snippetName');
$this->properties = $viewBag->property('snippetProperties', []);
}
/**
* Initializes the snippet from a CMS component information.
* @param string $componentClass Specifies the component class.
* @param string $componentCode Specifies the component code.
*/
public function initFromComponentInfo($componentClass, $componentCode)
{
$this->code = $componentCode;
$this->componentClass = $componentClass;
}
/**
* Returns the snippet name.
* This method should not be used in the front-end request handling.
* @return string
*/
public function getName()
{
if ($this->name !== null) {
return $this->name;
}
if ($this->componentClass === null) {
return null;
}
$component = $this->getComponent();
return $this->name = ComponentHelpers::getComponentName($component);
}
/**
* Returns the snippet description.
* This method should not be used in the front-end request handling.
* @return string
*/
public function getDescription()
{
if ($this->description !== null) {
return $this->description;
}
if ($this->componentClass === null) {
return null;
}
$component = $this->getComponent();
return $this->description = ComponentHelpers::getComponentDescription($component);
}
/**
* Returns the snippet component class name.
* If the snippet is a partial snippet, returns NULL.
* @return string Returns the snippet component class name
*/
public function getComponentClass()
{
return $this->componentClass;
}
/**
* Returns the snippet property list as array, in format compatible with Inspector.
*/
public function getProperties()
{
if (!$this->componentClass) {
return self::parseIniProperties($this->properties);
}
else {
return ComponentHelpers::getComponentsPropertyConfig($this->getComponent(), false, true);
}
}
/**
* Returns a list of component definitions declared on the page.
* @param string $pageName Specifies the static page file name (the name of the corresponding content block file).
* @param \Cms\Classes\Theme $theme Specifies a parent theme.
* @return array Returns an array of component definitions
*/
public static function listPageComponents($pageName, $theme, $markup)
{
$map = self::extractSnippetsFromMarkupCached($theme, $pageName, $markup);
$result = [];
foreach ($map as $snippetDeclaration => $snippetInfo) {
if (!isset($snippetInfo['component'])) {
continue;
}
$result[] = [
'class' => $snippetInfo['component'],
'alias' => $snippetInfo['code'],
'properties' => $snippetInfo['properties']
];
}
return $result;
}
/**
* Extends the partial form with Snippet fields.
*/
public static function extendPartialForm($formWidget)
{
/*
* Snippet code field
*/
$fieldConfig = [
'tab' => 'rainlab.pages::lang.snippet.partialtab',
'type' => 'text',
'label' => 'rainlab.pages::lang.snippet.code',
'comment' => 'rainlab.pages::lang.snippet.code_comment',
'span' => 'left'
];
$formWidget->tabs['fields']['viewBag[snippetCode]'] = $fieldConfig;
/*
* Snippet description field
*/
$fieldConfig = [
'tab' => 'rainlab.pages::lang.snippet.partialtab',
'type' => 'text',
'label' => 'rainlab.pages::lang.snippet.name',
'comment' => 'rainlab.pages::lang.snippet.name_comment',
'span' => 'right'
];
$formWidget->tabs['fields']['viewBag[snippetName]'] = $fieldConfig;
/*
* Snippet properties field
*/
$fieldConfig = [
'tab' => 'rainlab.pages::lang.snippet.partialtab',
'type' => 'datatable',
'height' => '150',
'dynamicHeight' => true,
'columns' => [
'title' => [
'title' => 'rainlab.pages::lang.snippet.column_property',
'validation' => [
'required' => [
'message' => 'Please provide the property title',
'requiredWith' => 'property'
]
]
],
'property' => [
'title' => 'rainlab.pages::lang.snippet.column_code',
'validation' => [
'required' => [
'message' => 'Please provide the property code',
'requiredWith' => 'title'
],
'regex' => [
'pattern' => '^[a-z][a-z0-9]*$',
'modifiers' => 'i',
'message' => Lang::get('rainlab.pages::lang.snippet.property_format_error')
]
]
],
'type' => [
'title' => 'rainlab.pages::lang.snippet.column_type',
'type' => 'dropdown',
'options' => [
'string' => 'rainlab.pages::lang.snippet.column_type_string',
'checkbox' => 'rainlab.pages::lang.snippet.column_type_checkbox',
'dropdown' => 'rainlab.pages::lang.snippet.column_type_dropdown'
],
'validation' => [
'required' => [
'requiredWith' => 'title'
]
]
],
'default' => [
'title' => 'rainlab.pages::lang.snippet.column_default'
],
'options' => [
'title' => 'rainlab.pages::lang.snippet.column_options'
]
]
];
$formWidget->tabs['fields']['viewBag[snippetProperties]'] = $fieldConfig;
}
public static function extendEditorPartialToolbar($dataHolder)
{
$dataHolder->buttons[] = [
'button' => 'rainlab.pages::lang.snippet.partialtab',
'icon' => 'octo-icon-code-snippet',
'popupTitle' => 'rainlab.pages::lang.snippet.settings_popup_title',
'useViewBag' => true,
'properties' => [
[
'property' => 'snippetCode',
'title' => 'rainlab.pages::lang.snippet.code',
'description' => 'rainlab.pages::lang.snippet.code_comment',
'type' => 'string',
'validation' => [
'required' => [
'message' => 'rainlab.pages::lang.snippet.code_required'
]
]
],
[
'property' => 'snippetName',
'title' => 'rainlab.pages::lang.snippet.name',
'description' => 'rainlab.pages::lang.snippet.name_comment',
'type' => 'string',
'validation' => [
'required' => [
'message' => 'rainlab.pages::lang.snippet.name_required'
]
]
],
[
'property' => 'snippetProperties',
'title' => '',
'type' => 'table',
'tab' => 'rainlab.pages::lang.snippet.properties',
'columns' => [
[
'column' => 'title',
'type' => 'string',
'title' => 'rainlab.pages::lang.snippet.column_property',
'validation' => [
'required' => [
'message' => 'rainlab.pages::lang.snippet.title_required'
]
]
],
[
'column' => 'property',
'type' => 'string',
'title' => 'rainlab.pages::lang.snippet.column_code',
'validation' => [
'required' => [
'message' => 'rainlab.pages::lang.snippet.property_required'
],
'regex' => [
'pattern' => '^[a-z][a-z0-9]*$',
'modifiers' => 'i',
'message' => 'rainlab.pages::lang.snippet.property_format_error'
]
]
],
[
'column' => 'type',
'title' => 'rainlab.pages::lang.snippet.column_type',
'type' => 'dropdown',
'placeholder' => 'rainlab.pages::lang.snippet.column_type_placeholder',
'options' => [
'string' => 'rainlab.pages::lang.snippet.column_type_string',
'checkbox' => 'rainlab.pages::lang.snippet.column_type_checkbox',
'dropdown' => 'rainlab.pages::lang.snippet.column_type_dropdown'
],
'validation' => [
'required' => [
'message' => 'rainlab.pages::lang.snippet.type_required'
]
]
],
[
'column' => 'default',
'type' => 'string',
'title' => 'rainlab.pages::lang.snippet.column_default'
],
[
'column' => 'options',
'type' => 'string',
'title' => 'rainlab.pages::lang.snippet.column_options'
]
]
]
]
];
}
/**
* Returns a component corresponding to the snippet.
* This method should not be used in the front-end request handling code.
* @return \Cms\Classes\ComponentBase
*/
protected function getComponent()
{
if ($this->componentClass === null) {
return null;
}
if ($this->componentObj !== null) {
return $this->componentObj;
}
$componentClass = $this->componentClass;
return $this->componentObj = new $componentClass();
}
//
// Parsing
//
/**
* Parses the static page markup and renders snippets defined on the page.
* @param string $pageName Specifies the static page file name (the name of the corresponding content block file).
* @param \Cms\Classes\Theme $theme Specifies a parent theme.
* @param string $markup Specifies the markup string to process.
* @return string Returns the processed string.
*/
public static function processPageMarkup($pageName, $theme, $markup)
{
$map = self::extractSnippetsFromMarkupCached($theme, $pageName, $markup);
$controller = CmsController::getController();
$partialSnippetMap = SnippetManager::instance()->getPartialSnippetMap($theme);
foreach ($map as $snippetDeclaration => $snippetInfo) {
$snippetCode = $snippetInfo['code'];
if (!isset($snippetInfo['component'])) {
if (!array_key_exists($snippetCode, $partialSnippetMap)) {
throw new ApplicationException(sprintf('Partial for the snippet %s is not found', $snippetCode));
}
$partialName = $partialSnippetMap[$snippetCode];
$generatedMarkup = $controller->renderPartial($partialName, $snippetInfo['properties']);
}
else {
$generatedMarkup = $controller->renderComponent($snippetCode);
}
$pattern = preg_quote($snippetDeclaration);
$markup = mb_ereg_replace($pattern, $generatedMarkup, $markup);
}
return $markup;
}
public static function processTemplateSettingsArray($settingsArray)
{
if (
!isset($settingsArray['viewBag']['snippetProperties']['TableData']) &&
!isset($settingsArray['viewBag']['snippetProperties']) // CMS Editor
) {
return $settingsArray;
}
$properties = [];
if (isset($settingsArray['viewBag']['snippetProperties']['TableData'])) {
$rows = $settingsArray['viewBag']['snippetProperties']['TableData'];
}
else {
$rows = $settingsArray['viewBag']['snippetProperties'];
}
foreach ($rows as $row) {
$property = array_get($row, 'property');
$settings = array_only($row, ['title', 'type', 'default', 'options']);
if (isset($settings['options'])) {
$settings['options'] = self::dropDownOptionsToArray($settings['options']);
}
$properties[$property] = $settings;
}
$settingsArray['viewBag']['snippetProperties'] = [];
foreach ($properties as $name => $value) {
$settingsArray['viewBag']['snippetProperties'][$name] = $value;
}
return $settingsArray;
}
public static function processTemplateSettings($template, $context = null)
{
if (!isset($template->viewBag['snippetProperties'])) {
return;
}
$parsedProperties = self::parseIniProperties($template->viewBag['snippetProperties']);
foreach ($parsedProperties as $index => &$property) {
if ($context !== 'editor') {
$property['id'] = $index;
}
if (isset($property['options']) && is_array($property['options'])) {
$property['options'] = self::dropDownOptionsToString($property['options']);
}
}
$template->viewBag['snippetProperties'] = $parsedProperties;
if ($context == 'editor') {
$template->settings['components']['viewBag'] = $template->viewBag;
}
}
/**
* Apples default property values and fixes property names.
*
* As snippet properties are defined with data attributes, they are lower case, whereas
* real property names are case sensitive. This method finds original property names defined
* in snippet classes or partials and replaces property names defined in the static page markup.
*/
protected static function preprocessPropertyValues($theme, $snippetCode, $componentClass, $properties)
{
$snippet = SnippetManager::instance()->findByCodeOrComponent($theme, $snippetCode, $componentClass, true);
if (!$snippet) {
throw new ApplicationException(Lang::get('rainlab.pages::lang.snippet.not_found', ['code' => $snippetCode]));
}
$properties = array_change_key_case($properties);
$snippetProperties = $snippet->getProperties();
foreach ($snippetProperties as $propertyInfo) {
$propertyCode = $propertyInfo['property'];
$lowercaseCode = strtolower($propertyCode);
if (!array_key_exists($lowercaseCode, $properties)) {
if (array_key_exists('default', $propertyInfo)) {
$properties[$propertyCode] = $propertyInfo['default'];
}
}
else {
$markupPropertyInfo = $properties[$lowercaseCode];
unset($properties[$lowercaseCode]);
$properties[$propertyCode] = $markupPropertyInfo;
}
}
return $properties;
}
/**
* Converts a keyed object to an array, converting the index to the "property" value.
* @return array
*/
protected static function parseIniProperties($properties)
{
foreach ($properties as $index => $value) {
$properties[$index]['property'] = $index;
}
return array_values($properties);
}
protected static function dropDownOptionsToArray($optionsString)
{
$options = explode('|', $optionsString);
$result = [];
foreach ($options as $index => $optionStr) {
$parts = explode(':', $optionStr, 2);
if (count($parts) > 1 ) {
$key = trim($parts[0]);
if (strlen($key)) {
if (!preg_match('/^[0-9a-z-_]+$/i', $key)) {
throw new ValidationException(['snippetProperties' => Lang::get('rainlab.pages::lang.snippet.invalid_option_key', ['key'=>$key])]);
}
$result[$key] = trim($parts[1]);
}
else {
$result[$index] = trim($optionStr);
}
}
else {
$result[$index] = trim($optionStr);
}
}
return $result;
}
protected static function dropDownOptionsToString($optionsArray)
{
$result = [];
$isAssoc = (bool) count(array_filter(array_keys($optionsArray), 'is_string'));
foreach ($optionsArray as $optionIndex => $optionValue) {
$result[] = $isAssoc
? $optionIndex.':'.$optionValue
: $optionValue;
}
return implode(' | ', $result);
}
protected static function extractSnippetsFromMarkup($markup, $theme)
{
$map = [];
$matches = [];
if (preg_match_all('/\<figure\s+[^\>]+\>.*\<\/figure\>/i', $markup, $matches)) {
foreach ($matches[0] as $snippetDeclaration) {
$nameMatch = [];
if (!preg_match('/data\-snippet\s*=\s*"([^"]+)"/', $snippetDeclaration, $nameMatch)) {
continue;
}
$snippetCode = $nameMatch[1];
$properties = [];
$propertyMatches = [];
if (preg_match_all('/data\-property-(?<property>[^=]+)\s*=\s*\"(?<value>[^\"]+)\"/i', $snippetDeclaration, $propertyMatches)) {
foreach ($propertyMatches['property'] as $index => $propertyName) {
$properties[$propertyName] = $propertyMatches['value'][$index];
}
}
$componentMatch = [];
$componentClass = null;
if (preg_match('/data\-component\s*=\s*"([^"]+)"/', $snippetDeclaration, $componentMatch)) {
$componentClass = $componentMatch[1];
}
// Apply default values for properties not defined in the markup
// and normalize property code names.
$properties = self::preprocessPropertyValues($theme, $snippetCode, $componentClass, $properties);
$map[$snippetDeclaration] = [
'code' => $snippetCode,
'component' => $componentClass,
'properties' => $properties
];
}
}
return $map;
}
protected static function extractSnippetsFromMarkupCached($theme, $pageName, $markup)
{
if (array_key_exists($pageName, self::$pageSnippetMap)) {
return self::$pageSnippetMap[$pageName];
}
$key = self::getMapCacheKey($theme);
$map = null;
$cached = Cache::get($key, false);
if ($cached !== false && ($cached = @unserialize($cached)) !== false) {
if (array_key_exists($pageName, $cached)) {
$map = $cached[$pageName];
}
}
if (!is_array($map)) {
$map = self::extractSnippetsFromMarkup($markup, $theme);
if (!is_array($cached)) {
$cached = [];
}
$cached[$pageName] = $map;
$comboConfig = Config::get('cms.parsedPageCacheTTL', Config::get('cms.template_cache_ttl', 10));
$expiresAt = now()->addMinutes($comboConfig);
Cache::put($key, serialize($cached), $expiresAt);
}
self::$pageSnippetMap[$pageName] = $map;
return $map;
}
/**
* Returns a cache key for this record.
*/
protected static function getMapCacheKey($theme)
{
$key = crc32($theme->getPath()).'snippet-map';
/**
* @event pages.snippet.getMapCacheKey
* Enables modifying the key used to reference cached RainLab.Pages snippet maps
*
* Example usage:
*
* Event::listen('pages.snippet.getMapCacheKey', function (&$key) {
* $key = $key . '-' . App::getLocale();
* });
*
*/
Event::fire('pages.snippet.getMapCacheKey', [&$key]);
return $key;
}
/**
* Clears the snippet map item cache
* @param \Cms\Classes\Theme $theme Specifies the current theme.
*/
public static function clearMapCache($theme)
{
Cache::forget(self::getMapCacheKey($theme));
}
}

View File

@ -0,0 +1,278 @@
<?php namespace RainLab\Pages\Classes;
use Event;
use Lang;
use Cache;
use Config;
use Cms\Classes\Partial;
use System\Classes\PluginManager;
use SystemException;
use RainLab\Pages\Classes\Snippet;
/**
* Returns information about snippets based on partials and components.
*
* @package rainlab\pages
* @author Alexey Bobkov, Samuel Georges
*/
class SnippetManager
{
use \October\Rain\Support\Traits\Singleton;
protected $snippets = null;
/**
* Returns a list of available snippets.
* @param \Cms\Classes\Theme $theme Specifies a parent theme.
* @return array Returns an unsorted array of snippet objects.
*/
public function listSnippets($theme)
{
if ($this->snippets !== null) {
return $this->snippets;
}
$themeSnippets = $this->listThemeSnippets($theme);
$componentSnippets = $this->listComponentSnippets();
$this->snippets = array_merge($themeSnippets, $componentSnippets);
/*
* @event pages.snippets.listSnippets
* Gives the ability to manage the snippet list dynamically.
*
* Example usage to add a snippet to the list:
*
* Event::listen('pages.snippets.listSnippets', function($manager) {
* $snippet = new \RainLab\Pages\Classes\Snippet();
* $snippet->initFromComponentInfo('\Example\Plugin\Components\ComponentClass', 'snippetCode');
* $manager->addSnippet($snippet);
* });
*
* Example usage to remove a snippet from the list:
*
* Event::listen('pages.snippets.listSnippets', function($manager) {
* $manager->removeSnippet('snippetCode');
* });
*/
Event::fire('pages.snippets.listSnippets', [$this]);
return $this->snippets;
}
/**
* Add snippet to the list of snippets
*
* @param Snippet $snippet
* @return void
*/
public function addSnippet(Snippet $snippet)
{
$this->snippets[] = $snippet;
}
/**
* Remove a snippet with the given code from the list of snippets
*
* @param string $snippetCode
* @return void
*/
public function removeSnippet(string $snippetCode)
{
$this->snippets = array_filter($this->snippets, function ($snippet) use ($snippetCode) {
return $snippet->code !== $snippetCode;
});
}
/**
* Finds a snippet by its code.
* This method is used internally by the system.
* @param \Cms\Classes\Theme $theme Specifies a parent theme.
* @param string $code Specifies the snippet code.
* @param string $$componentClass Specifies the snippet component class, if available.
* @param boolean $allowCaching Specifies whether caching is allowed for the call.
* @return array Returns an array of Snippet objects.
*/
public function findByCodeOrComponent($theme, $code, $componentClass, $allowCaching = false)
{
if (!$allowCaching) {
// If caching is not allowed, list all available snippets,
// find the snippet in the list and return it.
$snippets = $this->listSnippets($theme);
foreach ($snippets as $snippet) {
if ($componentClass && $snippet->getComponentClass() == $componentClass) {
return $snippet;
}
if ($snippet->code == $code) {
return $snippet;
}
}
return null;
}
// If caching is allowed, and the requested snippet is a partial snippet,
// try to load the partial name from the cache and initialize the snippet
// from the partial.
if (!strlen($componentClass)) {
$map = $this->getPartialSnippetMap($theme);
if (!array_key_exists($code, $map)) {
return null;
}
$partialName = $map[$code];
$partial = Partial::loadCached($theme, $partialName);
if (!$partial) {
return null;
}
$snippet = new Snippet;
$snippet->initFromPartial($partial);
return $snippet;
}
else {
// If the snippet is a component snippet, initialize it
// from the component
if (!class_exists($componentClass)) {
throw new SystemException(sprintf('The snippet component class %s is not found.', $componentClass));
}
$snippet = new Snippet;
$snippet->initFromComponentInfo($componentClass, $code);
return $snippet;
}
}
/**
* Clears front-end run-time cache.
* @param \Cms\Classes\Theme $theme Specifies a parent theme.
*/
public static function clearCache($theme)
{
Cache::forget(self::getPartialMapCacheKey($theme));
Snippet::clearMapCache($theme);
}
/**
* Returns a cache key for this record.
*/
protected static function getPartialMapCacheKey($theme)
{
$key = crc32($theme->getPath()).'snippet-partial-map';
/**
* @event pages.snippet.getPartialMapCacheKey
* Enables modifying the key used to reference cached RainLab.Pages partial maps
*
* Example usage:
*
* Event::listen('pages.snippet.getPartialMapCacheKey', function (&$key) {
* $key = $key . '-' . App::getLocale();
* });
*
*/
Event::fire('pages.snippet.getPartialMapCacheKey', [&$key]);
return $key;
}
/**
* Returns a list of partial-based snippets and corresponding partial names.
* @param \Cms\Classes\Theme $theme Specifies a parent theme.
* @return Returns an associative array with the snippet code in keys and partial file names in values.
*/
public function getPartialSnippetMap($theme)
{
$key = self::getPartialMapCacheKey($theme);
$result = [];
$cached = Cache::get($key, false);
if ($cached !== false && ($cached = @unserialize($cached)) !== false) {
return $cached;
}
$partials = Partial::listInTheme($theme);
foreach ($partials as $partial) {
$viewBag = $partial->getViewBag();
$snippetCode = $viewBag->property('snippetCode');
if (!strlen($snippetCode)) {
continue;
}
$result[$snippetCode] = $partial->getFileName();
}
$comboConfig = Config::get('cms.parsedPageCacheTTL', Config::get('cms.template_cache_ttl', 10));
$expiresAt = now()->addMinutes($comboConfig);
Cache::put($key, serialize($result), $expiresAt);
return $result;
}
/**
* Returns a list of snippets in the specified theme.
* @param \Cms\Classes\Theme $theme Specifies a parent theme.
* @return array Returns an array of Snippet objects.
*/
protected function listThemeSnippets($theme)
{
$result = [];
$partials = Partial::listInTheme($theme, true);
foreach ($partials as $partial) {
$viewBag = $partial->getViewBag();
if (strlen($viewBag->property('snippetCode'))) {
$snippet = new Snippet;
$snippet->initFromPartial($partial);
$result[] = $snippet;
}
}
return $result;
}
/**
* Returns a list of snippets created from components.
* @return array Returns an array of Snippet objects.
*/
protected function listComponentSnippets()
{
$result = [];
$pluginManager = PluginManager::instance();
$plugins = $pluginManager->getPlugins();
foreach ($plugins as $id => $plugin) {
if (!method_exists($plugin, 'registerPageSnippets')) {
continue;
}
$snippets = $plugin->registerPageSnippets();
if (!is_array($snippets)) {
continue;
}
foreach ($snippets as $componentClass => $componentCode) {
// TODO: register snippet components later, during
// the page life cycle.
$snippet = new Snippet;
$snippet->initFromComponentInfo($componentClass, $componentCode);
$result[] = $snippet;
}
}
return $result;
}
}

View File

@ -0,0 +1,38 @@
# ===================================
# Field Definitions
# ===================================
fields:
fileName:
label: cms::lang.editor.filename
attributes:
default-focus: 1
toolbar:
type: partial
path: content_toolbar
cssClass: collapse-visible
components: Cms\FormWidgets\Components
secondaryTabs:
stretch: true
fields:
markup:
tab: cms::lang.editor.content
stretch: true
type: codeeditor
language: html
theme: chrome
showGutter: false
highlightActiveLine: false
fontSize: 13
cssClass: pagesTextEditor
margin: 20
markup_html:
tab: cms::lang.editor.content
stretch: true
type: richeditor
size: huge
valueFrom: markup

View File

@ -0,0 +1,33 @@
# ===================================
# Field Definitions
# ===================================
fields:
name:
span: left
label: rainlab.pages::lang.menu.name
placeholder: rainlab.pages::lang.menu.new_name
attributes:
default-focus: 1
code:
span: right
placeholder: rainlab.pages::lang.menu.new_code
label: rainlab.pages::lang.menu.code
preset:
field: name
type: file
toolbar:
type: partial
path: menu_toolbar
cssClass: collapse-visible
tabs:
stretch: true
cssClass: master-area
fields:
items:
stretch: true
tab: rainlab.pages::lang.menu.items
type: RainLab\Pages\FormWidgets\MenuItems

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