added plugins

This commit is contained in:
root 2024-07-13 12:48:51 +00:00
parent 896ecdcfe0
commit 226875b230
615 changed files with 58635 additions and 3 deletions

View File

@ -0,0 +1,18 @@
<?php namespace AhmadFatoni\ApiGenerator;
use System\Classes\PluginBase;
class Plugin extends PluginBase
{
public $require = [
'RainLab.Builder'
];
public function registerComponents()
{
}
public function registerSettings()
{
}
}

View File

@ -0,0 +1,50 @@
# API Generator
> October CMS plugin to build RESTful APIs.
## Features
- Auto generate routes
- Auto Generate Controller (CRUD)
- Support relationship restful API
## Install
```
composer require AhmadFatoni.ApiGenerator
```
## Usage
### Form
- API Name : Name of your API module
- Base Endpoint : Base endpoint of your API, ex : api/v1/modulename
- Short Description : Describe your API
- Model : select model that will be created API
- Custom Condition : Build customer response using JSON modeling
### Custom Condition Example
```
{
'fillable': 'id,title,content',
'relation': [{
'name': 'user',
'fillable': 'id,first_name'
}, {
'name': 'categories',
'fillable': 'id,name
}]
}
```
* please replace single quote with quote
## Contribute
Pull Requests accepted.
## Contact
You can communicate with me using [linkedin](https://www.linkedin.com/in/ahmad-fatoni)
## License
The OctoberCMS platform is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

View File

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

View File

@ -0,0 +1,336 @@
<?php namespace AhmadFatoni\ApiGenerator\Controllers;
use Backend\Classes\Controller;
use AhmadFatoni\ApiGenerator\Models\ApiGenerator;
use BackendMenu;
use Backend;
use Illuminate\Http\Request;
use Illuminate\Filesystem\Filesystem;
use Redirect;
use Flash;
class ApiGeneratorController extends Controller
{
public $implement = ['Backend\Behaviors\ListController','Backend\Behaviors\FormController','Backend\Behaviors\ReorderController'];
public $listConfig = 'config_list.yaml';
public $formConfig = 'config_form.yaml';
public $reorderConfig = 'config_reorder.yaml';
protected $path = "/api/";
private $homePage = 'ahmadfatoni/apigenerator/apigeneratorcontroller';
protected $files;
public $requiredPermissions = ['ahmadfatoni.apigenerator.manage'];
public function __construct(Filesystem $files)
{
parent::__construct();
BackendMenu::setContext('AhmadFatoni.ApiGenerator', 'api-generator');
$this->files = $files;
}
/**
* delete selected data (multiple delete)
* @return [type] [description]
*/
public function index_onDelete()
{
if (($checkedIds = post('checked')) && is_array($checkedIds) && count($checkedIds)) {
foreach ($checkedIds as $id) {
if ((!$item = ApiGenerator::find($id)))
continue;
$name = $item->name;
if($item->delete()){
$this->deleteApi($name);
}
}
Flash::success('Successfully deleted those data.');
}
return $this->listRefresh();
}
/**
* generate API
* @param Request $request [description]
* @return [type] [description]
*/
public function generateApi(Request $request){
$data['model'] = $request->model;
$modelname = explode("\\", $request->model);
$modelname = $modelname[count($modelname)-1];
$data['modelname'] = $modelname;
$data['controllername'] = str_replace(" ", "", $request->name);
$data['endpoint'] = $request->endpoint;
$data['custom_format'] = $request->custom_format;
if( strpos($data['controllername'], ".") OR strpos($data['controllername'], "/") ){
Flash::success('Failed to create data, invalid API name.');
return Redirect::to( Backend::url($this->homePage));
}
if( isset($request->id) ){
$this->deleteApi($request->oldname, 'false');
}
$this->files->put(__DIR__ . $this->path . $data['controllername'].'Controller.php', $this->compile($data));
$this->files->put(__DIR__ . '/'.'../routes.php', $this->compileRoute($data));
return Redirect::to( Backend::url($this->homePage));
}
/**
* delete available API
* @param [type] $name [description]
* @param [type] $redirect [description]
* @return [type] [description]
*/
public function deleteApi($name, $redirect = null){
$fileLocation = __DIR__ . $this->path.$name;
$fileLocation = str_replace(".", "", $fileLocation);
if( ! file_exists($fileLocation.'Controller.php') ){
Flash::success('Failed to delete data, invalid file location.');
return Redirect::to( Backend::url($this->homePage));
}
if( strpos( strtolower($name), 'apigenerator' ) === false){
$data = [];
//generate new route
$this->files->put(__DIR__ . '/'.'../routes.php', $this->compileRoute($data));
//remove controller
if (file_exists( __DIR__ . $this->path.$name.'Controller.php' )) {
unlink(__DIR__ . $this->path.$name.'Controller.php');
}
if( $redirect != null ){
return 'success without redirect';
}
}
return Redirect::to( Backend::url($this->homePage));
}
public function updateApi($name){
}
/**
* compile controller from template
* @param [type] $data [description]
* @return [type] [description]
*/
public function compile($data){
if( $data['custom_format'] != ''){
$template = $this->files->get(__DIR__ .'/../template/customcontroller.dot');
$template = $this->replaceAttribute($template, $data);
$template = $this->replaceCustomAttribute($template, $data);
}else{
$template = $this->files->get(__DIR__ .'/../template/controller.dot');
$template = $this->replaceAttribute($template, $data);
}
return $template;
}
/**
* replace attribute
* @param [type] $template [description]
* @param [type] $data [description]
* @return [type] [description]
*/
public function replaceAttribute($template, $data){
if( isset( $data['model'] ) ){
$template = str_replace('{{model}}', $data['model'], $template);
}
$template = str_replace('{{modelname}}', $data['modelname'], $template);
$template = str_replace('{{controllername}}', $data['controllername'], $template);
return $template;
}
/**
* replace custom attribute
* @param [type] $template [description]
* @param [type] $data [description]
* @return [type] [description]
*/
public function replaceCustomAttribute($template, $data){
$arr = str_replace('\t', '', $data['custom_format']);
$arr = json_decode($arr);
$select = str_replace('<br />', '', $this->compileOpenIndexFunction($data['modelname'], 'index'));
$show = str_replace('<br />', '', $this->compileOpenIndexFunction($data['modelname'], 'show'));
$fillableParent = '';
if( isset($arr->fillable) AND $arr->fillable != null ) {
$fillableParent = $this->compileFillableParent($arr->fillable);
}
if( isset($arr->relation) AND $arr->relation != null AND is_array($arr->relation) AND count($arr->relation) > 0) {
$select .= str_replace('<br />', '', $this->compileFillableChild($arr->relation));
$show .= str_replace('<br />', '', $this->compileFillableChild($arr->relation));
}
$select .= "->select(".$fillableParent.")";
$show .= "->select(".$fillableParent.")->where('id', '=', \$id)->first();";
( $fillableParent != '') ? $select .= "->get()->toArray();" : $select .= "->toArray();" ;
$closeFunction = str_replace('<br />', '', nl2br(
"
return \$this->helpers->apiArrayResponseBuilder(200, 'success', \$data);
}"));
$select .= $closeFunction;
$show .= $closeFunction;
$template = str_replace('{{select}}', $select, $template);
$template = str_replace('{{show}}', $show, $template);
return $template;
}
public function compileOpenIndexFunction($modelname, $type){
if( $type == 'index'){
return nl2br("
public function index(){
\$data = \$this->".$modelname);
}else{
return nl2br("
public function show(\$id){
\$data = \$this->".$modelname);
}
}
public function compileFillableParent($fillable){
$fillableParentArr = explode(",", $fillable);
$fillableParent = '';
foreach ($fillableParentArr as $key) {
$fillableParent .= ",'".$key."'";
}
$fillableParent = substr_replace($fillableParent, '', 0 , 1);
return $fillableParent;
}
public function compileFillableChild($fillable){
$select = "->with(array(";
foreach ($fillable as $key) {
$fillableChild = "";
if( isset($key->fillable) AND $key->fillable != null ){
$fillableChildArr = explode(",", $key->fillable);
foreach ($fillableChildArr as $key2) {
$fillableChild .= ",'".$key2."'";
}
$fillableChild = substr_replace($fillableChild, '', 0 , 1);
}
$select .= nl2br(
"
'".$key->name."'=>function(\$query){
\$query->select(".$fillableChild.");
},");
}
$select .= " ))";
return $select;
}
public function compileRoute($data){
$oldData = ApiGenerator::all();
$routeList = "";
if( count($oldData) > 0 ){
$routeList .= $this->parseRouteOldData($oldData, $data);
}
if( count($data) > 0 ){
$data['modelname'] = $data['endpoint'];
if( $data['modelname'][0] == "/" ){
$data['modelname'] = substr_replace($data['modelname'], '', 0 , 1);
}
$routeList .= $this->parseRoute($data);
}
$route = $this->files->get(__DIR__ .'/../template/routes.dot');
$route = str_replace('{{route}}', $routeList, $route);
return $route;
}
public function parseRouteOldData($oldData, $data = null){
$routeList = "";
if( count($data) == 0 ) $data['modelname']='';
foreach ( $oldData as $key ) {
$modelname = explode("\\", $key->model);
$modelname = $modelname[count($modelname)-1];
$old['modelname'] = $key->endpoint;
$old['controllername'] = $key->name;
if( $data['modelname'] != $modelname ){
if( $old['modelname'][0] == "/" ){
$old['modelname'] = substr_replace($old['modelname'], '', 0 , 1);
}
$routeList .= $this->parseRoute($old);
}
}
return $routeList;
}
public function parseRoute($data){
$template = $this->files->get(__DIR__ .'/../template/route.dot');
$template = $this->replaceAttribute($template, $data);
return $template;
}
public static function getAfterFilters() {return [];}
public static function getBeforeFilters() {return [];}
public function callAction($method, $parameters=false) {
return call_user_func_array(array($this, $method), $parameters);
}
}

View File

@ -0,0 +1,99 @@
<?php namespace AhmadFatoni\ApiGenerator\Controllers\API;
use Cms\Classes\Controller;
use BackendMenu;
use Illuminate\Http\Request;
use AhmadFatoni\ApiGenerator\Helpers\Helpers;
use Illuminate\Support\Facades\Validator;
use Atash\Contact\Models\Card_data;
class cardController extends Controller
{
protected $Card_data;
protected $helpers;
public function __construct(Card_data $Card_data, Helpers $helpers)
{
parent::__construct();
$this->Card_data = $Card_data;
$this->helpers = $helpers;
}
public function index(){
// $data = $this->Card_data->all()->toArray();
$data = $this->Card_data->with(['translations:locale,model_id,attribute_data','image'])->get();
return $this->helpers->apiArrayResponseBuilder(200, 'success', $data);
}
public function show($id){
$data = $this->Card_data::find($id);
if ($data){
return $this->helpers->apiArrayResponseBuilder(200, 'success', [$data]);
} else {
$this->helpers->apiArrayResponseBuilder(404, 'not found', ['error' => 'Resource id=' . $id . ' could not be found']);
}
}
public function store(Request $request){
$arr = $request->all();
while ( $data = current($arr)) {
$this->Card_data->{key($arr)} = $data;
next($arr);
}
$validation = Validator::make($request->all(), $this->Card_data->rules);
if( $validation->passes() ){
$this->Card_data->save();
return $this->helpers->apiArrayResponseBuilder(201, 'created', ['id' => $this->Card_data->id]);
}else{
return $this->helpers->apiArrayResponseBuilder(400, 'fail', $validation->errors() );
}
}
public function update($id, Request $request){
$status = $this->Card_data->where('id',$id)->update($data);
if( $status ){
return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been updated successfully.');
}else{
return $this->helpers->apiArrayResponseBuilder(400, 'bad request', 'Error, data failed to update.');
}
}
public function delete($id){
$this->Card_data->where('id',$id)->delete();
return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been deleted successfully.');
}
public function destroy($id){
$this->Card_data->where('id',$id)->delete();
return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been deleted successfully.');
}
public static function getAfterFilters() {return [];}
public static function getBeforeFilters() {return [];}
public static function getMiddleware() {return [];}
public function callAction($method, $parameters=false) {
return call_user_func_array(array($this, $method), $parameters);
}
}

View File

@ -0,0 +1,99 @@
<?php namespace AhmadFatoni\ApiGenerator\Controllers\API;
use Cms\Classes\Controller;
use BackendMenu;
use Illuminate\Http\Request;
use AhmadFatoni\ApiGenerator\Helpers\Helpers;
use Illuminate\Support\Facades\Validator;
use Atash\Contact\Models\Credit_data;
class creditController extends Controller
{
protected $Credit_data;
protected $helpers;
public function __construct(Credit_data $Credit_data, Helpers $helpers)
{
parent::__construct();
$this->Credit_data = $Credit_data;
$this->helpers = $helpers;
}
public function index(){
// $data = $this->Credit_data->all()->toArray();
$data = $this->Credit_data->with(['translations:locale,model_id,attribute_data'])->get();
return $this->helpers->apiArrayResponseBuilder(200, 'success', $data);
}
public function show($id){
$data = $this->Credit_data::find($id);
if ($data){
return $this->helpers->apiArrayResponseBuilder(200, 'success', [$data]);
} else {
$this->helpers->apiArrayResponseBuilder(404, 'not found', ['error' => 'Resource id=' . $id . ' could not be found']);
}
}
public function store(Request $request){
$arr = $request->all();
while ( $data = current($arr)) {
$this->Credit_data->{key($arr)} = $data;
next($arr);
}
$validation = Validator::make($request->all(), $this->Credit_data->rules);
if( $validation->passes() ){
$this->Credit_data->save();
return $this->helpers->apiArrayResponseBuilder(201, 'created', ['id' => $this->Credit_data->id]);
}else{
return $this->helpers->apiArrayResponseBuilder(400, 'fail', $validation->errors() );
}
}
public function update($id, Request $request){
$status = $this->Credit_data->where('id',$id)->update($data);
if( $status ){
return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been updated successfully.');
}else{
return $this->helpers->apiArrayResponseBuilder(400, 'bad request', 'Error, data failed to update.');
}
}
public function delete($id){
$this->Credit_data->where('id',$id)->delete();
return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been deleted successfully.');
}
public function destroy($id){
$this->Credit_data->where('id',$id)->delete();
return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been deleted successfully.');
}
public static function getAfterFilters() {return [];}
public static function getBeforeFilters() {return [];}
public static function getMiddleware() {return [];}
public function callAction($method, $parameters=false) {
return call_user_func_array(array($this, $method), $parameters);
}
}

View File

@ -0,0 +1 @@
api controller here

View File

@ -0,0 +1,100 @@
<?php namespace AhmadFatoni\ApiGenerator\Controllers\API;
use Cms\Classes\Controller;
use BackendMenu;
use Illuminate\Http\Request;
use AhmadFatoni\ApiGenerator\Helpers\Helpers;
use Illuminate\Support\Facades\Validator;
use Atash\Contact\Models\TypeAccountReplenishment;
class typeAccountReplenishmentController extends Controller
{
protected $TypeAccountReplenishment;
protected $helpers;
public function __construct(TypeAccountReplenishment $TypeAccountReplenishment, Helpers $helpers)
{
parent::__construct();
$this->TypeAccountReplenishment = $TypeAccountReplenishment;
$this->helpers = $helpers;
}
public function index(){
// $data = $this->TypeAccountReplenishment->all()->toArray();
$data = $this->TypeAccountReplenishment->with(['translations:locale,model_id,attribute_data'])->get();
return $this->helpers->apiArrayResponseBuilder(200, 'success', $data);
}
public function show($id){
$data = $this->TypeAccountReplenishment::find($id);
if ($data){
return $this->helpers->apiArrayResponseBuilder(200, 'success', [$data]);
} else {
$this->helpers->apiArrayResponseBuilder(404, 'not found', ['error' => 'Resource id=' . $id . ' could not be found']);
}
}
public function store(Request $request){
$arr = $request->all();
while ( $data = current($arr)) {
$this->TypeAccountReplenishment->{key($arr)} = $data;
next($arr);
}
$validation = Validator::make($request->all(), $this->TypeAccountReplenishment->rules);
if( $validation->passes() ){
$this->TypeAccountReplenishment->save();
return $this->helpers->apiArrayResponseBuilder(201, 'created', ['id' => $this->TypeAccountReplenishment->id]);
}else{
return $this->helpers->apiArrayResponseBuilder(400, 'fail', $validation->errors() );
}
}
public function update($id, Request $request){
$status = $this->TypeAccountReplenishment->where('id',$id)->update($data);
if( $status ){
return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been updated successfully.');
}else{
return $this->helpers->apiArrayResponseBuilder(400, 'bad request', 'Error, data failed to update.');
}
}
public function delete($id){
$this->TypeAccountReplenishment->where('id',$id)->delete();
return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been deleted successfully.');
}
public function destroy($id){
$this->TypeAccountReplenishment->where('id',$id)->delete();
return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been deleted successfully.');
}
public static function getAfterFilters() {return [];}
public static function getBeforeFilters() {return [];}
public static function getMiddleware() {return [];}
public function callAction($method, $parameters=false) {
return call_user_func_array(array($this, $method), $parameters);
}
}

View File

@ -0,0 +1,99 @@
<?php namespace AhmadFatoni\ApiGenerator\Controllers\API;
use Cms\Classes\Controller;
use BackendMenu;
use Illuminate\Http\Request;
use AhmadFatoni\ApiGenerator\Helpers\Helpers;
use Illuminate\Support\Facades\Validator;
use RainLab\User\Models\User;
class usersigninController extends Controller
{
protected $User;
protected $helpers;
public function __construct(User $User, Helpers $helpers)
{
parent::__construct();
$this->User = $User;
$this->helpers = $helpers;
}
public function index(){
$data = $this->User->all()->toArray();
return $this->helpers->apiArrayResponseBuilder(200, 'success', $data);
}
public function show($id){
$data = $this->User::find($id);
if ($data){
return $this->helpers->apiArrayResponseBuilder(200, 'success', [$data]);
} else {
$this->helpers->apiArrayResponseBuilder(404, 'not found', ['error' => 'Resource id=' . $id . ' could not be found']);
}
}
public function store(Request $request){
$arr = $request->all();
while ( $data = current($arr)) {
$this->User->{key($arr)} = $data;
next($arr);
}
$validation = Validator::make($request->all(), $this->User->rules);
if( $validation->passes() ){
$this->User->save();
return $this->helpers->apiArrayResponseBuilder(201, 'created', ['id' => $this->User->id]);
}else{
return $this->helpers->apiArrayResponseBuilder(400, 'fail', $validation->errors() );
}
}
public function update($id, Request $request){
$status = $this->User->where('id',$id)->update($data);
if( $status ){
return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been updated successfully.');
}else{
return $this->helpers->apiArrayResponseBuilder(400, 'bad request', 'Error, data failed to update.');
}
}
public function delete($id){
$this->User->where('id',$id)->delete();
return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been deleted successfully.');
}
public function destroy($id){
$this->User->where('id',$id)->delete();
return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been deleted successfully.');
}
public static function getAfterFilters() {return [];}
public static function getBeforeFilters() {return [];}
public static function getMiddleware() {return [];}
public function callAction($method, $parameters=false) {
return call_user_func_array(array($this, $method), $parameters);
}
}

View File

@ -0,0 +1,18 @@
<div data-control="toolbar">
<a href="<?= Backend::url('ahmadfatoni/apigenerator/apigeneratorcontroller/create') ?>" class="btn btn-primary oc-icon-plus"><?= e(trans('backend::lang.form.create')) ?></a>
<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>
</div>

View File

@ -0,0 +1,3 @@
<div data-control="toolbar">
<a href="<?= Backend::url('ahmadfatoni/apigenerator/apigeneratorcontroller') ?>" class="btn btn-primary oc-icon-caret-left"><?= e(trans('backend::lang.form.return_to_list')) ?></a>
</div>

View File

@ -0,0 +1,10 @@
name: ApiGeneratorController
form: $/ahmadfatoni/apigenerator/models/apigenerator/fields.yaml
modelClass: AhmadFatoni\ApiGenerator\Models\ApiGenerator
defaultRedirect: ahmadfatoni/apigenerator/apigeneratorcontroller
create:
redirect: 'ahmadfatoni/apigenerator/apigeneratorcontroller/update/:id'
redirectClose: ahmadfatoni/apigenerator/apigeneratorcontroller
update:
redirect: ahmadfatoni/apigenerator/apigeneratorcontroller
redirectClose: ahmadfatoni/apigenerator/apigeneratorcontroller

View File

@ -0,0 +1,11 @@
list: $/ahmadfatoni/apigenerator/models/apigenerator/columns.yaml
modelClass: AhmadFatoni\ApiGenerator\Models\ApiGenerator
title: ApiGeneratorController
noRecordsMessage: 'backend::lang.list.no_records'
showSetup: true
showCheckboxes: true
toolbar:
buttons: list_toolbar
search:
prompt: 'backend::lang.list.search_prompt'
recordUrl: 'ahmadfatoni/apigenerator/apigeneratorcontroller/update/:id'

View File

@ -0,0 +1,4 @@
title: ApiGeneratorController
modelClass: AhmadFatoni\ApiGenerator\Models\ApiGenerator
toolbar:
buttons: reorder_toolbar

View File

@ -0,0 +1,97 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?= Backend::url('ahmadfatoni/apigenerator/apigeneratorcontroller') ?>">ApiListController</a></li>
<li><?= e($this->pageTitle) ?></li>
</ul>
<?php Block::endPut() ?>
<?php if (!$this->fatalError): ?>
<?= Form::open(['class' => 'layout']) ?>
<div class="layout-row">
<?= $this->formRender() ?>
</div>
<div class="modal fade" id="modal-id">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Custom Condition</h4>
</div>
<div class="modal-body">
<?php
$data = '{
"fillable": "id,title,content",
"relation": [{
"name": "user",
"fillable": "id,first_name"
}, {
"name": "categories",
"fillable": "id,name"
}]
}';
echo $data ;
?>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="form-buttons">
<div class="loading-indicator-container">
<button
type="submit"
data-request="onSave"
data-request-success="saveData()"
data-hotkey="ctrl+s, cmd+s"
data-load-indicator="<?= e(trans('backend::lang.form.saving')) ?>"
class="btn btn-primary">
<?= e(trans('backend::lang.form.create')) ?>
</button>
<a data-toggle="modal" href='#modal-id'>
<button type="button" class="btn btn-default">
Example Custom Condition
</button>
</a>
<a href="<?= Backend::url('ahmadfatoni/apigenerator/apigeneratorcontroller') ?>" class="btn btn-warning">Cancel</a>
</div>
</div>
<?= Form::close() ?>
<form method="post" id="generate" accept-charset="utf-8" action="<?= route('fatoni.generate.api') ?>" style="display:none">
<input type='text' name='name' id="name">
<input type='text' name='model' id="model">
<input type='text' name='custom_format' id="custom_format">
<input type='text' name='endpoint' id="endpoint">
<button
type="submit"
class="btn btn-primary" name="send" id="send">
send
</button>
</form>
<?php else: ?>
<p class="flash-message static error"><?= e(trans($this->fatalError)) ?></p>
<p><a href="<?= Backend::url('ahmadfatoni/apigenerator/apigeneratorcontroller') ?>" class="btn btn-default"><?= e(trans('backend::lang.form.return_to_list')) ?></a></p>
<?php endif ?>
<script type="text/javascript">
function saveData(){
document.getElementById('name').value = document.getElementById('Form-field-ApiGenerator-name').value;
document.getElementById('model').value = document.getElementById('Form-field-ApiGenerator-model').value;
document.getElementById('custom_format').value = document.getElementById('Form-field-ApiGenerator-custom_format').value;
document.getElementById('endpoint').value = document.getElementById('Form-field-ApiGenerator-endpoint').value;
document.getElementById('send').click();
}
</script>

View File

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

View File

@ -0,0 +1,22 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?= Backend::url('ahmadfatoni/apigenerator/apigeneratorcontroller') ?>">ApiGeneratorController</a></li>
<li><?= e($this->pageTitle) ?></li>
</ul>
<?php Block::endPut() ?>
<?php if (!$this->fatalError): ?>
<div class="form-preview">
<?= $this->formRenderPreview() ?>
</div>
<?php else: ?>
<p class="flash-message static error"><?= e($this->fatalError) ?></p>
<?php endif ?>
<p>
<a href="<?= Backend::url('ahmadfatoni/apigenerator/apigeneratorcontroller') ?>" class="btn btn-default oc-icon-chevron-left">
<?= e(trans('backend::lang.form.return_to_list')) ?>
</a>
</p>

View File

@ -0,0 +1,8 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?= Backend::url('ahmadfatoni/apigenerator/apigeneratorcontroller') ?>">ApiGeneratorController</a></li>
<li><?= e($this->pageTitle) ?></li>
</ul>
<?php Block::endPut() ?>
<?= $this->reorderRender() ?>

View File

@ -0,0 +1,133 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?= Backend::url('ahmadfatoni/apigenerator/apigeneratorcontroller') ?>">ApiListController</a></li>
<li><?= e($this->pageTitle) ?></li>
</ul>
<?php Block::endPut() ?>
<?php if (!$this->fatalError): ?>
<?= Form::open(['class' => 'layout']) ?>
<div class="layout-row">
<?= $this->formRender() ?>
</div>
<div class="modal fade" id="modal-id">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Custom Condition</h4>
</div>
<div class="modal-body">
<?php
$data = '{
"fillable": "id,title,content",
"relation": [{
"name": "user",
"fillable": "id,first_name"
}, {
"name": "categories",
"fillable": "id,name"
}]
}';
echo $data ;
?>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="form-buttons">
<div class="loading-indicator-container">
<button
type="submit"
data-request="onSave"
data-request-success="saveData()"
data-request-data="redirect:0"
data-hotkey="ctrl+s, cmd+s"
data-load-indicator="<?= e(trans('backend::lang.form.saving')) ?>"
class="btn btn-primary">
<?= e(trans('backend::lang.form.save')) ?>
</button>
<a href="<?= Backend::url('ahmadfatoni/apigenerator/apigeneratorcontroller') ?>" class="btn btn-warning"><div id="cancel">Cancel</div></a>
<button
type="button"
class="oc-icon-trash-o btn-icon danger pull-right"
data-request="onDelete"
data-request-success="delData()"
data-load-indicator="<?= e(trans('backend::lang.form.deleting')) ?>"
data-request-confirm="<?= e(trans('backend::lang.form.confirm_delete')) ?>">
</button>
<a data-toggle="modal" href='#modal-id'>
<button type="button" class="btn btn-default">
Example Custom Condition
</button>
</a>
</div>
</div>
<?= Form::close() ?>
<?php else: ?>
<p class="flash-message static error"><?= e(trans($this->fatalError)) ?></p>
<p><a href="<?= Backend::url('ahmadfatoni/apigenerator/apigeneratorcontroller') ?>" class="btn btn-default"><?= e(trans('backend::lang.form.return_to_list')) ?></a></p>
<?php endif ?>
<form method="get" id="del" style="display:none" action="<?= route('fatoni.delete.api', ['id'=> $this->widget->form->data->attributes['name'] ]) ?>">
<button
type="submit"
class="btn btn-primary" name="send" id="send">
send
</button>
</form>
<form method="post" id="generate" accept-charset="utf-8" action="<?= route('fatoni.generate.api') ?>" style="display:none">
<input type='text' name='id' id="id" value="<?= $this->widget->form->data->attributes['id'] ?>">
<input type='text' name='name' id="name">
<input type='text' name='endpoint' id="endpoint">
<input type='text' name='oldname' id="oldname" value="<?= $this->widget->form->data->attributes['name'] ?>">
<input type='text' name='oldmodel' id="oldmodel" value="<?= $this->widget->form->data->attributes['model'] ?>">
<input type='text' name='oldendpoint' id="oldendpoint" value="<?= $this->widget->form->data->attributes['endpoint'] ?>">
<textarea name='oldcustom_format' id="oldcustom_format"><?= $this->widget->form->data->attributes['custom_format'] ?></textarea>
<input type='text' name='model' id="model">
<input type='text' name='custom_format' id="custom_format">
<button
type="submit"
class="btn btn-primary" name="save" id="save">
send
</button>
</form>
<script type="text/javascript">
function delData(){
document.getElementById('send').click();
}
</script>
<script type="text/javascript">
function saveData(){
document.getElementById('name').value = document.getElementById('Form-field-ApiGenerator-name').value;
document.getElementById('model').value = document.getElementById('Form-field-ApiGenerator-model').value;
document.getElementById('custom_format').value = document.getElementById('Form-field-ApiGenerator-custom_format').value;
document.getElementById('endpoint').value = document.getElementById('Form-field-ApiGenerator-endpoint').value;
if (
document.getElementById('name').value != document.getElementById('oldname').value ||
document.getElementById('Form-field-ApiGenerator-model').value != document.getElementById('oldmodel').value ||
document.getElementById('Form-field-ApiGenerator-custom_format').value != document.getElementById('oldcustom_format').value ||
document.getElementById('Form-field-ApiGenerator-endpoint').value != document.getElementById('oldendpoint').value
){
document.getElementById('save').click();
}else{
document.getElementById('cancel').click();
}
}
</script>

View File

@ -0,0 +1,19 @@
<?php namespace AhmadFatoni\ApiGenerator\Helpers;
Class Helpers {
public function apiArrayResponseBuilder($statusCode = null, $message = null, $data = [])
{
$arr = [
'status_code' => (isset($statusCode)) ? $statusCode : 500,
'message' => (isset($message)) ? $message : 'error'
];
if (count($data) > 0) {
$arr['data'] = $data;
}
return response()->json($arr, $arr['status_code']);
//return $arr;
}
}

View File

@ -0,0 +1,6 @@
<?php return [
'plugin' => [
'name' => 'API-Generator',
'description' => 'Generate API base on Builder Plugin'
]
];

View File

@ -0,0 +1,76 @@
<?php namespace AhmadFatoni\ApiGenerator\Models;
use Model, Log;
use RainLab\Builder\Classes\ComponentHelper;
/**
* Model
*/
class ApiGenerator extends Model
{
use \October\Rain\Database\Traits\Validation;
/*
* Validation
*/
public $rules = [
'name' => 'required|unique:ahmadfatoni_apigenerator_data,name|regex:/^[\pL\s\-]+$/u',
'endpoint' => 'required|unique:ahmadfatoni_apigenerator_data,endpoint',
'custom_format' => 'json'
];
public $customMessages = [
'custom_format.json' => 'Invalid Json Format Custom Condition'
];
/*
* Disable timestamps by default.
* Remove this line if timestamps are defined in the database table.
*/
public $timestamps = false;
/**
* @var string The database table used by the model.
*/
public $table = 'ahmadfatoni_apigenerator_data';
/**
* get model List
* @return [type] [description]
*/
public function getModelOptions(){
return ComponentHelper::instance()->listGlobalModels();
}
/**
* [setCustomFormatAttribute description]
* @param [type] $value [description]
*/
public function setCustomFormatAttribute($value){
$json = str_replace('\t', '', $value);
$json = json_decode($json);
if( $json != null){
if( ! isset($json->fillable) AND ! isset($json->relation) ){
return $this->attributes['custom_format'] = 'invalid format';
}
if( isset($json->relation) AND $json->relation != null ){
foreach ($json->relation as $key) {
if( !isset($key->name) OR $key->name == null ){
return $this->attributes['custom_format'] = 'invalid format';
}
}
}
}
return $this->attributes['custom_format'] = $value;
}
}

View File

@ -0,0 +1,9 @@
columns:
name:
label: 'API NAME'
type: text
searchable: true
sortable: true
endpoint:
label: 'BASE ENDPOINT'
type: text

View File

@ -0,0 +1,33 @@
fields:
name:
label: 'API Name'
oc.commentPosition: ''
span: auto
placeholder: 'Name of your API'
required: 1
type: text
endpoint:
label: 'Base Endpoint'
oc.commentPosition: ''
span: auto
placeholder: api/v1/modulename
required: 1
type: text
description:
label: 'Short Description'
oc.commentPosition: ''
span: auto
placeholder: 'Descript your API'
type: text
model:
label: 'Select Model'
oc.commentPosition: ''
span: auto
required: 1
type: dropdown
custom_format:
label: 'Custom Condition (Fillable and Relation)'
size: large
oc.commentPosition: ''
span: full
type: textarea

View File

@ -0,0 +1,17 @@
plugin:
name: 'ahmadfatoni.apigenerator::lang.plugin.name'
description: 'ahmadfatoni.apigenerator::lang.plugin.description'
author: AhmadFatoni
icon: oc-icon-bolt
homepage: ''
navigation:
api-generator:
label: 'API Generator'
url: ahmadfatoni/apigenerator/apigeneratorcontroller
icon: icon-cogs
permissions:
- ahmadfatoni.apigenerator.manage
permissions:
ahmadfatoni.apigenerator.manage:
tab: 'API Generator'
label: 'Manage the API Generator'

View File

@ -0,0 +1,12 @@
<?php
Route::post('fatoni/generate/api', array('as' => 'fatoni.generate.api', 'uses' => 'AhmadFatoni\ApiGenerator\Controllers\ApiGeneratorController@generateApi'));
Route::post('fatoni/update/api/{id}', array('as' => 'fatoni.update.api', 'uses' => 'AhmadFatoni\ApiGenerator\Controllers\ApiGeneratorController@updateApi'));
Route::get('fatoni/delete/api/{id}', array('as' => 'fatoni.delete.api', 'uses' => 'AhmadFatoni\ApiGenerator\Controllers\ApiGeneratorController@deleteApi'));
Route::resource('api/v1/credit_data', 'AhmadFatoni\ApiGenerator\Controllers\API\creditController', ['except' => ['destroy', 'create', 'edit']]);
Route::get('api/v1/credit_data/{id}/delete', ['as' => 'api/v1/credit_data.delete', 'uses' => 'AhmadFatoni\ApiGenerator\Controllers\API\creditController@destroy']);
Route::resource('api/v1/card_data', 'AhmadFatoni\ApiGenerator\Controllers\API\cardController', ['except' => ['destroy', 'create', 'edit']]);
Route::get('api/v1/card_data/{id}/delete', ['as' => 'api/v1/card_data.delete', 'uses' => 'AhmadFatoni\ApiGenerator\Controllers\API\cardController@destroy']);
Route::resource('api/v1/type_account_replenishment', 'AhmadFatoni\ApiGenerator\Controllers\API\typeAccountReplenishmentController', ['except' => ['destroy', 'create', 'edit']]);
Route::get('api/v1/type_account_replenishment/{id}/delete', ['as' => 'api/v1/type_account_replenishment.delete', 'uses' => 'AhmadFatoni\ApiGenerator\Controllers\API\typeAccountReplenishmentController@destroy']);

View File

@ -0,0 +1,99 @@
<?php namespace AhmadFatoni\ApiGenerator\Controllers\API;
use Cms\Classes\Controller;
use BackendMenu;
use Illuminate\Http\Request;
use AhmadFatoni\ApiGenerator\Helpers\Helpers;
use Illuminate\Support\Facades\Validator;
use {{model}};
class {{controllername}}Controller extends Controller
{
protected ${{modelname}};
protected $helpers;
public function __construct({{modelname}} ${{modelname}}, Helpers $helpers)
{
parent::__construct();
$this->{{modelname}} = ${{modelname}};
$this->helpers = $helpers;
}
public function index(){
$data = $this->{{modelname}}->all()->toArray();
return $this->helpers->apiArrayResponseBuilder(200, 'success', $data);
}
public function show($id){
$data = $this->{{modelname}}::find($id);
if ($data){
return $this->helpers->apiArrayResponseBuilder(200, 'success', [$data]);
} else {
$this->helpers->apiArrayResponseBuilder(404, 'not found', ['error' => 'Resource id=' . $id . ' could not be found']);
}
}
public function store(Request $request){
$arr = $request->all();
while ( $data = current($arr)) {
$this->{{modelname}}->{key($arr)} = $data;
next($arr);
}
$validation = Validator::make($request->all(), $this->{{modelname}}->rules);
if( $validation->passes() ){
$this->{{modelname}}->save();
return $this->helpers->apiArrayResponseBuilder(201, 'created', ['id' => $this->{{modelname}}->id]);
}else{
return $this->helpers->apiArrayResponseBuilder(400, 'fail', $validation->errors() );
}
}
public function update($id, Request $request){
$status = $this->{{modelname}}->where('id',$id)->update($data);
if( $status ){
return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been updated successfully.');
}else{
return $this->helpers->apiArrayResponseBuilder(400, 'bad request', 'Error, data failed to update.');
}
}
public function delete($id){
$this->{{modelname}}->where('id',$id)->delete();
return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been deleted successfully.');
}
public function destroy($id){
$this->{{modelname}}->where('id',$id)->delete();
return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been deleted successfully.');
}
public static function getAfterFilters() {return [];}
public static function getBeforeFilters() {return [];}
public static function getMiddleware() {return [];}
public function callAction($method, $parameters=false) {
return call_user_func_array(array($this, $method), $parameters);
}
}

View File

@ -0,0 +1,35 @@
<?php namespace AhmadFatoni\ApiGenerator\Controllers\API;
use Cms\Classes\Controller;
use BackendMenu;
use Illuminate\Http\Request;
use {{model}};
class ApiListController extends Controller
{
protected ${{modelname}};
public function __construct({{modelname}} ${{modelname}})
{
parent::__construct();
$this->{{modelname}} = ${{modelname}};
}
public static function getAfterFilters() {return [];}
public static function getBeforeFilters() {return [];}
public static function getMiddleware() {return [];}
public function callAction($method, $parameters=false) {
return call_user_func_array(array($this, $method), $parameters);
}
// public function create(Request $request){
// $arr = $request->all();
// while ( $data = current($arr)) {
// $this->
// }
// return json_encode($this->{{modelname}}->store($request));
// }
}

View File

@ -0,0 +1,83 @@
<?php namespace AhmadFatoni\ApiGenerator\Controllers\API;
use Cms\Classes\Controller;
use BackendMenu;
use Illuminate\Http\Request;
use AhmadFatoni\ApiGenerator\Helpers\Helpers;
use {{model}};
class {{controllername}}Controller extends Controller
{
protected ${{modelname}};
protected $helpers;
public function __construct({{modelname}} ${{modelname}}, Helpers $helpers)
{
parent::__construct();
$this->{{modelname}} = ${{modelname}};
$this->helpers = $helpers;
}
{{select}}
{{show}}
public function store(Request $request){
$arr = $request->all();
while ( $data = current($arr)) {
$this->{{modelname}}->{key($arr)} = $data;
next($arr);
}
$validation = Validator::make($request->all(), $this->{{modelname}}->rules);
if( $validation->passes() ){
$this->{{modelname}}->save();
return $this->helpers->apiArrayResponseBuilder(201, 'created', ['id' => $this->{{modelname}}->id]);
}else{
return $this->helpers->apiArrayResponseBuilder(400, 'fail', $validation->errors() );
}
}
public function update($id, Request $request){
$status = $this->{{modelname}}->where('id',$id)->update($data);
if( $status ){
return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been updated successfully.');
}else{
return $this->helpers->apiArrayResponseBuilder(400, 'bad request', 'Error, data failed to update.');
}
}
public function delete($id){
$this->{{modelname}}->where('id',$id)->delete();
return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been deleted successfully.');
}
public function destroy($id){
$this->{{modelname}}->where('id',$id)->delete();
return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been deleted successfully.');
}
public static function getAfterFilters() {return [];}
public static function getBeforeFilters() {return [];}
public static function getMiddleware() {return [];}
public function callAction($method, $parameters=false) {
return call_user_func_array(array($this, $method), $parameters);
}
}

View File

@ -0,0 +1,3 @@
Route::resource('{{modelname}}', 'AhmadFatoni\ApiGenerator\Controllers\API\{{controllername}}Controller', ['except' => ['destroy', 'create', 'edit']]);
Route::get('{{modelname}}/{id}/delete', ['as' => '{{modelname}}.delete', 'uses' => 'AhmadFatoni\ApiGenerator\Controllers\API\{{controllername}}Controller@destroy']);

View File

@ -0,0 +1,6 @@
<?php
Route::post('fatoni/generate/api', array('as' => 'fatoni.generate.api', 'uses' => 'AhmadFatoni\ApiGenerator\Controllers\ApiGeneratorController@generateApi'));
Route::post('fatoni/update/api/{id}', array('as' => 'fatoni.update.api', 'uses' => 'AhmadFatoni\ApiGenerator\Controllers\ApiGeneratorController@updateApi'));
Route::get('fatoni/delete/api/{id}', array('as' => 'fatoni.delete.api', 'uses' => 'AhmadFatoni\ApiGenerator\Controllers\ApiGeneratorController@deleteApi'));
{{route}}

View File

@ -0,0 +1,26 @@
<?php namespace AhmadFatoni\ApiGenerator\Updates;
use Schema;
use October\Rain\Database\Updates\Migration;
class BuilderTableCreateAhmadfatoniApigeneratorData extends Migration
{
public function up()
{
Schema::create('ahmadfatoni_apigenerator_data', function($table)
{
$table->engine = 'InnoDB';
$table->increments('id');
$table->string('name');
$table->string('endpoint');
$table->string('model');
$table->string('description')->nullable();
$table->text('custom_format')->nullable();
});
}
public function down()
{
Schema::dropIfExists('ahmadfatoni_apigenerator_data');
}
}

View File

@ -0,0 +1,15 @@
1.0.1:
- 'Initialize plugin.'
1.0.2:
- 'Database implementation'
1.0.3:
- 'add builder plugin on requirements dependency'
- builder_table_create_ahmadfatoni_apigenerator_data.php
1.0.4:
- 'fixing bug on PHP 7'
1.0.5:
- 'fixing bug on request delete data'
1.0.6:
- 'fixing bug on generate endpoint'
1.0.7:
- 'fixing bug on October CMS v1.0.456'

1
plugins/offline/cors/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/vendor/

View File

@ -0,0 +1,7 @@
# How to contribute
Contributions to this project are highly welcome.
1. Submit your pull requests to the `develop` branch
1. Adhere to the [PSR-2 coding](http://www.php-fig.org/psr/psr-2/) standard
1. If you are not sure if your ideas are fit for this project, create an issue and ask

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 OFFLINE GmbH
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,49 @@
<?php namespace OFFLINE\CORS;
use OFFLINE\CORS\Classes\HandleCors;
use OFFLINE\CORS\Classes\HandlePreflight;
use OFFLINE\CORS\Classes\ServiceProvider;
use OFFLINE\CORS\Models\Settings;
use System\Classes\PluginBase;
class Plugin extends PluginBase
{
public function boot()
{
\App::register(ServiceProvider::class);
$this->app['Illuminate\Contracts\Http\Kernel']
->prependMiddleware(HandleCors::class);
if (request()->isMethod('OPTIONS')) {
$this->app['Illuminate\Contracts\Http\Kernel']
->prependMiddleware(HandlePreflight::class);
}
}
public function registerPermissions()
{
return [
'offline.cors.manage' => [
'label' => 'Can manage cors settings',
'tab' => 'CORS',
],
];
}
public function registerSettings()
{
return [
'cors' => [
'label' => 'CORS-Settings',
'description' => 'Manage CORS headers',
'category' => 'system::lang.system.categories.cms',
'icon' => 'icon-code',
'class' => Settings::class,
'order' => 500,
'keywords' => 'cors',
'permissions' => ['offline.cors.manage'],
],
];
}
}

View File

@ -0,0 +1,45 @@
# CORS plugin for October CMS
This plugin is based on [https://github.com/barryvdh/laravel-cors](https://github.com/barryvdh/laravel-cors/blob/master/config/cors.php).
All configuration for the plugin can be done via the backend settings.
The following cors headers are supported:
* Access-Control-Allow-Origin
* Access-Control-Allow-Headers
* Access-Control-Allow-Methods
* Access-Control-Allow-Credentials
* Access-Control-Expose-Headers
* Access-Control-Max-Age
Currently these headers are sent for every request. There is no per-route configuration possible at this time.
## Setup
After installing the plugin visit the CORS settings page in your October CMS backend settings.
You can add `*` as an entry to `Allowed origins`, `Allowed headers` and `Allowed methods` to allow any kind of CORS request from everywhere.
It is advised to be more explicit about these settings. You can add values for each header via the repeater fields.
> It is important to set these intial settings once for the plugin to work as excpected!
### Filesystem configuration
As an alternative to the backend settings you can create a `config/config.php` file in the plugins root directory to configure it.
The filesystem configuration will overwrite any defined backend setting.
```php
<?php
// plugins/offline/cors/config/config.php
return [
'supportsCredentials' => true,
'maxAge' => 3600,
'allowedOrigins' => ['*'],
'allowedHeaders' => ['*'],
'allowedMethods' => ['GET', 'POST'],
'exposedHeaders' => [''],
];
```

View File

@ -0,0 +1,71 @@
<?php
namespace OFFLINE\CORS\Classes;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* Based on asm89/stack-cors
*/
class Cors implements HttpKernelInterface
{
/**
* @var \Symfony\Component\HttpKernel\HttpKernelInterface
*/
private $app;
/**
* @var CorsService
*/
private $cors;
private $defaultOptions = [
'allowedHeaders' => [],
'allowedMethods' => [],
'allowedOrigins' => [],
'exposedHeaders' => false,
'maxAge' => false,
'supportsCredentials' => false,
];
/**
* Cors constructor.
*
* @param HttpKernelInterface $app
* @param array $options
*/
public function __construct(HttpKernelInterface $app, array $options = [])
{
$this->app = $app;
$this->cors = new CorsService(array_merge($this->defaultOptions, $options));
}
/**
* @param Request $request
* @param int $type
* @param bool $catch
*
* @return bool|Response
*/
public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
{
if ( ! $this->cors->isCorsRequest($request)) {
return $this->app->handle($request, $type, $catch);
}
if ($this->cors->isPreflightRequest($request)) {
return $this->cors->handlePreflightRequest($request);
}
if ( ! $this->cors->isActualRequestAllowed($request)) {
return new Response('Not allowed.', 403);
}
$response = $this->app->handle($request, $type, $catch);
return $this->cors->addActualRequestHeaders($response, $request);
}
}

View File

@ -0,0 +1,199 @@
<?php
namespace OFFLINE\CORS\Classes;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Based on asm89/stack-cors
*/
class CorsService
{
private $options;
public function __construct(array $options = [])
{
$this->options = $this->normalizeOptions($options);
}
private function normalizeOptions(array $options = [])
{
$options += [
'supportsCredentials' => false,
'maxAge' => 0,
];
// Make sure these values are arrays, if not specified in the backend settings.
$arrayKeys = [
'allowedOrigins',
'allowedHeaders',
'exposedHeaders',
'allowedMethods',
];
foreach ($arrayKeys as $key) {
if (!$options[$key]) {
$options[$key] = [];
}
}
// normalize array('*') to true
if (in_array('*', $options['allowedOrigins'])) {
$options['allowedOrigins'] = true;
}
if (in_array('*', $options['allowedHeaders'])) {
$options['allowedHeaders'] = true;
} else {
$options['allowedHeaders'] = array_map('strtolower', $options['allowedHeaders']);
}
if (in_array('*', $options['allowedMethods'])) {
$options['allowedMethods'] = true;
} else {
$options['allowedMethods'] = array_map('strtoupper', $options['allowedMethods']);
}
return $options;
}
public function isActualRequestAllowed(Request $request)
{
return $this->checkOrigin($request);
}
public function isCorsRequest(Request $request)
{
return $request->headers->has('Origin') && $request->headers->get('Origin') !== $request->getSchemeAndHttpHost();
}
public function isPreflightRequest(Request $request)
{
return $this->isCorsRequest($request)
&& $request->getMethod() === 'OPTIONS'
&& $request->headers->has('Access-Control-Request-Method');
}
public function addActualRequestHeaders(Response $response, Request $request)
{
if ( ! $this->checkOrigin($request)) {
return $response;
}
$response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin'));
if ( ! $response->headers->has('Vary')) {
$response->headers->set('Vary', 'Origin');
} else {
$response->headers->set('Vary', $response->headers->get('Vary') . ', Origin');
}
if ($this->options['supportsCredentials']) {
$response->headers->set('Access-Control-Allow-Credentials', 'true');
}
if ($this->options['exposedHeaders']) {
$response->headers->set('Access-Control-Expose-Headers', implode(', ', $this->options['exposedHeaders']));
}
return $response;
}
public function handlePreflightRequest(Request $request)
{
if (true !== $check = $this->checkPreflightRequestConditions($request)) {
return $check;
}
return $this->buildPreflightCheckResponse($request);
}
private function buildPreflightCheckResponse(Request $request)
{
$response = new Response();
if ($this->options['supportsCredentials']) {
$response->headers->set('Access-Control-Allow-Credentials', 'true');
}
$response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin'));
if ($this->options['maxAge']) {
$response->headers->set('Access-Control-Max-Age', $this->options['maxAge']);
}
$allowMethods = $this->options['allowedMethods'] === true
? strtoupper($request->headers->get('Access-Control-Request-Method'))
: implode(', ', $this->options['allowedMethods']);
$response->headers->set('Access-Control-Allow-Methods', $allowMethods);
$allowHeaders = $this->options['allowedHeaders'] === true
? strtoupper($request->headers->get('Access-Control-Request-Headers'))
: implode(', ', $this->options['allowedHeaders']);
$response->headers->set('Access-Control-Allow-Headers', $allowHeaders);
return $response;
}
private function checkPreflightRequestConditions(Request $request)
{
if ( ! $this->checkOrigin($request)) {
return $this->createBadRequestResponse(403, 'Origin not allowed');
}
if ( ! $this->checkMethod($request)) {
return $this->createBadRequestResponse(405, 'Method not allowed');
}
$requestHeaders = [];
// if allowedHeaders has been set to true ('*' allow all flag) just skip this check
if ($this->options['allowedHeaders'] !== true && $request->headers->has('Access-Control-Request-Headers')) {
$headers = strtolower($request->headers->get('Access-Control-Request-Headers'));
$requestHeaders = explode(',', $headers);
foreach ($requestHeaders as $header) {
if ( ! in_array(trim($header), $this->options['allowedHeaders'])) {
return $this->createBadRequestResponse(403, 'Header not allowed');
}
}
}
return true;
}
private function createBadRequestResponse($code, $reason = '')
{
return new Response($reason, $code);
}
private function checkOrigin(Request $request)
{
if ($this->options['allowedOrigins'] === true) {
// allow all '*' flag
return true;
}
$origin = $request->headers->get('Origin');
foreach ($this->options['allowedOrigins'] as $allowedOrigin) {
if (OriginMatcher::matches($allowedOrigin, $origin)) {
return true;
}
}
return false;
}
private function checkMethod(Request $request)
{
if ($this->options['allowedMethods'] === true) {
// allow all '*' flag
return true;
}
$requestMethod = strtoupper($request->headers->get('Access-Control-Request-Method'));
return in_array($requestMethod, $this->options['allowedMethods']);
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace OFFLINE\CORS\Classes;
use Closure;
class HandleCors
{
/**
* The CORS service
*
* @var CorsService
*/
protected $cors;
/**
* @param CorsService $cors
*/
public function __construct(CorsService $cors)
{
$this->cors = $cors;
}
/**
* Handle an incoming request. Based on Asm89\Stack\Cors by asm89
* @see https://github.com/asm89/stack-cors/blob/master/src/Asm89/Stack/Cors.php
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
*/
public function handle($request, Closure $next)
{
if ( ! $this->cors->isCorsRequest($request)) {
return $next($request);
}
if ( ! $this->cors->isActualRequestAllowed($request)) {
abort(403);
}
/** @var \Illuminate\Http\Response $response */
$response = $next($request);
return $this->cors->addActualRequestHeaders($response, $request);
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace OFFLINE\CORS\Classes;
use Closure;
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Routing\Router;
class HandlePreflight
{
/**
* @param CorsService $cors
*/
public function __construct(CorsService $cors, Router $router, Kernel $kernel)
{
$this->cors = $cors;
$this->router = $router;
$this->kernel = $kernel;
}
/**
* Handle an incoming OPTIONS request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
*/
public function handle($request, Closure $next)
{
$response = $next($request);
if ($this->cors->isPreflightRequest($request) && $this->hasMatchingCorsRoute($request)) {
$preflight = $this->cors->handlePreflightRequest($request);
$response->headers->add($preflight->headers->all());
}
$response->setStatusCode(204);
return $response;
}
/**
* Verify the current OPTIONS request matches a CORS-enabled route
*
* @param \Illuminate\Http\Request $request
*
* @return boolean
*/
private function hasMatchingCorsRoute($request)
{
// Check if CORS is added in a global middleware
if ($this->kernel->hasMiddleware(HandleCors::class)) {
return true;
}
// Check if CORS is added as a route middleware
$request = clone $request;
$request->setMethod($request->header('Access-Control-Request-Method'));
try {
$route = $this->router->getRoutes()->match($request);
// change of method name in laravel 5.3
if (method_exists($this->router, 'gatherRouteMiddleware')) {
$middleware = $this->router->gatherRouteMiddleware($route);
} else {
$middleware = $this->router->gatherRouteMiddlewares($route);
}
return in_array(HandleCors::class, $middleware);
} catch (\Exception $e) {
app('log')->error($e);
return false;
}
}
}

View File

@ -0,0 +1,100 @@
<?php
namespace OFFLINE\CORS\Classes;
class OriginMatcher
{
public static function matches($pattern, $origin)
{
if ($pattern === $origin) {
return true;
}
$scheme = parse_url($origin, PHP_URL_SCHEME);
$host = parse_url($origin, PHP_URL_HOST);
$port = parse_url($origin, PHP_URL_PORT);
$schemePattern = static::parseOriginPattern($pattern, PHP_URL_SCHEME);
$hostPattern = static::parseOriginPattern($pattern, PHP_URL_HOST);
$portPattern = static::parseOriginPattern($pattern, PHP_URL_PORT);
$schemeMatches = static::schemeMatches($schemePattern, $scheme);
$hostMatches = static::hostMatches($hostPattern, $host);
$portMatches = static::portMatches($portPattern, $port);
return $schemeMatches && $hostMatches && $portMatches;
}
public static function schemeMatches($pattern, $scheme)
{
return is_null($pattern) || $pattern === $scheme;
}
public static function hostMatches($pattern, $host)
{
$patternComponents = array_reverse(explode('.', $pattern));
$hostComponents = array_reverse(explode('.', $host));
foreach ($patternComponents as $index => $patternComponent) {
if ($patternComponent === '*') {
return true;
}
if ( ! isset($hostComponents[$index])) {
return false;
}
if ($hostComponents[$index] !== $patternComponent) {
return false;
}
}
return count($patternComponents) === count($hostComponents);
}
public static function portMatches($pattern, $port)
{
if ($pattern === "*") {
return true;
}
if ((string)$pattern === "") {
return (string)$port === "";
}
if (preg_match('/\A\d+\z/', $pattern)) {
return (string)$pattern === (string)$port;
}
if (preg_match('/\A(?P<from>\d+)-(?P<to>\d+)\z/', $pattern, $captured)) {
return $captured['from'] <= $port && $port <= $captured['to'];
}
throw new \InvalidArgumentException("Invalid port pattern: ${pattern}");
}
public static function parseOriginPattern($originPattern, $component = -1)
{
$matched = preg_match(
'!\A
(?: (?P<scheme> ([a-z][a-z0-9+\-.]*) ):// )?
(?P<host> (?:\*|[\w-]+)(?:\.[\w-]+)* )
(?: :(?P<port> (?: \*|\d+(?:-\d+)? ) ) )?
\z!x',
$originPattern,
$captured
);
if ( ! $matched) {
throw new \InvalidArgumentException("Invalid origin pattern ${originPattern}");
}
$components = [
'scheme' => $captured['scheme'] ?: null,
'host' => $captured['host'],
'port' => array_key_exists('port', $captured) ? $captured['port'] : null,
];
switch ($component) {
case -1:
return $components;
case PHP_URL_SCHEME:
return $components['scheme'];
case PHP_URL_HOST:
return $components['host'];
case PHP_URL_PORT:
return $components['port'];
}
throw new \InvalidArgumentException("Invalid component: ${component}");
}
}

View File

@ -0,0 +1,90 @@
<?php
namespace OFFLINE\CORS\Classes;
use October\Rain\Support\ServiceProvider as BaseServiceProvider;
use OFFLINE\CORS\Models\Settings;
class ServiceProvider extends BaseServiceProvider
{
/**
* Indicates if loading of the provider is deferred.
*
* @var bool
*/
protected $defer = false;
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->app->singleton(CorsService::class, function ($app) {
return new CorsService($this->getSettings());
});
}
/**
* Return default Settings
*/
protected function getSettings()
{
$supportsCredentials = (bool)$this->getConfigValue('supportsCredentials', false);
$maxAge = (int)$this->getConfigValue('maxAge', 0);
$allowedOrigins = $this->getConfigValue('allowedOrigins', []);
$allowedHeaders = $this->getConfigValue('allowedHeaders', []);
$allowedMethods = $this->getConfigValue('allowedMethods', []);
$exposedHeaders = $this->getConfigValue('exposedHeaders', []);
return compact(
'supportsCredentials',
'allowedOrigins',
'allowedHeaders',
'allowedMethods',
'exposedHeaders',
'maxAge'
);
}
/**
* Returns an effective config value.
*
* If a filesystem config is available it takes precedence
* over the backend settings values.
*
* @param $key
* @param null $default
*
* @return mixed
*/
public function getConfigValue($key, $default = null)
{
return $this->filesystemConfig($key) ?: $this->getValues(Settings::get($key, $default));
}
/**
* Return the filesystem config value if available.
*
* @param string $key
*
* @return mixed
*/
public function filesystemConfig($key)
{
return config('offline.cors::' . $key);
}
/**
* Extract the repeater field values.
*
* @param mixed $values
*
* @return array
*/
protected function getValues($values)
{
return \is_array($values) ? collect($values)->pluck('value')->toArray() : $values;
}
}

View File

@ -0,0 +1,13 @@
{
"name": "offline/oc-cors-plugin",
"description": "Setup and manage Cross-Origin Resource Sharing headers in October CMS",
"type": "october-plugin",
"license": "MIT",
"authors": [
{
"name": "Tobias Kündig",
"email": "tobias@offline.swiss"
}
],
"require": {}
}

View File

@ -0,0 +1,6 @@
<?php return [
'plugin' => [
'name' => 'CORS',
'description' => 'Verwalte Cross-Origin Resource Sharing Header in October CMS',
],
];

View File

@ -0,0 +1,6 @@
<?php return [
'plugin' => [
'name' => 'CORS',
'description' => 'Setup and manage Cross-Origin Resource Sharing headers',
],
];

View File

@ -0,0 +1,12 @@
<?php
namespace OFFLINE\CORS\Models;
use Model;
class Settings extends Model
{
public $implement = ['System.Behaviors.SettingsModel'];
public $settingsCode = 'offline_cors_settings';
public $settingsFields = 'fields.yaml';
}

View File

@ -0,0 +1,55 @@
fields:
supportsCredentials:
label: Supports credentials
type: switch
comment: 'Set Access-Control-Allow-Credentials header to true'
default: false
span: left
maxAge:
label: Max age
type: number
comment: 'Set Access-Control-Max-Age to this value'
default: 0
span: right
tabs:
fields:
allowedOrigins:
label: Allowed origins
tab: Allowed origins
type: repeater
span: left
form:
fields:
value:
type: text
label: Origin
allowedHeaders:
label: Allowed headers
tab: Allowed headers
type: repeater
span: left
form:
fields:
value:
type: text
label: Header
allowedMethods:
label: Allowed methods
tab: Allowed methods
type: repeater
span: left
form:
fields:
value:
type: text
label: Method
exposedHeaders:
label: Exposed headers
tab: Exposed headers
type: repeater
span: left
form:
fields:
value:
type: text
label: Header

View File

@ -0,0 +1,6 @@
plugin:
name: 'offline.cors::lang.plugin.name'
description: 'offline.cors::lang.plugin.description'
author: 'OFFLINE GmbH'
icon: oc-icon-code
homepage: ''

View File

@ -0,0 +1,14 @@
1.0.1:
- Initial release.
1.0.2:
- Fixed backend settings label (thanks to LukeTowers)
1.0.3:
- Added support for filesystem configuration file / Added plugin to Packagist (https://packagist.org/packages/offline/oc-cors-plugin)
1.0.4:
- Fixed minor bug when running the plugin without custom settings
1.0.5:
- "Return proper 204 response code for preflight requests (thanks to @adrian-marinescu-ch on GitHub)"
1.0.6:
- "Dummy release to sync with Packagist version"
1.0.7:
- "Optimized compatibility with October v2"

1
plugins/rainlab/blog/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.DS_Store

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,172 @@
<?php namespace RainLab\Blog;
use Backend;
use Controller;
use RainLab\Blog\Models\Post;
use System\Classes\PluginBase;
use RainLab\Blog\Classes\TagProcessor;
use RainLab\Blog\Models\Category;
use Event;
class Plugin extends PluginBase
{
public function pluginDetails()
{
return [
'name' => 'rainlab.blog::lang.plugin.name',
'description' => 'rainlab.blog::lang.plugin.description',
'author' => 'Alexey Bobkov, Samuel Georges',
'icon' => 'icon-pencil',
'homepage' => 'https://github.com/rainlab/blog-plugin'
];
}
public function registerComponents()
{
return [
'RainLab\Blog\Components\Post' => 'blogPost',
'RainLab\Blog\Components\Posts' => 'blogPosts',
'RainLab\Blog\Components\Categories' => 'blogCategories',
'RainLab\Blog\Components\RssFeed' => 'blogRssFeed'
];
}
public function registerPermissions()
{
return [
'rainlab.blog.manage_settings' => [
'tab' => 'rainlab.blog::lang.blog.tab',
'label' => 'rainlab.blog::lang.blog.manage_settings'
],
'rainlab.blog.access_posts' => [
'tab' => 'rainlab.blog::lang.blog.tab',
'label' => 'rainlab.blog::lang.blog.access_posts'
],
'rainlab.blog.access_categories' => [
'tab' => 'rainlab.blog::lang.blog.tab',
'label' => 'rainlab.blog::lang.blog.access_categories'
],
'rainlab.blog.access_other_posts' => [
'tab' => 'rainlab.blog::lang.blog.tab',
'label' => 'rainlab.blog::lang.blog.access_other_posts'
],
'rainlab.blog.access_import_export' => [
'tab' => 'rainlab.blog::lang.blog.tab',
'label' => 'rainlab.blog::lang.blog.access_import_export'
],
'rainlab.blog.access_publish' => [
'tab' => 'rainlab.blog::lang.blog.tab',
'label' => 'rainlab.blog::lang.blog.access_publish'
]
];
}
public function registerNavigation()
{
return [
'blog' => [
'label' => 'rainlab.blog::lang.blog.menu_label',
'url' => Backend::url('rainlab/blog/posts'),
'icon' => 'icon-pencil',
'iconSvg' => 'plugins/rainlab/blog/assets/images/blog-icon.svg',
'permissions' => ['rainlab.blog.*'],
'order' => 300,
'sideMenu' => [
'new_post' => [
'label' => 'rainlab.blog::lang.posts.new_post',
'icon' => 'icon-plus',
'url' => Backend::url('rainlab/blog/posts/create'),
'permissions' => ['rainlab.blog.access_posts']
],
'posts' => [
'label' => 'rainlab.blog::lang.blog.posts',
'icon' => 'icon-copy',
'url' => Backend::url('rainlab/blog/posts'),
'permissions' => ['rainlab.blog.access_posts']
],
'categories' => [
'label' => 'rainlab.blog::lang.blog.categories',
'icon' => 'icon-list-ul',
'url' => Backend::url('rainlab/blog/categories'),
'permissions' => ['rainlab.blog.access_categories']
]
]
]
];
}
public function registerSettings()
{
return [
'blog' => [
'label' => 'rainlab.blog::lang.blog.menu_label',
'description' => 'rainlab.blog::lang.blog.settings_description',
'category' => 'rainlab.blog::lang.blog.menu_label',
'icon' => 'icon-pencil',
'class' => 'RainLab\Blog\Models\Settings',
'order' => 500,
'keywords' => 'blog post category',
'permissions' => ['rainlab.blog.manage_settings']
]
];
}
/**
* Register method, called when the plugin is first registered.
*/
public function register()
{
/*
* Register the image tag processing callback
*/
TagProcessor::instance()->registerCallback(function($input, $preview) {
if (!$preview) {
return $input;
}
return preg_replace('|\<img src="image" alt="([0-9]+)"([^>]*)\/>|m',
'<span class="image-placeholder" data-index="$1">
<span class="upload-dropzone">
<span class="label">Click or drop an image...</span>
<span class="indicator"></span>
</span>
</span>',
$input);
});
}
public function boot()
{
/*
* Register menu items for the RainLab.Pages plugin
*/
Event::listen('pages.menuitem.listTypes', function() {
return [
'blog-category' => 'rainlab.blog::lang.menuitem.blog_category',
'all-blog-categories' => 'rainlab.blog::lang.menuitem.all_blog_categories',
'blog-post' => 'rainlab.blog::lang.menuitem.blog_post',
'all-blog-posts' => 'rainlab.blog::lang.menuitem.all_blog_posts',
'category-blog-posts' => 'rainlab.blog::lang.menuitem.category_blog_posts',
];
});
Event::listen('pages.menuitem.getTypeInfo', function($type) {
if ($type == 'blog-category' || $type == 'all-blog-categories') {
return Category::getMenuTypeInfo($type);
}
elseif ($type == 'blog-post' || $type == 'all-blog-posts' || $type == 'category-blog-posts') {
return Post::getMenuTypeInfo($type);
}
});
Event::listen('pages.menuitem.resolveItem', function($type, $item, $url, $theme) {
if ($type == 'blog-category' || $type == 'all-blog-categories') {
return Category::resolveMenuItem($item, $url, $theme);
}
elseif ($type == 'blog-post' || $type == 'all-blog-posts' || $type == 'category-blog-posts') {
return Post::resolveMenuItem($item, $url, $theme);
}
});
}
}

View File

@ -0,0 +1,318 @@
# Blog Plugin
A simple, extensible blogging platform for October CMS.
[Blog & Forum Building Tutorial Video](https://player.vimeo.com/video/97088926)
## Editing posts
The plugin uses the markdown markup for the posts. You can use any Markdown syntax and some special tags for embedding images and videos (requires RainLab Blog Video plugin). To embed an image use the image placeholder:
![1](image)
The number in the first part is the placeholder index. If you use multiple images in a post you should use an unique index for each image:
![1](image)
![2](image)
You can also add classes or ids to images by using the [markdown extra](http://michelf.ca/projects/php-markdown/extra/) syntax:
![1](image){#id .class}
## Excerpt Vs. Read more
Posts are managed by selecting *Blog > Posts* from the menu. Each post can contain an excerpt by entering some text in this field on the *Manage* tab. This content is displayed on the page using the `summary` attribute of the blog post.
{{ post.summary|raw }}
Alternatively this field can be left blank and the excerpt can be captured from the main content (*Edit* tab). Use the special tag `<!-- more -->` to specify a summary from the main content, all content above this tag will be treated as the summary. For example:
This is a great introduction to a great blog post. This text is included as part of the excerpt / summary.
<!-- more -->
Let's dive in to more detail about why this post is so great. This text will not be included in the summary.
Finally, if no excerpt is specified and the "more" tag is not used, the blog post will capture the first 600 characters of the content and use this for the summary.
## Implementing front-end pages
The plugin provides several components for building the post list page (archive), category page, post details page and category list for the sidebar.
### Post list page
Use the `blogPosts` component to display a list of latest blog posts on a page. The component has the following properties:
* **pageNumber** - this value is used to determine what page the user is on, it should be a routing parameter for the default markup. The default value is **{{ :page }}** to obtain the value from the route parameter `:page`.
* **categoryFilter** - a category slug to filter the posts by. If left blank, all posts are displayed.
* **postsPerPage** - how many posts to display on a single page (the pagination is supported automatically). The default value is 10.
* **noPostsMessage** - message to display in the empty post list.
* **sortOrder** - the column name and direction used for the sort order of the posts. The default value is **published_at desc**.
* **categoryPage** - path to the category page. The default value is **blog/category** - it matches the pages/blog/category.htm file in the theme directory. This property is used in the default component partial for creating links to the blog categories.
* **postPage** - path to the post details page. The default value is **blog/post** - it matches the pages/blog/post.htm file in the theme directory. This property is used in the default component partial for creating links to the blog posts.
* **exceptPost** - ignore a single post by its slug or unique ID. The ignored post will not be included in the list, useful for showing other/related posts.
* **exceptCategories** - ignore posts from a comma-separated list of categories, given by their unique slug. The ignored posts will not be included in the list.
The blogPosts component injects the following variables to the page where it's used:
* **posts** - a list of blog posts loaded from the database.
* **postPage** - contains the value of the `postPage` component's property.
* **category** - the blog category object loaded from the database. If the category is not found, the variable value is **null**.
* **categoryPage** - contains the value of the `categoryPage` component's property.
* **noPostsMessage** - contains the value of the `noPostsMessage` component's property.
The component supports pagination and reads the current page index from the `:page` URL parameter. The next example shows the basic component usage on the blog home page:
title = "Blog"
url = "/blog/:page?"
[blogPosts]
postsPerPage = "5"
==
{% component 'blogPosts' %}
The next example shows the basic component usage with the category filter:
title = "Blog Category"
url = "/blog/category/:slug/:page?"
[blogPosts]
categoryFilter = "{{ :slug }}"
==
function onEnd()
{
// Optional - set the page title to the category name
if ($this->category)
$this->page->title = $this->category->name;
}
==
{% if not category %}
<h2>Category not found</h2>
{% else %}
<h2>{{ category.name }}</h2>
{% component 'blogPosts' %}
{% endif %}
The post list and the pagination are coded in the default component partial `plugins/rainlab/blog/components/posts/default.htm`. If the default markup is not suitable for your website, feel free to copy it from the default partial and replace the `{% component %}` call in the example above with the partial contents.
### Post page
Use the `blogPost` component to display a blog post on a page. The component has the following properties:
* **slug** - the value used for looking up the post by its slug. The default value is **{{ :slug }}** to obtain the value from the route parameter `:slug`.
* **categoryPage** - path to the category page. The default value is **blog/category** - it matches the pages/blog/category.htm file in the theme directory. This property is used in the default component partial for creating links to the blog categories.
The component injects the following variables to the page where it's used:
* **post** - the blog post object loaded from the database. If the post is not found, the variable value is **null**.
The next example shows the basic component usage on the blog page:
title = "Blog Post"
url = "/blog/post/:slug"
[blogPost]
==
<?php
function onEnd()
{
// Optional - set the page title to the post title
if ($this->post)
$this->page->title = $this->post->title;
}
?>
==
{% if post %}
<h2>{{ post.title }}</h2>
{% component 'blogPost' %}
{% else %}
<h2>Post not found</h2>
{% endif %}
The post details is coded in the default component partial `plugins/rainlab/blog/components/post/default.htm`.
### Category list
Use the `blogCategories` component to display a list of blog post categories with links. The component has the following properties:
* **slug** - the value used for looking up the current category by its slug. The default value is **{{ :slug }}** to obtain the value from the route parameter `:slug`.
* **displayEmpty** - determines if empty categories should be displayed. The default value is false.
* **categoryPage** - path to the category page. The default value is **blog/category** - it matches the pages/blog/category.htm file in the theme directory. This property is used in the default component partial for creating links to the blog categories.
The component injects the following variables to the page where it's used:
* **categoryPage** - contains the value of the `categoryPage` component's property.
* **categories** - a list of blog categories loaded from the database.
* **currentCategorySlug** - slug of the current category. This property is used for marking the current category in the category list.
The component can be used on any page. The next example shows the basic component usage on the blog home page:
title = "Blog"
url = "/blog/:page?"
[blogCategories]
==
...
<div class="sidebar">
{% component 'blogCategories' %}
</div>
...
The category list is coded in the default component partial `plugins/rainlab/blog/components/categories/default.htm`.
### RSS feed
Use the `blogRssFeed` component to display an RSS feed containing the latest blog posts. The following properties are supported:
* **categoryFilter** - a category slug to filter the posts by. If left blank, all posts are displayed.
* **postsPerPage** - how many posts to display on the feed. The default value is 10.
* **blogPage** - path to the main blog page. The default value is **blog** - it matches the pages/blog.htm file in the theme directory. This property is used in the RSS feed for creating links to the main blog page.
* **postPage** - path to the post details page. The default value is **blog/post** - it matches the pages/blog/post.htm file in the theme directory. This property is used in the RSS feed for creating links to the blog posts.
The component can be used on any page, it will hijack the entire page cycle to display the feed in RSS format. The next example shows how to use it:
title = "RSS Feed"
url = "/blog/rss.xml"
[blogRssFeed]
blogPage = "blog"
postPage = "blog/post"
==
<!-- This markup will never be displayed -->
## Configuration
To overwrite the default configuration create a `config/rainlab/blog/config.php`. You can return only values you want to override.
### Summary
A summary attribute is generated for each post.
If you enter an excerpt manually, it gets used as summary. Alternatively, you can use the `summary_separator` (default is `<!-- more -->`) to mark the end of the summary. If a post contains no separator, the text gets truncated after the number of characters specified in `summary_default_length` (default is 600 characters).
## Markdown guide
October supports [standard markdown syntax](http://daringfireball.net/projects/markdown/) as well as [extended markdown syntax](http://michelf.ca/projects/php-markdown/extra/)
### Classes and IDs
Classes and IDs can be added to images and other elements as shown below:
```
[link](url){#id .class}
![1](image){#id .class}
# October {#id .class}
```
### Fenced code blogs
Markdown extra makes it possible to use fenced code blocks. With fenced code blocks you do not need indentation on the areas you want to mark as code:
```
Code goes here
```
You can also use the `~` symbol:
~~~
Code goes here
~~~
### Tables
A *simple* table can be defined as follows:
```
First Header | Second Header
------------- | -------------
Content Cell | Content Cell
Content Cell | Content Cell
```
If you want to you can also add a leading and tailing pipe:
```
| First Header | Second Header |
| ------------- | ------------- |
| Content Cell | Content Cell |
| Content Cell | Content Cell |
```
To add alignment to the cells you simply need to add a `:` either at the start or end of a separator:
```
| First Header | Second Header |
| :------------ | ------------: |
| Content Cell | Content Cell |
| Content Cell | Content Cell |
```
To center align cell just add `:` on both sides:
```
| First Header | Second Header |
| ------------- | :-----------: |
| Content Cell | Content Cell |
| Content Cell | Content Cell |
```
### Definition lists
Below is an example of a simple definition list:
```
Laravel
: A popular PHP framework
October
: Awesome CMS built on Laravel
```
A term can also have multiple definitions:
```
Laravel
: A popular PHP framework
October
: Awesome CMS built on Laravel
: Supports markdown extra
```
You can also associate more than 1 term to a definition:
```
Laravel
October
: Built using PHP
```
### Footnotes
With markdown extra it is possible to create reference style footnotes:
```
This is some text with a footnote.[^1]
[^1]: And this is the footnote.
```
### Abbreviations
With markdown extra you can add abbreviations to your markup. The use this functionality first create a definition list:
```
*[HTML]: Hyper Text Markup Language
*[PHP]: Hypertext Preprocessor
```
Now markdown extra will convert all occurrences of `HTML` and `PHP` as follows:
```
<abbr title="Hyper Text Markup Language">HTML</abbr>
<abbr title="Hypertext Preprocessor">PHP</abbr>
```

View File

@ -0,0 +1,3 @@
.export-behavior .export-columns {
max-height: 450px !important;
}

View File

@ -0,0 +1,85 @@
.blog-post-preview .editor-preview .preview-content {
padding: 20px;
}
.blog-post-preview .editor-preview span.image-placeholder {
display: block;
}
.blog-post-preview .editor-preview span.image-placeholder .upload-dropzone {
background: #ecf0f1;
display: block;
border: 1px solid #e5e9ec;
padding: 25px;
min-height: 123px;
position: relative;
text-align: center;
cursor: pointer;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.blog-post-preview .editor-preview span.image-placeholder .upload-dropzone span.label {
color: #b1b9be;
font-size: 16px;
display: inline-block;
margin-top: 25px;
}
.blog-post-preview .editor-preview span.image-placeholder .upload-dropzone:before {
display: inline-block;
font-family: FontAwesome;
font-weight: normal;
font-style: normal;
text-decoration: inherit;
-webkit-font-smoothing: antialiased;
*margin-right: .3em;
content: "\f03e";
position: absolute;
left: 25px;
top: 25px;
line-height: 100%;
font-size: 73px;
color: #d1d3d4;
}
.blog-post-preview .editor-preview span.image-placeholder .upload-dropzone.hover,
.blog-post-preview .editor-preview span.image-placeholder .upload-dropzone:hover {
background: #2f99da;
}
.blog-post-preview .editor-preview span.image-placeholder .upload-dropzone.hover:before,
.blog-post-preview .editor-preview span.image-placeholder .upload-dropzone:hover:before,
.blog-post-preview .editor-preview span.image-placeholder .upload-dropzone.hover span.label,
.blog-post-preview .editor-preview span.image-placeholder .upload-dropzone:hover span.label {
color: white;
}
.blog-post-preview .editor-preview span.image-placeholder input[type=file] {
position: absolute;
left: -10000em;
}
.blog-post-preview-container .loading-indicator {
position: absolute;
display: none;
width: 20px;
height: 20px;
padding: 0!important;
background: transparent;
right: 10px;
left: auto;
top: 10px;
}
.blog-post-preview-container.loading-indicator-visible .loading-indicator {
display: block;
}
html.cssanimations .blog-post-preview span.image-placeholder.loading .upload-dropzone:before {
display: none;
}
html.cssanimations .blog-post-preview span.image-placeholder.loading .upload-dropzone .indicator {
display: block;
width: 50px;
height: 50px;
position: absolute;
left: 35px;
top: 35px;
background-image: url('../../../../../modules/system/assets/ui/images/loader-transparent.svg');
background-size: 50px 50px;
background-position: 50% 50%;
-webkit-animation: spin 1s linear infinite;
animation: spin 1s linear infinite;
}

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="64px" height="64px" viewBox="0 0 64 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>blog-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-+-Group" sketch:type="MSLayerGroup">
<g id="Group" sketch:type="MSShapeGroup">
<path d="M0.754,3.84 C0.754,1.719 2.473,0 4.594,0 L32.754,0 L48.754,13.44 L48.754,60.16 C48.754,62.281 47.035,64 44.914,64 L4.594,64 C2.473,64 0.754,62.281 0.754,60.16 L0.754,3.84 Z" id="Fill-392" fill="#FFFFFF"></path>
<path d="M32.754,0 L32.754,9.6 C32.754,11.721 34.473,13.44 36.594,13.44 L48.754,13.44 L32.754,0 Z" id="Fill-393" fill="#F0F1F1"></path>
<path d="M38.0416209,23 L12.3559338,23 C11.4141253,23 10.6435547,22.55 10.6435547,22 C10.6435547,21.45 11.4141253,21 12.3559338,21 L38.0416209,21 C38.9834294,21 39.754,21.45 39.754,22 C39.754,22.55 38.9834294,23 38.0416209,23 Z" id="Fill-397" fill="#E2E4E5"></path>
<path d="M29.4797252,27 C29.4797252,27.55 28.7091546,28 27.767346,28 L12.3559338,28 C11.4141253,28 10.6435547,27.55 10.6435547,27 C10.6435547,26.45 11.4141253,26 12.3559338,26 L27.767346,26 C28.7091546,26 29.4797252,26.45 29.4797252,27" id="Fill-398" fill="#E2E4E5"></path>
<path d="M23.767346,11 L12.3559338,11 C11.4141253,11 10.6435547,10.55 10.6435547,10 C10.6435547,9.45 11.4141253,9 12.3559338,9 L23.767346,9 C24.7091546,9 25.4797252,9.45 25.4797252,10 C25.4797252,10.55 24.7091546,11 23.767346,11 Z" id="Fill-398-Copy" fill="#E1332C"></path>
<path d="M39.754,35 C39.754,35.55 38.9834294,36 38.0416209,36 L12.3559338,36 C11.4141253,36 10.6435547,35.55 10.6435547,35 C10.6435547,34.45 11.4141253,34 12.3559338,34 L38.0416209,34 C38.9834294,34 39.754,34.45 39.754,35" id="Fill-399" fill="#E2E4E5"></path>
<path d="M29.4797252,40 C29.4797252,40.55 28.7091546,41 27.767346,41 L12.3559338,41 C11.4141253,41 10.6435547,40.55 10.6435547,40 C10.6435547,39.45 11.4141253,39 12.3559338,39 L27.767346,39 C28.7091546,39 29.4797252,39.45 29.4797252,40" id="Fill-400" fill="#E2E4E5"></path>
<path d="M39.754,48 C39.754,48.55 38.9834294,49 38.0416209,49 L12.3559338,49 C11.4141253,49 10.6435547,48.55 10.6435547,48 C10.6435547,47.45 11.4141253,47 12.3559338,47 L38.0416209,47 C38.9834294,47 39.754,47.45 39.754,48" id="Fill-401" fill="#E2E4E5"></path>
<path d="M29.4797252,53 C29.4797252,53.55 28.7091546,54 27.767346,54 L12.3559338,54 C11.4141253,54 10.6435547,53.55 10.6435547,53 C10.6435547,52.45 11.4141253,52 12.3559338,52 L27.767346,52 C28.7091546,52 29.4797252,52.45 29.4797252,53" id="Fill-402" fill="#E2E4E5"></path>
</g>
<g id="Group" transform="translate(27.000000, 16.000000)" sketch:type="MSShapeGroup">
<path d="M31.8979,11.4983 L10.6949,35.4653 L4.8179,36.6753 C4.6479,35.8573 4.2249,35.0823 3.5509,34.4863 C2.8769,33.8893 2.0559,33.5643 1.2229,33.4953 L1.7069,27.5143 L22.9099,3.5473 L31.8979,11.4983 Z" id="Fill-647" fill="#F4D0A1"></path>
<path d="M31.8692,11.4729 C31.8852,11.4869 11.1512,34.9499 11.1512,34.9499 C11.7392,33.7029 11.5032,32.1759 10.4362,31.2309 C9.3092,30.2349 7.6512,30.2359 6.5312,31.1689 C7.2692,29.8979 7.0682,28.2519 5.9422,27.2549 C4.8622,26.3009 3.2942,26.2619 2.1802,27.0819 C2.1442,27.1089 22.8852,3.5759 22.8852,3.5759 C22.8992,3.5599 31.8692,11.4729 31.8692,11.4729 Z" id="Fill-648" fill="#FC5B55"></path>
<path d="M31.8979,11.4983 L22.9099,3.5473 L24.2349,2.0493 L33.2229,10.0003 L31.8979,11.4983 Z" id="Fill-649" fill="#FACB1B"></path>
<path d="M32.7497,1.5706 L32.6597,1.4916 C30.2027,-0.6824 26.4487,-0.4524 24.2747,2.0046 L24.2357,2.0496 L33.2227,10.0006 L33.2627,9.9556 C35.4367,7.4986 35.2067,3.7446 32.7497,1.5706" id="Fill-650" fill="#F89392"></path>
<path d="M11.1258,34.9796 C11.7438,33.7326 11.5138,32.1846 10.4358,31.2306 C9.3118,30.2366 7.6588,30.2356 6.5388,31.1626 C6.5198,31.1786 27.3788,7.5516 27.3788,7.5516 C27.3928,7.5356 31.8698,11.4736 31.8698,11.4736 C31.8848,11.4866 11.1258,34.9796 11.1258,34.9796 Z" id="Fill-651" fill="#E1332C"></path>
<path d="M4.8181,36.6754 L0.9001,37.4824 L1.2231,33.4954 C2.0561,33.5644 2.8771,33.8894 3.5511,34.4864 C4.2251,35.0824 4.6481,35.8574 4.8181,36.6754" id="Fill-652" fill="#3E3E3F"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -0,0 +1,185 @@
+function ($) { "use strict";
var PostForm = function () {
this.$form = $('#post-form')
this.$markdownEditor = $('[data-field-name=content] [data-control=markdowneditor]:first', this.$form)
this.$preview = $('.editor-preview', this.$markdownEditor)
this.formAction = this.$form.attr('action')
this.sessionKey = $('input[name=_session_key]', this.$form).val()
if (this.$markdownEditor.length > 0) {
this.codeEditor = this.$markdownEditor.markdownEditor('getEditorObject')
this.$markdownEditor.on('initPreview.oc.markdowneditor', $.proxy(this.initPreview, this))
this.initDropzones()
this.initFormEvents()
this.addToolbarButton()
}
this.initLayout()
}
PostForm.prototype.addToolbarButton = function() {
this.buttonClickCount = 1
var self = this,
$button = this.$markdownEditor.markdownEditor('findToolbarButton', 'image')
if (!$button.length) return
$button.data('button-action', 'insertLine')
$button.data('button-template', '\n\n![1](image)\n')
$button.on('click', function() {
$button.data('button-template', '\n\n!['+self.buttonClickCount+'](image)\n')
self.buttonClickCount++
})
}
PostForm.prototype.initPreview = function() {
this.initImageUploaders()
}
PostForm.prototype.updateScroll = function() {
// Reserved in case MarkdownEditor uses scrollbar plugin
// this.$preview.data('oc.scrollbar').update()
}
PostForm.prototype.initImageUploaders = function() {
var self = this
$('span.image-placeholder .upload-dropzone', this.$preview).each(function(){
var
$placeholder = $(this).parent(),
$link = $('span.label', $placeholder),
placeholderIndex = $placeholder.data('index')
var uploaderOptions = {
url: self.formAction,
clickable: [$(this).get(0), $link.get(0)],
previewsContainer: $('<div />').get(0),
paramName: 'file',
headers: {}
}
/*
* Add CSRF token to headers
*/
var token = $('meta[name="csrf-token"]').attr('content')
if (token) {
uploaderOptions.headers['X-CSRF-TOKEN'] = token
}
var dropzone = new Dropzone($(this).get(0), uploaderOptions)
dropzone.on('error', function(file, error) {
alert('Error uploading file: ' + error)
})
dropzone.on('success', function(file, data){
if (data.error)
alert(data.error)
else {
self.pauseUpdates()
var $img = $('<img src="'+data.path+'">')
$img.load(function(){
self.updateScroll()
})
$placeholder.replaceWith($img)
self.codeEditor.replace('!['+data.file+']('+data.path+')', {
needle: '!['+placeholderIndex+'](image)'
})
self.resumeUpdates()
}
})
dropzone.on('complete', function(){
$placeholder.removeClass('loading')
})
dropzone.on('sending', function(file, xhr, formData) {
formData.append('X_BLOG_IMAGE_UPLOAD', 1)
formData.append('_session_key', self.sessionKey)
$placeholder.addClass('loading')
})
})
}
PostForm.prototype.pauseUpdates = function() {
this.$markdownEditor.markdownEditor('pauseUpdates')
}
PostForm.prototype.resumeUpdates = function() {
this.$markdownEditor.markdownEditor('resumeUpdates')
}
PostForm.prototype.initDropzones = function() {
$(document).bind('dragover', function (e) {
var dropZone = $('span.image-placeholder .upload-dropzone'),
foundDropzone,
timeout = window.dropZoneTimeout
if (!timeout)
dropZone.addClass('in');
else
clearTimeout(timeout);
var found = false,
node = e.target
do {
if ($(node).hasClass('dropzone')) {
found = true
foundDropzone = $(node)
break
}
node = node.parentNode;
} while (node != null);
dropZone.removeClass('in hover')
if (found)
foundDropzone.addClass('hover')
window.dropZoneTimeout = setTimeout(function () {
window.dropZoneTimeout = null
dropZone.removeClass('in hover')
}, 100)
})
}
PostForm.prototype.initFormEvents = function() {
$(document).on('ajaxSuccess', '#post-form', function(event, context, data){
if (context.handler == 'onSave' && !data.X_OCTOBER_ERROR_FIELDS) {
$(this).trigger('unchange.oc.changeMonitor')
}
})
}
PostForm.prototype.initLayout = function() {
$('#Form-secondaryTabs .tab-pane.layout-cell:not(:first-child)').addClass('padded-pane')
$('#Form-secondaryTabs .nav-tabs > li:not(:first-child)').addClass('tab-content-bg')
}
PostForm.prototype.replacePlaceholder = function(placeholder, placeholderHtmlReplacement, mdCodePlaceholder, mdCodeReplacement) {
this.pauseUpdates()
placeholder.replaceWith(placeholderHtmlReplacement)
this.codeEditor.replace(mdCodeReplacement, {
needle: mdCodePlaceholder
})
this.updateScroll()
this.resumeUpdates()
}
$(document).ready(function(){
var form = new PostForm()
if ($.oc === undefined)
$.oc = {}
$.oc.blogPostForm = form
})
}(window.jQuery);

View File

@ -0,0 +1,99 @@
@import "../../../../../modules/backend/assets/less/core/boot.less";
.blog-post-preview .editor-preview {
.preview-content {
padding: 20px;
}
span.image-placeholder {
display: block;
.upload-dropzone {
background: #ecf0f1;
display: block;
border: 1px solid #e5e9ec;
padding: 25px;
min-height: 123px;
position: relative;
text-align: center;
cursor: pointer;
.box-sizing(border-box);
span.label {
color: #b1b9be;
font-size: 16px;
display: inline-block;
margin-top: 25px;
}
&:before {
display: inline-block;
.icon(@picture-o);
position: absolute;
left: 25px;
top: 25px;
line-height: 100%;
font-size: 73px;
color: #d1d3d4;
}
&.hover, &:hover {
background: #2f99da;
&:before, span.label {
color: white;
}
}
}
input[type=file] {
position: absolute;
left: -10000em;
}
}
}
.blog-post-preview-container {
.loading-indicator {
position: absolute;
display: none;
width: 20px;
height: 20px;
padding: 0!important;
background: transparent;
right: 10px;
left: auto;
top: 10px;
}
&.loading-indicator-visible {
.loading-indicator {
display: block;
}
}
}
html.cssanimations {
.blog-post-preview {
span.image-placeholder.loading {
.upload-dropzone {
&:before {
display: none;
}
.indicator {
display: block;
width: 50px;
height: 50px;
position: absolute;
left: 35px;
top: 35px;
background-image:url('../../../../../modules/system/assets/ui/images/loader-transparent.svg');
background-size: 50px 50px;
background-position: 50% 50%;
.animation(spin 1s linear infinite);
}
}
}
}
}

View File

@ -0,0 +1,39 @@
<?php namespace RainLab\Blog\Classes;
/**
* Blog Markdown tag processor.
*
* @package rainlab\blog
* @author Alexey Bobkov, Samuel Georges
*/
class TagProcessor
{
use \October\Rain\Support\Traits\Singleton;
/**
* @var array Cache of processing callbacks.
*/
private $callbacks = [];
/**
* Registers a callback function that handles blog post markup.
* The callback function should accept two arguments - the HTML string
* generated from Markdown contents and the preview flag determining whether
* the function should return a markup for the blog post preview form or for the
* front-end.
* @param callable $callback A callable function.
*/
public function registerCallback(callable $callback)
{
$this->callbacks[] = $callback;
}
public function processTags($markup, $preview)
{
foreach ($this->callbacks as $callback) {
$markup = $callback($markup, $preview);
}
return $markup;
}
}

View File

@ -0,0 +1,113 @@
<?php namespace RainLab\Blog\Components;
use Db;
use Carbon\Carbon;
use Cms\Classes\Page;
use Cms\Classes\ComponentBase;
use RainLab\Blog\Models\Category as BlogCategory;
class Categories extends ComponentBase
{
/**
* @var Collection A collection of categories to display
*/
public $categories;
/**
* @var string Reference to the page name for linking to categories.
*/
public $categoryPage;
/**
* @var string Reference to the current category slug.
*/
public $currentCategorySlug;
public function componentDetails()
{
return [
'name' => 'rainlab.blog::lang.settings.category_title',
'description' => 'rainlab.blog::lang.settings.category_description'
];
}
public function defineProperties()
{
return [
'slug' => [
'title' => 'rainlab.blog::lang.settings.category_slug',
'description' => 'rainlab.blog::lang.settings.category_slug_description',
'default' => '{{ :slug }}',
'type' => 'string',
],
'displayEmpty' => [
'title' => 'rainlab.blog::lang.settings.category_display_empty',
'description' => 'rainlab.blog::lang.settings.category_display_empty_description',
'type' => 'checkbox',
'default' => 0,
],
'categoryPage' => [
'title' => 'rainlab.blog::lang.settings.category_page',
'description' => 'rainlab.blog::lang.settings.category_page_description',
'type' => 'dropdown',
'default' => 'blog/category',
'group' => 'rainlab.blog::lang.settings.group_links',
],
];
}
public function getCategoryPageOptions()
{
return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName');
}
public function onRun()
{
$this->currentCategorySlug = $this->page['currentCategorySlug'] = $this->property('slug');
$this->categoryPage = $this->page['categoryPage'] = $this->property('categoryPage');
$this->categories = $this->page['categories'] = $this->loadCategories();
}
/**
* Load all categories or, depending on the <displayEmpty> option, only those that have blog posts
* @return mixed
*/
protected function loadCategories()
{
$categories = BlogCategory::with('posts_count')->getNested();
if (!$this->property('displayEmpty')) {
$iterator = function ($categories) use (&$iterator) {
return $categories->reject(function ($category) use (&$iterator) {
if ($category->getNestedPostCount() == 0) {
return true;
}
if ($category->children) {
$category->children = $iterator($category->children);
}
return false;
});
};
$categories = $iterator($categories);
}
/*
* Add a "url" helper attribute for linking to each category
*/
return $this->linkCategories($categories);
}
/**
* Sets the URL on each category according to the defined category page
* @return void
*/
protected function linkCategories($categories)
{
return $categories->each(function ($category) {
$category->setUrl($this->categoryPage, $this->controller);
if ($category->children) {
$this->linkCategories($category->children);
}
});
}
}

View File

@ -0,0 +1,156 @@
<?php namespace RainLab\Blog\Components;
use Event;
use BackendAuth;
use Cms\Classes\Page;
use Cms\Classes\ComponentBase;
use RainLab\Blog\Models\Post as BlogPost;
class Post extends ComponentBase
{
/**
* @var RainLab\Blog\Models\Post The post model used for display.
*/
public $post;
/**
* @var string Reference to the page name for linking to categories.
*/
public $categoryPage;
public function componentDetails()
{
return [
'name' => 'rainlab.blog::lang.settings.post_title',
'description' => 'rainlab.blog::lang.settings.post_description'
];
}
public function defineProperties()
{
return [
'slug' => [
'title' => 'rainlab.blog::lang.settings.post_slug',
'description' => 'rainlab.blog::lang.settings.post_slug_description',
'default' => '{{ :slug }}',
'type' => 'string',
],
'categoryPage' => [
'title' => 'rainlab.blog::lang.settings.post_category',
'description' => 'rainlab.blog::lang.settings.post_category_description',
'type' => 'dropdown',
'default' => 'blog/category',
],
];
}
public function getCategoryPageOptions()
{
return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName');
}
public function init()
{
Event::listen('translate.localePicker.translateParams', function ($page, $params, $oldLocale, $newLocale) {
$newParams = $params;
if (isset($params['slug'])) {
$records = BlogPost::transWhere('slug', $params['slug'], $oldLocale)->first();
if ($records) {
$records->translateContext($newLocale);
$newParams['slug'] = $records['slug'];
}
}
return $newParams;
});
}
public function onRun()
{
$this->categoryPage = $this->page['categoryPage'] = $this->property('categoryPage');
$this->post = $this->page['post'] = $this->loadPost();
if (!$this->post) {
$this->setStatusCode(404);
return $this->controller->run('404');
}
}
public function onRender()
{
if (empty($this->post)) {
$this->post = $this->page['post'] = $this->loadPost();
}
}
protected function loadPost()
{
$slug = $this->property('slug');
$post = new BlogPost;
$query = $post->query();
if ($post->isClassExtendedWith('RainLab.Translate.Behaviors.TranslatableModel')) {
$query->transWhere('slug', $slug);
} else {
$query->where('slug', $slug);
}
if (!$this->checkEditor()) {
$query->isPublished();
}
$post = $query->first();
/*
* Add a "url" helper attribute for linking to each category
*/
if ($post && $post->exists && $post->categories->count()) {
$post->categories->each(function($category) {
$category->setUrl($this->categoryPage, $this->controller);
});
}
return $post;
}
public function previousPost()
{
return $this->getPostSibling(-1);
}
public function nextPost()
{
return $this->getPostSibling(1);
}
protected function getPostSibling($direction = 1)
{
if (!$this->post) {
return;
}
$method = $direction === -1 ? 'previousPost' : 'nextPost';
if (!$post = $this->post->$method()) {
return;
}
$postPage = $this->getPage()->getBaseFileName();
$post->setUrl($postPage, $this->controller);
$post->categories->each(function($category) {
$category->setUrl($this->categoryPage, $this->controller);
});
return $post;
}
protected function checkEditor()
{
$backendUser = BackendAuth::getUser();
return $backendUser && $backendUser->hasAccess('rainlab.blog.access_posts');
}
}

View File

@ -0,0 +1,259 @@
<?php namespace RainLab\Blog\Components;
use Lang;
use Redirect;
use BackendAuth;
use Cms\Classes\Page;
use Cms\Classes\ComponentBase;
use October\Rain\Database\Model;
use October\Rain\Database\Collection;
use RainLab\Blog\Models\Post as BlogPost;
use RainLab\Blog\Models\Category as BlogCategory;
use RainLab\Blog\Models\Settings as BlogSettings;
class Posts extends ComponentBase
{
/**
* A collection of posts to display
*
* @var Collection
*/
public $posts;
/**
* Parameter to use for the page number
*
* @var string
*/
public $pageParam;
/**
* If the post list should be filtered by a category, the model to use
*
* @var Model
*/
public $category;
/**
* Message to display when there are no messages
*
* @var string
*/
public $noPostsMessage;
/**
* Reference to the page name for linking to posts
*
* @var string
*/
public $postPage;
/**
* Reference to the page name for linking to categories
*
* @var string
*/
public $categoryPage;
/**
* If the post list should be ordered by another attribute
*
* @var string
*/
public $sortOrder;
public function componentDetails()
{
return [
'name' => 'rainlab.blog::lang.settings.posts_title',
'description' => 'rainlab.blog::lang.settings.posts_description'
];
}
public function defineProperties()
{
return [
'pageNumber' => [
'title' => 'rainlab.blog::lang.settings.posts_pagination',
'description' => 'rainlab.blog::lang.settings.posts_pagination_description',
'type' => 'string',
'default' => '{{ :page }}',
],
'categoryFilter' => [
'title' => 'rainlab.blog::lang.settings.posts_filter',
'description' => 'rainlab.blog::lang.settings.posts_filter_description',
'type' => 'string',
'default' => '',
],
'postsPerPage' => [
'title' => 'rainlab.blog::lang.settings.posts_per_page',
'type' => 'string',
'validationPattern' => '^[0-9]+$',
'validationMessage' => 'rainlab.blog::lang.settings.posts_per_page_validation',
'default' => '10',
],
'noPostsMessage' => [
'title' => 'rainlab.blog::lang.settings.posts_no_posts',
'description' => 'rainlab.blog::lang.settings.posts_no_posts_description',
'type' => 'string',
'default' => Lang::get('rainlab.blog::lang.settings.posts_no_posts_default'),
'showExternalParam' => false,
],
'sortOrder' => [
'title' => 'rainlab.blog::lang.settings.posts_order',
'description' => 'rainlab.blog::lang.settings.posts_order_description',
'type' => 'dropdown',
'default' => 'published_at desc',
],
'categoryPage' => [
'title' => 'rainlab.blog::lang.settings.posts_category',
'description' => 'rainlab.blog::lang.settings.posts_category_description',
'type' => 'dropdown',
'default' => 'blog/category',
'group' => 'rainlab.blog::lang.settings.group_links',
],
'postPage' => [
'title' => 'rainlab.blog::lang.settings.posts_post',
'description' => 'rainlab.blog::lang.settings.posts_post_description',
'type' => 'dropdown',
'default' => 'blog/post',
'group' => 'rainlab.blog::lang.settings.group_links',
],
'exceptPost' => [
'title' => 'rainlab.blog::lang.settings.posts_except_post',
'description' => 'rainlab.blog::lang.settings.posts_except_post_description',
'type' => 'string',
'validationPattern' => '^[a-z0-9\-_,\s]+$',
'validationMessage' => 'rainlab.blog::lang.settings.posts_except_post_validation',
'default' => '',
'group' => 'rainlab.blog::lang.settings.group_exceptions',
],
'exceptCategories' => [
'title' => 'rainlab.blog::lang.settings.posts_except_categories',
'description' => 'rainlab.blog::lang.settings.posts_except_categories_description',
'type' => 'string',
'validationPattern' => '^[a-z0-9\-_,\s]+$',
'validationMessage' => 'rainlab.blog::lang.settings.posts_except_categories_validation',
'default' => '',
'group' => 'rainlab.blog::lang.settings.group_exceptions',
],
];
}
public function getCategoryPageOptions()
{
return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName');
}
public function getPostPageOptions()
{
return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName');
}
public function getSortOrderOptions()
{
$options = BlogPost::$allowedSortingOptions;
foreach ($options as $key => $value) {
$options[$key] = Lang::get($value);
}
return $options;
}
public function onRun()
{
$this->prepareVars();
$this->category = $this->page['category'] = $this->loadCategory();
$this->posts = $this->page['posts'] = $this->listPosts();
/*
* If the page number is not valid, redirect
*/
if ($pageNumberParam = $this->paramName('pageNumber')) {
$currentPage = $this->property('pageNumber');
if ($currentPage > ($lastPage = $this->posts->lastPage()) && $currentPage > 1) {
return Redirect::to($this->currentPageUrl([$pageNumberParam => $lastPage]));
}
}
}
protected function prepareVars()
{
$this->pageParam = $this->page['pageParam'] = $this->paramName('pageNumber');
$this->noPostsMessage = $this->page['noPostsMessage'] = $this->property('noPostsMessage');
/*
* Page links
*/
$this->postPage = $this->page['postPage'] = $this->property('postPage');
$this->categoryPage = $this->page['categoryPage'] = $this->property('categoryPage');
}
protected function listPosts()
{
$category = $this->category ? $this->category->id : null;
$categorySlug = $this->category ? $this->category->slug : null;
/*
* List all the posts, eager load their categories
*/
$isPublished = !$this->checkEditor();
$posts = BlogPost::with(['categories', 'featured_images'])->listFrontEnd([
'page' => $this->property('pageNumber'),
'sort' => $this->property('sortOrder'),
'perPage' => $this->property('postsPerPage'),
'search' => trim(input('search')),
'category' => $category,
'published' => $isPublished,
'exceptPost' => is_array($this->property('exceptPost'))
? $this->property('exceptPost')
: preg_split('/,\s*/', $this->property('exceptPost'), -1, PREG_SPLIT_NO_EMPTY),
'exceptCategories' => is_array($this->property('exceptCategories'))
? $this->property('exceptCategories')
: preg_split('/,\s*/', $this->property('exceptCategories'), -1, PREG_SPLIT_NO_EMPTY),
]);
/*
* Add a "url" helper attribute for linking to each post and category
*/
$posts->each(function($post) use ($categorySlug) {
$post->setUrl($this->postPage, $this->controller, ['category' => $categorySlug]);
$post->categories->each(function($category) {
$category->setUrl($this->categoryPage, $this->controller);
});
});
return $posts;
}
protected function loadCategory()
{
if (!$slug = $this->property('categoryFilter')) {
return null;
}
$category = new BlogCategory;
$category = $category->isClassExtendedWith('RainLab.Translate.Behaviors.TranslatableModel')
? $category->transWhere('slug', $slug)
: $category->where('slug', $slug);
$category = $category->first();
return $category ?: null;
}
protected function checkEditor()
{
$backendUser = BackendAuth::getUser();
return $backendUser &&
$backendUser->hasAccess('rainlab.blog.access_posts') &&
BlogSettings::get('show_all_posts', true);
}
}

View File

@ -0,0 +1,159 @@
<?php namespace RainLab\Blog\Components;
use Lang;
use Response;
use Cms\Classes\Page;
use Cms\Classes\ComponentBase;
use RainLab\Blog\Models\Post as BlogPost;
use RainLab\Blog\Models\Category as BlogCategory;
class RssFeed extends ComponentBase
{
/**
* A collection of posts to display
* @var Collection
*/
public $posts;
/**
* If the post list should be filtered by a category, the model to use.
* @var Model
*/
public $category;
/**
* Reference to the page name for the main blog page.
* @var string
*/
public $blogPage;
/**
* Reference to the page name for linking to posts.
* @var string
*/
public $postPage;
public function componentDetails()
{
return [
'name' => 'rainlab.blog::lang.settings.rssfeed_title',
'description' => 'rainlab.blog::lang.settings.rssfeed_description'
];
}
public function defineProperties()
{
return [
'categoryFilter' => [
'title' => 'rainlab.blog::lang.settings.posts_filter',
'description' => 'rainlab.blog::lang.settings.posts_filter_description',
'type' => 'string',
'default' => '',
],
'sortOrder' => [
'title' => 'rainlab.blog::lang.settings.posts_order',
'description' => 'rainlab.blog::lang.settings.posts_order_description',
'type' => 'dropdown',
'default' => 'created_at desc',
],
'postsPerPage' => [
'title' => 'rainlab.blog::lang.settings.posts_per_page',
'type' => 'string',
'validationPattern' => '^[0-9]+$',
'validationMessage' => 'rainlab.blog::lang.settings.posts_per_page_validation',
'default' => '10',
],
'blogPage' => [
'title' => 'rainlab.blog::lang.settings.rssfeed_blog',
'description' => 'rainlab.blog::lang.settings.rssfeed_blog_description',
'type' => 'dropdown',
'default' => 'blog/post',
'group' => 'rainlab.blog::lang.settings.group_links',
],
'postPage' => [
'title' => 'rainlab.blog::lang.settings.posts_post',
'description' => 'rainlab.blog::lang.settings.posts_post_description',
'type' => 'dropdown',
'default' => 'blog/post',
'group' => 'rainlab.blog::lang.settings.group_links',
],
];
}
public function getBlogPageOptions()
{
return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName');
}
public function getPostPageOptions()
{
return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName');
}
public function getSortOrderOptions()
{
$options = BlogPost::$allowedSortingOptions;
foreach ($options as $key => $value) {
$options[$key] = Lang::get($value);
}
return $options;
}
public function onRun()
{
$this->prepareVars();
$xmlFeed = $this->renderPartial('@default');
return Response::make($xmlFeed, '200')->header('Content-Type', 'text/xml');
}
protected function prepareVars()
{
$this->blogPage = $this->page['blogPage'] = $this->property('blogPage');
$this->postPage = $this->page['postPage'] = $this->property('postPage');
$this->category = $this->page['category'] = $this->loadCategory();
$this->posts = $this->page['posts'] = $this->listPosts();
$this->page['link'] = $this->pageUrl($this->blogPage);
$this->page['rssLink'] = $this->currentPageUrl();
}
protected function listPosts()
{
$category = $this->category ? $this->category->id : null;
/*
* List all the posts, eager load their categories
*/
$posts = BlogPost::with('categories')->listFrontEnd([
'sort' => $this->property('sortOrder'),
'perPage' => $this->property('postsPerPage'),
'category' => $category
]);
/*
* Add a "url" helper attribute for linking to each post and category
*/
$posts->each(function($post) {
$post->setUrl($this->postPage, $this->controller);
});
return $posts;
}
protected function loadCategory()
{
if (!$categoryId = $this->property('categoryFilter')) {
return null;
}
if (!$category = BlogCategory::whereSlug($categoryId)->first()) {
return null;
}
return $category;
}
}

View File

@ -0,0 +1,10 @@
{% if __SELF__.categories|length > 0 %}
<ul class="category-list">
{% partial __SELF__ ~ "::items"
categories = __SELF__.categories
currentCategorySlug = __SELF__.currentCategorySlug
%}
</ul>
{% else %}
<p>No categories were found.</p>
{% endif %}

View File

@ -0,0 +1,18 @@
{% for category in categories %}
{% set postCount = category.post_count %}
<li {% if category.slug == currentCategorySlug %}class="active"{% endif %}>
<a href="{{ category.url }}">{{ category.name }}</a>
{% if postCount %}
<span class="badge">{{ postCount }}</span>
{% endif %}
{% if category.children|length > 0 %}
<ul>
{% partial __SELF__ ~ "::items"
categories=category.children
currentCategorySlug=currentCategorySlug
%}
</ul>
{% endif %}
</li>
{% endfor %}

View File

@ -0,0 +1,27 @@
{% set post = __SELF__.post %}
<div class="content">{{ post.content_html|raw }}</div>
{% if post.featured_images|length %}
<div class="featured-images text-center">
{% for image in post.featured_images %}
<p>
<img
data-src="{{ image.filename }}"
src="{{ image.path }}"
alt="{{ image.description }}"
style="max-width: 100%" />
</p>
{% endfor %}
</div>
{% endif %}
<p class="info">
Posted
{% if post.categories|length %} in
{% for category in post.categories %}
<a href="{{ category.url }}">{{ category.name }}</a>{% if not loop.last %}, {% endif %}
{% endfor %}
{% endif %}
on {{ post.published_at|date('M d, Y') }}
</p>

View File

@ -0,0 +1,40 @@
{% set posts = __SELF__.posts %}
<ul class="post-list">
{% for post in posts %}
<li>
<h3><a href="{{ post.url }}">{{ post.title }}</a></h3>
<p class="info">
Posted
{% if post.categories|length %} in {% endif %}
{% for category in post.categories %}
<a href="{{ category.url }}">{{ category.name }}</a>{% if not loop.last %}, {% endif %}
{% endfor %}
on {{ post.published_at|date('M d, Y') }}
</p>
<p class="excerpt">{{ post.summary|raw }}</p>
</li>
{% else %}
<li class="no-data">{{ __SELF__.noPostsMessage }}</li>
{% endfor %}
</ul>
{% if posts.lastPage > 1 %}
<ul class="pagination">
{% if posts.currentPage > 1 %}
<li><a href="{{ this.page.baseFileName|page({ (__SELF__.pageParam): (posts.currentPage-1) }) }}">&larr; Prev</a></li>
{% endif %}
{% for page in 1..posts.lastPage %}
<li class="{{ posts.currentPage == page ? 'active' : null }}">
<a href="{{ this.page.baseFileName|page({ (__SELF__.pageParam): page }) }}">{{ page }}</a>
</li>
{% endfor %}
{% if posts.lastPage > posts.currentPage %}
<li><a href="{{ this.page.baseFileName|page({ (__SELF__.pageParam): (posts.currentPage+1) }) }}">Next &rarr;</a></li>
{% endif %}
</ul>
{% endif %}

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>{{ this.page.meta_title ?: this.page.title }}</title>
<link>{{ link }}</link>
<description>{{ this.page.meta_description ?: this.page.description }}</description>
<atom:link href="{{ rssLink }}" rel="self" type="application/rss+xml" />
{% for post in posts %}
<item>
<title>{{ post.title }}</title>
<link>{{ post.url }}</link>
<guid>{{ post.url }}</guid>
<pubDate>{{ post.published_at.toRfc2822String }}</pubDate>
<description>{{ post.summary }}</description>
</item>
{% endfor %}
</channel>
</rss>

View File

@ -0,0 +1,25 @@
{
"name": "rainlab/blog-plugin",
"type": "october-plugin",
"description": "Blog plugin for October CMS",
"homepage": "https://octobercms.com/plugin/rainlab-blog",
"keywords": ["october", "octobercms", "blog"],
"license": "MIT",
"authors": [
{
"name": "Alexey Bobkov",
"email": "aleksey.bobkov@gmail.com",
"role": "Co-founder"
},
{
"name": "Samuel Georges",
"email": "daftspunky@gmail.com",
"role": "Co-founder"
}
],
"require": {
"php": ">=7.0",
"composer/installers": "~1.0"
},
"minimum-stability": "dev"
}

View File

@ -0,0 +1,18 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Summary Config
|--------------------------------------------------------------------------
|
| Specify a custom tag and length for blog post summaries
|
*/
'summary_separator' => '<!-- more -->',
'summary_default_length' => 600
];

View File

@ -0,0 +1,47 @@
<?php namespace RainLab\Blog\Controllers;
use BackendMenu;
use Flash;
use Lang;
use Backend\Classes\Controller;
use RainLab\Blog\Models\Category;
class Categories extends Controller
{
public $implement = [
\Backend\Behaviors\FormController::class,
\Backend\Behaviors\ListController::class,
\Backend\Behaviors\ReorderController::class
];
public $formConfig = 'config_form.yaml';
public $listConfig = 'config_list.yaml';
public $reorderConfig = 'config_reorder.yaml';
public $requiredPermissions = ['rainlab.blog.access_categories'];
public function __construct()
{
parent::__construct();
BackendMenu::setContext('RainLab.Blog', 'blog', 'categories');
}
public function index_onDelete()
{
if (($checkedIds = post('checked')) && is_array($checkedIds) && count($checkedIds)) {
foreach ($checkedIds as $categoryId) {
if ((!$category = Category::find($categoryId))) {
continue;
}
$category->delete();
}
Flash::success(Lang::get('rainlab.blog::lang.category.delete_success'));
}
return $this->listRefresh();
}
}

View File

@ -0,0 +1,153 @@
<?php namespace RainLab\Blog\Controllers;
use Lang;
use Flash;
use BackendMenu;
use RainLab\Blog\Models\Post;
use RainLab\Blog\Models\Settings as BlogSettings;
use Backend\Classes\Controller;
/**
* Posts
*/
class Posts extends Controller
{
public $implement = [
\Backend\Behaviors\FormController::class,
\Backend\Behaviors\ListController::class,
\Backend\Behaviors\ImportExportController::class
];
public $formConfig = 'config_form.yaml';
public $listConfig = 'config_list.yaml';
public $importExportConfig = 'config_import_export.yaml';
/**
* @var array requiredPermissions
*/
public $requiredPermissions = ['rainlab.blog.access_other_posts', 'rainlab.blog.access_posts'];
/**
* @var bool turboVisitControl
*/
public $turboVisitControl = 'disable';
/**
* __construct
*/
public function __construct()
{
parent::__construct();
BackendMenu::setContext('RainLab.Blog', 'blog', 'posts');
}
public function index()
{
$this->vars['postsTotal'] = Post::count();
$this->vars['postsPublished'] = Post::isPublished()->count();
$this->vars['postsDrafts'] = $this->vars['postsTotal'] - $this->vars['postsPublished'];
$this->asExtension('ListController')->index();
}
public function create()
{
BackendMenu::setContextSideMenu('new_post');
$this->bodyClass = 'compact-container';
$this->addCss('/plugins/rainlab/blog/assets/css/rainlab.blog-preview.css');
$this->addJs('/plugins/rainlab/blog/assets/js/post-form.js');
return $this->asExtension('FormController')->create();
}
public function update($recordId = null)
{
$this->bodyClass = 'compact-container';
$this->addCss('/plugins/rainlab/blog/assets/css/rainlab.blog-preview.css');
$this->addJs('/plugins/rainlab/blog/assets/js/post-form.js');
return $this->asExtension('FormController')->update($recordId);
}
public function export()
{
$this->addCss('/plugins/rainlab/blog/assets/css/rainlab.blog-export.css');
return $this->asExtension('ImportExportController')->export();
}
public function listExtendQuery($query)
{
if (!$this->user->hasAnyAccess(['rainlab.blog.access_other_posts'])) {
$query->where('user_id', $this->user->id);
}
}
public function formExtendQuery($query)
{
if (!$this->user->hasAnyAccess(['rainlab.blog.access_other_posts'])) {
$query->where('user_id', $this->user->id);
}
}
public function formExtendFieldsBefore($widget)
{
if (!$model = $widget->model) {
return;
}
if ($model instanceof Post && $model->isClassExtendedWith('RainLab.Translate.Behaviors.TranslatableModel')) {
$widget->secondaryTabs['fields']['content']['type'] = 'RainLab\Blog\FormWidgets\MLBlogMarkdown';
}
if (BlogSettings::get('use_legacy_editor', false)) {
$widget->secondaryTabs['fields']['content']['legacyMode'] = true;
}
}
public function index_onDelete()
{
if (($checkedIds = post('checked')) && is_array($checkedIds) && count($checkedIds)) {
foreach ($checkedIds as $postId) {
if ((!$post = Post::find($postId)) || !$post->canEdit($this->user)) {
continue;
}
$post->delete();
}
Flash::success(Lang::get('rainlab.blog::lang.post.delete_success'));
}
return $this->listRefresh();
}
/**
* {@inheritDoc}
*/
public function listInjectRowClass($record, $definition = null)
{
if (!$record->published) {
return 'safe disabled';
}
}
public function formBeforeCreate($model)
{
$model->user_id = $this->user->id;
}
public function onRefreshPreview()
{
$data = post('Post');
$previewHtml = Post::formatHtml($data['content'], true);
return [
'preview' => $previewHtml
];
}
}

View File

@ -0,0 +1,25 @@
<div data-control="toolbar">
<a href="<?= Backend::url('rainlab/blog/categories/create') ?>" class="btn btn-primary oc-icon-plus">
<?= e(trans('rainlab.blog::lang.categories.new_category')) ?>
</a>
<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('rainlab.blog::lang.blog.delete_confirm')) ?>"
data-trigger-action="enable"
data-trigger=".control-list input[type=checkbox]"
data-trigger-condition="checked"
data-request-success="$(this).prop('disabled', false)"
data-stripe-load-indicator>
<?= e(trans('backend::lang.list.delete_selected')) ?>
</button>
<?php if (!class_exists('System')): ?>
<a href="<?= Backend::url('rainlab/blog/categories/reorder') ?>" class="btn btn-default oc-icon-sitemap">
<?= e(trans('rainlab.blog::lang.category.reorder')) ?>
</a>
<?php endif ?>
</div>

View File

@ -0,0 +1,5 @@
<div data-control="toolbar">
<a href="<?= Backend::url('rainlab/blog/categories') ?>" class="btn btn-primary oc-icon-caret-left">
<?= e(trans('rainlab.blog::lang.category.return_to_categories')) ?>
</a>
</div>

View File

@ -0,0 +1,16 @@
# ===================================
# Form Behavior Config
# ===================================
name: rainlab.blog::lang.blog.create_category
form: $/rainlab/blog/models/category/fields.yaml
modelClass: RainLab\Blog\Models\Category
defaultRedirect: rainlab/blog/categories
create:
redirect: rainlab/blog/categories/update/:id
redirectClose: rainlab/blog/categories
update:
redirect: rainlab/blog/categories
redirectClose: rainlab/blog/categories

View File

@ -0,0 +1,43 @@
# ===================================
# List Behavior Config
# ===================================
# Model List Column configuration
list: $/rainlab/blog/models/category/columns.yaml
# Model Class name
modelClass: RainLab\Blog\Models\Category
# List Title
title: rainlab.blog::lang.categories.list_title
# Link URL for each record
recordUrl: rainlab/blog/categories/update/:id
# Message to display if the list is empty
noRecordsMessage: backend::lang.list.no_records
# Records to display per page
recordsPerPage: 5
# 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
# Legacy (v1)
showTree: true
# Reordering
structure:
showTree: true
showReorder: true
treeExpanded: true
maxDepth: 0

View File

@ -0,0 +1,17 @@
# ===================================
# Reorder Behavior Config
# ===================================
# Reorder Title
title: rainlab.blog::lang.category.reorder
# Attribute name
nameFrom: name
# Model Class name
modelClass: RainLab\Blog\Models\Category
# Toolbar widget configuration
toolbar:
# Partial for toolbar buttons
buttons: reorder_toolbar

View File

@ -0,0 +1,46 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?= Backend::url('rainlab/blog/posts') ?>"><?= e(trans('rainlab.blog::lang.blog.menu_label')) ?></a></li>
<li><?= e(trans($this->pageTitle)) ?></li>
</ul>
<?php Block::endPut() ?>
<?php if (!$this->fatalError): ?>
<?= Form::open(['class' => 'layout']) ?>
<div class="layout-row">
<?= $this->formRender() ?>
</div>
<div class="form-buttons">
<div class="loading-indicator-container">
<button
type="submit"
data-request="onSave"
data-hotkey="ctrl+s, cmd+s"
data-load-indicator="<?= e(trans('backend::lang.form.saving')) ?>"
class="btn btn-primary">
<?= e(trans('backend::lang.form.create')) ?>
</button>
<button
type="button"
data-request="onSave"
data-request-data="close:1"
data-hotkey="ctrl+enter, cmd+enter"
data-load-indicator="<?= e(trans('backend::lang.form.saving')) ?>"
class="btn btn-default">
<?= e(trans('backend::lang.form.create_and_close')) ?>
</button>
<span class="btn-text">
<?= e(trans('backend::lang.form.or')) ?> <a href="<?= Backend::url('rainlab/blog/categories') ?>"><?= e(trans('backend::lang.form.cancel')) ?></a>
</span>
</div>
</div>
<?= Form::close() ?>
<?php else: ?>
<p class="flash-message static error"><?= e(trans($this->fatalError)) ?></p>
<p><a href="<?= Backend::url('rainlab/blog/posts') ?>" class="btn btn-default"><?= e(trans('rainlab.blog::lang.category.return_to_categories')) ?></a></p>
<?php endif ?>

View File

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

View File

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

View File

@ -0,0 +1,54 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?= Backend::url('rainlab/blog/posts') ?>"><?= e(trans('rainlab.blog::lang.blog.menu_label')) ?></a></li>
<li><?= e(trans($this->pageTitle)) ?></li>
</ul>
<?php Block::endPut() ?>
<?php if (!$this->fatalError): ?>
<?= Form::open(['class' => 'layout']) ?>
<div class="layout-row">
<?= $this->formRender() ?>
</div>
<div class="form-buttons">
<div class="loading-indicator-container">
<button
type="submit"
data-request="onSave"
data-request-data="redirect:0"
data-hotkey="ctrl+s, cmd+s"
data-load-indicator="<?= e(trans('backend::lang.form.saving')) ?>"
class="btn btn-primary">
<?= e(trans('backend::lang.form.save')) ?>
</button>
<button
type="button"
data-request="onSave"
data-request-data="close:1"
data-hotkey="ctrl+enter, cmd+enter"
data-load-indicator="<?= e(trans('backend::lang.form.saving')) ?>"
class="btn btn-default">
<?= e(trans('backend::lang.form.save_and_close')) ?>
</button>
<button
type="button"
class="oc-icon-trash-o btn-icon danger pull-right"
data-request="onDelete"
data-load-indicator="<?= e(trans('backend::lang.form.deleting')) ?>"
data-request-confirm="<?= e(trans('rainlab.blog::lang.category.delete_confirm')) ?>">
</button>
<span class="btn-text">
<?= e(trans('backend::lang.form.or')) ?> <a href="<?= Backend::url('rainlab/blog/categories') ?>"><?= e(trans('backend::lang.form.cancel')) ?></a>
</span>
</div>
</div>
<?= Form::close() ?>
<?php else: ?>
<p class="flash-message static error"><?= e(trans($this->fatalError)) ?></p>
<p><a href="<?= Backend::url('rainlab/blog/posts') ?>" class="btn btn-default"><?= e(trans('rainlab.blog::lang.category.return_to_categories')) ?></a></p>
<?php endif ?>

View File

@ -0,0 +1,37 @@
<div data-control="toolbar">
<a
href="<?= Backend::url('rainlab/blog/posts/create') ?>"
class="btn btn-primary oc-icon-plus">
<?= e(trans('rainlab.blog::lang.posts.new_post')) ?>
</a>
<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('rainlab.blog::lang.blog.delete_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 if ($this->user->hasAnyAccess(['rainlab.blog.access_import_export'])): ?>
<div class="btn-group">
<a
href="<?= Backend::url('rainlab/blog/posts/export') ?>"
class="btn btn-default oc-icon-download">
<?= e(trans('rainlab.blog::lang.posts.export_post')) ?>
</a>
<a
href="<?= Backend::url('rainlab/blog/posts/import') ?>"
class="btn btn-default oc-icon-upload">
<?= e(trans('rainlab.blog::lang.posts.import_post')) ?>
</a>
</div>
<?php endif ?>
</div>

View File

@ -0,0 +1,56 @@
<?php
$isCreate = $this->formGetContext() == 'create';
$pageUrl = isset($pageUrl) ? $pageUrl : null;
?>
<div class="form-buttons loading-indicator-container">
<!-- Save -->
<a
href="javascript:;"
class="btn btn-primary oc-icon-check save"
data-request="onSave"
data-load-indicator="<?= e(trans('backend::lang.form.saving')) ?>"
data-request-before-update="$(this).trigger('unchange.oc.changeMonitor')"
<?php if (!$isCreate): ?>data-request-data="redirect:0"<?php endif ?>
data-hotkey="ctrl+s, cmd+s">
<?= e(trans('backend::lang.form.save')) ?>
</a>
<?php if (!$isCreate): ?>
<!-- Save and Close -->
<a
href="javascript:;"
class="btn btn-primary oc-icon-check save"
data-request-before-update="$(this).trigger('unchange.oc.changeMonitor')"
data-request="onSave"
data-load-indicator="<?= e(trans('backend::lang.form.saving')) ?>">
<?= e(trans('backend::lang.form.save_and_close')) ?>
</a>
<?php endif ?>
<!-- Cancel -->
<a
href="<?= Backend::url('rainlab/blog/posts') ?>"
class="btn btn-primary oc-icon-arrow-left cancel">
<?= e(trans('backend::lang.form.cancel')) ?>
</a>
<!-- Preview -->
<a
href="<?= URL::to($pageUrl) ?>"
target="_blank"
class="btn btn-primary oc-icon-crosshairs <?php if (!false): ?>hide<?php endif ?>"
data-control="preview-button">
<?= e(trans('rainlab.blog::lang.blog.preview')) ?>
</a>
<?php if (!$isCreate): ?>
<!-- Delete -->
<button
type="button"
class="btn btn-default empty oc-icon-trash-o"
data-request="onDelete"
data-request-confirm="<?= e(trans('rainlab.blog::lang.post.delete_confirm')) ?>"
data-control="delete-button"></button>
<?php endif ?>
</div>

View File

@ -0,0 +1,16 @@
# ===================================
# Form Behavior Config
# ===================================
name: rainlab.blog::lang.blog.create_post
form: $/rainlab/blog/models/post/fields.yaml
modelClass: RainLab\Blog\Models\Post
defaultRedirect: rainlab/blog/posts
create:
redirect: rainlab/blog/posts/update/:id
redirectClose: rainlab/blog/posts
update:
redirect: rainlab/blog/posts
redirectClose: rainlab/blog/posts

View File

@ -0,0 +1,41 @@
# ===================================
# Import/Export Behavior Config
# ===================================
import:
# Page title
title: rainlab.blog::lang.posts.import_post
# Import List Column configuration
list: $/rainlab/blog/models/postimport/columns.yaml
# Import Form Field configuration
form: $/rainlab/blog/models/postimport/fields.yaml
# Import Model class
modelClass: RainLab\Blog\Models\PostImport
# Redirect when finished
redirect: rainlab/blog/posts
# Required permissions
permissions: rainlab.blog.access_import_export
export:
# Page title
title: rainlab.blog::lang.posts.export_post
# Output file name
fileName: posts.csv
# Export List Column configuration
list: $/rainlab/blog/models/postexport/columns.yaml
# Export Model class
modelClass: RainLab\Blog\Models\PostExport
# Redirect when finished
redirect: rainlab/blog/posts
# Required permissions
permissions: rainlab.blog.access_import_export

View File

@ -0,0 +1,47 @@
# ===================================
# List Behavior Config
# ===================================
# Model List Column configuration
list: $/rainlab/blog/models/post/columns.yaml
# Filter widget configuration
filter: $/rainlab/blog/models/post/scopes.yaml
# Model Class name
modelClass: RainLab\Blog\Models\Post
# List Title
title: rainlab.blog::lang.posts.list_title
# Link URL for each record
recordUrl: rainlab/blog/posts/update/:id
# Message to display if the list is empty
noRecordsMessage: backend::lang.list.no_records
# Records to display per page
recordsPerPage: 25
# Displays the list column set up button
showSetup: true
# Displays the sorting link on each column
showSorting: true
# Default sorting column
defaultSort:
column: published_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,23 @@
<?php if (!$this->fatalError): ?>
<div class="layout fancy-layout">
<?= Form::open([
'class' => 'layout',
'data-change-monitor' => 'true',
'data-window-close-confirm' => e(trans('rainlab.blog::lang.post.close_confirm')),
'id' => 'post-form'
]) ?>
<?= $this->formRender() ?>
<?= Form::close() ?>
</div>
<?php else: ?>
<div class="control-breadcrumb">
<?= Block::placeholder('breadcrumb') ?>
</div>
<div class="padded-container">
<p class="flash-message static error"><?= e(trans($this->fatalError)) ?></p>
<p><a href="<?= Backend::url('rainlab/blog/posts') ?>" class="btn btn-default"><?= e(trans('rainlab.blog::lang.post.return_to_posts')) ?></a></p>
</div>
<?php endif ?>

View File

@ -0,0 +1,27 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?= Backend::url('rainlab/blog/posts') ?>"><?= e(trans('rainlab.blog::lang.blog.menu_label')) ?></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">
<div class="loading-indicator-container">
<button
type="submit"
data-control="popup"
data-handler="onExportLoadForm"
data-keyboard="false"
class="btn btn-primary">
<?= e(trans('rainlab.blog::lang.posts.export_post')) ?>
</button>
</div>
</div>
<?= Form::close() ?>

View File

@ -0,0 +1,25 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?= Backend::url('rainlab/blog/posts') ?>"><?= e(trans('rainlab.blog::lang.blog.menu_label')) ?></a></li>
<li><?= e(trans($this->pageTitle)) ?></li>
</ul>
<?php Block::endPut() ?>
<?= Form::open(['class' => 'layout']) ?>
<div class="layout-row">
<?= $this->importRender() ?>
</div>
<div class="form-buttons">
<button
type="submit"
data-control="popup"
data-handler="onImportLoadForm"
data-keyboard="false"
class="btn btn-primary">
<?= e(trans('rainlab.blog::lang.posts.import_post')) ?>
</button>
</div>
<?= Form::close() ?>

View File

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

View File

@ -0,0 +1,25 @@
<?php if (!$this->fatalError): ?>
<div class="layout fancy-layout">
<?= Form::open([
'class' => 'layout',
'data-change-monitor' => 'true',
'data-window-close-confirm' => e(trans('rainlab.blog::lang.post.close_confirm')),
'id' => 'post-form'
]) ?>
<?= $this->formRender() ?>
<?= Form::close() ?>
</div>
<?php else: ?>
<div class="control-breadcrumb">
<?= Block::placeholder('breadcrumb') ?>
</div>
<div class="padded-container">
<p class="flash-message static error"><?= e(trans($this->fatalError)) ?></p>
<p><a href="<?= Backend::url('rainlab/blog/posts') ?>" class="btn btn-default"><?= e(trans('rainlab.blog::lang.post.return_to_posts')) ?></a></p>
</div>
<?php endif ?>

View File

@ -0,0 +1,133 @@
<?php namespace RainLab\Blog\FormWidgets;
use Lang;
use Input;
use Response;
use Validator;
use RainLab\Blog\Models\Post as PostModel;
use Backend\Classes\FormWidgetBase;
use Backend\FormWidgets\MarkdownEditor;
use System\Models\File;
use ValidationException;
use SystemException;
use Exception;
/**
* Special markdown editor for the Create/Edit Post form.
*
* @package rainlab\blog
* @author Alexey Bobkov, Samuel Georges
*/
class BlogMarkdown extends MarkdownEditor
{
/**
* {@inheritDoc}
*/
public function init()
{
$this->viewPath = base_path().'/modules/backend/formwidgets/markdowneditor/partials';
$this->checkUploadPostback();
parent::init();
}
/**
* {@inheritDoc}
*/
protected function loadAssets()
{
$this->assetPath = '/modules/backend/formwidgets/markdowneditor/assets';
parent::loadAssets();
}
/**
* Disable HTML cleaning on the widget level since the PostModel will handle it
*
* @return boolean
*/
protected function shouldCleanHtml()
{
return false;
}
/**
* {@inheritDoc}
*/
public function onRefresh()
{
$content = post($this->formField->getName());
$previewHtml = PostModel::formatHtml($content, true);
return [
'preview' => $previewHtml
];
}
/**
* Handle images being uploaded to the blog post
*
* @return void
*/
protected function checkUploadPostback()
{
if (!post('X_BLOG_IMAGE_UPLOAD')) {
return;
}
$uploadedFileName = null;
try {
$uploadedFile = Input::file('file');
if ($uploadedFile)
$uploadedFileName = $uploadedFile->getClientOriginalName();
$validationRules = ['max:'.File::getMaxFilesize()];
$validationRules[] = 'mimes:jpg,jpeg,bmp,png,gif';
$validation = Validator::make(
['file_data' => $uploadedFile],
['file_data' => $validationRules]
);
if ($validation->fails()) {
throw new ValidationException($validation);
}
if (!$uploadedFile->isValid()) {
throw new SystemException(Lang::get('cms::lang.asset.file_not_valid'));
}
$fileRelation = $this->model->content_images();
$file = new File();
$file->data = $uploadedFile;
$file->is_public = true;
$file->save();
$fileRelation->add($file, $this->sessionKey);
$result = [
'file' => $uploadedFileName,
'path' => $file->getPath()
];
$response = Response::make()->setContent($result);
$this->controller->setResponse($response);
} catch (Exception $ex) {
$message = $uploadedFileName
? Lang::get('cms::lang.asset.error_uploading_file', ['name' => $uploadedFileName, 'error' => $ex->getMessage()])
: $ex->getMessage();
$result = [
'error' => $message,
'file' => $uploadedFileName
];
$response = Response::make()->setContent($result);
$this->controller->setResponse($response);
}
}
}

View File

@ -0,0 +1,137 @@
<?php namespace RainLab\Blog\FormWidgets;
use RainLab\Blog\Models\Post;
use RainLab\Translate\Models\Locale;
/**
* A multi-lingual version of the blog markdown editor.
* This class should never be invoked without the RainLab.Translate plugin.
*
* @package rainlab\blog
* @author Alexey Bobkov, Samuel Georges
*/
class MLBlogMarkdown extends BlogMarkdown
{
use \RainLab\Translate\Traits\MLControl;
/**
* {@inheritDoc}
*/
protected $defaultAlias = 'mlmarkdowneditor';
public $originalAssetPath;
public $originalViewPath;
/**
* @var bool legacyMode disables the Vue integration
*/
public $legacyMode = true;
/**
* {@inheritDoc}
*/
public function init()
{
parent::init();
$this->initLocale();
}
/**
* {@inheritDoc}
*/
public function render()
{
$this->actAsParent();
$parentContent = parent::render();
$this->actAsParent(false);
if (!$this->isAvailable) {
return $parentContent;
}
$this->vars['markdowneditor'] = $parentContent;
$this->actAsControl(true);
return $this->makePartial('mlmarkdowneditor');
}
public function prepareVars()
{
parent::prepareVars();
$this->prepareLocaleVars();
}
/**
* Returns an array of translated values for this field
* @param $value
* @return array
*/
public function getSaveValue($value)
{
$localeData = $this->getLocaleSaveData();
/*
* Set the translated values to the model
*/
if ($this->model->methodExists('setAttributeTranslated')) {
foreach ($localeData as $locale => $value) {
$this->model->setAttributeTranslated('content', $value, $locale);
$this->model->setAttributeTranslated(
'content_html',
Post::formatHtml($value),
$locale
);
}
}
return array_get($localeData, $this->defaultLocale->code, $value);
}
/**
* {@inheritDoc}
*/
protected function loadAssets()
{
$this->actAsParent();
parent::loadAssets();
$this->actAsParent(false);
if (Locale::isAvailable()) {
$this->loadLocaleAssets();
$this->actAsControl(true);
$this->addJs('js/mlmarkdowneditor.js');
$this->actAsControl(false);
}
}
protected function actAsParent($switch = true)
{
if ($switch) {
$this->originalAssetPath = $this->assetPath;
$this->originalViewPath = $this->viewPath;
$this->assetPath = '/modules/backend/formwidgets/markdowneditor/assets';
$this->viewPath = base_path('/modules/backend/formwidgets/markdowneditor/partials');
}
else {
$this->assetPath = $this->originalAssetPath;
$this->viewPath = $this->originalViewPath;
}
}
protected function actAsControl($switch = true)
{
if ($switch) {
$this->originalAssetPath = $this->assetPath;
$this->originalViewPath = $this->viewPath;
$this->assetPath = '/plugins/rainlab/translate/formwidgets/mlmarkdowneditor/assets';
$this->viewPath = base_path('/plugins/rainlab/translate/formwidgets/mlmarkdowneditor/partials');
}
else {
$this->assetPath = $this->originalAssetPath;
$this->viewPath = $this->originalViewPath;
}
}
}

View File

@ -0,0 +1,100 @@
<?php
return [
'plugin' => [
'name' => 'Блог',
'description' => 'Стабилната блог платформа.'
],
'blog' => [
'menu_label' => 'Блог',
'menu_description' => 'управление на публикациите',
'posts' => 'публикации',
'create_post' => 'създай публикация',
'categories' => 'категории',
'create_category' => 'създай категория',
'tab' => 'Блог',
'access_posts' => 'управление на публикациите',
'access_categories' => 'управление на категории',
'access_other_posts' => 'управление на други потребители публикации в блога',
'delete_confirm' => 'Сигурни ли сте?',
'chart_published' => 'Публикувано',
'chart_drafts' => 'Чернови',
'chart_total' => 'Общо'
],
'posts' => [
'list_title' => 'Управление публикациите в блога',
'filter_category' => 'Категория',
'filter_published' => 'Скрий публикуваните',
'new_post' => 'Нова публикация'
],
'post' => [
'title' => 'Заглавие',
'title_placeholder' => 'Ново заглавие на публикацията',
'slug' => 'Slug',
'slug_placeholder' => 'нов slug на публикацията',
'categories' => 'Категории',
'created' => 'Създаден',
'updated' => 'Обновен',
'published' => 'Публикуван',
'published_validation' => 'Моля, посочете дата на публикуване',
'tab_edit' => 'Промяна',
'tab_categories' => 'Категории',
'categories_comment' => 'Изберете категории към който пренадлежи публикацията ',
'categories_placeholder' => 'Няма категирии, Създайте първата?!',
'tab_manage' => 'Управление',
'published_on' => 'публикувано в',
'excerpt' => 'Откъс',
'featured_images' => 'Избрани снимки',
'delete_confirm' => 'Наистина ли искате да изтриете тази публикация?',
'close_confirm' => 'Публикацията не е запазена.',
'return_to_posts' => 'Върни ме към всички публикации'
],
'categories' => [
'list_title' => 'Управление категориите в блога',
'new_category' => 'Нова категория',
'uncategorized' => 'Без категория'
],
'category' => [
'name' => 'Име',
'name_placeholder' => 'Ново име на категорията',
'slug' => 'Slug',
'slug_placeholder' => 'нов slug на категотията',
'posts' => 'публикации',
'delete_confirm' => 'Наистина ли искате да изтриете тази категория?',
'return_to_categories' => 'Върни ме към всички категории'
],
'settings' => [
'category_title' => 'Списък с категории',
'category_description' => 'Показва списък с категориите на блога.',
'category_slug' => 'категория slug',
'category_slug_description' => "Look up the blog category using the supplied slug value. This property is used by the default component partial for marking the currently active category.",
'category_display_empty' => 'Показване на празни категории',
'category_display_empty_description' => 'Показване на категории, които нямат никакви публикации.',
'category_page' => 'Страница на категория',
'category_page_description' => 'Име на страницата за категирия. Това се използва подразбиране от компонента.',
'post_title' => 'Публикация',
'post_description' => 'Показване на Публикациите в блога на страницата.',
'post_slug' => 'Post slug',
'post_slug_description' => "Търсене на публикации по зададен slug.",
'post_category' => 'Страница за Категория',
'post_category_description' => 'Име на страница за категория за генериране на линк.Това се използва подразбиране от компонента.',
'posts_title' => 'Лист с Публикации',
'posts_description' => 'Показване на лист с публикации на страницата.',
'posts_pagination' => 'Номер на страницата',
'posts_pagination_description' => 'Тази стойност се използва за определяне на коя страница е потребителя.',
'posts_filter' => 'Филтер Категория',
'posts_filter_description' => 'Въведи slug на категория или URL адрес за филтриране по. Оставете празно за да се покажат всички публикации.',
'posts_per_page' => 'Публикации на страница',
'posts_per_page_validation' => 'Невалиден формат за публикации на страница',
'posts_no_posts' => 'Няма публикации',
'posts_no_posts_description' => 'Съобщение което да се покаже, в случай ,че няма публикации за показване.Това се използва подразбиране от компонента.',
'posts_order' => 'подреждане на публикации',
'posts_order_description' => 'Атрибут по който да бъдат подредени публикациите',
'posts_category' => 'страница на категориите',
'posts_category_description' => 'Име на страницата за категории , за "публикувано в". Това се използва подразбиране от компонента.',
'posts_post' => 'Post page',
'posts_post_description' => 'Име на страницата за публикации "Прочетете повече". Това се използва подразбиране от компонента.',
'posts_except_post' => 'Except post',
'posts_except_post_description' => 'Enter ID/URL or variable with post ID/URL you want to except',
]
];

View File

@ -0,0 +1,154 @@
<?php
return [
'plugin' => [
'name' => 'Blog',
'description' => 'Robustní blogová platforma.'
],
'blog' => [
'menu_label' => 'Blog',
'menu_description' => 'Správa blogových příspěvků',
'posts' => 'Příspěvky',
'create_post' => 'Příspěvek',
'categories' => 'Kategorie',
'create_category' => 'Kategorie příspěvků',
'tab' => 'Blog',
'access_posts' => 'Správa blogových příspěvků',
'access_categories' => 'Správa blogových kategorií',
'access_other_posts' => 'Správa příspěvků ostatních uživatelů',
'access_import_export' => 'Možnost importu a exportu příspěvků',
'access_publish' => 'Možnost publikovat příspěvky',
'delete_confirm' => 'Jste si jistí?',
'chart_published' => 'Publikované',
'chart_drafts' => 'Návrhy',
'chart_total' => 'Celkem',
],
'posts' => [
'list_title' => 'Správa blogových příspěvků',
'filter_category' => 'Kategorie',
'filter_published' => 'Schovat publikované',
'filter_date' => 'Datum',
'new_post' => 'Nový příspěvek',
'export_post' => 'Export příspěvků',
'import_post' => 'Import příspěvků',
],
'post' => [
'title' => 'Název',
'title_placeholder' => 'Zadejte název',
'content' => 'Obsah',
'content_html' => 'HTML obsah',
'slug' => 'URL příspěvku',
'slug_placeholder' => 'zadejte-url-prispevku',
'categories' => 'Kategorie',
'author_email' => 'E-mail autora',
'created' => 'Vytvořeno',
'created_date' => 'Vytvořeno dne',
'updated' => 'Upraveno',
'updated_date' => 'Upraveno dne',
'published' => 'Publikováno',
'published_date' => 'Publikováno dne',
'published_validation' => 'Zadejte prosím datum publikace příspěvku',
'tab_edit' => 'Upravit',
'tab_categories' => 'Kategorie',
'categories_comment' => 'Vyberte kategorie do kterých příspěvek patří',
'categories_placeholder' => 'Nejsou zde žádné kategorie, nejdříve musíte nějaké vytvořit!',
'tab_manage' => 'Nastavení',
'published_on' => 'Publikováno dne',
'excerpt' => 'Perex příspěvku',
'summary' => 'Shrnutí',
'featured_images' => 'Obrázky',
'delete_confirm' => 'Opravdu chcete smazat tento příspěvek?',
'delete_success' => 'Vybrané příspěvky úspěšně odstraněny.',
'close_confirm' => 'Příspěvek není uložený.',
'return_to_posts' => 'Zpět na seznam příspěvků',
],
'categories' => [
'list_title' => 'Správa blogových kategorií',
'new_category' => 'Nová kategorie',
'uncategorized' => 'Nezařazeno',
],
'category' => [
'name' => 'Název',
'name_placeholder' => 'Název nové kategorie',
'description' => 'Popis',
'slug' => 'URL kategorie',
'slug_placeholder' => 'zadejte-url-kategorie',
'posts' => 'Počet příspěvků',
'delete_confirm' => 'Opravdu chcete smazat tuto kategorii?',
'delete_success' => 'Vybrané kategorie úspěšně odstraněny.',
'return_to_categories' => 'Zpět na seznam blogových kategorií',
'reorder' => 'Změnit pořadí',
],
'menuitem' => [
'blog_category' => 'Blogová kategorie',
'all_blog_categories' => 'Všechny blogové kategorie',
'blog_post' => 'Blogový příspěvek',
'all_blog_posts' => 'Všechny blogové příspěvky',
'category_blog_posts' => 'Blog category posts'
],
'settings' => [
'category_title' => 'Seznam kategorií',
'category_description' => 'Zobrazí na stránce seznam blogových kategorií.',
'category_slug' => 'URL kategorie',
'category_slug_description' => "Najde blogovou kategorii s tímto URL. Používá se pro zobrazení aktivní kategorie.",
'category_display_empty' => 'Zobrazit prázdné kategorie',
'category_display_empty_description' => 'Zobrazit kategorie bez blogových příspěvků.',
'category_page' => 'Stránka kategorií',
'category_page_description' => 'Vyberte stránku která slouží k zobrazení všech kategorií (nebo detailu kategorie).',
'post_title' => 'Příspěvek',
'post_description' => 'Zobrazí blogový příspěvek na stránce.',
'post_slug' => 'URL příspěvku',
'post_slug_description' => "Najde příspěvek dle zadané URL.",
'post_category' => 'Stránka kategorie',
'post_category_description' => 'Vyberte stránku která slouží k zobrazení všech kategorií (nebo detailu kategorie).',
'posts_title' => 'Seznam příspěvků',
'posts_description' => 'Zobrazí na stránce seznam posledních příspěvků na stránkách.',
'posts_pagination' => 'Číslo stránky',
'posts_pagination_description' => 'Číslo stránky určující na které stránce se uživatel nachází. Použito pro stránkování.',
'posts_filter' => 'Filtr kategorií',
'posts_filter_description' => 'Zadejte URL kategorie, nebo URL parametr pro filtrování příspěvků. Nechte prázdné pro zobrazení všech příspěvků.',
'posts_per_page' => 'Příspěvků na stránku',
'posts_per_page_validation' => 'Špatný formát počtu příspěvků na stránku, musí být zadáno jako číslo',
'posts_no_posts' => 'Hláška prázdné stránky',
'posts_no_posts_description' => 'Zpráva se zobrazí pokud se nepovede najít žádné články.',
'posts_no_posts_default' => 'Nenalezeny žádné příspěvky',
'posts_order' => 'Řazení článků',
'posts_order_decription' => 'Nastaví řazení článků ve výpisu',
'posts_category' => 'Stránka kategorií',
'posts_category_description' => 'Vyberte stránku která slouží k zobrazení všech kategorií (nebo detailu kategorie).',
'posts_post' => 'Stránka příspěvků',
'posts_post_description' => 'Vyberte stránku která slouží k zobrazení článků (nebo detailu článku).',
'posts_except_post' => 'Vyloučit příspěvěk',
'posts_except_post_description' => 'Zadejte ID nebo URL příspěvku který chcete vyloučit',
'posts_except_categories' => 'Vyloučené kategorie',
'posts_except_categories_description' => 'Pro vyloučení kategorií zadejte čárkou oddělené URL příspěvků nebo proměnnou, která tento seznam obsahuje.',
'rssfeed_blog' => 'Blogová stránka',
'rssfeed_blog_description' => 'Name of the main blog page file for generating links. This property is used by the default component partial.',
'rssfeed_title' => 'RSS Kanál',
'rssfeed_description' => 'Vygeneruje RSS kanál který obsahuje blogové příspěvky.',
'group_links' => 'Odkazy',
'group_exceptions' => 'Výjimky'
],
'sorting' => [
'title_asc' => 'Název (sestupně)',
'title_desc' => 'Název (vzestupně)',
'created_asc' => 'Vytvořeno (sestupně)',
'created_desc' => 'Vytvořeno (vzestupně)',
'updated_asc' => 'Upraveno (sestupně)',
'updated_desc' => 'Upraveno (vzestupně)',
'published_asc' => 'Publikováno (sestupně)',
'published_desc' => 'Publikováno (vzestupně)',
'random' => 'Náhodně'
],
'import' => [
'update_existing_label' => 'Uprav existující příspěvky',
'update_existing_comment' => 'Zvolte pokud chcete upravit příspěvky se stejným ID, názvem nebo URL.',
'auto_create_categories_label' => 'VYtvořit kategorie ze souboru',
'auto_create_categories_comment' => 'Chcete-li tuto funkci použít, měli byste se shodovat se sloupcem Kategorie, jinak vyberte výchozí kategorie, které chcete použít z níže uvedených položek.',
'categories_label' => 'Kategorie',
'categories_comment' => 'Vyberte kategorie ke kterým budou příspěvky přiřazeny (volitelné).',
'default_author_label' => 'Výchozí autor příspěvků (volitelné)',
'default_author_comment' => 'Import se pokusí použít existujícího autora, pokud odpovídá sloupci email, jinak se použije výše uvedený autor.',
'default_author_placeholder' => '-- vyberte autora --'
]
];

View File

@ -0,0 +1,132 @@
<?php
return [
'plugin' => [
'name' => 'Blog',
'description' => 'Eine robuste Blog Plattform.'
],
'blog' => [
'menu_label' => 'Blog',
'menu_description' => 'Blog Artikel bearbeiten',
'posts' => 'Artikel',
'create_post' => 'Blog Artikel',
'categories' => 'Kategorien',
'create_category' => 'Blog Kategorie',
'tab' => 'Blog',
'access_posts' => 'Blog Artikel verwalten',
'access_categories' => 'Blog Kategorien verwalten',
'access_other_posts' => 'Blog Artikel anderer Benutzer verwalten',
'access_import_export' => 'Blog Artikel importieren oder exportieren',
'access_publish' => 'Kann Artikel veröffentlichen',
'delete_confirm' => 'Bist du sicher?',
'chart_published' => 'Veröffentlicht',
'chart_drafts' => 'Entwurf',
'chart_total' => 'Gesamt'
],
'posts' => [
'list_title' => 'Blog Artikel verwalten',
'filter_category' => 'Kategorie',
'filter_published' => 'Veröffentlichte ausblenden',
'filter_date' => 'Date',
'new_post' => 'Neuer Artikel',
'export_post' => 'Exportiere Artikel',
'import_post' => 'Importiere Artikel'
],
'post' => [
'title' => 'Titel',
'title_placeholder' => 'Neuer Titel',
'content' => 'Inhalt',
'content_html' => 'HTML-Inhalt',
'slug' => 'Slug',
'slug_placeholder' => 'neuer-artikel-slug',
'categories' => 'Kategorien',
'author_email' => 'Autor E-Mail',
'created' => 'Erstellt',
'created_date' => 'Erstellzeitpunkt',
'updated' => 'Aktualisiert',
'updated_date' => 'Aktualisierungszeitpunk',
'published' => 'Veröffentlicht',
'published_date' => 'Veröffentlichungszeitpunkt',
'published_validation' => 'Bitte gebe das Datum der Veröffentlichung an',
'tab_edit' => 'Bearbeiten',
'tab_categories' => 'Kategorien',
'categories_comment' => 'Wähle die zugehörigen Kategorien',
'categories_placeholder' => 'Es existieren keine Kategorien. Bitte lege zuerst Kategorien an!',
'tab_manage' => 'Verwalten',
'published_on' => 'Veröffentlicht am',
'excerpt' => 'Textauszug',
'summary' => 'Zusammenfassung',
'featured_images' => 'Zugehörige Bilder',
'delete_confirm' => 'Möchtest du diesen Artikel wirklich löschen?',
'close_confirm' => 'Der Artikel ist noch nicht gespeichert.',
'return_to_posts' => 'Zurück zur Artikel-Übersicht',
'posted_byline' => 'Veröffentlicht in :categories am :date',
'posted_byline_no_categories' => 'Veröffentlicht am :date',
'date_format' => 'd. F Y',
],
'categories' => [
'list_title' => 'Blog Kategorien verwalten',
'new_category' => 'Neue Kategorie',
'uncategorized' => 'Allgemein'
],
'category' => [
'name' => 'Name',
'name_placeholder' => 'Neuer Kategorie Name',
'description' => 'Beschreibung',
'slug' => 'Slug',
'slug_placeholder' => 'neuer-kategorie-slug',
'posts' => 'Artikel',
'delete_confirm' => 'Möchtest du die Kategorie wirklich löschen?',
'return_to_categories' => 'Zurück zur Kategorie-Übersicht.',
'reorder' => 'Kategorien sortieren'
],
'menuitem' => [
'blog_category' => 'Blog Kategorie',
'all_blog_categories' => 'Alle Blog Kategorien',
'blog_post' => 'Blog Artikel',
'all_blog_posts' => 'Alle Blog Artikel',
'category_blog_posts' => 'Blog Kategorie Artikel'
],
'settings' => [
'category_title' => 'Blog Kategorie-Übersicht',
'category_description' => 'Zeigt eine Blog Kategorien-Übersicht.',
'category_slug' => 'Slug Parametername',
'category_slug_description' => 'Der URL-Routen-Parameter welcher verwendet wird um die aktuelle Kategorie zu bestimmen. Wird von der Standard-Komponente benötigt um die aktive Kategorie zu markieren.',
'category_display_empty' => 'Leere Kategorien anzeigen',
'category_display_empty_description' => 'Kategorien zeigen welche keine Artikel besitzen.',
'category_page' => 'Kategorien Seite',
'category_page_description' => 'Name der Kategorien-Seiten-Datei für die Kategorien Links. Wird von der Standard-Komponente benötigt.',
'post_title' => 'Blog Artikel',
'post_description' => 'Zeigt einen Blog Artikel auf der Seite.',
'post_slug' => 'Slug Parametername',
'post_slug_description' => 'Der URL-Routen-Parameter um den Post mittels "Slug" zu bestimmen.',
'post_category' => 'Kategorien-Seite',
'post_category_description' => 'Name der Kategorien-Seiten-Datei für Kategorie-Links.',
'posts_title' => 'Blog Artikel-Übersicht',
'posts_description' => 'Stellt eine Liste der neuesten Artikel auf der Seite dar.',
'posts_pagination' => 'Blättern Parametername',
'posts_pagination_description' => 'Der erwartete Parametername welcher für Seiten verwendet wird.',
'posts_filter' => 'Kategorien-Filter',
'posts_filter_description' => 'Bitte gebe ein Kategorien-Slug oder URL-Parameter an, mittels den die Artikel gefiltert werden. Wenn der Wert leer ist, werden alle Artikel angezeigt.',
'posts_per_page' => 'Artikel pro Seite',
'posts_per_page_validation' => 'Ungültiger "Artikel pro Seiten" Wert',
'posts_no_posts' => 'Keine Artikel Nachricht',
'posts_no_posts_description' => 'Nachricht welche dargestellt wird wenn keine Artikel vorhanden sind. Dieser Wert wird von der Standard-Komponente verwendet.',
'posts_order' => 'Artikel Sortierung',
'posts_order_description' => 'Attribute nach welchem Artikel sortiert werden.',
'posts_category' => 'Kategorien-Seite',
'posts_category_description' => 'Name der Kategorien-Seiten-Datei für "Veröffentlicht in" Kategorien-Links. Dieser Wert von der Standard-Komponente verwendet.',
'posts_post' => 'Artikel Seite',
'posts_post_description' => 'Name der Artikel-Seiten-Datei für die "Erfahre mehr" Links. Dieser Wert für von der Standard-Komponente verwendet.',
'posts_except_post' => 'Artikel ausschließen',
'posts_except_post_description' => 'Gebe direkt die ID/URL oder eine Variable mit der Artikel-ID/URL an um diesen Artikel auszuschließen. Dieser Wert für von der Standard-Komponente verwendet.',
'posts_except_categories' => 'Kategorien ausschließen',
'posts_except_categories_description' => 'Gebe eine kommagetrennte Liste von Kategorie-Slugs oder eine Variable mit einer solchen Liste an um deren Artikel auszuschließen. Die Dieser Wert für von der Standard-Komponente verwendet.',
'rssfeed_blog' => 'Blog Seite',
'rssfeed_blog_description' => 'Name der Artikel-Seiten-Datei für die Links. Dieser Wert für von der Standard-Komponente verwendet.',
'rssfeed_title' => 'RSS-Feed',
'rssfeed_description' => 'Erstellt einen RSS-Feed mit Artikeln aus dem Blog.',
'group_links' => 'Links',
'group_exceptions' => 'Ausnahmen'
]
];

View File

@ -0,0 +1,169 @@
<?php
return [
'plugin' => [
'name' => 'Blog',
'description' => 'A robust blogging platform.'
],
'blog' => [
'menu_label' => 'Blog',
'menu_description' => 'Manage Blog Posts',
'posts' => 'Posts',
'create_post' => 'Blog post',
'categories' => 'Categories',
'create_category' => 'Blog category',
'tab' => 'Blog',
'access_posts' => 'Manage the blog posts',
'access_categories' => 'Manage the blog categories',
'access_other_posts' => 'Manage other users blog posts',
'access_import_export' => 'Allowed to import and export posts',
'access_publish' => 'Allowed to publish posts',
'manage_settings' => 'Manage blog settings',
'delete_confirm' => 'Are you sure?',
'chart_published' => 'Published',
'chart_drafts' => 'Drafts',
'chart_total' => 'Total',
'settings_description' => 'Manage blog settings',
'show_all_posts_label' => 'Show All Posts to Backend Users',
'show_all_posts_comment' => 'Display both published and unpublished posts on the frontend to backend users',
'use_legacy_editor_label' => 'Use the Legacy Markdown Editor',
'use_legacy_editor_comment' => 'Enable the older version of the markdown editor when using October CMS v2 and above',
'tab_general' => 'General',
'preview' => 'Preview'
],
'posts' => [
'list_title' => 'Manage the blog posts',
'filter_category' => 'Category',
'filter_published' => 'Published',
'filter_date' => 'Date',
'new_post' => 'New Post',
'export_post' => 'Export Posts',
'import_post' => 'Import Posts'
],
'post' => [
'title' => 'Title',
'title_placeholder' => 'New post title',
'content' => 'Content',
'content_html' => 'HTML Content',
'slug' => 'Slug',
'slug_placeholder' => 'new-post-slug',
'categories' => 'Categories',
'author_email' => 'Author Email',
'created' => 'Created',
'created_date' => 'Created date',
'updated' => 'Updated',
'updated_date' => 'Updated date',
'published' => 'Published',
'published_by' => 'Published by',
'current_user' => 'Current user',
'published_date' => 'Published date',
'published_validation' => 'Please specify the published date',
'tab_edit' => 'Edit',
'tab_categories' => 'Categories',
'categories_comment' => 'Select categories the blog post belongs to',
'categories_placeholder' => 'There are no categories, you should create one first!',
'tab_manage' => 'Manage',
'published_on' => 'Published on',
'excerpt' => 'Excerpt',
'summary' => 'Summary',
'featured_images' => 'Featured Images',
'delete_confirm' => 'Delete this post?',
'delete_success' => 'Successfully deleted those posts.',
'close_confirm' => 'The post is not saved.',
'return_to_posts' => 'Return to posts list',
'posted_byline' => 'Posted in :categories on :date.',
'posted_byline_no_categories' => 'Posted on :date.',
'date_format' => 'M d, Y',
],
'categories' => [
'list_title' => 'Manage the blog categories',
'new_category' => 'New Category',
'uncategorized' => 'Uncategorized'
],
'category' => [
'name' => 'Name',
'name_placeholder' => 'New category name',
'description' => 'Description',
'slug' => 'Slug',
'slug_placeholder' => 'new-category-slug',
'posts' => 'Posts',
'delete_confirm' => 'Delete this category?',
'delete_success' => 'Successfully deleted those categories.',
'return_to_categories' => 'Return to the blog category list',
'reorder' => 'Reorder Categories'
],
'menuitem' => [
'blog_category' => 'Blog category',
'all_blog_categories' => 'All blog categories',
'blog_post' => 'Blog post',
'all_blog_posts' => 'All blog posts',
'category_blog_posts' => 'Blog category posts'
],
'settings' => [
'category_title' => 'Category List',
'category_description' => 'Displays a list of blog categories on the page.',
'category_slug' => 'Category slug',
'category_slug_description' => "Look up the blog category using the supplied slug value. This property is used by the default component partial for marking the currently active category.",
'category_display_empty' => 'Display empty categories',
'category_display_empty_description' => 'Show categories that do not have any posts.',
'category_page' => 'Category page',
'category_page_description' => 'Name of the category page file for the category links. This property is used by the default component partial.',
'post_title' => 'Post',
'post_description' => 'Displays a blog post on the page.',
'post_slug' => 'Post slug',
'post_slug_description' => "Look up the blog post using the supplied slug value.",
'post_category' => 'Category page',
'post_category_description' => 'Name of the category page file for the category links. This property is used by the default component partial.',
'posts_title' => 'Post List',
'posts_description' => 'Displays a list of latest blog posts on the page.',
'posts_pagination' => 'Page number',
'posts_pagination_description' => 'This value is used to determine what page the user is on.',
'posts_filter' => 'Category filter',
'posts_filter_description' => 'Enter a category slug or URL parameter to filter the posts by. Leave empty to show all posts.',
'posts_per_page' => 'Posts per page',
'posts_per_page_validation' => 'Invalid format of the posts per page value',
'posts_no_posts' => 'No posts message',
'posts_no_posts_description' => 'Message to display in the blog post list in case if there are no posts. This property is used by the default component partial.',
'posts_no_posts_default' => 'No posts found',
'posts_order' => 'Post order',
'posts_order_description' => 'Attribute on which the posts should be ordered',
'posts_category' => 'Category page',
'posts_category_description' => 'Name of the category page file for the "Posted into" category links. This property is used by the default component partial.',
'posts_post' => 'Post page',
'posts_post_description' => 'Name of the blog post page file for the "Learn more" links. This property is used by the default component partial.',
'posts_except_post' => 'Except post',
'posts_except_post_description' => 'Enter ID/URL or variable with post ID/URL you want to exclude. You may use a comma-separated list to specify multiple posts.',
'posts_except_post_validation' => 'Post exceptions must be a single slug or ID, or a comma-separated list of slugs and IDs',
'posts_except_categories' => 'Except categories',
'posts_except_categories_description' => 'Enter a comma-separated list of category slugs or variable with such a list of categories you want to exclude',
'posts_except_categories_validation' => 'Category exceptions must be a single category slug, or a comma-separated list of slugs',
'rssfeed_blog' => 'Blog page',
'rssfeed_blog_description' => 'Name of the main blog page file for generating links. This property is used by the default component partial.',
'rssfeed_title' => 'RSS Feed',
'rssfeed_description' => 'Generates an RSS feed containing posts from the blog.',
'group_links' => 'Links',
'group_exceptions' => 'Exceptions'
],
'sorting' => [
'title_asc' => 'Title (ascending)',
'title_desc' => 'Title (descending)',
'created_asc' => 'Created (ascending)',
'created_desc' => 'Created (descending)',
'updated_asc' => 'Updated (ascending)',
'updated_desc' => 'Updated (descending)',
'published_asc' => 'Published (ascending)',
'published_desc' => 'Published (descending)',
'random' => 'Random'
],
'import' => [
'update_existing_label' => 'Update existing posts',
'update_existing_comment' => 'Check this box to update posts that have exactly the same ID, title or slug.',
'auto_create_categories_label' => 'Create categories specified in the import file',
'auto_create_categories_comment' => 'You should match the Categories column to use this feature, otherwise select the default categories to use from the items below.',
'categories_label' => 'Categories',
'categories_comment' => 'Select the categories that imported posts will belong to (optional).',
'default_author_label' => 'Default post author (optional)',
'default_author_comment' => 'The import will try to use an existing author if you match the Author Email column, otherwise the author specified above is used.',
'default_author_placeholder' => '-- select author --'
]
];

View File

@ -0,0 +1,166 @@
<?php
return [
'plugin' => [
'name' => 'Blog',
'description' => 'Una plataforma robusta de blogging.'
],
'blog' => [
'menu_label' => 'Blog',
'menu_description' => 'Administrar Publicaciones',
'posts' => 'Publicaciones',
'create_post' => 'Crear publicación',
'categories' => 'Categorías',
'create_category' => 'Categoría',
'tab' => 'Blog',
'access_posts' => 'Administrar las publicaciones',
'access_categories' => 'Administrar las categorías',
'access_other_posts' => 'Administrar publicaciones de otros usuarios',
'access_import_export' => 'Autorizado para importar y exportar publicaciones',
'access_publish' => 'Autorizado para publicar publicaciones',
'manage_settings' => 'Administrar configuración del blog',
'delete_confirm' => '¿Está seguro?',
'chart_published' => 'Publicado',
'chart_drafts' => 'Borradores',
'chart_total' => 'Total',
'settings_description' => 'Administrar configuración del blog',
'show_all_posts_label' => 'Mostrar todas las publicaciones a los usuarios de backend',
'show_all_posts_comment' => 'Mostrar las publicaciones publicados y los borradores a los usuarios de backend',
'tab_general' => 'General'
],
'posts' => [
'list_title' => 'Administrar publicaciones',
'filter_category' => 'Categoría',
'filter_published' => 'Publicado',
'filter_date' => 'Fecha',
'new_post' => 'Nueva publicación',
'export_post' => 'Exportar publicaciones',
'import_post' => 'Importar publicaciones'
],
'post' => [
'title' => 'Título',
'title_placeholder' => 'Título de la publicación',
'content' => 'Contenido',
'content_html' => 'Contenido HTML',
'slug' => 'Identificador',
'slug_placeholder' => 'nueva-publicacion',
'categories' => 'Categorías',
'author_email' => 'Email del Autor',
'created' => 'Creado',
'created_date' => 'Fecha de Creación',
'updated' => 'Actualizado',
'updated_date' => 'Fecha de Actualización',
'published' => 'Publicado',
'published_by' => 'Publicado por',
'current_user' => 'Usuario actual',
'published_date' => 'Fecha de publicación',
'published_validation' => 'Por favor, especifique la fecha de publicación',
'tab_edit' => 'Editar',
'tab_categories' => 'Categorías',
'categories_comment' => 'Seleccione las categorías para la publicación',
'categories_placeholder' => 'No hay categorías, ¡crea una primero!',
'tab_manage' => 'Administrar',
'published_on' => 'Publicado el',
'excerpt' => 'Resumen',
'summary' => 'Resumen',
'featured_images' => 'Imágenes Destacadas',
'delete_confirm' => '¿Borrar la publicación?',
'delete_success' => 'Publicación borrada correctamente',
'close_confirm' => 'La publicación no está guardada.',
'return_to_posts' => 'Volver a la lista de publicaciones',
'posted_byline' => 'Publicado en :categories el :date.',
'posted_byline_no_categories' => 'Publicado el :date.',
'date_format' => 'd de M de Y',
],
'categories' => [
'list_title' => 'Administrar las categorías',
'new_category' => 'Nueva categoría',
'uncategorized' => 'Sin Categoría'
],
'category' => [
'name' => 'Nombre',
'name_placeholder' => 'Nombre de la categoría',
'description' => 'Descripción',
'slug' => 'Identificador',
'slug_placeholder' => 'nueva-categoría',
'posts' => 'Publicaciones',
'delete_confirm' => '¿Borrar esta categoría?',
'delete_success' => 'Categorías borradas correctamente.',
'return_to_categories' => 'Volver a la lista de categorías',
'reorder' => 'Re-ordenar Categorías'
],
'menuitem' => [
'blog_category' => 'Categoría del blog',
'all_blog_categories' => 'Todas las categorías del blog',
'blog_post' => 'Publicación del blog',
'all_blog_posts' => 'Todas las publicaciones del blog',
'category_blog_posts' => 'Publicaciones del blog por categorías'
],
'settings' => [
'category_title' => 'Lista de Categorías',
'category_description' => 'Muestra en la página una lista de las categorías.',
'category_slug' => 'Identificador de la categoría',
'category_slug_description' => "Localiza una categoría utilizando el identificador proporcionado. Esta propiedad es utilizada dentro del parcial que viene por defecto en el componente para marcar la categoría activa.",
'category_display_empty' => 'Mostrar categorías vacías',
'category_display_empty_description' => 'Mostrar categorías que no tienen ninguna publicación.',
'category_page' => 'Página de categorías',
'category_page_description' => 'Nombre del archivo de página utilizado para los enlaces de categorías. Esta propiedad es utilizada dentro del parcial que viene por defecto en el componente.',
'post_title' => 'Publicación',
'post_description' => 'Muestra una publicación en la página.',
'post_slug' => 'Identificador de la publicación',
'post_slug_description' => "Se buscará la publicación utilizando el valor del identificador proporcionado.",
'post_category' => 'Página de categoría',
'post_category_description' => 'Nombre del archivo de página utilizado para los enlaces de categorías. Esta propiedad es utilizada dentro del parcial que viene por defecto en el componente.',
'posts_title' => 'Lista de publicaciones',
'posts_description' => 'Muestra una lista de las últimas publicaciones en la página.',
'posts_pagination' => 'Número de página',
'posts_pagination_description' => 'Este valor se utiliza para determinar en que página se encuentra el usuario.',
'posts_filter' => 'Filtro de categoría',
'posts_filter_description' => 'Ingrese un identificador de categoría o parámetro URL. Se utilizará para filtrar las publicaciones. Deje el campo vacío para mostrar todas las publicaciones.',
'posts_per_page' => 'Publicaciones por página',
'posts_per_page_validation' => 'Formato inválido para el valor de publicaciones por página',
'posts_no_posts' => 'Mensaje cuando no hay publicaciones',
'posts_no_posts_description' => 'Mensaje que se mostrará en la lista de publicaciones del blog cuando no haya ningúno. Esta propiedad es utilizada dentro del parcial que viene por defecto en el componente.',
'posts_no_posts_default' => 'No se encontraron publicaciones.',
'posts_order' => 'Ordenar publicaciones por',
'posts_order_description' => 'Atributo mediante el cual se deberán ordenar las publicaciones',
'posts_category' => 'Página de Categoría',
'posts_category_description' => 'Nombre del archivo de página utilizado para los enlaces de categoría "Publicado en". Esta propiedad es utilizada dentro del parcial que viene por defecto en el componente.',
'posts_post' => 'Página de las publicaciones',
'posts_post_description' => 'Nombre del archivo de página utilizado para los enlaces "Saber más". Esta propiedad es utilizada dentro del parcial que viene por defecto en el componente.',
'posts_except_post' => 'Exceptuar publicación',
'posts_except_post_description' => 'Ingrese una ID/URL o variable que contenga una ID/URL de la publicación que se quiera excluir',
'posts_except_post_validation' => 'La publicación a excluir debe ser una ID/URL, o una lista separada por comas de IDs/URLs',
'posts_except_categories' => 'Excluir categorías',
'posts_except_categories_description' => 'Introduce una lista separada por comas de IDs/URLs de categorías con las categorías a excluir.',
'posts_except_categories_validation' => 'Las categorías excluidas deben ser una URL de categoría o una lista separada por comas',
'rssfeed_blog' => 'Página del blog',
'rssfeed_blog_description' => 'Nombre del archivo de página principal para generación de enlaces. Esta propiedad es utilizada dentro del parcial que viene por defecto en el componente.',
'rssfeed_title' => 'RSS Feed',
'rssfeed_description' => 'Genera un feed de RSS con las publicaciones del blog.',
'group_links' => 'Enlaces',
'group_exceptions' => 'Excepciones'
],
'sorting' => [
'title_asc' => 'Título (ascendiente)',
'title_desc' => 'Título (descendiente)',
'created_asc' => 'Creado (ascendiente)',
'created_desc' => 'Creado (descendiente)',
'updated_asc' => 'Editado (ascendiente)',
'updated_desc' => 'Editado (descendiente)',
'published_asc' => 'Publicado (ascendiente)',
'published_desc' => 'Publicado (descendiente)',
'random' => 'Aleatorio'
],
'import' => [
'update_existing_label' => 'Editar publicaciones existentes',
'update_existing_comment' => 'Selecciona este check para actualizar las publicaciones con exactamente la misma ID, título o URL.',
'auto_create_categories_label' => 'Crear categorías especificadas en el archivo a importar',
'auto_create_categories_comment' => 'Debes hacer coincidir la columna Categoría para usar esta funcionalidad, sino selecciona la categoría por defecto para para usar para los elementos de abajo.',
'categories_label' => 'Categorías',
'categories_comment' => 'Selecciona las categorías a las que pertenecerán las publicaciones importadas (opcional).',
'default_author_label' => 'Autor de publicación por defecto (opcional)',
'default_author_comment' => 'La importación intentará usar un autor existente si coicide con la columna "Author Email", sino se usará el autor especificado arriba.',
'default_author_placeholder' => '-- Selecciona Autor/a --'
]
];

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