PageRenderTime 49ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/symphony/lib/toolkit/fields/field.taglist.php

http://github.com/symphonycms/symphony-2
PHP | 743 lines | 528 code | 127 blank | 88 comment | 76 complexity | 56bd33c1dbe9c81c51788e6183b0762d MD5 | raw file
  1. <?php
  2. /**
  3. * @package toolkit
  4. */
  5. /**
  6. * The Tag List field is really a different interface for the Select Box
  7. * field, offering a tag interface that can have static suggestions,
  8. * suggestions from another field or a dynamic list based on what an Author
  9. * has previously used for this field.
  10. */
  11. class FieldTagList extends Field implements ExportableField, ImportableField
  12. {
  13. public function __construct()
  14. {
  15. parent::__construct();
  16. $this->_name = __('Tag List');
  17. $this->_required = true;
  18. $this->_showassociation = true;
  19. $this->entryQueryFieldAdapter = new EntryQueryListAdapter($this);
  20. $this->set('required', 'no');
  21. }
  22. /*-------------------------------------------------------------------------
  23. Definition:
  24. -------------------------------------------------------------------------*/
  25. public function canFilter()
  26. {
  27. return true;
  28. }
  29. public function canPrePopulate()
  30. {
  31. return true;
  32. }
  33. public function requiresSQLGrouping()
  34. {
  35. return true;
  36. }
  37. public function allowDatasourceParamOutput()
  38. {
  39. return true;
  40. }
  41. public function fetchSuggestionTypes()
  42. {
  43. return array('association', 'static');
  44. }
  45. /*-------------------------------------------------------------------------
  46. Setup:
  47. -------------------------------------------------------------------------*/
  48. public function createTable()
  49. {
  50. return Symphony::Database()
  51. ->create('tbl_entries_data_' . General::intval($this->get('id')))
  52. ->ifNotExists()
  53. ->fields([
  54. 'id' => [
  55. 'type' => 'int(11)',
  56. 'auto' => true,
  57. ],
  58. 'entry_id' => 'int(11)',
  59. 'handle' => [
  60. 'type' => 'varchar(255)',
  61. 'null' => true,
  62. ],
  63. 'value' => [
  64. 'type' => 'varchar(255)',
  65. 'null' => true,
  66. ],
  67. ])
  68. ->keys([
  69. 'id' => 'primary',
  70. 'entry_id' => 'key',
  71. 'handle' => 'key',
  72. 'value' => 'key',
  73. ])
  74. ->execute()
  75. ->success();
  76. }
  77. /*-------------------------------------------------------------------------
  78. Utilities:
  79. -------------------------------------------------------------------------*/
  80. public function fetchAssociatedEntryCount($value)
  81. {
  82. $value = array_map('trim', array_map([$this, 'cleanValue'], explode(',', $value)));
  83. return Symphony::Database()
  84. ->select()
  85. ->count('handle')
  86. ->from('tbl_entries_data_' . $this->get('id'))
  87. ->where(['handle' => ['in' => $value]])
  88. ->execute()
  89. ->integer(0);
  90. }
  91. public function fetchAssociatedEntrySearchValue($data, $field_id = null, $parent_entry_id = null)
  92. {
  93. if (!is_array($data)) {
  94. return $data;
  95. }
  96. if (!is_array($data['handle'])) {
  97. $data['handle'] = array($data['handle']);
  98. $data['value'] = array($data['value']);
  99. }
  100. return implode(',', $data['handle']);
  101. }
  102. /**
  103. * Find all the entries that reference this entry's tags.
  104. *
  105. * @param integer $entry_id
  106. * @param integer $parent_field_id
  107. * @return array
  108. */
  109. public function findRelatedEntries($entry_id, $parent_field_id)
  110. {
  111. // We have the entry_id of the entry that has the referenced tag values
  112. // Lets find out what those handles are so we can then referenced the
  113. // child section looking for them.
  114. $handles = Symphony::Database()
  115. ->select(['handle'])
  116. ->from("tbl_entries_data_$parent_field_id")
  117. ->where(['entry_id' => $entry_id])
  118. ->execute()
  119. ->column('handle');
  120. if (empty($handles)) {
  121. return [];
  122. }
  123. $ids = Symphony::Database()
  124. ->select(['entry_id'])
  125. ->from('tbl_entries_data_' . $this->get('id'))
  126. ->where(['handle' => ['in' => $handles]])
  127. ->execute()
  128. ->column('entry_id');
  129. return $ids;
  130. }
  131. /**
  132. * Find all the entries that contain the tags that have been referenced
  133. * from this field own entry.
  134. *
  135. * @param integer $field_id
  136. * @param integer $entry_id
  137. * @return array
  138. */
  139. public function findParentRelatedEntries($field_id, $entry_id)
  140. {
  141. // Get all the `handles` that have been referenced from the
  142. // child association.
  143. $handles = Symphony::Database()
  144. ->select(['handle'])
  145. ->from('tbl_entries_data_' . $this->get('id'))
  146. ->where(['entry_id' => $entry_id])
  147. ->execute()
  148. ->column('handle');
  149. // Now find the associated entry ids for those `handles` in
  150. // the parent section.
  151. $ids = Symphony::Database()
  152. ->select(['entry_id'])
  153. ->from("tbl_entries_data_$field_id")
  154. ->where(['handle' => ['in' => $handles]])
  155. ->execute()
  156. ->column('entry_id');
  157. return $ids;
  158. }
  159. public function set($field, $value)
  160. {
  161. if ($field == 'pre_populate_source' && !is_array($value)) {
  162. $value = preg_split('/\s*,\s*/', $value, -1, PREG_SPLIT_NO_EMPTY);
  163. }
  164. $this->_settings[$field] = $value;
  165. }
  166. public function getToggleStates()
  167. {
  168. if (!is_array($this->get('pre_populate_source'))) {
  169. return;
  170. }
  171. $values = array();
  172. foreach ($this->get('pre_populate_source') as $item) {
  173. if ($item === 'none') {
  174. break;
  175. }
  176. $result = Symphony::Database()
  177. ->select(['value'])
  178. ->distinct()
  179. ->from('tbl_entries_data_' . ($item == 'existing' ? $this->get('id') : $item))
  180. ->orderBy(['value' => 'ASC'])
  181. ->execute()
  182. ->column('value');
  183. if (!is_array($result) || empty($result)) {
  184. continue;
  185. }
  186. $values = array_merge($values, $result);
  187. }
  188. return array_unique($values);
  189. }
  190. private static function __tagArrayToString(array $tags)
  191. {
  192. if (empty($tags)) {
  193. return null;
  194. }
  195. sort($tags);
  196. return implode(', ', $tags);
  197. }
  198. /*-------------------------------------------------------------------------
  199. Settings:
  200. -------------------------------------------------------------------------*/
  201. public function findDefaults(array &$settings)
  202. {
  203. if (!isset($settings['pre_populate_source'])) {
  204. $settings['pre_populate_source'] = array('existing');
  205. }
  206. if (!isset($settings['show_association'])) {
  207. $settings['show_association'] = 'no';
  208. }
  209. }
  210. public function displaySettingsPanel(XMLElement &$wrapper, $errors = null)
  211. {
  212. parent::displaySettingsPanel($wrapper, $errors);
  213. // Suggestions
  214. $label = Widget::Label(__('Suggestion List'));
  215. $sections = (new SectionManager)->select()->execute()->rows();
  216. $field_groups = array();
  217. foreach ($sections as $section) {
  218. $field_groups[$section->get('id')] = array('fields' => $section->fetchFields(), 'section' => $section);
  219. }
  220. $options = array(
  221. array('none', (in_array('none', $this->get('pre_populate_source'))), __('No Suggestions')),
  222. array('existing', (in_array('existing', $this->get('pre_populate_source'))), __('Existing Values')),
  223. );
  224. foreach ($field_groups as $group) {
  225. if (!is_array($group['fields'])) {
  226. continue;
  227. }
  228. $fields = array();
  229. foreach ($group['fields'] as $f) {
  230. if ($f->get('id') != $this->get('id') && $f->canPrePopulate()) {
  231. $fields[] = array($f->get('id'), (in_array($f->get('id'), $this->get('pre_populate_source'))), $f->get('label'));
  232. }
  233. }
  234. if (is_array($fields) && !empty($fields)) {
  235. $options[] = array('label' => $group['section']->get('name'), 'options' => $fields);
  236. }
  237. }
  238. $label->appendChild(Widget::Select('fields['.$this->get('sortorder').'][pre_populate_source][]', $options, array('multiple' => 'multiple')));
  239. $wrapper->appendChild($label);
  240. // Validation rule
  241. $this->buildValidationSelect($wrapper, $this->get('validator'), 'fields['.$this->get('sortorder').'][validator]', 'input', $errors);
  242. // Associations
  243. $fieldset = new XMLElement('fieldset');
  244. $this->appendAssociationInterfaceSelect($fieldset);
  245. $this->appendShowAssociationCheckbox($fieldset);
  246. $wrapper->appendChild($fieldset);
  247. // Requirements and table display
  248. $this->appendStatusFooter($wrapper);
  249. }
  250. public function commit()
  251. {
  252. if (!parent::commit()) {
  253. return false;
  254. }
  255. $id = $this->get('id');
  256. if ($id === false) {
  257. return false;
  258. }
  259. $fields = array();
  260. $fields['pre_populate_source'] = (is_null($this->get('pre_populate_source')) ? 'none' : implode(',', $this->get('pre_populate_source')));
  261. $fields['validator'] = ($fields['validator'] == 'custom' ? null : $this->get('validator'));
  262. if (!FieldManager::saveSettings($id, $fields)) {
  263. return false;
  264. }
  265. SectionManager::removeSectionAssociation($id);
  266. if (is_array($this->get('pre_populate_source'))) {
  267. foreach ($this->get('pre_populate_source') as $field_id) {
  268. if ($field_id === 'none' || $field_id === 'existing') {
  269. continue;
  270. }
  271. if (!is_null($field_id) && is_numeric($field_id)) {
  272. SectionManager::createSectionAssociation(null, $id, (int) $field_id, $this->get('show_association') === 'yes' ? true : false, $this->get('association_ui'), $this->get('association_editor'));
  273. }
  274. }
  275. }
  276. return true;
  277. }
  278. /*-------------------------------------------------------------------------
  279. Publish:
  280. -------------------------------------------------------------------------*/
  281. public function displayPublishPanel(XMLElement &$wrapper, $data = null, $flagWithError = null, $fieldnamePrefix = null, $fieldnamePostfix = null, $entry_id = null)
  282. {
  283. $value = null;
  284. if (isset($data['value'])) {
  285. $value = (is_array($data['value']) ? self::__tagArrayToString($data['value']) : $data['value']);
  286. }
  287. $label = Widget::Label($this->get('label'));
  288. if ($this->get('required') !== 'yes') {
  289. $label->appendChild(new XMLElement('i', __('Optional')));
  290. }
  291. $label->appendChild(
  292. Widget::Input('fields'.$fieldnamePrefix.'['.$this->get('element_name').']'.$fieldnamePostfix, (strlen($value) != 0 ? General::sanitize($value) : null))
  293. );
  294. if ($flagWithError != null) {
  295. $wrapper->appendChild(Widget::Error($label, $flagWithError));
  296. } else {
  297. $wrapper->appendChild($label);
  298. }
  299. if ($this->get('pre_populate_source') != null) {
  300. $existing_tags = $this->getToggleStates();
  301. if (is_array($existing_tags) && !empty($existing_tags)) {
  302. $taglist = new XMLElement('ul');
  303. $taglist->setAttribute('class', 'tags');
  304. $taglist->setAttribute('data-interactive', 'data-interactive');
  305. foreach ($existing_tags as $tag) {
  306. $taglist->appendChild(
  307. new XMLElement('li', General::sanitize($tag))
  308. );
  309. }
  310. $wrapper->appendChild($taglist);
  311. }
  312. }
  313. }
  314. private function parseUserSubmittedData($data)
  315. {
  316. if (!is_array($data)) {
  317. $data = preg_split('/\,\s*/i', $data, -1, PREG_SPLIT_NO_EMPTY);
  318. }
  319. return array_filter(array_map('trim', $data));
  320. }
  321. public function checkPostFieldData($data, &$message, $entry_id = null)
  322. {
  323. $message = null;
  324. if ($this->get('required') === 'yes' && strlen(trim($data)) == 0) {
  325. $message = __('‘%s’ is a required field.', array($this->get('label')));
  326. return self::__MISSING_FIELDS__;
  327. }
  328. if ($this->get('validator')) {
  329. $data = $this->parseUserSubmittedData($data);
  330. if (empty($data)) {
  331. return self::__OK__;
  332. }
  333. if (!General::validateString($data, $this->get('validator'))) {
  334. $message = __("'%s' contains invalid data. Please check the contents.", array($this->get('label')));
  335. return self::__INVALID_FIELDS__;
  336. }
  337. }
  338. return self::__OK__;
  339. }
  340. public function processRawFieldData($data, &$status, &$message = null, $simulate = false, $entry_id = null)
  341. {
  342. $status = self::__OK__;
  343. $data = $this->parseUserSubmittedData($data);
  344. if (empty($data)) {
  345. return null;
  346. }
  347. // Do a case insensitive removal of duplicates
  348. $data = General::array_remove_duplicates($data, true);
  349. sort($data);
  350. $result = array();
  351. foreach ($data as $value) {
  352. $result['value'][] = $value;
  353. $result['handle'][] = Lang::createHandle($value);
  354. }
  355. return $result;
  356. }
  357. /*-------------------------------------------------------------------------
  358. Output:
  359. -------------------------------------------------------------------------*/
  360. public function appendFormattedElement(XMLElement &$wrapper, $data, $encode = false, $mode = null, $entry_id = null)
  361. {
  362. if (!is_array($data) || empty($data) || is_null($data['value'])) {
  363. return;
  364. }
  365. $list = new XMLElement($this->get('element_name'));
  366. if (!is_array($data['handle']) && !is_array($data['value'])) {
  367. $data = array(
  368. 'handle' => array($data['handle']),
  369. 'value' => array($data['value'])
  370. );
  371. }
  372. foreach ($data['value'] as $index => $value) {
  373. $list->appendChild(new XMLElement('item', General::sanitize($value), array(
  374. 'handle' => $data['handle'][$index]
  375. )));
  376. }
  377. $wrapper->appendChild($list);
  378. }
  379. public function prepareTextValue($data, $entry_id = null)
  380. {
  381. if (!is_array($data) || empty($data)) {
  382. return '';
  383. }
  384. $value = '';
  385. if (isset($data['value'])) {
  386. $value = (is_array($data['value']) ? self::__tagArrayToString($data['value']) : $data['value']);
  387. }
  388. return General::sanitize($value);
  389. }
  390. public function getParameterPoolValue(array $data, $entry_id = null)
  391. {
  392. return $this->prepareExportValue($data, ExportableField::LIST_OF + ExportableField::HANDLE, $entry_id);
  393. }
  394. /*-------------------------------------------------------------------------
  395. Import:
  396. -------------------------------------------------------------------------*/
  397. public function getImportModes()
  398. {
  399. return array(
  400. 'getValue' => ImportableField::STRING_VALUE,
  401. 'getPostdata' => ImportableField::ARRAY_VALUE
  402. );
  403. }
  404. public function prepareImportValue($data, $mode, $entry_id = null)
  405. {
  406. $message = $status = null;
  407. $modes = (object)$this->getImportModes();
  408. if (is_array($data)) {
  409. $data = implode(', ', $data);
  410. }
  411. if ($mode === $modes->getValue) {
  412. return $data;
  413. } elseif ($mode === $modes->getPostdata) {
  414. return $this->processRawFieldData($data, $status, $message, true, $entry_id);
  415. }
  416. return null;
  417. }
  418. /*-------------------------------------------------------------------------
  419. Export:
  420. -------------------------------------------------------------------------*/
  421. /**
  422. * Return a list of supported export modes for use with `prepareExportValue`.
  423. *
  424. * @return array
  425. */
  426. public function getExportModes()
  427. {
  428. return array(
  429. 'listHandle' => ExportableField::LIST_OF
  430. + ExportableField::HANDLE,
  431. 'listValue' => ExportableField::LIST_OF
  432. + ExportableField::VALUE,
  433. 'listHandleToValue' => ExportableField::LIST_OF
  434. + ExportableField::HANDLE
  435. + ExportableField::VALUE,
  436. 'getPostdata' => ExportableField::POSTDATA
  437. );
  438. }
  439. /**
  440. * Give the field some data and ask it to return a value using one of many
  441. * possible modes.
  442. *
  443. * @param mixed $data
  444. * @param integer $mode
  445. * @param integer $entry_id
  446. * @return array|null
  447. */
  448. public function prepareExportValue($data, $mode, $entry_id = null)
  449. {
  450. $modes = (object)$this->getExportModes();
  451. if (isset($data['handle']) && is_array($data['handle']) === false) {
  452. $data['handle'] = array(
  453. $data['handle']
  454. );
  455. }
  456. if (isset($data['value']) && is_array($data['value']) === false) {
  457. $data['value'] = array(
  458. $data['value']
  459. );
  460. }
  461. // Handle => value pairs:
  462. if ($mode === $modes->listHandleToValue) {
  463. return isset($data['handle'], $data['value'])
  464. ? array_combine($data['handle'], $data['value'])
  465. : array();
  466. // Array of handles:
  467. } elseif ($mode === $modes->listHandle) {
  468. return isset($data['handle'])
  469. ? $data['handle']
  470. : array();
  471. // Array of values:
  472. } elseif ($mode === $modes->listValue) {
  473. return isset($data['value'])
  474. ? $data['value']
  475. : array();
  476. // Comma seperated values:
  477. } elseif ($mode === $modes->getPostdata) {
  478. return isset($data['value'])
  479. ? implode(', ', $data['value'])
  480. : null;
  481. }
  482. }
  483. /*-------------------------------------------------------------------------
  484. Filtering:
  485. -------------------------------------------------------------------------*/
  486. public function displayFilteringOptions(XMLElement &$wrapper)
  487. {
  488. if ($this->get('pre_populate_source') != null) {
  489. $existing_tags = $this->getToggleStates();
  490. if (is_array($existing_tags) && !empty($existing_tags)) {
  491. $taglist = new XMLElement('ul');
  492. $taglist->setAttribute('class', 'tags');
  493. $taglist->setAttribute('data-interactive', 'data-interactive');
  494. foreach ($existing_tags as $tag) {
  495. $taglist->appendChild(
  496. new XMLElement('li', General::sanitize($tag))
  497. );
  498. }
  499. $wrapper->appendChild($taglist);
  500. }
  501. }
  502. }
  503. public function fetchFilterableOperators()
  504. {
  505. return array(
  506. array(
  507. 'title' => 'is',
  508. 'filter' => ' ',
  509. 'help' => __('Find values that are an exact match for the given string.')
  510. ),
  511. array(
  512. 'filter' => 'sql: NOT NULL',
  513. 'title' => 'is not empty',
  514. 'help' => __('Find entries where any value is selected.')
  515. ),
  516. array(
  517. 'filter' => 'sql: NULL',
  518. 'title' => 'is empty',
  519. 'help' => __('Find entries where no value is selected.')
  520. ),
  521. array(
  522. 'filter' => 'sql-null-or-not: ',
  523. 'title' => 'is empty or not',
  524. 'help' => __('Find entries where no value is selected or it is not equal to this value.')
  525. ),
  526. array(
  527. 'filter' => 'not: ',
  528. 'title' => 'is not',
  529. 'help' => __('Find entries where the value is not equal to this value.')
  530. ),
  531. array(
  532. 'filter' => 'regexp: ',
  533. 'title' => 'contains',
  534. 'help' => __('Find entries where the value matches the regex.')
  535. ),
  536. array(
  537. 'filter' => 'not-regexp: ',
  538. 'title' => 'does not contain',
  539. 'help' => __('Find entries where the value does not match the regex.')
  540. )
  541. );
  542. }
  543. /**
  544. * @deprecated @since Symphony 3.0.0
  545. * @see Field::buildDSRetrievalSQL()
  546. */
  547. public function buildDSRetrievalSQL($data, &$joins, &$where, $andOperation = false)
  548. {
  549. if (Symphony::Log()) {
  550. Symphony::Log()->pushDeprecateWarningToLog(
  551. get_called_class() . '::buildDSRetrievalSQL()',
  552. 'EntryQueryFieldAdapter::filter()'
  553. );
  554. }
  555. $field_id = $this->get('id');
  556. if (self::isFilterRegex($data[0])) {
  557. $this->buildRegexSQL($data[0], array('value', 'handle'), $joins, $where);
  558. } elseif (self::isFilterSQL($data[0])) {
  559. $this->buildFilterSQL($data[0], array('value', 'handle'), $joins, $where);
  560. } else {
  561. $negation = false;
  562. $null = false;
  563. if (preg_match('/^not:/', $data[0])) {
  564. $data[0] = preg_replace('/^not:/', null, $data[0]);
  565. $negation = true;
  566. } elseif (preg_match('/^sql-null-or-not:/', $data[0])) {
  567. $data[0] = preg_replace('/^sql-null-or-not:/', null, $data[0]);
  568. $negation = true;
  569. $null = true;
  570. }
  571. foreach ($data as &$value) {
  572. $value = $this->cleanValue($value);
  573. }
  574. if ($andOperation) {
  575. $condition = ($negation) ? '!=' : '=';
  576. foreach ($data as $key => $bit) {
  577. $joins .= " LEFT JOIN `tbl_entries_data_$field_id` AS `t{$field_id}_{$this->_key}` ON (`e`.`id` = `t{$field_id}_{$this->_key}`.entry_id) ";
  578. $where .= " AND ((
  579. t{$field_id}_{$this->_key}.value $condition '$bit'
  580. OR t{$field_id}_{$this->_key}.handle $condition '$bit'
  581. )";
  582. if ($null) {
  583. $where .= " OR `t{$field_id}_{$this->_key}`.`value` IS NULL) ";
  584. } else {
  585. $where .= ") ";
  586. }
  587. $this->_key++;
  588. }
  589. } else {
  590. $data = "'".implode("', '", $data)."'";
  591. // Apply a different where condition if we are using $negation. RE: #29
  592. if ($negation) {
  593. $condition = 'NOT EXISTS';
  594. $where .= " AND $condition (
  595. SELECT *
  596. FROM `tbl_entries_data_$field_id` AS `t{$field_id}_{$this->_key}`
  597. WHERE `t{$field_id}_{$this->_key}`.entry_id = `e`.id AND (
  598. `t{$field_id}_{$this->_key}`.handle IN ($data) OR
  599. `t{$field_id}_{$this->_key}`.value IN ($data)
  600. )
  601. )";
  602. } else {
  603. // Normal filtering
  604. $joins .= " LEFT JOIN `tbl_entries_data_$field_id` AS `t{$field_id}_{$this->_key}` ON (`e`.`id` = `t{$field_id}_{$this->_key}`.entry_id) ";
  605. $where .= " AND (
  606. t{$field_id}_{$this->_key}.value IN ($data)
  607. OR t{$field_id}_{$this->_key}.handle IN ($data)
  608. ";
  609. // If we want entries with null values included in the result
  610. $where .= ($null) ? " OR `t{$field_id}_{$this->_key}`.`relation_id` IS NULL) " : ") ";
  611. }
  612. }
  613. }
  614. return true;
  615. }
  616. }