PluginProbe
Add Code To Head / trunk
Add Code To Head vtrunk
trunk 1.07 1.09 1.13 1.15 1.17 1.23
add-code-to-head / add-code-to-head.php

add-code-to-head.php in Add Code To Head trunk, at add-code-to-head.php

253 lines 8.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Name: Add Code to Head
4 * Plugin URI: http://hbjitney.com/add-code-to-header.html
5 * Description: Adds custom HTML code (JavaScript, CSS, etc.) to each public page's &lt;head&gt;.
6 * Version: 1.23
7 * Author: HBJitney, LLC
8 * Author URI: http://hbjitney.com/
9 * License: GPL-3.0-or-later
10 * License URI: https://www.gnu.org/licenses/gpl-3.0.html
11 * Text Domain: add-code-to-head
12 *
13 * This program is free software: you can redistribute it and/or modify
14 * it under the terms of the GNU General Public License as published by
15 * the Free Software Foundation, either version 3 of the License, or
16 * (at your option) any later version.
17 *
18 * This program is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public License
24 * along with this program. If not, see <https://www.gnu.org/licenses/>.
25 */
26
27 // Block direct file access. Prevent direct execution.
28 defined( 'ABSPATH' ) || exit;
29
30 define( 'ACTH_OPTION_KEY', 'acth_options' );
31 define( 'ACTH_VERSION', '1.20' );
32
33 /**
34 * Main plugin class.
35 *
36 * Wraps all hooks and callbacks to avoid polluting the global namespace.
37 */
38 class AddCodeToHead {
39
40 /** @var AddCodeToHead|null Singleton instance. */
41 private static ?AddCodeToHead $instance = null;
42
43 /**
44 * Return (and, on first call, create) the singleton instance.
45 *
46 * @return AddCodeToHead
47 */
48 public static function get_instance(): AddCodeToHead {
49 if ( null === self::$instance ) {
50 self::$instance = new self();
51 }
52 return self::$instance;
53 }
54
55 /**
56 * Private constructor — use AddCodeToHead::get_instance() instead.
57 * Registers all WordPress action/filter hooks.
58 */
59 private function __construct() {
60 add_action( 'admin_menu', array( $this, 'add_admin' ) );
61 add_action( 'admin_init', array( $this, 'admin_init' ) );
62 add_action( 'wp_head', array( $this, 'display' ) );
63 // Add "Settings" shortcut link on the Plugins list screen.
64 add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ),
65 array( $this, 'add_settings_link' ) );
66 }
67
68 // -------------------------------------------------------------------------
69 // Admin menu
70 // -------------------------------------------------------------------------
71
72 /**
73 * Register the options page under Settings.
74 */
75 public function add_admin(): void {
76 add_options_page(
77 __( 'Add Code to Head', 'add-code-to-head' ),
78 __( 'Add Code to Head', 'add-code-to-head' ),
79 'manage_options',
80 'acth_plugin',
81 array( $this, 'plugin_options_page' )
82 );
83 }
84
85 /**
86 * Inject our "Settings" link on the Installed Plugins page
87 *
88 * @param string[] $links Existing action links.
89 * @return string[]
90 */
91 public function add_settings_link( array $links ): array {
92 $settings_link = sprintf(
93 '<a href="%s">%s</a>',
94 esc_url( admin_url( 'options-general.php?page=acth_plugin' ) ),
95 esc_html__( 'Settings', 'add-code-to-head' )
96 );
97 array_unshift( $links, $settings_link );
98 return $links;
99 }
100
101 // -------------------------------------------------------------------------
102 // Options page rendering
103 // -------------------------------------------------------------------------
104
105 /**
106 * Render the options page.
107 * Capability is already enforced by add_options_page(), but we re-check
108 * here as a belt-and-suspenders guard before outputting anything.
109 */
110 public function plugin_options_page(): void {
111 if ( ! current_user_can( 'manage_options' ) ) {
112 return;
113 }
114 ?>
115 <div class="wrap">
116 <h1><?php esc_html_e( 'Add Code to Head', 'add-code-to-head' ); ?></h1>
117 <form action="options.php" method="post">
118 <?php
119 settings_fields( ACTH_OPTION_KEY );
120 do_settings_sections( 'acth_plugin' );
121 submit_button( __( 'Save Changes', 'add-code-to-head' ) );
122 ?>
123 </form>
124 </div>
125 <?php
126 }
127
128 // -------------------------------------------------------------------------
129 // Settings API registration
130 // -------------------------------------------------------------------------
131
132 public function admin_init(): void {
133 register_setting(
134 ACTH_OPTION_KEY,
135 ACTH_OPTION_KEY,
136 array( $this, 'options_validate' )
137 );
138
139 add_settings_section(
140 'acth_section',
141 '', // No section title
142 '__return_null',
143 'acth_plugin',
144 array(
145 // Note: before_section requires WP 6.1+
146 'before_section' => $this->warning_html(),
147 )
148 );
149
150 add_settings_field(
151 'acth_string',
152 __( 'Code', 'add-code-to-head' ),
153 array( $this, 'text_field' ),
154 'acth_plugin',
155 'acth_section'
156 );
157 }
158
159 /**
160 * Build the warning banner shown above the textarea.
161 * Kept as a method so the string is constructed once
162 * and can be unit-tested.
163 *
164 * @return string Safe HTML string.
165 */
166 private function warning_html(): string {
167 return sprintf(
168 '<div class="notice notice-warning"><p><strong>%s</strong><br>%s</p><p>%s</p></div>',
169 esc_html__( '⚠️ WARNING:', 'add-code-to-head' ),
170 esc_html__( 'All code entered here is added to every public page on the blog. Any script you add here will run on every page, for every visitor.', 'add-code-to-head' ),
171 esc_html__( 'Only Trusted Administrators should use this function.', 'add-code-to-head' )
172 );
173 }
174
175 // -------------------------------------------------------------------------
176 // Field rendering
177 // -------------------------------------------------------------------------
178
179 /**
180 * Render the textarea field.
181 */
182 public function text_field(): void {
183 $options = get_option( ACTH_OPTION_KEY );
184 $val = isset( $options['text_string'] ) ? $options['text_string'] : '';
185 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- esc_textarea() applied.
186 printf(
187 '<textarea id="%s" name="%s[text_string]" rows="20" cols="90">%s</textarea>',
188 esc_attr( ACTH_OPTION_KEY ),
189 esc_attr( ACTH_OPTION_KEY ),
190 esc_textarea( $val )
191 );
192 }
193
194 // -------------------------------------------------------------------------
195 // Sanitization / validation
196 // -------------------------------------------------------------------------
197
198 /**
199 * Sanitize incoming option value before it is stored.
200 *
201 * Users with `unfiltered_html` (administrators on single-site installs) may
202 * save arbitrary markup — that is the intentional purpose of this plugin.
203 * All other roles have their input stripped to safe post-level HTML.
204 *
205 * @param array $input Raw POST input.
206 * @return array Sanitized options array.
207 */
208 public function options_validate( array $input ): array {
209 $new = array();
210 $new['text_string'] = isset( $input['text_string'] ) ? trim( $input['text_string'] ) : '';
211
212 if ( ! current_user_can( 'unfiltered_html' ) ) {
213 $new['text_string'] = wp_kses_post( $new['text_string'] );
214 }
215
216 return $new;
217 }
218
219 // -------------------------------------------------------------------------
220 // Front-end output
221 // -------------------------------------------------------------------------
222
223 /**
224 * Echo the saved code into the public <head>.
225 *
226 * The is_admin() guard prevents accidental output if hook timing ever changes.
227 *
228 * NOTE: Output is intentionally NOT escaped — arbitrary HTML/JS injection
229 * into the <head> is the entire purpose of this plugin. Access is controlled
230 * at save-time via options_validate() and the manage_options capability.
231 */
232 public function display(): void {
233 if ( is_admin() ) {
234 return;
235 }
236
237 $options = get_option( ACTH_OPTION_KEY );
238 $code = isset( $options['text_string'] ) ? $options['text_string'] : '';
239
240 if ( '' !== $code ) {
241 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
242 echo "\n" . $code . "\n";
243 }
244 }
245 }
246
247 // ****************************************************************************
248 // * === BOOTSTRAP === *
249 // * Direct instantiation via the singleton factory on the *
250 // * 'plugins_loaded' hook, the correct point to initialize a plugin. *
251 // ****************************************************************************
252 add_action( 'plugins_loaded', array( 'AddCodeToHead', 'get_instance' ) );
253