• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ideasonpurpose / wp-theme-init / #68

07 Sep 2025 10:20PM UTC coverage: 99.502% (-0.5%) from 100.0%
#68

push

php-coveralls

joemaller
Admin assets on wp-login. Closes #53

1 of 1 new or added line in 1 file covered. (100.0%)

3 existing lines in 1 file now uncovered.

600 of 603 relevant lines covered (99.5%)

1.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.7
/src/ThemeInit.php
1
<?php
2
namespace IdeasOnPurpose;
3

4
class ThemeInit
5
{
6
    /**
7
     * Placeholders for mocking
8
     */
9
    public $ABSPATH;
10
    public $WP_DEBUG = false;
11

12
    public function __construct($options = [])
13
    {
14
        $this->ABSPATH = defined('ABSPATH') ? ABSPATH : getcwd(); // WordPress always defines this
2✔
15
        $this->WP_DEBUG = defined('WP_DEBUG') && WP_DEBUG;
2✔
16

17
        $defaults = ['showIncludes' => true, 'enableComments' => false, 'jQueryMigrate' => true];
2✔
18
        $options = array_merge($defaults, $options);
2✔
19

20
        /**
21
         * De-Howdy the WordPress Admin menu
22
         * NOTE: Changed priority in WP v6.6.1, filter priority bumped from 25 to 9992
23
         * @link https://github.com/WordPress/wordpress-develop/commit/fc71dae8db2c057eab88f026b7b394ab0990ba9e
24
         */
25
        add_filter('admin_bar_menu', [$this, 'deHowdy'], 9992);
2✔
26

27
        /**
28
         * IOP Design Credit
29
         *
30
         * Kinsta also applies this filter with priority 99, but they replace
31
         * the entire string. We need to call ours after theirs.
32
         */
33
        add_filter('admin_footer_text', [$this, 'iopCredit'], 500);
2✔
34

35
        /**
36
         * Disable Remote Block Patterns
37
         * @link https://developer.wordpress.org/block-editor/reference-guides/filters/editor-filters/#block-patterns
38
         * TODO: Should this be in the plugin?
39
         */
40
        add_filter('should_load_remote_block_patterns', '__return_false');
2✔
41

42
        /**
43
         * Disable the Block Directory (suggests third-party blocks from the Block Editor)
44
         * @link https://developer.wordpress.org/block-editor/reference-guides/filters/editor-filters/#block-directory
45
         */
46
        add_action('admin_init', function () {
2✔
47
            remove_action(
1✔
48
                'enqueue_block_editor_assets',
1✔
49
                'wp_enqueue_editor_block_directory_assets'
1✔
50
            );
1✔
51
        });
2✔
52

53
        /**
54
         * Disable WordPress auto-updates
55
         */
56
        add_filter('automatic_updater_disabled', '__return_true');
2✔
57

58
        /**
59
         * Override WP_POST_REVISIONS
60
         *
61
         * Default to 6 revisions
62
         * @link https://developer.wordpress.org/reference/functions/wp_revisions_to_keep/
63
         */
64
        add_filter('wp_revisions_to_keep', fn() => 6);
2✔
65

66
        /**
67
         * Strip version from theme name when reading/writing options
68
         */
69
        $stylesheet = get_option('stylesheet');
2✔
70
        add_filter("option_theme_mods_{$stylesheet}", [$this, 'readOption'], 10, 2);
2✔
71
        add_filter("pre_update_option_theme_mods_{$stylesheet}", [$this, 'writeOption'], 10, 3);
2✔
72

73
        add_action('admin_init', [$this, 'debugFlushRewriteRules']);
2✔
74

75
        $this->cleanWPHead();
2✔
76
        $this->debugFlushRewriteRules();
2✔
77

78
        /**
79
         * Sets JPEG_QUALITY
80
         * Add Imagick\HQ scaling
81
         * Compress all newly added images
82
         */
83
        new ThemeInit\Media();
2✔
84

85
        /**
86
         * Add Post State Labels to WP Admin
87
         *  - Includes "404 Page" label
88
         */
89
        new ThemeInit\Admin\PostStates();
2✔
90

91
        /**
92
         * Add the Template Audit column and wp-admin page
93
         */
94
        new ThemeInit\Admin\TemplateAudit();
2✔
95

96
        /**
97
         * Attempts to set the DISALLOW_FILE_EDIT constant to true (disabling the Theme File Editor)
98
         * or displays a notice when the values is explicitly set to false.
99
         */
100
        new ThemeInit\Admin\DisallowFileEdit();
2✔
101

102
        /**
103
         * Log time of last_login for all users
104
         */
105
        new ThemeInit\Admin\LastLogin();
2✔
106

107
        /**
108
         * Clear stale wordpress_logged_in cookies
109
         */
110
        new ThemeInit\Admin\LoginCookieCleaner();
2✔
111

112
        /**
113
         * Add Metabox Reset buttons to Admin User Profiles
114
         */
115
        new ThemeInit\Admin\ResetMetaboxes();
2✔
116

117
        /**
118
         * Clean up the wp-admin dashboard
119
         */
120
        new ThemeInit\Admin\CleanDashboard();
2✔
121

122
        /**
123
         * Common plugin tweaks
124
         */
125
        new ThemeInit\Plugins\ACF();
2✔
126
        new ThemeInit\Plugins\EnableMediaReplace();
2✔
127
        new ThemeInit\Plugins\SEOFramework();
2✔
128

129
        if ($options['showIncludes'] !== false) {
2✔
130
            new ThemeInit\Debug\ShowIncludes();
2✔
131
        }
132

133
        if ($options['enableComments'] === false) {
2✔
134
            new ThemeInit\Extras\GlobalCommentsDisable();
2✔
135
        }
136

137
        /**
138
         * TODO: EXPERIMENTAL
139
         * Provide a switch to remove jquery-migrate
140
         */
141
        if ($options['jQueryMigrate'] === false) {
2✔
142
            new ThemeInit\Extras\RemoveJQueryMigrate();
1✔
143
        }
144
        new ThemeInit\Extras\Shortcodes();
2✔
145

146
        // TODO: Is this too permissive? Reason not to disable unless WP_ENV == 'development'?
147
        if (class_exists('Kint')) {
2✔
148
            /** @disregard P1014 "undefined property '$enabled_mode'" **/
UNCOV
149
            \Kint::$enabled_mode = false;
×
UNCOV
150
            if ($this->WP_DEBUG) {
×
151
                /** @disregard P1014 "undefined property '$enabled_mode'" **/
UNCOV
152
                \Kint::$enabled_mode = true;
×
153
            }
154
        }
155
        // @codeCoverageIgnoreEnd
156

157
        /**
158
         * Load IOP common i18n text domain 'iopwp'
159
         *
160
         * TODO: Namespace collision?
161
         */
162
        // new WP\I18n();
163
    }
164

165
    /**
166
     * Remove some WP Head garbage
167
     * Many thanks to Soil: https://roots.io/plugins/soil/
168
     */
169
    private function cleanWPHead()
170
    {
171
        remove_action('admin_print_scripts', 'print_emoji_detection_script');
2✔
172
        remove_action('admin_print_styles', 'print_emoji_styles');
2✔
173
        remove_action('wp_head', 'adjacent_posts_rel_link_wp_head', 10);
2✔
174
        remove_action('wp_head', 'feed_links_extra', 3);
2✔
175
        remove_action('wp_head', 'print_emoji_detection_script', 7);
2✔
176
        // remove_action('wp_head', 'rest_output_link_wp_head', 10);
177
        remove_action('wp_head', 'rsd_link');
2✔
178
        remove_action('wp_head', 'wlwmanifest_link');
2✔
179
        remove_action('wp_head', 'wp_generator');
2✔
180
        // remove_action('wp_head', 'wp_oembed_add_discovery_links');
181
        // remove_action('wp_head', 'wp_oembed_add_host_js');
182
        // remove_action('wp_head', 'wp_shortlink_wp_head', 10);
183
        remove_action('wp_print_styles', 'print_emoji_styles');
2✔
184
        remove_filter('comment_text_rss', 'wp_staticize_emoji');
2✔
185
        remove_filter('the_content_feed', 'wp_staticize_emoji');
2✔
186
        remove_filter('wp_mail', 'wp_staticize_emoji_for_email');
2✔
187
    }
188

189
    /**
190
     * Insert design credit into admin footer
191
     * @link https://github.com/WordPress/WordPress/blob/5.8.1/wp-admin/admin-footer.php#L33-L50
192
     */
193
    public function iopCredit($default)
194
    {
195
        if (!$default) {
1✔
196
            $default =
1✔
197
                '<span id="footer-thankyou">Thank you for creating with <a href="https://wordpress.org/">WordPress</a>.</span>';
1✔
198
        }
199

200
        $href = '<a href="https://www.ideasonpurpose.com">Ideas On Purpose</a>';
1✔
201
        $credit = sprintf(__('Design and development by %s.', 'iopwp'), $href);
1✔
202

203
        $default = preg_replace('%\.?</a>.?</span>%', '</a>.</span>', $default);
1✔
204
        return preg_replace('%(\.?)</span>$%', "$1 $credit</span>", $default);
1✔
205
    }
206

207
    /**
208
     * Remove "Howdy" from the WordPress admin bar
209
     */
210
    public function deHowdy($wp_admin_bar)
211
    {
212
        $account_node = $wp_admin_bar->get_node('my-account');
1✔
213
        $account_title = str_replace('Howdy, ', '', $account_node->title);
1✔
214
        $wp_admin_bar->add_node([
1✔
215
            'id' => 'my-account',
1✔
216
            'title' => $account_title,
1✔
217
        ]);
1✔
218
    }
219

220
    /**
221
     * Should be called as late as possible, either shutdown or something right before shutdown
222
     * need to check that it's not breaking JSON or other API-ish responses by appending
223
     * a blob of arbitrary text to the content.
224
     *
225
     * @codeCoverageIgnore
226
     *
227
     */
228
    public function totalExecutionTime()
229
    {
230
        /**
231
                     * Need to be sure we don't dump this into a JSON response or other structured data request
232
                     *
233
                     * TODO: Check code from wp-includes/admin-bar.php for skipping AJAX, JSON, etc.
234
                     *       https://github.com/WordPress/WordPress/blob/42d52ce08099f9fae82a1977da0237b32c863e94/wp-includes/admin-bar.php#L1179-L1181
235
                     *
236
                     *      if ( defined( 'XMLRPC_REQUEST' ) || defined( 'DOING_AJAX' ) || defined( 'IFRAME_REQUEST' ) || wp_is_json_request() ) {
237
                     *
238
                     */
239
        // if (wp_doing_ajax()) {
240
        //     return;
241
        // }
242
        // error_log('SHUTDOWN');
243
        // error_log(print_r($_SERVER, true));
244

245
        // $time = microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'];
246
        // $msg = sprintf('Total processing time: %0.4f seconds', $time);
247
        // echo "\n<!--\n\n$msg\n -->";
248
        // printf('<script>console.log("%%c⏱", "font-weight: bold;", "%s");</script>', $msg);
249
    }
250

251
    /**
252
     * Webpack/Browsersync reload on post save
253
     * Currently attempts to reload if WP_DEBUG is true
254
     * More info:
255
     * https://www.browsersync.io/docs/http-protocol
256
     * https://blogs.oracle.com/fatbloke/networking-in-virtualbox#NAT
257
     * https://superuser.com/a/310745/193584
258
     *
259
     * TODO: This was disabled from init() on 2019-11-06 for a few reasons:
260
     *       1. Since _everything_ is going through the devserver proxy,
261
     *          saving a post in admin will trigger a reload of the page
262
     *          being authored. This breaks the default workflow and causes
263
     *          pops up a number of "Reload site?" alerts.
264
     *
265
     *       2. Trying to reach the 10.0.2.2 Vagrant external IP from Docker
266
     *          was causing a blocking DNS stall for 10 seconds per request.
267
     *          This made the backend nearly unusable.
268
     *
269
     * @codeCoverageIgnore
270
     */
271
    private function browsersyncReload()
272
    {
273
        if (defined('WP_DEBUG') && WP_DEBUG) {
274
            add_action('save_post', function () {
275
                $args = ['blocking' => false, 'sslverify' => false];
276
                // Sloppy, but there's no assurance we're actually serving over ssl
277
                // This hits both possible endpoints and ignores replies, one of these should work
278
                wp_remote_get('http://10.0.2.2:3000/__browser_sync__?method=reload', $args);
279
                wp_remote_get('https://10.0.2.2:3000/__browser_sync__?method=reload', $args);
280

281
                /**
282
                 * /webpack/reload is specific to ideasonpurpose/docker-build
283
                 */
284
                wp_remote_get('http://host.docker.internal:8080/webpack/reload', $args);
285
            });
286
        }
287
    }
288

289
    /**
290
     * Used to auto-update permalinks in development so we don't have to keep
291
     * the permalinks admin page open.  /wp-admin/options-permalink.php
292
     *
293
     * https://developer.wordpress.org/reference/functions/flush_rewrite_rules/
294
     */
295
    public function debugFlushRewriteRules()
296
    {
297
        if ($this->WP_DEBUG) {
5✔
298
            /*
299
             * This code is adapted from wp-includes/admin-bar.php for skipping AJAX, JSON, etc.
300
             *       https://github.com/WordPress/WordPress/blob/42d52ce08099f9fae82a1977da0237b32c863e94/wp-includes/admin-bar.php#L1179-L1181
301
             */
302
            if (
303
                defined('XMLRPC_REQUEST') ||
5✔
304
                defined('DOING_AJAX') ||
5✔
305
                defined('IFRAME_REQUEST') ||
5✔
306
                wp_is_json_request() ||
5✔
307
                is_embed() ||
5✔
308
                !is_admin()
5✔
309
            ) {
310
                return false;
3✔
311
            }
312

313
            $htaccess = file_exists($this->ABSPATH . '.htaccess');
2✔
314
            $htaccess_log = $htaccess ? ' including .htaccess file' : '';
2✔
315

316
            /**
317
             * Log a reminder about flushing rewrite rules every 15 minutes
318
             */
319
            if (get_transient('flush_rewrite_log') === false && !isset($_GET['service-worker'])) {
2✔
320
                error_log(
2✔
321
                    "WP_DEBUG is true: Flushing rewrite rules{$htaccess_log}.\nRequest: {$_SERVER['REQUEST_URI']}"
2✔
322
                );
2✔
323
                set_transient('flush_rewrite_log', true, 15 * MINUTE_IN_SECONDS);
2✔
324
            }
325

326
            flush_rewrite_rules($htaccess);
2✔
327
        }
328
    }
329

330
    /**
331
     * Strip version numbers from theme names when reading/writing options
332
     *
333
     * Our build pipeline outputs versioned themes in directories which look
334
     * something like `{theme-name}-{semver}` where the semver string has dots
335
     * replaced with underscores (workaround for some WP oddity I've forgotten)
336
     * A theme directory might look something like this:
337
     *      `iop-theme-2_3_11`
338
     * Indicating the theme basename is `iop-theme` and the version is `2.3.11`
339
     *
340
     * WordPress stores some options, especially menu assignments, under a key
341
     * derived from the theme directory. The problem is that updating the theme
342
     * using snapshots means the option-name changes and the new theme can't find
343
     * the old settings. The solution here is to strip the version number from the
344
     * option name when writing, then request the version-less option on read.
345
     *
346
     * If we someday decide to use plain version-less theme folders, these filters
347
     * and methods can be removed.
348
     *
349
     * Both filters are called from options.php:
350
     * @link https://github.com/WordPress/WordPress/blob/48f35e42fc790a62d85d2a6e104550fa5a1019b9/wp-includes/option.php#L166-L179
351
     * @link https://github.com/WordPress/WordPress/blob/48f35e42fc790a62d85d2a6e104550fa5a1019b9/wp-includes/option.php#L373-L384
352
     *
353
     * The regex is a minor modification of the officially-sanctioned semver regex
354
     * Modifications include:
355
     *   - Pattern starts with a leading hyphen to match our naming convention
356
     *   - Pattern matches for `(?:\.|_)` instead of just `\.`
357
     *   - Drops the `g` Global and `m` multiline flags
358
     * @link https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
359
     * @link https://regex101.com/r/Ly7O1x/3/
360
     */
361
    public $semverRegex = '/-(?P<major>0|[1-9]\d*)(?:\.|_)(?P<minor>0|[1-9]\d*)(?:\.|_)(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/';
362

363
    public function readOption($val, $opt)
364
    {
365
        $optBase = preg_replace($this->semverRegex, '', $opt);
1✔
366

367
        // if $optBase and $opt match, getting the option will nest infinitely
368
        return $optBase === $opt ? $val : get_option($optBase);
1✔
369
    }
370

371
    public function writeOption($val, $oldVal, $opt)
372
    {
373
        $optBase = preg_replace($this->semverRegex, '', $opt);
1✔
374
        /**
375
         * Because this filter is triggered _from inside_ update_option(),
376
         * calling update_option() again with the same inputs would cause
377
         * WordPress to nest infinitely and crash the server.
378
         *
379
         * We must check that $optBase and $opt are different before we can
380
         * update the value attached to the corrected option name.
381
         */
382
        if ($optBase !== $opt) {
1✔
383
            update_option($optBase, $val);
1✔
384
            /**
385
             * Returning $oldVal short-circuits the original update_option()
386
             * call. Since we've already updated the value under the modified
387
             * name, there's no need to write an extra wp_options entry which
388
             * will never be used.
389
             */
390
            return $oldVal;
1✔
391
        }
392
        return $val;
1✔
393
    }
394
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc