Commit c061da40 authored by Administrator's avatar Administrator 💬

chore: add forlorn plugin files

parent e9056798
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: grav
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
custom: # Replace with a single custom sponsorship URL
This diff is collapsed.
This diff is collapsed.
---
title: Error
robots: noindex,nofollow
template: error
routable: false
http_response_code: 404
---
Woops! Looks like this page doesn't exist.
import $ from 'jquery';
import '../../utils/cron-ui';
import { translations } from 'grav-config';
export default class CronField {
constructor() {
this.items = $();
$('[data-grav-field="cron"]').each((index, cron) => this.addCron(cron));
$('body').on('mutation._grav', this._onAddedNodes.bind(this));
}
addCron(cron) {
cron = $(cron);
this.items = this.items.add(cron);
cron.find('.cron-selector').each((index, container) => {
container = $(container);
const input = container.closest('[data-grav-field]').find('input');
container.jqCron({
numeric_zero_pad: true,
enabled_minute: true,
multiple_dom: true,
multiple_month: true,
multiple_mins: true,
multiple_dow: true,
multiple_time_hours: true,
multiple_time_minutes: true,
default_period: 'hour',
default_value: input.val() || '* * * * *',
no_reset_button: false,
bind_to: input,
bind_method: {
set: function($element, value) {
$element.val(value);
}
},
texts: {
en: {
empty: translations.GRAV_CORE['CRON.EVERY'],
empty_minutes: translations.GRAV_CORE['CRON.EVERY'],
empty_time_hours: translations.GRAV_CORE['CRON.EVERY_HOUR'],
empty_time_minutes: translations.GRAV_CORE['CRON.EVERY_MINUTE'],
empty_day_of_week: translations.GRAV_CORE['CRON.EVERY_DAY_OF_WEEK'],
empty_day_of_month: translations.GRAV_CORE['CRON.EVERY_DAY_OF_MONTH'],
empty_month: translations.GRAV_CORE['CRON.EVERY_MONTH'],
name_minute: translations.GRAV_CORE['NICETIME.MINUTE'],
name_hour: translations.GRAV_CORE['NICETIME.HOUR'],
name_day: translations.GRAV_CORE['NICETIME.DAY'],
name_week: translations.GRAV_CORE['NICETIME.WEEK'],
name_month: translations.GRAV_CORE['NICETIME.MONTH'],
name_year: translations.GRAV_CORE['NICETIME.YEAR'],
text_period: translations.GRAV_CORE['CRON.TEXT_PERIOD'],
text_mins: translations.GRAV_CORE['CRON.TEXT_MINS'],
text_time: translations.GRAV_CORE['CRON.TEXT_TIME'],
text_dow: translations.GRAV_CORE['CRON.TEXT_DOW'],
text_month: translations.GRAV_CORE['CRON.TEXT_MONTH'],
text_dom: translations.GRAV_CORE['CRON.TEXT_DOM'],
error1: translations.GRAV_CORE['CRON.ERROR1'],
error2: translations.GRAV_CORE['CRON.ERROR2'],
error3: translations.GRAV_CORE['CRON.ERROR3'],
error4: translations.GRAV_CORE['CRON.ERROR4'],
weekdays: translations.GRAV_CORE['DAYS_OF_THE_WEEK'],
months: translations.GRAV_CORE['MONTHS_OF_THE_YEAR']
}
}
});
});
}
_onAddedNodes(event, target/* , record, instance */) {
let crons = $(target).find('[data-grav-field="cron"]');
if (!crons.length) { return; }
crons.each((index, list) => {
list = $(list);
if (!~this.items.index(list)) {
this.addCron(list);
}
});
}
}
export let Instance = new CronField();
/*
// cron-selector
$(document).ready(function() {
$('.cron-selector').each(function(index, wrapper) {
wrapper = $(wrapper);
const input = wrapper.closest('[data-grav-field]').find('input');
wrapper.jqCron({
enabled_minute: true,
multiple_dom: true,
multiple_month: true,
multiple_mins: true,
multiple_dow: true,
multiple_time_hours: true,
multiple_time_minutes: true,
default_period: 'week',
default_value: input.val() || '3 * * * *',
no_reset_button: false,
bind_to: input,
bind_method: {
set: function($element, value) {
$element.val(value);
}
},
texts: {
en: {
empty: 'every',
empty_minutes: 'every',
empty_time_hours: 'every hour',
empty_time_minutes: 'every minute',
empty_day_of_week: 'every day of the week',
empty_day_of_month: 'every day of the month',
empty_month: 'every month',
name_minute: 'minute',
name_hour: 'hour',
name_day: 'day',
name_week: 'week',
name_month: 'month',
name_year: 'year',
text_period: 'Every <b />',
text_mins: ' at <b /> minute(s) past the hour',
text_time: ' at <b />:<b />',
text_dow: ' on <b />',
text_month: ' of <b />',
text_dom: ' on <b />',
error1: 'The tag %s is not supported !',
error2: 'Bad number of elements',
error3: 'The jquery_element should be set into jqCron settings',
error4: 'Unrecognized expression',
weekdays: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'],
months: ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december']
}
}
});
});
});
*/
import $ from 'jquery';
$(document).ready(function() {
$('.copy-to-clipboard').click(function(event) {
var $tempElement = $('<input>');
$('body').append($tempElement);
$tempElement.val($(this).prev('input').val()).select();
document.execCommand('Copy');
$tempElement.remove();
$(this).attr('data-hint', 'Copied to clipboard!').addClass('hint--left');
});
});
import $ from 'jquery';
import { setParam } from 'mout/queryString';
const prepareQuery = (key, value) => {
return setParam(global.location.href, key, value);
};
$(document).on('change', '.logs-content .block-select select[name]', (event) => {
const target = $(event.currentTarget);
const name = target.attr('name');
const value = target.val();
global.location.href = prepareQuery(name, value);
});
This diff is collapsed.
import $ from 'jquery';
import Selectize from 'selectize';
Selectize.define('option_click', function(options) {
const self = this;
const setup = self.setup;
this.setup = function() {
setup.apply(self, arguments);
let clicking = false;
// Detect click on a .clickable
self.$dropdown_content.on('mousedown click', function(e) {
const target = $(e.target);
if (target.hasClass('clickable') || target.closest('.clickable').length) {
if (e.type === 'mousedown') {
clicking = true;
self.isFocused = false; // awful hack to defuse the document mousedown listener
} else {
self.isFocused = true;
setTimeout(function() {
clicking = false; // wait until blur has been preempted
});
}
} else { // cleanup in case user right-clicked or dragged off the element
clicking = false;
self.isFocused = true;
}
});
// Intercept default handlers
self.$dropdown.off('mousedown click', '[data-selectable]').on('mousedown click', '[data-selectable]', function() {
if (!clicking) {
return self.onOptionSelect.apply(self, arguments);
}
});
self.$control_input.off('blur').on('blur', function() {
if (!clicking) {
return self.onBlur.apply(self, arguments);
}
});
};
});
This diff is collapsed.
This diff is collapsed.
<svg id="grav-logo-large" class="grav-logo" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" viewBox="0 0 444 103" clip-rule="evenodd"><path d="M87.3 54.2H76.64c-1.6 0-2.93 1.3-2.93 2.94v20.43l-.2.18c-6.27 5.28-14.2 8.2-22.3 8.2-19.14-.02-34.7-15.6-34.7-34.73 0-19.14 15.56-34.7 34.7-34.7 7.44 0 14.56 2.34 20.58 6.8 1.17.87 2.8.75 3.83-.28l7.58-7.58c.6-.6.9-1.4.86-2.25-.04-.82-.44-1.6-1.1-2.12C73.94 3.94 62.66 0 51.22 0 22.98 0 0 22.98 0 51.22s22.98 51.22 51.22 51.22c14.58 0 28.53-6.27 38.26-17.22.48-.54.74-1.23.74-1.95V57.14c0-1.63-1.3-2.94-2.93-2.94M443.24 4.7c-.54-.8-1.47-1.3-2.45-1.3h-11.6c-1.16 0-2.22.68-2.68 1.76l-32.65 75.8-33.22-75.82c-.47-1.06-1.53-1.75-2.7-1.75h-11.6c-1 0-1.92.5-2.46 1.32-.55.83-.65 1.87-.24 2.78l40.24 91.8c.47 1.08 1.53 1.77 2.7 1.77h14.65c1.17 0 2.23-.7 2.7-1.77L443.5 7.48c.37-.9.28-1.95-.26-2.77"/><path d="M291.1 5.14c-.48-1.06-1.53-1.75-2.7-1.75h-14.65c-1.17 0-2.23.68-2.7 1.76l-39.53 91.8c-.4.92-.3 1.96.24 2.78.54.83 1.46 1.33 2.45 1.33h11.6c1.16 0 2.22-.7 2.7-1.77l32.62-75.8 33.23 75.8c.47 1.08 1.52 1.77 2.68 1.77h11.63c1 0 1.92-.5 2.45-1.33.56-.83.64-1.88.25-2.8L291.1 5.15zM185.1 67.48l.65-.3c11.56-5.6 19.03-17.45 19.03-30.23 0-18.5-15.05-33.56-33.56-33.56h-42.5c-1.6 0-2.92 1.3-2.92 2.92v91.8c0 1.63 1.3 2.95 2.93 2.95h10.64c1.62 0 2.93-1.3 2.93-2.94V19.9h28.92c9.4 0 17.06 7.64 17.06 17.05 0 7.85-5.33 14.65-12.97 16.55-1.36.34-2.74.52-4.08.52h-10.64c-1.1 0-2.1.62-2.6 1.6-.5 1-.42 2.17.24 3.07l30.16 41.17c.55.75 1.42 1.2 2.36 1.2h13.22c1.1 0 2.12-.62 2.6-1.6.52-1 .42-2.18-.24-3.07l-21.2-28.92z"/></svg>
<svg id="grav-logo-small" class="grav-logo" width="100%" height="100%" viewBox="0 0 91 103" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;"><g transform="matrix(1,0,0,1,-411.231,-328.021)"><path d="M498.522,382.22L487.883,382.22C486.266,382.22 484.953,383.536 484.953,385.158L484.953,405.594L484.744,405.77C478.482,411.052 470.564,413.956 462.45,413.956C443.311,413.956 427.739,398.384 427.739,379.241C427.739,360.097 443.311,344.527 462.45,344.527C469.893,344.527 477.01,346.883 483.034,351.344C484.203,352.21 485.825,352.091 486.856,351.064L494.441,343.477C495.034,342.888 495.342,342.07 495.294,341.235C495.248,340.4 494.847,339.627 494.189,339.106C485.176,331.956 473.905,328.021 462.45,328.021C434.21,328.021 411.232,350.998 411.232,379.241C411.232,407.485 434.21,430.46 462.45,430.46C477.032,430.46 490.979,424.187 500.713,413.242C501.191,412.705 501.455,412.014 501.455,411.295L501.455,385.158C501.455,383.536 500.142,382.22 498.522,382.22" style="fill-rule:nonzero;"/></g></svg>
// +-------------------------------------------------------------------------+
// | Flat Colors |
// +-------------------------------------------------------------------------+
// Greens
$color-navy: #001f3f;
$color-blue: #0074D9;
$color-aqua: #7FDBFF;
$color-teal: #39CCCC;
$color-olive: #3D9970;
$color-green: #2ECC40;
$color-lime: #01FF70;
$color-yellow: #FFDC00;
$color-orange: #FF851B;
$color-red: #FF4136;
$color-maroon: #85144b;
$color-fuchsia: #F012BE;
$color-purple: #B10DC9;
/*
* This file is part of the Arnapou jqCron package.
*
* (c) Arnaud Buathier <arnaud@arnapou.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
.jqCron-selector {
position: relative;
}
.jqCron-cross,
.jqCron-selector-title {
cursor: pointer;
border-radius: 3px;
border: 1px solid #ddd;
margin: 0 0.2em;
padding: 0 0.5em;
}
.jqCron-container.disable .jqCron-cross:hover,
.jqCron-container.disable .jqCron-selector-title:hover,
.jqCron-cross,
.jqCron-selector-title {
background: #eee;
border-color: #ddd;
}
.jqCron-cross:hover,
.jqCron-selector-title:hover {
background-color: #ddd;
border-color: #aaa;
}
.jqCron-cross {
border-radius: 1em;
font-size: 80%;
padding: 0 0.3em;
}
.jqCron-selector-list {
background: #eee;
border: 1px solid #aaa;
-webkit-box-shadow: 2px 2px 3px #ccc;
box-shadow: 2px 2px 3px #ccc;
left: 0.2em;
list-style: none;
margin: 0;
padding: 0;
position: absolute;
top: 1.5em;
z-index: 5;
}
.jqCron-selector-list li {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
box-sizing: border-box;
cursor: default;
display: inline-block !important;
margin: 0;
padding: 0.1em 0.4em;
width: 100%;
}
.jqCron-selector-list li.selected {
background: #0088cc;
color: white;
}
.jqCron-selector-list li:hover {
background: #5fb9e7;
color: white;
}
.jqCron-selector-list.cols2 {
width: 4em;
}
.jqCron-selector-list.cols2 li {
width: 50%;
}
.jqCron-selector-list.cols3 {
width: 6em;
}
.jqCron-selector-list.cols3 li {
width: 33%;
}
.jqCron-selector-list.cols4 {
width: 8em;
}
.jqCron-selector-list.cols4 li {
width: 25%;
}
.jqCron-selector-list.cols5 {
width: 10em;
}
.jqCron-selector-list.cols5 li {
width: 20%;
}
.jqCron-error .jqCron-selector-title {
background: #fee;
border: 1px solid #fdd;
color: red;
}
.jqCron-container.disable * {
color: #888;
}
.jqCron-container.disable .jqCron-selector-title {
background: #eee !important;
}
// Backups
#backups-stats {
#backups-usage {
position: relative;
margin-bottom: 1.5rem;
h1 {
position: absolute;
right: 1rem;
top: 5px;
}
}
.backups-usage-wrapper {
height: 40px;
background: linear-gradient(90deg, $color-green 0%, $color-lime 20%, $color-yellow 40%, $color-orange 60%, $color-red 80%, $color-maroon 100%);
> div {
float: right;
height: 40px;
&.full {
width: 100%;
}
}
}
}
.backups-content {
#admin-dashboard {
display: block;
#backups-stats {
margin-left: 1rem;
margin-right: 1rem;
h1 {
padding: 0rem;
@include breakpoint(mobile-only) {
font-size: 1.1rem;
}
}
}
}
}
// Scheduler
.scheduler-content {
#admin-main .admin-block & .alert {
margin-top: -1rem;
margin-bottom: 2rem;
}
.secondary-accent {
.button {
float: right;
margin-top: -3px;
}
}
#cron-install {
@extend .default-animation;
padding: 0 1.5rem;
pre {
padding: 0.5rem;
}
&.hide {
display: none;
}
}
}
// Reports
.report-output {
#admin-main .admin-block & .alert {
margin-top: 0;
margin-bottom: 0;
}
td {
.key {
font-weight: bold;
}
.value {
}
}
}
// Direct install
.direct-install-content {
padding: 30px;
.button {
margin-top: 10px;
margin-bottom: 50px;
}
}
{% set delete_url = uri.addNonce(base_url_relative ~ "/backup.json/backup:%BACKUP_FILE/task" ~ config.system.param_sep ~ 'backupDelete', 'admin-form', 'admin-nonce') %}
<table class="backups-history noflex">
<thead>
<tr>
<th>#</th>
<th>{{ "PLUGIN_ADMIN.BACKUP_DATE"|tu }}</th>
<th>{{ "PLUGIN_ADMIN.NAME"|tu }}</th>
<th class="right pad">{{ "PLUGIN_ADMIN.SIZE"|tu }}</th>
<th class="right pad">{{ "PLUGIN_ADMIN.ACTION"|tu }}</th>
</tr>
</thead>
<tbody>
{% for backup in backups|default([]) %}
{% set encoded_name = backup.filename|base64_encode|url_encode %}
{% set backup_delete = delete_url|replace({'%BACKUP_FILE': encoded_name}) %}
<tr>
<td>{{ loop.index }}</td>
<td> <i class="fa fa-clock-o"></i> {{ backup.date|date }}</td>
<td>{{ backup.title }}</td>
<td class="right pad">{{ backup.size|nicefilesize }}</td>
<td class="right pad nowrap" >
<a class="button button-small hint--bottom" href="{{ grav.backups.getBackupDownloadUrl(backup.path, admin.base) }}" data-hint="Download"><i class="fa fa-download"></i></a>
<span class="button button-small danger hint--bottom" data-hint="Delete" data-backup data-ajax="{{ backup_delete }}"><i class="fa fa-close"></i></span>
</td>
</tr>
{% else %}
<tr>
<td colspan="5" class="error" style="text-align: center;">{{ "PLUGIN_ADMIN.BACKUPS_NOT_GENERATED"|tu }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% extends "forms/field.html.twig" %}
{% block input %}
<input type="hidden" class="input" name="{{ (scope ~ field.name)|fieldName }}" value="{{ value|join(', ') }}" />
<div class="cron-selector"></div>
{% endblock %}
{% set jobs = grav.scheduler.getAllJobs() %}
{% set job_states = grav.scheduler.getJobStates().content() %}
<table class="cron-status noflex">
<thead>
<tr>
<th style="flex:3;">Job ID</th>
<th style="flex:3;">Run</th>
<th>Status</th>
<th class="right pad">State</th>
</tr>
</thead>
<tbody>
{% for job in jobs %}
{% set job_status = attribute(data.status,job.id) %}
{% set job_enabled = job_status is defined and job_status == 'disabled' ? 0 : 1 %}
{% set job_id = job.id %}
{% set job_id_md5 = job_id|md5 %}
{% set job_state = attribute(job_states, job_id) %}
{% set job_at = job.getAt|default('* * * * *') %}
{% set job_backlink = job.backlink %}
<tr>
<td style="flex:3;overflow:hidden;">
{% if job_backlink %}
<a href="{{ base_url_relative ~ job_backlink }}"><kbd>{{ job.id }}</kbd></a>
{% else %}
<kbd>{{ job.id }}</kbd>
{% endif %}
</td>
<td style="flex:3;" class="cron-at">
{% if job_enabled %}
<span class="hint--bottom" data-hint="next run: {{ cron(job_at).getNextRunDate()|date(config.date_format.default) }}">{{ job_at|nicecron }}</span>
{% else %}
{{ job_at|nicecron }}
{% endif %}
</td>
<td>
{% if job_state.state == 'failure' %}
{% set run_type = 'error' %}
{% set run_hint = job_state.error %}
{% set run_text = "<i class=\"fa fa-warning\"></i> Failure" %}
{% else %}
{% set run_type = 'info' %}
{% if job_state.state is not defined %}
{% set run_hint = "not run yet" %}
{% set run_text = "<i class=\"fa fa-check\"></i> Ready" %}
{% else %}
{% set run_hint = "last run: " ~ attribute(job_state,'last-run')|date(config.date_format.default) %}
{% set run_text = "<i class=\"fa fa-check\"></i> Success" %}
{% endif %}
{% endif %}
<span class="hint--bottom" data-hint="{{ run_hint }}">
<span class="badge {{ run_type }}">{{ run_text|raw }}</span>
</span>
</td>
<td class="right pad">
<div class="form-data" data-grav-field="toggle" data-grav-disabled="" data-grav-default="null" data-grav-field-name="data[status][{{ job_id }}]">
<div class="switch-toggle switch-grav switch-2 ">
<input type="radio" value="enabled" id="toggle_status.{{ job_id_md5 }}1" name="data[status][{{ job_id }}]" class="highlight" {% if job_enabled %}checked="checked"{% endif %}>
<label for="toggle_status.{{ job_id_md5 }}1">Enabled</label>
<input type="radio" value="disabled" id="toggle_status.{{ job_id_md5 }}0" name="data[status][{{ job_id }}]" class="" {% if not job_enabled %}checked="checked"{% endif %}>
<label for="toggle_status.{{ job_id_md5 }}0">Disabled</label>
</div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="button-group">
{% set profiles = grav.backups.getBackupProfiles() %}
{% set backup_url = uri.addNonce(base_url_relative ~ "/backup.json/id" ~ config.system.param_sep ~ "%BACKUP_ID/task" ~ config.system.param_sep ~ "backup", 'admin-form', 'admin-nonce') %}
<button class="button" data-backup data-ajax="{{ backup_url|replace({'%BACKUP_ID':'0'}) }}">
<i class="fa fa-life-ring"></i> {{ "PLUGIN_ADMIN.BACKUP_NOW"|tu }}
</button>
<button type="button" class="button dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-caret-down"></i>
</button>
<ul class="dropdown-menu">
{% for id, profile in profiles %}
<li>
<a data-backup data-ajax="{{ backup_url|replace({'%BACKUP_ID':id}) }}" class="button">{{ profile.name }}</a>
</li>
{% endfor %}
<li>
<a href="{{ base_url ~ '/tools/backups' }}" class="button">Backups Manager</a>
</li>
</ul>
</div>
{% for entry in feed %}
<li><span class="date">{{ entry.nicetime }}</span> <a href="{{ entry.url }}" target="_blank" title="{{ entry.title|striptags|e('html_attr') }}">{{ entry.title }}</a>
{% endfor %}
{% if custom_admin_footer %}
{{ custom_admin_footer|raw }}
{% else %}
<a href="http://getgrav.org" target="_blank" rel="noopener noreferrer">Grav</a> v<span class="grav-version">{{ constant('GRAV_VERSION') }}</span> - Admin v{{ admin_version }} - {{ "PLUGIN_ADMIN.WAS_MADE_WITH"|tu|lower }} <i class="fa fa-heart-o pulse"></i> {{ "PLUGIN_ADMIN.BY"|tu|lower }} <a href="https://trilby.media" target="_blank" rel="noopener noreferrer">Trilby Media</a>.
{% endif %}
{% if config.plugins.admin.logo_text %}
<h1 class="text-logo">
{{ config.plugins.admin.logo_text }}
</h1>
{% else %}
<h1 class="{{ custom_login_logo ? 'custom-logo' : 'default-logo' }}">
{{ title }}
{% if custom_login_logo %}
<img src="{{ url(custom_login_logo) }}" title="Login" />
{% else %}
{% include('@admin-images/logo.svg') %}
{% endif %}
</h1>
{% endif %}
<style>
.error-message {
background-color: #fce4e4;
border: 1px solid #fcc2c3;
width: 100%;
padding: 20px 30px;
}
.error-text {
color: #cc0033;
font-family: Helvetica, Arial, sans-serif;
width: 100%;
font-weight: bold;
line-height: 20px;
text-shadow: 1px 1px rgba(250,250,250,.3);
}
.full-height {
display: block;
width: 100%;
height: 100%;
}
</style>
<div id="noscript" class="full-height">
<main id="admin-main" class="full-height">
<div id="titlebar" class="titlebar">
<h1><i class="fa fa-fw fa-exclamation-triangle"></i>{{ "PLUGIN_ADMIN.ERROR"|tu }}</h1>
</div>
<div class="full-height">
<div class="error-message {% if config.plugins.admin.content_padding %}content-padding{% endif %}">
<span class="error-text">{{ "PLUGIN_ADMIN.ADMIN_NOSCRIPT_MSG"|tu }}</span>
</div>
</div>
</main>
</div>
<script type="text/javascript">
function checkjs() {
var element = document.getElementById("noscript");
element.style.display = 'none';
}
checkjs();
</script>
{% for entry_id, entry in notifications %}
<div class="alert {{ entry.type }} position-dashboard">
<a href="#" data-notification-action="hide-notification" data-notification-id="{{ entry_id }}" class="close hide-notification"><i class="fa fa-close"></i></a>
{{ entry.message|raw }}
</div>
{% endfor %}
{% for entry in notifications %}
<li class="single-notification {{ entry.type }}-notification"><span class="badge alert {{ entry.type }}">{{ entry.type|capitalize }}</span><a target="_blank" href="{{ entry.link }}" title="{{ entry.message|striptags|e('html_attr') }}">{{ entry.message|raw }}</a></li>
{% endfor %}
{% for entry_id, entry in notifications %}
<div class="alert {{ entry.type }} position-plugins">
<a href="#" data-notification-action="hide-notification" data-notification-id="{{ entry_id }}" class="close hide-notification"><i class="fa fa-close"></i></a>
{{ entry.message|raw }}
</div>
{% endfor %}
{% for entry_id, entry in notifications %}
<div class="alert {{ entry.type }} position-themes">
<a href="#" data-notification-action="hide-notification" data-notification-id="{{ entry_id }}" class="close hide-notification"><i class="fa fa-close"></i></a>
{{ entry.message|raw }}
</div>
{% endfor %}
{% for entry_id, entry in notifications %}
<div class="alert {{ entry.type }} position-top">
<a href="#" data-notification-action="hide-notification" data-notification-id="{{ entry_id }}" class="close hide-notification"><i class="fa fa-close"></i></a>
{{ entry.message|raw }}
</div>
{% endfor %}
<div class="button-bar">
<a class="button" href="{{ base_url }}"><i class="fa fa-reply"></i> {{ "PLUGIN_ADMIN.BACK"|tu }}</a>
{% include 'partials/backups-button.html.twig' %}
<button class="button" type="submit" name="task" value="save" form="blueprints"><i class="fa fa-check"></i> {{ "PLUGIN_ADMIN.SAVE"|tu }}</button>
</div>
<h1><i class="fa fa-fw fa-briefcase"></i> {{ "PLUGIN_ADMIN.TOOLS"|tu }} - {{ "PLUGIN_ADMIN.BACKUPS"|tu }}</h1>
<div class="backups-content">
{% set data = admin.data('config/backups') %}
{% set backups = grav.backups.getAvailableBackups() %}
{% set profiles = grav.backups.getBackupProfiles() %}
{% set purge_config = grav.backups.getPurgeConfig() %}
{% set newest_date = (backups|first).date %}
{% set newest_backup = newest_date ? newest_date|nicetime(false, false) : 'none' %}
{% set oldest_date = (backups|last).date %}
{% set oldest_backup = oldest_date ? oldest_date|nicetime(false, false) : 'none' %}
{% switch purge_config.trigger %}
{% case 'number' %}
{% set count = backups|count %}
{% set percent_used = count == 0 ? 0 : 100 - (count / purge_config.max_backups_count * 100) %}
{% set bar_msg = "PLUGIN_ADMIN.BACKUPS_PURGE_NUMBER"|tu([count, purge_config.max_backups_count]) %}
{% case 'time' %}
{% set last = backups|last %}
{% set days = last == null ? 0 : (date('now')).diff(last.time).days %}
{% set percent_used = days == 0 ? 0 : 100 - (days / purge_config.max_backups_time * 100) %}
{% set bar_msg = "PLUGIN_ADMIN.BACKUPS_PURGE_TIME"|tu([(purge_config.max_backups_time - days)]) %}
{% default %}
{% set space_used = grav.backups.getTotalBackupsSize() %}
{% set space_available = purge_config.max_backups_space * 1024 * 1024 * 1024 %}
{% set percent_used = space_used == 0 ? 0 : 100 - (space_used / space_available * 100) %}
{% set bar_msg = "PLUGIN_ADMIN.BACKUPS_PURGE_SPACE"|tu([space_used|nicefilesize, space_available|nicefilesize]) %}
{% endswitch %}
<div id="admin-dashboard">
<div id="backups-stats" class="dashboard-item">
<div class="primary-accent default-box-shadow">
<h1>{{ "PLUGIN_ADMIN.BACKUPS_STATS"|tu }}</h1>
<div class="admin-statistics-chart">
<div class="stats-info">
<div id="backups-usage">
<div class="backups-usage-wrapper">
{% if percent_used >= 100 %}
<div class="usage full"></div>
{% else %}
<div class="usage" style="width:{{ percent_used }}%"></div>
{% endif %}
</div>
<h1>{{ bar_msg }}</h1>
</div>
</div>
<div class="flush-bottom button-bar stats-bar">
<span class="stat">
<b>{{ backups|length }}</b>
<i>{{ "PLUGIN_ADMIN.BACKUPS_COUNT"|tu }}</i>
</span>
<span class="stat">
<b>{{ profiles|count }}</b>
<i>{{ "PLUGIN_ADMIN.BACKUPS_PROFILES_COUNT"|tu }}</i>
</span>
<span class="stat">
<b>{{ newest_backup }}</b>
<i>{{ "PLUGIN_ADMIN.BACKUPS_NEWEST"|tu }}</i>
</span>
<span class="stat">
<b>{{ oldest_backup }}</b>
<i>{{ "PLUGIN_ADMIN.BACKUPS_OLDEST"|tu }}</i>
</span>
</div>
</div>
</div>
</div>
</div>
{% include 'partials/blueprints.html.twig' with { blueprints: data.blueprints, data: data } %}
{% include 'partials/modal-changes-detected.html.twig' %}
</div>
<div class="logs-content">
{% macro render_select(name, options, selected, autokey=false) %}
<div class="forms-select-wrapper">
<select class="form-select" name="{{ name }}" data-grav-selectize>
{% for key,option in options %}
{% if autokey %}
{% set key = key|of_type('int') ? option|lower : key %}
{% endif %}
<option value="{{ key }}" {{ key == selected ? ' selected' : '' }}>{{ option|titleize }}</option>
{% endfor %}
</select>
</div>
{% endmacro %}
{% import _self as macro %}
{% set file = grav.uri.query('log') ?: 'grav' %}
{% set verbose = grav.uri.query('verbose') == 'true' ? true : false %}
{% set lines = grav.uri.query('lines') ?: 20 %}
{% set logfile = grav.locator.findResource("log://" ~ file ~ '.log') %}
{% set logs = logviewer.objectTail(logfile, lines|int, false) %}
<div class="logs-output">
<form id="logs-form">
<div class="block block-select">
<div class="form-field">
<div class="form-data">
{% set log_files = config.plugins.admin.log_viewer_files|default(['grav','email']) %}
{% set lines_list = {10:'10 entries', 25:'25 entries', 50:'50 entries', 100:'100 entries', 200:'200 entries', 500:'500 entries'} %}
{{ macro.render_select('log', log_files, file, true) }}
{{ macro.render_select('verbose', {'false':'Essential Output', 'true':'Verbose Output'}, verbose) }}
{{ macro.render_select('lines', lines_list, lines) }}
</div>
</div>
</div>
</form>
<h1>{{ file|titleize }} Log File</h1>
<h3>Display the {{ lines }} most recent entries...</h3>
<table class="noflex">
<thead>
<tr>
<th class="date">Date</th>
<th class="level">Level</th>
<th class="message">Message</th>
</tr>
</thead>
<tbody>
{% for log in logs %}
<tr>
<td class="date">{{ log.date|date }}</td>
<td class="level"><span class="badge {{ log.level|lower }}">{{ log.level }}</span></td>
<td class="message">{{ log.message }}</td>
{% if verbose %}
</tr>
<tr class="trace">
<td colspan="2">&nbsp;</td>
<td>
<div class="overflow">
<ol>
{% for tracerow in log.trace %}
<li><code>{{ tracerow }}</code></li>
{% endfor %}
</ol>
</div>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="reports-content">
{% set reports = admin.generateReports() %}
<div class="report-output">
{% for title, report in reports %}
<h1>{{ title }}</h1>
{{ report|raw }}
{% endfor %}
</div>
{% include 'partials/modal-changes-detected.html.twig' %}
</div>
<div class="button-bar">
<a class="button" href="{{ base_url }}"><i class="fa fa-reply"></i> {{ "PLUGIN_ADMIN.BACK"|tu }}</a>
<button class="button" type="submit" name="task" value="save" form="blueprints"><i class="fa fa-check"></i> {{ "PLUGIN_ADMIN.SAVE"|tu }}</button>
</div>
<h1><i class="fa fa-fw fa-briefcase"></i> {{ "PLUGIN_ADMIN.TOOLS"|tu }} - {{ "PLUGIN_ADMIN.SCHEDULER"|tu }}</h1>
<div class="scheduler-content">
{% set data = admin.data('config/scheduler') %}
{% set cron_status = grav.scheduler.isCrontabSetup()%}
{% if cron_status == 1 %}
<div class="alert notice secondary-accent">
<div id="show-instructions" class="button button-small"><i class="fa fa-clock-o"></i> {{ "PLUGIN_ADMIN.SCHEDULER_INSTALL_INSTRUCTIONS"|tu }}</div>
<i class="fa fa-check"></i> {{ "PLUGIN_ADMIN.SCHEDULER_INSTALLED_READY"|tu }}
</div>
{% elseif cron_status == 2 %}
<div class="alert warning"> {{ "PLUGIN_ADMIN.SCHEDULER_CRON_NA"|tu }}</div>
{% else %}
<div class="alert warning"> {{ "PLUGIN_ADMIN.SCHEDULER_NOT_ENABLED"|tu }}</div>
{% endif %}
<div id="cron-install" class="form-border overlay {{ cron_status == 1 ? 'hide' : ''}}">
<pre><code>{{- grav.scheduler.getCronCommand()|trim -}}</code></pre>
<p>{{ "PLUGIN_ADMIN.SCHEDULER_POST_INSTRUCTIONS"|tu|raw }}</p>
</div>
{% include 'partials/blueprints.html.twig' with { blueprints: data.blueprints, data: data } %}
{% include 'partials/modal-changes-detected.html.twig' %}
<script>
$('#show-instructions').click(function() {
$('#cron-install').toggleClass( "hide" );
});
</script>
</div>
{% if result %}
<div class="alert warning"> Security Scan complete: <strong>{{ result|length }} potential XSS issues found...</strong></div>
<table>
{% for route, results in result %}
<tr>
<td><i class="fa fa-file-text-o"></i> <a href="{{ base_url ~ '/pages' ~ route }}">{{ route }}</a></td>
{% set results_string = [] %}
{% for key, value in results %}
{% set results_string = results_string|merge(['<span class="key">' ~ key ~ '</span>: <span class="value">' ~ value|titleize ~ '</span>']) %}
{% endfor %}
<td class="double">{{ results_string|join(', ')|raw }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<div class="alert info">Security Scan complete: <strong>No issues found...</strong></div>
{% endif %}
{% if result %}
<div class="alert warning"><strong>YAML Linting:</strong> Found <strong>{{ result|length }}</strong> linting errors</div>
<table>
{% for path, error in result %}
{% set page_path = base_path ~ (path|pathinfo).dirname %}
{% set page = grav.pages.get(page_path) %}
<tr>
<td><i class="fa fa-file-text-o"></i>
{% if page.url %}
<a href="{{ base_url ~ '/pages' ~ page.route }}/mode:expert">{{ page.route }}</a>
{% else %}
{{ path }}
{% endif %}
</td>
<td class="double">{{ error }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<div class="alert info"><strong>YAML Linting:</strong> No errors found.</div>
{% endif %}
The MIT License (MIT)
Copyright (c) 2015 Frederic Guillot
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.
<?php
namespace PicoFeed;
use PicoFeed\Config\Config;
use PicoFeed\Logging\Logger;
/**
* Base class
*
* @package PicoFeed
* @author Frederic Guillot
*/
abstract class Base
{
/**
* Config class instance
*
* @access protected
* @var \PicoFeed\Config\Config
*/
protected $config;
/**
* Constructor.
*
* @param \PicoFeed\Config\Config $config Config class instance
*/
public function __construct(Config $config = null)
{
$this->config = $config ?: new Config();
Logger::setTimezone($this->config->getTimezone());
}
public function setConfig(Config $config) {
$this->config = $config;
}
}
<?php
namespace PicoFeed\Client;
use PicoFeed\PicoFeedException;
/**
* ClientException Exception.
*
* @author Frederic Guillot
*/
abstract class ClientException extends PicoFeedException
{
}
<?php
namespace PicoFeed\Client;
/**
* @author Bernhard Posselt
*/
class ForbiddenException extends ClientException
{
}
<?php
namespace PicoFeed\Client;
use ArrayAccess;
use PicoFeed\Logging\Logger;
/**
* Class to handle HTTP headers case insensitivity.
*
* @author Bernhard Posselt
* @author Frederic Guillot
*/
class HttpHeaders implements ArrayAccess
{
private $headers = array();
public function __construct(array $headers)
{
foreach ($headers as $key => $value) {
$this->headers[strtolower($key)] = $value;
}
}
public function offsetGet($offset)
{
return $this->offsetExists($offset) ? $this->headers[strtolower($offset)] : '';
}
public function offsetSet($offset, $value)
{
$this->headers[strtolower($offset)] = $value;
}
public function offsetExists($offset)
{
return isset($this->headers[strtolower($offset)]);
}
public function offsetUnset($offset)
{
unset($this->headers[strtolower($offset)]);
}
/**
* Parse HTTP headers.
*
* @static
*
* @param array $lines List of headers
*
* @return array
*/
public static function parse(array $lines)
{
$status = 0;
$headers = array();
foreach ($lines as $line) {
if (strpos($line, 'HTTP/1') === 0) {
$headers = array();
$status = (int) substr($line, 9, 3);
} elseif (strpos($line, ': ') !== false) {
list($name, $value) = explode(': ', $line);
if ($value) {
$headers[trim($name)] = trim($value);
}
}
}
Logger::setMessage(get_called_class().' HTTP status code: '.$status);
foreach ($headers as $name => $value) {
Logger::setMessage(get_called_class().' HTTP header: '.$name.' => '.$value);
}
return array($status, new self($headers));
}
}
<?php
namespace PicoFeed\Client;
/**
* InvalidCertificateException Exception.
*
* @author Frederic Guillot
*/
class InvalidCertificateException extends ClientException
{
}
<?php
namespace PicoFeed\Client;
/**
* InvalidUrlException Exception.
*
* @author Frederic Guillot
*/
class InvalidUrlException extends ClientException
{
}
<?php
namespace PicoFeed\Client;
/**
* MaxRedirectException Exception.
*
* @author Frederic Guillot
*/
class MaxRedirectException extends ClientException
{
}
<?php
namespace PicoFeed\Client;
/**
* MaxSizeException Exception.
*
* @author Frederic Guillot
*/
class MaxSizeException extends ClientException
{
}
<?php
namespace PicoFeed\Client;
use PicoFeed\Logging\Logger;
/**
* Stream context HTTP client.
*
* @author Frederic Guillot
*/
class Stream extends Client
{
/**
* Prepare HTTP headers.
*
* @return string[]
*/
private function prepareHeaders()
{
$headers = array(
'Connection: close',
'User-Agent: '.$this->user_agent,
);
// disable compression in passthrough mode. It could result in double
// compressed content which isn't decodeable by browsers
if (function_exists('gzdecode') && !$this->isPassthroughEnabled()) {
$headers[] = 'Accept-Encoding: gzip';
}
if ($this->etag) {
$headers[] = 'If-None-Match: '.$this->etag;
$headers[] = 'A-IM: feed';
}
if ($this->last_modified) {
$headers[] = 'If-Modified-Since: '.$this->last_modified;
}
if ($this->proxy_username) {
$headers[] = 'Proxy-Authorization: Basic '.base64_encode($this->proxy_username.':'.$this->proxy_password);
}
if ($this->username && $this->password) {
$headers[] = 'Authorization: Basic '.base64_encode($this->username.':'.$this->password);
}
$headers = array_merge($headers, $this->request_headers);
return $headers;
}
/**
* Construct the final URL from location headers.
*
* @param array $headers List of HTTP response header
*/
private function setEffectiveUrl($headers)
{
foreach ($headers as $header) {
if (stripos($header, 'Location') === 0) {
list(, $value) = explode(': ', $header);
$this->url = Url::resolve($value, $this->url);
}
}
}
/**
* Prepare stream context.
*
* @return array
*/
private function prepareContext()
{
$context = array(
'http' => array(
'method' => 'GET',
'protocol_version' => 1.1,
'timeout' => $this->timeout,
'max_redirects' => $this->max_redirects,
),
);
if ($this->proxy_hostname) {
Logger::setMessage(get_called_class().' Proxy: '.$this->proxy_hostname.':'.$this->proxy_port);
$context['http']['proxy'] = 'tcp://'.$this->proxy_hostname.':'.$this->proxy_port;
$context['http']['request_fulluri'] = true;
if ($this->proxy_username) {
Logger::setMessage(get_called_class().' Proxy credentials: Yes');
} else {
Logger::setMessage(get_called_class().' Proxy credentials: No');
}
}
$context['http']['header'] = implode("\r\n", $this->prepareHeaders());
return $context;
}
/**
* Do the HTTP request.
*
* @return array HTTP response ['body' => ..., 'status' => ..., 'headers' => ...]
* @throws InvalidUrlException
* @throws MaxSizeException
* @throws TimeoutException
*/
public function doRequest()
{
$body = '';
// Create context
$context = stream_context_create($this->prepareContext());
// Make HTTP request
$stream = @fopen($this->url, 'r', false, $context);
if (!is_resource($stream)) {
throw new InvalidUrlException('Unable to establish a connection');
}
// Get HTTP headers response
$metadata = stream_get_meta_data($stream);
list($status, $headers) = HttpHeaders::parse($metadata['wrapper_data']);
if ($this->isPassthroughEnabled()) {
header(':', true, $status);
if (isset($headers['Content-Type'])) {
header('Content-Type: '.$headers['Content-Type']);
}
fpassthru($stream);
} else {
// Get the entire body until the max size
$body = stream_get_contents($stream, $this->max_body_size + 1);
// If the body size is too large abort everything
if (strlen($body) > $this->max_body_size) {
throw new MaxSizeException('Content size too large');
}
if ($metadata['timed_out']) {
throw new TimeoutException('Operation timeout');
}
}
fclose($stream);
$this->setEffectiveUrl($metadata['wrapper_data']);
return array(
'status' => $status,
'body' => $this->decodeBody($body, $headers),
'headers' => $headers,
);
}
/**
* Decode body response according to the HTTP headers.
*
* @param string $body Raw body
* @param HttpHeaders $headers HTTP headers
*
* @return string
*/
public function decodeBody($body, HttpHeaders $headers)
{
if (isset($headers['Transfer-Encoding']) && $headers['Transfer-Encoding'] === 'chunked') {
$body = $this->decodeChunked($body);
}
if (isset($headers['Content-Encoding']) && $headers['Content-Encoding'] === 'gzip') {
$body = gzdecode($body);
}
return $body;
}
/**
* Decode a chunked body.
*
* @param string $str Raw body
*
* @return string Decoded body
*/
public function decodeChunked($str)
{
for ($result = ''; !empty($str); $str = trim($str)) {
// Get the chunk length
$pos = strpos($str, "\r\n");
$len = hexdec(substr($str, 0, $pos));
// Append the chunk to the result
$result .= substr($str, $pos + 2, $len);
$str = substr($str, $pos + 2 + $len);
}
return $result;
}
}
<?php
namespace PicoFeed\Client;
/**
* TimeoutException Exception.
*
* @author Frederic Guillot
*/
class TimeoutException extends ClientException
{
}
<?php
namespace PicoFeed\Client;
/**
* @author Bernhard Posselt
*/
class UnauthorizedException extends ClientException
{
}
<?php
namespace PicoFeed\Encoding;
/**
* Encoding class.
*/
class Encoding
{
public static function convert($input, $encoding)
{
if ($encoding === 'utf-8' || $encoding === '') {
return $input;
}
// suppress all notices since it isn't possible to silence only the
// notice "Wrong charset, conversion from $in_encoding to $out_encoding is not allowed"
set_error_handler(function () {}, E_NOTICE);
// convert input to utf-8 and strip invalid characters
$value = iconv($encoding, 'UTF-8//IGNORE', $input);
// stop silencing of notices
restore_error_handler();
// return input if something went wrong, maybe it's usable anyway
if ($value === false) {
return $input;
}
return $value;
}
}
<?php
namespace PicoFeed\Generator;
use PicoFeed\Parser\Item;
/**
* Content Generator Interface
*
* @package PicoFeed\Generator
* @author Frederic Guillot
*/
interface ContentGeneratorInterface
{
/**
* Execute Content Generator
*
* @access public
* @param Item $item
* @return boolean
*/
public function execute(Item $item);
}
<?php
namespace PicoFeed\Generator;
use PicoFeed\Base;
use PicoFeed\Parser\Item;
/**
* File Content Generator
*
* @package PicoFeed\Generator
* @author Frederic Guillot
*/
class FileContentGenerator extends Base implements ContentGeneratorInterface
{
private $extensions = array('pdf');
/**
* Execute Content Generator
*
* @access public
* @param Item $item
* @return boolean
*/
public function execute(Item $item)
{
foreach ($this->extensions as $extension) {
if (substr($item->getUrl(), - strlen($extension)) === $extension) {
$item->setContent('<a href="'.$item->getUrl().'" target="_blank">'.$item->getUrl().'</a>');
return true;
}
}
return false;
}
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment