Introduce theme logging + log settings
CmsObject changes can now be tracked (disabled by default) Request logging is now disabled by default (security vector)
This commit is contained in:
parent
cc1a67373c
commit
462c9cd4e8
|
|
@ -188,25 +188,7 @@ class NavigationManager
|
|||
$this->items = [];
|
||||
}
|
||||
|
||||
foreach ($definitions as $code => $definition) {
|
||||
$item = (object) array_merge(self::$mainItemDefaults, array_merge($definition, [
|
||||
'code' => $code,
|
||||
'owner' => $owner
|
||||
]));
|
||||
|
||||
foreach ($item->sideMenu as $sideMenuItemCode => $sideMenuDefinition) {
|
||||
$item->sideMenu[$sideMenuItemCode] = (object) array_merge(
|
||||
self::$sideItemDefaults,
|
||||
array_merge($sideMenuDefinition, [
|
||||
'code' => $sideMenuItemCode,
|
||||
'owner' => $owner
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
$itemKey = $this->makeItemKey($owner, $code);
|
||||
$this->items[$itemKey] = $item;
|
||||
}
|
||||
$this->addMainMenuItems($owner, $definitions);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -229,9 +211,8 @@ class NavigationManager
|
|||
*/
|
||||
public function addMainMenuItem($owner, $code, array $definition)
|
||||
{
|
||||
$sideMenu = isset($definition['sideMenu']) ? $definition['sideMenu'] : null;
|
||||
|
||||
$itemKey = $this->makeItemKey($owner, $code);
|
||||
|
||||
if (isset($this->items[$itemKey])) {
|
||||
$definition = array_merge((array) $this->items[$itemKey], $definition);
|
||||
}
|
||||
|
|
@ -243,8 +224,8 @@ class NavigationManager
|
|||
|
||||
$this->items[$itemKey] = $item;
|
||||
|
||||
if ($sideMenu !== null) {
|
||||
$this->addSideMenuItems($owner, $code, $sideMenu);
|
||||
if ($item->sideMenu) {
|
||||
$this->addSideMenuItems($owner, $code, $item->sideMenu);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -280,21 +261,24 @@ class NavigationManager
|
|||
public function addSideMenuItem($owner, $code, $sideCode, array $definition)
|
||||
{
|
||||
$itemKey = $this->makeItemKey($owner, $code);
|
||||
|
||||
if (!isset($this->items[$itemKey])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$mainItem = $this->items[$itemKey];
|
||||
|
||||
$definition = array_merge($definition, [
|
||||
'code' => $sideCode,
|
||||
'owner' => $owner
|
||||
]);
|
||||
|
||||
$mainItem = $this->items[$itemKey];
|
||||
if (isset($mainItem->sideMenu[$sideCode])) {
|
||||
$definition = array_merge((array) $mainItem->sideMenu[$sideCode], $definition);
|
||||
}
|
||||
|
||||
$item = (object) array_merge(self::$sideItemDefaults, $definition);
|
||||
|
||||
$this->items[$itemKey]->sideMenu[$sideCode] = $item;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ use System\Classes\SettingsManager;
|
|||
use System\Classes\CombineAssets;
|
||||
use Cms\Classes\ComponentManager;
|
||||
use Cms\Classes\Page as CmsPage;
|
||||
use Cms\Classes\CmsObject;
|
||||
use Cms\Models\ThemeData;
|
||||
use Cms\Models\ThemeLog;
|
||||
|
||||
class ServiceProvider extends ModuleServiceProvider
|
||||
{
|
||||
|
|
@ -26,6 +28,7 @@ class ServiceProvider extends ModuleServiceProvider
|
|||
parent::register('cms');
|
||||
|
||||
$this->registerComponents();
|
||||
$this->registerThemeLogging();
|
||||
$this->registerAssetBundles();
|
||||
$this->registerCombinerEvents();
|
||||
|
||||
|
|
@ -55,7 +58,7 @@ class ServiceProvider extends ModuleServiceProvider
|
|||
}
|
||||
|
||||
/**
|
||||
* Register components
|
||||
* Register components.
|
||||
*/
|
||||
protected function registerComponents()
|
||||
{
|
||||
|
|
@ -65,7 +68,17 @@ class ServiceProvider extends ModuleServiceProvider
|
|||
}
|
||||
|
||||
/**
|
||||
* Register asset bundles
|
||||
* Registers theme logging on templates.
|
||||
*/
|
||||
protected function registerThemeLogging()
|
||||
{
|
||||
CmsObject::extend(function($model) {
|
||||
ThemeLog::bindEventsToModel($model);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register asset bundles.
|
||||
*/
|
||||
protected function registerAssetBundles()
|
||||
{
|
||||
|
|
@ -274,6 +287,16 @@ class ServiceProvider extends ModuleServiceProvider
|
|||
'permissions' => ['cms.manage_themes'],
|
||||
'order' => 300
|
||||
],
|
||||
'theme_logs' => [
|
||||
'label' => 'cms::lang.theme_log.menu_label',
|
||||
'description' => 'cms::lang.theme_log.menu_description',
|
||||
'category' => SettingsManager::CATEGORY_LOGS,
|
||||
'icon' => 'icon-magic',
|
||||
'url' => Backend::url('cms/themelogs'),
|
||||
'permissions' => ['system.access_logs'],
|
||||
'order' => 910,
|
||||
'keywords' => 'theme change log'
|
||||
]
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
del {
|
||||
text-decoration: none;
|
||||
color: #b30000;
|
||||
background: #fadad7;
|
||||
}
|
||||
ins {
|
||||
background: #eaf2c2;
|
||||
color: #406619;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* Template Diff plugin
|
||||
*
|
||||
* Data attributes:
|
||||
* - data-plugin="template-diff" - enables the plugin on an element
|
||||
*
|
||||
* JavaScript API:
|
||||
* $('pre').templateDiff({ option: 'value' })
|
||||
*
|
||||
* Dependences:
|
||||
* - jsdiff (diff.js)
|
||||
*/
|
||||
|
||||
+function ($) { "use strict";
|
||||
|
||||
// TEMPALTE DIFF CLASS DEFINITION
|
||||
// ============================
|
||||
|
||||
var TemplateDiff = function(element, options) {
|
||||
this.options = options
|
||||
this.$el = $(element)
|
||||
|
||||
// Init
|
||||
this.init()
|
||||
}
|
||||
|
||||
TemplateDiff.DEFAULTS = {
|
||||
oldFieldName: null,
|
||||
newFieldName: null,
|
||||
contentTag: '',
|
||||
diffType: 'lines' // chars, words, lines
|
||||
}
|
||||
|
||||
TemplateDiff.prototype.init = function() {
|
||||
var
|
||||
oldValue = $('[data-field-name="'+this.options.oldFieldName+'"] .form-control '+this.options.contentTag).html(),
|
||||
newValue = $('[data-field-name="'+this.options.newFieldName+'"] .form-control '+this.options.contentTag).html()
|
||||
|
||||
oldValue = $('<div />').html(oldValue).text()
|
||||
newValue = $('<div />').html(newValue).text()
|
||||
|
||||
this.diffStrings(oldValue, newValue)
|
||||
}
|
||||
|
||||
TemplateDiff.prototype.diffStrings = function(oldValue, newValue) {
|
||||
var result = this.$el.get(0)
|
||||
var diffType = 'diff' + this.options.diffType[0].toUpperCase() + this.options.diffType.slice(1)
|
||||
var diff = JsDiff[diffType](oldValue, newValue)
|
||||
var fragment = document.createDocumentFragment();
|
||||
for (var i=0; i < diff.length; i++) {
|
||||
|
||||
if (diff[i].added && diff[i + 1] && diff[i + 1].removed) {
|
||||
var swap = diff[i];
|
||||
diff[i] = diff[i + 1];
|
||||
diff[i + 1] = swap;
|
||||
}
|
||||
|
||||
var node;
|
||||
if (diff[i].removed) {
|
||||
node = document.createElement('del');
|
||||
node.appendChild(document.createTextNode(diff[i].value));
|
||||
}
|
||||
else if (diff[i].added) {
|
||||
node = document.createElement('ins');
|
||||
node.appendChild(document.createTextNode(diff[i].value));
|
||||
}
|
||||
else {
|
||||
node = document.createTextNode(diff[i].value);
|
||||
}
|
||||
fragment.appendChild(node);
|
||||
}
|
||||
|
||||
result.textContent = '';
|
||||
result.appendChild(fragment);
|
||||
}
|
||||
|
||||
// TEMPALTE DIFF PLUGIN DEFINITION
|
||||
// ============================
|
||||
|
||||
var old = $.fn.templateDiff
|
||||
|
||||
$.fn.templateDiff = function (option) {
|
||||
var args = Array.prototype.slice.call(arguments, 1), result
|
||||
this.each(function () {
|
||||
var $this = $(this)
|
||||
var data = $this.data('oc.example')
|
||||
var options = $.extend({}, TemplateDiff.DEFAULTS, $this.data(), typeof option == 'object' && option)
|
||||
if (!data) $this.data('oc.example', (data = new TemplateDiff(this, options)))
|
||||
if (typeof option == 'string') result = data[option].apply(data, args)
|
||||
if (typeof result != 'undefined') return false
|
||||
})
|
||||
|
||||
return result ? result : this
|
||||
}
|
||||
|
||||
$.fn.templateDiff.Constructor = TemplateDiff
|
||||
|
||||
// TEMPALTE DIFF NO CONFLICT
|
||||
// =================
|
||||
|
||||
$.fn.templateDiff.noConflict = function () {
|
||||
$.fn.templateDiff = old
|
||||
return this
|
||||
}
|
||||
|
||||
// TEMPALTE DIFF DATA-API
|
||||
// ===============
|
||||
|
||||
$(document).render(function () {
|
||||
$('[data-plugin="template-diff"]').templateDiff()
|
||||
});
|
||||
|
||||
}(window.jQuery);
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -488,5 +488,4 @@ class CmsCompoundObject extends CmsObject
|
|||
|
||||
return parent::__call($method, $parameters);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -319,5 +319,4 @@ class CmsObject extends HalcyonModel implements CmsObjectContract
|
|||
throw $ex;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
<?php namespace Cms\Controllers;
|
||||
|
||||
use Str;
|
||||
use Lang;
|
||||
use File;
|
||||
use Flash;
|
||||
use Backend;
|
||||
use Redirect;
|
||||
use BackendMenu;
|
||||
use Backend\Classes\Controller;
|
||||
use ApplicationException;
|
||||
use System\Classes\SettingsManager;
|
||||
use Cms\Models\ThemeLog;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Request Logs controller
|
||||
*
|
||||
* @package october\system
|
||||
* @author Alexey Bobkov, Samuel Georges
|
||||
*/
|
||||
class ThemeLogs extends Controller
|
||||
{
|
||||
public $implement = [
|
||||
'Backend.Behaviors.FormController',
|
||||
'Backend.Behaviors.ListController'
|
||||
];
|
||||
|
||||
public $requiredPermissions = ['system.access_logs'];
|
||||
|
||||
public $formConfig = 'config_form.yaml';
|
||||
|
||||
public $listConfig = 'config_list.yaml';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
BackendMenu::setContext('October.System', 'system', 'settings');
|
||||
SettingsManager::setContext('October.Cms', 'theme_logs');
|
||||
}
|
||||
|
||||
public function index_onRefresh()
|
||||
{
|
||||
return $this->listRefresh();
|
||||
}
|
||||
|
||||
public function index_onEmptyLog()
|
||||
{
|
||||
ThemeLog::truncate();
|
||||
Flash::success(Lang::get('cms::lang.theme_log.empty_success'));
|
||||
return $this->listRefresh();
|
||||
}
|
||||
|
||||
public function index_onDelete()
|
||||
{
|
||||
if (($checkedIds = post('checked')) && is_array($checkedIds) && count($checkedIds)) {
|
||||
|
||||
foreach ($checkedIds as $recordId) {
|
||||
if (!$record = ThemeLog::find($recordId)) continue;
|
||||
$record->delete();
|
||||
}
|
||||
|
||||
Flash::success(Lang::get('backend::lang.list.delete_selected_success'));
|
||||
}
|
||||
else {
|
||||
Flash::error(Lang::get('backend::lang.list.delete_selected_empty'));
|
||||
}
|
||||
|
||||
return $this->listRefresh();
|
||||
}
|
||||
|
||||
public function preview($id)
|
||||
{
|
||||
$this->addCss('/modules/cms/assets/css/themelogs/template-diff.css', 'core');
|
||||
$this->addJs('/modules/cms/assets/vendor/jsdiff/diff.js', 'core');
|
||||
$this->addJs('/modules/cms/assets/js/themelogs/template-diff.js', 'core');
|
||||
|
||||
return $this->asExtension('FormController')->preview($id);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<div class="form-control">
|
||||
<pre><?= e($value.PHP_EOL) ?></pre>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<div class="form-control">
|
||||
<pre
|
||||
data-plugin="template-diff"
|
||||
data-old-field-name="old_content"
|
||||
data-new-field-name="content"
|
||||
data-content-tag="pre"></pre>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<div
|
||||
class="form-control"
|
||||
data-plugin="template-diff"
|
||||
data-old-field-name="old_template"
|
||||
data-diff-type="words"
|
||||
data-new-field-name="template">
|
||||
</div>
|
||||
|
|
@ -0,0 +1 @@
|
|||
<div class="form-control"><?= e($value) ?></div>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
<p>
|
||||
<?= e(trans('cms::lang.theme_log.hint')) ?>
|
||||
</p>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php if ($formModel->type == $formModel::TYPE_DELETE): ?>
|
||||
<div class="callout fade in callout-danger no-subheader m-b">
|
||||
<div class="header">
|
||||
<i class="icon-minus"></i>
|
||||
<h3><?= e(trans('cms::lang.theme_log.template_deleted')) ?></h3>
|
||||
</div>
|
||||
</div>
|
||||
<?php elseif ($formModel->type == $formModel::TYPE_CREATE): ?>
|
||||
<div class="callout fade in callout-success no-subheader m-b">
|
||||
<div class="header">
|
||||
<i class="icon-plus"></i>
|
||||
<h3><?= e(trans('cms::lang.theme_log.template_created')) ?></h3>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="callout fade in callout-info no-subheader">
|
||||
<div class="header">
|
||||
<i class="icon-pencil"></i>
|
||||
<h3><?= e(trans('cms::lang.theme_log.template_updated')) ?></h3>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<div data-control="toolbar" class="loading-indicator-container">
|
||||
<a
|
||||
href="javascript:;"
|
||||
data-request="onRefresh"
|
||||
data-load-indicator="<?= e(trans('backend::lang.list.updating')) ?>"
|
||||
class="btn btn-primary oc-icon-refresh">
|
||||
<?= e(trans('backend::lang.list.refresh')) ?>
|
||||
</a>
|
||||
<a
|
||||
href="javascript:;"
|
||||
data-request="onEmptyLog"
|
||||
data-request-confirm="<?= e(trans('backend::lang.list.delete_selected_confirm')) ?>"
|
||||
data-load-indicator="<?= e(trans('cms::lang.theme_log.empty_loading')) ?>"
|
||||
class="btn btn-default oc-icon-eraser">
|
||||
<?= e(trans('cms::lang.theme_log.empty_link')) ?>
|
||||
</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-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>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<div class="scoreboard-item title-value">
|
||||
<h4><?= e(trans('cms::lang.theme_log.id_label')) ?></h4>
|
||||
<p>#<?= e($formModel->id) ?></p>
|
||||
</div>
|
||||
<?php if ($formModel->user): ?>
|
||||
<div class="scoreboard-item title-value">
|
||||
<h4><?= e(trans('cms::lang.theme_log.user')) ?></h4>
|
||||
<p><?= e($formModel->user->full_name) ?></p>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
<div class="scoreboard-item title-value">
|
||||
<h4><?= e(trans('cms::lang.theme_log.created_at')) ?></h4>
|
||||
<p><?= e($formModel->created_at->toDayDateTimeString()) ?></p>
|
||||
</div>
|
||||
<div class="scoreboard-item title-value">
|
||||
<h4><?= e(trans('cms::lang.theme_log.theme_name')) ?></h4>
|
||||
<p><?= e($formModel->theme_name) ?></p>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# ===================================
|
||||
# Form Behavior Config
|
||||
# ===================================
|
||||
|
||||
# Record name
|
||||
name: system::lang.event_log.menu_label
|
||||
|
||||
# Model Form Field configuration
|
||||
form: ~/modules/cms/models/themelog/fields.yaml
|
||||
|
||||
# Model Class name
|
||||
modelClass: Cms\Models\ThemeLog
|
||||
|
||||
# Default redirect location
|
||||
defaultRedirect: cms/themelogs
|
||||
|
||||
# Preview page
|
||||
preview:
|
||||
title: cms::lang.theme_log.preview_title
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# ===================================
|
||||
# List Behavior Config
|
||||
# ===================================
|
||||
|
||||
title: cms::lang.theme_log.menu_label
|
||||
list: ~/modules/cms/models/themelog/columns.yaml
|
||||
modelClass: Cms\Models\ThemeLog
|
||||
recordUrl: cms/themelogs/preview/:id
|
||||
noRecordsMessage: backend::lang.list.no_records
|
||||
recordsPerPage: 30
|
||||
showSetup: true
|
||||
showCheckboxes: true
|
||||
defaultSort:
|
||||
column: count
|
||||
direction: desc
|
||||
|
||||
toolbar:
|
||||
buttons: list_toolbar
|
||||
search:
|
||||
prompt: backend::lang.list.search_prompt
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<div class="padded-container container-flush">
|
||||
<?= $this->makeHintPartial('system_requestlogs_hint', 'hint') ?>
|
||||
</div>
|
||||
|
||||
<?= $this->listRender() ?>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?php Block::put('breadcrumb') ?>
|
||||
<ul>
|
||||
<li><a href="<?= Backend::url('cms/themelogs') ?>"><?= e(trans('cms::lang.theme_log.menu_label')) ?></a></li>
|
||||
<li><?= e(trans($this->pageTitle)) ?></li>
|
||||
</ul>
|
||||
<?php Block::endPut() ?>
|
||||
|
||||
<?php if (!$this->fatalError): ?>
|
||||
|
||||
<div class="scoreboard">
|
||||
<div data-control="toolbar">
|
||||
<?= $this->makePartial('preview_scoreboard') ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<?= $this->makePartial('hint_preview') ?>
|
||||
</div>
|
||||
|
||||
<div class="layout-item stretch layout-column">
|
||||
<?= $this->formRenderPreview() ?>
|
||||
</div>
|
||||
|
||||
<?php else: ?>
|
||||
|
||||
<p class="flash-message static error"><?= e(trans($this->fatalError)) ?></p>
|
||||
|
||||
<?php endif ?>
|
||||
|
||||
<p>
|
||||
<a href="<?= Backend::url('cms/themelogs') ?>" class="btn btn-default oc-icon-chevron-left">
|
||||
<?= e(trans('cms::lang.theme_log.return_link')) ?>
|
||||
</a>
|
||||
</p>
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
use October\Rain\Database\Schema\Blueprint;
|
||||
use October\Rain\Database\Updates\Migration;
|
||||
|
||||
class DbCmsThemeLogs extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::create('cms_theme_logs', function (Blueprint $table) {
|
||||
$table->engine = 'InnoDB';
|
||||
$table->increments('id');
|
||||
$table->string('type', 20)->index();
|
||||
$table->string('theme')->nullable()->index();
|
||||
$table->string('template')->nullable();
|
||||
$table->string('old_template')->nullable();
|
||||
$table->longText('content')->nullable();
|
||||
$table->longText('old_content')->nullable();
|
||||
$table->integer('user_id')->index()->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('cms_theme_logs');
|
||||
}
|
||||
}
|
||||
|
|
@ -324,5 +324,34 @@ return [
|
|||
'resize_image' => 'Resize image',
|
||||
'image_size' => 'Image size:',
|
||||
'selected_size' => 'Selected:'
|
||||
]
|
||||
],
|
||||
'theme_log' => [
|
||||
'hint' => 'This log displays any changes made to the theme by administrators in the back-end area.',
|
||||
'menu_label' => 'Theme log',
|
||||
'menu_description' => 'View changes made to the active theme.',
|
||||
'empty_link' => 'Empty theme log',
|
||||
'empty_loading' => 'Emptying theme log...',
|
||||
'empty_success' => 'Theme log emptied',
|
||||
'return_link' => 'Return to theme log',
|
||||
'id' => 'ID',
|
||||
'id_label' => 'Log ID',
|
||||
'created_at' => 'Date & Time',
|
||||
'user' => 'User',
|
||||
'type' => 'Type',
|
||||
'type_create' => 'Create',
|
||||
'type_update' => 'Update',
|
||||
'type_delete' => 'Delete',
|
||||
'theme_name' => 'Theme',
|
||||
'theme_code' => 'Theme code',
|
||||
'old_template' => 'Template (Old)',
|
||||
'new_template' => 'Template (New)',
|
||||
'template' => 'Template',
|
||||
'diff' => 'Changes',
|
||||
'old_value' => 'Old value',
|
||||
'new_value' => 'New value',
|
||||
'preview_title' => 'Template changes',
|
||||
'template_updated' => 'Template was updated',
|
||||
'template_created' => 'Template was created',
|
||||
'template_deleted' => 'Template was deleted',
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -33,8 +33,9 @@ class MaintenanceSetting extends Model
|
|||
|
||||
public function getCmsPageOptions()
|
||||
{
|
||||
if (!$theme = Theme::getEditTheme())
|
||||
if (!$theme = Theme::getEditTheme()) {
|
||||
throw new ApplicationException('Unable to find the active theme.');
|
||||
}
|
||||
|
||||
return Page::listInTheme($theme)->lists('fileName', 'fileName');
|
||||
}
|
||||
|
|
@ -45,8 +46,9 @@ class MaintenanceSetting extends Model
|
|||
*/
|
||||
public function beforeValidate()
|
||||
{
|
||||
if (!$theme = Theme::getEditTheme())
|
||||
if (!$theme = Theme::getEditTheme()) {
|
||||
throw new ApplicationException('Unable to find the active theme.');
|
||||
}
|
||||
|
||||
$themeMap = $this->getSettingsValue('theme_map', []);
|
||||
$themeMap[$theme->getDirName()] = $this->getSettingsValue('cms_page');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,133 @@
|
|||
<?php namespace Cms\Models;
|
||||
|
||||
use App;
|
||||
use Model;
|
||||
use BackendAuth;
|
||||
use Cms\Classes\Theme;
|
||||
use System\Models\LogSetting;
|
||||
use October\Rain\Halcyon\Model as HalcyonModel;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Model for changes made to the theme
|
||||
*
|
||||
* @package october\cms
|
||||
* @author Alexey Bobkov, Samuel Georges
|
||||
*/
|
||||
class ThemeLog extends Model
|
||||
{
|
||||
const TYPE_CREATE = 'create';
|
||||
const TYPE_UPDATE = 'update';
|
||||
const TYPE_DELETE = 'delete';
|
||||
|
||||
/**
|
||||
* @var string The database table used by the model.
|
||||
*/
|
||||
protected $table = 'cms_theme_logs';
|
||||
|
||||
/**
|
||||
* @var array Relations
|
||||
*/
|
||||
public $belongsTo = [
|
||||
'user' => ['Backend\Models\User']
|
||||
];
|
||||
|
||||
protected $themeCache;
|
||||
|
||||
/**
|
||||
* Adds observers to the model for logging purposes.
|
||||
*/
|
||||
public static function bindEventsToModel(HalcyonModel $template)
|
||||
{
|
||||
$template->bindEvent('model.beforeDelete', function() use ($template) {
|
||||
self::add($template, self::TYPE_DELETE);
|
||||
});
|
||||
|
||||
$template->bindEvent('model.beforeSave', function() use ($template) {
|
||||
self::add($template, $template->exists ? self::TYPE_UPDATE : self::TYPE_CREATE);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a log record
|
||||
* @return self
|
||||
*/
|
||||
public static function add(HalcyonModel $template, $type = null)
|
||||
{
|
||||
if (!App::hasDatabase()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!LogSetting::get('log_theme')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$type) {
|
||||
$type = self::TYPE_UPDATE;
|
||||
}
|
||||
|
||||
$isDelete = $type === self::TYPE_DELETE;
|
||||
$dirName = $template->getObjectTypeDirName();
|
||||
$templateName = $template->fileName;
|
||||
$oldTemplateName = $template->getOriginal('fileName');
|
||||
$newContent = $template->toCompiled();
|
||||
$oldContent = $template->getOriginal('content');
|
||||
|
||||
if ($newContent === $oldContent && !$isDelete) {
|
||||
traceLog($newContent, $oldContent);
|
||||
traceLog('Content not dirty for: '. $template->getObjectTypeDirName().'/'.$template->fileName);
|
||||
return;
|
||||
}
|
||||
|
||||
$record = new self;
|
||||
$record->type = $type;
|
||||
$record->theme = Theme::getEditThemeCode();
|
||||
$record->template = $isDelete ? '' : $dirName.'/'.$templateName;
|
||||
$record->old_template = $oldTemplateName ? $dirName.'/'.$oldTemplateName : '';
|
||||
$record->content = $isDelete ? '' : $newContent;
|
||||
$record->old_content = $oldContent;
|
||||
|
||||
if ($user = BackendAuth::getUser()) {
|
||||
$record->user_id = $user->id;
|
||||
}
|
||||
|
||||
try {
|
||||
$record->save();
|
||||
}
|
||||
catch (Exception $ex) {}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
public function getThemeNameAttribute()
|
||||
{
|
||||
$code = $this->theme;
|
||||
|
||||
if (!isset($this->themeCache[$code])) {
|
||||
$this->themeCache[$code] = Theme::load($code);
|
||||
}
|
||||
|
||||
$theme = $this->themeCache[$code];
|
||||
|
||||
return $theme->getConfigValue('name', $theme->getDirName());
|
||||
}
|
||||
|
||||
public function getTypeOptions()
|
||||
{
|
||||
return [
|
||||
self::TYPE_CREATE => 'cms::lang.theme_log.type_create',
|
||||
self::TYPE_UPDATE => 'cms::lang.theme_log.type_update',
|
||||
self::TYPE_DELETE => 'cms::lang.theme_log.type_delete'
|
||||
];
|
||||
}
|
||||
|
||||
public function getAnyTemplateAttribute()
|
||||
{
|
||||
return $this->template ?: $this->old_template;
|
||||
}
|
||||
|
||||
public function getTypeNameAttribute()
|
||||
{
|
||||
return array_get($this->getTypeOptions(), $this->type);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
# ===================================
|
||||
# Column Definitions
|
||||
# ===================================
|
||||
|
||||
columns:
|
||||
id:
|
||||
label: cms::lang.theme_log.id
|
||||
searchable: yes
|
||||
invisible: true
|
||||
width: 75px
|
||||
|
||||
created_at:
|
||||
label: cms::lang.theme_log.created_at
|
||||
searchable: yes
|
||||
width: 160px
|
||||
type: timetense
|
||||
|
||||
type:
|
||||
label: cms::lang.theme_log.type
|
||||
invisible: true
|
||||
|
||||
any_template:
|
||||
label: cms::lang.theme_log.template
|
||||
|
||||
new_template:
|
||||
label: cms::lang.theme_log.new_template
|
||||
searchable: true
|
||||
invisible: true
|
||||
|
||||
old_template:
|
||||
label: cms::lang.theme_log.old_template
|
||||
searchable: true
|
||||
invisible: true
|
||||
|
||||
user:
|
||||
label: cms::lang.theme_log.user
|
||||
relation: user
|
||||
select: concat(first_name, ' ', last_name)
|
||||
|
||||
theme_name:
|
||||
label: cms::lang.theme_log.theme_name
|
||||
sortable: false
|
||||
|
||||
theme:
|
||||
label: cms::lang.theme_log.theme_code
|
||||
searchable: true
|
||||
invisible: true
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
# ===================================
|
||||
# Field Definitions
|
||||
# ===================================
|
||||
|
||||
tabs:
|
||||
fields:
|
||||
|
||||
diff_template:
|
||||
tab: cms::lang.theme_log.diff
|
||||
type: partial
|
||||
path: field_diff_template
|
||||
|
||||
diff_content:
|
||||
tab: cms::lang.theme_log.diff
|
||||
type: partial
|
||||
path: field_diff_content
|
||||
|
||||
template:
|
||||
tab: cms::lang.theme_log.new_value
|
||||
type: partial
|
||||
path: field_template
|
||||
|
||||
content:
|
||||
tab: cms::lang.theme_log.new_value
|
||||
type: partial
|
||||
path: field_content
|
||||
|
||||
old_template:
|
||||
tab: cms::lang.theme_log.old_value
|
||||
type: partial
|
||||
path: field_template
|
||||
|
||||
old_content:
|
||||
tab: cms::lang.theme_log.old_value
|
||||
type: partial
|
||||
path: field_content
|
||||
|
|
@ -377,6 +377,10 @@ class ServiceProvider extends ModuleServiceProvider
|
|||
*/
|
||||
protected function registerBackendSettings()
|
||||
{
|
||||
Event::listen('system.settings.extendItems', function($manager) {
|
||||
\System\Models\LogSetting::filterSettingItems($manager);
|
||||
});
|
||||
|
||||
SettingsManager::instance()->registerCallback(function ($manager) {
|
||||
$manager->registerSettingItems('October.System', [
|
||||
'updates' => [
|
||||
|
|
@ -434,7 +438,16 @@ class ServiceProvider extends ModuleServiceProvider
|
|||
'permissions' => ['system.access_logs'],
|
||||
'order' => 910,
|
||||
'keywords' => '404 error'
|
||||
]
|
||||
],
|
||||
'log_settings' => [
|
||||
'label' => 'system::lang.log.menu_label',
|
||||
'description' => 'system::lang.log.menu_description',
|
||||
'category' => SettingsManager::CATEGORY_LOGS,
|
||||
'icon' => 'icon-dot-circle-o',
|
||||
'class' => 'System\Models\LogSetting',
|
||||
'permissions' => ['system.manage_logs'],
|
||||
'order' => 990
|
||||
],
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
<?php namespace System\Classes;
|
||||
|
||||
use Event;
|
||||
use Backend;
|
||||
use BackendAuth;
|
||||
use System\Classes\PluginManager;
|
||||
use SystemException;
|
||||
|
||||
/**
|
||||
* Manages the system settings.
|
||||
|
|
@ -41,9 +43,9 @@ class SettingsManager
|
|||
protected $items;
|
||||
|
||||
/**
|
||||
* @var array Flat collection of all items.
|
||||
* @var array Grouped collection of all items, by category.
|
||||
*/
|
||||
protected $allItems;
|
||||
protected $groupedItems;
|
||||
|
||||
/**
|
||||
* @var string Active plugin or module owner.
|
||||
|
|
@ -106,6 +108,11 @@ class SettingsManager
|
|||
$this->registerSettingItems($id, $items);
|
||||
}
|
||||
|
||||
/*
|
||||
* Extensibility
|
||||
*/
|
||||
Event::fire('system.settings.extendItems', [$this]);
|
||||
|
||||
/*
|
||||
* Sort settings items
|
||||
*/
|
||||
|
|
@ -123,21 +130,22 @@ class SettingsManager
|
|||
* Process each item in to a category array
|
||||
*/
|
||||
$catItems = [];
|
||||
foreach ($this->items as $item) {
|
||||
foreach ($this->items as $code => $item) {
|
||||
$category = $item->category ?: self::CATEGORY_MISC;
|
||||
if (!isset($catItems[$category])) {
|
||||
$catItems[$category] = [];
|
||||
}
|
||||
|
||||
$catItems[$category][] = $item;
|
||||
$catItems[$category][$code] = $item;
|
||||
}
|
||||
|
||||
$this->allItems = $this->items;
|
||||
$this->items = $catItems;
|
||||
$this->groupedItems = $catItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a collection of all settings
|
||||
* Returns a collection of all settings by group, filtered by context
|
||||
* @param string $context
|
||||
* @return array
|
||||
*/
|
||||
public function listItems($context = null)
|
||||
{
|
||||
|
|
@ -146,10 +154,10 @@ class SettingsManager
|
|||
}
|
||||
|
||||
if ($context !== null) {
|
||||
return $this->filterByContext($this->items, $context);
|
||||
return $this->filterByContext($this->groupedItems, $context);
|
||||
}
|
||||
|
||||
return $this->items;
|
||||
return $this->groupedItems;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -218,33 +226,77 @@ class SettingsManager
|
|||
$this->items = [];
|
||||
}
|
||||
|
||||
$this->addSettingItems($owner, $definitions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically add an array of setting items
|
||||
* @param string $owner
|
||||
* @param array $definitions
|
||||
*/
|
||||
public function addSettingItems($owner, array $definitions)
|
||||
{
|
||||
foreach ($definitions as $code => $definition) {
|
||||
$item = array_merge(self::$itemDefaults, array_merge($definition, [
|
||||
'code' => $code,
|
||||
'owner' => $owner
|
||||
]));
|
||||
$this->addSettingItem($owner, $code, $definition);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Link to the generic settings page
|
||||
*/
|
||||
if (isset($item['class'])) {
|
||||
$uri = [];
|
||||
/**
|
||||
* Dynamically add a single setting item
|
||||
* @param string $owner
|
||||
* @param string $code
|
||||
* @param array $definitions
|
||||
*/
|
||||
public function addSettingItem($owner, $code, array $definition)
|
||||
{
|
||||
$itemKey = $this->makeItemKey($owner, $code);
|
||||
|
||||
if (strpos($owner, '.') !== null) {
|
||||
list($author, $plugin) = explode('.', $owner);
|
||||
$uri[] = strtolower($author);
|
||||
$uri[] = strtolower($plugin);
|
||||
}
|
||||
else {
|
||||
$uri[] = strtolower($owner);
|
||||
}
|
||||
$item = array_merge(self::$itemDefaults, array_merge($definition, [
|
||||
'code' => $code,
|
||||
'owner' => $owner
|
||||
]));
|
||||
|
||||
$uri[] = strtolower($code);
|
||||
$uri = implode('/', $uri);
|
||||
$item['url'] = Backend::url('system/settings/update/' . $uri);
|
||||
/*
|
||||
* Link to the generic settings page
|
||||
*/
|
||||
if (isset($item['class'])) {
|
||||
$uri = [];
|
||||
|
||||
if (strpos($owner, '.') !== null) {
|
||||
list($author, $plugin) = explode('.', $owner);
|
||||
$uri[] = strtolower($author);
|
||||
$uri[] = strtolower($plugin);
|
||||
}
|
||||
else {
|
||||
$uri[] = strtolower($owner);
|
||||
}
|
||||
|
||||
$this->items[] = (object)$item;
|
||||
$uri[] = strtolower($code);
|
||||
$uri = implode('/', $uri);
|
||||
$item['url'] = Backend::url('system/settings/update/' . $uri);
|
||||
}
|
||||
|
||||
$this->items[$itemKey] = (object) $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a single setting item
|
||||
*/
|
||||
public function removeSettingItem($owner, $code)
|
||||
{
|
||||
if (!$this->items) {
|
||||
throw new SystemException('Unable to remove settings item before items are loaded.');
|
||||
}
|
||||
|
||||
$itemKey = $this->makeItemKey($owner, $code);
|
||||
unset($this->items[$itemKey]);
|
||||
|
||||
if ($this->groupedItems) {
|
||||
foreach ($this->groupedItems as $category => $items) {
|
||||
if (isset($items[$itemKey])) {
|
||||
unset($this->groupedItems[$category][$itemKey]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -269,7 +321,7 @@ class SettingsManager
|
|||
*/
|
||||
public function getContext()
|
||||
{
|
||||
return (object)[
|
||||
return (object) [
|
||||
'itemCode' => $this->contextItemCode,
|
||||
'owner' => $this->contextOwner
|
||||
];
|
||||
|
|
@ -283,14 +335,14 @@ class SettingsManager
|
|||
*/
|
||||
public function findSettingItem($owner, $code)
|
||||
{
|
||||
if ($this->allItems === null) {
|
||||
if ($this->items === null) {
|
||||
$this->loadItems();
|
||||
}
|
||||
|
||||
$owner = strtolower($owner);
|
||||
$code = strtolower($code);
|
||||
|
||||
foreach ($this->allItems as $item) {
|
||||
foreach ($this->items as $item) {
|
||||
if (strtolower($item->owner) == $owner && strtolower($item->code) == $code) {
|
||||
return $item;
|
||||
}
|
||||
|
|
@ -317,4 +369,14 @@ class SettingsManager
|
|||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to make a unique key for an item.
|
||||
* @param object $item
|
||||
* @return string
|
||||
*/
|
||||
protected function makeItemKey($owner, $code)
|
||||
{
|
||||
return strtoupper($owner).'.'.strtoupper($code);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -312,7 +312,7 @@ return [
|
|||
'menu_description' => 'View system log messages with their recorded time and details.',
|
||||
'empty_link' => 'Empty event log',
|
||||
'empty_loading' => 'Emptying event log...',
|
||||
'empty_success' => 'Successfully emptied the event log.',
|
||||
'empty_success' => 'Event log emptied',
|
||||
'return_link' => 'Return to event log',
|
||||
'id' => 'ID',
|
||||
'id_label' => 'Event ID',
|
||||
|
|
@ -327,7 +327,7 @@ return [
|
|||
'menu_description' => 'View bad or redirected requests, such as Page not found (404).',
|
||||
'empty_link' => 'Empty request log',
|
||||
'empty_loading' => 'Emptying request log...',
|
||||
'empty_success' => 'Successfully emptied the request log.',
|
||||
'empty_success' => 'Request log emptied',
|
||||
'return_link' => 'Return to request log',
|
||||
'id' => 'ID',
|
||||
'id_label' => 'Log ID',
|
||||
|
|
@ -349,5 +349,15 @@ return [
|
|||
'manage_editor' => 'Manage code editor preferences',
|
||||
'view_the_dashboard' => 'View the dashboard',
|
||||
'manage_branding' => 'Customize the back-end'
|
||||
],
|
||||
'log' => [
|
||||
'menu_label' => 'Log settings',
|
||||
'menu_description' => 'Specify which areas should use logging.',
|
||||
'log_events' => 'Log system events',
|
||||
'log_events_comment' => 'Browser requests that may require attention, such as 404 errors.',
|
||||
'log_requests' => 'Log bad requests',
|
||||
'log_requests_comment' => 'When a change is made to the theme using the back-end.',
|
||||
'log_theme' => 'Log theme changes',
|
||||
'log_theme_comment' => 'Store system events in the database in addition to the file-based log.',
|
||||
]
|
||||
];
|
||||
|
|
|
|||
|
|
@ -33,7 +33,8 @@ class EventLog extends Model
|
|||
class_exists('Model') &&
|
||||
Model::getConnectionResolver() &&
|
||||
App::hasDatabase() &&
|
||||
!defined('OCTOBER_NO_EVENT_LOGGING')
|
||||
!defined('OCTOBER_NO_EVENT_LOGGING') &&
|
||||
LogSetting::get('log_requests')
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
<?php namespace System\Models;
|
||||
|
||||
use Model;
|
||||
use ApplicationException;
|
||||
|
||||
/**
|
||||
* System log settings
|
||||
*
|
||||
* @package october\system
|
||||
* @author Alexey Bobkov, Samuel Georges
|
||||
*/
|
||||
class LogSetting extends Model
|
||||
{
|
||||
use \October\Rain\Database\Traits\Validation;
|
||||
|
||||
public $implement = ['System.Behaviors.SettingsModel'];
|
||||
|
||||
public $settingsCode = 'system_log_settings';
|
||||
|
||||
public $settingsFields = 'fields.yaml';
|
||||
|
||||
/**
|
||||
* Validation rules
|
||||
*/
|
||||
public $rules = [];
|
||||
|
||||
public static function filterSettingItems($manager)
|
||||
{
|
||||
if (!self::isConfigured()) {
|
||||
$manager->removeSettingItem('October.System', 'request_logs');
|
||||
$manager->removeSettingItem('October.Cms', 'theme_logs');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!self::get('log_events')) {
|
||||
$manager->removeSettingItem('October.System', 'event_logs');
|
||||
}
|
||||
|
||||
if (!self::get('log_requests')) {
|
||||
$manager->removeSettingItem('October.System', 'request_logs');
|
||||
}
|
||||
|
||||
if (!self::get('log_theme')) {
|
||||
$manager->removeSettingItem('October.Cms', 'theme_logs');
|
||||
}
|
||||
}
|
||||
|
||||
public function initSettingsData()
|
||||
{
|
||||
$this->log_events = true;
|
||||
$this->log_requests = false;
|
||||
$this->log_theme = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -37,6 +37,10 @@ class RequestLog extends Model
|
|||
return;
|
||||
}
|
||||
|
||||
if (!LogSetting::get('log_requests')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$record = static::firstOrNew([
|
||||
'url' => substr(Request::fullUrl(), 0, 255),
|
||||
'status_code' => $statusCode,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
# ===================================
|
||||
# Field Definitions
|
||||
# ===================================
|
||||
|
||||
tabs:
|
||||
defaultTab: Logging
|
||||
fields:
|
||||
|
||||
log_requests:
|
||||
label: system::lang.log.log_requests
|
||||
span: auto
|
||||
type: switch
|
||||
comment: system::lang.log.log_events_comment
|
||||
|
||||
log_theme:
|
||||
label: system::lang.log.log_theme
|
||||
span: auto
|
||||
type: switch
|
||||
comment: system::lang.log.log_requests_comment
|
||||
|
||||
log_events:
|
||||
label: system::lang.log.log_events
|
||||
span: auto
|
||||
type: switch
|
||||
comment: system::lang.log.log_theme_comment
|
||||
Loading…
Reference in New Issue