Installation
GDPR Compliance
General CMS architecture
Commands
Features info
- List views
- Form fields
- Images
- Check if the resource has images
- Get uploaded images
- Get image model
- Get image relationship
- Get minimal dimensions for an image
- Get retina factor
- Get required form images
- Get image fields and meta fields
- Get upload directory
- Creating a thumbnail
- Set image model
- Set minimal dimensions
- Set retina factor
- Resetting passwords
- Localization
- Change CMS logo and favicon
- Brute force protection
Installation
Support
Currently igniCMS support Laravel v5.4 and v5.5
Prerequisites
- nodejs >= 4.0
- yarn or npm
- bower
- gulp
- composer
Installation
-
Run
composer require despark/igni-core
. -
Add igniCMS service providers before the application service providers in the
config/app.php
, as shown below (Optional for Laravel 5.5)
Example
...
/*
* igniCMS Service Providers
*/
Despark\Cms\Providers\AdminServiceProvider::class,
Despark\Cms\Providers\IgniServiceProvider::class,
Despark\Cms\Providers\EntityServiceProvider::class,
Despark\Cms\Providers\JavascriptServiceProvider::class,
/*
* Package Service Providers...
*/
Laravel\Tinker\TinkerServiceProvider::class,
...
- Config your database settings in your
.env
file.
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=mydbw
DB_USERNAME=user
DB_PASSWORD=password
- Run this command in the terminal (it’ll set all necessary resources to use the CMS. To complete this step you should have composer, npm & bower, installed globally):
php artisan igni:install
- Config your
config/auth.php
file to use Igni’s User model
Example
...
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
...
- All done! Now go to the
<your_site_url>/admin
and use your credentials
GDPR Compliance
Restrict Processing
Case covered:
As per GDPR’s Article 18 users must be provided with the ability to restrict their data from processing by the staff of the service. Below you can find how to implement this functionality in your IgniCMS powered web app.
Soluton:
igniCMS has a global scope called NotRestricted, which returns users that are not flagged as restricted(is_restricted = 0). The logged in users can request a restriction by submitting a POST request to /user/restrict
. The igniCMS admin can do the same thing by clicking on the Restrict processing button at /admin/user
.
Only the user or the DBA can restore access to the user’s data. The logged in users can request a restriction removal by submitting a POST request to /user/free
.
Example form for requesting a restriction
<form action="" method="POST">
<button type="submit">Restrict my profile</button>
</form>
Example form for requesting a restriction removal
<form action="" method="POST">
<button type="submit">Remove restriction to my profile</button>
</form>
Export user’s data
Cases covered:
As per GDPR’s Article 20 users must be provided with the ability to manually export or request an export of all the data which is stored about them on the server.
Solution:
The logged in users can request a data export by submitting a POST request to /user/export
. The igniCMS admin can do the same thing by clicking on the Full data export button at /admin/user
. If confirmed, a background process which forms a JSON of all the data of the user, saves it on the server as with a hashed name and sends an email with a link to it to the user. There is a command which gets all exported files older than 48h and deletes them. You can check how to set up a cron job at Laravel’s docs
If you want the background process to be in a queue, please check Laravel’s docs for setup. After that in the ignicms
config file specify your queue name:
'user_export_queue' => env('IGNI_USER_EXPORT_QUEUE', 'user-export')
Here is how the export function works
/**
* @param null $id
* @return \Illuminate\Http\RedirectResponse
*/
public function export($id = null)
{
$relationships = $this->model->relationships();
if ($id) {
$this->model = $this->model->findOrFail($id);
} else {
$this->model = auth()->user();
}
$this->model->load($relationships);
dispatch((new UserRequestedExport($this->model))->onQueue(config('ignicms.user_export_queue')));
if (request()->expectsJson()) {
return response(['success' => 'success'], 200);
}
$this->notify([
'type' => 'info',
'title' => 'Successful export!',
'description' => 'The user\'s data is exported and sent successfully.',
]);
return redirect()->back();
}
class CleanUserExports extends Command
{
/**
* The console command signature.
*
* @var string
*/
protected $signature = 'igni:user:exports:clean';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Delete user exports older than 48 hours.';
/**
* Create a new command instance.
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$storageDisk = Storage::disk('public');
$storagePrefix = $storageDisk->getDriver()->getAdapter()->getPathPrefix();
$files = $storageDisk->files('user-exports');
$filesToDelete = [];
$maxLifetimeInMinutes = 2880;
foreach ($files as $file) {
if (file_exists($storagePrefix . $file)) {
$minutesPassedSinceCreated = date('i', filectime($storagePrefix . $file));
if ($minutesPassedSinceCreated >= $maxLifetimeInMinutes) {
$filesToDelete[] = $file;
}
}
}
$count = count($filesToDelete);
$bar = $this->output->createProgressBar($count);
foreach ($filesToDelete as $file) {
$storageDisk->delete($file);
$bar->advance();
}
$bar->finish();
$this->info(PHP_EOL . 'User exports were deleted successfully');
}
}
Example form for requesting a user data export
<form action="" method="POST">
<button type="submit">Export data</button>
</form>
Users’ right to be FORGOTTEN
The igniCMS team finds the solution by adding CASCADE options to the foreign keys. If you don’t like this approach you can modify the Users’ delete method as you prefer. Example:
/**
* Remove the specified resource from storage.
*
* @param int $id
*
* @return Response
*/
public function destroy($id)
{
$user = $this->model->findOrFail($id);
// Here goes your removal logic for the users' relationships
// Example: Given we have a user that writes an article and we want to remove the user but keep the article as with not author(user_id = NULL)
$user->articles->update(['user_id' => null]);
// Or you want to remove the articles
$user->articles->delete();
// Then we delete the user himself
$user->delete();
$this->notify([
'type' => 'danger',
'title' => 'Successful deleted user!',
'description' => 'The user is deleted successfully.',
]);
return redirect()->back();
}
General CMS acrchitecture
Models
The models which are generated for resources using the igni:
commands will be stored in the app/Models
directory. Those models follow the standard Laravel convention, so you must input the well know Laravel protected arrays, e.g. $fillable
and $rules
and also the relationships with other models. We advise you to store all the models in the app/Models
directory (even the ones which are not generated using igni:
commands) in order to keep consistency within your project architecture.
Controllers
Those generated using igni:
commands will be stored in the app/Http/Controller/Admin
directory. When generated the Igni CMS controllers are blank classes extending Igni’s AdminController
.
Migrations
They will be stored in the default location - database/migrations
. If you are creating an empty resource you must set the table name, columns and relationships on your own.
Entities
What are entities?
The resource entity is the main config file for its functionality. In it you can set which model/controller to use, desired actions, fields, columns and much more. The entity files are stored in config/entities
.
How to use them?
Here’s an example entity config which defines a user management resource.
return [
'name' => 'User',
'description' => 'User resource',
'model' => config('auth.providers.users.model'),
'controller' => \Despark\Cms\Http\Controllers\UsersController::class,
'adminColumns' => [
'name',
'email',
'Admin?' => 'is_admin',
],
'actions' => ['edit', 'create', 'destroy'],
'adminFormFields' => [
'name' => [
'type' => 'text',
'label' => 'Name',
],
'email' => [
'type' => 'text',
'label' => 'Email',
],
'is_admin' => [
'type' => 'checkbox',
'label' => 'is_admin',
],
'password' => [
'type' => 'password',
'label' => 'Password',
],
],
'adminMenu' => [
'user_management' => [
'name' => 'User Management',
'iconClass' => 'fa-users',
],
'users' => [
'name' => 'Users',
'link' => 'user.index',
'parent' => 'user_management',
],
],
];
name
You can set your page title, browser tab title and add button name in that item. (E.g. If you call it User, the button for creating a new item will be called “Add User”).
model and controller
Define the used controller and model for your entity here.
adminColumns
Array defining which columns to show in your table at the listing page. You can also use relationships here.
Example
You have a relationship between a user and a car and you want to show the user’s name and their car model in the users listing:
'adminColumns' => [
'name',
'car model' => 'car.model',
],
Keep in mind that in the listing all 0 and 1 values are casted to No/Yes.
actions
You can also limit the available actions for a resource. By default they are set to all:
'actions' => ['edit', 'create', 'destroy'],
adminFormFields
In that array you set all the fields to which the admins have access in the create/edit form. Don’t forget to also set the fields as fillable in the model. For full listing of available field type please go to Form fields.
image_fields
Here you set the settings for all of your image fields. Example
'image_fields' => [
'column_name' => [
'thumbnails' => [
'admin' => [
'width' => 150,
'height' => 150,
'type' => 'resize',
],
'normal' => [
'width' => 400,
'height' => 400,
'type' => 'resize',
],
],
],
'column_name_2' => [
'thumbnails' => [
'admin' => [
'width' => 75,
'height' => 75,
'type' => 'resize',
],
'normal' => [
'width' => 200,
'height' => 200,
'type' => 'crop',
],
],
],
],
The admin
array contains the settings for the thumbnail created for the CMS, which is shown when editing the resource. The available types of image manipulations are resize
, crop
and fit
. In the normal
array you should define the standard size of the images. If retina_factor
is enabled in config/ignicms.php
then you must upload images with size at least double the size that is entered in normal
(E.g. for column_name
in the example above you must upload an image with minimum 800x800px).
adminMenu
Here is the place where you set the menu items shown in the sidebar of the CMS. You can create normal or nested sidebar menu items. In order to create a nested sidebar list you need to use the parent
key and state the name of the item you want to assign as parent. The weight
column is not required. It is used to sort your items in a way that you prefer. In the example below, the User management item will be always on top of the sidebar items and it will have the following nested items: Users and Roles in that particular order.
Example
Nested sidebar list
config/entities/users.php
'adminMenu' => [
'user_management' => [
'name' => 'User Management',
'iconClass' => 'fa-users',
'weight' => 1,
],
'users' => [
'name' => 'User',
'link' => 'user.index',
'parent' => 'user_management',
'weight' => 2,
],
],
config/entities/roles.php
'adminMenu' => [
'roles' => [
'name' => 'Role',
'link' => 'role.index',
'parent' => 'user_management',
'weight' => 3,
],
],
Example
Normal sidebar list
config/entities/users.php
'adminMenu' => [
'users' => [
'name' => 'User',
'link' => 'user.index',
'iconClass' => 'fa-users',
'weight' => 1,
],
],
Commands
Run install
Install a fresh copy of igniCMS. For more information refer to the Installation section Example
php artisan igni:install
Create new resource
Use the command php artisan igni:make:resource
to create all necessary files for manipulating resources. You should specify the resource name (in title case).
Example
php artisan igni:make:resource "Blog Post"
Create Pages module
Use the command php artisan igni:make:pages
to create all necessary files for manipulating Pages.
Example
php artisan igni:make:pages
Create Contacts module
If you want a command for creating a Contacts page resource, you should add our contacts module for IgniCMS. You can find full information about it here.
Image rebuilding
You can rebuild your uploaded images php artisan igni:images:rebuild
. If you want you can specify which resources to rebuild with the --resources=*
switch.
Example
php artisan igni:image:rebuild --resources App\\Test
You can exclude some resources with --without=*
.
Features info
List views
Defining the listing columns
The columns of an entity’s listing are degined in the adminColumns
key in the entity config. This is an array defining which columns to show in your table at the listing page. You can also use relationships here.
Example
You have a relationship between a user and a car and you want to show the user’s name and their car model in the users listing:
'adminColumns' => [
'name',
'car model' => 'car.model',
],
Keep in mind that in the listing all 0 and 1 values are casted to No/Yes.
Actions
You can also limit the available actions for a resource. By default they are set to all:
'actions' => ['edit', 'create', 'destroy'],
Sorting
By default the listing can be sorted by all columns different than the actions column. You can however define the default sorting which is applied up on table load in the adminColumnsSort
key. You can also define a sort by multiple columns as shown in the example. The key is the index of the column (starting from 0) and the value is the sorting type.
'adminColumnsSort' => [4 => 'desc', 2 => 'asc'],
Form fields
Checkbox
'column_name' => [
'type' => 'checkbox',
'label' => 'I am a checkbox',
],
Date picker
'column_name' => [
'type' => 'date',
'label' => 'I am a date picker',
],
Datetime picker
'column_name' => [
'type' => 'datetime',
'label' => 'I am a datetime picker',
],
Hidden
'column_name' => [
'type' => 'hidden',
],
Image single
Keep in mind that the images relationship is polymorphic, so you don’t have to make a column in your table. You just need to replace column_name
with your desired one.
'column_name' => [
'type' => 'imageSingle',
'label' => 'I am a image single upload'
'previewLink' => true, //Defines whether the CMS user should be able to see the original image file when clicking on the thumbnail preview in the form
],
Password
'column_name' => [
'type' => 'password',
'label' => 'I am a password',
],
Select
In order to use the select field, you need to build up a Source model class. Here is an example Roles class:
use Despark\Cms\Contracts\SourceModel;
use App\Models\Role;
/**
* Class Roles.
*/
class Roles implements SourceModel
{
/**
* @var array
*/
protected $options;
/**
* @return array
*/
public function toOptionsArray()
{
if (! isset($this->options)) {
$this->options = Role::orderBy('name')->pluck('name', 'id')->toArray();
}
return $this->options;
}
}
And here is how to define the field in the entity:
'column_name' => [
'type' => 'select',
'label' => 'I am a select',
'sourceModel' => \App\Sources\Roles::class,
],
Text
'column_name' => [
'type' => 'text',
'label' => 'I am a text',
],
Textarea
'column_name' => [
'type' => 'textarea',
'label' => 'I am a textarea',
],
Wysiwyg
'column_name' => [
'type' => 'wysiwyg',
'label' => 'I am a wysiwyg',
],
Many to many select
This field type can be used for implementing tagging functionality. First, we need to make the many to many relationship between the Models. For this example we are using a User and Permission class.
use Despark\Cms\Models\AdminModel;
use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Illuminate\Foundation\Auth\Access\Authorizable;
use Illuminate\Notifications\Notifiable;
class User extends AdminModel implements UserContract, CanResetPasswordContract
{
use Notifiable;
use Authenticatable, Authorizable, CanResetPassword;
...
protected $rules = [
'name' => 'required',
'email' => 'required|email|unique:users,email',
'password' => 'required|min:6|max:20',
'permissions' => 'required',
];
...
// Here we define the method_name => request_column_name
public function getManyToManyFields()
{
return [
'permissions' => 'permissions',
];
}
// Here we define a relationship to the Permission class
public function permissions()
{
return $this->belongsToMany(Permission::class);
}
use Despark\Cms\Models\AdminModel;
class Permission extends AdminModel
{
protected $table = 'permissions';
protected $fillable = ['name'];
protected $rules = ['name' => 'required|max:50'];
// Here we define a relationship to the User class
public function users()
{
return $this->belongsToMany(User::class);
}
protected $identifier = 'permission';
}
Next we need to get the data for the Select. For example in the app/Sources
directory, we can create Permissions
class which gets the needed data:
use App\Models\Permission;
use Despark\Cms\Contracts\SourceModel;
/**
* Class Permissions.
*/
class Permissions implements SourceModel
{
/**
* @var array
*/
protected $options;
/**
* @return array
*/
public function toOptionsArray()
{
if (! isset($this->options)) {
$this->options = Permission::orderBy('name')
->pluck('name', 'id')
->toArray();
}
return $this->options;
}
}
Finally, we need to setup the field in the entity:
'column_name[]' => [ // In this case permissions[]
'type' => 'manyToManySelect',
'label' => 'Permissions',
'additionalClass' => 'select2-tags', // This is not required. Use it if you need some extra classes
'sourceModel' => \App\Sources\Permissions::class,
'relationMethod' => 'permissions', // The name of the relation
'validateName' => 'permissions', // Which name to validate in the protected $rules array
'selectedKey' => 'id', // Which key will be the value for the select
],
It also works with polymorphic relationships. You can find more info about this type of relationships in the Laravel docs
Custom
If you need to do a more customized functionality than the ones which are provided for you out of the box you can use a custom field implementation. To accomplish this you must create a Handler, Template and Factory, the place of these files depends on you. In the example below we’ve implemented a color picker and stored the files for it in app/Fields
:
use Despark\Cms\Contracts\FieldContract;
use Despark\Cms\Fields\Custom;
/**
* Class Color.
*/
class Color extends Custom implements FieldContract
{
protected $model;
protected $fieldName;
protected $value;
protected $options;
/**
* Color constructor.
*
* @param Custom $parent
*/
public function __construct($fieldName, array $options, $value = null, $model = null)
{
$this->model = $model;
$this->fieldName = $fieldName;
$this->value = $value;
$this->options = $options;
if (! isset($options['template'])) {
throw new \Exception('Template is required for field '.$fieldName);
}
if (isset($options['template']) && \View::exists($options['template'])) {
$this->template = $options['template'];
}
}
public function getModel()
{
return $this->model;
}
}
Our factory class will be stored in app\Factories
:
use Despark\Cms\Fields\Contracts\Factory;
use App\Fields\Color;
class ColorFactory implements Factory
{
public function make(array $data)
{
extract($data);
$field = new Color($field, $options, $value, $model);
return $field;
}
}
Let’s create a template in app/resources/views/admin
called color.blade.php
:
<div class="form-group ">
{!! Form::label($field->getFieldName(), $field->getOptions('label')) !!}
<div class="input-group my-colorpicker2">
{!! Form::text($field->getFieldName(), $field->getModel()->active_color, [
'id' => $field->getFieldName(),
'class' => "form-control",
'placeholder' => $field->getOptions('label'),
] ) !!}
<div class="input-group-addon">
<i></i>
</div>
</div>
<div class="text-red">
</div>
</div>
@push('additionalScripts')
<script type="text/javascript">
$(".my-colorpicker2").colorpicker({
format: 'hex'
});
</script>
@endpush
Finally, let’s define the field in the entity:
'column_name' => [
'type' => 'custom',
'handler' => \App\Fields\Color::class,
'template' => 'admin.color',
'factory' => \App\Factories\ColorFactory::class,
'label' => 'I am a custom field',
],
Images
Images
Check if the resource has images
$model->hasImages($type = null);
Get uploaded images
Here is how you can get your image for a specific resource:
$image = $model->getImages();
You can pass the id of the field given in the resource entity file as an argument. In that case the function will return the image associated with the given id.
$image = $model->getImages($fieldName);
You can also use
$image = $model->getImagesOfType($fieldName);
To display the image in your view you can use the following function:
{!! $image->toHtml('normal') !!}
where normal
is the image thumbnail provided in the resource entity file.
Example resource entity file:
'image_fields' => [
'image' => [
'thumbnails' => [
'admin' => [
'width' => 150,
'height' => null,
'type' => 'resize',
],
'normal' => [
'width' => 600,
'height' => 368,
'type' => 'resize',
],
],
],
],
Get image model
$model->getImageModel();
Get image relationship
$model->images();
Get minimal dimensions for an image
$model->getMinDimensions($fieldName);
By default, the data is returned as an array. If you want it as string, you can set a second parameter as true. Also you can get only the minimal width or height:
$model->getMinWidth($fieldName);
$model->getMinHeight($fieldName);
Get retina factor
$model->getRetinaFactor();
Get required form images
$model->getRequiredImages();
Get image fields and meta fields
$model->getImageFields();
$model->getImageMetaFields($fieldName);
$model->getImageMetaFieldsHtml($fieldName);
Get upload directory
$model->getCurrentUploadDir();
Creating a thumbnail
$model->createThumbnail($sourceImagePath, $thumbName, $newFileName, $width = null, $height = null, $resizeType = 'crop', $color = null
);
Set image model
$model->setImageModel($imageModel);
Set minimal dimensions
$model->setMinDimensions($field, $minDimensions);
Set retina factor
$model->setRetinaFactor($factor);
Resetting passwords
In order to use the reset password functionality, you must fill in the MAIL and APP settings in your .env
file or modify the defaults in config/app.php
and config/mail.php
.
...
APP_NAME=IgniCMS
APP_URL=http://my-site-url.com
...
MAIL_DRIVER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_FROM_ADDRESS=no-reply@ignicms.com
MAIL_FROM_NAME=IgniCMS
After modifying the APP_URL, the URLs in the email sent to the user will be working as expecred!
If you want to change the standard email template, you can find out how in the Laravel docs.
Localization
IgniCMS provides internationalization out of the box through the i18n package. You can find full information about it here.
Change CMS logo and favicon
You can change the CMS logo in config/ignicms.php
.
...
// For best performance the image must be with width 234px
'logo' => 'images/logo.png',
...
and the favicon in the same file right below the logo configuration.
...
'favicon' => 'images/favicon.ico',
...
Brute force protection
If someone unsuccessfully tries to login with a certain email 5 times in a row, this account will be blocked for 15 minutes.