PageRenderTime 57ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 1ms

/library/XenForo/Model/StyleProperty.php

https://github.com/hanguyenhuu/DTUI_201105
PHP | 3183 lines | 2380 code | 287 blank | 516 comment | 295 complexity | a0b35b7e7a56d16fa984fa521a9ea90f MD5 | raw file
Possible License(s): LGPL-2.1, GPL-2.0, BSD-3-Clause
  1. <?php
  2. /**
  3. * Model for style properties and style property definitions.
  4. * Note that throughout, style ID -1 is the ACP master.
  5. *
  6. * @package XenForo_StyleProperty
  7. */
  8. class XenForo_Model_StyleProperty extends XenForo_Model
  9. {
  10. protected static $_tempProperties = null;
  11. /**
  12. * Gets the specified style property by its ID. Includes
  13. * definition info.
  14. *
  15. * @param integer $id
  16. *
  17. * @return array|false
  18. */
  19. public function getStylePropertyById($id)
  20. {
  21. return $this->_getDb()->fetchRow('
  22. SELECT property_definition.*,
  23. style_property.*
  24. FROM xf_style_property AS style_property
  25. INNER JOIN xf_style_property_definition AS property_definition ON
  26. (property_definition.property_definition_id = style_property.property_definition_id)
  27. WHERE style_property.property_id = ?
  28. ', $id);
  29. }
  30. /**
  31. * Gets the specified style property by its definition ID
  32. * and style ID. Includes definition info.
  33. *
  34. * @param integer $definitionId
  35. * @param integer $styleId
  36. *
  37. * @return array|false
  38. */
  39. public function getStylePropertyByDefinitionAndStyle($definitionId, $styleId)
  40. {
  41. return $this->_getDb()->fetchRow('
  42. SELECT property_definition.*,
  43. style_property.*
  44. FROM xf_style_property AS style_property
  45. INNER JOIN xf_style_property_definition AS property_definition ON
  46. (property_definition.property_definition_id = style_property.property_definition_id)
  47. WHERE style_property.property_definition_id = ?
  48. AND style_property.style_id = ?
  49. ', array($definitionId, $styleId));
  50. }
  51. /**
  52. * Gets all style properties in the specified styles. This only
  53. * includes properties that have been customized or initially defined
  54. * in the specified styles. Includes definition info.
  55. *
  56. * @param array $styleIds
  57. *
  58. * @return array Format: [property id] => info
  59. */
  60. public function getStylePropertiesInStyles(array $styleIds)
  61. {
  62. if (!$styleIds)
  63. {
  64. return array();
  65. }
  66. return $this->fetchAllKeyed('
  67. SELECT property_definition.*,
  68. style_property.*
  69. FROM xf_style_property AS style_property
  70. INNER JOIN xf_style_property_definition AS property_definition ON
  71. (property_definition.property_definition_id = style_property.property_definition_id)
  72. WHERE style_property.style_id IN (' . $this->_getDb()->quote($styleIds) . ')
  73. ORDER BY property_definition.display_order
  74. ', 'property_id');
  75. }
  76. /**
  77. * Gets all style properties in a style with the specified definition IDs
  78. * that have been customized or defined directly in the style.
  79. *
  80. * @param integer $styleId
  81. * @param array $definitionIds
  82. *
  83. * @return array Format: [definition id] => info
  84. */
  85. public function getStylePropertiesInStyleByDefinitions($styleId, array $definitionIds)
  86. {
  87. if (!$definitionIds)
  88. {
  89. return array();
  90. }
  91. return $this->fetchAllKeyed('
  92. SELECT property_definition.*,
  93. style_property.*
  94. FROM xf_style_property AS style_property
  95. INNER JOIN xf_style_property_definition AS property_definition ON
  96. (property_definition.property_definition_id = style_property.property_definition_id)
  97. WHERE style_property.style_id = ?
  98. AND style_property.definition_id IN (' . $this->_getDb()->quote($definitionIds) . ')
  99. ORDER BY property_definition.display_order
  100. ', 'property_definition_id', $styleId);
  101. }
  102. /**
  103. * Gets the effective style properties in a style. This includes properties
  104. * that have been customized/created in a parent style.
  105. *
  106. * Includes effectiveState key in each property (customized, inherited, default).
  107. *
  108. * @param integer $styleId
  109. * @param array|null $path Path from style to root (earlier positions closer to style); if null, determined automatically
  110. * @param array|null $properties List of properties in this style and all parent styles; if null, determined automatically
  111. *
  112. * @return array Format: [definition id] => info
  113. */
  114. public function getEffectiveStylePropertiesInStyle($styleId, array $path = null, array $properties = null)
  115. {
  116. if ($path === null)
  117. {
  118. $path = $this->getParentPathFromStyle($styleId);
  119. }
  120. if ($properties === null)
  121. {
  122. $properties = $this->getStylePropertiesInStyles($path);
  123. }
  124. $effective = array();
  125. $propertyPriorities = array();
  126. foreach ($properties AS $property)
  127. {
  128. $definitionId = $property['property_definition_id'];
  129. $propertyPriority = array_search($property['style_id'], $path);
  130. if (!isset($propertyPriorities[$definitionId]) || $propertyPriority < $propertyPriorities[$definitionId])
  131. {
  132. switch ($property['style_id'])
  133. {
  134. case $property['definition_style_id']:
  135. $property['effectiveState'] = 'default';
  136. break;
  137. case $styleId:
  138. $property['effectiveState'] = 'customized';
  139. break;
  140. default:
  141. $property['effectiveState'] = 'inherited';
  142. break;
  143. }
  144. $effective[$definitionId] = $property;
  145. $propertyPriorities[$definitionId] = $propertyPriority;
  146. }
  147. }
  148. return $effective;
  149. }
  150. /**
  151. * Gets the effective properties and groups in a style. Properties are organized
  152. * within the groups (in properties key).
  153. *
  154. * @param integer $styleId
  155. * @param array|null $path Path from style to root (earlier positions closer to style); if null, determined automatically
  156. * @param array|null $properties List of properties in this style and all parent styles; if null, determined automatically
  157. *
  158. * @return array Format: [group name] => group info, with [properties][definition id] => property info
  159. */
  160. public function getEffectiveStylePropertiesByGroup($styleId, array $path = null, array $properties = null)
  161. {
  162. if ($path === null)
  163. {
  164. $path = $this->getParentPathFromStyle($styleId);
  165. }
  166. if ($properties === null)
  167. {
  168. $properties = $this->getEffectiveStylePropertiesInStyle($styleId, $path, $properties);
  169. }
  170. $groups = $this->getEffectiveStylePropertyGroupsInStyle($styleId, $path);
  171. $invalidGroupings = array();
  172. foreach ($properties AS $definitionId => $property)
  173. {
  174. if (isset($groups[$property['group_name']]))
  175. {
  176. $groups[$property['group_name']]['properties'][$definitionId] = $property;
  177. }
  178. else
  179. {
  180. $invalidGroupings[$definitionId] = $property;
  181. }
  182. }
  183. if ($invalidGroupings)
  184. {
  185. $groups[''] = array(
  186. 'property_group_id' => 0,
  187. 'group_name' => '',
  188. 'group_style_id' => $styleId,
  189. 'title' => '(ungrouped)',
  190. 'description' => '',
  191. 'addon_id' => '',
  192. 'properties' => $invalidGroupings
  193. );
  194. }
  195. return $groups;
  196. }
  197. /**
  198. * Fetches all color palette properties for the specified style.
  199. *
  200. * @param integer $styleId
  201. * @param array|null $path Path from style to root (earlier positions closer to style); if null, determined automatically
  202. * @param array|null $properties List of properties in this style and all parent styles; if null, determined automatically
  203. *
  204. * @return array
  205. */
  206. public function getColorPalettePropertiesInStyle($styleId, array $path = null, array $properties = null)
  207. {
  208. $groups = $this->getEffectiveStylePropertiesByGroup($styleId, $path, $properties);
  209. return $groups['color']['properties'];
  210. }
  211. /**
  212. * Reorganizes a property list to key properties by name. This is only safe
  213. * to do when getting properties (effective or not) for a single style.
  214. *
  215. * @param array $properties
  216. *
  217. * @return array
  218. */
  219. public function keyPropertiesByName(array $properties)
  220. {
  221. $output = array();
  222. foreach ($properties AS $property)
  223. {
  224. $output[$property['property_name']] = $property;
  225. }
  226. return $output;
  227. }
  228. /**
  229. * Filters a list of properties into 2 groups: scalar properties and css properties.
  230. *
  231. * @param array [scalar props, css props]
  232. */
  233. public function filterPropertiesByType(array $properties)
  234. {
  235. $scalar = array();
  236. $css = array();
  237. foreach ($properties AS $key => $property)
  238. {
  239. if ($property['property_type'] == 'scalar')
  240. {
  241. $scalar[$key] = $property;
  242. }
  243. else
  244. {
  245. $css[$key] = $property;
  246. }
  247. }
  248. return array($scalar, $css);
  249. }
  250. /**
  251. * Gets the specified style property group.
  252. *
  253. * @param integer $groupId
  254. *
  255. * @return array|false
  256. */
  257. public function getStylePropertyGroupById($groupId)
  258. {
  259. return $this->_getDb()->fetchRow('
  260. SELECT *
  261. FROM xf_style_property_group
  262. WHERE property_group_id = ?
  263. ', $groupId);
  264. }
  265. /**
  266. * Gets all style property groups defined in the specified styles.
  267. *
  268. * @param array $styleIds
  269. *
  270. * @return array Format: [property group id] => info
  271. */
  272. public function getStylePropertyGroupsInStyles(array $styleIds)
  273. {
  274. if (!$styleIds)
  275. {
  276. return array();
  277. }
  278. return $this->fetchAllKeyed('
  279. SELECT *
  280. FROM xf_style_property_group
  281. WHERE group_style_id IN (' . $this->_getDb()->quote($styleIds) . ')
  282. ORDER BY display_order
  283. ', 'property_group_id');
  284. }
  285. /**
  286. * Gets the effective list of groups that apply to a style.
  287. *
  288. * @param integer $styleId
  289. * @param array|null $path Path from style to root (earlier positions closer to style); if null, determined automatically
  290. *
  291. * @return array Format: [group name] => info
  292. */
  293. public function getEffectiveStylePropertyGroupsInStyle($styleId, array $path = null)
  294. {
  295. if ($path === null)
  296. {
  297. $path = $this->getParentPathFromStyle($styleId);
  298. }
  299. $groups = $this->getStylePropertyGroupsInStyles($path);
  300. $output = array();
  301. foreach ($groups AS $group)
  302. {
  303. $output[$group['group_name']] = $group;
  304. }
  305. return $output;
  306. }
  307. /**
  308. * Gets name-value pairs of style property groups in a style.
  309. *
  310. * @param integer $styleId
  311. *
  312. * @return array Format: [name] => title
  313. */
  314. public function getStylePropertyGroupOptions($styleId)
  315. {
  316. $groups = $this->prepareStylePropertyGroups($this->getEffectiveStylePropertyGroupsInStyle($styleId));
  317. $output = array();
  318. foreach ($groups AS $group)
  319. {
  320. $output[$group['group_name']] = $group['title'];
  321. }
  322. return $output;
  323. }
  324. /**
  325. * Prepares a style property group for display. If properties are found
  326. * within, they will be automatically prepared.
  327. *
  328. * @param array $property
  329. * @param integer|null $displayStyleId The ID of the style the groups/properties are being edited in
  330. *
  331. * @return array Prepared version
  332. */
  333. public function prepareStylePropertyGroup(array $group, $displayStyleId = null)
  334. {
  335. $group['masterTitle'] = $group['title'];
  336. $group['masterDescription'] = $group['description'];
  337. if ($group['addon_id'])
  338. {
  339. $group['title'] = new XenForo_Phrase($this->getStylePropertyGroupTitlePhraseName($group));
  340. $group['description'] = new XenForo_Phrase($this->getStylePropertyGroupDescriptionPhraseName($group));
  341. }
  342. if (!$group['group_name'])
  343. {
  344. $group['canEdit'] = false;
  345. }
  346. else if ($displayStyleId === null)
  347. {
  348. $group['canEdit'] = $this->canEditStylePropertyDefinition($group['group_style_id']);
  349. }
  350. else
  351. {
  352. $group['canEdit'] = (
  353. $this->canEditStylePropertyDefinition($group['group_style_id'])
  354. && $group['group_style_id'] == $displayStyleId
  355. );
  356. }
  357. if (!empty($group['properties']))
  358. {
  359. $group['properties'] = $this->prepareStyleProperties($group['properties'], $displayStyleId);
  360. }
  361. return $group;
  362. }
  363. /**
  364. * Prepares a list of style property groups. If properties are found within,
  365. * they will be automatically prepared.
  366. *
  367. * @param array $groups
  368. * @param integer|null $displayStyleId The ID of the style the groups/properties are being edited in
  369. *
  370. * @return array
  371. */
  372. public function prepareStylePropertyGroups(array $groups, $displayStyleId = null)
  373. {
  374. foreach ($groups AS &$group)
  375. {
  376. $group = $this->prepareStylePropertyGroup($group, $displayStyleId);
  377. }
  378. return $groups;
  379. }
  380. /**
  381. * Gets the default style property group record.
  382. *
  383. * @param integer $styleId
  384. *
  385. * @return array
  386. */
  387. public function getDefaultStylePropertyGroup($styleId)
  388. {
  389. return array(
  390. 'group_name' => '',
  391. 'group_style_id' => $styleId,
  392. 'title' => '',
  393. 'display_order' => 1,
  394. 'sub_group' => '',
  395. 'addon_id' => null,
  396. 'masterTitle' => '',
  397. 'masterDescription' => '',
  398. );
  399. }
  400. /**
  401. * Gets the name of the style property group title phrase.
  402. *
  403. * @param array $group
  404. *
  405. * @return string
  406. */
  407. public function getStylePropertyGroupTitlePhraseName(array $group)
  408. {
  409. switch ($group['group_style_id'])
  410. {
  411. case -1: $suffix = 'admin'; break;
  412. case 0: $suffix = 'master'; break;
  413. default: return '';
  414. }
  415. return "style_property_group_$group[group_name]_$suffix";
  416. }
  417. /**
  418. * Gets the name of the style property group description phrase.
  419. *
  420. * @param array $group
  421. *
  422. * @return string
  423. */
  424. public function getStylePropertyGroupDescriptionPhraseName(array $group)
  425. {
  426. switch ($group['group_style_id'])
  427. {
  428. case -1: $suffix = 'admin'; break;
  429. case 0: $suffix = 'master'; break;
  430. default: return '';
  431. }
  432. return "style_property_group_$group[group_name]_{$suffix}_desc";
  433. }
  434. /**
  435. * Gets the parent path from the specified style. For real styles,
  436. * this is the parent list. However, this function can handle styles
  437. * 0 (master) and -1 (ACP).
  438. *
  439. * @param $styleId
  440. * @return array Parent list; earlier positions are more specific
  441. */
  442. public function getParentPathFromStyle($styleId)
  443. {
  444. switch (intval($styleId))
  445. {
  446. case 0: return array(0);
  447. case -1: return array(-1, 0);
  448. default:
  449. $style = $this->_getStyleModel()->getStyleById($styleId);
  450. if ($style)
  451. {
  452. return explode(',', $style['parent_list']);
  453. }
  454. else
  455. {
  456. return array();
  457. }
  458. }
  459. }
  460. /**
  461. * Gets style info in the style property-specific way.
  462. *
  463. * @param integer $styleId
  464. *
  465. * @return array|false
  466. */
  467. public function getStyle($styleId)
  468. {
  469. if ($styleId >= 0)
  470. {
  471. return $this->getModelFromCache('XenForo_Model_Style')->getStyleById($styleId, true);
  472. }
  473. else
  474. {
  475. return array(
  476. 'style_id' => -1,
  477. 'parent_list' => '-1,0',
  478. 'title' => new XenForo_Phrase('admin_control_panel')
  479. );
  480. }
  481. }
  482. /**
  483. * Group a list of style properties by the style they belong to.
  484. * This uses the customization style (not definition style) for grouping.
  485. *
  486. * @param array $properties
  487. *
  488. * @return array Format: [style id][definition id] => info
  489. */
  490. public function groupStylePropertiesByStyle(array $properties)
  491. {
  492. $newProperties = array();
  493. foreach ($properties AS $property)
  494. {
  495. $newProperties[$property['style_id']][$property['property_definition_id']] = $property;
  496. }
  497. return $newProperties;
  498. }
  499. /**
  500. * Rebuilds the property cache for all styles.
  501. */
  502. public function rebuildPropertyCacheForAllStyles()
  503. {
  504. $this->rebuildPropertyCacheInStyleAndChildren(0);
  505. }
  506. /**
  507. * Rebuild the style property cache in the specified style and all
  508. * child/dependent styles.
  509. *
  510. * @param integer $styleId
  511. *
  512. * @return array The property cache for the requested style
  513. */
  514. public function rebuildPropertyCacheInStyleAndChildren($styleId)
  515. {
  516. if ($styleId == -1)
  517. {
  518. $rebuildStyleIds = array(-1);
  519. $dataStyleIds = array(-1, 0);
  520. $styles = array();
  521. }
  522. else
  523. {
  524. $styleModel = $this->_getStyleModel();
  525. $styles = $styleModel->getAllStyles();
  526. $styleTree = $styleModel->getStyleTreeAssociations($styles);
  527. $rebuildStyleIds = $styleModel->getAllChildStyleIdsFromTree($styleId, $styleTree);
  528. $rebuildStyleIds[] = $styleId;
  529. $dataStyleIds = array_keys($styles);
  530. $dataStyleIds[] = 0;
  531. if ($styleId == 0)
  532. {
  533. $rebuildStyleIds[] = -1;
  534. $dataStyleIds[] = -1;
  535. }
  536. }
  537. $properties = $this->groupStylePropertiesByStyle(
  538. $this->getStylePropertiesInStyles($dataStyleIds)
  539. );
  540. $styleOutput = false;
  541. foreach ($rebuildStyleIds AS $rebuildStyleId)
  542. {
  543. $sourceStyle = (isset($styles[$rebuildStyleId]) ? $styles[$rebuildStyleId] : array());
  544. switch ($rebuildStyleId)
  545. {
  546. case 0:
  547. continue 2;
  548. case -1:
  549. $sourceStyleIds = array(-1, 0);
  550. break;
  551. default:
  552. $sourceStyleIds = explode(',', $sourceStyle['parent_list']);
  553. }
  554. $styleProperties = array();
  555. foreach ($sourceStyleIds AS $sourceStyleId)
  556. {
  557. if (isset($properties[$sourceStyleId]))
  558. {
  559. $styleProperties = array_merge($styleProperties, $properties[$sourceStyleId]);
  560. }
  561. }
  562. $effectiveProperties = $this->getEffectiveStylePropertiesInStyle(
  563. $rebuildStyleId, $sourceStyleIds, $styleProperties
  564. );
  565. $cacheOutput = $this->updatePropertyCacheInStyle($rebuildStyleId, $effectiveProperties, $sourceStyle);
  566. if ($rebuildStyleId == $styleId)
  567. {
  568. $styleOutput = $cacheOutput;
  569. }
  570. }
  571. return $styleOutput;
  572. }
  573. /**
  574. * Updates the property cache in the specified style.
  575. *
  576. * @param integer $styleId
  577. * @param array $effectiveProperties List of effective properties in style.
  578. * @param array|null $style Style information; queried if needed
  579. *
  580. * @return array|false Compiled property cache
  581. */
  582. public function updatePropertyCacheInStyle($styleId, array $effectiveProperties, array $style = null)
  583. {
  584. if ($styleId == 0)
  585. {
  586. return false;
  587. }
  588. $propertyCache = array();
  589. foreach ($effectiveProperties AS $property)
  590. {
  591. if ($property['property_type'] == 'scalar')
  592. {
  593. $propertyCache[$property['property_name']] = $property['property_value'];
  594. }
  595. else
  596. {
  597. $propertyCache[$property['property_name']] = unserialize($property['property_value']);
  598. }
  599. }
  600. foreach ($propertyCache AS &$propertyValue)
  601. {
  602. if (is_array($propertyValue))
  603. {
  604. $propertyValue = $this->compileCssPropertyForCache($propertyValue, $propertyCache);
  605. }
  606. else
  607. {
  608. $propertyValue = $this->compileScalarPropertyForCache($propertyValue, $propertyCache);
  609. }
  610. }
  611. if ($styleId == -1)
  612. {
  613. $this->_getDataRegistryModel()->set('adminStyleModifiedDate', XenForo_Application::$time);
  614. $this->_getDataRegistryModel()->set('adminStyleProperties', $propertyCache);
  615. }
  616. else if ($styleId > 0)
  617. {
  618. $dw = XenForo_DataWriter::create('XenForo_DataWriter_Style');
  619. if ($style)
  620. {
  621. $dw->setExistingData($style, true);
  622. }
  623. else
  624. {
  625. $dw->setExistingData($styleId);
  626. }
  627. $dw->set('properties', $propertyCache);
  628. $dw->save();
  629. }
  630. return $propertyCache;
  631. }
  632. /**
  633. * Compiles a CSS property from it's user-input-based version to the property cache version.
  634. *
  635. * @param array $original Original, input-based CSS rule
  636. * @param array $properties A list of all properties, for resolving variable style references
  637. *
  638. * @return array CSS rule for cache
  639. */
  640. public function compileCssPropertyForCache(array $original, array $properties = array())
  641. {
  642. $output = $original;
  643. $output = $this->compileCssProperty_sanitize($output, $original);
  644. foreach ($output AS &$outputValue)
  645. {
  646. $outputValue = $this->replaceVariablesInStylePropertyValue($outputValue, $properties);
  647. }
  648. $output = $this->compileCssProperty_compileRules($output, $original);
  649. $output = $this->compileCssProperty_cleanUp($output, $original);
  650. return $output;
  651. }
  652. /**
  653. * Sanitizes the values in the CSS property output array.
  654. *
  655. * @param array $output Output CSS property
  656. * @param array $original Original format of CSS property
  657. *
  658. * @return array Updated output CSS property
  659. */
  660. public function compileCssProperty_sanitize(array $output, array $original)
  661. {
  662. // remove empty properties so isset can be used (0 is a valid value in many places)
  663. foreach ($output AS $key => &$value)
  664. {
  665. if (is_array($value))
  666. {
  667. if (count($value) == 0)
  668. {
  669. unset($output[$key]);
  670. }
  671. }
  672. else if (trim($value) === '')
  673. {
  674. unset($output[$key]);
  675. }
  676. else if ($value !== '0' && strval(intval($value)) === $value)
  677. {
  678. // not 0 and looks like an int, add "px" unit
  679. $value = $value . 'px';
  680. }
  681. }
  682. // translate array-based text decoration to css style
  683. if (!empty($output['text-decoration']) && is_array($output['text-decoration']))
  684. {
  685. if (isset($output['text-decoration']['none']))
  686. {
  687. $output['text-decoration'] = 'none';
  688. }
  689. else
  690. {
  691. $output['text-decoration'] = implode(' ', $output['text-decoration']);
  692. }
  693. }
  694. return $output;
  695. }
  696. /**
  697. * Compiles all the rules of a CSS property.
  698. *
  699. * @param array $output Output CSS property
  700. * @param array $original Original format of CSS property
  701. *
  702. * @return array Updated output CSS property
  703. */
  704. public function compileCssProperty_compileRules(array $output, array $original)
  705. {
  706. // handle the font short cut (includes all text-related rules)
  707. if (false && isset($output['font-size'], $output['font-family']))
  708. {
  709. // font shortcut now disabled on account of the line-height issue
  710. $output['font'] = 'font: ' . $this->_getCssValue(
  711. $output, array('font-style', 'font-variant', 'font-weight', 'font-size', 'font-family')
  712. ) . ';';
  713. }
  714. else
  715. {
  716. $output['font'] = $this->_getCssValueRule(
  717. $output, array('font-style', 'font-variant', 'font-weight', 'font-size', 'font-family')
  718. );
  719. }
  720. $output['font'] .= "\n" . $this->_getCssValueRule(
  721. $output, array('color', 'text-decoration')
  722. );
  723. // background shortcut
  724. if (isset($output['background-image']) && $output['background-image'] != 'none')
  725. {
  726. $output['background-image'] = trim($output['background-image']);
  727. if (!preg_match('#^url\s*\(#', $output['background-image']))
  728. {
  729. $output['background-image'] = preg_replace('/^("|\')(.*)\\1$/', '\\2', $output['background-image']);
  730. $output['background-image'] = 'url(\'' . $output['background-image'] . '\')';
  731. }
  732. }
  733. if (!empty($output['background-none']))
  734. {
  735. $output['background'] = 'background: none;';
  736. $output['background-color'] = 'none';
  737. $output['background-image'] = 'none';
  738. }
  739. else if ( // force the background shortcut if a color + image is specified, OR if color = rgba
  740. isset($output['background-color'], $output['background-image'])
  741. ||
  742. (isset($output['background-color']) && substr(strtolower($output['background-color']), 0, 4) == 'rgba')
  743. )
  744. {
  745. $output['background'] = 'background: ' . $this->_getCssValue(
  746. $output, array('background-color', 'background-image', 'background-repeat', 'background-position')
  747. ) . ';';
  748. }
  749. else
  750. {
  751. $output['background'] = $this->_getCssValueRule($output,
  752. array('background-color', 'background-image', 'background-repeat', 'background-position')
  753. );
  754. }
  755. // padding, margin shortcuts
  756. $this->_getPaddingMarginShortCuts('padding', $output);
  757. $this->_getPaddingMarginShortCuts('margin', $output);
  758. // border shortcut
  759. if (isset($output['border-width'], $output['border-style'], $output['border-color']))
  760. {
  761. $output['border'] = 'border: ' . $this->_getCssValue(
  762. $output, array('border-width', 'border-style', 'border-color')
  763. ) . ';';
  764. }
  765. else
  766. {
  767. $output['border'] = $this->_getCssValueRule(
  768. $output, array('border-width', 'border-style', 'border-color')
  769. );
  770. }
  771. foreach (array('top', 'right', 'bottom', 'left') AS $borderSide)
  772. {
  773. $borderSideName = "border-$borderSide";
  774. if (isset($output["$borderSideName-width"], $output["$borderSideName-style"], $output["$borderSideName-color"]))
  775. {
  776. $borderSideCss = $borderSideName . ': ' . $this->_getCssValue(
  777. $output, array("$borderSideName-width", "$borderSideName-style", "$borderSideName-color")
  778. ) . ';';
  779. }
  780. else
  781. {
  782. $borderSideCss = $this->_getCssValueRule(
  783. $output, array("$borderSideName-width", "$borderSideName-style", "$borderSideName-color")
  784. );
  785. }
  786. if ($borderSideCss)
  787. {
  788. $output['border'] .= "\n" . $borderSideCss;
  789. }
  790. }
  791. // border radius shortcut, ties into border
  792. if (isset($output['border-radius']))
  793. {
  794. $output['border'] .= "\nborder-radius: " . $output['border-radius'] . ';';
  795. }
  796. foreach (array('top-left', 'top-right', 'bottom-right', 'bottom-left') AS $radiusCorner)
  797. {
  798. $radiusCornerName = "border-$radiusCorner-radius";
  799. if (isset($output[$radiusCornerName]))
  800. {
  801. $output['border'] .= "\n$radiusCornerName: " . $output[$radiusCornerName] . ';';
  802. }
  803. }
  804. return $output;
  805. }
  806. protected function _getPaddingMarginShortCuts($type, array &$output)
  807. {
  808. $test = $output;
  809. // push all the values into the test array for purposes of determining how to build the short cut
  810. if (isset($output[$type . '-all']))
  811. {
  812. foreach (array('top', 'left', 'bottom', 'right') AS $side)
  813. {
  814. if (!isset($output["{$type}-{$side}"]))
  815. {
  816. $test["{$type}-{$side}"] = $output[$type . '-all'];
  817. }
  818. }
  819. }
  820. if (isset($test[$type . '-top'], $test[$type . '-right'], $test[$type . '-bottom'], $test[$type . '-left']))
  821. {
  822. if ($test[$type . '-top'] == $test[$type . '-right']
  823. && $test[$type . '-top'] == $test[$type . '-bottom']
  824. && $test[$type . '-top'] == $test[$type . '-left'])
  825. {
  826. $output[$type] = $type . ': ' . $test[$type . '-top'] . ';';
  827. }
  828. else if ($test[$type . '-top'] == $test[$type . '-bottom'] && $test[$type . '-right'] == $test[$type . '-left'])
  829. {
  830. $output[$type] = $type . ': ' . $this->_getCssValue(
  831. $test, array($type . '-top', $type . '-right')
  832. ) . ';';
  833. }
  834. else if ($test[$type . '-right'] == $test[$type . '-left'])
  835. {
  836. $output[$type] = $type . ': ' . $this->_getCssValue(
  837. $test, array($type . '-top', $type . '-right', $type . '-bottom')
  838. ) . ';';
  839. }
  840. else
  841. {
  842. $output[$type] = $type . ': ' . $this->_getCssValue(
  843. $test, array($type . '-top', $type . '-right', $type . '-bottom', $type . '-left')
  844. ) . ';';
  845. }
  846. }
  847. else
  848. {
  849. $output[$type] = $this->_getCssValueRule(
  850. $test, array($type . '-top', $type . '-right', $type . '-bottom', $type . '-left')
  851. );
  852. }
  853. }
  854. /**
  855. * Cleans up the CSS property output after compilation.
  856. *
  857. * @param array $output Output CSS property
  858. * @param array $original Original format of CSS property
  859. *
  860. * @return array Updated output CSS property
  861. */
  862. public function compileCssProperty_cleanUp(array $output, array $original)
  863. {
  864. foreach ($output AS $key => &$value)
  865. {
  866. if (preg_match('/^(
  867. background-(none|image|position|repeat)
  868. |font-(variant|weight|style)
  869. |text-decoration
  870. |border-style
  871. |border-(left|right|top|bottom)-style
  872. )/x', $key))
  873. {
  874. unset($output[$key]);
  875. continue;
  876. }
  877. $value = trim($value);
  878. if ($value === '')
  879. {
  880. unset($output[$key]);
  881. continue;
  882. }
  883. }
  884. return $output;
  885. }
  886. /**
  887. * Compiles a scalar property value for the cache.
  888. *
  889. * @param string $original Original property value
  890. * @param array $properties A list of all properties, for resolving variable style references
  891. *
  892. * @return string
  893. */
  894. public function compileScalarPropertyForCache($original, array $properties = array())
  895. {
  896. return $this->replaceVariablesInStylePropertyValue($original, $properties);
  897. }
  898. /**
  899. * Replaces variable references in a style property value.
  900. *
  901. * @param string $value Property value. This is an individual string value.
  902. * @param array $properties List of properites to read from.
  903. *
  904. * @return string
  905. */
  906. public function replaceVariablesInStylePropertyValue($value, array $properties, array $seenProperties = array())
  907. {
  908. if (!$properties)
  909. {
  910. return $value;
  911. }
  912. $outputValue = $this->convertAtPropertiesToTemplateSyntax($value, $properties);
  913. $varProperties = array();
  914. preg_match_all('#\{xen:property\s+("|\'|)([a-z0-9_-]+)(\.([a-z0-9_-]+))?\\1\s*\}#i', $outputValue, $matches, PREG_SET_ORDER);
  915. foreach ($matches AS $match)
  916. {
  917. $varProperties[$match[0]] = (isset($match[4]) ? array($match[2], $match[4]) : array($match[2]));
  918. }
  919. foreach ($varProperties AS $matchSearch => $match)
  920. {
  921. $matchReplace = '';
  922. $matchName = implode('.', $match);
  923. if (!isset($seenProperties[$matchName]))
  924. {
  925. $matchProperty = $match[0];
  926. if (isset($match[1]))
  927. {
  928. $matchSubProperty = $match[1];
  929. if (isset($properties[$matchProperty][$matchSubProperty]) && is_array($properties[$matchProperty]))
  930. {
  931. $matchReplace = $properties[$matchProperty][$matchSubProperty];
  932. }
  933. }
  934. else if (isset($properties[$matchProperty]) && !is_array($properties[$matchProperty]))
  935. {
  936. $matchReplace = $properties[$matchProperty];
  937. }
  938. }
  939. if ($matchReplace)
  940. {
  941. $newSeenProperties = $seenProperties;
  942. $newSeenProperties[$matchName] = true;
  943. $matchReplace = $this->replaceVariablesInStylePropertyValue($matchReplace, $properties, $newSeenProperties);
  944. }
  945. $outputValue = str_replace($matchSearch, $matchReplace, $outputValue);
  946. }
  947. return $outputValue;
  948. }
  949. /**
  950. * Helper for CSS property cache compilation. Gets the value(s) for one or more
  951. * CSS rule keys. Multiple keys will be separated by a space.
  952. *
  953. * @param array $search Array to search for keys in
  954. * @param string|array $key One or more keys to search for
  955. *
  956. * @return string Values for matching keys; space separated
  957. */
  958. protected function _getCssValue(array $search, $key)
  959. {
  960. if (is_array($key))
  961. {
  962. $parts = array();
  963. foreach ($key AS $searchKey)
  964. {
  965. if (isset($search[$searchKey]))
  966. {
  967. $parts[] = $search[$searchKey];
  968. }
  969. }
  970. return implode(' ', $parts);
  971. }
  972. else
  973. {
  974. return (isset($search[$key]) ? $search[$key] : '');
  975. }
  976. }
  977. /**
  978. * Helper for CSS property cache compilation. Gets the full rule(s) for one or more
  979. * CSS rule keys.
  980. *
  981. * @param array $search Array to search for keys in
  982. * @param string|array $key One or more keys to search for
  983. *
  984. * @return string Full CSS rules
  985. */
  986. protected function _getCssValueRule(array $search, $key)
  987. {
  988. if (is_array($key))
  989. {
  990. $parts = array();
  991. foreach ($key AS $searchKey)
  992. {
  993. if (isset($search[$searchKey]))
  994. {
  995. $parts[] = "$searchKey: " . $search[$searchKey] . ";";
  996. }
  997. }
  998. return implode("\n", $parts);
  999. }
  1000. else if (isset($search[$key]))
  1001. {
  1002. return "$key: " . $search[$key] . ";";
  1003. }
  1004. }
  1005. /**
  1006. * Updates the specified style property value.
  1007. *
  1008. * @param array $definition Style property definition
  1009. * @param integer $styleId Style property is being changed in
  1010. * @param mixed $newValue New value (string for scalar; array for css)
  1011. * @param boolean $extraOptions Extra options to pass to the data writer
  1012. * @param mixed $existingProperty If array/false, considered to be the property to be updated; otherwise, determined automatically
  1013. * @param string $existingValue The existing value in the place of this property. This may
  1014. * come from the parent style (unlike $existingProperty). This prevents customization from
  1015. * occurring when a value isn't changed.
  1016. *
  1017. * @param string Returns the property value as it looks going into the DB
  1018. */
  1019. public function updateStylePropertyValue(array $definition, $styleId, $newValue,
  1020. array $extraOptions = array(), $existingProperty = null, $existingValue = null
  1021. )
  1022. {
  1023. $styleId = intval($styleId);
  1024. if ($existingProperty !== false && !is_array($existingProperty))
  1025. {
  1026. $existingProperty = $this->getStylePropertyByDefinitionAndStyle(
  1027. $definition['property_definition_id'], $styleId
  1028. );
  1029. }
  1030. if ($definition['property_type'] == 'scalar')
  1031. {
  1032. $newValue = strval($newValue);
  1033. }
  1034. else if (!is_array($newValue))
  1035. {
  1036. $newValue = array();
  1037. }
  1038. $dw = XenForo_DataWriter::create('XenForo_DataWriter_StyleProperty');
  1039. $dw->setOption(XenForo_DataWriter_StyleProperty::OPTION_VALUE_FORMAT, $definition['property_type']);
  1040. $dw->setOption(XenForo_DataWriter_StyleProperty::OPTION_VALUE_COMPONENTS, unserialize($definition['css_components']));
  1041. $dw->setOption(XenForo_DataWriter_StyleProperty::OPTION_REBUILD_CACHE, true);
  1042. foreach ($extraOptions AS $option => $optionValue)
  1043. {
  1044. $dw->setOption($option, $optionValue);
  1045. }
  1046. $dw->setExtraData(XenForo_DataWriter_StyleProperty::DATA_DEFINITION, $definition);
  1047. if ($existingProperty)
  1048. {
  1049. $dw->setExistingData($existingProperty, true);
  1050. }
  1051. else
  1052. {
  1053. $dw->set('property_definition_id', $definition['property_definition_id']);
  1054. $dw->set('style_id', $styleId);
  1055. }
  1056. $dw->set('property_value', $newValue);
  1057. $dw->preSave();
  1058. if ($dw->get('property_value') === $existingValue)
  1059. {
  1060. return $dw->get('property_value');
  1061. }
  1062. $dw->save();
  1063. return $dw->get('property_value');
  1064. }
  1065. /**
  1066. * Saves a set of style property changes from the input format.
  1067. *
  1068. * @param integer $styleId Style to change properties in
  1069. * @param array $properties Properties from input; keyed by definition ID
  1070. * @param array $reset List of properties to reset if customized; keyed by definition ID
  1071. */
  1072. public function saveStylePropertiesInStyleFromInput($styleId, array $properties, array $reset = array())
  1073. {
  1074. $existingProperties = $this->getEffectiveStylePropertiesInStyle($styleId);
  1075. XenForo_Db::beginTransaction($this->_getDb());
  1076. foreach ($properties AS $definitionId => $propertyValue)
  1077. {
  1078. if (!isset($existingProperties[$definitionId]))
  1079. {
  1080. continue;
  1081. }
  1082. $propertyDefinition = $existingProperties[$definitionId];
  1083. if ($propertyDefinition['style_id'] == $styleId)
  1084. {
  1085. $existingProperty = $propertyDefinition;
  1086. if (!empty($reset[$definitionId]) && $propertyDefinition['definition_style_id'] != $styleId)
  1087. {
  1088. $dw = XenForo_DataWriter::create('XenForo_DataWriter_StyleProperty');
  1089. $dw->setOption(XenForo_DataWriter_StyleProperty::OPTION_REBUILD_CACHE, false);
  1090. $dw->setExtraData(XenForo_DataWriter_StyleProperty::DATA_DEFINITION, $propertyDefinition);
  1091. $dw->setExistingData($existingProperty, true);
  1092. $dw->delete();
  1093. continue;
  1094. }
  1095. }
  1096. else
  1097. {
  1098. $existingProperty = false;
  1099. }
  1100. $this->updateStylePropertyValue(
  1101. $propertyDefinition, $styleId, $propertyValue,
  1102. array(XenForo_DataWriter_StyleProperty::OPTION_REBUILD_CACHE => false),
  1103. $existingProperty, $propertyDefinition['property_value'] // this is the effective value
  1104. );
  1105. }
  1106. $this->rebuildPropertyCacheInStyleAndChildren($styleId);
  1107. XenForo_Db::commit($this->_getDb());
  1108. }
  1109. /**
  1110. * Get the specified style property definition by ID. Includes default
  1111. * property value.
  1112. *
  1113. * @param integer $propertyDefinitionId
  1114. *
  1115. * @return array|false
  1116. */
  1117. public function getStylePropertyDefinitionById($propertyDefinitionId)
  1118. {
  1119. return $this->_getDb()->fetchRow('
  1120. SELECT property_definition.*,
  1121. property.property_value
  1122. FROM xf_style_property_definition AS property_definition
  1123. LEFT JOIN xf_style_property AS property ON
  1124. (property.property_definition_id = property_definition.property_definition_id
  1125. AND property.style_id = property_definition.definition_style_id)
  1126. WHERE property_definition.property_definition_id = ?
  1127. ', $propertyDefinitionId);
  1128. }
  1129. /**
  1130. * Gets the specified style property definition by its name and definition
  1131. * style ID. Includes default property value.
  1132. *
  1133. * @param string $name
  1134. * @param integer $styleId
  1135. *
  1136. * @return array|false
  1137. */
  1138. public function getStylePropertyDefinitionByNameAndStyle($name, $styleId)
  1139. {
  1140. return $this->_getDb()->fetchRow('
  1141. SELECT property_definition.*,
  1142. property.property_value
  1143. FROM xf_style_property_definition AS property_definition
  1144. LEFT JOIN xf_style_property AS property ON
  1145. (property.property_definition_id = property_definition.property_definition_id
  1146. AND property.style_id = property_definition.definition_style_id)
  1147. WHERE property_definition.property_name = ?
  1148. AND property_definition.definition_style_id = ?
  1149. ', array($name, $styleId));
  1150. }
  1151. /**
  1152. * Get the specified style property definitions by their IDs.
  1153. * Includes default property value.
  1154. *
  1155. * @param array $propertyDefinitionIds
  1156. *
  1157. * @return array Format: [property definition id] => info
  1158. */
  1159. public function getStylePropertyDefinitionsByIds(array $propertyDefinitionIds)
  1160. {
  1161. if (!$propertyDefinitionIds)
  1162. {
  1163. return array();
  1164. }
  1165. return $this->fetchAllKeyed('
  1166. SELECT property_definition.*,
  1167. property.property_value
  1168. FROM xf_style_property_definition AS property_definition
  1169. LEFT JOIN xf_style_property AS property ON
  1170. (property.property_definition_id = property_definition.property_definition_id
  1171. AND property.style_id = property_definition.definition_style_id)
  1172. WHERE property_definition.property_definition_id IN (' . $this->_getDb()->quote($propertyDefinitionIds) . ')
  1173. ', 'property_definition_id');
  1174. }
  1175. /**
  1176. * Returns an array of all style property definitions in the specified group
  1177. *
  1178. * @param string $groupId
  1179. * @param integer|null $styleId If specified, limits to definitions in a specified style
  1180. *
  1181. * @return array
  1182. */
  1183. public function getStylePropertyDefinitionsByGroup($groupName, $styleId = null)
  1184. {
  1185. $properties = $this->fetchAllKeyed('
  1186. SELECT *
  1187. FROM xf_style_property_definition
  1188. WHERE group_name = ?
  1189. ' . ($styleId !== null ? 'AND definition_style_id = ' . $this->_getDb()->quote($styleId) : '') . '
  1190. ORDER BY display_order
  1191. ', 'property_name', $groupName);
  1192. return $properties;
  1193. }
  1194. /**
  1195. * Create of update a style property definition. Input data
  1196. * is named after fields in the style property definition, as well as
  1197. * property_value_scalar and property_value_css.
  1198. *
  1199. * @param integer $definitionId Definition to update; if 0, creates a new one
  1200. * @param array $input List of data from input to change in definition
  1201. *
  1202. * @return array Definition info after saving
  1203. */
  1204. public function createOrUpdateStylePropertyDefinition($definitionId, array $input)
  1205. {
  1206. XenForo_Db::beginTransaction($this->_getDb());
  1207. $dw = XenForo_DataWriter::create('XenForo_DataWriter_StylePropertyDefinition');
  1208. if ($definitionId)
  1209. {
  1210. $dw->setExistingData($definitionId);
  1211. }
  1212. else
  1213. {
  1214. $dw->set('definition_style_id', $input['definition_style_id']);
  1215. }
  1216. $dw->set('group_name', $input['group_name']);
  1217. $dw->set('property_name', $input['property_name']);
  1218. $dw->set('title', $input['title']);
  1219. $dw->set('description', $input['description']);
  1220. $dw->set('property_type', $input['property_type']);
  1221. $dw->set('css_components', $input['css_components']);
  1222. $dw->set('scalar_type', $input['scalar_type']);
  1223. $dw->set('scalar_parameters', $input['scalar_parameters']);
  1224. $dw->set('display_order', $input['display_order']);
  1225. $dw->set('sub_group', $input['sub_group']);
  1226. $dw->set('addon_id', $input['addon_id']);
  1227. $dw->save();
  1228. $definition = $dw->getMergedData();
  1229. if ($input['property_type'] == 'scalar')
  1230. {
  1231. $propertyValue = $input['property_value_scalar'];
  1232. $newPropertyValue = $this->updateStylePropertyValue(
  1233. $definition, $definition['definition_style_id'], $propertyValue
  1234. );
  1235. }
  1236. else
  1237. {
  1238. $propertyValue = $input['property_value_css'];
  1239. if ($definitionId && !$dw->isChanged('property_type'))
  1240. {
  1241. // TODO: update value when possible
  1242. }
  1243. else
  1244. {
  1245. $newPropertyValue = $this->updateStylePropertyValue(
  1246. $definition, $definition['definition_style_id'], array()
  1247. );
  1248. }
  1249. }
  1250. XenForo_Db::commit($this->_getDb());
  1251. return $definition;
  1252. }
  1253. /**
  1254. * Prepares a style property (or definition) for display.
  1255. *
  1256. * @param array $property
  1257. * @param integer|null $displayStyleId The ID of the style the properties are being edited in
  1258. *
  1259. * @return array Prepared version
  1260. */
  1261. public function prepareStyleProperty(array $property, $displayStyleId = null)
  1262. {
  1263. $property['cssComponents'] = unserialize($property['css_components']);
  1264. if ($property['property_type'] == 'scalar')
  1265. {
  1266. $property['propertyValueScalar'] = $property['property_value'];
  1267. $property['propertyValueCss'] = array();
  1268. }
  1269. else
  1270. {
  1271. $property['propertyValueScalar'] = '';
  1272. $property['propertyValueCss'] = unserialize($property['property_value']);
  1273. }
  1274. $property['masterTitle'] = $property['title'];
  1275. if ($property['addon_id'])
  1276. {
  1277. $property['title'] = new XenForo_Phrase($this->getStylePropertyTitlePhraseName($property));
  1278. }
  1279. $property['masterDescription'] = $property['description'];
  1280. if ($property['addon_id'])
  1281. {
  1282. $property['description'] = new XenForo_Phrase($this->getStylePropertyDescriptionPhraseName($property));
  1283. }
  1284. if ($displayStyleId === null)
  1285. {
  1286. $property['canEditDefinition'] = $this->canEditStylePropertyDefinition($property['definition_style_id']);
  1287. }
  1288. else
  1289. {
  1290. $property['canEditDefinition'] = (
  1291. $this->canEditStylePropertyDefinition($property['definition_style_id'])
  1292. && $property['definition_style_id'] == $displayStyleId
  1293. );
  1294. }
  1295. $property['canReset'] = (
  1296. isset($property['effectiveState'])
  1297. && $property['effectiveState'] == 'customized'
  1298. );
  1299. return $property;
  1300. }
  1301. /**
  1302. * Prepares a list of style properties.
  1303. *
  1304. * @param array $properties
  1305. * @param integer|null $displayStyleId The ID of the style the properties are being edited in
  1306. *
  1307. * @return array
  1308. */
  1309. public function prepareStyleProperties(array $properties, $displayStyleId = null)
  1310. {
  1311. foreach ($properties AS &$property)
  1312. {
  1313. $property = $this->prepareStyleProperty($property, $displayStyleId);
  1314. }
  1315. return $properties;
  1316. }
  1317. /**
  1318. * Gets the name of the style property title phrase.
  1319. *
  1320. * @param array $property
  1321. *
  1322. * @return string
  1323. */
  1324. public function getStylePropertyTitlePhraseName(array $property)
  1325. {
  1326. switch ($property['definition_style_id'])
  1327. {
  1328. case -1: $suffix = 'admin'; break;
  1329. case 0: $suffix = 'master'; break;
  1330. default: return '';
  1331. }
  1332. return "style_property_$property[property_name]_$suffix";
  1333. }
  1334. /**
  1335. * Gets the name of the style property description phrase.
  1336. *
  1337. * @param array $property
  1338. *
  1339. * @return string
  1340. */
  1341. public function getStylePropertyDescriptionPhraseName(array $property)
  1342. {
  1343. switch ($property['definition_style_id'])
  1344. {
  1345. case -1: $suffix = 'admin'; break;
  1346. case 0: $suffix = 'master'; break;
  1347. default: return '';
  1348. }
  1349. return "style_property_$property[property_name]_description_$suffix";
  1350. }
  1351. /**
  1352. * Gets the default style property definition.
  1353. *
  1354. * @param integer $styleId
  1355. * @param string $groupName
  1356. *
  1357. * @return array
  1358. */
  1359. public function getDefaultStylePropertyDefinition($styleId, $groupName = '')
  1360. {
  1361. $components = array(
  1362. 'text' => true,
  1363. 'background' => true,
  1364. 'border' => true,
  1365. 'layout' => true,
  1366. 'extra' => true
  1367. );
  1368. return array(
  1369. 'definition_style_id' => $styleId,
  1370. 'group_name' => $groupName,
  1371. 'title' => '',
  1372. 'property_name' => '',
  1373. 'property_type' => 'css',
  1374. 'property_value' => '',
  1375. 'css_components' => serialize($components),
  1376. 'display_order' => 1,
  1377. 'sub_group' => '',
  1378. 'addon_id' => null,
  1379. 'cssComponents' => $components,
  1380. 'propertyValueScalar' => '',
  1381. 'propertyValueCss' => array(),
  1382. 'masterTitle' => ''
  1383. );
  1384. }
  1385. /**
  1386. * Determines if a style property definition can be edited in
  1387. * the specified style.
  1388. *
  1389. * @param integer $styleId
  1390. *
  1391. * @return boolean
  1392. */
  1393. public function canEditStylePropertyDefinition($styleId)
  1394. {
  1395. return XenForo_Application::debugMode();
  1396. }
  1397. /**
  1398. * Determines if a style property can be edited in the specified style.
  1399. *
  1400. * @param integer $styleId
  1401. *
  1402. * @return boolean
  1403. */
  1404. public function canEditStyleProperty($styleId)
  1405. {
  1406. if ($styleId > 0)
  1407. {
  1408. return true;
  1409. }
  1410. else
  1411. {
  1412. return XenForo_Application::debugMode();
  1413. }
  1414. }
  1415. /**
  1416. * Get the style property development directory based on the style a property/definition
  1417. * is defined in. Returns an empty string for non-master properties.
  1418. *
  1419. * @param integer $styleId
  1420. *
  1421. * @return string
  1422. */
  1423. public function getStylePropertyDevelopmentDirectory($styleId)
  1424. {
  1425. if ($styleId > 0)
  1426. {
  1427. return '';
  1428. }
  1429. $config = XenForo_Application::get('config');
  1430. if (!$config->debug || !$config->development->directory)
  1431. {
  1432. return '';
  1433. }
  1434. if ($styleId == 0)
  1435. {
  1436. return XenForo_Application::getInstance()->getRootDir()
  1437. . '/' . $config->development->directory . '/file_output/style_properties';
  1438. }
  1439. else
  1440. {
  1441. return XenForo_Application::getInstance()->getRootDir()
  1442. . '/' . $config->development->directory . '/file_output/admin_style_properties';
  1443. }
  1444. }
  1445. /**
  1446. * Gets the full path to a specific style property development file.
  1447. * Ensures directory is writable.
  1448. *
  1449. * @param string $propertyName
  1450. * @param integer $styleId
  1451. *
  1452. * @return string
  1453. */
  1454. public function getStylePropertyDevelopmentFileName($propertyName, $styleId)
  1455. {
  1456. $dir = $this->getStylePropertyDevelopmentDirectory($styleId);
  1457. if (!$dir)
  1458. {
  1459. throw new XenForo_Exception('Tried to write non-master/admin style property value to development directory, or debug mode is not enabled');
  1460. }
  1461. if (!is_dir($dir) || !is_writable($dir))
  1462. {
  1463. throw new XenForo_Exception("Style property development directory $dir is not writable");
  1464. }
  1465. return ($dir . '/' . $propertyName . '.xml');
  1466. }
  1467. public function writeAllStylePropertyDevelopmentFiles()
  1468. {
  1469. $definitions = $this->getDefaultStylePropertyDefinition(0);
  1470. Zend_Debug::dump($definitions);
  1471. }
  1472. /**
  1473. * Writes out a style property development file.
  1474. *
  1475. * @param array $definition Property definition
  1476. * @param array $property Property value
  1477. */
  1478. public function writeStylePropertyDevelopmentFile(array $definition, array $property)
  1479. {
  1480. if ($definition['addon_id'] != 'XenForo' && $property['style_id'] > 0)
  1481. {
  1482. return;
  1483. }
  1484. $fileName = $this->getStylePropertyDevelopmentFileName($definition['property_name'], $property['style_id']);
  1485. // TODO: in the future, the writing system could be split into writing out definition and values in separate functions.
  1486. // it would make a clearer code path.
  1487. $document = new DOMDocument('1.0', 'utf-8');
  1488. $document->formatOutput = true;
  1489. $node = $document->createElement('property');
  1490. $document->appendChild($node);
  1491. $includeDefinition = ($definition['definition_style_id'] == $property['style_id']);
  1492. if ($includeDefinition)
  1493. {
  1494. $node->setAttribute('definition', 1);
  1495. $node->setAttribute('group_name', $definition['group_name']);
  1496. $node->setAttribute('property_type', $definition['property_type']);
  1497. $node->setAttribute('scalar_type', $definition['scalar_type']);
  1498. $node->setAttribute('scalar_parameters', $definition['scalar_parameters']);
  1499. $components = unserialize($definition['css_components']);
  1500. $node->setAttribute('css_components', implode(',', array_keys($components)));
  1501. $node->setAttribute('display_order', $definition['display_order']);
  1502. $node->setAttribute('sub_group', $definition['sub_group']);
  1503. XenForo_Helper_DevelopmentXml::createDomElements($node, array(
  1504. 'title' => isset($definition['masterTitle']) ? $definition['masterTitle'] : strval($definition['title']),
  1505. 'description' => isset($definition['masterDescription']) ? $definition['masterDescription'] : strval($definition['description'])
  1506. ));
  1507. }
  1508. else
  1509. {
  1510. $node->setAttribute('property_type', $definition['property_type']);
  1511. }
  1512. $valueNode = $document->createElement('value');
  1513. if ($definition['property_type'] == 'scalar')
  1514. {
  1515. $valueNode->appendChild($document->createCDATASection($property['property_value']));
  1516. }
  1517. else
  1518. {
  1519. $jsonValue = json_encode(unserialize($property['property_value']));
  1520. // format one value per line
  1521. $jsonValue = preg_replace('/(?<!:|\\\\)","/', '",' . "\n" . '"', $jsonValue);
  1522. $valueNode->appendChild($document->createCDATASection($jsonValue));
  1523. }
  1524. $node->appendChild($valueNode);
  1525. $document->save($fileName);
  1526. }
  1527. /**
  1528. * Moves a style property development file, for renames.
  1529. *
  1530. * @param array $oldDefinition
  1531. * @param array $newDefinition
  1532. */
  1533. public function moveStylePropertyDevelopmentFile(array $oldDefinition, array $newDefinition)
  1534. {
  1535. if ($oldDefinition['addon_id'] != 'XenForo' || $oldDefinition['definition_style_id'] > 0)
  1536. {
  1537. return;
  1538. }
  1539. if ($newDefinition['definition_style_id'] > 0)
  1540. {
  1541. $this->deleteStylePropertyDevelopmentFile($oldDefinition['property_name'], $oldDefinition['definition_style_id']);
  1542. return;
  1543. }
  1544. $oldFile = $this->getStylePropertyDevelopmentFileName($oldDefinition['property_name'], $oldDefinition['definition_style_id']);
  1545. $newFile = $this->getStylePropertyDevelopmentFileName($newDefinition['property_name'], $newDefinition['definition_style_id']);
  1546. if (file_exists($oldFile))
  1547. {
  1548. rename($oldFile, $newFile);
  1549. }
  1550. }
  1551. /**
  1552. * Updates the definition-related elements of the style property development file
  1553. * without touching the property value.
  1554. *
  1555. * @param array $definition
  1556. */
  1557. public function updateStylePropertyDevelopmentFile(array $definition)
  1558. {
  1559. if ($definition['addon_id'] != 'XenForo' || $definition['definition_style_id'] > 0)
  1560. {
  1561. return;
  1562. }
  1563. $fileName = $this->getStylePropertyDevelopmentFileName($definition['property_name'], $definition['definition_style_id']);
  1564. if (file_exists($fileName))
  1565. {
  1566. $document = new SimpleXMLElement($fileName, 0, true);
  1567. if ((string)$document['definition'])
  1568. {
  1569. $value = (string)$document->value;
  1570. if ((string)$document['property_type'] == 'css')
  1571. {
  1572. $value = serialize(json_decode($value, true));
  1573. }
  1574. $property = $definition + array(
  1575. 'style_id' => $definition['definition_style_id'],
  1576. 'property_value' => $value
  1577. );
  1578. $this->writeStylePropertyDevelopmentFile($definition, $property);
  1579. }
  1580. }
  1581. }
  1582. /**
  1583. * Deletes a style property development file.
  1584. *
  1585. * @param string $name
  1586. * @param integer $styleId Definition style ID.
  1587. */
  1588. public function deleteStylePropertyDevelopmentFile($name, $styleId)
  1589. {
  1590. if ($styleId > 0)
  1591. {
  1592. return;
  1593. }
  1594. $fileName = $this->getStylePropertyDevelopmentFileName($name, $styleId);
  1595. if (file_exists($fileName))
  1596. {
  1597. unlink($fileName);
  1598. }
  1599. }
  1600. /**
  1601. * Deletes the style property file if needed. This is used when reverting a
  1602. * customized property; the file is only deleted if we're deleting the property
  1603. * from a style other than the one it was created in.
  1604. *
  1605. * @param array $definition
  1606. * @param array $property
  1607. */
  1608. public function deleteStylePropertyDevelopmentFileIfNeeded(array $definition, array $property)
  1609. {
  1610. if ($property['style_id'] > 0)
  1611. {
  1612. return;
  1613. }
  1614. if ($property['style_id'] == $definition['definition_style_id'])
  1615. {
  1616. // assume this will be deleted by the definition
  1617. return;
  1618. }
  1619. $this->deleteStylePropertyDevelopmentFile($definition['property_name'], $property['style_id']);
  1620. }
  1621. /**
  1622. * Imports style properties/groups from the development location. This only imports
  1623. * one style's worth of properties at a time.
  1624. *
  1625. * @param integer $styleId Style to import for
  1626. */
  1627. public function importStylePropertiesFromDevelopment($styleId)
  1628. {
  1629. if ($styleId > 0)
  1630. {
  1631. return;
  1632. }
  1633. $dir = $this->getStylePropertyDevelopmentDirectory($styleId);
  1634. if (!$dir)
  1635. {
  1636. return;
  1637. }
  1638. if (!is_dir($dir))
  1639. {
  1640. throw new XenForo_Exception("Style property development directory doesn't exist");
  1641. }
  1642. $files = glob("$dir/*.xml");
  1643. $newProperties = array();
  1644. $newGroups = array();
  1645. foreach ($files AS $fileName)
  1646. {
  1647. $name = basename($fileName, '.xml');
  1648. $document = new SimpleXMLElement($fileName, 0, true);
  1649. if (substr($name, 0, 6) == 'group.')
  1650. {
  1651. $newGroups[] = array(
  1652. 'group_name' => substr($name, 6),
  1653. 'title' => (string)$document->title,
  1654. 'description' => (string)$document->description,
  1655. 'display_order' => (string)$document['display_order']
  1656. );
  1657. }
  1658. else
  1659. {
  1660. if ((string)$document['definition'])
  1661. {
  1662. $property = array(
  1663. 'title' => (string)$document->title,
  1664. 'description' => (string)$document->description,
  1665. 'definition' => (string)$document['definition'],
  1666. 'group_name' => (string)$document['group_name'],
  1667. 'property_type' => (string)$document['property_type'],
  1668. 'scalar_type' => (string)$document['scalar_type'],
  1669. 'scalar_parameters' => (string)$document['scalar_parameters'],
  1670. 'display_order' => (string)$document['display_order'],
  1671. 'sub_group' => (string)$document['sub_group']
  1672. );
  1673. $components = (string)$document['css_components'];
  1674. if ($components)
  1675. {
  1676. $property['css_components'] = array_fill_keys(explode(',', $components), true);
  1677. }
  1678. else
  1679. {
  1680. $property['css_components'] = array();
  1681. }
  1682. }
  1683. else
  1684. {
  1685. $property = array(
  1686. 'property_type' => (string)$document['property_type']
  1687. );
  1688. }
  1689. $property['property_name'] = $name;
  1690. if ($property['property_type'] == 'scalar')
  1691. {
  1692. $property['property_value'] = (string)$document->value;
  1693. }
  1694. else
  1695. {
  1696. $property['property_value'] = json_decode((string)$document->value, true);
  1697. }
  1698. $newProperties[] = $property;
  1699. }
  1700. }
  1701. $this->importStylePropertiesFromArray($newProperties, $newGroups, $styleId, 'XenForo');
  1702. }
  1703. /**
  1704. * Deletes the style properties and definitions in a style (and possibly limited to an add-on).
  1705. *
  1706. * @param integer $styleId Style to delete from. 0 for master, -1 for admin
  1707. * @param string|null $addOnId If not null, limits deletions to an ad-on
  1708. * @param boolean $leaveChildCustomizations If true, child customizations of a deleted definition will be left
  1709. */
  1710. public function deleteStylePropertiesAndDefinitionsInStyle($styleId, $addOnId = null, $leaveChildCustomizations = false)
  1711. {
  1712. $properties = $this->getStylePropertiesInStyles(array($styleId));
  1713. $delPropertyIds = array();
  1714. $delPropertyDefinitionIds = array();
  1715. foreach ($properties AS $property)
  1716. {
  1717. if ($addOnId === null || $property['addon_id'] == $addOnId)
  1718. {
  1719. if ($property['definition_style_id'] == $styleId)
  1720. {
  1721. $delPropertyDefinitionIds[] = $property['property_definition_id'];
  1722. }
  1723. else if ($property['style_id'] == $styleId)
  1724. {
  1725. $delPropertyIds[] = $property['property_id'];
  1726. }
  1727. }
  1728. }
  1729. if ($delPropertyIds)
  1730. {
  1731. $this->_db->delete('xf_style_property',
  1732. 'property_id IN (' . $this->_db->quote($delPropertyIds) . ')'
  1733. );
  1734. }
  1735. if ($delPropertyDefinitionIds)
  1736. {
  1737. $this->_db->delete('xf_style_property_definition',
  1738. 'property_definition_id IN (' . $this->_db->quote($delPropertyDefinitionIds) . ')'
  1739. );
  1740. if ($leaveChildCustomizations)
  1741. {
  1742. $this->_db->delete('xf_style_property',
  1743. 'property_definition_id IN (' . $this->_db->quote($delPropertyDefinitionIds) . ')
  1744. AND style_id = ' . $this->_db->quote($styleId)
  1745. );
  1746. }
  1747. else
  1748. {
  1749. $this->_db->delete('xf_style_property',
  1750. 'property_definition_id IN (' . $this->_db->quote($delPropertyDefinitionIds) . ')'
  1751. );
  1752. }
  1753. }
  1754. }
  1755. /**
  1756. * Delete the style property groups in the specified style, matching the add-on if provided.
  1757. *
  1758. * @param integer $styleId Style to delete from. 0 for master, -1 for admin
  1759. * @param string|null $addOnId If not null, limits deletions to an ad-on
  1760. */
  1761. public function deleteStylePropertyGroupsInStyle($styleId, $addOnId = null)
  1762. {
  1763. $db = $this->_getDb();
  1764. if ($addOnId === null)
  1765. {
  1766. $db->delete('xf_style_property_group', 'group_style_id = ' . $db->quote($styleId));
  1767. }
  1768. else
  1769. {
  1770. $db->delete('xf_style_property_group',
  1771. 'group_style_id = ' . $db->quote($styleId) . ' AND addon_id = ' . $db->quote($addOnId)
  1772. );
  1773. }
  1774. }
  1775. /**
  1776. * Appends the style property list to an XML document.
  1777. *
  1778. * @param DOMElement $rootNode Node to append to
  1779. * @param integer $styleId Style to read values/definitions from
  1780. * @param string|null $addOnId If not null, limits to values/definitions in the specified add-on
  1781. */
  1782. public function appendStylePropertyXml(DOMElement $rootNode, $styleId, $addOnId = null)
  1783. {
  1784. $document = $rootNode->ownerDocument;
  1785. $properties = $this->getStylePropertiesInStyles(array($styleId));
  1786. ksort($properties);
  1787. foreach ($properties AS $property)
  1788. {
  1789. if ($addOnId === null || $property['addon_id'] == $addOnId)
  1790. {
  1791. $node = $document->createElement('property');
  1792. $node->setAttribute('property_name', $property['property_name']);
  1793. $node->setAttribute('property_type', $property['property_type']);
  1794. if ($property['definition_style_id'] == $styleId)
  1795. {
  1796. $node->setAttribute('definition', 1);
  1797. $node->setAttribute('group_name', $property['group_name']);
  1798. $node->setAttribute('title', isset($property['masterTitle']) ? $property['masterTitle'] : strval($property['title']));
  1799. $node->setAttribute('description', isset($property['masterDescription']) ? $property['masterDescription'] : strval($property['description']));
  1800. $components = unserialize($property['css_components']);
  1801. $node->setAttribute('css_components', implode(',', array_keys($components)));
  1802. $node->setAttribute('scalar_type', $property['scalar_type']);
  1803. $node->setAttribute('scalar_parameters', $property['scalar_parameters']);
  1804. $node->setAttribute('display_order', $property['display_order']);
  1805. $node->setAttribute('sub_group', $property['sub_group']);
  1806. }
  1807. if ($property['property_type'] == 'scalar')
  1808. {
  1809. $node->appendChild($document->createCDATASection($property['property_value']));
  1810. }
  1811. else
  1812. {
  1813. $node->appendChild($document->createCDATASection(json_encode(unserialize($property['property_value']))));
  1814. }
  1815. $rootNode->appendChild($node);
  1816. }
  1817. }
  1818. $groups = $this->getStylePropertyGroupsInStyles(array($styleId));
  1819. ksort($groups);
  1820. foreach ($groups AS $group)
  1821. {
  1822. if ($addOnId === null || $group['addon_id'] == $addOnId)
  1823. {
  1824. $node = $document->createElement('group');
  1825. $rootNode->appendChild($node);
  1826. $node->setAttribute('group_name', $group['group_name']);
  1827. $node->setAttribute('display_order', $group['display_order']);
  1828. XenForo_Helper_DevelopmentXml::createDomElements($node, array(
  1829. 'title' => $group['title'],
  1830. 'description' => $group['description']
  1831. ));
  1832. }
  1833. }
  1834. }
  1835. /**
  1836. * Gets the style property development XML.
  1837. *
  1838. * @param integer $styleId
  1839. *
  1840. * @return DOMDocument
  1841. */
  1842. public function getStylePropertyDevelopmentXml($styleId)
  1843. {
  1844. $rootTag = ($styleId == -1 ? 'admin_style_properties' : 'style_properties');
  1845. $document = new DOMDocument('1.0', 'utf-8');
  1846. $document->formatOutput = true;
  1847. $rootNode = $document->createElement($rootTag);
  1848. $document->appendChild($rootNode);
  1849. $this->appendStylePropertyXml($rootNode, $styleId, 'XenForo');
  1850. return $document;
  1851. }
  1852. /**
  1853. * Imports the development admin navigation XML data.
  1854. *
  1855. * @param string $fileName File to read the XML from
  1856. */
  1857. public function importStylePropertyDevelopmentXml($fileName, $styleId)
  1858. {
  1859. $document = new SimpleXMLElement($fileName, 0, true);
  1860. $this->importStylePropertyXml($document, $styleId, 'XenForo');
  1861. }
  1862. /**
  1863. * Imports style properties and definitions from XML.
  1864. *
  1865. * @param SimpleXMLElement $xml XML node to search within
  1866. * @param integer $styleId Target style ID
  1867. * @param string|null $addOnId If not null, target add-on for definitions; if null, add-on is ''
  1868. */
  1869. public function importStylePropertyXml(SimpleXMLElement $xml, $styleId, $addOnId = null)
  1870. {
  1871. if ($xml->property === null)
  1872. {
  1873. return;
  1874. }
  1875. $newProperties = array();
  1876. foreach ($xml->property AS $xmlProperty)
  1877. {
  1878. $property = array(
  1879. 'property_name' => (string)$xmlProperty['property_name'],
  1880. 'group_name' => (string)$xmlProperty['group_name'],
  1881. 'title' => (string)$xmlProperty['title'],
  1882. 'description' => (string)$xmlProperty['description'],
  1883. 'definition' => (string)$xmlProperty['definition'],
  1884. 'property_type' => (string)$xmlProperty['property_type'],
  1885. 'scalar_type' => (string)$xmlProperty['scalar_type'],
  1886. 'scalar_parameters' => (string)$xmlProperty['scalar_parameters'],
  1887. 'display_order' => (string)$xmlProperty['display_order'],
  1888. 'sub_group' => (string)$xmlProperty['sub_group']
  1889. );
  1890. $components = (string)$xmlProperty['css_components'];
  1891. if ($components)
  1892. {
  1893. $components = array_fill_keys(explode(',', $components), true);
  1894. $property['css_components'] = $components;
  1895. }
  1896. else
  1897. {
  1898. $property['css_components'] = array();
  1899. }
  1900. if ($property['property_type'] == 'scalar')
  1901. {
  1902. $property['property_value'] = (string)$xmlProperty;
  1903. }
  1904. else
  1905. {
  1906. $property['property_value'] = json_decode((string)$xmlProperty, true);
  1907. }
  1908. $newProperties[] = $property;
  1909. }
  1910. $newGroups = array();
  1911. foreach ($xml->group AS $xmlGroup)
  1912. {
  1913. $newGroups[] = array(
  1914. 'group_name' => (string)$xmlGroup['group_name'],
  1915. 'title' => (string)$xmlGroup->title,
  1916. 'description' => (string)$xmlGroup->description,
  1917. 'display_order' => (string)$xmlGroup['display_order']
  1918. );
  1919. }
  1920. $this->importStylePropertiesFromArray($newProperties, $newGroups, $styleId, $addOnId);
  1921. }
  1922. /**
  1923. * Imports style properties and definitions from an array.
  1924. *
  1925. * @param array $newProperties List of properties and definitions to import
  1926. * @param array $newGroups List of groups to import
  1927. * @param integer $styleId Target style ID
  1928. * @param string|null $addOnId If not null, only replaces properties with this add-on; otherwise, all in style
  1929. */
  1930. public function importStylePropertiesFromArray(array $newProperties, array $newGroups, $styleId, $addOnId = null)
  1931. {
  1932. // must be run before delete to keep values accessible
  1933. $existingProperties = $this->keyPropertiesByName($this->getEffectiveStylePropertiesInStyle($styleId));
  1934. $addOnIdString = ($addOnId !== null ? $addOnId : '');
  1935. $db = $this->_getDb();
  1936. XenForo_Db::beginTransaction($db);
  1937. $this->deleteStylePropertiesAndDefinitionsInStyle($styleId, $addOnId, true);
  1938. $this->deleteStylePropertyGroupsInStyle($styleId, $addOnId);
  1939. // run after the delete to not include removed data
  1940. $existingGroups = $this->getEffectiveStylePropertyGroupsInStyle($styleId);
  1941. foreach ($newGroups AS $group)
  1942. {
  1943. if (isset($existingGroups[$group['group_name']]))
  1944. {
  1945. continue;
  1946. }
  1947. $dw = XenForo_DataWriter::create('XenForo_DataWriter_StylePropertyGroup');
  1948. $dw->setOption(XenForo_DataWriter_StylePropertyGroup::OPTION_UPDATE_MASTER_PHRASE, false);
  1949. $dw->setOption(XenForo_DataWriter_StylePropertyGroup::OPTION_UPDATE_DEVELOPMENT, false);
  1950. $dw->bulkSet(array(
  1951. 'group_name' => $group['group_name'],
  1952. 'group_style_id' => $styleId,
  1953. 'title' => $group['title'],
  1954. 'description' => $group['description'],
  1955. 'display_order' => $group['display_order'],
  1956. 'addon_id' => $addOnIdString
  1957. ));
  1958. $dw->save();
  1959. }
  1960. foreach ($newProperties AS $property)
  1961. {
  1962. $propertyName = $property['property_name'];
  1963. $propertyValue = $property['property_value'];
  1964. $definition = null;
  1965. $deletedDefinitionId = 0;
  1966. $existingProperty = null;
  1967. if (isset($existingProperties[$propertyName]))
  1968. {
  1969. $definition = $existingProperties[$propertyName];
  1970. if ($definition['definition_style_id'] == $styleId && ($addOnId === null || $definition['addon_id'] == $addOnId))
  1971. {
  1972. $deletedDefinitionId = $definition['property_definition_id'];
  1973. $definition = null;
  1974. }
  1975. }
  1976. if (!empty($property['definition']))
  1977. {
  1978. if (!$definition)
  1979. {
  1980. $dw = XenForo_DataWriter::create('XenForo_DataWriter_StylePropertyDefinition');
  1981. $dw->setOption(XenForo_DataWriter_StylePropertyDefinition::OPTION_UPDATE_MASTER_PHRASE, false);
  1982. $dw->setOption(XenForo_DataWriter_StylePropertyDefinition::OPTION_UPDATE_DEVELOPMENT, false);
  1983. $dw->setOption(XenForo_DataWriter_StylePropertyDefinition::OPTION_CHECK_DUPLICATE, false);
  1984. $dw->bulkSet(array(
  1985. 'property_name' => $propertyName,
  1986. 'group_name' => $property['group_name'],
  1987. 'title' => $property['title'],
  1988. 'description' => $property['description'],
  1989. 'definition_style_id' => $styleId,
  1990. 'property_type' => $property['property_type'],
  1991. 'css_components' => $property['css_components'],
  1992. 'scalar_type' => $property['scalar_type'],
  1993. 'scalar_parameters' => $property['scalar_parameters'],
  1994. 'display_order' => $property['display_order'],
  1995. 'sub_group' => $property['sub_group'],
  1996. 'addon_id' => $addOnIdString
  1997. ));
  1998. $dw->save();
  1999. $definition = $dw->getMergedData();
  2000. if ($deletedDefinitionId)
  2001. {
  2002. $db->update('xf_style_property', array(
  2003. 'property_definition_id' => $definition['property_definition_id']
  2004. ), 'property_definition_id = ' . $db->quote($deletedDefinitionId));
  2005. }
  2006. $existingProperty = false;
  2007. }
  2008. }
  2009. else if ($definition)
  2010. {
  2011. if ($definition['style_id'] == $styleId && $addOnId !== null && $definition['addon_id'] !== $addOnId)
  2012. {
  2013. $existingProperty = $definition;
  2014. }
  2015. else
  2016. {
  2017. $existingProperty = false;
  2018. }
  2019. }
  2020. else
  2021. {
  2022. // non-definition and no matching definition
  2023. continue;
  2024. }
  2025. $this->updateStylePropertyValue(
  2026. $definition, $styleId, $propertyValue,
  2027. array(
  2028. XenForo_DataWriter_StyleProperty::OPTION_REBUILD_CACHE => false,
  2029. XenForo_DataWriter_StyleProperty::OPTION_UPDATE_DEVELOPMENT => false
  2030. ),
  2031. $existingProperty
  2032. );
  2033. }
  2034. $this->rebuildPropertyCacheInStyleAndChildren($styleId);
  2035. XenForo_Db::commit($db);
  2036. }
  2037. /**
  2038. * Replaces {xen:property} references in a template with the @ property version.
  2039. * This allows for easier editing and viewing of properties.
  2040. *
  2041. * @param string $templateText Template with {xen:property} references
  2042. * @param integer $editStyleId The style the template is being edited in
  2043. * @param array|null $properties A list of valid style properties; if null, grabbed automatically ([name] => property)
  2044. *
  2045. * @return string Replaced template text
  2046. */
  2047. public function replacePropertiesInTemplateForEditor($templateText, $editStyleId, array $properties = null)
  2048. {
  2049. if ($properties === null)
  2050. {
  2051. $properties = $this->keyPropertiesByName($this->getEffectiveStylePropertiesInStyle($editStyleId));
  2052. }
  2053. $validComponents = array('font', 'background', 'padding', 'margin', 'border', 'extra');
  2054. preg_match_all('#(?P<leading_space>[ \t]*)\{xen:property\s+("|\'|)(?P<property>[a-z0-9._-]+)\\2\s*\}#si', $templateText, $matches, PREG_SET_ORDER);
  2055. foreach ($matches AS $match)
  2056. {
  2057. $propertyReference = $match['property'];
  2058. $parts = explode('.', $propertyReference, 2);
  2059. $propertyName = $parts[0];
  2060. $propertyComponent = (count($parts) == 2 ? $parts[1] : false);
  2061. if (!isset($properties[$propertyName]) || $properties[$propertyName]['property_type'] == 'scalar')
  2062. {
  2063. continue;
  2064. }
  2065. if ($propertyComponent && !in_array($propertyComponent, $validComponents))
  2066. {
  2067. continue;
  2068. }
  2069. $propertyValue = unserialize($properties[$propertyName]['property_value']);
  2070. $outputValue = $propertyValue;
  2071. $outputValue = $this->compileCssProperty_sanitize($outputValue, $propertyValue);
  2072. $outputValue = $this->compileCssProperty_compileRules($outputValue, $propertyValue);
  2073. $outputValue = $this->compileCssProperty_cleanUp($outputValue, $propertyValue);
  2074. $replacementRules = '';
  2075. if ($propertyComponent)
  2076. {
  2077. if (isset($outputValue[$propertyComponent]))
  2078. {
  2079. $replacementRules = $outputValue[$propertyComponent];
  2080. }
  2081. }
  2082. else
  2083. {
  2084. foreach ($validComponents AS $validComponent)
  2085. {
  2086. if (isset($outputValue[$validComponent]))
  2087. {
  2088. $replacementRules .= "\n" . $outputValue[$validComponent];
  2089. }
  2090. }
  2091. if (isset($outputValue['width']))
  2092. {
  2093. $replacementRules .= "\nwidth: $outputValue[width];";
  2094. }
  2095. if (isset($outputValue['height']))
  2096. {
  2097. $replacementRules .= "\nheight: $outputValue[height];";
  2098. }
  2099. }
  2100. $leadingSpace = $match['leading_space'];
  2101. $replacementRules = preg_replace('#(^|;(\r?\n)+)[ ]*([*a-z0-9_/\-]+):\s*#i', "\\1{$leadingSpace}\\3: ", trim($replacementRules));
  2102. $replacement = $leadingSpace . '@property "' . $propertyReference . '";'
  2103. . "\n" . $replacementRules
  2104. . "\n" . $leadingSpace . '@property "/' . $propertyReference . '";';
  2105. $templateText = str_replace($match[0], $replacement, $templateText);
  2106. }
  2107. preg_match_all('/\{xen:property\s+("|\'|)(?P<propertyName>[a-z0-9_]+)(?P<propertyComponent>\.[a-z0-9._-]+)?\\1\s*\}/si', $templateText, $matches, PREG_SET_ORDER);
  2108. foreach ($matches AS $match)
  2109. {
  2110. if (!in_array(strtolower($match['propertyName']), XenForo_DataWriter_StylePropertyDefinition::$reservedNames))
  2111. {
  2112. $replacement = '@' . $match['propertyName'] . (empty($match['propertyComponent']) ? '' : $match['propertyComponent']);
  2113. $templateText = str_replace($match[0], $replacement, $templateText);
  2114. }
  2115. }
  2116. return $templateText;
  2117. }
  2118. protected static function _atToPropertyCallback($property)
  2119. {
  2120. $name = $property[1];
  2121. if (!isset(self::$_tempProperties[$name]))
  2122. {
  2123. return $property[0];
  2124. }
  2125. if (!in_array(strtolower($name), XenForo_DataWriter_StylePropertyDefinition::$reservedNames))
  2126. {
  2127. return '{xen:property ' . $name . (empty($property[2]) ? '' : $property[2]) . '}';
  2128. }
  2129. return $property[0];
  2130. }
  2131. /**
  2132. * Converts @propertyName to {xen:property propertyName}
  2133. *
  2134. * @param string $text
  2135. * @param array $properties
  2136. *
  2137. * @return string
  2138. */
  2139. public function convertAtPropertiesToTemplateSyntax($text, array $properties)
  2140. {
  2141. self::$_tempProperties = $properties;
  2142. $text = preg_replace_callback(
  2143. '/(?<=[^a-z0-9_]|^)@([a-z0-9_]+)(\.[a-z0-9._-]+)?/si',
  2144. array('self', '_atToPropertyCallback'),
  2145. $text
  2146. );
  2147. self::$_tempProperties = null;
  2148. return $text;
  2149. }
  2150. /**
  2151. * Translates @ property style references from the template editor into a structured array,
  2152. * and rewrites the template text to the standard {xen:property} format.
  2153. *
  2154. * @param string $templateText Template with @ property references
  2155. * @param string $outputText By reference, the template with {xen:property} values instead
  2156. * @param array $properties A list of valid style properties in the correct style; keyed by named
  2157. *
  2158. * @return array Property values from the template text. Change detection still needs to be run.
  2159. */
  2160. public function translateEditorPropertiesToArray($templateText, &$outputText, array $properties)
  2161. {
  2162. // replace @property 'foo'; .... @property '/foo'; with {xen:property foo}
  2163. $outputText = $templateText;
  2164. $outputProperties = array();
  2165. preg_match_all('/
  2166. @property\s+("|\')(?P<name>[a-z0-9._-]+)\\1;
  2167. (?P<rules>([^@]*?|@(?!property))*)
  2168. @property\s+("|\')\/(?P=name)\\5;
  2169. /siUx', $templateText, $matches, PREG_SET_ORDER
  2170. );
  2171. foreach ($matches AS $match)
  2172. {
  2173. $parts = explode('.', $match['name'], 2);
  2174. $propertyName = $parts[0];
  2175. $propertyComponent = (count($parts) == 2 ? $parts[1] : false);
  2176. if ($propertyComponent == 'font')
  2177. {
  2178. $propertyComponent = 'text';
  2179. }
  2180. else if ($propertyComponent == 'margin' || $propertyComponent == 'padding')
  2181. {
  2182. $propertyComponent = 'layout';
  2183. }
  2184. if (!isset($properties[$propertyName]) || $properties[$propertyName]['property_type'] != 'css')
  2185. {
  2186. continue;
  2187. }
  2188. $validComponents = unserialize($properties[$propertyName]['css_components']);
  2189. if ($propertyComponent && !isset($validComponents[$propertyComponent]))
  2190. {
  2191. continue;
  2192. }
  2193. $set = array(
  2194. 'name' => $propertyName,
  2195. 'component' => $propertyComponent,
  2196. 'rules' => array()
  2197. );
  2198. $extra = array();
  2199. $nonPropertyRules = array();
  2200. $paddingValues = array();
  2201. $marginValues = array();
  2202. $comments = array();
  2203. preg_match_all('#/\*(.+)(\*/|$)#siU', $match['rules'], $commentMatches, PREG_SET_ORDER);
  2204. foreach ($commentMatches AS $commentMatch)
  2205. {
  2206. $comments[] = $commentMatch[1];
  2207. $match['rules'] = str_replace($commentMatch[0], '', $match['rules']);
  2208. }
  2209. preg_match_all('/
  2210. (?<=^|\s|;)(?P<name>[a-z0-9-_*]+)
  2211. \s*:\s*
  2212. (?P<value>[^;]*)
  2213. (;|$)
  2214. /siUx', $match['rules'], $ruleMatches, PREG_SET_ORDER
  2215. );
  2216. foreach ($ruleMatches AS $ruleMatch)
  2217. {
  2218. $value = trim($ruleMatch['value']);
  2219. if ($value === '')
  2220. {
  2221. continue;
  2222. }
  2223. $name = strtolower($ruleMatch['name']);
  2224. switch ($name)
  2225. {
  2226. case 'color':
  2227. case 'text-decoration':
  2228. $group = 'text';
  2229. break;
  2230. case 'width':
  2231. case 'height':
  2232. $group = 'layout';
  2233. break;
  2234. default:
  2235. $regex = '/^('
  2236. . 'font|font-(family|size|style|variant|weight)'
  2237. . '|background|background-(color|image|position|repeat)'
  2238. . '|padding|padding-.*|margin|margin-.*'
  2239. . '|border|border-.*-radius|border(-(top|right|bottom|left))?(-(color|style|width|radius))?'
  2240. . ')$/';
  2241. if (preg_match($regex, $name, $nameMatch))
  2242. {
  2243. $ruleParts = explode('-', $nameMatch[1], 2);
  2244. $group = $ruleParts[0];
  2245. }
  2246. else
  2247. {
  2248. $group = 'extra';
  2249. }
  2250. }
  2251. // css references font, but the css components list references text
  2252. if ($group == 'font')
  2253. {
  2254. $group = 'text';
  2255. }
  2256. else if ($group == 'padding' || $group == 'margin')
  2257. {
  2258. $group = 'layout';
  2259. }
  2260. if (($propertyComponent && $group != $propertyComponent) || !isset($validComponents[$group]))
  2261. {
  2262. if (isset($validComponents['extra']))
  2263. {
  2264. $extra[$name] = $value;
  2265. }
  2266. else
  2267. {
  2268. $nonPropertyRules[] = $ruleMatch[0];
  2269. }
  2270. }
  2271. else
  2272. {
  2273. $isValidRule = false;
  2274. if ($group == 'extra')
  2275. {
  2276. $isValidRule = true;
  2277. $extra[$name] = $value;
  2278. }
  2279. else if ($value == 'inherit')
  2280. {
  2281. $isValidRule = false; // can't put inherit rules in properties
  2282. $nonPropertyRules[] = $ruleMatch[0];
  2283. }
  2284. else
  2285. {
  2286. $isValidRule = false;
  2287. switch ($name)
  2288. {
  2289. case 'font':
  2290. $ruleOutput = $this->parseFontCss($value);
  2291. if (is_array($ruleOutput))
  2292. {
  2293. $isValidRule = true;
  2294. $set['rules'] = array_merge($set['rules'], $ruleOutput);
  2295. }
  2296. break;
  2297. case 'text-decoration':
  2298. $isValidRule = true;
  2299. if ($value == 'none')
  2300. {
  2301. $set['rules']['text-decoration'] = array('none' => 'none');
  2302. }
  2303. else
  2304. {
  2305. $decorations = preg_split('/\s+/', strtolower($value), -1, PREG_SPLIT_NO_EMPTY);
  2306. $set['rules']['text-decoration'] = array_combine($decorations, $decorations);
  2307. }
  2308. break;
  2309. case 'background':
  2310. $ruleOutput = $this->parseBackgroundCss($value);
  2311. if (is_array($ruleOutput))
  2312. {
  2313. $isValidRule = true;
  2314. $set['rules'] = array_merge($set['rules'], $ruleOutput);
  2315. }
  2316. break;
  2317. case 'background-image':
  2318. $isValidRule = true;
  2319. if (preg_match('/^url\(("|\'|)([^)]+)\\1\)$/iU', $value, $imageMatch))
  2320. {
  2321. $set['rules']['background-image'] = $imageMatch[2];
  2322. }
  2323. else
  2324. {
  2325. $set['rules']['background-image'] = $value;
  2326. }
  2327. break;
  2328. case 'padding':
  2329. if ($this->parsePaddingMarginCss($value, $paddingValues))
  2330. {
  2331. $isValidRule = true;
  2332. }
  2333. break;
  2334. case 'padding-top':
  2335. case 'padding-right':
  2336. case 'padding-bottom':
  2337. case 'padding-left':
  2338. $paddingValues[substr($name, 8)] = $value;
  2339. unset($paddingValues['all']);
  2340. $isValidRule = true;
  2341. break;
  2342. case 'margin':
  2343. if ($this->parsePaddingMarginCss($value, $marginValues))
  2344. {
  2345. $isValidRule = true;
  2346. }
  2347. break;
  2348. case 'margin-top':
  2349. case 'margin-right':
  2350. case 'margin-bottom':
  2351. case 'margin-left':
  2352. $marginValues[substr($name, 7)] = $value;
  2353. unset($marginValues['all']);
  2354. $isValidRule = true;
  2355. break;
  2356. case 'border':
  2357. case 'border-top':
  2358. case 'border-right':
  2359. case 'border-bottom':
  2360. case 'border-left':
  2361. $ruleOutput = $this->parseBorderCss($value, $name);
  2362. if (is_array($ruleOutput))
  2363. {
  2364. $isValidRule = true;
  2365. $set['rules'] = array_merge($set['rules'], $ruleOutput);
  2366. }
  2367. break;
  2368. default:
  2369. $isValidRule = true;
  2370. $set['rules'][$name] = $value;
  2371. }
  2372. if (!$isValidRule)
  2373. {
  2374. $nonPropertyRules[] = "-xenforo-nomatch-" . $ruleMatch[0];
  2375. }
  2376. }
  2377. }
  2378. }
  2379. if ($paddingValues)
  2380. {
  2381. if (isset($paddingValues['all']))
  2382. {
  2383. $set['rules']['padding-all'] = $paddingValues['all'];
  2384. }
  2385. else
  2386. {
  2387. foreach (array('top', 'right', 'bottom', 'left') AS $paddingSide)
  2388. {
  2389. if (isset($paddingValues[$paddingSide]))
  2390. {
  2391. $set['rules']["padding-$paddingSide"] = $paddingValues[$paddingSide];
  2392. }
  2393. }
  2394. }
  2395. }
  2396. if ($marginValues)
  2397. {
  2398. if (isset($marginValues['all']))
  2399. {
  2400. $set['rules']['margin-all'] = $marginValues['all'];
  2401. }
  2402. else
  2403. {
  2404. foreach (array('top', 'right', 'bottom', 'left') AS $marginSide)
  2405. {
  2406. if (isset($marginValues[$marginSide]))
  2407. {
  2408. $set['rules']["margin-$marginSide"] = $marginValues[$marginSide];
  2409. }
  2410. }
  2411. }
  2412. }
  2413. if ($extra || $comments)
  2414. {
  2415. $set['rules']['extra'] = '';
  2416. if ($extra)
  2417. {
  2418. foreach ($extra AS $extraRule => $extraValue)
  2419. {
  2420. $set['rules']['extra'] .= "\n$extraRule: $extraValue;";
  2421. }
  2422. }
  2423. if ($comments)
  2424. {
  2425. foreach ($comments AS $comment)
  2426. {
  2427. $set['rules']['extra'] .= "\n/*$comment*/";
  2428. }
  2429. }
  2430. $set['rules']['extra'] = trim($set['rules']['extra']);
  2431. }
  2432. $outputProperties[] = $set;
  2433. $replacement = '{xen:property ' . $match['name'] . '}';
  2434. foreach ($nonPropertyRules AS $nonPropertyRule)
  2435. {
  2436. $replacement .= "\n\t$nonPropertyRule";
  2437. }
  2438. $outputText = str_replace($match[0], $replacement, $outputText);
  2439. }
  2440. $outputText = $this->convertAtPropertiesToTemplateSyntax($outputText, $properties);
  2441. return $outputProperties;
  2442. }
  2443. /**
  2444. * Parses font shortcut CSS.
  2445. *
  2446. * @param string $value
  2447. *
  2448. * @return array|false List of property rules to apply or false if shortcut could not be parsed
  2449. */
  2450. public function parseFontCss($value)
  2451. {
  2452. preg_match('/
  2453. ^
  2454. ((?P<font_style>italic|oblique|normal)\s+)?
  2455. ((?P<font_variant>small-caps|normal)\s+)?
  2456. ((?P<font_weight>bold(?:er)?|lighter|[1-9]00|normal)\s+)?
  2457. (?P<font_size>
  2458. xx-small|x-small|small|medium|large|x-large|xx-large|smaller|larger
  2459. |0|-?\d+(\.\d+)?(%|[a-z]+)
  2460. |\{xen:property\s+("|\'|)([a-z0-9._-]+)("|\'|)\s*\}
  2461. |@[a-z0-9._-]+
  2462. )
  2463. \s+
  2464. (?P<font_family>\S.*)
  2465. $
  2466. /siUx', $value, $fontMatch
  2467. );
  2468. if (!$fontMatch)
  2469. {
  2470. return false;
  2471. }
  2472. $output = array();
  2473. if (!empty($fontMatch['font_style']) && strtolower($fontMatch['font_style']) != 'normal')
  2474. {
  2475. $output['font-style'] = 'italic';
  2476. }
  2477. else
  2478. {
  2479. $output['font-style'] = '';
  2480. }
  2481. if (!empty($fontMatch['font_variant']) && strtolower($fontMatch['font_variant']) != 'normal')
  2482. {
  2483. $output['font-variant'] = 'small-caps';
  2484. }
  2485. else
  2486. {
  2487. $output['font-variant'] = '';
  2488. }
  2489. if (!empty($fontMatch['font_weight']) && strtolower($fontMatch['font_weight']) != 'normal')
  2490. {
  2491. $output['font-weight'] = 'bold';
  2492. }
  2493. else
  2494. {
  2495. $output['font-weight'] = '';
  2496. }
  2497. $output['font-size'] = $fontMatch['font_size'];
  2498. $output['font-family'] = $fontMatch['font_family'];
  2499. return $output;
  2500. }
  2501. /**
  2502. * Parses background shortcut CSS.
  2503. *
  2504. * @param string $value
  2505. *
  2506. * @return array|false List of property rules to apply or false if shortcut could not be parsed
  2507. */
  2508. public function parseBackgroundCss($value)
  2509. {
  2510. if (strtolower($value) == 'none')
  2511. {
  2512. return array(
  2513. 'background-none' => '1',
  2514. 'background-color' => '',
  2515. 'background-image' => '',
  2516. 'background-repeat' => '',
  2517. 'background-position' => ''
  2518. );
  2519. }
  2520. $output = array();
  2521. do
  2522. {
  2523. if (preg_match('/^(repeat-x|repeat-y|no-repeat|repeat)/i', $value, $match))
  2524. {
  2525. if (isset($output['background-repeat']))
  2526. {
  2527. return false;
  2528. }
  2529. $output['background-repeat'] = $match[0];
  2530. }
  2531. else if (preg_match('/^(none|url\(("|\'|)(?P<background_image_url>[^)]+)\\2\))/i', $value, $match))
  2532. {
  2533. if (isset($output['background-image']))
  2534. {
  2535. return false;
  2536. }
  2537. if ($match[0] == 'none')
  2538. {
  2539. $output['background-image'] = 'none';
  2540. }
  2541. else
  2542. {
  2543. $output['background-image'] = $match['background_image_url'];
  2544. }
  2545. }
  2546. else if (preg_match('/^(
  2547. (
  2548. (left|center|right|0|-?\d+(\.\d+)?(%|[a-z]+))
  2549. (
  2550. \s+(top|center|bottom|0|-?\d+(\.\d+)?(%|[a-z]+))
  2551. )?
  2552. )|top|center|bottom
  2553. )/ix', $value, $match))
  2554. {
  2555. if (isset($output['background-position']))
  2556. {
  2557. return false;
  2558. }
  2559. $output['background-position'] = $match[0];
  2560. }
  2561. else if (preg_match('/^(
  2562. rgb\(\s*\d+%?\s*,\s*\d+%?\s*,\s*\d+%?\s*\)
  2563. |rgba\(\s*\d+%?\s*,\s*\d+%?\s*,\s*\d+%?\s*,\s*[0-9.]+\s*\)
  2564. |\#[a-f0-9]{6}|\#[a-f0-9]{3}
  2565. |[a-z]+
  2566. )/ix', $value, $match)
  2567. )
  2568. {
  2569. if (isset($output['background-color']))
  2570. {
  2571. return false;
  2572. }
  2573. $output['background-color'] = $match[0];
  2574. }
  2575. else if (preg_match('/^(
  2576. (\{xen:property\s+("|\'|)([a-z0-9._-]+)("|\'|)\s*\})
  2577. |@[a-z0-9._-]+
  2578. )/ix', $value, $match))
  2579. {
  2580. $handled = false;
  2581. foreach (array('background-color', 'background-image', 'background-position') AS $ruleName)
  2582. {
  2583. if (!isset($output[$ruleName]))
  2584. {
  2585. $output[$ruleName] = $match[0];
  2586. $handled = true;
  2587. break;
  2588. }
  2589. }
  2590. if (!$handled)
  2591. {
  2592. return false;
  2593. }
  2594. }
  2595. else
  2596. {
  2597. return false;
  2598. }
  2599. $value = strval(substr($value, strlen($match[0])));
  2600. if (preg_match('/^(\s+|$)/', $value))
  2601. {
  2602. $value = ltrim($value);
  2603. }
  2604. else
  2605. {
  2606. return false;
  2607. }
  2608. }
  2609. while ($value !== '');
  2610. if (!$output)
  2611. {
  2612. return false;
  2613. }
  2614. return array_merge(
  2615. array(
  2616. 'background-color' => '',
  2617. 'background-image' => '',
  2618. 'background-repeat' => '',
  2619. 'background-position' => ''
  2620. ),
  2621. $output
  2622. );
  2623. }
  2624. /**
  2625. * Parses padding/margin shortcut CSS.
  2626. *
  2627. * @param string $value
  2628. * @param array $values By reference. Pushes out the effective padding/margin values to later be pulled together.
  2629. *
  2630. * @return boolean
  2631. */
  2632. public function parsePaddingMarginCss($value, array &$values)
  2633. {
  2634. $value = preg_replace('#\{xen:property\s+("|\'|)([a-z0-9_-]+(\.[a-z0-9_-]+)?)\\1\s*\}#i', '@\\2', $value);
  2635. $paddingParts = preg_split('/\s+/', $value, -1, PREG_SPLIT_NO_EMPTY);
  2636. if (count($paddingParts) > 4)
  2637. {
  2638. return false;
  2639. }
  2640. foreach ($paddingParts AS $paddingPart)
  2641. {
  2642. if ($paddingPart[0] !== '@' && !preg_match('#^0|auto|-?\d+(\.\d+)?(%|[a-z]+)$#i', $paddingPart))
  2643. {
  2644. return false;
  2645. }
  2646. }
  2647. switch (count($paddingParts))
  2648. {
  2649. case 1:
  2650. $values = array(
  2651. 'all' => $paddingParts[0],
  2652. 'top' => $paddingParts[0],
  2653. 'right' => $paddingParts[0],
  2654. 'bottom' => $paddingParts[0],
  2655. 'left' => $paddingParts[0],
  2656. );
  2657. break;
  2658. case 2:
  2659. $values = array(
  2660. 'top' => $paddingParts[0],
  2661. 'right' => $paddingParts[1],
  2662. 'bottom' => $paddingParts[0],
  2663. 'left' => $paddingParts[1],
  2664. );
  2665. break;
  2666. case 3:
  2667. $values = array(
  2668. 'top' => $paddingParts[0],
  2669. 'right' => $paddingParts[1],
  2670. 'bottom' => $paddingParts[2],
  2671. 'left' => $paddingParts[1],
  2672. );
  2673. break;
  2674. case 4:
  2675. $values = array(
  2676. 'top' => $paddingParts[0],
  2677. 'right' => $paddingParts[1],
  2678. 'bottom' => $paddingParts[2],
  2679. 'left' => $paddingParts[3],
  2680. );
  2681. break;
  2682. }
  2683. return true;
  2684. }
  2685. /**
  2686. * Parses border shortcut CSS.
  2687. *
  2688. * @param string $value
  2689. * @param string $name The name of the shortcut (border, border-top, etc)
  2690. *
  2691. * @return array|false List of property rules to apply or false if shortcut could not be parsed
  2692. */
  2693. public function parseBorderCss($value, $name)
  2694. {
  2695. $output = array();
  2696. do
  2697. {
  2698. if (preg_match('/^(thin|medium|thick|0|-?\d+(\.\d+)?[a-z]+)/i', $value, $match))
  2699. {
  2700. if (isset($output["$name-width"]))
  2701. {
  2702. return false;
  2703. }
  2704. $output["$name-width"] = $match[0];
  2705. }
  2706. else if (preg_match('/^(none|hidden|dashed|dotted|double|groove|inset|outset|ridge|solid)/i', $value, $match))
  2707. {
  2708. if (isset($output["$name-style"]))
  2709. {
  2710. return false;
  2711. }
  2712. $output["$name-style"] = $match[0];
  2713. }
  2714. else if (preg_match('/^(
  2715. rgb\(\s*\d+%?\s*,\s*\d+%?\s*,\s*\d+%?\s*\)
  2716. |rgba\(\s*\d+%?\s*,\s*\d+%?\s*,\s*\d+%?\s*,\s*[0-9.]+\s*\)
  2717. |\#[a-f0-9]{6}|\#[a-f0-9]{3}
  2718. |[a-z]+
  2719. )/ix', $value, $match)
  2720. )
  2721. {
  2722. if (isset($output["$name-color"]))
  2723. {
  2724. return false;
  2725. }
  2726. $output["$name-color"] = $match[0];
  2727. }
  2728. else if (preg_match('/^(
  2729. (\{xen:property\s+("|\'|)([a-z0-9._-]+)\\2\s*\})
  2730. |@[a-z0-9._-]+
  2731. )/ix', $value, $match))
  2732. {
  2733. $handled = false;
  2734. foreach (array("$name-width", "$name-color") AS $ruleName)
  2735. {
  2736. if (!isset($output[$ruleName]))
  2737. {
  2738. $output[$ruleName] = $match[0];
  2739. $handled = true;
  2740. break;
  2741. }
  2742. }
  2743. if (!$handled)
  2744. {
  2745. return false;
  2746. }
  2747. }
  2748. else
  2749. {
  2750. return false;
  2751. }
  2752. $value = strval(substr($value, strlen($match[0])));
  2753. if (preg_match('/^(\s+|$)/', $value))
  2754. {
  2755. $value = ltrim($value);
  2756. }
  2757. else
  2758. {
  2759. return false;
  2760. }
  2761. }
  2762. while ($value !== '');
  2763. if (!$output)
  2764. {
  2765. return false;
  2766. }
  2767. return array_merge(
  2768. array(
  2769. "$name-width" => '1px',
  2770. "$name-style" => 'none',
  2771. "$name-color" => 'black'
  2772. ),
  2773. $output
  2774. );
  2775. }
  2776. /**
  2777. * Saves style properties in the specified style based on the @ property
  2778. * references that have been parsed out of the template(s).
  2779. *
  2780. * @param integer $styleId Style to save properties into
  2781. * @param array $updates List of property data to update (return from translateEditorPropertiesToArray)
  2782. * @param array $properties List of style properties available in this style. Keyed by name!
  2783. */
  2784. public function saveStylePropertiesInStyleFromTemplate($styleId, array $updates, array $properties)
  2785. {
  2786. $input = array();
  2787. foreach ($updates AS $update)
  2788. {
  2789. if (!isset($properties[$update['name']]))
  2790. {
  2791. continue;
  2792. }
  2793. $property = $properties[$update['name']];
  2794. $definitionId = $property['property_definition_id'];
  2795. if ($update['component'])
  2796. {
  2797. if (isset($input[$definitionId]))
  2798. {
  2799. $base = $input[$definitionId];
  2800. }
  2801. else
  2802. {
  2803. $base = unserialize($property['property_value']);
  2804. }
  2805. $input[$definitionId] = array_merge($base, $update['rules']);
  2806. }
  2807. else
  2808. {
  2809. $input[$definitionId] = $update['rules'];
  2810. }
  2811. }
  2812. if ($input)
  2813. {
  2814. $this->saveStylePropertiesInStyleFromInput($styleId, $input);
  2815. }
  2816. }
  2817. /**
  2818. * Updates the group_name of all style property definitions in group $sourceGroup to $destinationGroup.
  2819. *
  2820. * @param string $sourceGroup
  2821. * @param string $destinationGroup
  2822. */
  2823. public function moveStylePropertiesBetweenGroups($sourceGroup, $destinationGroup)
  2824. {
  2825. XenForo_Db::beginTransaction($this->_getDb());
  2826. foreach ($this->getStylePropertyDefinitionsByGroup($sourceGroup, 0) AS $property)
  2827. {
  2828. $dw = XenForo_DataWriter::create('XenForo_DataWriter_StylePropertyDefinition', XenForo_DataWriter::ERROR_EXCEPTION);
  2829. $dw->setExistingData($property['property_definition_id']);
  2830. $dw->set('group_name', $destinationGroup);
  2831. $dw->save();
  2832. }
  2833. XenForo_Db::commit($this->_getDb());
  2834. }
  2835. /**
  2836. * @return XenForo_Model_Style
  2837. */
  2838. protected function _getStyleModel()
  2839. {
  2840. return $this->getModelFromCache('XenForo_Model_Style');
  2841. }
  2842. }