Typesetter/include/admin/Notifications.php
2022-05-04 12:41:57 +02:00

959 lines
23 KiB
PHP

<?php
namespace gp\admin{
defined('is_running') or die('Not an entry point...');
class Notifications{
public $notifications = [];
public $filters = [];
private $debug = false;
private static $singleton;
public function __construct(){
global $gpAdmin;
self::$singleton = $this;
// get all notifications
$this->CheckNotifications();
$this->ApplyFilters();
// Get active filters from the admin sessio
if( !empty($gpAdmin['notification_filters']) ){
$this->filters = $gpAdmin['notification_filters'];
}elseif( isset($gpAdmin['notifications']['filters']) ){
$this->filters = $gpAdmin['notifications']['filters'];
unset($gpAdmin['notifications']);
}
}
public static function GetSingleton(){
if( !self::$singleton ){
new self();
}
}
/**
* Outputs a list of notifications
* To be rendered in a gpabox
*
*/
public function ListNotifications(){
global $langmessage;
$this->FilterUserDefined();
$this->debug('$notifications = ' . pre($this->notifications));
echo '<div class="inline_box show-notifications-box">';
$filter_list_by = '';
if( isset($_REQUEST['type']) ){
$filter_list_by = rawurldecode($_REQUEST['type']);
$this->Tabs($filter_list_by);
}
foreach( $this->notifications as $type => $notification ){
if( empty($filter_list_by) ){
$title = $this->GetTitle($notification['title']);
echo '<h3>' . $title . '</h3>';
}elseif( $type != $filter_list_by ){
continue;
}
echo '<table class="bordered full_width">';
echo '<tbody>';
echo '<tr>';
echo '<th>' . $langmessage['Item'] . '</th>';
echo '<th>' . $langmessage['options'] . '</th>';
echo '<th style="text-align:right;">' . $langmessage['Visibility'] . '</th>';
echo '</tr>';
foreach( $notification['items'] as $id => $item ){
$tr_class = '';
$link_icon = '<i class="fa fa-bell"></i>';
$link_title = $langmessage['Hide'];
if( $item['priority'] < 0 ){
$tr_class = ' class="notification-item-muted"';
$link_icon = '<i class="fa fa-bell-slash"></i>';
$link_title = $langmessage['Show'];
}
echo '<tr' . $tr_class . '>';
echo '<td>' . $item['label'] . '</td>';
echo '<td>' . $item['action'] . '</td>';
echo '<td style="text-align:right;">';
echo \gp\tool::Link(
'Admin/Notifications/Manage',
$link_icon,
'cmd=toggle_priority'
. '&id=' . rawurlencode($id)
. '&type=' . rawurlencode($filter_list_by),
array(
'title' => $link_title,
'class' => 'toggle-notification',
'data-cmd' => 'gpabox',
)
);
echo '</td>';
echo '</tr>';
}
echo '</table>';
}
echo '<p>';
echo '<button class="admin_box_close gpcancel">';
echo $langmessage['Close'];
echo '</button>';
echo '</p>';
echo '</div>';
}
/**
* Tabs
* Output tabs to navigate between notifications
* @param string $filter_list_by tab type to be active
*
*/
public function Tabs($filter_list_by){
echo '<div class="gp_tabs">';
foreach( $this->notifications as $type => $notification ){
$class = '';
if( $filter_list_by && $type == $filter_list_by ){
$class = 'selected';
}
echo \gp\tool::Link(
'Admin/Notifications',
$this->GetTitle($notification['title']) . ' ('.$notification['count'].')',
'cmd=ShowNotifications&type=' . rawurlencode($type),
array(
'title' => $this->GetTitle($notification['title']),
'class' => $class,
'style' => 'margin-right:0.5em;',
'data-cmd' => 'gpabox',
)
);
}
echo '</div>';
}
/**
* Notification Title
* @param string $title
* @return string $title translation if available
*
*/
public function GetTitle($title){
global $langmessage;
if( isset($langmessage[$title]) ){
return $langmessage[$title];
}
return htmlspecialchars($title);
}
/**
* Manage Notifications
* Set display filters and priority for notification items by $_REQUEST
*
*/
public function ManageNotifications(){
global $gpAdmin;
$cmd = \gp\tool::GetCommand();
switch( $cmd ){
case 'toggle_priority':
if( !empty($_REQUEST['id']) ){
$this->SetFilter($_REQUEST['id'], 'toggle_priority');
}
break;
case 'set_priority':
if( !empty($_REQUEST['id']) &&
isset($_REQUEST['new_priority']) &&
is_numeric($_REQUEST['new_priority'])
){
$this->SetFilter($_REQUEST['id'], 'set_priority', $_REQUEST['new_priority']);
}
break;
}
$gpAdmin['notification_filters'] = array_filter($this->filters); // Save filters to the admin session
$this->ListNotifications();
self::UpdateNotifications();
}
/**
* Set a single filter
* @param string $id of notification item to be modified
* @param string $do filter action
* @param string $val filter value
*
* currently implemented actions:
* 'toggle_priority' will toggle priority between -1 (=muted) and it's initial value
* 'set_priority' will set the priority value to $val
*
*/
public function SetFilter($id, $do, $val=false){
// check if id exists in notifications
$id_exists = false;
foreach( $this->notifications as $type => $notification ){
if( array_key_exists($id,$notification['items']) ){
$id_exists = true;
break;
}
}
if( !$id_exists ){
// notification id no longer exists, purge possible stray filter
if( isset($this->filters[$id]) ){
unset($this->filters[$id]);
}
return;
}
switch( $do ){
case 'toggle_priority':
if( isset($this->filters[$id]['priority']) ){
unset($this->filters[$id]['priority']);
}else{
$this->filters[$id]['priority'] = -1;
}
break;
case 'set_priority':
$this->filters[$id]['priority'] = (int)$val;
return;
}
$this->debug(
'Notifications SetFilter Error: unknown command "'
. htmlspecialchars($do) . '"'
);
}
/**
* Apply filters to the notifications array
* Remove inapropriate items for users lacking permissions to deal with them
* Apply user defined (display) filters
*
*/
public function ApplyFilters(){
global $gpAdmin;
// Remove items lacking user permissions and therefore cannot be dealt with anyway
// deprecated addons
if( !\gp\admin\Tools::HasPermission('Admin/Addons') ){
$this->FilterType('Deprecated Addons','deprecated_addons');
}
// debug / development
if( $gpAdmin['granted'] != 'all' || $gpAdmin['editing'] != 'all' ){
$this->FilterType('Development','superuser');
}
// extra content draft
if( !\gp\admin\Tools::HasPermission('Admin/Extra') ){
$this->FilterType('Working Drafts','extra');
}
// theme update
if( !\gp\admin\Tools::HasPermission('Admin_Theme_Content/Remote') ){
$this->FilterType('updates','theme');
}
// addon update
if( !\gp\admin\Tools::HasPermission('Admin/Addons/Remote') ){
$this->FilterType('updates','plugin');
}
// core update
if( !\gp\admin\Tools::HasPermission('Admin/Uninstall') ){ // can't find a permission for core updates so I use Uninstall
$this->FilterType('updates','core');
}
// page draft
$this->FilterCallback('Working Drafts',function($item){
if( $item['type'] == 'page' && !\gp\admin\Tools::CanEdit($item['title']) ){
return true;
}
return false;
});
// private page
$this->FilterCallback('Private Pages',function($item){
if( !\gp\admin\Tools::CanEdit($item['title']) ){
return true;
}
return false;
});
}
/**
* Apply user defined (display) filters
* Remove empty, count items, and get priority
*
*/
public function FilterUserDefined(){
foreach( $this->notifications as $notification_type => &$notification ){
if( empty($this->notifications[$notification_type]['items']) ){
unset($this->notifications[$notification_type]);
continue;
}
$count = 0;
$priority = 0;
$total_priority = 0;
foreach( $notification['items'] as $id => &$item ){
$total_priority = max( $item['priority'], $total_priority );
if( isset($this->filters[$id]) ){
$item = $this->filters[$id] + $item;
}
if( $item['priority'] > 0 ){
$priority = max( $item['priority'], $priority );
$count++;
}
}
$notification['count'] = $count;
$notification['priority'] = $priority;
$notification['total_priority'] = $total_priority;
}
// sort by priority
uasort($this->notifications,function($a,$b){
if( $b['priority'] !== $a['priority'] ){
return strnatcmp($b['priority'],$a['priority']);
}
if( $b['total_priority'] !== $a['total_priority'] ){
return strnatcmp($b['total_priority'],$a['total_priority']);
}
return strnatcmp($b['title'],$a['title']);
});
}
/**
* Filter notifications matching a notification type and item type
*
*/
public function FilterType( $notification_type, $item_type ){
if( !isset($this->notifications[$notification_type]) ){
return;
}
foreach( $this->notifications[$notification_type]['items'] as $id => $item ){
if( $item['type'] !== $item_type ){
continue;
}
unset($this->notifications[$notification_type]['items'][$id]);
}
}
/**
* Filter notifications matching a notification type with a callback
*
*/
public function FilterCallback( $notification_type, $callback ){
if( !isset($this->notifications[$notification_type]) ){
return;
}
foreach( $this->notifications[$notification_type]['items'] as $id => $item ){
if( $callback($item) === true ){
unset($this->notifications[$notification_type]['items'][$id]);
}
}
}
/**
* Aggregate all sources of notifications
*
*/
public function CheckNotifications(){
$this->notifications = array();
$items = $this->GetDrafts();
$this->Add('Working Drafts', $items, '#329880');
$items = $this->GetPrivatePages();
$this->Add('Private Pages', $items, '#ad5f45');
$items = $this->GetUpdatesNotifications();
$this->Add('updates', $items, '#3153b7');
$items = $this->GetDeprecatedAddons();
$this->Add('Deprecated Addons', $items, '#d61b1b');
$items = $this->GetDebugNotifications();
$this->Add('Development', $items, '#ff8c00', '#000');
\gp\tool\Plugins::Action('Notifications',[$this]);
}
/**
* Add Notifications
* @param string $title will sho up as panelgroup submenu item
* @param array $items see below
* @param string $bg (optional) badge background color (any valid css color value)
* @param string $color (optional) badge text color (any valid color value), use a dark color on light $bg
*
* $items are associative arrays containing the following keys:
* 'label' => (string) text or html, first element of the item row
* 'id' => (string) arbitrary but permanently unique string or number (required for user defined filtering)
* 'priority' => (integer) a number above 0, determines the ranking amongst other notifications. Anything above 100 is high-priority
* 'action' => (string) plain text or html describing and/or linking to possible solutions
* 'type' => (string) optional, only relevant for automatic system-internal filters
*
*/
public function Add( $title, $items, $bg = '#555', $color = '#fff'){
if( empty($items) ){
return;
}
if( !isset($this->notifications[$title]) ){
$this->notifications[$title] = [
'title' => $title,
'badge_bg' => $bg,
'badge_color' => $color,
'items' => [],
];
}
foreach( $items as $item ){
if( !isset($item['id']) ){
trigger_error('id not set for notification '.pre($item)); // should we create an id?
continue;
}
$item += ['priority'=>0];
$item['priority'] = (int)$item['priority'];
$id = hash('crc32b', $item['id']);
unset($item['id']);
$this->notifications[$title]['items'][$id] = $item;
}
}
/**
* Get Notifications
* Outputs a Notifications panelgroup
* @param boolean $in_panel if panelgroup shall be rendered in admin menu
*
*/
public function GetNotifications($in_panel=true){
global $langmessage;
$this->FilterUserDefined();
if( count($this->notifications) < 1 ){
return;
}
$total_count = 0;
$main_badge_style = '';
$expand_class = ''; // expand_child_click
$badge_format = ' <span class="dashboard-badge">(%2$d)</b>';
$panel_class = '';
$default_style = ['badge_bg'=>'transparent','color'=>'#fff'];
if( $in_panel ){
$badge_format = ' <b class="admin-panel-badge" style="%1$s">%2$d</b>';
$expand_class = 'expand_child';
$panel_class = 'admin-panel-notifications';
}
ob_start();
foreach($this->notifications as $type => $notification ){
if( empty($notification['items']) ){
$this->debug('notification => items subarray mising or empty');
continue;
}
$total_count += $notification['count'];
$title = $this->GetTitle($notification['title']);
$badge_html = '';
$badge_style = '';
$notification += $default_style;
if( $notification['count'] > 0 ){
$badge_style = 'background-color:'.$notification['badge_bg'].';color:'.$notification['badge_color'].';';
$badge_html = sprintf($badge_format, $badge_style, $notification['count']);
}
echo '<li class="' . $expand_class . '">';
echo \gp\tool::Link(
'Admin/Notifications',
$title . $badge_html,
'cmd=ShowNotifications&type=' . rawurlencode($type),
array(
'title' => $notification['count'] . ' ' . $title,
'class' => 'admin-panel-notification', // . '-' . rawurlencode($type)',
'data-cmd' => 'gpabox',
)
);
echo '</li>';
if( empty($main_badge_style) ){
$main_badge_style = $badge_style;
}
}
$links = ob_get_clean();
$panel_label = $langmessage['Notifications'];
$badge_html = $total_count > 0 ?
'<b class="admin-panel-badge" style="' . $main_badge_style . '">' . $total_count . '</b>' :
'';
\gp\Admin\Tools::_AdminPanelLinks(
$in_panel,
$links,
$panel_label,
'fa fa-bell',
'notifications',
$panel_class, // new param 'class'
$badge_html // new param 'badge'
);
}
/**
* Get a Notification array for deprecatd addons as defined in /gpconfig.php
* @return array $deprecated_note single notification array
*
*/
public function GetDeprecatedAddons(){
global $config, $deprecated_addons, $langmessage;
$deprecated_note = array();
if( !\notify_deprecated || !is_array($config['addons']) || empty($deprecated_addons) ){
return $deprecated_note;
}
foreach($deprecated_addons as $deprec_addon_name => $deprec_addon_data){
if( empty($deprec_addon_data['notify']) ){
continue;
}
foreach( $config['addons'] as $inst_addon_key => $inst_addon_data ){
if($deprec_addon_name != ($inst_addon_data['name'] ?? null)){
// addon is not installed
continue;
}
$version_affected = empty($deprec_addon_data['upto_version']) ||
$deprec_addon_data['upto_version'] == 'all' ||
version_compare($deprec_addon_data['upto_version'], $inst_addon_data['version'], '<=');
if( !$version_affected ){
continue;
}
$label = $deprec_addon_name;
$uninstall_link = \gp\tool::Link(
'Admin/Addons',
$langmessage['uninstall'],
'cmd=uninstall&addon=' . $inst_addon_data['data_folder'],
array(
'data-cmd' => 'gpabox',
'title' => $langmessage['uninstall']
)
);
$action = empty($deprec_addon_data['reason']) ?
$uninstall_link :
$deprec_addon_data['reason'] . ' ' . $uninstall_link;
$deprecated_note[] = array(
'type' => 'deprecated_addons',
'label' => $label,
'id' => 'deprecated addon ' . $label,
'priority' => 200, // that's a rather high priority
'action' => $action,
);
}
}
return $deprecated_note;
}
/**
* Get a Notification array for debugging / development relevant information
* @return array $debug_note single notification array
*
*/
public function GetDebugNotifications(){
global $langmessage;
$debug_note = array();
if( ini_get('display_errors') ){
$label = '<strong>ini_set(display_errors,' . htmlspecialchars(ini_get('display_errors')) . ')</strong>';
$debug_note[] = array(
'type' => 'any_user',
'label' => $label,
// Adding the server name to the id makes sure that it will change when moving the site (e.g. when going live)
// Thus possible set hide-filters will invalidate and the warning will show up again.
'id' => $label . \gp\tool::ServerName(),
'priority' => 500, // that's a high priority
'action' => 'edit gpconfig.php or notify administrator! <br/>This should only be enabled in exceptional cases.',
);
}
if( \gpdebug ){
$label = 'gpdebug is enabled';
$debug_note[] = array(
'type' => 'superuser',
'label' => $label,
'id' => $label . \gp\tool::ServerName(),
'priority' => 75,
'action' => 'edit gpconfig.php',
);
}
if( \create_css_sourcemaps ){
$label = 'create_css_sourcemaps is enabled';
$debug_note[] = array(
'type' => 'superuser',
'label' => $label,
'id' => $label . \gp\tool::ServerName(),
'priority' => 75,
'action' => 'edit gpconfig.php',
);
}
return $debug_note;
}
/**
* Convert $new_versions to notification array
* @return array $updated single notification array containing possible updates
*
*/
public function GetUpdatesNotifications(){
global $langmessage;
$updates = array();
if( gp_remote_update && isset(\gp\Admin\Tools::$new_versions['core']) ){
$label = \CMS_NAME . ' ' . \gp\Admin\Tools::$new_versions['core'];
$updates[] = array(
'type' => 'core',
'label' => $label,
'id' => $label,
'priority' => 60,
'action' => '<a href="' . \gp\tool::GetDir('/include/install/update.php') . '">'
. $langmessage['upgrade'] . '</a>',
);
}
foreach(\gp\Admin\Tools::$new_versions as $addon_id => $new_addon_info){
if( !is_numeric($addon_id) ){
continue;
}
$label = $new_addon_info['name'] . ': ' . $new_addon_info['version'];
$url = \gp\admin\Tools::RemoteUrl( $new_addon_info['type'] );
if( $url === false ){
continue;
}
$updates[] = array(
'type' => $new_addon_info['type'],
'label' => $label,
'id' => $label,
'priority' => 60,
'action' => '<a href="' . $url . '/' . $addon_id . '" data-cmd="remote">'
. $langmessage['upgrade'] . '</a>',
);
}
return $updates;
}
/**
* Update the Notifications panelgroup in Admin Menu via AJAX
*
*/
public static function UpdateNotifications(){
global $page;
if( !\gp\admin\Tools::HasPermission('Admin/Notifications') ){
return;
}
self::GetSingleton();
ob_start();
self::$singleton->GetNotifications();
$panelgroup = ob_get_clean();
$page->ajaxReplace[] = array('replace', '.admin-panel-notifications', $panelgroup);
}
/**
* Get information about working drafts
* @return array $drafts single notification array of current working drafts
*
*/
public function GetDrafts(){
global $dataDir, $gp_index, $gp_titles, $langmessage;
$draft_types = array(
'page' => $dataDir . '/data/_pages',
'extra' => $dataDir . '/data/_extra',
);
$drafts = array();
foreach( $draft_types as $type => $dir ){
$folders = \gp\tool\Files::readDir($dir,1);
foreach( $folders as $folder ){
// no draft
$draft_path = $dir . '/' . $folder . '/draft.php';
if( !\gp\tool\Files::Exists($draft_path) ){
continue;
}
// drafts of 'trashed' pages will not count
$deleted_path = $dir . '/' . $folder . '/deleted.php';
if( \gp\tool\Files::Exists($deleted_path) ){
continue;
}
$draft = array(
'type' => $type,
'id' => $folder,
'priority' => 100,
);
switch( $type ){
case 'extra':
$draft['label'] = str_replace('_', ' ', $folder) . ' (' . $langmessage['theme_content'] . ')';
$draft['action'] = \gp\tool::Link(
'Admin/Extra',
$langmessage['theme_content'],
'',
array(
'class' => 'getdrafts-extra-content-link',
'title' => $langmessage['theme_content'],
)
);
$draft['preview_link'] = \gp\tool::Link(
'Admin/Extra',
$langmessage['preview'],
'cmd=PreviewText&file=' . $folder,
array(
'class' => 'getdrafts-extra-preview',
'title' => $langmessage['preview'],
)
);
$draft['publish_link'] = \gp\tool::Link(
'Admin/Extra',
$langmessage['Publish Draft'],
'cmd=PublishDraft&file=' . $folder,
array(
'data-cmd' => 'gpajax',
'class' => 'getdrafts-extra-publish',
'title' => $langmessage['Publish Draft'],
)
);
$draft['folder'] = $folder;
break;
case 'page':
$draft['index'] = substr($folder, strpos($folder, "_") + 1);
$draft['title'] = \gp\tool::IndexToTitle($draft['index']);
$draft['label'] = \gp\tool::GetLabel($draft['title']) . ' (' . $langmessage['Page'] . ')';
$draft['action'] = \gp\tool::Link(
$draft['title'],
$langmessage['view/edit_page'], //$draft['label'],
'',
array(
'class' => 'getdrafts-page-link',
'title' => $langmessage['view/edit_page'],
)
);
$draft['publish_link'] = \gp\tool::Link(
$draft['title'],
$langmessage['Publish Draft'],
'cmd=PublishDraft',
array(
'data-cmd' => 'creq',
'class' => 'getdrafts-page-publish',
'title' => $langmessage['Publish Draft'],
)
);
break;
}
$drafts[] = $draft;
}
}
return $drafts;
}
/**
* Get information of all private (invisible) pages
* @return array $private_pages single notification array of pages currently set as private
*
*/
public function GetPrivatePages(){
global $gp_titles, $langmessage, $page;
$private_pages = array();
foreach( $gp_titles as $index => $title ){
if( !isset($title['vis']) || $title['vis'] !== 'private' ){
continue;
}
$private_page = array(
'index' => $index,
'title' => \gp\tool::IndexToTitle($index),
'id' => 'private_page' . $index,
'priority' => 40,
'label' => \gp\tool::GetLabelIndex($index),
);
// increase priority by 100 when viewing the current page
if( isset($page->gp_index) && $page->gp_index == $index ){
$private_page['priority'] += 100;
}
$private_page['action'] = \gp\tool::Link(
$private_page['title'],
$langmessage['view/edit_page'],
'',
array(
'class' => 'getprivate-page-link',
'title' => $langmessage['view/edit_page'],
)
);
$private_page['make_public_link'] = \gp\tool::Link(
'Admin/Menu/Ajax',
$langmessage['Visibility'] . '<i class="fa fa-long-arrow-right"></i> ' . $langmessage['Public'],
'cmd=ToggleVisibility&index=' . $index,
array(
'data-cmd' => 'postlink',
'class' => 'getprivate-make-public',
'title' => $langmessage['Publish Draft'],
)
);
$private_pages[] = $private_page;
}
return $private_pages;
}
public function debug($msg){
$this->debug && debug($msg);
}
}
}