| 1 | <?php |
| 2 | /* |
| 3 | +--------------------------------------------------------------------+ |
| 4 | | Copyright CiviCRM LLC. All rights reserved. | |
| 5 | | | |
| 6 | | This work is published under the GNU AGPLv3 license with some | |
| 7 | | permitted exceptions and without any warranty. For full license | |
| 8 | | and copyright information, see https://civicrm.org/licensing | |
| 9 | +--------------------------------------------------------------------+ |
| 10 | */ |
| 11 | |
| 12 | /** |
| 13 | * |
| 14 | * @package CRM |
| 15 | * @copyright CiviCRM LLC https://civicrm.org/licensing |
| 16 | * $Id$ |
| 17 | * |
| 18 | */ |
| 19 | |
| 20 | require_once 'HTML/QuickForm/Renderer/ArraySmarty.php'; |
| 21 | |
| 22 | /** |
| 23 | * Customize QF output to meet our specific requirements |
| 24 | */ |
| 25 | class CRM_Core_Form_Renderer extends HTML_QuickForm_Renderer_ArraySmarty { |
| 26 | |
| 27 | /** |
| 28 | * We only need one instance of this object. So we use the singleton |
| 29 | * pattern and cache the instance in this variable |
| 30 | * |
| 31 | * @var object |
| 32 | */ |
| 33 | static private $_singleton = NULL; |
| 34 | |
| 35 | /** |
| 36 | * The converter from array size to css class. |
| 37 | * |
| 38 | * @var array |
| 39 | */ |
| 40 | public static $_sizeMapper = [ |
| 41 | 2 => 'two', |
| 42 | 4 => 'four', |
| 43 | 6 => 'six', |
| 44 | 8 => 'eight', |
| 45 | 12 => 'twelve', |
| 46 | 20 => 'medium', |
| 47 | 30 => 'big', |
| 48 | 45 => 'huge', |
| 49 | ]; |
| 50 | |
| 51 | /** |
| 52 | * Constructor. |
| 53 | */ |
| 54 | public function __construct() { |
| 55 | $template = CRM_Core_Smarty::singleton(); |
| 56 | parent::__construct($template); |
| 57 | } |
| 58 | |
| 59 | /** |
| 60 | * Static instance provider. |
| 61 | * |
| 62 | * Method providing static instance of as in Singleton pattern. |
| 63 | */ |
| 64 | public static function &singleton() { |
| 65 | if (!isset(self::$_singleton)) { |
| 66 | self::$_singleton = new CRM_Core_Form_Renderer(); |
| 67 | } |
| 68 | return self::$_singleton; |
| 69 | } |
| 70 | |
| 71 | /** |
| 72 | * Creates an array representing an element containing. |
| 73 | * the key for storing this. We allow the parent to do most of the |
| 74 | * work, but then we add some CiviCRM specific enhancements to |
| 75 | * make the html compliant with our css etc |
| 76 | * |
| 77 | * |
| 78 | * @param HTML_QuickForm_element $element |
| 79 | * @param bool $required |
| 80 | * Whether an element is required. |
| 81 | * @param string $error |
| 82 | * Error associated with the element. |
| 83 | * |
| 84 | * @return array |
| 85 | */ |
| 86 | public function _elementToArray(&$element, $required, $error) { |
| 87 | self::updateAttributes($element, $required, $error); |
| 88 | |
| 89 | $el = parent::_elementToArray($element, $required, $error); |
| 90 | |
| 91 | // add label html |
| 92 | if (!empty($el['label'])) { |
| 93 | $id = $element->getAttribute('id'); |
| 94 | if (!empty($id)) { |
| 95 | $el['label'] = '<label for="' . $id . '">' . $el['label'] . '</label>'; |
| 96 | } |
| 97 | else { |
| 98 | $el['label'] = "<label>{$el['label']}</label>"; |
| 99 | } |
| 100 | } |
| 101 | |
| 102 | // Display-only (frozen) elements |
| 103 | if (!empty($el['frozen'])) { |
| 104 | if ($element->getAttribute('data-api-entity') && $element->getAttribute('data-entity-value')) { |
| 105 | $this->renderFrozenEntityRef($el, $element); |
| 106 | } |
| 107 | elseif ($element->getAttribute('type') == 'text' && $element->getAttribute('data-select-params')) { |
| 108 | $this->renderFrozenSelect2($el, $element); |
| 109 | } |
| 110 | elseif ($element->getAttribute('type') == 'text' && $element->getAttribute('data-crm-datepicker')) { |
| 111 | $this->renderFrozenDatepicker($el, $element); |
| 112 | } |
| 113 | elseif ($element->getAttribute('type') == 'text' && $element->getAttribute('formatType')) { |
| 114 | list($date, $time) = CRM_Utils_Date::setDateDefaults($element->getValue(), $element->getAttribute('formatType'), $element->getAttribute('format'), $element->getAttribute('timeformat')); |
| 115 | $date .= ($element->getAttribute('timeformat')) ? " $time" : ''; |
| 116 | $el['html'] = $date . '<input type="hidden" value="' . $element->getValue() . '" name="' . $element->getAttribute('name') . '">'; |
| 117 | } |
| 118 | // Render html for wysiwyg textareas |
| 119 | if ($el['type'] == 'textarea' && isset($element->_attributes['class']) && strstr($element->_attributes['class'], 'wysiwyg')) { |
| 120 | $el['html'] = '<span class="crm-frozen-field">' . $el['value'] . '</span>'; |
| 121 | } |
| 122 | else { |
| 123 | $el['html'] = '<span class="crm-frozen-field">' . $el['html'] . '</span>'; |
| 124 | } |
| 125 | } |
| 126 | // Active form elements |
| 127 | else { |
| 128 | $typesToShowEditLink = ['select', 'group']; |
| 129 | $hasEditPath = NULL !== $element->getAttribute('data-option-edit-path'); |
| 130 | |
| 131 | if (in_array($element->getType(), $typesToShowEditLink) && $hasEditPath) { |
| 132 | $this->addOptionsEditLink($el, $element); |
| 133 | } |
| 134 | |
| 135 | if ($element->getAttribute('allowClear')) { |
| 136 | $this->appendUnselectButton($el, $element); |
| 137 | } |
| 138 | } |
| 139 | |
| 140 | return $el; |
| 141 | } |
| 142 | |
| 143 | /** |
| 144 | * Update the attributes of this element and add a few CiviCRM |
| 145 | * based attributes so we can style this form element better |
| 146 | * |
| 147 | * |
| 148 | * @param HTML_QuickForm_element $element |
| 149 | * @param bool $required |
| 150 | * Whether an element is required. |
| 151 | * @param string $error |
| 152 | * Error associated with the element. |
| 153 | * |
| 154 | */ |
| 155 | public static function updateAttributes(&$element, $required, $error) { |
| 156 | // lets create an id for all input elements, so we can generate nice label tags |
| 157 | // to make it nice and clean, we'll just use the elementName if it is non null |
| 158 | $attributes = []; |
| 159 | if (!$element->getAttribute('id')) { |
| 160 | $name = $element->getAttribute('name'); |
| 161 | if ($name) { |
| 162 | $attributes['id'] = str_replace([']', '['], |
| 163 | ['', '_'], |
| 164 | $name |
| 165 | ); |
| 166 | } |
| 167 | } |
| 168 | |
| 169 | $class = $element->getAttribute('class'); |
| 170 | $type = $element->getType(); |
| 171 | if (!$class) { |
| 172 | if ($type == 'text' || $type == 'password') { |
| 173 | $size = $element->getAttribute('size'); |
| 174 | if (!empty($size)) { |
| 175 | $class = CRM_Utils_Array::value($size, self::$_sizeMapper); |
| 176 | } |
| 177 | } |
| 178 | } |
| 179 | |
| 180 | if ($type == 'select' && $element->getAttribute('multiple')) { |
| 181 | $type = 'multiselect'; |
| 182 | } |
| 183 | // Add widget-specific class |
| 184 | if (!$class || strpos($class, 'crm-form-') === FALSE) { |
| 185 | $class = ($class ? "$class " : '') . 'crm-form-' . $type; |
| 186 | } |
| 187 | elseif (strpos($class, 'crm-form-entityref') !== FALSE) { |
| 188 | self::preProcessEntityRef($element); |
| 189 | } |
| 190 | elseif (strpos($class, 'crm-form-contact-reference') !== FALSE) { |
| 191 | self::preprocessContactReference($element); |
| 192 | } |
| 193 | // Hack to support html5 fields (number, url, etc) |
| 194 | else { |
| 195 | foreach (CRM_Core_Form::$html5Types as $type) { |
| 196 | if (strpos($class, "crm-form-$type") !== FALSE) { |
| 197 | $element->setAttribute('type', $type); |
| 198 | // Also add the "base" class for consistent styling |
| 199 | $class .= ' crm-form-text'; |
| 200 | break; |
| 201 | } |
| 202 | } |
| 203 | } |
| 204 | |
| 205 | if ($required) { |
| 206 | $class .= ' required'; |
| 207 | } |
| 208 | |
| 209 | if ($error) { |
| 210 | $class .= ' error'; |
| 211 | } |
| 212 | |
| 213 | $attributes['class'] = $class; |
| 214 | $element->updateAttributes($attributes); |
| 215 | } |
| 216 | |
| 217 | /** |
| 218 | * Convert IDs to values and format for display. |
| 219 | * |
| 220 | * @param HTML_QuickForm_element $field |
| 221 | */ |
| 222 | public static function preProcessEntityRef($field) { |
| 223 | $val = $field->getValue(); |
| 224 | // Temporarily convert string values to an array |
| 225 | if (!is_array($val)) { |
| 226 | // Try to auto-detect method of serialization |
| 227 | $val = strpos($val, ',') ? explode(',', str_replace(', ', ',', $val)) : (array) CRM_Utils_Array::explodePadded($val); |
| 228 | } |
| 229 | if ($val) { |
| 230 | $entity = $field->getAttribute('data-api-entity'); |
| 231 | // Get api params, ensure it is an array |
| 232 | $params = $field->getAttribute('data-api-params'); |
| 233 | $params = $params ? json_decode($params, TRUE) : []; |
| 234 | $result = civicrm_api3($entity, 'getlist', ['id' => $val] + $params); |
| 235 | // Purify label output of entityreference fields |
| 236 | if (!empty($result['values'])) { |
| 237 | foreach ($result['values'] as &$res) { |
| 238 | if (!empty($res['label'])) { |
| 239 | $res['label'] = CRM_Utils_String::purifyHTML($res['label']); |
| 240 | } |
| 241 | } |
| 242 | } |
| 243 | if ($field->isFrozen()) { |
| 244 | // Prevent js from treating frozen entityRef as a "live" field |
| 245 | $field->removeAttribute('class'); |
| 246 | } |
| 247 | if (!empty($result['values'])) { |
| 248 | $field->setAttribute('data-entity-value', json_encode($result['values'])); |
| 249 | } |
| 250 | // CRM-15803 - Remove invalid values |
| 251 | $val = array_intersect($val, CRM_Utils_Array::collect('id', $result['values'])); |
| 252 | } |
| 253 | // Convert array values back to a string |
| 254 | $field->setValue(implode(',', $val)); |
| 255 | } |
| 256 | |
| 257 | /** |
| 258 | * Render datepicker as text. |
| 259 | * |
| 260 | * @param array $el |
| 261 | * @param HTML_QuickForm_element $field |
| 262 | */ |
| 263 | public function renderFrozenDatepicker(&$el, $field) { |
| 264 | $settings = json_decode($field->getAttribute('data-crm-datepicker'), TRUE); |
| 265 | $settings += ['date' => TRUE, 'time' => TRUE]; |
| 266 | $val = $field->getValue(); |
| 267 | if ($val) { |
| 268 | $dateFormat = NULL; |
| 269 | if (!$settings['time']) { |
| 270 | $val = substr($val, 0, 10); |
| 271 | } |
| 272 | elseif (!$settings['date']) { |
| 273 | $dateFormat = Civi::settings()->get('dateformatTime'); |
| 274 | } |
| 275 | $val = CRM_Utils_Date::customFormat($val, $dateFormat); |
| 276 | } |
| 277 | $el['html'] = $val . '<input type="hidden" value="' . $field->getValue() . '" name="' . $field->getAttribute('name') . '">'; |
| 278 | } |
| 279 | |
| 280 | /** |
| 281 | * Render select2 as text. |
| 282 | * |
| 283 | * @param array $el |
| 284 | * @param HTML_QuickForm_element $field |
| 285 | */ |
| 286 | public function renderFrozenSelect2(&$el, $field) { |
| 287 | $params = json_decode($field->getAttribute('data-select-params'), TRUE); |
| 288 | $val = $field->getValue(); |
| 289 | if ($val && !empty($params['data'])) { |
| 290 | $display = []; |
| 291 | foreach (explode(',', $val) as $item) { |
| 292 | $match = CRM_Utils_Array::findInTree($item, $params['data']); |
| 293 | if (isset($match['text']) && strlen($match['text'])) { |
| 294 | $display[] = CRM_Utils_String::purifyHTML($match['text']); |
| 295 | } |
| 296 | } |
| 297 | $el['html'] = implode('; ', $display) . '<input type="hidden" value="' . $field->getValue() . '" name="' . $field->getAttribute('name') . '">'; |
| 298 | } |
| 299 | } |
| 300 | |
| 301 | /** |
| 302 | * Render entity references as text. |
| 303 | * If user has permission, format as link (for now limited to contacts). |
| 304 | * |
| 305 | * @param array $el |
| 306 | * @param HTML_QuickForm_element $field |
| 307 | */ |
| 308 | public function renderFrozenEntityRef(&$el, $field) { |
| 309 | $entity = $field->getAttribute('data-api-entity'); |
| 310 | $vals = json_decode($field->getAttribute('data-entity-value'), TRUE); |
| 311 | $display = []; |
| 312 | |
| 313 | // Custom fields of type contactRef store their data in a slightly different format |
| 314 | if ($field->getAttribute('data-crm-custom') && $entity == 'Contact') { |
| 315 | $vals = [['id' => $vals['id'], 'label' => $vals['text']]]; |
| 316 | } |
| 317 | |
| 318 | foreach ($vals as $val) { |
| 319 | // Format contact as link |
| 320 | if ($entity == 'Contact' && CRM_Contact_BAO_Contact_Permission::allow($val['id'], CRM_Core_Permission::VIEW)) { |
| 321 | $url = CRM_Utils_System::url("civicrm/contact/view", ['reset' => 1, 'cid' => $val['id']]); |
| 322 | $val['label'] = '<a class="view-contact no-popup" href="' . $url . '" title="' . ts('View Contact') . '">' . CRM_Utils_String::purifyHTML($val['label']) . '</a>'; |
| 323 | } |
| 324 | $display[] = $val['label']; |
| 325 | } |
| 326 | |
| 327 | $el['html'] = implode('; ', $display) . '<input type="hidden" value="' . $field->getValue() . '" name="' . $field->getAttribute('name') . '">'; |
| 328 | } |
| 329 | |
| 330 | /** |
| 331 | * Pre-fill contact name for a custom field of type ContactReference |
| 332 | * |
| 333 | * Todo: Migrate contact reference fields to use EntityRef |
| 334 | * |
| 335 | * @param HTML_QuickForm_element $field |
| 336 | */ |
| 337 | public static function preprocessContactReference($field) { |
| 338 | $val = $field->getValue(); |
| 339 | if ($val && is_numeric($val)) { |
| 340 | |
| 341 | $list = array_keys(CRM_Core_BAO_Setting::valueOptions(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, |
| 342 | 'contact_reference_options' |
| 343 | ), '1'); |
| 344 | |
| 345 | $return = array_unique(array_merge(['sort_name'], $list)); |
| 346 | |
| 347 | $contact = civicrm_api('contact', 'getsingle', ['id' => $val, 'return' => $return, 'version' => 3]); |
| 348 | |
| 349 | if (!empty($contact['id'])) { |
| 350 | $view = []; |
| 351 | foreach ($return as $fld) { |
| 352 | if (!empty($contact[$fld])) { |
| 353 | $view[] = $contact[$fld]; |
| 354 | } |
| 355 | } |
| 356 | $field->setAttribute('data-entity-value', json_encode([ |
| 357 | 'id' => $contact['id'], |
| 358 | 'text' => implode(' :: ', $view), |
| 359 | ])); |
| 360 | } |
| 361 | } |
| 362 | } |
| 363 | |
| 364 | /** |
| 365 | * @param array $el |
| 366 | * @param HTML_QuickForm_element $field |
| 367 | */ |
| 368 | public function addOptionsEditLink(&$el, $field) { |
| 369 | if (CRM_Core_Permission::check('administer CiviCRM')) { |
| 370 | // NOTE: $path is used on the client-side to know which option lists need rebuilding, |
| 371 | // that's why we need that bit of data both in the link and in the form element |
| 372 | $path = $field->getAttribute('data-option-edit-path'); |
| 373 | // NOTE: If we ever needed to support arguments in this link other than reset=1 we could split $path here if it contains a ? |
| 374 | $url = CRM_Utils_System::url($path, 'reset=1'); |
| 375 | $el['html'] .= ' <a href="' . $url . '" class="crm-option-edit-link medium-popup crm-hover-button" target="_blank" title="' . ts('Edit Options') . '" data-option-edit-path="' . $path . '"><i class="crm-i fa-wrench"></i></a>'; |
| 376 | } |
| 377 | } |
| 378 | |
| 379 | /** |
| 380 | * @param array $el |
| 381 | * @param HTML_QuickForm_element $field |
| 382 | */ |
| 383 | public function appendUnselectButton(&$el, $field) { |
| 384 | // Initially hide if not needed |
| 385 | // Note: visibility:hidden prevents layout jumping around unlike display:none |
| 386 | $display = $field->getValue() !== NULL ? '' : ' style="visibility:hidden;"'; |
| 387 | $el['html'] .= ' <a href="#" class="crm-hover-button crm-clear-link"' . $display . ' title="' . ts('Clear') . '"><i class="crm-i fa-times"></i></a>'; |
| 388 | } |
| 389 | |
| 390 | } |