Files
KGV-PWA/kgv-pwa.php
Ronny Grobel b7a7f173e3 release: 1.0.2
2026-04-20 23:00:42 +02:00

544 lines
16 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Plugin Name: KGV PWA
* Plugin URI: https://wordpress.apex-project.de/
* Description: Progressive Web App Unterstützung fuer KGV-Webseiten Web App Manifest, Service Worker und Install-Banner.
* Version: 1.0.2
* Author: Ronny Grobel
* Author URI: https://apex-project.de/
* Text Domain: kgv-pwa
* Update URI: https://git.apex-project.de/Wordpress_Plugins/KGV-PWA
* Gitea Plugin URI: https://git.apex-project.de/Wordpress_Plugins/KGV-PWA
* Requires Plugins: KGV-Updater
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
final class KGV_PWA_Plugin {
const VERSION = '1.0.2';
const OPTION_KEY = 'kgv_pwa_settings';
const REWRITE_VERSION_OPTION = 'kgv_pwa_rewrite_version';
private static $instance = null;
public static function instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
add_action( 'init', array( $this, 'add_rewrite_rules' ) );
add_action( 'init', array( $this, 'maybe_flush_rewrite_rules' ), 20 );
add_filter( 'query_vars', array( $this, 'add_query_vars' ) );
add_action( 'template_redirect', array( $this, 'handle_manifest' ) );
add_action( 'template_redirect', array( $this, 'handle_service_worker' ) );
add_action( 'wp_head', array( $this, 'output_head_tags' ) );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
add_action( 'admin_menu', array( $this, 'add_settings_page' ) );
add_action( 'admin_init', array( $this, 'register_settings' ) );
add_shortcode( 'kgv_pwa_install_button', array( $this, 'render_install_button' ) );
}
// -------------------------------------------------------------------------
// Activation / Deactivation
// -------------------------------------------------------------------------
public static function activate() {
self::instance()->add_rewrite_rules();
flush_rewrite_rules();
update_option( self::REWRITE_VERSION_OPTION, self::VERSION );
if ( false === get_option( self::OPTION_KEY ) ) {
add_option( self::OPTION_KEY, self::default_settings() );
}
}
public static function deactivate() {
flush_rewrite_rules();
delete_option( self::REWRITE_VERSION_OPTION );
}
public function maybe_flush_rewrite_rules() {
$installed_version = get_option( self::REWRITE_VERSION_OPTION, '' );
if ( self::VERSION === $installed_version ) {
return;
}
flush_rewrite_rules( false );
update_option( self::REWRITE_VERSION_OPTION, self::VERSION );
}
// -------------------------------------------------------------------------
// Rewrite Rules & Query Vars
// -------------------------------------------------------------------------
public function add_rewrite_rules() {
add_rewrite_rule( '^manifest\.json$', 'index.php?kgv_pwa=manifest', 'top' );
add_rewrite_rule( '^sw\.js$', 'index.php?kgv_pwa=sw', 'top' );
}
public function add_query_vars( $vars ) {
$vars[] = 'kgv_pwa';
return $vars;
}
// -------------------------------------------------------------------------
// Manifest Endpoint
// -------------------------------------------------------------------------
public function handle_manifest() {
if ( 'manifest' !== get_query_var( 'kgv_pwa' ) ) {
return;
}
if ( ! is_user_logged_in() ) {
status_header( 403 );
exit;
}
$settings = $this->get_settings();
$manifest = array(
'name' => $settings['app_name'],
'short_name' => $settings['short_name'],
'description' => $settings['description'],
'start_url' => home_url( '/' ),
'scope' => home_url( '/' ),
'display' => $settings['display'],
'background_color' => $settings['background_color'],
'theme_color' => $settings['theme_color'],
'lang' => str_replace( '_', '-', get_locale() ),
'icons' => $this->get_manifest_icons( $settings ),
);
header( 'Content-Type: application/manifest+json; charset=utf-8' );
header( 'Cache-Control: max-age=3600' );
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo wp_json_encode( $manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
exit;
}
private function get_manifest_icons( $settings ) {
$icons = array();
foreach ( array( '192', '512' ) as $size ) {
$key = 'icon_' . $size;
if ( ! empty( $settings[ $key ] ) ) {
$icons[] = array(
'src' => esc_url_raw( $settings[ $key ] ),
'sizes' => $size . 'x' . $size,
'type' => 'image/png',
'purpose' => 'any maskable',
);
}
}
// Fallback: WordPress-Site-Icon
if ( empty( $icons ) ) {
$site_icon_512 = get_site_icon_url( 512 );
$site_icon_192 = get_site_icon_url( 192 );
if ( $site_icon_512 ) {
$icons[] = array(
'src' => $site_icon_512,
'sizes' => '512x512',
'type' => 'image/png',
'purpose' => 'any',
);
}
if ( $site_icon_192 ) {
$icons[] = array(
'src' => $site_icon_192,
'sizes' => '192x192',
'type' => 'image/png',
'purpose' => 'any',
);
}
}
return $icons;
}
// -------------------------------------------------------------------------
// Service Worker Endpoint
// -------------------------------------------------------------------------
public function handle_service_worker() {
if ( 'sw' !== get_query_var( 'kgv_pwa' ) ) {
return;
}
if ( ! is_user_logged_in() ) {
status_header( 403 );
exit;
}
$sw_file = plugin_dir_path( __FILE__ ) . 'assets/js/service-worker.js';
if ( ! file_exists( $sw_file ) ) {
status_header( 404 );
exit;
}
header( 'Content-Type: application/javascript; charset=utf-8' );
header( 'Service-Worker-Allowed: /' );
header( 'Cache-Control: no-cache, no-store, must-revalidate' );
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
readfile( $sw_file );
exit;
}
// -------------------------------------------------------------------------
// Frontend: Head Tags & Script
// -------------------------------------------------------------------------
public function output_head_tags() {
if ( ! is_user_logged_in() ) {
return;
}
$settings = $this->get_settings();
$app_name = esc_attr( $settings['app_name'] );
$theme_hex = esc_attr( $settings['theme_color'] );
$icon_url = ! empty( $settings['icon_192'] ) ? esc_url( $settings['icon_192'] ) : '';
echo '<link rel="manifest" href="' . esc_url( $this->get_manifest_url() ) . '">' . "\n";
echo '<meta name="theme-color" content="' . $theme_hex . '">' . "\n";
echo '<meta name="application-name" content="' . $app_name . '">' . "\n";
echo '<meta name="apple-mobile-web-app-capable" content="yes">' . "\n";
echo '<meta name="apple-mobile-web-app-status-bar-style" content="default">' . "\n";
echo '<meta name="apple-mobile-web-app-title" content="' . $app_name . '">' . "\n";
if ( $icon_url ) {
echo '<link rel="apple-touch-icon" href="' . $icon_url . '">' . "\n";
}
}
private function get_manifest_url() {
// Query-URL funktioniert auch dann, wenn Pretty-Rewrite noch nicht aktiv ist.
return home_url( '/?kgv_pwa=manifest' );
}
private function get_service_worker_url() {
// Query-URL funktioniert auch dann, wenn Pretty-Rewrite noch nicht aktiv ist.
return home_url( '/?kgv_pwa=sw' );
}
public function enqueue_assets() {
if ( ! is_user_logged_in() ) {
return;
}
$sw_url = $this->get_service_worker_url();
$scope = trailingslashit( home_url( '/' ) );
wp_enqueue_style(
'kgv-pwa',
plugin_dir_url( __FILE__ ) . 'assets/css/kgv-pwa.css',
array(),
self::VERSION
);
wp_enqueue_script(
'kgv-pwa-register',
plugin_dir_url( __FILE__ ) . 'assets/js/pwa-register.js',
array(),
self::VERSION,
true
);
wp_localize_script(
'kgv-pwa-register',
'kgvPwaConfig',
array(
'swUrl' => esc_url_raw( $sw_url ),
'scope' => esc_url_raw( $scope ),
'requireLogin' => true,
'isLoggedIn' => is_user_logged_in(),
'iosInstallNotice' => __( 'Auf iPhone/iPad: Im Browser auf Teilen tippen und dann "Zum Home-Bildschirm" waehlen.', 'kgv-pwa' ),
)
);
}
/**
* Shortcode [kgv_pwa_install_button] rendert einen Installations-Button.
* Kann in Seiten, Posts und Widgets verwendet werden.
*
* Attribute:
* label Beschriftung des Buttons (Standard: "App installieren")
* class Zusätzliche CSS-Klassen
*/
public function render_install_button( $atts ) {
if ( ! is_user_logged_in() ) {
return '';
}
$atts = shortcode_atts(
array(
'label' => __( 'App installieren', 'kgv-pwa' ),
'class' => '',
),
$atts,
'kgv_pwa_install_button'
);
$classes = 'kgv-pwa-install-btn';
if ( ! empty( $atts['class'] ) ) {
$classes .= ' ' . sanitize_html_class( $atts['class'] );
}
return sprintf(
'<button type="button" class="%s" onclick="document.dispatchEvent(new CustomEvent(\'kgv-pwa-install\'))">%s</button>',
esc_attr( $classes ),
esc_html( $atts['label'] )
);
}
// -------------------------------------------------------------------------
// Admin Settings
// -------------------------------------------------------------------------
public function add_settings_page() {
add_options_page(
__( 'KGV PWA', 'kgv-pwa' ),
__( 'KGV PWA', 'kgv-pwa' ),
'manage_options',
'kgv-pwa',
array( $this, 'render_settings_page' )
);
}
public function register_settings() {
register_setting(
'kgv_pwa_settings_group',
self::OPTION_KEY,
array( $this, 'sanitize_settings' )
);
}
public function sanitize_settings( $input ) {
$defaults = self::default_settings();
$output = array();
$output['app_name'] = isset( $input['app_name'] )
? sanitize_text_field( $input['app_name'] )
: $defaults['app_name'];
$output['short_name'] = isset( $input['short_name'] )
? sanitize_text_field( $input['short_name'] )
: $defaults['short_name'];
$output['description'] = isset( $input['description'] )
? sanitize_textarea_field( $input['description'] )
: '';
$output['theme_color'] = isset( $input['theme_color'] )
&& preg_match( '/^#[0-9a-fA-F]{6}$/', $input['theme_color'] )
? $input['theme_color']
: $defaults['theme_color'];
$output['background_color'] = isset( $input['background_color'] )
&& preg_match( '/^#[0-9a-fA-F]{6}$/', $input['background_color'] )
? $input['background_color']
: $defaults['background_color'];
$valid_display = array( 'standalone', 'fullscreen', 'minimal-ui', 'browser' );
$output['display'] = isset( $input['display'] )
&& in_array( $input['display'], $valid_display, true )
? $input['display']
: $defaults['display'];
$output['icon_192'] = isset( $input['icon_192'] ) ? esc_url_raw( $input['icon_192'] ) : '';
$output['icon_512'] = isset( $input['icon_512'] ) ? esc_url_raw( $input['icon_512'] ) : '';
return $output;
}
private static function default_settings() {
return array(
'app_name' => get_bloginfo( 'name' ),
'short_name' => get_bloginfo( 'name' ),
'description' => get_bloginfo( 'description' ),
'theme_color' => '#2e7d32',
'background_color' => '#ffffff',
'display' => 'standalone',
'icon_192' => '',
'icon_512' => '',
);
}
public function get_settings() {
$settings = get_option( self::OPTION_KEY, array() );
if ( ! is_array( $settings ) ) {
$settings = array();
}
return wp_parse_args( $settings, self::default_settings() );
}
public function render_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$settings = $this->get_settings();
$option_prefix = esc_attr( self::OPTION_KEY );
$display_modes = array(
'standalone' => 'Standalone',
'fullscreen' => 'Fullscreen',
'minimal-ui' => 'Minimal UI',
'browser' => 'Browser',
);
?>
<div class="wrap">
<h1><?php esc_html_e( 'KGV PWA Einstellungen', 'kgv-pwa' ); ?></h1>
<p>
<?php esc_html_e( 'Manifest:', 'kgv-pwa' ); ?>
<a href="<?php echo esc_url( $this->get_manifest_url() ); ?>" target="_blank">
<?php echo esc_url( $this->get_manifest_url() ); ?>
</a>
&nbsp;|&nbsp;
<?php esc_html_e( 'Service Worker:', 'kgv-pwa' ); ?>
<a href="<?php echo esc_url( $this->get_service_worker_url() ); ?>" target="_blank">
<?php echo esc_url( $this->get_service_worker_url() ); ?>
</a>
</p>
<form method="post" action="options.php">
<?php settings_fields( 'kgv_pwa_settings_group' ); ?>
<table class="form-table" role="presentation">
<tr>
<th scope="row">
<label for="kgv_pwa_app_name"><?php esc_html_e( 'App-Name', 'kgv-pwa' ); ?></label>
</th>
<td>
<input type="text"
id="kgv_pwa_app_name"
name="<?php echo $option_prefix; ?>[app_name]"
value="<?php echo esc_attr( $settings['app_name'] ); ?>"
class="regular-text">
</td>
</tr>
<tr>
<th scope="row">
<label for="kgv_pwa_short_name"><?php esc_html_e( 'Kurzname', 'kgv-pwa' ); ?></label>
</th>
<td>
<input type="text"
id="kgv_pwa_short_name"
name="<?php echo $option_prefix; ?>[short_name]"
value="<?php echo esc_attr( $settings['short_name'] ); ?>"
class="regular-text">
<p class="description"><?php esc_html_e( 'Maximal 12 Zeichen empfohlen.', 'kgv-pwa' ); ?></p>
</td>
</tr>
<tr>
<th scope="row">
<label for="kgv_pwa_description"><?php esc_html_e( 'Beschreibung', 'kgv-pwa' ); ?></label>
</th>
<td>
<textarea id="kgv_pwa_description"
name="<?php echo $option_prefix; ?>[description]"
rows="3"
class="large-text"><?php echo esc_textarea( $settings['description'] ); ?></textarea>
</td>
</tr>
<tr>
<th scope="row">
<label for="kgv_pwa_theme_color"><?php esc_html_e( 'Theme-Farbe', 'kgv-pwa' ); ?></label>
</th>
<td>
<input type="color"
id="kgv_pwa_theme_color"
name="<?php echo $option_prefix; ?>[theme_color]"
value="<?php echo esc_attr( $settings['theme_color'] ); ?>">
</td>
</tr>
<tr>
<th scope="row">
<label for="kgv_pwa_background_color"><?php esc_html_e( 'Hintergrundfarbe', 'kgv-pwa' ); ?></label>
</th>
<td>
<input type="color"
id="kgv_pwa_background_color"
name="<?php echo $option_prefix; ?>[background_color]"
value="<?php echo esc_attr( $settings['background_color'] ); ?>">
</td>
</tr>
<tr>
<th scope="row">
<label for="kgv_pwa_display"><?php esc_html_e( 'Anzeigemodus', 'kgv-pwa' ); ?></label>
</th>
<td>
<select id="kgv_pwa_display" name="<?php echo $option_prefix; ?>[display]">
<?php foreach ( $display_modes as $val => $label ) : ?>
<option value="<?php echo esc_attr( $val ); ?>"<?php selected( $settings['display'], $val ); ?>>
<?php echo esc_html( $label ); ?>
</option>
<?php endforeach; ?>
</select>
<p class="description"><?php esc_html_e( '"Standalone" empfohlen App öffnet ohne Browser-UI.', 'kgv-pwa' ); ?></p>
</td>
</tr>
<tr>
<th scope="row">
<label for="kgv_pwa_icon_192"><?php esc_html_e( 'Icon 192 × 192 (URL)', 'kgv-pwa' ); ?></label>
</th>
<td>
<input type="url"
id="kgv_pwa_icon_192"
name="<?php echo $option_prefix; ?>[icon_192]"
value="<?php echo esc_url( $settings['icon_192'] ); ?>"
class="large-text"
placeholder="https://…/icon-192.png">
<p class="description"><?php esc_html_e( 'PNG, 192 × 192 px. Leer lassen, um das WordPress-Site-Icon zu verwenden.', 'kgv-pwa' ); ?></p>
</td>
</tr>
<tr>
<th scope="row">
<label for="kgv_pwa_icon_512"><?php esc_html_e( 'Icon 512 × 512 (URL)', 'kgv-pwa' ); ?></label>
</th>
<td>
<input type="url"
id="kgv_pwa_icon_512"
name="<?php echo $option_prefix; ?>[icon_512]"
value="<?php echo esc_url( $settings['icon_512'] ); ?>"
class="large-text"
placeholder="https://…/icon-512.png">
<p class="description"><?php esc_html_e( 'PNG, 512 × 512 px.', 'kgv-pwa' ); ?></p>
</td>
</tr>
</table>
<?php submit_button( __( 'Einstellungen speichern', 'kgv-pwa' ) ); ?>
</form>
</div>
<?php
}
}
register_activation_hook( __FILE__, array( 'KGV_PWA_Plugin', 'activate' ) );
register_deactivation_hook( __FILE__, array( 'KGV_PWA_Plugin', 'deactivate' ) );
add_action( 'plugins_loaded', array( 'KGV_PWA_Plugin', 'instance' ) );