CRM-15297 add Drupal8 functions from GSOC to Drupal8 class
authorEileen McNaughton <eileen@fuzion.co.nz>
Tue, 30 Sep 2014 22:40:23 +0000 (11:40 +1300)
committerEileen McNaughton <eileen@fuzion.co.nz>
Tue, 30 Sep 2014 22:40:23 +0000 (11:40 +1300)
where the d8 function is unchanged from d7 put in the DrupalBase class instead

CRM/Utils/System/Drupal8.php
CRM/Utils/System/DrupalBase.php

index 461a6946e6dd029b0b077b8e139fffda00487acd..57d3f919f24dbdbffb175a04edd3b3add9bff692 100644 (file)
  */
 class CRM_Utils_System_Drupal8 extends CRM_Utils_System_DrupalBase {
 
+  /**
+   * Function to create a user in Drupal.
+   *
+   * @param array  $params associated array
+   * @param string $mail email id for cms user
+   *
+   * @return uid if user exists, false otherwise
+   *
+   * @access public
+   *
+   */
+  function createUser(&$params, $mail) {
+    $user = \Drupal::currentUser();
+    $user_register_conf = \Drupal::config('user.settings')->get('register');
+    $verify_mail_conf = \Drupal::config('user.settings')->get('verify_mail');
+
+    // Don't create user if we don't have permission to.
+    if (!$user->hasPermission('administer users') && $user_register_conf == 'admin_only') {
+      return FALSE;
+    }
+
+    $account = entity_create('user');
+    $account->setUsername($params['cms_name'])->setEmail($params[$mail]);
+
+    // Allow user to set password only if they are an admin or if
+    // the site settings don't require email verification.
+    if (!$verify_mail_conf || $user->hasPermission('administer users')) {
+      // @Todo: do we need to check that passwords match or assume this has already been done for us?
+      $account->setPassword($params['cms_pass']);
+    }
+
+    // Only activate account if we're admin or if anonymous users don't require
+    // approval to create accounts.
+    if ($user_register_conf != 'visitors' && !$user->hasPermission('administer users')) {
+      $account->block();
+    }
+
+    // Validate the user object
+    $violations = $account->validate();
+    if (count($violations)) {
+      return FALSE;
+    }
+
+    try {
+      $account->save();
+    }
+    catch (\Drupal\Core\Entity\EntityStorageException $e) {
+      return FALSE;
+    }
+
+    // Send off any emails as required.
+    // Possible values for $op:
+    //    - 'register_admin_created': Welcome message for user created by the admin.
+    //    - 'register_no_approval_required': Welcome message when user
+    //      self-registers.
+    //    - 'register_pending_approval': Welcome message, user pending admin
+    //      approval.
+    // @Todo: Should we only send off emails if $params['notify'] is set?
+    switch (TRUE) {
+      case $user_register_conf == 'admin_only' || $user->isAuthenticated():
+        _user_mail_notify('register_admin_created', $account);
+        break;
+      case $user_register_conf == 'visitors':
+        _user_mail_notify('register_no_approval_required', $account);
+        break;
+      case 'visitors_admin_approval':
+        _user_mail_notify('register_pending_approval', $account);
+        break;
+    }
+
+    return $account->id();
+  }
+
+  /**
+   * Update the Drupal user's email address.
+   *
+   * @param integer $ufID  User ID in CMS
+   * @param string $email Primary contact email address
+   */
+  function updateCMSName($ufID, $email) {
+    $user = user_load($ufID);
+    if ($user && $user->getEmail() != $email) {
+      $user->setEmail($email);
+
+      if (!count($user->validate())) {
+        $user->save();
+      }
+    }
+  }
+
+  /**
+   * Check if username and email exists in the drupal db
+   *
+   * @param array $params       array of name and mail values
+   * @param array $errors  errors
+   * @param string $emailName   field label for the 'email'
+   *
+   *
+   * @return void
+   */
+  static function checkUserNameEmailExists(&$params, &$errors, $emailName = 'email') {
+    // If we are given a name, let's check to see if it already exists.
+    if (!empty($params['name'])) {
+      $name = $params['name'];
+
+      $user = entity_create('user');
+      $user->setUsername($name);
+
+      // This checks for both username uniqueness and validity.
+      $violations = iterator_to_array($user->validate());
+      // We only care about violations on the username field; discard the rest.
+      $violations = array_filter($violations, function ($v) { return $v->getPropertyPath() == 'name.0.value'; });
+      if (count($violations) > 0) {
+        $errors['cms_name'] = $violations[0]->getMessage();
+      }
+    }
+
+    // And if we are given an email address, let's check to see if it already exists.
+    if (!empty($params[$emailName])) {
+      $mail = $params[$emailName];
+
+      $user = entity_create('user');
+      $user->setEmail($mail);
+
+      // This checks for both email uniqueness.
+      $violations = iterator_to_array($user->validate());
+      // We only care about violations on the email field; discard the rest.
+      $violations = array_filter($violations, function ($v) { return $v->getPropertyPath() == 'mail.0.value'; });
+      if (count($violations) > 0) {
+        $errors[$emailName] = $violations[0]->getMessage();
+      }
+    }
+  }
+
+  /**
+   * Function to get the drupal destination string. When this is passed in the
+   * URL the user will be directed to it after filling in the drupal form
+   *
+   * @param object $form Form object representing the 'current' form - to which the user will be returned
+   * @return string $destination destination value for URL
+   *
+   */
+  function getLoginDestination(&$form) {
+    $args = NULL;
+
+    $id = $form->get('id');
+    if ($id) {
+      $args .= "&id=$id";
+    }
+    else {
+      $gid = $form->get('gid');
+      if ($gid) {
+        $args .= "&gid=$gid";
+      }
+      else {
+        // Setup Personal Campaign Page link uses pageId
+        $pageId = $form->get('pageId');
+        if ($pageId) {
+          $component = $form->get('component');
+          $args .= "&pageId=$pageId&component=$component&action=add";
+        }
+      }
+    }
+
+    $destination = NULL;
+    if ($args) {
+      // append destination so user is returned to form they came from after login
+      $destination = CRM_Utils_System::currentPath() . '?reset=1' . $args;
+    }
+    return $destination;
+  }
+
   /**
    * Get user login URL for hosting CMS (method declared in each CMS system class)
    *
@@ -47,14 +219,413 @@ class CRM_Utils_System_Drupal8 extends CRM_Utils_System_DrupalBase {
    * @static
    */
   public function getLoginURL($destination = '') {
-    $loginURL = CRM_Core_Config::singleton()->userFrameworkBaseURL;
-    //@todo this is not the right way to get the url in d8
-    $loginURL .= 'user';
-    if (!empty($destination)) {
-      // append destination so user is returned to form they came from after login
-      $loginURL .= '?destination=' . urlencode($destination);
+    $query = $destination ? array('destination' => $destination) : array();
+    return \Drupal::url('user.page', array(), array('query' => $query));
+  }
+
+
+  /**
+   * sets the title of the page
+   *
+   * @param string $title
+   * @param string $pageTitle
+   *
+   * @return void
+   * @access public
+   */
+  function setTitle($title, $pageTitle = NULL) {
+    if (!$pageTitle) {
+      $pageTitle = $title;
+    }
+
+    \Drupal::service('civicrm.page_state')->setTitle($pageTitle);
+  }
+
+  /**
+   * Append an additional breadcrumb tag to the existing breadcrumb
+   *
+   * @param $breadcrumbs
+   *
+   * @internal param string $title
+   * @internal param string $url
+   *
+   * @return void
+   * @access public
+   */
+  function appendBreadCrumb($breadcrumbs) {
+    $civicrmPageState = \Drupal::service('civicrm.page_state');
+    foreach ($breadcrumbs as $breadcrumb) {
+      $civicrmPageState->addBreadcrumb($breadcrumb['title'], $breadcrumb['url']);
+    }
+  }
+
+  /**
+   * Reset an additional breadcrumb tag to the existing breadcrumb
+   *
+   * @return void
+   * @access public
+   */
+  function resetBreadCrumb() {
+    \Drupal::service('civicrm.page_state')->resetBreadcrumbs();
+  }
+
+  /**
+   * Append a string to the head of the html file
+   *
+   * @param string $header the new string to be appended
+   *
+   * @return void
+   * @access public
+   */
+  function addHTMLHead($header) {
+    \Drupal::service('civicrm.page_state')->addHtmlHeader($header);
+  }
+
+  /**
+   * Add a script file
+   *
+   * @param $url: string, absolute path to file
+   * @param $region string, location within the document: 'html-header', 'page-header', 'page-footer'
+   *
+   * Note: This function is not to be called directly
+   * @see CRM_Core_Region::render()
+   *
+   * @return bool TRUE if we support this operation in this CMS, FALSE otherwise
+   * @access public
+   */
+  public function addScriptUrl($url, $region) {
+    $options = array('group' => JS_LIBRARY, 'weight' => 10);
+    switch ($region) {
+      case 'html-header':
+      case 'page-footer':
+        $options['scope'] = substr($region, 5);
+        break;
+      default:
+        return FALSE;
+    }
+    // If the path is within the drupal directory we can use the more efficient 'file' setting
+    $options['type'] = $this->formatResourceUrl($url) ? 'file' : 'external';
+    \Drupal::service('civicrm.page_state')->addJS($url, $options);
+    return TRUE;
+  }
+
+  /**
+   * Add an inline script
+   *
+   * @param $code: string, javascript code
+   * @param $region string, location within the document: 'html-header', 'page-header', 'page-footer'
+   *
+   * Note: This function is not to be called directly
+   * @see CRM_Core_Region::render()
+   *
+   * @return bool TRUE if we support this operation in this CMS, FALSE otherwise
+   * @access public
+   */
+  public function addScript($code, $region) {
+    $options = array('type' => 'inline', 'group' => JS_LIBRARY, 'weight' => 10);
+    switch ($region) {
+      case 'html-header':
+      case 'page-footer':
+        $options['scope'] = substr($region, 5);
+        break;
+      default:
+        return FALSE;
+    }
+    \Drupal::service('civicrm.page_state')->addJS($code, $options);
+    return TRUE;
+  }
+
+  /**
+   * Add a css file
+   *
+   * @param $url: string, absolute path to file
+   * @param $region string, location within the document: 'html-header', 'page-header', 'page-footer'
+   *
+   * Note: This function is not to be called directly
+   * @see CRM_Core_Region::render()
+   *
+   * @return bool TRUE if we support this operation in this CMS, FALSE otherwise
+   * @access public
+   */
+  public function addStyleUrl($url, $region) {
+    if ($region != 'html-header') {
+      return FALSE;
     }
-    return $loginURL;
+    $options = array();
+    // If the path is within the drupal directory we can use the more efficient 'file' setting
+    $options['type'] = $this->formatResourceUrl($url) ? 'file' : 'external';
+    \Drupal::service('civicrm.page_state')->addCSS($url, $options);
+    return TRUE;
   }
 
-}
\ No newline at end of file
+  /**
+   * Add an inline style
+   *
+   * @param $code: string, css code
+   * @param $region string, location within the document: 'html-header', 'page-header', 'page-footer'
+   *
+   * Note: This function is not to be called directly
+   * @see CRM_Core_Region::render()
+   *
+   * @return bool TRUE if we support this operation in this CMS, FALSE otherwise
+   * @access public
+   */
+  public function addStyle($code, $region) {
+    if ($region != 'html-header') {
+      return FALSE;
+    }
+    $options = array('type' => 'inline');
+    \Drupal::service('civicrm.page_state')->addCSS($code, $options);
+    return TRUE;
+  }
+
+  /**
+   * Check if a resource url is within the drupal directory and format appropriately
+   *
+   * This seems to be a legacy function. We assume all resources are within the drupal
+   * directory and always return TRUE. As well, we clean up the $url.
+   *
+   * @param $url
+   *
+   * @return bool
+   */
+  function formatResourceUrl(&$url) {
+    // Remove leading slash if present.
+    $url = ltrim($url, '/');
+
+    // Remove query string — presumably added to stop intermediary caching.
+    if (($pos = strpos($url, '?')) !== FALSE) {
+      $url = substr($url, 0, $pos);
+    }
+
+    return TRUE;
+  }
+
+  /**
+   * Rewrite various system urls to https
+   *
+   * This function does nothing in Drupal 8. Changes to the base_url should be made
+   * in settings.php directly.
+   *
+   * @param null
+   *
+   * @return void
+   * @access public
+   */
+  function mapConfigToSSL() {
+  }
+
+  /**
+   * @param string $path         The base path (eg. civicrm/search/contact)
+   * @param string $query      The query string (eg. reset=1&cid=66) but html encoded(?) (optional)
+   * @param bool $absolute     Produce an absolute including domain and protocol (optional)
+   * @param string $fragment   A named anchor (optional)
+   * @param bool $htmlize      Produce a html encoded url (optional)
+   * @param bool $frontend     A joomla hack (unused)
+   * @param bool $forceBackend A joomla jack (unused)
+   * @return string
+   */
+  function url($path = '', $query = '', $absolute = FALSE, $fragment = '', $htmlize = FALSE, $frontend = FALSE, $forceBackend = FALSE) {
+    $query = html_entity_decode($query);
+    $url = \Drupal\civicrm\CivicrmHelper::parseURL("{$path}?{$query}");
+
+    try {
+      $url = \Drupal::url($url['route_name'], array(), array(
+        'query' => $url['query'],
+        'absolute' => $absolute,
+        'fragment' => $fragment,
+      ));
+    }
+    catch (Exception $e) {
+      $url = '';
+    }
+
+    if ($htmlize) {
+      $url = htmlentities($url);
+    }
+    return $url;
+  }
+
+
+  /**
+   * Authenticate the user against the drupal db
+   *
+   * @param string $name     the user name
+   * @param string $password the password for the above user name
+   * @param boolean $loadCMSBootstrap load cms bootstrap?
+   * @param NULL|string $realPath filename of script
+   *
+   * @return mixed false if no auth
+   *               array(
+   *  contactID, ufID, unique string ) if success
+   * @access public
+   *
+   * This always bootstraps Drupal
+   */
+  function authenticate($name, $password, $loadCMSBootstrap = FALSE, $realPath = NULL) {
+    (new CRM_Utils_System_Drupal8())->loadBootStrap(array(), FALSE);
+
+    $uid = \Drupal::service('user.auth')->authenticate($name, $password);
+    $contact_id = CRM_Core_BAO_UFMatch::getContactId($uid);
+
+    return array($contact_id, $uid, mt_rand());
+  }
+
+  /**
+   * Load user into session
+   */
+  function loadUser($username) {
+    $user = user_load_by_name($username);
+    if (!$user) {
+      return FALSE;
+    }
+
+    // Set Drupal's current user to the loaded user.
+    \Drupal::currentUser()->setAccount($user);
+
+    $uid = $user->id();
+    $contact_id = CRM_Core_BAO_UFMatch::getContactId($uid);
+
+    // Store the contact id and user id in the session
+    $session = CRM_Core_Session::singleton();
+    $session->set('ufID', $uid);
+    $session->set('userID', $contact_id);
+    return TRUE;
+  }
+
+  /**
+   * Determine the native ID of the CMS user
+   *
+   * @param $username
+   * @return int|NULL
+   */
+  function getUfId($username) {
+    if ($id = user_load_by_name($username)->id()) {
+      return $id;
+    }
+  }
+
+  /**
+   * Set a message in the UF to display to a user
+   *
+   * @param string $message the message to set
+   *
+   * @access public
+   */
+  function setMessage($message) {
+    drupal_set_message($message);
+  }
+
+  function permissionDenied() {
+    \Drupal::service('civicrm.page_state')->setAccessDenied();
+  }
+
+  /**
+   * In previous versions, this function was the controller for logging out. In Drupal 8, we rewrite the route
+   * to hand off logout to the standard Drupal logout controller. This function should therefore never be called.
+   */
+  function logout() {
+    // Pass
+  }
+
+  /**
+   * load drupal bootstrap
+   *
+   * @param array $params Either uid, or name & pass.
+   * @param boolean $loadUser boolean Require CMS user load.
+   * @param boolean $throwError If true, print error on failure and exit.
+   * @param boolean|string $realPath path to script
+   *
+   * @return bool
+   * @Todo Handle setting cleanurls configuration for CiviCRM?
+   */
+  function loadBootStrap($params = array(), $loadUser = TRUE, $throwError = TRUE, $realPath = NULL) {
+    static $run_once = FALSE;
+    if ($run_once) return TRUE; else $run_once = TRUE;
+
+    if (!($root = $this->cmsRootPath())) {
+      return FALSE;
+    }
+    chdir($root);
+
+    // Create a mock $request object
+    $autoloader = require_once $root . '/core/vendor/autoload.php';
+    // @Todo: do we need to handle case where $_SERVER has no HTTP_HOST key, ie. when run via cli?
+    $request = new \Symfony\Component\HttpFoundation\Request(array(), array(), array(), array(), array(), $_SERVER);
+
+    // Create a kernel and boot it.
+    \Drupal\Core\DrupalKernel::createFromRequest($request, $autoloader, 'prod')->prepareLegacyRequest($request);
+
+    // Initialize Civicrm
+    \Drupal::service('civicrm');
+
+    // We need to call the config hook again, since we now know
+    // all the modules that are listening on it (CRM-8655).
+    CRM_Utils_Hook::config($config);
+
+    if ($loadUser) {
+      if (!empty($params['uid']) && $username = \Drupal\user\Entity\User::load($uid)->getUsername()) {
+        $this->loadUser($username);
+      }
+      elseif (!empty($params['name']) && !empty($params['pass']) && $this->authenticate($params['name'], $params['pass'])) {
+        $this->loadUser($params['name']);
+      }
+    }
+    return TRUE;
+  }
+
+  /**
+   * Determine the location of the CMS root.
+   * @param null $path
+   *
+   * @return NULL|string
+   */
+  function cmsRootPath($path = NULL) {
+    if (defined('DRUPAL_ROOT')) {
+      return DRUPAL_ROOT;
+    }
+
+    // It looks like Drupal hasn't been bootstrapped.
+    // We're going to attempt to discover the root Drupal path
+    // by climbing out of the folder hierarchy and looking around to see
+    // if we've found the Drupal root directory.
+    if (!$path) {
+      $path = $_SERVER['SCRIPT_FILENAME'];
+    }
+
+    // Normalize and explode path into its component paths.
+    $paths = explode(DIRECTORY_SEPARATOR, realpath($path));
+
+    // Remove script filename from array of directories.
+    array_pop($paths);
+
+    while (count($paths)) {
+      $candidate = implode('/', $paths);
+      if (file_exists($candidate . "/core/includes/bootstrap.inc")) {
+        return $candidate;
+      }
+
+      array_pop($paths);
+    }
+  }
+
+  /**
+   * Check if user is logged in.
+   *
+   * @return bool
+   */
+  public function isUserLoggedIn() {
+    return \Drupal::currentUser()->isAuthenticated();
+  }
+
+  /**
+   * Get currently logged in user uf id.
+   *
+   * @return int $userID logged in user uf id.
+   */
+  public function getLoggedInUfID() {
+    if ($id = \Drupal::currentUser()->id()) {
+      return $id;
+    }
+  }
+}
index 0e63845ee1ebf74756c6d9b4ec968cc15810cb14..71f154d015c3ce12789f7b6c778963076c8e6a06 100644 (file)
@@ -297,4 +297,194 @@ abstract class CRM_Utils_System_DrupalBase extends CRM_Utils_System_Base {
   function appendCoreResources(&$list) {
     $list[] = 'js/crm.drupal.js';
   }
+
+  /**
+   * Reset any system caches that may be required for proper CiviCRM
+   * integration.
+   */
+  function flush() {
+    drupal_flush_all_caches();
+  }
+
+  /**
+   * Get a list of all installed modules, including enabled and disabled ones
+   *
+   * @return array CRM_Core_Module
+   *
+   */
+  function getModules() {
+    $result = array();
+    $q = db_query('SELECT name, status FROM {system} WHERE type = \'module\' AND schema_version <> -1');
+    foreach ($q as $row) {
+      $result[] = new CRM_Core_Module('drupal.' . $row->name, ($row->status == 1) ? TRUE : FALSE);
+    }
+    return $result;
+  }
+
+  /**
+   * Find any users/roles/security-principals with the given permission
+   * and replace it with one or more permissions.
+   *
+   * @param $oldPerm string
+   * @param $newPerms array, strings
+   *
+   * @return void
+   */
+  function replacePermission($oldPerm, $newPerms) {
+    $roles = user_roles(FALSE, $oldPerm);
+    if (!empty($roles)) {
+      foreach (array_keys($roles) as $rid) {
+        user_role_revoke_permissions($rid, array($oldPerm));
+        user_role_grant_permissions($rid, $newPerms);
+      }
+    }
+  }
+  /**
+   * Format the url as per language Negotiation.
+   *
+   * @param string $url
+   *
+   * @return string $url, formatted url.
+   * @static
+   */
+  function languageNegotiationURL($url, $addLanguagePart = TRUE, $removeLanguagePart = FALSE) {
+    if (empty($url)) {
+      return $url;
+    }
+
+    //CRM-7803 -from d7 onward.
+    $config = CRM_Core_Config::singleton();
+    if (function_exists('variable_get') &&
+      module_exists('locale') &&
+      function_exists('language_negotiation_get')
+    ) {
+      global $language;
+
+      //does user configuration allow language
+      //support from the URL (Path prefix or domain)
+      if (language_negotiation_get('language') == 'locale-url') {
+        $urlType = variable_get('locale_language_negotiation_url_part');
+
+        //url prefix
+        if ($urlType == LOCALE_LANGUAGE_NEGOTIATION_URL_PREFIX) {
+          if (isset($language->prefix) && $language->prefix) {
+            if ($addLanguagePart) {
+              $url .= $language->prefix . '/';
+            }
+            if ($removeLanguagePart) {
+              $url = str_replace("/{$language->prefix}/", '/', $url);
+            }
+          }
+        }
+        //domain
+        if ($urlType == LOCALE_LANGUAGE_NEGOTIATION_URL_DOMAIN) {
+          if (isset($language->domain) && $language->domain) {
+            if ($addLanguagePart) {
+              $url = (CRM_Utils_System::isSSL() ? 'https' : 'http') . '://' . $language->domain . base_path();
+            }
+            if ($removeLanguagePart && defined('CIVICRM_UF_BASEURL')) {
+              $url = str_replace('\\', '/', $url);
+              $parseUrl = parse_url($url);
+
+              //kinda hackish but not sure how to do it right
+              //hope http_build_url() will help at some point.
+              if (is_array($parseUrl) && !empty($parseUrl)) {
+                $urlParts           = explode('/', $url);
+                $hostKey            = array_search($parseUrl['host'], $urlParts);
+                $ufUrlParts         = parse_url(CIVICRM_UF_BASEURL);
+                $urlParts[$hostKey] = $ufUrlParts['host'];
+                $url                = implode('/', $urlParts);
+              }
+            }
+          }
+        }
+      }
+    }
+    return $url;
+  }
+
+  /**
+   * GET CMS Version
+   * @return string
+   */
+  function getVersion() {
+    return defined('VERSION') ? VERSION : 'Unknown';
+  }
+
+  /**
+   */
+  function updateCategories() {
+    // copied this from profile.module. Seems a bit inefficient, but i dont know a better way
+    // CRM-3600
+    cache_clear_all();
+    menu_rebuild();
+  }
+
+  /**
+   * Get the default location for CiviCRM blocks
+   *
+   * @return string
+   *
+   */
+  function getDefaultBlockLocation() {
+    return 'sidebar_first';
+  }
+  /**
+   * Get the locale set in the hosting CMS
+   *
+   * @return string  with the locale or null for none
+   *
+   */
+  function getUFLocale() {
+    // return CiviCRM’s xx_YY locale that either matches Drupal’s Chinese locale
+    // (for CRM-6281), Drupal’s xx_YY or is retrieved based on Drupal’s xx
+    // sometimes for CLI based on order called, this might not be set and/or empty
+    global $language;
+
+    if (empty($language)) {
+      return NULL;
+    }
+
+    if ($language->language == 'zh-hans') {
+      return 'zh_CN';
+    }
+
+    if ($language->language == 'zh-hant') {
+      return 'zh_TW';
+    }
+
+    if (preg_match('/^.._..$/', $language->language)) {
+      return $language->language;
+    }
+
+    return CRM_Core_I18n_PseudoConstant::longForShort(substr($language->language, 0, 2));
+  }
+  /**
+   * Perform any post login activities required by the UF -
+   * e.g. for drupal: records a watchdog message about the new session, saves the login timestamp,
+   * calls hook_user op 'login' and generates a new session.
+   *
+   * @param array params
+   *
+   * FIXME: Document values accepted/required by $params
+   *
+   */
+  function userLoginFinalize($params = array()){
+    user_login_finalize($params);
+  }
+
+  /**
+   * figure out the post url for the form
+   *
+   * @param mix $action the default action if one is pre-specified
+   *
+   * @return string the url to post the form
+   * @access public
+   */
+  function postURL($action) {
+    if (!empty($action)) {
+      return $action;
+    }
+    return $this->url($_GET['q']);
+  }
 }