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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions