Files
rangerhq-logbook/wp-notes.php
T
ranger bd8c5f2340 ux: replace generic cog admin-menu icon with closed-book (v3.3.5)
The WordPress admin sidebar showed "Logbook" with the generic
cog wheel icon (dashicons-admin-generic) — fine for a settings
page, off-brand for a plugin whose entire identity is "logbook".

Swapped to dashicons-book-alt (closed book) — the most literal
possible match in the Dashicons set, and visually reinforces the
brand at the first place users see the plugin every day.

Alternatives also viable (commented in the changelog for future
reference): dashicons-edit (pencil), dashicons-clipboard,
dashicons-welcome-write-blog, dashicons-format-aside.

VERSION
- wp-notes.php header 3.3.4 → 3.3.5
- WP_NOTES_VERSION constant 3.3.4 → 3.3.5
- About page version-history leads with v3.3.5; v3.3.4 demoted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 10:09:49 +01:00

1692 lines
63 KiB
PHP

<?php
/**
* Logbook — WordPress work-logbook plugin
*
* Plugin Name: Logbook
* Plugin URI: https://icanhelp.ie/wp-notes
* Description: A lightweight task &amp; logbook plugin for WordPress. Log your daily work, mark tasks done, and keep a tidy record inside the dashboard. Perfect for freelancers showing clients what's been delivered and students proving work to teachers.
* Version: 3.3.5
* Requires at least: 5.0
* Requires PHP: 7.2
* Author: IR240474
* Author URI: https://rangersmyth.xyz/
* License: GPL v2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: a-wp-notes
* Domain Path: /languages
*
* @package WP_Notes
*/
/**
* Security check and WordPress core loading
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
die;
}
// One-time initialization of plugin constants
global $wp_notes_init;
if (!isset($wp_notes_init)) {
$wp_notes_init = true;
// Plugin Constants
if (!defined('WP_NOTES_VERSION')) define('WP_NOTES_VERSION', '3.3.5');
if (!defined('WP_NOTES_FILE')) define('WP_NOTES_FILE', __FILE__);
if (!defined('WP_NOTES_PATH')) define('WP_NOTES_PATH', plugin_dir_path(__FILE__));
if (!defined('WP_NOTES_URL')) define('WP_NOTES_URL', plugin_dir_url(__FILE__));
if (!defined('WP_NOTES_BASENAME')) define('WP_NOTES_BASENAME', plugin_basename(__FILE__));
}
// Initialize error logging if not already defined
if (!function_exists('wp_notes_log_error')) {
function wp_notes_log_error($message) {
if (defined('WP_DEBUG') && WP_DEBUG === true) {
error_log('[Logbook] ' . $message);
}
}
}
// Plugin class autoloader
spl_autoload_register(function ($class) {
$prefix = 'WP_Notes\\';
$base_dir = plugin_dir_path(__FILE__) . 'includes/';
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) {
return;
}
$relative_class = substr($class, $len);
$file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
if (file_exists($file)) {
require $file;
}
});
// Plugin initialization function
function wp_notes_init() {
// Load plugin functionality
if (class_exists('WP_Notes\\Plugin')) {
$plugin = new WP_Notes\Plugin();
$plugin->run();
}
}
// Hook into WordPress init
add_action('plugins_loaded', 'wp_notes_init', 10);
// Register activation and deactivation hooks
register_activation_hook(__FILE__, 'wp_notes_activate');
register_deactivation_hook(__FILE__, 'wp_notes_deactivate');
/**
* Plugin activation handler
*/
function wp_notes_activate() {
// Ensure PHP version is compatible
if (version_compare(PHP_VERSION, '7.2', '<')) {
deactivate_plugins(basename(__FILE__));
wp_die('This plugin requires PHP version 7.2 or higher.');
}
// Initialize plugin settings
add_option('wp_notes_version', WP_NOTES_VERSION);
// Create database tables
wp_notes_create_tables();
// Backup existing notes
$existing_notes = get_option('wp_notes', array());
$existing_done_notes = get_option('wp_done_notes', array());
if (!empty($existing_notes)) {
update_option('wp_notes_backup', $existing_notes);
}
if (!empty($existing_done_notes)) {
update_option('wp_done_notes_backup', $existing_done_notes);
}
}
/**
* Plugin deactivation handler
*/
function wp_notes_deactivate() {
delete_option('wp_notes_version');
}
/**
* Create required database tables
*/
function wp_notes_create_tables() {
global $wpdb;
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
$charset_collate = $wpdb->get_charset_collate();
$sql = array();
// Notes table
$sql[] = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wp_notes (
id bigint(20) NOT NULL AUTO_INCREMENT,
user_id bigint(20) NOT NULL,
note_content text NOT NULL,
note_status varchar(20) NOT NULL DEFAULT 'active',
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY user_id (user_id),
KEY note_status (note_status)
) $charset_collate;";
foreach ($sql as $query) {
dbDelta($query);
}
}
// Initialize plugin
add_action('plugins_loaded', 'wp_notes_init');
// Set text domain for translations
function wp_notes_load_textdomain() {
load_plugin_textdomain('a-wp-notes', false, dirname(plugin_basename(__FILE__)) . '/languages');
}
add_action('plugins_loaded', 'wp_notes_load_textdomain');
// Include required files
if (defined('WP_NOTES_PATH')) {
require_once WP_NOTES_PATH . 'inc/admin-bar.php';
require_once WP_NOTES_PATH . 'inc/wp-notes-about.php';
require_once WP_NOTES_PATH . 'inc/wp-notes-feedback.php';
require_once WP_NOTES_PATH . 'inc/wp-notes-display.php';
require_once WP_NOTES_PATH . 'inc/wp-notes-styles.php';
require_once WP_NOTES_PATH . 'inc/wp-notes-updater.php';
}
// Admin Menu
function wp_notes_admin_menu() {
add_menu_page(
'Logbook',
'Logbook',
'manage_options',
'wp-notes',
'wp_notes_page_callback',
'dashicons-book-alt', // closed book — reinforces the "logbook" identity
3
);
// "My Log" — the main landing submenu. Same slug as the parent
// menu so clicking either Logbook or My Log lands on the same
// central dashboard (the parent's wp_notes_page_callback already
// renders form + active list + completed list).
//
// CRITICAL: callback must be empty here. WordPress registers BOTH
// the parent's and the submenu's callbacks against the same page
// hook when slugs match, and runs them in order — passing
// wp_notes_create_page as the callback caused a duplicate
// "Create a New WP Note" form to render BELOW the main page
// content. Empty callback means only the parent's renderer fires.
add_submenu_page(
'wp-notes', // Parent slug
'My Log', // Page title (browser tab)
'My Log', // Menu label (sidebar)
'manage_options', // Capability
'wp-notes', // Menu slug (matches parent → same page)
'' // Empty callback — see comment above
);
// Settings submenu
add_submenu_page(
'wp-notes',
'Settings',
'Settings',
'manage_options',
'wp-notes-settings',
'wp_notes_settings_page'
);
// Import/Export submenu
add_submenu_page(
'wp-notes',
'Import/Export',
'Import/Export',
'manage_options',
'wp-notes-import-export',
'wp_notes_import_export_page'
);
// About submenu — the Logbook brand is already carried by the
// parent menu, so the submenu can be plain-spoken.
add_submenu_page(
'wp-notes',
'About', // Page title (browser tab)
'About', // Menu label (sidebar)
'manage_options',
'wp-notes-about',
'wp_notes_about_page' // Renderer in inc/wp-notes-about.php
);
// Register settings with validation callback
register_setting(
'wp_notes_settings',
'wp_notes_settings',
array(
'sanitize_callback' => 'wp_notes_validate_settings',
'default' => array(
'default_font' => 'Arial',
'default_size' => '16'
)
)
);
add_settings_section(
'wp_notes_main_section',
'General Settings',
'wp_notes_section_callback',
'wp-notes-settings'
);
add_settings_field(
'wp_notes_default_font',
'Default Font',
'wp_notes_font_callback',
'wp-notes-settings',
'wp_notes_main_section'
);
add_settings_field(
'wp_notes_default_size',
'Default Size',
'wp_notes_size_callback',
'wp-notes-settings',
'wp_notes_main_section'
);
}
// Settings validation callback
function wp_notes_validate_settings($input) {
$output = array();
// Validate font
$allowed_fonts = ['Arial', 'Helvetica', 'Times New Roman', 'Verdana'];
$output['default_font'] = in_array($input['default_font'], $allowed_fonts) ? $input['default_font'] : 'Arial';
// Validate size (8-72px)
$size = absint($input['default_size']);
$output['default_size'] = ($size >= 8 && $size <= 72) ? $size : 16;
add_settings_error(
'wp_notes_settings',
'settings_updated',
'Settings saved successfully.',
'updated'
);
return $output;
}
// Settings page callback
function wp_notes_settings_page() {
// Show any settings messages
settings_errors('wp_notes_settings');
?>
<div class="wrap">
<h1>Logbook Settings</h1>
<form method="post" action="options.php">
<?php
settings_fields('wp_notes_settings');
do_settings_sections('wp-notes-settings');
submit_button();
?>
</form>
<?php
// Updates panel — checks the Gitea repo for a newer release.
// Defined in inc/wp-notes-updater.php.
if ( function_exists( 'wp_notes_render_updates_panel' ) ) {
wp_notes_render_updates_panel();
}
?>
</div>
<?php
}
// Settings section callback
function wp_notes_section_callback() {
echo '<p>Configure default settings for Logbook.</p>';
}
// Font setting callback
function wp_notes_font_callback() {
$options = get_option('wp_notes_settings');
$current = $options['default_font'] ?? 'Arial';
?>
<select name="wp_notes_settings[default_font]">
<option value="Arial" <?php selected($current, 'Arial'); ?>>Arial</option>
<option value="Helvetica" <?php selected($current, 'Helvetica'); ?>>Helvetica</option>
<option value="Times New Roman" <?php selected($current, 'Times New Roman'); ?>>Times New Roman</option>
<option value="Verdana" <?php selected($current, 'Verdana'); ?>>Verdana</option>
</select>
<?php
}
// Size setting callback
function wp_notes_size_callback() {
$options = get_option('wp_notes_settings');
$current = $options['default_size'] ?? '16';
?>
<input type="number" name="wp_notes_settings[default_size]" value="<?php echo esc_attr($current); ?>" min="8" max="72">
<span class="description">Font size in pixels (8-72)</span>
<?php
}
// Import/Export page callback
function wp_notes_import_export_page() {
// Display settings errors
settings_errors();
if (isset($_POST['export_notes'])) {
wp_notes_export_data();
}
if (isset($_POST['import_notes']) && !empty($_FILES['import_file']['tmp_name'])) {
try {
wp_notes_import_data();
} catch (Exception $e) {
add_settings_error(
'wp_notes_import',
'import_error',
'Error importing notes: ' . esc_html($e->getMessage()),
'error'
);
}
}
// Get current settings for display
$options = get_option('wp_notes_settings');
?>
<div class="wrap">
<h1>Import/Export Notes</h1>
<!-- Export Section -->
<div class="postbox">
<div class="inside">
<h2>Export Notes</h2>
<p>Download your notes as JSON or CSV file.</p>
<form method="post">
<select name="export_format">
<option value="json">JSON</option>
<option value="csv">CSV</option>
</select>
<?php wp_nonce_field('wp_notes_export', 'export_nonce'); ?>
<input type="submit" name="export_notes" class="button button-primary" value="Export Notes">
</form>
</div>
</div>
<!-- Import Section -->
<div class="postbox">
<div class="inside">
<h2>Import Notes</h2>
<p>Import notes from JSON or CSV file.</p>
<form method="post" enctype="multipart/form-data">
<input type="file" name="import_file" accept=".json,.csv">
<?php wp_nonce_field('wp_notes_import', 'import_nonce'); ?>
<input type="submit" name="import_notes" class="button button-primary" value="Import Notes">
</form>
</div>
</div>
</div>
<?php
}
// Export functionality
function wp_notes_export_data() {
if (!isset($_POST['export_nonce']) || !wp_verify_nonce($_POST['export_nonce'], 'wp_notes_export')) {
wp_die('Security check failed');
}
$format = $_POST['export_format'] ?? 'json';
$notes = get_option('wp_notes', array());
$done_notes = get_option('wp_done_notes', array());
$data = array(
'active_notes' => $notes,
'completed_notes' => $done_notes,
'export_date' => current_time('mysql')
);
if ($format === 'json') {
header('Content-Type: application/json');
header('Content-Disposition: attachment; filename=wp-notes-export-' . date('Y-m-d') . '.json');
echo wp_json_encode($data, JSON_PRETTY_PRINT);
} else {
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename=wp-notes-export-' . date('Y-m-d') . '.csv');
$output = fopen('php://output', 'w');
fputcsv($output, array('Type', 'Text', 'Created By', 'Created On', 'Status'));
foreach ($notes as $note) {
fputcsv($output, array(
'active',
$note['text'],
$note['author_name'] ?? 'Unknown',
$note['timestamp'] ?? '',
'Active'
));
}
foreach ($done_notes as $note) {
fputcsv($output, array(
'completed',
$note['text'],
$note['author_name'] ?? 'Unknown',
$note['timestamp'] ?? '',
'Completed'
));
}
fclose($output);
}
exit;
}
// Import functionality
function wp_notes_import_data() {
if (!isset($_POST['import_nonce']) || !wp_verify_nonce($_POST['import_nonce'], 'wp_notes_import')) {
wp_die('Security check failed');
}
$file = $_FILES['import_file'];
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if ($ext === 'json') {
$content = file_get_contents($file['tmp_name']);
$data = json_decode($content, true);
if (json_last_error() === JSON_ERROR_NONE) {
if (isset($data['active_notes'])) {
update_option('wp_notes', $data['active_notes']);
}
if (isset($data['completed_notes'])) {
update_option('wp_done_notes', $data['completed_notes']);
}
add_settings_error('wp_notes_import', 'import_success', 'Notes imported successfully', 'updated');
} else {
add_settings_error('wp_notes_import', 'import_error', 'Invalid JSON file');
}
} elseif ($ext === 'csv') {
$handle = fopen($file['tmp_name'], 'r');
$headers = fgetcsv($handle);
$notes = array();
$done_notes = array();
while (($data = fgetcsv($handle)) !== false) {
$note = array(
'text' => $data[1],
'author_name' => $data[2],
'timestamp' => $data[3],
'color' => '#000000',
'size' => '16',
'font' => 'Arial'
);
if ($data[0] === 'active') {
$notes[] = $note;
} else {
$done_notes[] = $note;
}
}
fclose($handle);
update_option('wp_notes', $notes);
update_option('wp_done_notes', $done_notes);
add_settings_error('wp_notes_import', 'import_success', 'Notes imported successfully', 'updated');
} else {
add_settings_error('wp_notes_import', 'import_error', 'Invalid file format. Please use JSON or CSV files.');
}
}
add_action('admin_menu', 'wp_notes_admin_menu');
// The Tools → My Notes shortcut used to live here and routed to a
// separate bare-bones form at ?page=wp-notes-create. It was removed
// as a duplicate — the My Notes page (and the admin-bar "New Note"
// shortcut that jumps to its #new-note anchor) covers the same need
// with the proper UI. Any stale bookmark to the old URL is caught
// by wp_notes_redirect_legacy_create_page() below.
// Redirect anyone hitting the legacy ?page=wp-notes-create URL to
// the My Notes page, so existing bookmarks don't 404 / show "you do
// not have sufficient permissions" after the Tools shortcut removal.
add_action('admin_init', 'wp_notes_redirect_legacy_create_page');
function wp_notes_redirect_legacy_create_page() {
if (!is_admin()) { return; }
if (!isset($_GET['page'])) { return; }
if (sanitize_key(wp_unslash($_GET['page'])) !== 'wp-notes-create') { return; }
wp_safe_redirect(admin_url('admin.php?page=wp-notes'));
exit;
}
// Enqueue Scripts
function wp_notes_enqueue_scripts() {
// wp_enqueue_script('wp-notes-activity-tracker', WP_NOTES_URL . 'js/wp-notes-activity.js', ['jquery'], null, true); // Activity tracking removed 2025-05-09
wp_enqueue_script('wp-notes-feedback', WP_NOTES_URL . 'js/wp-notes-feedback.js', ['jquery'], null, true);
wp_localize_script('wp-notes-feedback', 'wp_notes_feedback_vars', array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('wp_notes_feedback_nonce')
));
}
add_action('admin_enqueue_scripts', 'wp_notes_enqueue_scripts');
// Emoji picker initialization
add_action('admin_footer', 'wp_notes_emoji_picker_init');
function wp_notes_emoji_picker_init() {
?>
<script type="text/javascript">
jQuery(document).ready(function($) {
// Initialize emoji picker
function initEmojiPicker() {
// Handle emoji picker toggle
$('.emoji-input').on('click', function(e) {
e.preventDefault();
e.stopPropagation();
var $container = $(this).closest('.emoji-picker-container');
$('.emoji-picker-dropdown').not($container.find('.emoji-picker-dropdown')).hide();
$container.find('.emoji-picker-dropdown').toggle();
});
// Handle emoji selection
$('.emoji-option').on('click', function(e) {
e.preventDefault();
e.stopPropagation();
var emoji = $(this).data('emoji');
var $container = $(this).closest('.emoji-picker-container');
var $noteText = $('#wp_notes_text');
// Get cursor position or selection
var start = $noteText[0].selectionStart;
var end = $noteText[0].selectionEnd;
var text = $noteText.val();
// Insert emoji at cursor position
var newText = text.substring(0, start) + emoji + text.substring(end);
$noteText.val(newText);
// Update cursor position
var newCursorPos = start + emoji.length;
$noteText[0].setSelectionRange(newCursorPos, newCursorPos);
// Focus back on textarea
$noteText.focus();
// Hide dropdown
$container.find('.emoji-picker-dropdown').hide();
});
// Close emoji picker when clicking outside
$(document).on('click', function(e) {
if (!$(e.target).closest('.emoji-picker-container').length) {
$('.emoji-picker-dropdown').hide();
}
});
// Handle keyboard navigation — focus returns to the input
// (the picker trigger) after Escape now that the dedicated
// button has been removed.
$('.emoji-picker-container').on('keydown', function(e) {
var $dropdown = $(this).find('.emoji-picker-dropdown');
var $input = $(this).find('.emoji-input');
// Toggle dropdown with Enter or Space when the input is focused
if ((e.key === 'Enter' || e.key === ' ') && e.target === $input[0]) {
e.preventDefault();
$dropdown.toggle();
}
// Close with Escape
if (e.key === 'Escape' && $dropdown.is(':visible')) {
$dropdown.hide();
$input.focus();
}
});
}
// Initialize on page load
initEmojiPicker();
});
</script>
<?php
}
// JavaScript for saving edited notes
add_action('admin_footer', 'wp_notes_add_inline_scripts');
function wp_notes_add_inline_scripts() {
echo "
<script type='text/javascript'>
jQuery(document).ready(function($) {
// Show edit form
$('.edit-note').on('click', function(e) {
e.preventDefault();
var noteId = $(this).data('note-id');
$('#edit-note-' + noteId).toggle();
});
// AJAX submit the edit form
$('.edit-note-form').on('submit', function(e) {
e.preventDefault();
var noteId = $(this).data('note-id');
var formData = $(this).serialize() + '&action=wp_notes_save_edit&note_id=' + noteId;
$.post(ajaxurl, formData, function(response) {
if (response.success) {
alert('Note updated successfully!');
location.reload(); // Reload to display updated notes
} else {
alert('Error updating note: ' + response.data);
}
});
});
// Persist the dismissal of the empty-state notice.
// WP core's common.js attaches the X button and hides the
// notice on click; we just hook the same click and fire an
// AJAX call to set the user_meta flag so the notice does
// not reappear on the next page load.
$(document).on('click', '.wp-notes-empty .notice-dismiss', function() {
var \$notice = $(this).closest('.wp-notes-empty');
var type = \$notice.data('wp-notes-empty-type');
var nonce = \$notice.data('wp-notes-nonce');
if (!type || !nonce) { return; }
$.post(ajaxurl, {
action: 'wp_notes_dismiss_empty',
type: type,
nonce: nonce
});
});
});
</script>";
}
// Receive the "Leave Feedback" form from the About page and forward
// it to the site admin via wp_mail(). Topics is an array of short
// keys (improve / help / bug / feature / use-case / thanks / other);
// message is an optional free-text textarea. Sender info is taken
// from wp_get_current_user() so we never trust client-supplied
// identity. Capability-gated to manage_options because the About
// page is admin-only.
add_action('wp_ajax_wp_notes_submit_feedback', 'wp_notes_ajax_submit_feedback');
function wp_notes_ajax_submit_feedback() {
if (!current_user_can('manage_options')) {
wp_send_json_error('Insufficient permissions.', 403);
}
check_ajax_referer('wp_notes_feedback_submit', 'nonce');
$allowed_topics = ['improve', 'help', 'bug', 'feature', 'use-case', 'thanks', 'other'];
$raw_topics = isset($_POST['topics']) && is_array($_POST['topics']) ? wp_unslash($_POST['topics']) : [];
$topics = array_values(array_intersect($allowed_topics, array_map('sanitize_key', $raw_topics)));
$message = isset($_POST['message']) ? sanitize_textarea_field(wp_unslash($_POST['message'])) : '';
if (empty($topics) && $message === '') {
wp_send_json_error('Pick at least one topic or write a message.', 400);
}
$user = wp_get_current_user();
$site = get_bloginfo('name');
$to = get_option('admin_email');
$topic_labels = [
'improve' => 'Ideas to improve the plugin',
'help' => 'Needs help with the plugin',
'bug' => 'Reporting a bug',
'feature' => 'New feature request',
'use-case' => 'Sharing a use case',
'thanks' => 'Saying thanks',
'other' => 'Other',
];
$topics_pretty = array_map(function ($t) use ($topic_labels) { return $topic_labels[$t] ?? $t; }, $topics);
$subject = sprintf('[%s] Logbook feedback from %s', $site, $user->display_name ?: $user->user_login);
$body = "Feedback received via Logbook → About page\n";
$body .= str_repeat('-', 48) . "\n\n";
$body .= 'From: ' . ($user->display_name ?: $user->user_login) . ' <' . $user->user_email . ">\n";
$body .= 'Site: ' . home_url() . "\n";
$body .= 'Plugin: v' . WP_NOTES_VERSION . "\n\n";
$body .= "Topics:\n";
if (!empty($topics_pretty)) {
foreach ($topics_pretty as $label) { $body .= ' - ' . $label . "\n"; }
} else {
$body .= " (none selected)\n";
}
$body .= "\nMessage:\n";
$body .= $message !== '' ? $message . "\n" : "(no message provided)\n";
$headers = [
'Content-Type: text/plain; charset=UTF-8',
'Reply-To: ' . sanitize_email($user->user_email),
];
$sent = wp_mail($to, $subject, $body, $headers);
if (!$sent) {
wp_send_json_error('Email could not be sent. Check the site mail configuration.', 500);
}
wp_send_json_success(['delivered_to' => $to]);
}
// Persist a user's dismissal of the "No active/completed notes found"
// empty-state notice so it doesn't reappear on the next page load.
// Triggered by inline JS in wp_notes_add_inline_scripts() when the
// user clicks the X on a .wp-notes-empty notice.
add_action('wp_ajax_wp_notes_dismiss_empty', 'wp_notes_ajax_dismiss_empty');
function wp_notes_ajax_dismiss_empty() {
if (!current_user_can('edit_posts')) {
wp_send_json_error('Insufficient permissions.', 403);
}
check_ajax_referer('wp_notes_dismiss_empty', 'nonce');
$type = isset($_POST['type']) ? sanitize_key(wp_unslash($_POST['type'])) : '';
if (!in_array($type, ['active', 'completed'], true)) {
wp_send_json_error('Invalid list type.', 400);
}
update_user_meta(get_current_user_id(), 'wp_notes_dismissed_empty_' . $type, 1);
wp_send_json_success(['dismissed' => $type]);
}
// Handle saving edited notes
add_action('wp_ajax_wp_notes_save_edit', 'wp_notes_save_edit');
function wp_notes_save_edit() {
// Security checks
if (!current_user_can('edit_posts')) {
wp_send_json_error('Insufficient permissions.');
return;
}
// Security check using nonce
check_ajax_referer('wp_notes_nonce', '_wpnonce');
// Validate required fields
$required_fields = ['note_id', 'new_text', 'edit_color', 'edit_size', 'edit_font'];
foreach ($required_fields as $field) {
if (!isset($_POST[$field])) {
wp_send_json_error("Missing required field: $field");
return;
}
}
// Get and sanitize data
$note_id = absint($_POST['note_id']);
$new_text = sanitize_text_field($_POST['new_text']);
$new_color = sanitize_hex_color($_POST['edit_color']);
$new_size = absint($_POST['edit_size']);
// Validate font against allowed list
$allowed_fonts = ['Arial', 'Helvetica', 'Times New Roman', 'Verdana'];
$new_font = in_array($_POST['edit_font'], $allowed_fonts) ? $_POST['edit_font'] : 'Arial';
// Validate size range
if ($new_size < 8 || $new_size > 72) {
wp_send_json_error('Font size must be between 8 and 72 pixels.');
return;
}
$notes = get_option('wp_notes', array());
if (!isset($notes[$note_id])) {
wp_send_json_error('Note not found.');
return;
}
// Update note with validation
$notes[$note_id] = array_merge($notes[$note_id], [
'text' => $new_text,
'color' => $new_color ?: '#000000',
'size' => $new_size,
'font' => $new_font,
'modified_timestamp' => current_time('mysql'),
'modified_by' => get_current_user_id()
]);
if (!update_option('wp_notes', $notes)) {
wp_send_json_error('Failed to update note.');
return;
}
wp_send_json_success([
'message' => 'Note updated successfully',
'note' => $notes[$note_id]
]);
}
/**
* Main Logbook page — central hub for note management
*/
function wp_notes_page_callback() {
if (!current_user_can('edit_posts')) {
wp_die(__('You do not have sufficient permissions to access this page.', 'a-wp-notes'));
}
$notes = get_option('wp_notes', array());
$done_notes = get_option('wp_done_notes', array());
$total_notes = count($notes);
$total_done = count($done_notes);
// Get user settings
$settings = get_option('wp_notes_settings', array(
'default_font' => 'Arial',
'default_size' => '16'
));
?>
<div class="wrap">
<!-- Header Section with WordPress Admin Styling -->
<h1 class="wp-heading-inline">Logbook</h1>
<span class="page-title-action">v<?php echo esc_html(WP_NOTES_VERSION); ?></span>
<hr class="wp-header-end">
<p class="description" style="max-width: 720px;">
Log your daily tasks and keep a tidy work-log inside WordPress.
<a href="<?php echo esc_url(admin_url('admin.php?page=wp-notes-about')); ?>">Read more on the About page →</a>
</p>
<!-- Note Creation Form with WordPress Admin Styling -->
<div class="postbox">
<div class="postbox-header">
<h2 class="hndle">New Log Entry</h2>
</div>
<div class="inside">
<form method="post" class="wp-notes-form">
<div class="form-field">
<label for="wp_notes_text" class="screen-reader-text">Note Content:</label>
<textarea
name="wp_notes_text"
id="wp_notes_text"
class="large-text"
required
rows="4"
aria-label="Enter your note content"
placeholder="Enter your note here..."></textarea>
</div>
<div class="wp-notes-formatting form-field">
<fieldset>
<legend class="screen-reader-text">Note Formatting Options</legend>
<div class="formatting-options">
<div class="format-option">
<label for="wp_notes_color" class="format-label">
<span class="dashicons dashicons-color-picker"></span>
Text Color
</label>
<input
type="color"
name="wp_notes_color"
id="wp_notes_color"
value="#000000"
aria-label="Choose text color">
</div>
<div class="format-option">
<label for="wp_notes_size" class="format-label">
<span class="dashicons dashicons-editor-textcolor"></span>
Size (px)
</label>
<input
type="number"
name="wp_notes_size"
id="wp_notes_size"
min="8"
max="72"
value="16"
class="small-text"
aria-label="Choose text size">
</div>
<div class="format-option">
<label for="wp_notes_font" class="format-label">
<span class="dashicons dashicons-editor-paragraph"></span>
Font
</label>
<select
name="wp_notes_font"
id="wp_notes_font"
class="regular-text"
aria-label="Choose font">
<option value="Arial">Arial</option>
<option value="Helvetica">Helvetica</option>
<option value="Times New Roman">Times New Roman</option>
<option value="Verdana">Verdana</option>
</select>
</div>
<div class="format-option">
<label for="wp_notes_emoji" class="format-label">
<span class="dashicons dashicons-smiley"></span>
Emoji
</label>
<div class="emoji-picker-container">
<input
type="text"
name="wp_notes_emoji"
id="wp_notes_emoji"
placeholder="Click to add emoji"
class="small-text emoji-input"
aria-label="Add emoji"
readonly>
<div class="emoji-picker-dropdown" style="display: none;">
<div class="emoji-list">
<!-- Common emojis -->
<button type="button" class="emoji-option" data-emoji="😊">😊</button>
<button type="button" class="emoji-option" data-emoji="👍">👍</button>
<button type="button" class="emoji-option" data-emoji="✅">✅</button>
<button type="button" class="emoji-option" data-emoji="⭐">⭐</button>
<button type="button" class="emoji-option" data-emoji="📝">📝</button>
<button type="button" class="emoji-option" data-emoji="❗">❗</button>
<button type="button" class="emoji-option" data-emoji="❓">❓</button>
<button type="button" class="emoji-option" data-emoji="⚠️">⚠️</button>
</div>
</div>
</div>
</div>
</div>
</fieldset>
</div>
<div class="submit-button">
<input
type="submit"
value="Add Note"
class="button button-primary button-large"
aria-label="Add new note">
</div>
</form>
</div>
</div>
<style>
.wp-notes-form .form-field { margin-bottom: 1em; }
.wp-notes-formatting { background: #f9f9f9; padding: 15px; border-radius: 4px; }
.formatting-options { display: flex; gap: 20px; flex-wrap: wrap; align-items: center; }
.format-option { display: flex; flex-direction: column; gap: 5px; }
.format-label { display: flex; align-items: center; gap: 5px; }
.submit-button { margin-top: 1em; }
.emoji-picker-container { position: relative; }
.emoji-picker-dropdown {
position: absolute;
top: 100%;
left: 0;
background: white;
border: 1px solid #ccd0d4;
border-radius: 4px;
padding: 10px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
z-index: 100;
}
.emoji-list {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 5px;
}
.emoji-option {
border: none;
background: none;
font-size: 20px;
padding: 5px;
cursor: pointer;
border-radius: 4px;
}
.emoji-option:hover {
background: #f0f0f1;
}
.emoji-input {
cursor: pointer;
}
</style>
<!-- Notes List -->
<h2>Log entries</h2>
<!-- Active Notes List -->
<div class="wp-notes-active" id="active-notes">
<?php wp_notes_display_notes('active'); ?>
</div>
<!-- Completed Notes List -->
<div class="wp-notes-completed" id="completed-notes">
<?php wp_notes_display_notes('completed'); ?>
</div>
<!-- Footer: support link (lives at the BOTTOM, not the top) -->
<p class="wp-notes-footer-support">
<a href="https://www.buymeacoffee.com/davidkeanek" target="_blank" rel="noopener"
class="button button-secondary"
aria-label="Support the developer by buying them a coffee">
☕ Buy me a coffee
</a>
</p>
</div>
<?php
}
// List Table Function
function wp_notes_list_table() {
// Check user capabilities
if (!current_user_can('edit_posts')) {
wp_die(__('You do not have sufficient permissions to access this page.', 'a-wp-notes'));
}
$notes = get_option('wp_notes', array());
if (!$notes) {
echo '<p>' . esc_html__('No notes found.', 'a-wp-notes') . '</p>';
return;
}
/* Dashboard widget table. No checkbox column — bulk actions are not
wired up; re-add when there is something to act on. */
echo '<table class="wp-list-table widefat striped">
<thead>
<tr>
<th scope="col" class="manage-column">Note</th>
<th scope="col" class="manage-column">Created By</th>
<th scope="col" class="manage-column">Created On</th>
<th scope="col" class="manage-column">Actions</th>
</tr>
</thead>
<tbody>';
foreach ($notes as $key => $note) {
$text = esc_html($note['text']);
$color = esc_attr($note['color'] ?? '#000000');
$size = esc_attr($note['size'] ?? '14');
$font = esc_attr($note['font'] ?? 'Arial');
$timestamp = esc_html($note['timestamp'] ?? current_time('mysql'));
$author = esc_html($note['author_name'] ?? 'Unknown');
echo "<tr>
<td style='color: $color; font-size: {$size}px; font-family: $font;'>$text</td>
<td>$author</td>
<td>$timestamp</td>
<td>
<form method='post' style='display:inline;'>
<input type='hidden' name='note_id' value='$key'>
<button type='submit' name='mark_done' value='Mark as Done' class='button button-secondary'>Mark as Done</button>
</form>
<button type='button' class='button edit-note' data-note-id='$key'>Edit</button>
<div id='edit-note-$key' style='display:none;'>
<form class='edit-note-form' data-note-id='$key'>
" . wp_nonce_field('wp_notes_nonce', '_wpnonce', true, false) . "
<input type='hidden' name='note_id' value='$key'>
<textarea name='new_text'>$text</textarea><br>
<input type='color' name='edit_color' value='$color'><br>
<input type='number' name='edit_size' value='$size' min='8' max='72'><br>
<select name='edit_font'>
<option value='Arial' " . selected('Arial', $font, false) . ">Arial</option>
<option value='Helvetica' " . selected('Helvetica', $font, false) . ">Helvetica</option>
<option value='Times New Roman' " . selected('Times New Roman', $font, false) . ">Times New Roman</option>
<option value='Verdana' " . selected('Verdana', $font, false) . ">Verdana</option>
</select><br>
<button type='submit' class='button button-primary'>Save</button>
</form>
</div>
</td>
</tr>";
}
echo '</tbody></table>';
// JavaScript for form toggling and AJAX submissions
?>
<script type="text/javascript">
jQuery(document).ready(function($) {
// Show edit form
$('.edit-note').on('click', function(e) {
e.preventDefault();
var noteId = $(this).data('note-id');
$('.edit-note-form').not('#edit-note-' + noteId).hide();
$('#edit-note-' + noteId).toggle();
});
// AJAX submit the edit form
$('.edit-note-form').on('submit', function(e) {
e.preventDefault();
var $form = $(this);
var $submitButton = $form.find('button[type="submit"]');
var noteId = $form.data('note-id');
// Log form data for debugging
console.log('Form data:', $form.serialize());
console.log('Note ID:', noteId);
// Disable submit button and show loading state
$submitButton.prop('disabled', true).text('Saving...');
// AJAX request
$.ajax({
url: ajaxurl,
type: 'POST',
data: $form.serialize() + '&action=wp_notes_save_edit&note_id=' + noteId,
success: function(response) {
if (response.success) {
location.reload();
} else {
alert('Error: ' + (response.data || 'Unknown error occurred'));
}
},
error: function(xhr, status, error) {
alert('Server error: ' + error);
},
complete: function() {
$submitButton.prop('disabled', false).text('Save');
}
});
// Emoji Picker Functionality
$('.emoji-picker-button, .emoji-input').on('click', function(e) {
e.preventDefault();
var $container = $(this).closest('.emoji-picker-container');
$container.find('.emoji-picker-dropdown').toggle();
});
// Handle emoji selection
$('.emoji-option').on('click', function() {
var emoji = $(this).data('emoji');
var $container = $(this).closest('.emoji-picker-container');
var $input = $container.find('.emoji-input');
// Append emoji to note text
var $noteText = $('#wp_notes_text');
var currentPosition = $noteText[0].selectionStart;
var text = $noteText.val();
var newText = text.slice(0, currentPosition) + emoji + text.slice(currentPosition);
$noteText.val(newText);
// Update cursor position
$noteText[0].setSelectionRange(currentPosition + emoji.length, currentPosition + emoji.length);
$noteText.focus();
// Hide dropdown
$container.find('.emoji-picker-dropdown').hide();
});
// Close emoji picker when clicking outside
$(document).on('click', function(e) {
if (!$(e.target).closest('.emoji-picker-container').length) {
$('.emoji-picker-dropdown').hide();
}
});
});
</script>
<?php
}
// Done Notes Table Function
function wp_notes_done_table() {
$done_notes = get_option('wp_done_notes', array());
if (!$done_notes) {
echo '<p>' . esc_html__('No completed notes.', 'a-wp-notes') . '</p>';
return;
}
echo '<form method="post"><table class="wp-list-table widefat striped">
<thead>
<tr>
<th scope="col" class="manage-column column-cb check-column">
<input type="checkbox" id="select-all-done">
</th>
<th scope="col">Note</th>
<th scope="col">Created By</th>
<th scope="col">Created On</th>
<th scope="col">Completed By</th>
<th scope="col">Completed On</th>
<th scope="col">Last Modified</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>';
foreach ($done_notes as $key => $note) {
echo '<tr>
<td><input type="checkbox" name="done_ids[]" value="' . esc_attr($key) . '" class="done-checkbox"></td>
<td style="color: ' . esc_attr($note['color']) . '; font-size: 14px; font-family: ' . esc_attr($note['font']) . ';">'
. esc_html($note['text']) . '</td>
<td>' . esc_html($note['author_name'] ?? 'Unknown') . '</td>
<td>' . esc_html($note['timestamp'] ?? 'Unknown') . '</td>
<td>' . esc_html($note['completed_by'] ?? 'Unknown') . '</td>
<td>' . esc_html($note['completed_on'] ?? 'Unknown') . '</td>
<td>' . esc_html($note['last_modified'] ?? 'N/A') . '</td>
<td>
<input type="submit" name="restore_note" value="Restore" class="button button-secondary">
</td>
</tr>';
}
echo '</tbody></table></form>';
// Add JavaScript for select all functionality
?>
<script type="text/javascript">
jQuery(document).ready(function($) {
$('#select-all-done').change(function() {
$('.done-checkbox').prop('checked', $(this).prop('checked'));
});
});
</script>
<?php
}
// Handle note actions
add_action('admin_init', 'wp_notes_handle_actions');
function wp_notes_handle_actions() {
$current_user = wp_get_current_user();
// Check if we're adding a new note
if (isset($_POST['wp_notes_text'])) {
$notes = get_option('wp_notes', array());
// Create the new note
$new_note = array(
'text' => sanitize_text_field($_POST['wp_notes_text']),
'color' => sanitize_hex_color($_POST['wp_notes_color']) ?: '#000000',
'size' => absint($_POST['wp_notes_size']) ?: 16,
'font' => sanitize_text_field($_POST['wp_notes_font']) ?: 'Arial',
'timestamp' => current_time('mysql'),
'author' => $current_user->ID,
'author_name' => $current_user->display_name
);
$notes[] = $new_note;
update_option('wp_notes', $notes);
wp_redirect(admin_url('admin.php?page=wp-notes'));
exit;
}
// Handle "Mark as Done" submissions
if (isset($_POST['mark_done']) && isset($_POST['note_id'])) {
$notes = get_option('wp_notes', array());
$done_notes = get_option('wp_done_notes', array());
$note_id = absint($_POST['note_id']);
if (isset($notes[$note_id])) {
$note = $notes[$note_id];
$note['completed_by'] = $current_user->display_name;
$note['completed_on'] = current_time('mysql');
$done_notes[] = $note;
unset($notes[$note_id]);
update_option('wp_notes', $notes);
update_option('wp_done_notes', $done_notes);
wp_redirect(admin_url('admin.php?page=wp-notes'));
exit;
}
}
// Handle marking notes as done if 'mark_done' action is triggered
if (isset($_POST['mark_done']) && isset($_POST['note_ids'])) {
$notes = get_option('wp_notes', array());
$done_notes = get_option('wp_done_notes', array());
// Move selected notes to done list
foreach ($notes as $key => $note) {
if (in_array($key, $_POST['note_ids'])) {
$note['completed_by'] = $current_user->display_name;
$note['completed_on'] = current_time('mysql');
$done_notes[] = $note;
unset($notes[$key]); // Remove from active notes
}
}
// Update both lists in the database
update_option('wp_notes', $notes);
update_option('wp_done_notes', $done_notes);
// Redirect to avoid form resubmission
wp_redirect(admin_url('admin.php?page=wp-notes'));
exit;
}
// Handle restore note action
if (isset($_POST['restore_note']) && isset($_POST['done_ids'])) {
$notes = get_option('wp_notes', array());
$done_notes = get_option('wp_done_notes', array());
$current_user = wp_get_current_user();
$new_done_notes = array();
foreach ($done_notes as $key => $note) {
if (in_array($key, $_POST['done_ids'])) {
$note['last_modified'] = current_time('mysql');
$note['restored_by'] = $current_user->display_name;
$notes[] = $note;
} else {
$new_done_notes[] = $note;
}
}
update_option('wp_notes', $notes);
update_option('wp_done_notes', $new_done_notes);
}
// Handle edit note action
if (isset($_POST['edit_note']) && isset($_POST['note_ids'])) {
// Add edit functionality here
$notes = get_option('wp_notes', array());
$current_user = wp_get_current_user();
foreach ($_POST['note_ids'] as $key) {
if (isset($notes[$key])) {
$notes[$key]['last_modified'] = current_time('mysql');
$notes[$key]['modified_by'] = $current_user->display_name;
}
}
update_option('wp_notes', $notes);
}
}
// AJAX handler for editing a note
add_action('wp_ajax_wp_notes_edit_note', 'wp_notes_edit_note');
function wp_notes_edit_note() {
if (!isset($_POST['note_id']) || !isset($_POST['edit_text'])) {
wp_send_json_error('Invalid request');
return;
}
$note_id = absint($_POST['note_id']);
$new_text = sanitize_text_field($_POST['edit_text']);
$new_color = sanitize_hex_color($_POST['edit_color']);
$new_size = absint($_POST['edit_size']);
$new_font = sanitize_text_field($_POST['edit_font']);
$notes = get_option('wp_notes', array());
if (isset($notes[$note_id])) {
$notes[$note_id]['text'] = $new_text;
$notes[$note_id]['color'] = $new_color;
$notes[$note_id]['size'] = $new_size;
$notes[$note_id]['font'] = $new_font;
update_option('wp_notes', $notes);
wp_send_json_success();
} else {
wp_send_json_error('Note not found.');
}
}
// Optimize the restore function for performance
function wp_notes_restore($note_id) {
$done_notes = get_option('wp_done_notes', array());
$notes = get_option('wp_notes', array());
if (isset($done_notes[$note_id])) {
$notes[$note_id] = $done_notes[$note_id];
unset($done_notes[$note_id]);
update_option('wp_notes', $notes);
update_option('wp_done_notes', $done_notes);
wp_redirect(admin_url('admin.php?page=wp-notes'));
}
}
// Dashboard widget
function wp_notes_dashboard_widget() {
wp_notes_list_table();
}
function wp_notes_add_dashboard_widgets() {
wp_add_dashboard_widget(
'wp_notes_dashboard_widget',
'Logbook',
'wp_notes_dashboard_widget'
);
}
add_action('wp_dashboard_setup', 'wp_notes_add_dashboard_widgets');
// Register Custom Post Type for notes
function wp_notes_register_cpt() {
$labels = array(
'name' => __('Notes', 'a-wp-notes'),
'singular_name' => __('Note', 'a-wp-notes'),
'menu_name' => __('Logbook', 'a-wp-notes'),
'add_new' => __('Add New', 'a-wp-notes'),
'add_new_item' => __('Add New Note', 'a-wp-notes'),
'edit_item' => __('Edit Note', 'a-wp-notes'),
'new_item' => __('New Note', 'a-wp-notes'),
'view_item' => __('View Note', 'a-wp-notes'),
'search_items' => __('Search Notes', 'a-wp-notes'),
'not_found' => __('No notes found', 'a-wp-notes'),
'not_found_in_trash'=> __('No notes found in trash', 'a-wp-notes'),
);
$args = array(
'labels' => $labels,
'public' => false,
// show_ui=false: the live UI stores notes in wp_options, not as
// CPT posts. show_ui=true caused WordPress to auto-inject "All
// Notes" and "Add New" submenus that pointed at post-new.php and
// routed users into the standard post editor — which writes to
// the wrong storage. The CPT remains registered so the
// wp_notes_migrate_to_cpt() helper can still use wp_insert_post.
'show_ui' => false,
'show_in_menu' => false,
'capability_type' => 'post',
'hierarchical' => false,
'supports' => array('title', 'editor', 'author', 'custom-fields'),
'has_archive' => false,
'menu_position' => null,
'show_in_rest' => false,
);
register_post_type('wp_note', $args);
// Register taxonomy for note categories
$tax_labels = array(
'name' => __('Note Categories', 'a-wp-notes'),
'singular_name' => __('Note Category', 'a-wp-notes'),
'search_items' => __('Search Categories', 'a-wp-notes'),
'all_items' => __('All Categories', 'a-wp-notes'),
'parent_item' => __('Parent Category', 'a-wp-notes'),
'parent_item_colon' => __('Parent Category:', 'a-wp-notes'),
'edit_item' => __('Edit Category', 'a-wp-notes'),
'update_item' => __('Update Category', 'a-wp-notes'),
'add_new_item' => __('Add New Category', 'a-wp-notes'),
'new_item_name' => __('New Category Name', 'a-wp-notes'),
'menu_name' => __('Categories', 'a-wp-notes'),
);
register_taxonomy('wp_note_category', 'wp_note', array(
'hierarchical' => true,
'labels' => $tax_labels,
// Hidden alongside the parent CPT — live UI doesn't surface
// categories yet, and showing the taxonomy in admin with the
// parent hidden would just dead-link.
'show_ui' => false,
'show_admin_column' => false,
'query_var' => true,
'show_in_rest' => false,
));
}
add_action('init', 'wp_notes_register_cpt');
// Add custom meta box for note properties
function wp_notes_add_meta_boxes() {
add_meta_box(
'wp_notes_properties',
__('Note Properties', 'a-wp-notes'),
'wp_notes_render_properties_meta_box',
'wp_note',
'side',
'high'
);
}
add_action('add_meta_boxes', 'wp_notes_add_meta_boxes');
// Render meta box content
function wp_notes_render_properties_meta_box($post) {
// Add nonce for security
wp_nonce_field('wp_notes_meta_box', 'wp_notes_meta_box_nonce');
// Get current values
$color = get_post_meta($post->ID, '_wp_note_color', true) ?: '#000000';
$size = get_post_meta($post->ID, '_wp_note_size', true) ?: '16';
$font = get_post_meta($post->ID, '_wp_note_font', true) ?: 'Arial';
$status = get_post_meta($post->ID, '_wp_note_status', true) ?: 'active';
?>
<p>
<label for="wp_note_color"><?php _e('Color:', 'a-wp-notes'); ?></label><br>
<input type="color" id="wp_note_color" name="wp_note_color" value="<?php echo esc_attr($color); ?>">
</p>
<p>
<label for="wp_note_size"><?php _e('Size:', 'a-wp-notes'); ?></label><br>
<input type="number" id="wp_note_size" name="wp_note_size" value="<?php echo esc_attr($size); ?>" min="8" max="72">
</p>
<p>
<label for="wp_note_font"><?php _e('Font:', 'a-wp-notes'); ?></label><br>
<select id="wp_note_font" name="wp_note_font">
<option value="Arial" <?php selected($font, 'Arial'); ?>>Arial</option>
<option value="Helvetica" <?php selected($font, 'Helvetica'); ?>>Helvetica</option>
<option value="Times New Roman" <?php selected($font, 'Times New Roman'); ?>>Times New Roman</option>
<option value="Verdana" <?php selected($font, 'Verdana'); ?>>Verdana</option>
</select>
</p>
<p>
<label for="wp_note_status"><?php _e('Status:', 'a-wp-notes'); ?></label><br>
<select id="wp_note_status" name="wp_note_status">
<option value="active" <?php selected($status, 'active'); ?>><?php _e('Active', 'a-wp-notes'); ?></option>
<option value="completed" <?php selected($status, 'completed'); ?>><?php _e('Completed', 'a-wp-notes'); ?></option>
</select>
</p>
<?php
}
// Save meta box data
function wp_notes_save_meta_box_data($post_id) {
// Check if our nonce is set and verify it
if (!isset($_POST['wp_notes_meta_box_nonce']) ||
!wp_verify_nonce($_POST['wp_notes_meta_box_nonce'], 'wp_notes_meta_box')) {
return;
}
// Don't save during autosave
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
// Check user permissions
if (!current_user_can('edit_post', $post_id)) {
return;
}
// Save the meta fields
$fields = array(
'_wp_note_color' => 'sanitize_hex_color',
'_wp_note_size' => 'absint',
'_wp_note_font' => 'sanitize_text_field',
'_wp_note_status' => 'sanitize_text_field'
);
foreach ($fields as $key => $sanitize_callback) {
if (isset($_POST[str_replace('_', '', $key)])) {
$value = call_user_func($sanitize_callback, $_POST[str_replace('_', '', $key)]);
update_post_meta($post_id, $key, $value);
}
}
}
add_action('save_post_wp_note', 'wp_notes_save_meta_box_data');
// Data Migration Tool
function wp_notes_migrate_to_cpt() {
$existing_notes = get_option('wp_notes', array());
$done_notes = get_option('wp_done_notes', array());
$migrated_count = 0;
$errors = array();
// Start migration
foreach ($existing_notes as $note) {
$post_data = array(
'post_title' => wp_trim_words($note['text'], 10, '...'),
'post_content' => $note['text'],
'post_status' => 'publish',
'post_type' => 'wp_note',
'post_author' => $note['author'] ?? get_current_user_id(),
);
// Insert the post
$post_id = wp_insert_post($post_data, true);
if (!is_wp_error($post_id)) {
// Add note meta data
update_post_meta($post_id, '_wp_note_color', $note['color'] ?? '#000000');
update_post_meta($post_id, '_wp_note_size', $note['size'] ?? '16');
update_post_meta($post_id, '_wp_note_font', $note['font'] ?? 'Arial');
update_post_meta($post_id, '_wp_note_status', 'active');
update_post_meta($post_id, '_wp_note_created', $note['timestamp'] ?? '');
update_post_meta($post_id, '_wp_note_author_name', $note['author_name'] ?? '');
$migrated_count++;
} else {
$errors[] = $post_id->get_error_message();
}
}
// Migrate completed notes
foreach ($done_notes as $note) {
$post_data = array(
'post_title' => wp_trim_words($note['text'], 10, '...'),
'post_content' => $note['text'],
'post_status' => 'publish',
'post_type' => 'wp_note',
'post_author' => $note['author'] ?? get_current_user_id(),
);
$post_id = wp_insert_post($post_data, true);
if (!is_wp_error($post_id)) {
update_post_meta($post_id, '_wp_note_color', $note['color'] ?? '#000000');
update_post_meta($post_id, '_wp_note_size', $note['size'] ?? '16');
update_post_meta($post_id, '_wp_note_font', $note['font'] ?? 'Arial');
update_post_meta($post_id, '_wp_note_status', 'completed');
update_post_meta($post_id, '_wp_note_created', $note['timestamp'] ?? '');
update_post_meta($post_id, '_wp_note_completed_by', $note['completed_by'] ?? '');
update_post_meta($post_id, '_wp_note_completed_on', $note['completed_on'] ?? '');
update_post_meta($post_id, '_wp_note_author_name', $note['author_name'] ?? '');
$migrated_count++;
} else {
$errors[] = $post_id->get_error_message();
}
}
// Create backup of old data
update_option('wp_notes_pre_migration_backup', array(
'notes' => $existing_notes,
'done_notes' => $done_notes,
'migration_date' => current_time('mysql')
));
return array(
'success' => true,
'migrated_count' => $migrated_count,
'errors' => $errors
);
}
// Add migration notice for admin
function wp_notes_migration_notice() {
if (!current_user_can('manage_options')) {
return;
}
$migrated = get_option('wp_notes_migration_completed');
if (!$migrated) {
?>
<div class="notice notice-info is-dismissible">
<p>
<?php _e('Logbook needs to migrate your existing notes to the new storage system.', 'a-wp-notes'); ?>
<a href="<?php echo esc_url(admin_url('admin.php?page=wp-notes-settings&action=migrate')); ?>" class="button button-primary">
<?php _e('Start Migration', 'a-wp-notes'); ?>
</a>
</p>
</div>
<?php
}
}
add_action('admin_notices', 'wp_notes_migration_notice');
// Handle migration request
function wp_notes_handle_migration() {
if (isset($_GET['page']) && $_GET['page'] === 'wp-notes-settings' &&
isset($_GET['action']) && $_GET['action'] === 'migrate') {
check_admin_referer('wp_notes_migration');
$result = wp_notes_migrate_to_cpt();
if ($result['success']) {
update_option('wp_notes_migration_completed', true);
add_settings_error(
'wp_notes_migration',
'migration_success',
sprintf(
__('Successfully migrated %d notes. %s', 'a-wp-notes'),
$result['migrated_count'],
!empty($result['errors']) ? 'Errors: ' . implode(', ', $result['errors']) : ''
),
'updated'
);
} else {
add_settings_error(
'wp_notes_migration',
'migration_error',
__('Migration failed. Please try again or contact support.', 'a-wp-notes'),
'error'
);
}
wp_redirect(admin_url('admin.php?page=wp-notes-settings'));
exit;
}
}
add_action('admin_init', 'wp_notes_handle_migration');
// End of wp-notes.php
?>