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:
Samuel Georges 2017-02-08 05:43:40 +11:00
parent cc1a67373c
commit 462c9cd4e8
33 changed files with 2302 additions and 71 deletions

View File

@ -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;
}

View File

@ -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'
]
]);
});
}

View File

@ -0,0 +1,10 @@
del {
text-decoration: none;
color: #b30000;
background: #fadad7;
}
ins {
background: #eaf2c2;
color: #406619;
text-decoration: none;
}

View File

@ -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);

1407
modules/cms/assets/vendor/jsdiff/diff.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -488,5 +488,4 @@ class CmsCompoundObject extends CmsObject
return parent::__call($method, $parameters);
}
}

View File

@ -319,5 +319,4 @@ class CmsObject extends HalcyonModel implements CmsObjectContract
throw $ex;
}
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,3 @@
<div class="form-control">
<pre><?= e($value.PHP_EOL) ?></pre>
</div>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1 @@
<div class="form-control"><?= e($value) ?></div>

View File

@ -0,0 +1,4 @@
<p>
<?= e(trans('cms::lang.theme_log.hint')) ?>
</p>

View File

@ -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 ?>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
<div class="padded-container container-flush">
<?= $this->makeHintPartial('system_requestlogs_hint', 'hint') ?>
</div>
<?= $this->listRender() ?>

View File

@ -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>

View File

@ -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');
}
}

View File

@ -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',
],
];

View File

@ -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');

View File

@ -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);
}
}

View File

@ -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

View File

@ -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

View File

@ -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
],
]);
});
}

View File

@ -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);
}
}

View File

@ -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.',
]
];

View File

@ -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')
);
}

View File

@ -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;
}
}

View File

@ -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,

View File

@ -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