/symphony/lib/toolkit/fields/field.taglist.php
PHP | 743 lines | 528 code | 127 blank | 88 comment | 76 complexity | 56bd33c1dbe9c81c51788e6183b0762d MD5 | raw file
- <?php
- /**
- * @package toolkit
- */
- /**
- * The Tag List field is really a different interface for the Select Box
- * field, offering a tag interface that can have static suggestions,
- * suggestions from another field or a dynamic list based on what an Author
- * has previously used for this field.
- */
- class FieldTagList extends Field implements ExportableField, ImportableField
- {
- public function __construct()
- {
- parent::__construct();
- $this->_name = __('Tag List');
- $this->_required = true;
- $this->_showassociation = true;
- $this->entryQueryFieldAdapter = new EntryQueryListAdapter($this);
- $this->set('required', 'no');
- }
- /*-------------------------------------------------------------------------
- Definition:
- -------------------------------------------------------------------------*/
- public function canFilter()
- {
- return true;
- }
- public function canPrePopulate()
- {
- return true;
- }
- public function requiresSQLGrouping()
- {
- return true;
- }
- public function allowDatasourceParamOutput()
- {
- return true;
- }
- public function fetchSuggestionTypes()
- {
- return array('association', 'static');
- }
- /*-------------------------------------------------------------------------
- Setup:
- -------------------------------------------------------------------------*/
- public function createTable()
- {
- return Symphony::Database()
- ->create('tbl_entries_data_' . General::intval($this->get('id')))
- ->ifNotExists()
- ->fields([
- 'id' => [
- 'type' => 'int(11)',
- 'auto' => true,
- ],
- 'entry_id' => 'int(11)',
- 'handle' => [
- 'type' => 'varchar(255)',
- 'null' => true,
- ],
- 'value' => [
- 'type' => 'varchar(255)',
- 'null' => true,
- ],
- ])
- ->keys([
- 'id' => 'primary',
- 'entry_id' => 'key',
- 'handle' => 'key',
- 'value' => 'key',
- ])
- ->execute()
- ->success();
- }
- /*-------------------------------------------------------------------------
- Utilities:
- -------------------------------------------------------------------------*/
- public function fetchAssociatedEntryCount($value)
- {
- $value = array_map('trim', array_map([$this, 'cleanValue'], explode(',', $value)));
- return Symphony::Database()
- ->select()
- ->count('handle')
- ->from('tbl_entries_data_' . $this->get('id'))
- ->where(['handle' => ['in' => $value]])
- ->execute()
- ->integer(0);
- }
- public function fetchAssociatedEntrySearchValue($data, $field_id = null, $parent_entry_id = null)
- {
- if (!is_array($data)) {
- return $data;
- }
- if (!is_array($data['handle'])) {
- $data['handle'] = array($data['handle']);
- $data['value'] = array($data['value']);
- }
- return implode(',', $data['handle']);
- }
- /**
- * Find all the entries that reference this entry's tags.
- *
- * @param integer $entry_id
- * @param integer $parent_field_id
- * @return array
- */
- public function findRelatedEntries($entry_id, $parent_field_id)
- {
- // We have the entry_id of the entry that has the referenced tag values
- // Lets find out what those handles are so we can then referenced the
- // child section looking for them.
- $handles = Symphony::Database()
- ->select(['handle'])
- ->from("tbl_entries_data_$parent_field_id")
- ->where(['entry_id' => $entry_id])
- ->execute()
- ->column('handle');
- if (empty($handles)) {
- return [];
- }
- $ids = Symphony::Database()
- ->select(['entry_id'])
- ->from('tbl_entries_data_' . $this->get('id'))
- ->where(['handle' => ['in' => $handles]])
- ->execute()
- ->column('entry_id');
- return $ids;
- }
- /**
- * Find all the entries that contain the tags that have been referenced
- * from this field own entry.
- *
- * @param integer $field_id
- * @param integer $entry_id
- * @return array
- */
- public function findParentRelatedEntries($field_id, $entry_id)
- {
- // Get all the `handles` that have been referenced from the
- // child association.
- $handles = Symphony::Database()
- ->select(['handle'])
- ->from('tbl_entries_data_' . $this->get('id'))
- ->where(['entry_id' => $entry_id])
- ->execute()
- ->column('handle');
- // Now find the associated entry ids for those `handles` in
- // the parent section.
- $ids = Symphony::Database()
- ->select(['entry_id'])
- ->from("tbl_entries_data_$field_id")
- ->where(['handle' => ['in' => $handles]])
- ->execute()
- ->column('entry_id');
- return $ids;
- }
- public function set($field, $value)
- {
- if ($field == 'pre_populate_source' && !is_array($value)) {
- $value = preg_split('/\s*,\s*/', $value, -1, PREG_SPLIT_NO_EMPTY);
- }
- $this->_settings[$field] = $value;
- }
- public function getToggleStates()
- {
- if (!is_array($this->get('pre_populate_source'))) {
- return;
- }
- $values = array();
- foreach ($this->get('pre_populate_source') as $item) {
- if ($item === 'none') {
- break;
- }
- $result = Symphony::Database()
- ->select(['value'])
- ->distinct()
- ->from('tbl_entries_data_' . ($item == 'existing' ? $this->get('id') : $item))
- ->orderBy(['value' => 'ASC'])
- ->execute()
- ->column('value');
- if (!is_array($result) || empty($result)) {
- continue;
- }
- $values = array_merge($values, $result);
- }
- return array_unique($values);
- }
- private static function __tagArrayToString(array $tags)
- {
- if (empty($tags)) {
- return null;
- }
- sort($tags);
- return implode(', ', $tags);
- }
- /*-------------------------------------------------------------------------
- Settings:
- -------------------------------------------------------------------------*/
- public function findDefaults(array &$settings)
- {
- if (!isset($settings['pre_populate_source'])) {
- $settings['pre_populate_source'] = array('existing');
- }
- if (!isset($settings['show_association'])) {
- $settings['show_association'] = 'no';
- }
- }
- public function displaySettingsPanel(XMLElement &$wrapper, $errors = null)
- {
- parent::displaySettingsPanel($wrapper, $errors);
- // Suggestions
- $label = Widget::Label(__('Suggestion List'));
- $sections = (new SectionManager)->select()->execute()->rows();
- $field_groups = array();
- foreach ($sections as $section) {
- $field_groups[$section->get('id')] = array('fields' => $section->fetchFields(), 'section' => $section);
- }
- $options = array(
- array('none', (in_array('none', $this->get('pre_populate_source'))), __('No Suggestions')),
- array('existing', (in_array('existing', $this->get('pre_populate_source'))), __('Existing Values')),
- );
- foreach ($field_groups as $group) {
- if (!is_array($group['fields'])) {
- continue;
- }
- $fields = array();
- foreach ($group['fields'] as $f) {
- if ($f->get('id') != $this->get('id') && $f->canPrePopulate()) {
- $fields[] = array($f->get('id'), (in_array($f->get('id'), $this->get('pre_populate_source'))), $f->get('label'));
- }
- }
- if (is_array($fields) && !empty($fields)) {
- $options[] = array('label' => $group['section']->get('name'), 'options' => $fields);
- }
- }
- $label->appendChild(Widget::Select('fields['.$this->get('sortorder').'][pre_populate_source][]', $options, array('multiple' => 'multiple')));
- $wrapper->appendChild($label);
- // Validation rule
- $this->buildValidationSelect($wrapper, $this->get('validator'), 'fields['.$this->get('sortorder').'][validator]', 'input', $errors);
- // Associations
- $fieldset = new XMLElement('fieldset');
- $this->appendAssociationInterfaceSelect($fieldset);
- $this->appendShowAssociationCheckbox($fieldset);
- $wrapper->appendChild($fieldset);
- // Requirements and table display
- $this->appendStatusFooter($wrapper);
- }
- public function commit()
- {
- if (!parent::commit()) {
- return false;
- }
- $id = $this->get('id');
- if ($id === false) {
- return false;
- }
- $fields = array();
- $fields['pre_populate_source'] = (is_null($this->get('pre_populate_source')) ? 'none' : implode(',', $this->get('pre_populate_source')));
- $fields['validator'] = ($fields['validator'] == 'custom' ? null : $this->get('validator'));
- if (!FieldManager::saveSettings($id, $fields)) {
- return false;
- }
- SectionManager::removeSectionAssociation($id);
- if (is_array($this->get('pre_populate_source'))) {
- foreach ($this->get('pre_populate_source') as $field_id) {
- if ($field_id === 'none' || $field_id === 'existing') {
- continue;
- }
- if (!is_null($field_id) && is_numeric($field_id)) {
- SectionManager::createSectionAssociation(null, $id, (int) $field_id, $this->get('show_association') === 'yes' ? true : false, $this->get('association_ui'), $this->get('association_editor'));
- }
- }
- }
- return true;
- }
- /*-------------------------------------------------------------------------
- Publish:
- -------------------------------------------------------------------------*/
- public function displayPublishPanel(XMLElement &$wrapper, $data = null, $flagWithError = null, $fieldnamePrefix = null, $fieldnamePostfix = null, $entry_id = null)
- {
- $value = null;
- if (isset($data['value'])) {
- $value = (is_array($data['value']) ? self::__tagArrayToString($data['value']) : $data['value']);
- }
- $label = Widget::Label($this->get('label'));
- if ($this->get('required') !== 'yes') {
- $label->appendChild(new XMLElement('i', __('Optional')));
- }
- $label->appendChild(
- Widget::Input('fields'.$fieldnamePrefix.'['.$this->get('element_name').']'.$fieldnamePostfix, (strlen($value) != 0 ? General::sanitize($value) : null))
- );
- if ($flagWithError != null) {
- $wrapper->appendChild(Widget::Error($label, $flagWithError));
- } else {
- $wrapper->appendChild($label);
- }
- if ($this->get('pre_populate_source') != null) {
- $existing_tags = $this->getToggleStates();
- if (is_array($existing_tags) && !empty($existing_tags)) {
- $taglist = new XMLElement('ul');
- $taglist->setAttribute('class', 'tags');
- $taglist->setAttribute('data-interactive', 'data-interactive');
- foreach ($existing_tags as $tag) {
- $taglist->appendChild(
- new XMLElement('li', General::sanitize($tag))
- );
- }
- $wrapper->appendChild($taglist);
- }
- }
- }
- private function parseUserSubmittedData($data)
- {
- if (!is_array($data)) {
- $data = preg_split('/\,\s*/i', $data, -1, PREG_SPLIT_NO_EMPTY);
- }
- return array_filter(array_map('trim', $data));
- }
- public function checkPostFieldData($data, &$message, $entry_id = null)
- {
- $message = null;
- if ($this->get('required') === 'yes' && strlen(trim($data)) == 0) {
- $message = __('‘%s’ is a required field.', array($this->get('label')));
- return self::__MISSING_FIELDS__;
- }
- if ($this->get('validator')) {
- $data = $this->parseUserSubmittedData($data);
- if (empty($data)) {
- return self::__OK__;
- }
- if (!General::validateString($data, $this->get('validator'))) {
- $message = __("'%s' contains invalid data. Please check the contents.", array($this->get('label')));
- return self::__INVALID_FIELDS__;
- }
- }
- return self::__OK__;
- }
- public function processRawFieldData($data, &$status, &$message = null, $simulate = false, $entry_id = null)
- {
- $status = self::__OK__;
- $data = $this->parseUserSubmittedData($data);
- if (empty($data)) {
- return null;
- }
- // Do a case insensitive removal of duplicates
- $data = General::array_remove_duplicates($data, true);
- sort($data);
- $result = array();
- foreach ($data as $value) {
- $result['value'][] = $value;
- $result['handle'][] = Lang::createHandle($value);
- }
- return $result;
- }
- /*-------------------------------------------------------------------------
- Output:
- -------------------------------------------------------------------------*/
- public function appendFormattedElement(XMLElement &$wrapper, $data, $encode = false, $mode = null, $entry_id = null)
- {
- if (!is_array($data) || empty($data) || is_null($data['value'])) {
- return;
- }
- $list = new XMLElement($this->get('element_name'));
- if (!is_array($data['handle']) && !is_array($data['value'])) {
- $data = array(
- 'handle' => array($data['handle']),
- 'value' => array($data['value'])
- );
- }
- foreach ($data['value'] as $index => $value) {
- $list->appendChild(new XMLElement('item', General::sanitize($value), array(
- 'handle' => $data['handle'][$index]
- )));
- }
- $wrapper->appendChild($list);
- }
- public function prepareTextValue($data, $entry_id = null)
- {
- if (!is_array($data) || empty($data)) {
- return '';
- }
- $value = '';
- if (isset($data['value'])) {
- $value = (is_array($data['value']) ? self::__tagArrayToString($data['value']) : $data['value']);
- }
- return General::sanitize($value);
- }
- public function getParameterPoolValue(array $data, $entry_id = null)
- {
- return $this->prepareExportValue($data, ExportableField::LIST_OF + ExportableField::HANDLE, $entry_id);
- }
- /*-------------------------------------------------------------------------
- Import:
- -------------------------------------------------------------------------*/
- public function getImportModes()
- {
- return array(
- 'getValue' => ImportableField::STRING_VALUE,
- 'getPostdata' => ImportableField::ARRAY_VALUE
- );
- }
- public function prepareImportValue($data, $mode, $entry_id = null)
- {
- $message = $status = null;
- $modes = (object)$this->getImportModes();
- if (is_array($data)) {
- $data = implode(', ', $data);
- }
- if ($mode === $modes->getValue) {
- return $data;
- } elseif ($mode === $modes->getPostdata) {
- return $this->processRawFieldData($data, $status, $message, true, $entry_id);
- }
- return null;
- }
- /*-------------------------------------------------------------------------
- Export:
- -------------------------------------------------------------------------*/
- /**
- * Return a list of supported export modes for use with `prepareExportValue`.
- *
- * @return array
- */
- public function getExportModes()
- {
- return array(
- 'listHandle' => ExportableField::LIST_OF
- + ExportableField::HANDLE,
- 'listValue' => ExportableField::LIST_OF
- + ExportableField::VALUE,
- 'listHandleToValue' => ExportableField::LIST_OF
- + ExportableField::HANDLE
- + ExportableField::VALUE,
- 'getPostdata' => ExportableField::POSTDATA
- );
- }
- /**
- * Give the field some data and ask it to return a value using one of many
- * possible modes.
- *
- * @param mixed $data
- * @param integer $mode
- * @param integer $entry_id
- * @return array|null
- */
- public function prepareExportValue($data, $mode, $entry_id = null)
- {
- $modes = (object)$this->getExportModes();
- if (isset($data['handle']) && is_array($data['handle']) === false) {
- $data['handle'] = array(
- $data['handle']
- );
- }
- if (isset($data['value']) && is_array($data['value']) === false) {
- $data['value'] = array(
- $data['value']
- );
- }
- // Handle => value pairs:
- if ($mode === $modes->listHandleToValue) {
- return isset($data['handle'], $data['value'])
- ? array_combine($data['handle'], $data['value'])
- : array();
- // Array of handles:
- } elseif ($mode === $modes->listHandle) {
- return isset($data['handle'])
- ? $data['handle']
- : array();
- // Array of values:
- } elseif ($mode === $modes->listValue) {
- return isset($data['value'])
- ? $data['value']
- : array();
- // Comma seperated values:
- } elseif ($mode === $modes->getPostdata) {
- return isset($data['value'])
- ? implode(', ', $data['value'])
- : null;
- }
- }
- /*-------------------------------------------------------------------------
- Filtering:
- -------------------------------------------------------------------------*/
- public function displayFilteringOptions(XMLElement &$wrapper)
- {
- if ($this->get('pre_populate_source') != null) {
- $existing_tags = $this->getToggleStates();
- if (is_array($existing_tags) && !empty($existing_tags)) {
- $taglist = new XMLElement('ul');
- $taglist->setAttribute('class', 'tags');
- $taglist->setAttribute('data-interactive', 'data-interactive');
- foreach ($existing_tags as $tag) {
- $taglist->appendChild(
- new XMLElement('li', General::sanitize($tag))
- );
- }
- $wrapper->appendChild($taglist);
- }
- }
- }
- public function fetchFilterableOperators()
- {
- return array(
- array(
- 'title' => 'is',
- 'filter' => ' ',
- 'help' => __('Find values that are an exact match for the given string.')
- ),
- array(
- 'filter' => 'sql: NOT NULL',
- 'title' => 'is not empty',
- 'help' => __('Find entries where any value is selected.')
- ),
- array(
- 'filter' => 'sql: NULL',
- 'title' => 'is empty',
- 'help' => __('Find entries where no value is selected.')
- ),
- array(
- 'filter' => 'sql-null-or-not: ',
- 'title' => 'is empty or not',
- 'help' => __('Find entries where no value is selected or it is not equal to this value.')
- ),
- array(
- 'filter' => 'not: ',
- 'title' => 'is not',
- 'help' => __('Find entries where the value is not equal to this value.')
- ),
- array(
- 'filter' => 'regexp: ',
- 'title' => 'contains',
- 'help' => __('Find entries where the value matches the regex.')
- ),
- array(
- 'filter' => 'not-regexp: ',
- 'title' => 'does not contain',
- 'help' => __('Find entries where the value does not match the regex.')
- )
- );
- }
- /**
- * @deprecated @since Symphony 3.0.0
- * @see Field::buildDSRetrievalSQL()
- */
- public function buildDSRetrievalSQL($data, &$joins, &$where, $andOperation = false)
- {
- if (Symphony::Log()) {
- Symphony::Log()->pushDeprecateWarningToLog(
- get_called_class() . '::buildDSRetrievalSQL()',
- 'EntryQueryFieldAdapter::filter()'
- );
- }
- $field_id = $this->get('id');
- if (self::isFilterRegex($data[0])) {
- $this->buildRegexSQL($data[0], array('value', 'handle'), $joins, $where);
- } elseif (self::isFilterSQL($data[0])) {
- $this->buildFilterSQL($data[0], array('value', 'handle'), $joins, $where);
- } else {
- $negation = false;
- $null = false;
- if (preg_match('/^not:/', $data[0])) {
- $data[0] = preg_replace('/^not:/', null, $data[0]);
- $negation = true;
- } elseif (preg_match('/^sql-null-or-not:/', $data[0])) {
- $data[0] = preg_replace('/^sql-null-or-not:/', null, $data[0]);
- $negation = true;
- $null = true;
- }
- foreach ($data as &$value) {
- $value = $this->cleanValue($value);
- }
- if ($andOperation) {
- $condition = ($negation) ? '!=' : '=';
- foreach ($data as $key => $bit) {
- $joins .= " LEFT JOIN `tbl_entries_data_$field_id` AS `t{$field_id}_{$this->_key}` ON (`e`.`id` = `t{$field_id}_{$this->_key}`.entry_id) ";
- $where .= " AND ((
- t{$field_id}_{$this->_key}.value $condition '$bit'
- OR t{$field_id}_{$this->_key}.handle $condition '$bit'
- )";
- if ($null) {
- $where .= " OR `t{$field_id}_{$this->_key}`.`value` IS NULL) ";
- } else {
- $where .= ") ";
- }
- $this->_key++;
- }
- } else {
- $data = "'".implode("', '", $data)."'";
- // Apply a different where condition if we are using $negation. RE: #29
- if ($negation) {
- $condition = 'NOT EXISTS';
- $where .= " AND $condition (
- SELECT *
- FROM `tbl_entries_data_$field_id` AS `t{$field_id}_{$this->_key}`
- WHERE `t{$field_id}_{$this->_key}`.entry_id = `e`.id AND (
- `t{$field_id}_{$this->_key}`.handle IN ($data) OR
- `t{$field_id}_{$this->_key}`.value IN ($data)
- )
- )";
- } else {
- // Normal filtering
- $joins .= " LEFT JOIN `tbl_entries_data_$field_id` AS `t{$field_id}_{$this->_key}` ON (`e`.`id` = `t{$field_id}_{$this->_key}`.entry_id) ";
- $where .= " AND (
- t{$field_id}_{$this->_key}.value IN ($data)
- OR t{$field_id}_{$this->_key}.handle IN ($data)
- ";
- // If we want entries with null values included in the result
- $where .= ($null) ? " OR `t{$field_id}_{$this->_key}`.`relation_id` IS NULL) " : ") ";
- }
- }
- }
- return true;
- }
- }