PluginProbe
MM Title Manager — Hide Page and Post Titles / trunk
MM Title Manager — Hide Page and Post Titles vtrunk
1.12 1.11 trunk 1.10 1.10.1 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.9 1.9.1
hide-titles / hide-titles.php

hide-titles.php in MM Title Manager — Hide Page and Post Titles trunk, at hide-titles.php

533 lines 19.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Name: MM Title Manager
4 * Plugin URI: https://wordpress.org/plugins/hide-titles/
5 * Description: Control the visibility of titles on single posts and pages, globally or per item. Navigation menus always keep their labels.
6 * Version: 1.12
7 * Requires at least: 4.4
8 * Requires PHP: 7.4
9 * Tested up to: 7.1
10 * Author: Mehraz Morshed
11 * Author URI: https://profiles.wordpress.org/mehrazmorshed/
12 * License: GPL v2 or later
13 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
14 * Text Domain: hide-titles
15 * Domain Path: /languages
16 *
17 * @package Hide_Titles
18 */
19
20 /**
21 * MM Title Manager is free software: you can redistribute it and/or modify
22 * it under the terms of the GNU General Public License as published by the
23 * Free Software Foundation, either version 2 of the License, or (at your
24 * option) any later version.
25 *
26 * MM Title Manager is distributed in the hope that it will be useful, but
27 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
28 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
29 * for more details.
30 *
31 * You should have received a copy of the GNU General Public License along
32 * with this program. If not, see <https://www.gnu.org/licenses/>.
33 */
34
35 // Exit if accessed directly.
36 defined( 'ABSPATH' ) || exit;
37
38 /**
39 * Plugin version. Single source of truth for cache busting and enqueues.
40 */
41 define( 'HIDE_TITLES_VERSION', '1.10.1' );
42
43 /**
44 * Option name that stores the global behavior ('nothing' or 'all').
45 */
46 define( 'HIDE_TITLES_OPTION', 'hide-titles-option' );
47
48 /**
49 * Post meta key that stores the per-post/page toggle ('on' or absent).
50 */
51 define( 'HIDE_TITLES_META_KEY', '_hide_title' );
52
53 /**
54 * Load the plugin text domain for translations.
55 *
56 * @since 1.0.0
57 * @return void
58 */
59 function hide_titles_load_textdomain() {
60 load_plugin_textdomain( 'hide-titles', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
61 }
62 add_action( 'init', 'hide_titles_load_textdomain' );
63
64 /**
65 * Register the top-level admin menu page.
66 *
67 * @since 1.2.0
68 * @return void
69 */
70 function hide_titles_option_page() {
71 add_menu_page(
72 __( 'MM Title Manager', 'hide-titles' ),
73 __( 'MM Title Manager', 'hide-titles' ),
74 'manage_options',
75 'hide-titles',
76 'hide_titles_create_page',
77 'dashicons-heading',
78 101
79 );
80 }
81 add_action( 'admin_menu', 'hide_titles_option_page' );
82
83 /**
84 * Enqueue admin styles on the settings screen and the post/page editors.
85 *
86 * @since 1.2.0
87 * @param string $hook_suffix The current admin page hook suffix.
88 * @return void
89 */
90 function hide_titles_style_settings( $hook_suffix ) {
91 unset( $hook_suffix );
92
93 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
94 if ( ! $screen ) {
95 return;
96 }
97
98 $screens = array( 'toplevel_page_hide-titles', 'post', 'page' );
99 if ( ! in_array( $screen->id, $screens, true ) ) {
100 return;
101 }
102
103 wp_enqueue_style(
104 'hide-titles-admin',
105 plugins_url( 'css/hide-titles-settings.css', __FILE__ ),
106 array(),
107 HIDE_TITLES_VERSION
108 );
109 }
110 add_action( 'admin_enqueue_scripts', 'hide_titles_style_settings' );
111
112 /**
113 * Render the settings page.
114 *
115 * @since 1.2.0
116 * @return void
117 */
118 function hide_titles_create_page() {
119 $current = get_option( HIDE_TITLES_OPTION, 'nothing' );
120 ?>
121 <div class="hide_titles_main">
122 <div class="hide_titles_body hide_titles_common">
123 <header class="ht-mm-hero">
124 <p class="ht-mm-hero__eyebrow"><?php esc_html_e( 'MM Title Manager', 'hide-titles' ); ?></p>
125 <h1 class="ht-mm-hero__title" id="page-title"><?php esc_html_e( 'Title visibility', 'hide-titles' ); ?></h1>
126 <p class="ht-mm-hero__lead"><?php esc_html_e( 'Control titles on single posts and pages. Navigation menus always keep their labels.', 'hide-titles' ); ?></p>
127 </header>
128
129 <form class="ht-mm-form" action="options.php" method="post">
130 <?php wp_nonce_field( 'update-options' ); ?>
131
132 <fieldset class="ht-mm-fieldset">
133 <legend class="ht-mm-fieldset__legend"><?php esc_html_e( 'Global behavior', 'hide-titles' ); ?></legend>
134 <p class="ht-mm-fieldset__hint"><?php esc_html_e( 'Applies site-wide unless you override per post or page in the editor.', 'hide-titles' ); ?></p>
135
136 <div class="ht-mm-options">
137 <label class="ht-mm-card">
138 <input type="radio" name="hide-titles-option" id="hide-titles-option-nothing" value="nothing" <?php checked( $current, 'nothing' ); ?> />
139 <span class="ht-mm-card__icon dashicons dashicons-visibility" aria-hidden="true"></span>
140 <span class="ht-mm-card__text">
141 <strong class="ht-mm-card__title"><?php esc_html_e( 'Show all titles', 'hide-titles' ); ?></strong>
142 <span class="ht-mm-card__desc"><?php esc_html_e( 'Default WordPress title output everywhere.', 'hide-titles' ); ?></span>
143 </span>
144 </label>
145
146 <label class="ht-mm-card">
147 <input type="radio" name="hide-titles-option" id="hide-titles-option-all" value="all" <?php checked( $current, 'all' ); ?> />
148 <span class="ht-mm-card__icon dashicons dashicons-no-alt" aria-hidden="true"></span>
149 <span class="ht-mm-card__text">
150 <strong class="ht-mm-card__title"><?php esc_html_e( 'Hide all titles', 'hide-titles' ); ?></strong>
151 <span class="ht-mm-card__desc"><?php esc_html_e( 'Removes the title on single posts and pages; menus still show link text.', 'hide-titles' ); ?></span>
152 </span>
153 </label>
154 </div>
155 </fieldset>
156
157 <input type="hidden" name="action" value="update" />
158 <input type="hidden" name="page_options" value="hide-titles-option" />
159
160 <p class="ht-mm-actions">
161 <button type="submit" name="submit" class="button button-primary button-hero ht-mm-submit"><?php esc_html_e( 'Save changes', 'hide-titles' ); ?></button>
162 </p>
163 </form>
164
165 <section class="ht-mm-tip" aria-labelledby="ht-mm-per-post-heading">
166 <h2 id="ht-mm-per-post-heading" class="ht-mm-tip__title"><?php esc_html_e( 'Per post or page', 'hide-titles' ); ?></h2>
167 <p class="ht-mm-tip__text"><?php esc_html_e( 'Edit any post or page and use the MM Title Manager box in the sidebar to hide only that item’s title. Menu labels stay visible.', 'hide-titles' ); ?></p>
168 <figure class="ht-mm-tip__figure">
169 <img class="screenshot" src="<?php echo esc_url( plugin_dir_url( __FILE__ ) . 'img/single.png' ); ?>" alt="" />
170 </figure>
171 </section>
172 </div>
173
174 <div class="hide_titles_aside hide_titles_common">
175 <h2 class="aside-title"><?php esc_html_e( 'About Plugin Author', 'hide-titles' ); ?></h2>
176 <div class="author-card">
177 <a class="link" href="https://profiles.wordpress.org/mehrazmorshed/" target="_blank" rel="noopener noreferrer">
178 <img class="center" src="<?php echo esc_url( plugin_dir_url( __FILE__ ) . 'img/author.png' ); ?>" width="128" height="128" alt="" />
179 <span class="author-title author-title--name"><?php esc_html_e( 'Mehraz Morshed', 'hide-titles' ); ?></span>
180 <span class="author-title author-title--role"><?php esc_html_e( 'WordPress Developer', 'hide-titles' ); ?></span>
181 </a>
182 <nav class="author-socials" aria-label="<?php esc_attr_e( 'Author social links', 'hide-titles' ); ?>">
183 <a class="link" href="https://www.facebook.com/mehrazmorshed" target="_blank" rel="noopener noreferrer"><span class="dashicons dashicons-facebook" aria-hidden="true"></span><span class="screen-reader-text"><?php esc_html_e( 'Facebook', 'hide-titles' ); ?></span></a>
184 <a class="link" href="https://twitter.com/mehrazmorshed" target="_blank" rel="noopener noreferrer"><span class="dashicons dashicons-twitter" aria-hidden="true"></span><span class="screen-reader-text"><?php esc_html_e( 'Twitter', 'hide-titles' ); ?></span></a>
185 <a class="link" href="https://www.linkedin.com/in/mehrazmorshed" target="_blank" rel="noopener noreferrer"><span class="dashicons dashicons-linkedin" aria-hidden="true"></span><span class="screen-reader-text"><?php esc_html_e( 'LinkedIn', 'hide-titles' ); ?></span></a>
186 <a class="link" href="https://www.youtube.com/@mehrazmorshed" target="_blank" rel="noopener noreferrer"><span class="dashicons dashicons-youtube" aria-hidden="true"></span><span class="screen-reader-text"><?php esc_html_e( 'YouTube', 'hide-titles' ); ?></span></a>
187 </nav>
188 </div>
189
190 <h3 class="aside-title"><?php esc_html_e( 'Other Useful Plugins', 'hide-titles' ); ?></h3>
191 <div class="author-card">
192 <a class="link" href="https://wordpress.org/plugins/turn-off-comments/" target="_blank" rel="noopener noreferrer">
193 <span class="dashicons dashicons-admin-plugins" aria-hidden="true"></span> <b><?php esc_html_e( 'Disable Comments', 'hide-titles' ); ?></b>
194 </a>
195 <hr />
196 <a class="link" href="https://wordpress.org/plugins/hide-admin-navbar/" target="_blank" rel="noopener noreferrer">
197 <span class="dashicons dashicons-admin-plugins" aria-hidden="true"></span> <b><?php esc_html_e( 'Hide Admin Toolbar', 'hide-titles' ); ?></b>
198 </a>
199 <hr />
200 <a class="link" href="https://wordpress.org/plugins/about-post-author/" target="_blank" rel="noopener noreferrer">
201 <span class="dashicons dashicons-admin-plugins" aria-hidden="true"></span> <b><?php esc_html_e( 'Simple Author Box', 'hide-titles' ); ?></b>
202 </a>
203 </div>
204
205 <h3 class="aside-title"><?php esc_html_e( 'Support', 'hide-titles' ); ?></h3>
206 <a class="link" href="https://www.buymeacoffee.com/mehrazmorshed" target="_blank" rel="noopener noreferrer">
207 <button type="button" class="button button-primary btn"><?php esc_html_e( 'Donate to this plugin', 'hide-titles' ); ?></button>
208 </a>
209 <p class="ht-mm-or"><span><?php esc_html_e( 'or', 'hide-titles' ); ?></span></p>
210 <a class="link" href="https://wordpress.org/support/plugin/hide-titles/reviews/#new-post" target="_blank" rel="noopener noreferrer">
211 <button type="button" class="button button-secondary btn"><?php esc_html_e( 'Leave a review', 'hide-titles' ); ?></button>
212 </a>
213 </div>
214 </div>
215 <?php
216 }
217
218 /**
219 * Filter post and page titles to hide them when configured.
220 *
221 * Handles both the global "hide all" setting and the per-post toggle in a
222 * single, well-guarded callback:
223 *
224 * - Titles are never altered inside the admin.
225 * - The per-post toggle hides that item's title wherever it renders.
226 * - The global mode only blanks the main title of the current singular
227 * front-end view, so archives, widgets, and secondary loops keep their
228 * titles. Navigation menu labels are restored separately via
229 * {@see hide_titles_restore_nav_menu_title()}.
230 *
231 * @since 1.10.1 Merged the previous global and per-post handlers and added
232 * the admin and singular-context guards (returns an empty
233 * string instead of boolean false).
234 *
235 * @param string $title The post title.
236 * @param int|null $post_id Optional. The post ID. Default null.
237 * @return string The (possibly emptied) title.
238 */
239 function hide_titles_filter_the_title( $title, $post_id = null ) {
240 if ( is_admin() ) {
241 return $title;
242 }
243
244 $post_id = (int) $post_id;
245 if ( $post_id < 1 ) {
246 return $title;
247 }
248
249 // Per-post/page override: applies wherever that item's title renders.
250 if ( 'on' === get_post_meta( $post_id, HIDE_TITLES_META_KEY, true ) ) {
251 return '';
252 }
253
254 // Global mode: only blank the main title of the current singular view.
255 if ( 'all' === get_option( HIDE_TITLES_OPTION, 'nothing' )
256 && is_singular()
257 && get_queried_object_id() === $post_id
258 ) {
259 return '';
260 }
261
262 return $title;
263 }
264 add_filter( 'the_title', 'hide_titles_filter_the_title', 10, 2 );
265
266 /**
267 * Restore menu link text when a title is hidden via the_title.
268 *
269 * WordPress runs post titles through `the_title` when building menu items
270 * (see wp_setup_nav_menu_item). Without this, a per-post hidden title would
271 * become blank in navigation menus. We keep the stored navigation label, or
272 * the raw post title, for display in menus only.
273 *
274 * @since 1.10.0
275 *
276 * @param string $title The menu item's title.
277 * @param object $menu_item The menu item object.
278 * @param stdClass $args An object of wp_nav_menu() arguments.
279 * @param int $depth Depth of menu item.
280 * @return string
281 */
282 function hide_titles_restore_nav_menu_title( $title, $menu_item, $args, $depth ) {
283 unset( $args, $depth );
284
285 if ( ! is_object( $menu_item ) || 'post_type' !== $menu_item->type || empty( $menu_item->object_id ) ) {
286 return $title;
287 }
288
289 $object_id = (int) $menu_item->object_id;
290 if ( $object_id < 1 ) {
291 return $title;
292 }
293
294 $hide_globally = ( 'all' === get_option( HIDE_TITLES_OPTION, 'nothing' ) );
295 $hide_per_post = ( 'on' === get_post_meta( $object_id, HIDE_TITLES_META_KEY, true ) );
296
297 if ( ! $hide_globally && ! $hide_per_post ) {
298 return $title;
299 }
300
301 if ( isset( $menu_item->post_title ) && '' !== $menu_item->post_title ) {
302 return $menu_item->post_title;
303 }
304
305 $raw = get_post_field( 'post_title', $object_id, 'raw' );
306
307 return ( is_string( $raw ) && '' !== $raw ) ? $raw : $title;
308 }
309 add_filter( 'nav_menu_item_title', 'hide_titles_restore_nav_menu_title', 10, 4 );
310
311 /**
312 * Register the editor meta box for the per-post toggle.
313 *
314 * @since 1.6.0
315 * @return void
316 */
317 function hide_titles_meta_box() {
318 add_meta_box(
319 'hide-title-meta-box',
320 __( 'MM Title Manager', 'hide-titles' ),
321 'hide_titles_meta_box_callback',
322 array( 'post', 'page' ),
323 'side',
324 'default'
325 );
326 }
327 add_action( 'add_meta_boxes', 'hide_titles_meta_box' );
328
329 /**
330 * Render the meta box.
331 *
332 * @since 1.6.0
333 * @param WP_Post $post The post being edited.
334 * @return void
335 */
336 function hide_titles_meta_box_callback( $post ) {
337 $value = get_post_meta( $post->ID, HIDE_TITLES_META_KEY, true );
338 wp_nonce_field( 'hide_titles_meta_save', 'hide_titles_meta_nonce' );
339 ?>
340 <div class="ht-mm-meta">
341 <label class="ht-mm-meta__toggle" for="hide-titles-checkbox">
342 <input type="checkbox" id="hide-titles-checkbox" name="hide_titles_checkbox" <?php checked( $value, 'on' ); ?> />
343 <span class="ht-mm-meta__label"><?php esc_html_e( 'Hide title on this page', 'hide-titles' ); ?></span>
344 </label>
345 <p class="ht-mm-meta__hint"><?php esc_html_e( 'Hides the title in the content area only. Navigation menus and the admin list keep their titles.', 'hide-titles' ); ?></p>
346 <p class="ht-mm-meta__review"><a href="https://wordpress.org/support/plugin/hide-titles/reviews/#new-post" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Leave a review', 'hide-titles' ); ?></a></p>
347 </div>
348 <?php
349 }
350
351 /**
352 * Save the meta box value.
353 *
354 * @since 1.6.0
355 * @param int $post_id The post ID being saved.
356 * @return void
357 */
358 function hide_titles_save_meta( $post_id ) {
359 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
360 return;
361 }
362
363 if ( ! isset( $_POST['hide_titles_meta_nonce'] )
364 || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['hide_titles_meta_nonce'] ) ), 'hide_titles_meta_save' )
365 ) {
366 return;
367 }
368
369 if ( ! current_user_can( 'edit_post', $post_id ) ) {
370 return;
371 }
372
373 if ( isset( $_POST['hide_titles_checkbox'] ) ) {
374 update_post_meta( $post_id, HIDE_TITLES_META_KEY, 'on' );
375 } else {
376 delete_post_meta( $post_id, HIDE_TITLES_META_KEY );
377 }
378 }
379 add_action( 'save_post', 'hide_titles_save_meta' );
380
381 /**
382 * Runs on plugin activation.
383 *
384 * Records the installation datetime once and sets a one-time flag used to
385 * redirect the user to the settings page after activation.
386 *
387 * @since 1.10.1 Merged the two former activation callbacks.
388 * @return void
389 */
390 function hide_titles_activate() {
391 if ( ! get_option( 'hide_titles_installed' ) ) {
392 update_option( 'hide_titles_installed', current_time( 'mysql' ), false );
393 }
394
395 add_option( 'hide_titles_plugin_do_activation_redirect', true, '', false );
396 }
397 register_activation_hook( __FILE__, 'hide_titles_activate' );
398
399 /**
400 * Redirect to the settings page once, immediately after activation.
401 *
402 * @since 1.2.0
403 * @return void
404 */
405 function hide_titles_plugin_redirect() {
406 if ( ! get_option( 'hide_titles_plugin_do_activation_redirect', false ) ) {
407 return;
408 }
409
410 delete_option( 'hide_titles_plugin_do_activation_redirect' );
411
412 // Do not redirect during bulk (multi) plugin activation.
413 if ( ! isset( $_GET['active-multi'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only presence check, no data processed.
414 wp_safe_redirect( admin_url( 'admin.php?page=hide-titles' ) );
415 exit;
416 }
417 }
418 add_action( 'admin_init', 'hide_titles_plugin_redirect' );
419
420 /**
421 * Clean up all plugin data on uninstall.
422 *
423 * @since 1.10.1 Now removes every option the plugin creates and sweeps the
424 * per-post meta so no orphaned rows are left behind.
425 * @return void
426 */
427 function hide_titles_uninstall() {
428 delete_option( 'hide_titles_installed' );
429 delete_option( HIDE_TITLES_OPTION );
430 delete_option( 'hide_titles_plugin_do_activation_redirect' );
431 delete_post_meta_by_key( HIDE_TITLES_META_KEY );
432 }
433 register_uninstall_hook( __FILE__, 'hide_titles_uninstall' );
434
435 /**
436 * Handle the "dismiss" action for the migration notice.
437 *
438 * @since 1.10.1
439 * @return void
440 */
441 function hide_titles_handle_migration_dismiss() {
442 if ( ! isset( $_GET['hide_titles_dismiss_migration'], $_GET['_wpnonce'] ) ) {
443 return;
444 }
445
446 if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'hide_titles_dismiss_migration' ) ) {
447 return;
448 }
449
450 $user_id = get_current_user_id();
451 if ( $user_id ) {
452 update_user_meta( $user_id, 'hide_titles_migration_dismissed', 1 );
453 }
454
455 wp_safe_redirect( remove_query_arg( array( 'hide_titles_dismiss_migration', '_wpnonce' ) ) );
456 exit;
457 }
458 add_action( 'admin_init', 'hide_titles_handle_migration_dismiss' );
459
460 /**
461 * Show a migration notice to legacy installs only.
462 *
463 * The notice is shown only when all of the following are true: the successor
464 * plugin is not active, the user has not dismissed it, and the install
465 * predates 30 October 2025. Fresh installs are never nagged.
466 *
467 * @since 1.10.1 Added the dismissal check, guarded is_plugin_active(), moved
468 * to a warning notice, and stopped showing on fresh installs.
469 * @return void
470 */
471 function hide_titles_show_migration_notice() {
472 $user_id = get_current_user_id();
473 if ( $user_id && get_user_meta( $user_id, 'hide_titles_migration_dismissed', true ) ) {
474 return;
475 }
476
477 if ( function_exists( 'is_plugin_active' ) && is_plugin_active( 'daisy-titles/daisy-titles.php' ) ) {
478 return;
479 }
480
481 // Only nudge installs that predate the cutoff. Fresh/unknown installs are left alone.
482 $install_date = get_option( 'hide_titles_installed' );
483 if ( ! $install_date || strtotime( $install_date ) >= strtotime( '2025-10-30' ) ) {
484 return;
485 }
486
487 $activate_url = '';
488 if ( file_exists( WP_PLUGIN_DIR . '/daisy-titles/daisy-titles.php' ) ) {
489 $activate_url = wp_nonce_url(
490 add_query_arg(
491 array(
492 'action' => 'activate',
493 'plugin' => 'daisy-titles/daisy-titles.php',
494 ),
495 admin_url( 'plugins.php' )
496 ),
497 'activate-plugin_daisy-titles/daisy-titles.php'
498 );
499 } else {
500 $activate_url = wp_nonce_url(
501 add_query_arg(
502 array(
503 'action' => 'install-plugin',
504 'plugin' => 'daisy-titles',
505 ),
506 admin_url( 'update.php' )
507 ),
508 'install-plugin_daisy-titles'
509 );
510 }
511
512 $is_installed = ( '' !== $activate_url && false !== strpos( $activate_url, 'action=activate' ) );
513 $button_label = $is_installed
514 ? __( 'Activate Daisy Titles now', 'hide-titles' )
515 : __( 'Migrate to Daisy Titles now', 'hide-titles' );
516
517 $dismiss_url = wp_nonce_url(
518 add_query_arg( 'hide_titles_dismiss_migration', '1' ),
519 'hide_titles_dismiss_migration'
520 );
521 ?>
522 <div class="notice notice-warning">
523 <h4><?php esc_html_e( 'Important notice about MM Title Manager', 'hide-titles' ); ?></h4>
524 <p><?php esc_html_e( 'This plugin is no longer actively maintained. Please migrate to our new, improved plugin “Daisy Titles” for continued support, new features, and future updates.', 'hide-titles' ); ?></p>
525 <p>
526 <a href="<?php echo esc_url( $activate_url ); ?>" class="button button-primary"><?php echo esc_html( $button_label ); ?></a>
527 <a href="<?php echo esc_url( $dismiss_url ); ?>" class="button button-link"><?php esc_html_e( 'Dismiss', 'hide-titles' ); ?></a>
528 </p>
529 </div>
530 <?php
531 }
532 add_action( 'admin_notices', 'hide_titles_show_migration_notice' );
533