Skip to content

PageFinder HY093 error on consecutive find() calls as non-superuser when content templates have useRoles + childTemplates #2207

@adrianbj

Description

@adrianbj

Hi @ryancramerdesign - apologies for the Claude written issue below. I found this issue on an old site of mine that I upgraded to 3.0.257 and then 3.0.258.

Non-superusers couldn't load the admin without SQLSTATE[HY093]: Invalid parameter number: number of bound variables does not match number of tokens error and also couldn't load certain pages on the frontend.

I switched to PageFinder2.php just to see if that helped but it didn't. After implementing the proposed fix, everything is fine again. That said, I couldn't seem to replicate this on any other site on the same server and with a symlinked wire folder so I don't really know what is different with this site - perhaps it is template user access restrictions, but maybe not exactly as described below?

Anyway, it would be great if you could implement the fix - btw, I implemented it into PageFinder2.php even though Claude built it for the original PageFinder.php file.

PS - initially I thought the error seemed like it might be related to PDO::ATTR_EMULATE_PREPARES but this is enabled.

PW Version: 3.0.257+

PageFinder HY093 error on consecutive find() calls as non-superuser when content templates have useRoles + childTemplates

Consecutive Pages::find() calls with different parent_id values throw a PDO exception for non-superuser users when content templates have useRoles=1 combined with
childTemplates. The first call always succeeds; the second always fails. Superusers are unaffected as they bypass access control.

Setup to reproduce

  1. Create template "parent-tpl" with useRoles=1, assign view access to guest + a custom role, and set childTemplates to restrict children to a specific template
  2. Create template "child-tpl" (no useRoles)
  3. On "parent-tpl", set childTemplates=[child-tpl]
  4. Create two parent pages (/parent-a/, /parent-b/) using "parent-tpl"
  5. Create at least one child page under each parent using "child-tpl"
  6. Ensure pages_access is properly populated
  7. Create a non-superuser with the custom role

Test (run as non-superuser)

  // Same parent twice — works fine                                                                                                                                      
  $r1 = $pages->find("parent_id={parent-a-id}, limit=1"); // OK                                                                                                            
  $r2 = $pages->find("parent_id={parent-a-id}, limit=1"); // OK                                                                                                            
  // Different parents — second call fails                                                                                                                                 
  $r1 = $pages->find("parent_id={parent-a-id}, limit=1"); // OK                                                                                                            
  $r2 = $pages->find("parent_id={parent-b-id}, limit=1"); // FAILS                                                                                                         

Expected: Both calls return results.

Actual: Second call throws:
SQLSTATE[HY093]: Invalid parameter number: number of bound variables
does not match number of tokens
at wire/core/PageFinder.php:853

Root Cause

In PageFinder::getQueryAllowedTemplates() (line ~2278), static variables cache access control SQL across all PageFinder calls within a request:

  static $where = null;                                                                                                                                                    
  static $where2 = null;                                                                                                                                                   
  static $leftjoin = null;
  static $cacheUserID = null;                                                                                                                                              

On the cached path (lines 2300-2308), the static $where and $where2 variables are passed through the hookable getQueryAllowedTemplatesWhere() method and the return
values are written back to the static variables:

  if(!is_null($where)) {                                                                                                                                                 
      if($hasWhereHook) {
          $where = $this->getQueryAllowedTemplatesWhere($query, $where);   // mutates static
          $where2 = $this->getQueryAllowedTemplatesWhere($query, $where2); // mutates static                                                                               
      }
      $query->where($where);                                                                                                                                               
      $query->where($where2);                                                                                                                                              
      $query->leftjoin($leftjoin);
      return;                                                                                                                                                              
  }                                                                                                                                                                      

Each pass through the hook can add bind parameter placeholders to the $where strings, but the corresponding bind values only exist on the current query object. The
static strings accumulate placeholders from previous calls, causing a mismatch when the next query is executed.

The same parent works twice because PagesLoader (line ~409) has a selector string cache that returns results before reaching PageFinder on a cache hit, so the bug path
is never reached.

Proposed Fix

Use local copies instead of mutating the statics (lines 2300-2308):

  if(!is_null($where)) {                                                                                                                                                   
      $localWhere = $where;                                                                                                                                              
      $localWhere2 = $where2;
      if($hasWhereHook) {
          $localWhere = $this->getQueryAllowedTemplatesWhere($query, $localWhere);
          $localWhere2 = $this->getQueryAllowedTemplatesWhere($query, $localWhere2);                                                                                       
      }
      $query->where($localWhere);                                                                                                                                          
      if($localWhere2) $query->where($localWhere2);                                                                                                                        
      if($leftjoin) $query->leftjoin($leftjoin);
      return;                                                                                                                                                              
  }                                                                                                                                                                      

Notes

  • Works as superuser (access control bypassed entirely at line 2276)
  • Works when only system templates (admin, home, user) have useRoles
  • Works when check_access=0 is added to the selector
  • Not caused by third-party modules (tested with all disabled)
  • Same wire/ folder works on other sites that don't have useRoles + childTemplates on content templates

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions