@prefix sioc: <http://rdfs.org/sioc/ns#> .
@prefix dc: <http://purl.org/dc/elements/1.1/> .
@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix content: <http://purl.org/rss/1.0/modules/content/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

<https://lhero.org/?post_type=lh-portfolio-doc&#038;p=150415>
  a sioc:Post ;
  dc:title "Notes" ;
  dcterms:identifier 150415 ;
  dc:modified "2026-10-22T11:41:20Z"^^xsd:dateTime ;
  dc:created "2026-10-15T12:19:34Z"^^xsd:dateTime ;
  sioc:link <https://lhero.org/portfolio/lh-site-privacy/notes/> ;
  sioc:has_creator <https://lhero.org/author/1/#account> ;
  sioc:has_container <https://lhero.org/#posts> ;
  content:encoded """<ul class="lh_portfolio-meta"><li><strong>Part of:</strong> <a href="https://lhero.org/portfolio/lh-site-privacy/">LH Site Privacy</a></li></ul>
<h2 class="wp-block-heading">Where the level is stored</h2>



<p class="wp-block-paragraph">Levels live in core&#8217;s <code>blog_public</code> option: 1 and 0 are core&#8217;s own values, and the private levels are -1 (logged-in users), -2 (site users), -3 (administrators), -4 (password) and -5 (members). On multisite, core mirrors the option into the <code>public</code> column of the sites table, so a private site drops out of <code>get_sites( array( 'public' =&gt; 1 ) )</code> listings. The values match those used by the old More Privacy Options plugin and LH Multisite Privacy Options, so sites already set by either keep their level and are enforced as soon as this plugin is active. An unrecognised negative value is treated as administrators only.</p>



<p class="wp-block-paragraph">Core treats only 0 as noindex and only 0 in robots.txt, and leaves sitemaps on for any non-zero value, so the plugin adds noindex, a disallow-all robots.txt and turns sitemaps off for every private level.</p>



<h2 class="wp-block-heading">Password level</h2>



<ul class="wp-block-list">
<li>The password is stored with <code>wp_hash_password()</code>. The Settings &gt; Reading field is a registered setting whose value is handed to the password class and never stored as an option itself.</li>



<li>A correct password sets a host-only, HTTP-only cookie (secure on HTTPS) holding an expiry and an HMAC over the site ID, the expiry and a version string. Setting a new password rotates the version, which invalidates every existing cookie. Nothing goes in the URL, so links cannot leak access.</li>



<li>Failed attempts are counted per site and IP address in a transient, five per 15 minutes by default.</li>



<li>The submitted password is unslashed but deliberately not sanitised, since any sanitiser would change a valid password.</li>
</ul>



<h2 class="wp-block-heading">Enforcement points</h2>



<ul class="wp-block-list">
<li><strong>Front end:</strong> <code>template_redirect</code> at priority 0, ahead of other plugins that serve responses there (lh-ai-legibility&#8217;s Markdown and llms.txt run at priority 1, and lh-oauth-server&#8217;s <code>/authorize</code> at 10). robots.txt and the favicon are left alone.</li>



<li><strong>REST API:</strong> <code>rest_authentication_errors</code> at priority 101, after core&#8217;s cookie and nonce check at 100, so the user is final. An earlier error (such as lh-oauth-server&#8217;s 401 challenge for a missing bearer token) is passed through untouched. In-process <code>rest_do_request()</code> calls do not pass through this filter.</li>



<li><strong>admin-ajax and admin-post:</strong> <code>admin_init</code> at priority 0. Ordinary wp-admin screens are already restricted by WordPress.</li>



<li><strong>Caches:</strong> every request to a private site sends no-cache headers, <code>X-Robots-Tag: noindex, nofollow</code> and LiteSpeed&#8217;s no-cache control. A level change purges LiteSpeed, so pages cached while a site was public are not served once it is private.</li>



<li><strong>Not covered:</strong> uploaded files (served by the web server without loading WordPress) and XML-RPC.</li>
</ul>



<h2 class="wp-block-heading">Mastodon apps</h2>



<p class="wp-block-paragraph">While Enable Mastodon Apps is active, <code>LH_Site_Privacy_Compat</code> lets the requests a Mastodon client makes before it has a token through the gate: the v1 and v2 instance routes, app registration, nodeinfo, the plugin&#8217;s own authorization-header diagnostic and <code>/oauth/revoke</code>. They match the routes LH Disable REST API for anonymous users leaves open on public sites, and none return posts or members. <code>/oauth/token</code> needs nothing here, because Enable Mastodon Apps reports the site as public to privacy plugins for that one request. <code>/oauth/authorize</code> is deliberately not exempt: the gate sends visitors to log in first, so only someone with access to the site can grant an app access. Once the app has a token, Enable Mastodon Apps sets the current user from it and the normal access check applies.</p>



<p class="wp-block-paragraph">The instance responses include the site&#8217;s name, tagline and icon, so a logged-out visitor can see those on a private site that runs Enable Mastodon Apps. The admin email and user and post counts Enable Mastodon Apps would also include are blanked network-wide on LocalHero by LH Sites Specific.</p>



<h2 class="wp-block-heading">Every level on every site</h2>



<p class="wp-block-paragraph">Every site can use every level; there is deliberately no network setting to narrow the choice. The only exception is the members level, which is offered only where a membership plugin answers who is a member, since without one nobody could qualify. Saving a level that cannot be used, from the Reading screen or an ability alike, is refused and the stored value kept.</p>



<p class="wp-block-paragraph">The one network setting is the level for new sites (<code>lh_site_privacy_default_level</code>, Network Admin &gt; Settings). It is applied on <code>wp_initialize_site</code> to new sites created as public; a site cloned from a template may later overwrite it with the template&#8217;s own value.</p>



<h2 class="wp-block-heading">Replaces</h2>



<p class="wp-block-paragraph">LH Multisite Privacy Options, LH Private Site and LH Extra Multisite Privacy. Their behaviour of bouncing a logged-in user with no role on a site to their primary blog was deliberately dropped in favour of the no-access page.</p>
"""^^rdf:XMLLiteral ;
  sioc:content """Part of: LH Site Privacy
Where the level is stored



Levels live in core&#8217;s blog_public option: 1 and 0 are core&#8217;s own values, and the private levels are -1 (logged-in users), -2 (site users), -3 (administrators), -4 (password) and -5 (members). On multisite, core mirrors the option into the public column of the sites table, so a private site drops out of get_sites( array( 'public' =&gt; 1 ) ) listings. The values match those used by the old More Privacy Options plugin and LH Multisite Privacy Options, so sites already set by either keep their level and are enforced as soon as this plugin is active. An unrecognised negative value is treated as administrators only.



Core treats only 0 as noindex and only 0 in robots.txt, and leaves sitemaps on for any non-zero value, so the plugin adds noindex, a disallow-all robots.txt and turns sitemaps off for every private level.



Password level




The password is stored with wp_hash_password(). The Settings &gt; Reading field is a registered setting whose value is handed to the password class and never stored as an option itself.



A correct password sets a host-only, HTTP-only cookie (secure on HTTPS) holding an expiry and an HMAC over the site ID, the expiry and a version string. Setting a new password rotates the version, which invalidates every existing cookie. Nothing goes in the URL, so links cannot leak access.



Failed attempts are counted per site and IP address in a transient, five per 15 minutes by default.



The submitted password is unslashed but deliberately not sanitised, since any sanitiser would change a valid password.




Enforcement points




Front end: template_redirect at priority 0, ahead of other plugins that serve responses there (lh-ai-legibility&#8217;s Markdown and llms.txt run at priority 1, and lh-oauth-server&#8217;s /authorize at 10). robots.txt and the favicon are left alone.



REST API: rest_authentication_errors at priority 101, after core&#8217;s cookie and nonce check at 100, so the user is final. An earlier error (such as lh-oauth-server&#8217;s 401 challenge for a missing bearer token) is passed through untouched. In-process rest_do_request() calls do not pass through this filter.



admin-ajax and admin-post: admin_init at priority 0. Ordinary wp-admin screens are already restricted by WordPress.



Caches: every request to a private site sends no-cache headers, X-Robots-Tag: noindex, nofollow and LiteSpeed&#8217;s no-cache control. A level change purges LiteSpeed, so pages cached while a site was public are not served once it is private.



Not covered: uploaded files (served by the web server without loading WordPress) and XML-RPC.




Mastodon apps



While Enable Mastodon Apps is active, LH_Site_Privacy_Compat lets the requests a Mastodon client makes before it has a token through the gate: the v1 and v2 instance routes, app registration, nodeinfo, the plugin&#8217;s own authorization-header diagnostic and /oauth/revoke. They match the routes LH Disable REST API for anonymous users leaves open on public sites, and none return posts or members. /oauth/token needs nothing here, because Enable Mastodon Apps reports the site as public to privacy plugins for that one request. /oauth/authorize is deliberately not exempt: the gate sends visitors to log in first, so only someone with access to the site can grant an app access. Once the app has a token, Enable Mastodon Apps sets the current user from it and the normal access check applies.



The instance responses include the site&#8217;s name, tagline and icon, so a logged-out visitor can see those on a private site that runs Enable Mastodon Apps. The admin email and user and post counts Enable Mastodon Apps would also include are blanked network-wide on LocalHero by LH Sites Specific.



Every level on every site



Every site can use every level; there is deliberately no network setting to narrow the choice. The only exception is the members level, which is offered only where a membership plugin answers who is a member, since without one nobody could qualify. Saving a level that cannot be used, from the Reading screen or an ability alike, is refused and the stored value kept.



The one network setting is the level for new sites (lh_site_privacy_default_level, Network Admin &gt; Settings). It is applied on wp_initialize_site to new sites created as public; a site cloned from a template may later overwrite it with the template&#8217;s own value.



Replaces



LH Multisite Privacy Options, LH Private Site and LH Extra Multisite Privacy. Their behaviour of bouncing a logged-in user with no role on a site to their primary blog was deliberately dropped in favour of the no-access page.
""" ;
  sioc:topic <https://lhero.org/?taxonomy=author&term=cap-1> .

<https://lhero.org/author/1/#account> rdfs:seeAlso <https://lhero.org/author/1/?feed=lhrdf&format=turtle> .
<https://lhero.org/?taxonomy=author&term=cap-1> rdfs:seeAlso <https://lhero.org/?taxonomy=author&term=cap-1&feed=lhrdf&format=turtle> .
