symphonycms/symphony-2

View on GitHub
symphony/lib/toolkit/fields/field.taglist.php

Summary

Maintainability
F
5 days
Test Coverage
<?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;
    }
}