Page MenuHomePhorge

Regression in PhabricatorStandardCustomField.php:304 - field can be an integer
Closed, ResolvedPublic

Description

It seems we have a regression when an User has an extra field that is non-string and non-empty.

Steps to reproduce:

Set the config user.custom-field-definitions to this value:

{
  "mycompany:life": {
    "name": "Estimated Hours of Life",
    "type": "int",
    "instructions": "Estimated number of hours this human will take.",
  }
}

Then, visit your profile and set a value.

What happens:

EXCEPTION: (InvalidArgumentException) Call to phutil_nonempty_string() expected null or a string, got: int. at [<arcanist>/src/utils/utils.php:2127]
arcanist(head=arcpatch-D25304, ref.master=97e163187418, ref.arcpatch-D25304=b1c4cb85e0d1), phorge(head=arcpatch-D25322, ref.master=d725ffaa7728, ref.arcpatch-D25322=c076e9ab360f)
#0 <#2> phutil_nonempty_string(integer) called at [<phorge>/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php:304]
#1 <#2> PhabricatorStandardCustomField::renderPropertyViewValue(array) called at [<phorge>/src/infrastructure/customfield/field/PhabricatorCustomField.php:1301]
#2 <#2> PhabricatorCustomField::renderPropertyViewValue(array) called at [<phorge>/src/infrastructure/customfield/field/PhabricatorCustomFieldList.php:159]
#3 <#2> PhabricatorCustomFieldList::appendFieldsToPropertyList(PhabricatorUser, PhabricatorUser, PHUIPropertyListView) called at [<phorge>/src/applications/people/controller/PhabricatorPeopleProfileManageController.php:74]
#4 <#2> PhabricatorPeopleProfileManageController::buildPropertyView(PhabricatorUser) called at [<phorge>/src/applications/people/controller/PhabricatorPeopleProfileManageController.php:29]
#5 <#2> PhabricatorPeopleProfileManageController::handleRequest(AphrontRequest) called at [<phorge>/src/aphront/configuration/AphrontApplicationConfiguration.php:284]
#6 phlog(InvalidArgumentException) called at [<phorge>/src/aphront/handler/PhabricatorDefaultRequestExceptionHandler.php:41]
#7 PhabricatorDefaultRequestExceptionHandler::handleRequestThrowable(AphrontRequest, InvalidArgumentException) called at [<phorge>/src/aphront/configuration/AphrontApplicationConfiguration.php:751]
#8 AphrontApplicationConfiguration::handleThrowable(InvalidArgumentException) called at [<phorge>/src/aphront/configuration/AphrontApplicationConfiguration.php:296]
#9 AphrontApplicationConfiguration::processRequest(AphrontRequest, PhutilDeferredLog, AphrontPHPHTTPSink, MultimeterControl) called at [<phorge>/src/aphront/configuration/AphrontApplicationConfiguration.php:204]
#10 AphrontApplicationConfiguration::runHTTPRequest(AphrontPHPHTTPSink) called at [<phorge>/webroot/index.php:35]

Proposed solution

Maybe this is a good minimal fix:

diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php
index 37f9e808ab..f7f1fb216e 100644
--- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php
+++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php
@@ -301,7 +301,8 @@ abstract class PhabricatorStandardCustomField
   }

   public function renderPropertyViewValue(array $handles) {
-    if (!phutil_nonempty_string($this->getFieldValue())) {
+    $v_str = phutil_string_cast($this->getFieldValue());
+    if ($v_str === '') {
       return null;
     }
     return $this->getFieldValue();

Note that this does not work since phutil_nonempty_stringlike() does not allow a string:

diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php
index 37f9e808ab..f7f1fb216e 100644
--- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php
+++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php
@@ -301,7 +301,8 @@ abstract class PhabricatorStandardCustomField
   }

   public function renderPropertyViewValue(array $handles) {
-    if (!phutil_nonempty_string($this->getFieldValue())) {
+    if (!phutil_nonempty_stringlike($this->getFieldValue())) {
       return null;
     }
     return $this->getFieldValue();

Also note that this probably is not OK since phutil_nonempty_scalar() can explode when receiving a boolean T15239, and AFAIK an extra field can be boolean.

diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php
index 37f9e808ab..f7f1fb216e 100644
--- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php
+++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php
@@ -301,7 +301,8 @@ abstract class PhabricatorStandardCustomField
   }

   public function renderPropertyViewValue(array $handles) {
-    if (!phutil_nonempty_string($this->getFieldValue())) {
+    if (!phutil_nonempty_scalar($this->getFieldValue())) {
       return null;
     }
     return $this->getFieldValue();

Revisions and Commits

Event Timeline

valerio.bozzolan triaged this task as Unbreak Now! priority.
valerio.bozzolan created this object in space S1 Public.
avivey subscribed.

I don't think I intended to land this change.

Wouldn’t this be better as a null + strlen check? It was originally a strlen I assume.

Honestly I do not love the idea of if(strlen($something)) to answer the question "Is this empty when casted to string?".

Reasons:

  1. strlen() accepts a string, and it's not so much documented that it should be able to receive other things, so again, we are using an undocumented feature that may be deprecated e.g. strlen(123)
  2. strlen() does an internal cast to string to work, that is not much visible, but is what we want to work (that is why doing an explicit cast to string makes sense)
  3. strlen() also calculate the length of the string, and we don't need that information and so it's probably a bit of CPU-wasting. We are just interested in understanding if that thing, casted to string, it's empty or not.

I'm pretty sure that any place where strlen is getting something that isn't a string (including int), that's actually a bug. In Phabricator code, strlen was only supposed to be used when expecting a string or null; All the places we now find aren't strings, are places where we were wrong about the type of the object.
Epriestley says something like that at https://secure.phabricator.com/T13588#257329 .

The implicit cast done is often a problem.

Strictly speaking, the "right" fix here might be to override this method in PhabricatorStandardCustomFieldInt, and/or explicitly cast the input to string in this instance. renderPropertyViewValue() isn't explicitly documented, but I expect it might return complex objects (It returns phutil_safe_html in PhabricatorStandardCustomFieldPHIDs, e.g.).

So I think:

  1. Immediate fix: explicit cast to string
  2. Longer term - I'll probably get the Custom Fields diff out this week.

ok?

I agree that non-string/null should be handled differently. I guess I don’t see the difference between null + strlen being used vs. the proposed nonempty_string/stringlike, and that making that change is explicitly acknowledging that casting is expected/intentional when it isn’t and instead the different types should be handled appropriately (your suggested long-term solution).

Evan’s comments suggest that switching to one of the phutil functions should only be done when the type is unambiguously known to be a string, which is either often not the case or difficult/tedious to analyze for every call site. The result being that it’s probably a safer change to update to null checks on most places where it’s uncertain. In this case we’ve determined the uncertainty to be that non-string values are present and so the long-term solution is desired at some point.