Subversion Repositories web.active

Rev

Rev 22 | Blame | Compare with Previous | Last modification | View Log | Download

<?php namespace ProcessWire;

/**
 * ProcessWire Page Paths
 *
 * Keeps a cache of page paths to improve performance and 
 * make paths more queryable by selectors.
 *
 * ProcessWire 3.x, Copyright 2021 by Ryan Cramer
 * https://processwire.com
 * 
 * @property array $rootSegments
 *
 */

class PagePaths extends WireData implements Module, ConfigurableModule {

  public static function getModuleInfo() {
    return array(
      'title' => 'Page Paths', 
      'version' => 4, 
      'summary' => "Enables page paths/urls to be queryable by selectors. Also offers potential for improved load performance. Builds an index at install (may take time on a large site).",
      'singular' => true, 
      'autoload' => true, 
    );
  }

  /**
   * Table created by this module
   *
   */
  const dbTableName = 'pages_paths';

  /**
   * @var Languages|false
   *
   */
  protected $languages = null;

  /**
   * Construct
   * 
   */
  public function __construct() {
    $this->set('rootSegments', array());
    parent::__construct();
  }

  /**
   * Initialize the hooks
   *
   */
  public function init() {
    $pages = $this->wire()->pages;
    $pages->addHook('moved', $this, 'hookPageMoved'); 
    $pages->addHook('renamed', $this, 'hookPageMoved'); 
    $pages->addHook('added', $this, 'hookPageMoved'); 
    $pages->addHook('deleted', $this, 'hookPageDeleted');
  }

  /**
   * API ready
   * 
   */
  public function ready() {
    if($this->wire()->languages) {
      $this->addHookBefore('LanguageSupportFields::languageDeleted', $this, 'hookLanguageDeleted');
    }
  }
  
  /*** HOOKS ******************************************************************************************/

  /**
   * Hook called when a page is moved or renamed
   * 
   * @param HookEvent $event
   *
   */
  public function hookPageMoved(HookEvent $event) {
    $page = $event->arguments[0];
    // $this->updatePagePath($page->id, $page->path);
    $this->updatePagePaths($page); 
  }

  /**
   * Hook called when a page is deleted
   * 
   * @param HookEvent $event
   *
   */
  public function hookPageDeleted(HookEvent $event) {
    $table = self::dbTableName;
    $page = $event->arguments[0];
    $database = $this->wire()->database;
    $query = $database->prepare("DELETE FROM $table WHERE pages_id=:pages_id"); 
    $query->bindValue(":pages_id", $page->id, \PDO::PARAM_INT);
    $query->execute();
    $this->rebuildRootSegments();
  }

  /**
   * When a language is deleted
   * 
   * @param HookEvent $event
   * 
   */
  public function hookLanguageDeleted(HookEvent $event) {
    $languages = $this->getLanguages();
    if(!$languages) return;
    $language = $event->arguments[0]; /** @var Language $language */
    if(!$language->id || $language->isDefault()) return;
    $table = self::dbTableName;
    $database = $this->wire()->database;
    $sql = "DELETE FROM $table WHERE language_id=:language_id";
    $query = $database->prepare($sql);
    $query->bindValue(':language_id', $language->id, \PDO::PARAM_INT);
    $this->executeQuery($query);
  }
  
  /*** PUBLIC API *************************************************************************************/

  /**
   * Given a page ID, return the page path, NULL if not found, or boolean false if cannot be determined.
   *
   * @param int $pageId Page ID
   * @param int $languageId Optionally specify language ID for path or 0 for default language
   * @return string|null Returns path or null if not found
   *
   */
  public function getPath($pageId, $languageId = 0) {
    
    $table = self::dbTableName;
    $database = $this->wire()->database;
    $sanitizer = $this->wire()->sanitizer;
    
    $languageId = $this->languageId($languageId);
    
    $sql = "SELECT path FROM `$table` WHERE pages_id=:pages_id AND language_id=:language_id";
    $query = $database->prepare($sql); 
    $query->bindValue(":pages_id", $pageId, \PDO::PARAM_INT);
    $query->bindValue(":language_id", $languageId, \PDO::PARAM_INT);
    $path = null;
    
    if(!$this->executeQuery($query)) return null;
    
    if($query->rowCount()) {
      $path = $query->fetchColumn();
      $path = strlen($path) ? $sanitizer->pagePathName("/$path/", Sanitizer::toUTF8) : '/';
    }
    
    $query->closeCursor();
    
    return $path;
  }
  
  /**
   * Given a page ID, return all paths found for page 
   * 
   * Return value is indexed by language ID (and index 0 for default language)
   *
   * @param int $pageId Page ID
   * @return array
   *
   */
  public function getPaths($pageId) {

    $table = self::dbTableName;
    $database = $this->wire()->database;
    $sanitizer = $this->wire()->sanitizer;
    $paths = array();

    $sql = "SELECT path, language_id FROM `$table` WHERE pages_id=:pages_id ";

    $query = $database->prepare($sql);
    $query->bindValue(":pages_id", $pageId, \PDO::PARAM_INT);
    
    if(!$this->executeQuery($query)) return $paths;

    while($row = $query->fetch(\PDO::FETCH_NUM)) {
      $path = $row[0];
      $languageId = (int) $row[1];
      $path = strlen($path) ? $sanitizer->pagePathName("/$path/", Sanitizer::toUTF8) : '/';
      $paths[$languageId] = $path; 
    }

    $query->closeCursor();

    return $paths;
  }

  /**
   * Given a page path, return the page ID or NULL if not found.
   *
   * @param string $path
   * @return int|null
   *
   */
  public function getID($path) {
    $id = $this->getPageId($path);
    return $id ? $id : null;
  }

  /**
   * Given a page path, return the page ID or 0 if not found.
   *
   * @param string|array $path
   * @return int|null
   * @since 3.0.186
   *
   */
  public function getPageID($path) {
    $a = $this->getPageAndLanguageId($path);
    return $a[0];
  }

  /**
   * Given a page path return array of [ page_id, language_id ]
   * 
   * If not found, returned page_id and language_id will be 0. 
   * 
   * @param string|array $path
   * @return array
   * @since 3.0.186
   * 
   */
  public function getPageAndLanguageID($path) {
    
    $table = self::dbTableName;
    $database = $this->wire()->database;
    $paths = is_array($path) ? array_values($path) : array($path);
    $bindValues = array();
    $wheres = array();
    
    foreach($paths as $n => $path) {
      $path = $this->wire()->sanitizer->pagePathName($path, Sanitizer::toAscii);
      $path = trim($path, '/');
      $wheres[] = "path=:path$n";
      $bindValues["path$n"] = $path;
    }

    $where = implode(' OR ', $wheres);
    $sql = "SELECT pages_id, language_id FROM $table WHERE $where LIMIT 1";
    $query = $database->prepare($sql);
    $row = array(0, 0);
    
    foreach($bindValues as $bindKey => $bindValue) {
      $query->bindValue(":$bindKey", $bindValue);
    }
  
    if(!$this->executeQuery($query)) return $row;
    
    if($query->rowCount()) {
      $row = $query->fetch(\PDO::FETCH_NUM);
    }
    
    $query->closeCursor();
    
    return array((int) $row[0], (int) $row[1]); 
  }
  
  /**
   * Get page information about a given path
   * 
   * Returned array includes the following:
   * 
   *  - `id` (int): ID of page for given path
   *  - `language_id` (int): ID of language path was for, or 0 for default language
   *  - `templates_id` (int): ID of template used by page
   *  - `parent_id` (int): ID of parent page
   *  - `status` (int): Status value for page ($page->status)
   *  - `path` (string): Path that was found
   *
   * @param string $path
   * @return array|bool Returns info array on success, boolean false if not found
   * @since 3.0.186
   *
   */
  public function getPageInfo($path) {

    $sanitizer = $this->wire()->sanitizer;
    $database = $this->wire()->database;
    $languages = $this->wire()->languages;
    $config = $this->wire()->config;
    
    $table = self::dbTableName;
    $useUTF8 = $config->pageNameCharset === 'UTF8';
    
    if($languages && !$languages->hasPageNames()) $languages = null;
    
    if($useUTF8) {
      $path = $sanitizer->pagePathName($path, Sanitizer::toAscii);
    }
    
    $columns = array(
      'pages_paths.path AS path', 
      'pages_paths.pages_id AS id', 
      'pages_paths.language_id AS language_id',
      'pages.templates_id AS templates_id',
      'pages.parent_id AS parent_id', 
      'pages.status AS status'
    );
    
    if($languages) {
      foreach($languages as $language) {
        if($language->isDefault()) continue;
        $columns[] = "pages.status$language->id AS status$language->id";
      }
    }
    
    $cols = implode(', ', $columns);

    $sql =
      "SELECT $cols FROM $table " .
      "JOIN pages ON pages_paths.pages_id=pages.id " .
      "WHERE pages_paths.path=:path";

    $query = $database->prepare($sql);
    $query->bindValue(':path', trim($path, '/'));
    
    if(!$this->executeQuery($query)) return false;

    $row = $query->fetch(\PDO::FETCH_ASSOC);
    $query->closeCursor();
    
    if(!$row) return false;
  
    foreach($row as $key => $value) {
      if($key === 'id' || strpos($key, 'status') === 0 || strpos($key, '_id')) {
        $row[$key] = (int) $value;
      }
    }

    if($useUTF8 && $row) {
      $row['path'] = $sanitizer->pagePathName($row['path'], Sanitizer::toUTF8);
    }
        
    return $row;
  }
  /**
   * Rebuild all paths table starting with $page and descending to its children
   *
   * @param Page|null $page Page to start rebuild from or omit to rebuild all
   * @return int Number of paths added
   * @since 3.0.186
   *
   */
  public function rebuild(Page $page = null) {
    set_time_limit(3600);
    $table = self::dbTableName;
    if($page === null) {
      // rebuild all
      $this->wire()->database->exec("DELETE FROM $table");
      $page = $this->wire()->pages->get('/');
    }
    $result = $this->updatePagePaths($page, true);
    return $result;
  }

  /**
   * Perform a path match for use by PageFinder
   *
   * @param DatabaseQuerySelect $query
   * @param Selector $selector
   * @throws PageFinderSyntaxException
   *
   */
  public function getMatchQuery(DatabaseQuerySelect $query, Selector $selector) {

    static $n = 0;
    
    $sanitizer = $this->wire()->sanitizer;
    $database = $this->wire()->database;
    
    $n++;
    $table = self::dbTableName;
    $alias = "$table$n";
    $value = $selector->value;
    $operator = $selector->operator;
    // $joinType = $selector->not ? 'leftjoin' : 'join';

    $query->join("$table AS $alias ON pages.id=$alias.pages_id"); 

    if(in_array($operator, array('=', '!=', '<>', '>', '<', '>=', '<='))) {
      if(!is_array($value)) $value = array($value);
      $where = '';
      foreach($value as $path) {
        if($where) $where .= $selector->not ? " AND " : " OR ";
        $path = $sanitizer->pagePathName($path, Sanitizer::toAscii);
        $path = $database->escapeStr(trim($path, '/')); 
        $where .= ($selector->not ? "NOT " : "") . "$alias.path{$operator}'$path'";
      }
      $query->where("($where)");

    } else {
      if(is_array($value)) {
        $error = "Multi value using '|' is not supported with path/url and '$operator' operator";
        throw new PageFinderSyntaxException($error);
      }
      if($selector->not) {
        $error = "NOT mode isn't yet supported with path/url and '$operator' operator";
        throw new PageFinderSyntaxException($error);
      }
      /** @var DatabaseQuerySelectFulltext $ft */
      $ft = $this->wire(new DatabaseQuerySelectFulltext($query));
      $ft->match($alias, 'path', $operator, trim($value, '/'));
    }
  }
  
  /*** PROTECTED API **********************************************************************************/
  
  /**
   * Updates path for page and all children
   *
   * @param Page|int $page
   * @param bool|null $hasChildren Does this page have children? Specify false if known not to have children, true otherwise.
   * @param array $paths Paths indexed by language ID, use index 0 for default language. 
   * @return int Number of paths updated
   * @since 3.0.186
   *
   */
  protected function updatePagePaths($page, $hasChildren = null, array $paths = array()) {
    
    static $level = 0;

    $rootPageId = $this->wire()->config->rootPageID;
    $database = $this->wire()->database;
    $sanitizer = $this->wire()->sanitizer;
    $languages = $this->getLanguages();
    $table = self::dbTableName;
    $numUpdated = 1;
    $homeDefaultName = '';
    $rebuildRoot = false;
    $level++;
    
    if($hasChildren === null) {
      $hasChildren = $page instanceof Page ? $page->numChildren > 0 : true;
    }

    if(empty($paths)) {
      // determine the paths
      if(!is_object($page) || !$page instanceof Page) {
        throw new WireException('Page object required on first call to updatePagePaths');
      }
      $pageId = $page->id;
      if($page->parent_id === $rootPageId) $rebuildRoot = true;
      if($languages) {
        // multi-language
        foreach($languages as $language) {
          /** @var Language $language */
          $languageId = $language->isDefault() ? 0 : $language->id;
          $paths[$languageId] = $page->localPath($language);
          if($pageId === 1 && !$languageId) $homeDefaultName = $page->name;
        }
      } else {
        // single language
        $paths[0] = $page->path();
      }
    } else {
      // $paths already populated
      $pageId = (int) "$page";
    }
    
    if($pageId === $rootPageId) $rebuildRoot = true;

    // sanitize and prepare paths for DB storage
    foreach($paths as $languageId => $path) {
      $path = $sanitizer->pagePathName($path, Sanitizer::toAscii);
      $paths[$languageId] = trim($path, '/');
    }
    
    $sql =  
      "INSERT INTO $table (pages_id, language_id, path) " . 
      "VALUES(:pages_id, :language_id, :path) " .
      "ON DUPLICATE KEY UPDATE " . 
      "pages_id=VALUES(pages_id), language_id=VALUES(language_id), path=VALUES(path)";

    $query = $database->prepare($sql);
    $query->bindValue(":pages_id", $pageId, \PDO::PARAM_INT);
    
    foreach($paths as $languageId => $path) {
      $query->bindValue(":language_id", $languageId, \PDO::PARAM_INT);
      $query->bindValue(":path", $path);
      if($this->executeQuery($query)) $numUpdated += $query->rowCount();
    }

    if($hasChildren) {
      if($homeDefaultName && $homeDefaultName !== 'home' && empty($paths[0])) {
        // for when homepage has a name (lang segment) but it isn’t used on actual homepage
        // but is used on children
        $paths[0] = $homeDefaultName;
      }
      $numUpdated += $this->updatePagePathsChildren($pageId, $paths);
    }
    
    if($level === 1 && $numUpdated > 0) {
      $this->message(
        sprintf($this->_n('Updated %d path', 'Updated %d paths', $numUpdated), $numUpdated),
        Notice::admin
      );
    }
    
    $level--;
    
    if($rebuildRoot && !$level) $this->rebuildRootSegments();

    return $numUpdated;
  }

  /**
   * Companion to updatePagePaths method to handle children 
   * 
   * @param int $pageId
   * @param array $paths Paths indexed by language ID, index 0 for default language
   * @return int
   * @since 3.0.186
   *
   */
  protected function updatePagePathsChildren($pageId, array $paths) {
  
    $database = $this->wire()->database;
    $languages = $this->getLanguages();
    $nameColumns = array('pages.name AS name');
    $numUpdated = 0;
    
    if($languages) {
      foreach($languages as $language) {
        /** @var Language $language */
        if($language->isDefault()) continue;
        $nameColumns[] = "pages.name$language->id AS name$language->id";
      }
    }
    
    $sql =
      "SELECT pages.id AS id, " . implode(', ', $nameColumns) . ", " .
      "COUNT(children.id) AS kids " .
      "FROM pages " .
      "LEFT JOIN pages AS children ON children.id=pages.parent_id " .
      "WHERE pages.parent_id=:id " .
      "GROUP BY pages.id ";

    $query = $database->prepare($sql);
    $query->bindValue(":id", $pageId, \PDO::PARAM_INT);
    $rows = array();
    
    if(!$this->executeQuery($query)) return $numUpdated;
    
    while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
      $rows[] = $row;
    }
    
    $query->closeCursor();

    foreach($rows as $row) {
      $childPaths = array();
      foreach($paths as $languageId => $path) {
        $key = $languageId ? "name$languageId" : "name";
        $name = !empty($row[$key]) ? $row[$key] : $row["name"];
        $childPaths[$languageId] = "$path/$name";
      }
      $numUpdated += $this->updatePagePaths((int) $row['id'], $row['kids'] > 0, $childPaths);
    }
    
    return $numUpdated;
  }

  
  /*** ROOT SEGMENTS ******************************************************************************/

  /**
   * Is given segment/page name a root segment?
   *
   * A root segment is one that is owned by the homepage or a direct parent of the homepage, i.e.
   * /about/ might be a root page segment and /de/ might be a root language segment. If it is a
   * root page segment like /about/ then this will return the ID of that page. If it is a root
   * language segment like /de/ then it will return the homepage ID (1).
   *
   * @param string $segment Page name string or path containing it
   * @return int Returns page ID or 0 for no match.
   * @since 3.0.186
   *
   */
  public function isRootSegment($segment) {
    $segment = trim($segment, '/');
    if(strpos($segment, '/')) list($segment,) = explode('/', $segment, 2);
    $rootSegments = $this->getRootSegments();
    $key = array_search($segment, $rootSegments);
    if($key === false) return 0;
    $key = ltrim($key, '_');
    if(strpos($key, '.')) {
      list($pageId, /*$languageId*/) = explode('.', $key, 2);
    } else {
      $pageId = $key;
    }
    return (int) $pageId;
  }

  /**
   * Get root segments
   *
   * @param bool $rebuild
   * @return array
   * @since 3.0.186
   *
   */
  public function getRootSegments($rebuild = false) {
    if(empty($this->rootSegments) || $rebuild) $this->rebuildRootSegments();
    return $this->rootSegments;
  }

  /**
   * Rebuild root segments stored in module config
   * 
   * @since 3.0.186
   * 
   */
  protected function rebuildRootSegments() {
    
    $database = $this->wire()->database;
    $config = $this->wire()->config;
    $languages = $this->getLanguages();
    $cols = array('id', 'name');
    $segments = array();
    
    if($languages) {
      foreach($languages as $language) {
        if(!$language->isDefault()) $cols[] = "name$language->id";
      }
    }
    
    $sql = 'SELECT ' . implode(',', $cols) . ' FROM pages WHERE parent_id=:id ';
    if($languages) $sql .= 'OR id=:id';
    $query = $database->prepare($sql);
    $query->bindValue(':id', $config->rootPageID, \PDO::PARAM_INT);
    $query->execute();
  
    while($row = $query->fetch(\PDO::FETCH_ASSOC)){
      $id = (int) $row['id'];
      unset($row['id']);
      foreach($row as $col => $name) {
        if(!strlen("$name")) continue;
        if($id === 1 && $col === 'name' && $name === Pages::defaultRootName) continue; // skip "/home/"
        $col = str_replace('name', '', $col);
        if(strlen($col)) {
          $segments["_$id.$col"] = $name; // _pageID.languageID i.e. 123.456
        } else {
          $segments["_$id"] = $name; // _pageID i.e. 123
        }
      }
    }
    
    $query->closeCursor();
  
    $this->rootSegments = $segments;
    $this->wire()->modules->saveConfig($this, 'rootSegments', $segments);
    
    return $segments;
  }

  
  /*** LANGUAGES **********************************************************************************/
  
  /**
   * Returns Languages object or false if not available
   *
   * @return Languages|Language[]|false
   *
   */
  public function getLanguages() {
    if($this->languages !== null) return $this->languages;
    $languages = $this->wire()->languages;
    if(!$languages) {
      $this->languages = false;
    } else if($languages->hasPageNames()) {
      $this->languages = $languages;
    } else {
      $this->languages = false;
    }
    return $this->languages;
  }

  /**
   * @param Language|int|string $language
   * @return int Returns language ID or 0 for default language
   * @since 3.0.186
   * 
   */
  protected function languageId($language) {
    $language = $this->language($language); 
    if(!$language->id || $language->isDefault()) return 0;
    return $language->id;
  }

  /**
   * @param Language|int|string $language
   * @return Language|NullPage
   * @since 3.0.186
   * 
   */
  protected function language($language) {
    $languages = $this->getLanguages();
    if(!$languages) return new NullPage();
    if(is_object($language)) return ($language instanceof Language ? $language : new NullPage());
    return $languages->get($language);
  }
  
  /*** MODULE MAINT *******************************************************************************/

  /**
   * Execute a query/PDOStatement
   * 
   * @param \PDOStatement $query
   * @param bool $throw Allow exceptions to be thrown? (default=true)
   * @return bool
   * @throws \PDOException
   * 
   */
  protected function executeQuery($query, $throw = true) {
    try {
      $result = $query->execute();
    } catch(\Exception $e) {
      if(!$this->checkTableSchema()) {
        if($throw) throw $e;
        $this->error($e->getMessage(), Notice::superuser | Notice::log);
      }
      $result = false;
    }
    return $result;
  }

  /**
   * Check db schema
   * 
   * @return bool True if changes made, false if not
   * 
   */
  protected function checkTableSchema() {
    $table = self::dbTableName;
    $database = $this->wire()->database;
    if(!$database->columnExists($table, 'language_id')) {
      $sqls = array(
        "ALTER TABLE $table ADD language_id INT UNSIGNED NOT NULL DEFAULT 0 AFTER pages_id",
        "ALTER TABLE $table DROP PRIMARY KEY, ADD PRIMARY KEY(pages_id, language_id)",
        "ALTER TABLE $table ADD INDEX language_id (language_id)",
        "ALTER TABLE $table DROP INDEX path, ADD UNIQUE KEY path(path(500), language_id)",
      );
      foreach($sqls as $sql) {
        $database->exec($sql);
      }
      $this->message("Added language_id column to table $table", Notice::admin);
      return true;
    }
    return false;
  }

  /**
   * Upgrade module
   * 
   * @param $fromVersion
   * @param $toVersion
   * @since 3.0.186
   * 
   */
  public function ___upgrade($fromVersion, $toVersion) {
    if($fromVersion && $toVersion) {} // ignore
    $this->checkTableSchema();
    $this->rebuildRootSegments();
  }

  /**
   * Install the module
   *
   */
  public function ___install() {
    
    $table = self::dbTableName;
    $database = $this->wire()->database;
    $engine = $this->wire()->config->dbEngine;
    $charset = $this->wire()->config->dbCharset;

    $database->query("DROP TABLE IF EXISTS $table"); 

    $sql =  
      "CREATE TABLE $table (" .
        "pages_id int(10) unsigned NOT NULL, " .
        "language_id int unsigned NOT NULL DEFAULT 0, " . 
        "path text CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL, " .
        "PRIMARY KEY (pages_id, language_id), " .
        "UNIQUE KEY path (path(500), language_id), " .
        "INDEX language_id (language_id), " . 
        "FULLTEXT KEY path_fulltext (path)" .
      ") ENGINE=$engine DEFAULT CHARSET=$charset";

    $database->query($sql); 
  }

  /**
   * Uninstall the module
   *
   */
  public function ___uninstall() {
    $this->wire()->database->query("DROP TABLE " . self::dbTableName); 
  }

  /**
   * Module config
   * 
   * @param InputfieldWrapper $inputfields
   * 
   */
  public function getModuleConfigInputfields(InputfieldWrapper $inputfields) {
  
    $session = $this->wire()->session;
    $input = $this->wire()->input;
    $numPages = 0;
    $numRows = -1;
    
    if($input->requestMethod('POST')) {
      if($input->post('_rebuild')) $session->setFor($this, 'rebuild', true);
    } else {
      $numPages = $this->wire()->pages->count("id>0, include=all");
      if($session->getFor($this, 'rebuild')) {
        $session->removeFor($this, 'rebuild');
        $timer = Debug::timer();
        $this->rebuild();
        $elapsed = Debug::timer($timer);
        $this->message(sprintf($this->_('Completed rebuild in %d seconds'), $elapsed), Notice::noGroup);
      } else {
        $table = self::dbTableName;
        $query = $this->wire()->database->prepare("SELECT COUNT(*) FROM $table");
        if($this->executeQuery($query, false)) {
          $numRows = (int) $query->fetchColumn();
          $query->closeCursor();
        }
      }
    }
    
    $f = $inputfields->InputfieldCheckbox;
    $f->attr('name', '_rebuild');
    $f->label = sprintf($this->_('Rebuild page paths index for %d pages'), $numPages); 
    $f->label2 = $this->_('Rebuild now');
    if($numPages) $f->description =
      $this->_('Estimated rebuild time is up to 5 seconds per 1000 pages.') . ' ' . 
      sprintf($this->_('There are %d pages to process.'), $numPages); 
    if($numRows > 0) {
      $f->notes = sprintf($this->_('There are currently %d rows stored by this module (path paths and versions of path paths).'), $numRows);
    } else if($numRows === 0 && $input->requestMethod('GET')) {
      $this->warning($this->_('Please choose the “rebuild now” option to create your page paths index.'));
    }
      
    $inputfields->add($f);
  }

}