Files
rangerhq-logbook/wp-notes.php
T
ranger 1c93c82ef5 fix: update checker — fall back to /tags when no formal Gitea Release exists (v3.3.2)
The v3.3.0 update checker only queried Gitea's /releases/latest
endpoint, which requires a formal Release object (created via the
Gitea web UI with optional notes + zip assets attached). A plain
"git tag v3.3.x && git push --tags" from the terminal does NOT
create that Release object — so the checker kept returning "No
releases tagged on the Gitea repo yet" even when tags existed.

wp_notes_fetch_latest_release() now falls back to the
/tags?limit=1 endpoint when /releases/latest returns 404 (or any
non-200). It synthesises a release-like payload from the newest
tag — tag_name, html_url pointing at the tag view, tag message as
the body, empty assets[] so the existing download-URL logic falls
through to Gitea's source-archive URL pattern (/archive/<tag>.zip).

Net effect: the "Check now" button now finds the latest version
whether David creates formal Gitea Releases OR just pushes tags
with "git push --tags". No workflow change required.

Discovered while diagnosing why "Check now" wasn't seeing today's
v3.1.0/v3.2.0/v3.3.0/v3.3.1 tags (just pushed in this session)
— the tags were there, the formal Release objects were not.

KNOWN LIMITATION (not a bug — flagged)
The Gitea repo ranger/a-wp-notes-v3 is currently private. Anonymous
API requests get a 404 (Gitea's standard behaviour for private
repos). The updater code is correct but can't actually reach the
API on a private repo without authentication. Fix: change the
repo visibility to public on Gitea — appropriate anyway for a
GPL-licensed plugin headed for the WordPress.org marketplace.

VERSION BUMP
- wp-notes.php header 3.3.1 → 3.3.2
- WP_NOTES_VERSION constant 3.3.1 → 3.3.2
- About page version-history leads with v3.3.2; v3.3.1 demoted

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 09:29:33 +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.2
* 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.2');
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-admin-generic',
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
?>