array(), 'latent' => array(), ); /* * Named by the block in wp-config.php. A block of an earlier format does * not name it, and then this loader does nothing at all - not even * DLOGR_BOOT_DATA, whose absence is what tells the plugin that the block * does not load it. The Status tab then asks for it to be set up again. */ if ( ! defined( 'DLOGR_BASE_DIR' ) ) { return $taken; } $base = rtrim( (string) DLOGR_BASE_DIR, '/\\' ); $data = dlogr_boot_read( $base . '/boot-data.json' ); /* * Told to the plugin, which repairs the file on wp_loaded. Defined in both * branches on purpose: an undefined constant means the block never got * this far, which is a different problem with a different fix. */ if ( ! defined( 'DLOGR_BOOT_DATA' ) ) { define( 'DLOGR_BOOT_DATA', false !== $data ); } if ( false === $data ) { return $taken; } /* * The display mode applies whether logging is on or off. Core only touches * display_errors inside `if ( WP_DEBUG )`, so doing it here is what makes * the setting true to its name. */ $display = false; switch ( $data['display'] ) { case 'on': $display = true; // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged, WordPress.PHP.IniSet.display_errors_Disallowed, PluginCheck.CodeAnalysis.PHPErrorReporting.IniDirectiveDisplay_errors -- This is the plugin's whole purpose: the user asked for errors to be shown. ini_set( 'display_errors', '1' ); break; case 'server': // Null means "leave the server setting alone" - core reads it that way. $display = null; break; default: // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged, WordPress.PHP.IniSet.display_errors_Disallowed, PluginCheck.CodeAnalysis.PHPErrorReporting.IniDirectiveDisplay_errors -- Hiding errors from visitors is the safe default and the reason the setting exists. ini_set( 'display_errors', '0' ); break; } dlogr_boot_claim( 'WP_DEBUG_DISPLAY', $display, $taken ); // Logging switched off: say so explicitly, or the environment type decides for us. if ( empty( $data['enabled'] ) ) { dlogr_boot_claim( 'WP_DEBUG', false, $taken ); dlogr_boot_claim( 'WP_DEBUG_LOG', false, $taken ); return $taken; } /* * The current directory is the one carrying the marker. Its random name * is written down nowhere this loader could read (2026-10-11): the data * file sits at a known address in uploads and holds no secret. */ $dirs = dlogr_boot_marked_dirs( $base ); $dir_ok = ! empty( $dirs ); /* * Reported to the plugin, which recreates the directory or its marker on * wp_loaded under the name stored in the database. This loader is the only * thing that sees the directory go missing on an ordinary request, and * without the constant the finding was lost: the data file is still valid, * so nothing else had a reason to look. Whether a name is stored at all is * the plugin's question - see DLOGR_Boot::maybe_repair(). */ if ( ! defined( 'DLOGR_BOOT_DIR' ) ) { define( 'DLOGR_BOOT_DIR', $dir_ok ); } /* * Two marked directories: the first by name is used, the same one on every * request, and the plugin takes the extra marker off. */ if ( count( $dirs ) > 1 && ! defined( 'DLOGR_BOOT_DIR_MANY' ) ) { define( 'DLOGR_BOOT_DIR_MANY', true ); } /* * No directory: write nowhere rather than let core fall back to * wp-content/debug.log, which nothing protects. This request is lost either * way - the repair above lands on the next one. */ if ( ! $dir_ok ) { dlogr_boot_claim( 'WP_DEBUG', true, $taken ); dlogr_boot_claim( 'WP_DEBUG_LOG', false, $taken ); return $taken; } /* * Built from the base, a name that matched the pattern and one of seven * fixed file names - nothing in the data file takes part. Whoever manages * to rewrite that file still cannot point the log anywhere else. */ $log_path = $base . '/' . $dirs[0] . '/' . dlogr_boot_context_file( $data['rest_prefix'] ); dlogr_boot_claim( 'WP_DEBUG', true, $taken ); dlogr_boot_claim( 'WP_DEBUG_LOG', $log_path, $taken ); /* * The constant alone is not enough: wp_debug_mode() runs from * wp-settings.php, after this whole file. Setting error_log here starts the * recording immediately and catches everything that happens further down in * wp-config.php - which the stock WordPress scheme never sees. */ // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged, WordPress.PHP.IniSet.log_errors_Disallowed -- Routing errors into a file is what this plugin is for. ini_set( 'log_errors', '1' ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged, WordPress.PHP.IniSet.Risky -- Same reason, and it mirrors what core does a few dozen lines later. ini_set( 'error_log', $log_path ); return $taken; }; /** * Declares one of the constants this plugin manages, or records who beat us. * * The `defined()` question was already being asked here before any of this: the * branch that says "yes, somebody was first" is what used to be thrown away. * Our define() does not run in that branch, so the plugin's setting is not in * charge of the constant - whether or not the value happens to match today. * * @param string $name Constant name. * @param mixed $value What this plugin means to set. * @param array $taken Collected names, by group. Passed by reference. * @return void */ function dlogr_boot_claim( $name, $value, array &$taken ) { if ( ! defined( $name ) ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.VariableConstantNameFound -- The three callers pass WP_DEBUG, WP_DEBUG_LOG and WP_DEBUG_DISPLAY: constants of WordPress itself, which this plugin manages by design and must not rename. define( $name, $value ); return; } $group = dlogr_boot_effect( $name, constant( $name ) ) === dlogr_boot_effect( $name, $value ) ? 'latent' : 'conflict'; $taken[ $group ][] = $name; } /** * Reduces a value to what WordPress actually distinguishes about it. * * Comparing raw values would report a conflict for `define( 'WP_DEBUG', 1 )`, * which core reads as exactly what we would have set: it only ever tests the * constant for truth. * * ⚠️ null must survive for WP_DEBUG_DISPLAY. "Leave the server setting alone" * and "force errors hidden" are both falsy and mean different things, and * flattening them to bool would hide a display mode being taken over. * * @param string $name Constant name. * @param mixed $value Value to reduce. * @return mixed Comparable form. */ function dlogr_boot_effect( $name, $value ) { if ( 'WP_DEBUG_DISPLAY' === $name ) { return null === $value ? null : (bool) $value; } /* * A path is the one thing core keeps verbatim: it writes there. Everything * else about WP_DEBUG_LOG is "core's own file" or "do not write". */ if ( 'WP_DEBUG_LOG' === $name ) { return is_string( $value ) ? $value : (bool) $value; } return (bool) $value; } /** * Reads the data file and accepts it only when every value is one it expects. * * The file sits at a known address in uploads, so the values are judged here * rather than trusted: a file that does not look exactly like ours counts as * no file, the plugin is told, and the repair writes it anew. The REST prefix * is only ever searched for in the request path, so any string will do. * * @param string $file Absolute path. * @return array|false */ function dlogr_boot_read( $file ) { if ( ! is_readable( $file ) || ! function_exists( 'json_decode' ) ) { return false; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- WordPress is not loaded yet; a local file of a few dozen bytes. $json = file_get_contents( $file ); $data = false === $json ? null : json_decode( $json, true ); if ( ! is_array( $data ) || ! isset( $data['enabled'], $data['display'], $data['rest_prefix'] ) ) { return false; } if ( ! is_bool( $data['enabled'] ) || ! in_array( $data['display'], array( 'on', 'off', 'server' ), true ) || ! is_string( $data['rest_prefix'] ) ) { return false; } return $data; } /** * Log directories in the plugin's folder that carry the marker. * * Only names of the plugin's own pattern count - which is also what keeps a * planted entry from leading the log anywhere else. * * @param string $base Absolute path of the plugin's folder in uploads. * @return array Directory names, sorted. */ function dlogr_boot_marked_dirs( $base ) { /* * Silenced: a folder that cannot be listed makes scandir() print warnings, * and they would go wherever PHP logs at this point - before our own * redirection, into the server's log on every request. */ // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- See above; false is handled right below. $entries = @scandir( $base ); if ( false === $entries ) { return array(); } $found = array(); foreach ( $entries as $entry ) { if ( preg_match( '/^dlogr-logs-[a-z0-9]{8}$/', $entry ) && is_file( $base . '/' . $entry . '/.dlogr-current' ) ) { $found[] = $entry; } } return $found; } /** * Picks the log file for the current request. * * The order of the checks is load-bearing, not cosmetic. admin-ajax.php defines * both DOING_AJAX and WP_ADMIN, so checking the admin first would send all ajax * into admin.log; cron launched through WP-CLI defines both WP_CLI and * DOING_CRON, and we count that as cron. Do not sort this list. * * @param string $rest_prefix REST prefix as the site reports it. * @return string File name inside the log directory. */ function dlogr_boot_context_file( $rest_prefix ) { if ( defined( 'DOING_CRON' ) && DOING_CRON ) { return 'cron.log'; } if ( dlogr_boot_is_rest( $rest_prefix ) ) { return 'rest.log'; } if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { return 'ajax.log'; } if ( defined( 'WP_CLI' ) && WP_CLI ) { return 'cli.log'; } if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) { return 'xmlrpc.log'; } if ( defined( 'WP_ADMIN' ) && WP_ADMIN ) { return 'admin.log'; } return 'front.log'; } /** * Whether this request is a REST call. * * REST_REQUEST cannot be used: it is defined on the parse_request hook, deep * inside a WordPress that has not been loaded yet. So the request is read the * same two ways WordPress itself routes REST - by path and by the rest_route * parameter, the latter for sites without pretty permalinks. * * @param string $rest_prefix REST prefix as the site reports it. * @return bool */ function dlogr_boot_is_rest( $rest_prefix ) { $prefix = defined( 'DLOGR_REST_PREFIX' ) ? (string) DLOGR_REST_PREFIX : (string) $rest_prefix; $prefix = trim( $prefix, '/' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nothing is written or trusted here: the value only picks a file name. if ( isset( $_GET['rest_route'] ) ) { return true; } if ( '' === $prefix || empty( $_SERVER['REQUEST_URI'] ) ) { return false; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Sanitising functions live in a WordPress that has not loaded yet; the value is only searched for a substring and never stored or printed. $path = (string) $_SERVER['REQUEST_URI']; $mark = strpos( $path, '?' ); if ( false !== $mark ) { $path = substr( $path, 0, $mark ); } // The trailing slash makes /wp-json and /wp-json/v2/... match the same way. return false !== strpos( rtrim( $path, '/' ) . '/', '/' . $prefix . '/' ); } $dlogr_boot_taken = $dlogr_boot(); unset( $dlogr_boot ); /* * Declared here rather than inside the closure because the closure has five * ways out and a constant cannot be declared twice. Absence means "none of * this happened" - unambiguous, because whether the loader ran at all is * answered by DLOGR_BOOT_DATA, which is declared on every path. * * Names, not a flag: the page has to say which constant and what it costs, and * it cannot work that out afterwards. Only this file knew both sides. */ if ( ! empty( $dlogr_boot_taken['conflict'] ) ) { define( 'DLOGR_BOOT_CONFLICT', implode( ',', $dlogr_boot_taken['conflict'] ) ); } if ( ! empty( $dlogr_boot_taken['latent'] ) ) { define( 'DLOGR_BOOT_LATENT_CONFLICT', implode( ',', $dlogr_boot_taken['latent'] ) ); } unset( $dlogr_boot_taken );