CRM-13120 - APIv3 - Don't return "null" from "create" action
authorTim Otten <totten@civicrm.org>
Thu, 1 Aug 2013 21:19:00 +0000 (14:19 -0700)
committerTim Otten <totten@civicrm.org>
Thu, 1 Aug 2013 22:45:15 +0000 (15:45 -0700)
 * Split CRM_Core_HTMLInputCoder in three:
   * CRM_Utils_API_AbstractFieldCoder
   * CRM_Utils_API_HTMLInputCoder
   * CRM_Core_HTMLInputCoder (for backward compatibility)
 * Add CRM_Utils_API_NullOutputCoder (based on AbstractFieldCoder)

----------------------------------------
* CRM-13120:
  http://issues.civicrm.org/jira/browse/CRM-13120

CRM/Core/HTMLInputCoder.php
CRM/Utils/API/AbstractFieldCoder.php [new file with mode: 0644]
CRM/Utils/API/HTMLInputCoder.php [new file with mode: 0644]
CRM/Utils/API/NullOutputCoder.php [new file with mode: 0644]
api/api.php

index 5bfd11fee1731e20e29ea85d263027d88efb148e..ba7eeb85fe871d85036553bf7877552696112dc0 100644 (file)
  | see the CiviCRM license FAQ at http://civicrm.org/licensing        |
  +--------------------------------------------------------------------+
 */
-
-/**
- * This class captures the encoding practices of CRM-5667 in a reusable
- * fashion.  In this design, all submitted values are partially HTML-encoded
- * before saving to the database.  If a DB reader needs to output in
- * non-HTML medium, then it should undo the partial HTML encoding.
- *
- * This class should be short-lived -- 4.3 should introduce an alternative
- * escaping scheme and consequently remove HTMLInputCoder.
- *
- * @package CRM
- * @copyright CiviCRM LLC (c) 2004-2013
- * $Id$
- *
- */
-
-require_once 'api/Wrapper.php';
-class CRM_Core_HTMLInputCoder implements API_Wrapper {
-  private static $skipFields = NULL;
-
-  /**
-   * @var CRM_Core_HTMLInputCoder
-   */
-  private static $_singleton = NULL;
-
-  /**
-   * @return CRM_Core_HTMLInputCoder
-   */
-  public static function singleton() {
-    if (self::$_singleton === NULL) {
-      self::$_singleton = new CRM_Core_HTMLInputCoder();
-    }
-    return self::$_singleton;
-  }
-
-  /**
-   * @return array<string> list of field names
-   */
-  public static function getSkipFields() {
-    if (self::$skipFields === NULL) {
-      self::$skipFields = array(
-        'widget_code',
-        'html_message',
-        'body_html',
-        'msg_html',
-        'description',
-        'intro',
-        'thankyou_text',
-        'tf_thankyou_text',
-        'intro_text',
-        'page_text',
-        'body_text',
-        'footer_text',
-        'thankyou_footer',
-        'thankyou_footer_text',
-        'new_text',
-        'renewal_text',
-        'help_pre',
-        'help_post',
-        'confirm_title',
-        'confirm_text',
-        'confirm_footer_text',
-        'confirm_email_text',
-        'event_full_text',
-        'waitlist_text',
-        'approval_req_text',
-        'report_header',
-        'report_footer',
-        'cc_id',
-        'bcc_id',
-        'premiums_intro_text',
-        'honor_block_text',
-        'pay_later_text',
-        'pay_later_receipt',
-        'label', // This is needed for FROM Email Address configuration. dgg
-        'url',  // This is needed for navigation items urls
-        'details',
-        'msg_text', // message templates’ text versions
-        'text_message', // (send an) email to contact’s and CiviMail’s text version
-        'data', // data i/p of persistent table
-        'sqlQuery', // CRM-6673
-        'pcp_title',
-        'pcp_intro_text',
-        'new', // The 'new' text in word replacements
-      );
-    }
-    return self::$skipFields;
-  }
+class CRM_Core_HTMLInputCoder {
 
   /**
    * @param string $fldName
    * @return bool TRUE if encoding should be skipped for this field
    */
   public static function isSkippedField($fldName) {
-    $skipFields = self::getSkipFields();
-
-    // Field should be skipped
-    if (in_array($fldName, $skipFields)) {
-      return TRUE;
-    }
-    // Field is multilingual and after cutting off _xx_YY should be skipped (CRM-7230)…
-    if ((preg_match('/_[a-z][a-z]_[A-Z][A-Z]$/', $fldName) && in_array(substr($fldName, 0, -6), $skipFields))) {
-      return TRUE;
-    }
-    // Field can take multiple entries, eg. fieldName[1], fieldName[2], etc.
-    // We remove the index and check again if the fieldName in the list of skipped fields.
-    $matches = array();
-    if (preg_match('/^(.*)\[\d+\]/', $fldName, $matches) && in_array($matches[1], $skipFields)) {
-      return TRUE;
-    }
-
-    return FALSE;
+    return CRM_Utils_API_HTMLInputCoder::singleton()->isSkippedField($fldName);
   }
 
   /**
@@ -147,71 +43,7 @@ class CRM_Core_HTMLInputCoder implements API_Wrapper {
    *    If FALSE, then non-string values will be preserved
    */
   public static function encodeInput(&$values, $castToString = TRUE) {
-    if (is_array($values)) {
-      foreach ($values as &$value) {
-        self::encodeInput($value);
-      }
-    } elseif ($castToString || is_string($values)) {
-      $values = str_replace(array('<', '>'), array('&lt;', '&gt;'), $values);
-    }
-  }
-
-  public static function decodeOutput(&$values, $castToString = TRUE) {
-    if (is_array($values)) {
-      foreach ($values as &$value) {
-        self::decodeOutput($value);
-      }
-    } elseif ($castToString || is_string($values)) {
-      $values = str_replace(array('&lt;', '&gt;'), array('<', '>'), $values);
-    }
-  }
-
-  /**
-   * {@inheritDoc}
-   */
-  public function fromApiInput($apiRequest) {
-    $lowerAction = strtolower($apiRequest['action']);
-    if ($apiRequest['version'] == 3 && in_array($lowerAction, array('get', 'create'))) {
-      // note: 'getsingle', 'replace', 'update', and chaining all build on top of 'get'/'create'
-      foreach ($apiRequest['params'] as $key => $value) {
-        // Don't apply escaping to API control parameters (e.g. 'api.foo' or 'options.foo')
-        // and don't apply to other skippable fields
-        if (!self::isApiControlField($key) && !self::isSkippedField($key)) {
-          self::encodeInput($apiRequest['params'][$key], FALSE);
-        }
-      }
-    } elseif ($apiRequest['version'] == 3 && $lowerAction == 'setvalue') {
-      if (isset($apiRequest['params']['field']) && isset($apiRequest['params']['value'])) {
-        if (!self::isSkippedField($apiRequest['params']['field'])) {
-          self::encodeInput($apiRequest['params']['value'], FALSE);
-        }
-      }
-    }
-    return $apiRequest;
+    return CRM_Utils_API_HTMLInputCoder::singleton()->encodeInput($values, $castToString);
   }
 
-  /**
-   * {@inheritDoc}
-   */
-  public function toApiOutput($apiRequest, $result) {
-    $lowerAction = strtolower($apiRequest['action']);
-    if ($apiRequest['version'] == 3 && in_array($lowerAction, array('get', 'create', 'setvalue'))) {
-      foreach ($result as $key => $value) {
-        // Don't apply escaping to API control parameters (e.g. 'api.foo' or 'options.foo')
-        // and don't apply to other skippable fields
-        if (!self::isApiControlField($key) && !self::isSkippedField($key)) {
-          self::decodeOutput($result[$key], FALSE);
-        }
-      }
-    }
-    // setvalue?
-    return $result;
-  }
-
-  /**
-   * @return bool
-   */
-  protected function isApiControlField($key) {
-    return (FALSE !== strpos($key, '.'));
-  }
-}
+}
\ No newline at end of file
diff --git a/CRM/Utils/API/AbstractFieldCoder.php b/CRM/Utils/API/AbstractFieldCoder.php
new file mode 100644 (file)
index 0000000..7712940
--- /dev/null
@@ -0,0 +1,135 @@
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | CiviCRM version 4.3                                                |
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC (c) 2004-2013                                |
+ +--------------------------------------------------------------------+
+ | This file is a part of CiviCRM.                                    |
+ |                                                                    |
+ | CiviCRM is free software; you can copy, modify, and distribute it  |
+ | under the terms of the GNU Affero General Public License           |
+ | Version 3, 19 November 2007 and the CiviCRM Licensing Exception.   |
+ |                                                                    |
+ | CiviCRM is distributed in the hope that it will be useful, but     |
+ | WITHOUT ANY WARRANTY; without even the implied warranty of         |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.               |
+ | See the GNU Affero General Public License for more details.        |
+ |                                                                    |
+ | You should have received a copy of the GNU Affero General Public   |
+ | License and the CiviCRM Licensing Exception along                  |
+ | with this program; if not, contact CiviCRM LLC                     |
+ | at info[AT]civicrm[DOT]org. If you have questions about the        |
+ | GNU Affero General Public License or the licensing of CiviCRM,     |
+ | see the CiviCRM license FAQ at http://civicrm.org/licensing        |
+ +--------------------------------------------------------------------+
+*/
+
+/**
+ * Base class for writing API_Wrappers which generically manipulate the content
+ * of all fields (except for some black-listed skip-fields).
+ *
+ * @package CRM
+ * @copyright CiviCRM LLC (c) 2004-2013
+ * $Id$
+ *
+ */
+
+require_once 'api/Wrapper.php';
+abstract class CRM_Utils_API_AbstractFieldCoder implements API_Wrapper {
+
+  /**
+   * @return array<string> list of field names
+   */
+  public function getSkipFields() {
+    return NULL;
+  }
+
+  /**
+   * @param string $fldName
+   * @return bool TRUE if encoding should be skipped for this field
+   */
+  public function isSkippedField($fldName) {
+    $skipFields = $this->getSkipFields();
+    if ($skipFields === NULL) {
+      return FALSE;
+    }
+
+    // Field should be skipped
+    if (in_array($fldName, $skipFields)) {
+      return TRUE;
+    }
+    // Field is multilingual and after cutting off _xx_YY should be skipped (CRM-7230)…
+    if ((preg_match('/_[a-z][a-z]_[A-Z][A-Z]$/', $fldName) && in_array(substr($fldName, 0, -6), $skipFields))) {
+      return TRUE;
+    }
+    // Field can take multiple entries, eg. fieldName[1], fieldName[2], etc.
+    // We remove the index and check again if the fieldName in the list of skipped fields.
+    $matches = array();
+    if (preg_match('/^(.*)\[\d+\]/', $fldName, $matches) && in_array($matches[1], $skipFields)) {
+      return TRUE;
+    }
+
+    return FALSE;
+  }
+
+  /**
+   * This function is going to filter the
+   * submitted values.
+   *
+   * @param array|string $values the field value from the API
+   */
+  public abstract function encodeInput(&$values);
+
+  public abstract function decodeOutput(&$values);
+
+  /**
+   * {@inheritDoc}
+   */
+  public function fromApiInput($apiRequest) {
+    $lowerAction = strtolower($apiRequest['action']);
+    if ($apiRequest['version'] == 3 && in_array($lowerAction, array('get', 'create'))) {
+      // note: 'getsingle', 'replace', 'update', and chaining all build on top of 'get'/'create'
+      foreach ($apiRequest['params'] as $key => $value) {
+        // Don't apply escaping to API control parameters (e.g. 'api.foo' or 'options.foo')
+        // and don't apply to other skippable fields
+        if (!$this->isApiControlField($key) && !$this->isSkippedField($key)) {
+          $this->encodeInput($apiRequest['params'][$key]);
+        }
+      }
+    }
+    elseif ($apiRequest['version'] == 3 && $lowerAction == 'setvalue') {
+      if (isset($apiRequest['params']['field']) && isset($apiRequest['params']['value'])) {
+        if (!$this->isSkippedField($apiRequest['params']['field'])) {
+          $this->encodeInput($apiRequest['params']['value']);
+        }
+      }
+    }
+    return $apiRequest;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function toApiOutput($apiRequest, $result) {
+    $lowerAction = strtolower($apiRequest['action']);
+    if ($apiRequest['version'] == 3 && in_array($lowerAction, array('get', 'create', 'setvalue'))) {
+      foreach ($result as $key => $value) {
+        // Don't apply escaping to API control parameters (e.g. 'api.foo' or 'options.foo')
+        // and don't apply to other skippable fields
+        if (!$this->isApiControlField($key) && !$this->isSkippedField($key)) {
+          $this->decodeOutput($result[$key]);
+        }
+      }
+    }
+    // setvalue?
+    return $result;
+  }
+
+  /**
+   * @return bool
+   */
+  protected function isApiControlField($key) {
+    return (FALSE !== strpos($key, '.'));
+  }
+}
diff --git a/CRM/Utils/API/HTMLInputCoder.php b/CRM/Utils/API/HTMLInputCoder.php
new file mode 100644 (file)
index 0000000..422aa77
--- /dev/null
@@ -0,0 +1,142 @@
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | CiviCRM version 4.3                                                |
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC (c) 2004-2013                                |
+ +--------------------------------------------------------------------+
+ | This file is a part of CiviCRM.                                    |
+ |                                                                    |
+ | CiviCRM is free software; you can copy, modify, and distribute it  |
+ | under the terms of the GNU Affero General Public License           |
+ | Version 3, 19 November 2007 and the CiviCRM Licensing Exception.   |
+ |                                                                    |
+ | CiviCRM is distributed in the hope that it will be useful, but     |
+ | WITHOUT ANY WARRANTY; without even the implied warranty of         |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.               |
+ | See the GNU Affero General Public License for more details.        |
+ |                                                                    |
+ | You should have received a copy of the GNU Affero General Public   |
+ | License and the CiviCRM Licensing Exception along                  |
+ | with this program; if not, contact CiviCRM LLC                     |
+ | at info[AT]civicrm[DOT]org. If you have questions about the        |
+ | GNU Affero General Public License or the licensing of CiviCRM,     |
+ | see the CiviCRM license FAQ at http://civicrm.org/licensing        |
+ +--------------------------------------------------------------------+
+*/
+
+/**
+ * This class captures the encoding practices of CRM-5667 in a reusable
+ * fashion.  In this design, all submitted values are partially HTML-encoded
+ * before saving to the database.  If a DB reader needs to output in
+ * non-HTML medium, then it should undo the partial HTML encoding.
+ *
+ * This class should be short-lived -- 4.3 should introduce an alternative
+ * escaping scheme and consequently remove HTMLInputCoder.
+ *
+ * @package CRM
+ * @copyright CiviCRM LLC (c) 2004-2013
+ * $Id$
+ *
+ */
+
+class CRM_Utils_API_HTMLInputCoder extends CRM_Utils_API_AbstractFieldCoder {
+  private $skipFields = NULL;
+
+  /**
+   * @var CRM_Utils_API_HTMLInputCoder
+   */
+  private static $_singleton = NULL;
+
+  /**
+   * @return CRM_Utils_API_HTMLInputCoder
+   */
+  public static function singleton() {
+    if (self::$_singleton === NULL) {
+      self::$_singleton = new CRM_Utils_API_HTMLInputCoder();
+    }
+    return self::$_singleton;
+  }
+
+  /**
+   * @return array<string> list of field names
+   */
+  public function getSkipFields() {
+    if ($this->skipFields === NULL) {
+      $this->skipFields = array(
+        'widget_code',
+        'html_message',
+        'body_html',
+        'msg_html',
+        'description',
+        'intro',
+        'thankyou_text',
+        'tf_thankyou_text',
+        'intro_text',
+        'page_text',
+        'body_text',
+        'footer_text',
+        'thankyou_footer',
+        'thankyou_footer_text',
+        'new_text',
+        'renewal_text',
+        'help_pre',
+        'help_post',
+        'confirm_title',
+        'confirm_text',
+        'confirm_footer_text',
+        'confirm_email_text',
+        'event_full_text',
+        'waitlist_text',
+        'approval_req_text',
+        'report_header',
+        'report_footer',
+        'cc_id',
+        'bcc_id',
+        'premiums_intro_text',
+        'honor_block_text',
+        'pay_later_text',
+        'pay_later_receipt',
+        'label', // This is needed for FROM Email Address configuration. dgg
+        'url',  // This is needed for navigation items urls
+        'details',
+        'msg_text', // message templates’ text versions
+        'text_message', // (send an) email to contact’s and CiviMail’s text version
+        'data', // data i/p of persistent table
+        'sqlQuery', // CRM-6673
+        'pcp_title',
+        'pcp_intro_text',
+        'new', // The 'new' text in word replacements
+      );
+    }
+    return $this->skipFields;
+  }
+
+  /**
+   * This function is going to filter the
+   * submitted values across XSS vulnerability.
+   *
+   * @param array|string $values
+   * @param bool $castToString If TRUE, all scalars will be filtered (and therefore cast to strings)
+   *    If FALSE, then non-string values will be preserved
+   */
+  public function encodeInput(&$values, $castToString = FALSE) {
+    if (is_array($values)) {
+      foreach ($values as &$value) {
+        $this->encodeInput($value, TRUE);
+      }
+    } elseif ($castToString || is_string($values)) {
+      $values = str_replace(array('<', '>'), array('&lt;', '&gt;'), $values);
+    }
+  }
+
+  public function decodeOutput(&$values, $castToString = FALSE) {
+    if (is_array($values)) {
+      foreach ($values as &$value) {
+        $this->decodeOutput($value, TRUE);
+      }
+    } elseif ($castToString || is_string($values)) {
+      $values = str_replace(array('&lt;', '&gt;'), array('<', '>'), $values);
+    }
+  }
+}
diff --git a/CRM/Utils/API/NullOutputCoder.php b/CRM/Utils/API/NullOutputCoder.php
new file mode 100644 (file)
index 0000000..0de35a4
--- /dev/null
@@ -0,0 +1,87 @@
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | CiviCRM version 4.3                                                |
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC (c) 2004-2013                                |
+ +--------------------------------------------------------------------+
+ | This file is a part of CiviCRM.                                    |
+ |                                                                    |
+ | CiviCRM is free software; you can copy, modify, and distribute it  |
+ | under the terms of the GNU Affero General Public License           |
+ | Version 3, 19 November 2007 and the CiviCRM Licensing Exception.   |
+ |                                                                    |
+ | CiviCRM is distributed in the hope that it will be useful, but     |
+ | WITHOUT ANY WARRANTY; without even the implied warranty of         |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.               |
+ | See the GNU Affero General Public License for more details.        |
+ |                                                                    |
+ | You should have received a copy of the GNU Affero General Public   |
+ | License and the CiviCRM Licensing Exception along                  |
+ | with this program; if not, contact CiviCRM LLC                     |
+ | at info[AT]civicrm[DOT]org. If you have questions about the        |
+ | GNU Affero General Public License or the licensing of CiviCRM,     |
+ | see the CiviCRM license FAQ at http://civicrm.org/licensing        |
+ +--------------------------------------------------------------------+
+*/
+
+/**
+ * Work-around for CRM-13120 - The "create" action incorrectly returns string literal "null"
+ * when the actual value is NULL or "". Rewrite the output.
+ *
+ * @package CRM
+ * @copyright CiviCRM LLC (c) 2004-2013
+ * $Id$
+ */
+
+require_once 'api/Wrapper.php';
+class CRM_Utils_API_NullOutputCoder extends CRM_Utils_API_AbstractFieldCoder {
+
+  /**
+   * @var CRM_Utils_API_NullOutputCoder
+   */
+  private static $_singleton = NULL;
+
+  /**
+   * @return CRM_Utils_API_NullOutputCoder
+   */
+  public static function singleton() {
+    if (self::$_singleton === NULL) {
+      self::$_singleton = new CRM_Utils_API_NullOutputCoder();
+    }
+    return self::$_singleton;
+  }
+
+  /**
+   * This function is going to filter the
+   * submitted values across XSS vulnerability.
+   *
+   * @param array|string $values
+   * @param bool $castToString If TRUE, all scalars will be filtered (and therefore cast to strings)
+   *    If FALSE, then non-string values will be preserved
+   */
+  public function encodeInput(&$values) {
+  }
+
+  public function decodeOutput(&$values, $castToString = FALSE) {
+    if (is_array($values)) {
+      foreach ($values as &$value) {
+        $this->decodeOutput($value, TRUE);
+      }
+    }
+    elseif ($castToString || is_string($values)) {
+      if ($values === 'null') {
+        $values = '';
+      }
+    }
+  }
+
+  public function toApiOutput($apiRequest, $result) {
+    $lowerAction = strtolower($apiRequest['action']);
+    if ($lowerAction === 'create') {
+      return parent::toApiOutput($apiRequest, $result);
+    } else {
+      return $result;
+    }
+  }
+}
index 5062422df6d50280a1d51bf268309bf4fec9667c..76637548ce14bd15f3499bf7737436abe82c2364 100644 (file)
  *   array to be passed to function
  */
 function civicrm_api($entity, $action, $params, $extra = NULL) {
-  $apiWrappers = array(CRM_Core_HTMLInputCoder::singleton());
+  $apiWrappers = array(
+    CRM_Utils_API_HTMLInputCoder::singleton(),
+    CRM_Utils_API_NullOutputCoder::singleton()
+  );
   try {
     require_once ('api/v3/utils.php');
     require_once 'api/Exception.php';
@@ -58,6 +61,7 @@ function civicrm_api($entity, $action, $params, $extra = NULL) {
       civicrm_api3_verify_mandatory($apiRequest['params'], NULL, _civicrm_api3_getrequired($apiRequest));
     }
 
+    // For input filtering, process $apiWrappers in forward order
     foreach ($apiWrappers as $apiWrapper) {
       $apiRequest = $apiWrapper->fromApiInput($apiRequest);
     }
@@ -78,7 +82,8 @@ function civicrm_api($entity, $action, $params, $extra = NULL) {
       return civicrm_api3_create_error("API (" . $apiRequest['entity'] . "," . $apiRequest['action'] . ") does not exist (join the API team and implement it!)");
     }
 
-    foreach ($apiWrappers as $apiWrapper) {
+    // For output filtering, process $apiWrappers in reverse order
+    foreach (array_reverse($apiWrappers) as $apiWrapper) {
       $result = $apiWrapper->toApiOutput($apiRequest, $result);
     }