<?php
namespace XF\Repository;
use XF\Mvc\Entity\Finder;
use XF\Mvc\Entity\Repository;
use function in_array, is_array, is_scalar;
class StyleProperty extends Repository
{
public function findPropertyMapForEditing(\XF\Entity\Style $style, $inGroup = null)
{
$finder = $this->finder('XF:StylePropertyMap');
$finder->where('style_id', $style->style_id)
->with('Property', true)
->with('Property.Group')
->whereAddOnActive([
'relation' => 'Property.AddOn',
'column' => 'Property.addon_id'
])
->keyedBy('property_name')
->order('Property.display_order');
if ($inGroup)
{
$finder->where('Property.group_name', $inGroup);
}
return $finder;
}
/**
* @param \XF\Entity\Style $style
*
* @return \XF\Mvc\Entity\Finder
*/
public function findPropertyMapInStyle(\XF\Entity\Style $style)
{
$finder = $this->finder('XF:StylePropertyMap');
$finder->where('style_id', $style->style_id)
->setDefaultOrder('property_name')
->keyedBy('property_name')
->with('Property', true);
return $finder;
}
/**
* @param \XF\Entity\Style $style
* @param array $groups List of available groups in style, keyed by group_name
*
* @return \XF\Entity\StylePropertyMap[]
*/
public function getUngroupedPropertyMapsInStyle(\XF\Entity\Style $style, array $groups)
{
$maps = $this->findPropertyMapForEditing($style)->fetch();
$ungrouped = [];
foreach ($maps AS $k => $map)
{
if (!isset($groups[$map->Property->group_name]))
{
$ungrouped[$k] = $map;
}
}
return $ungrouped;
}
/**
* @param \XF\Entity\Style $style
*
* @return Finder
*/
public function findPropertiesInStyle(\XF\Entity\Style $style)
{
$finder = $this->finder('XF:StyleProperty');
$finder->where('style_id', $style->style_id)
->order('property_name');
return $finder;
}
public function getStyleColorData(\XF\Entity\Style $style = null)
{
if ($style === null)
{
$style = $this->repository('XF:Style')->getMasterStyle();
}
$styleType = $this->getEffectivePropertyInStyle($style, 'styleType');
return [
'config' => [
'styleType' => ($styleType ? $styleType->property_value : 'light')
],
'colors' => [
'colors' => [
'title' => \XF::phrase('basic'),
'colors' => $this->getColorMapFromPropertyGroup($style, 'color')
],
'palette' => [
'title' => \XF::phrase('palette'),
'colors' => $this->getColorMapFromPropertyGroup($style, 'palette')
]
]
];
}
protected function getColorMapFromPropertyGroup(\XF\Entity\Style $style, $group)
{
$options = [];
$propMaps = $this->findPropertyMapForEditing($style, $group)->fetch();
foreach ($propMaps AS $propMap)
{
$prop = $propMap->Property;
if ($prop->value_type == 'color')
{
$options['@xf-' . $prop->property_name] = [
'title' => $prop->title,
'value' => $prop->property_value
];
}
}
return $options;
}
/**
* @param \XF\Entity\Style $style
*
* @return \XF\Entity\StyleProperty[]
*/
public function getEffectivePropertiesInStyle(\XF\Entity\Style $style)
{
$finder = $this->findPropertyMapInStyle($style);
$finder->pluckFrom('Property', 'property_name');
return $finder->fetch()->toArray();
}
/**
* @param \XF\Entity\Style $style
* @param string $propertyName
*
* @return \XF\Entity\StyleProperty|null
*/
public function getEffectivePropertyInStyle(\XF\Entity\Style $style, $propertyName)
{
$finder = $this->findPropertyMapInStyle($style)->where('property_name', $propertyName);
$map = $finder->fetchOne();
return ($map ? $map->Property : null);
}
/**
* @param \XF\Entity\Style $style
*
* @return \XF\Entity\StylePropertyGroup[]
*/
public function getEffectivePropertyGroupsInStyle(\XF\Entity\Style $style)
{
$stylePath = $style->parent_list;
if (!$stylePath)
{
$stylePath = [0];
}
$entities = $this->finder('XF:StylePropertyGroup')
->where('style_id', $stylePath)
->whereAddOnActive()
->fetch();
$styleGrouped = [];
foreach ($entities AS $entity)
{
$styleGrouped[$entity->style_id][] = $entity;
}
// traverse from most generic (last) to most specific (first) in case of name conflict
$keyedEntities = [];
foreach (array_reverse($stylePath) AS $styleId)
{
if (empty($styleGrouped[$styleId]))
{
continue;
}
foreach ($styleGrouped[$styleId] AS $entity)
{
$keyedEntities[$entity->group_name] = $entity;
}
}
uasort($keyedEntities, function($a, $b)
{
$aOrder = $a->display_order;
$bOrder = $b->display_order;
if ($aOrder == $bOrder)
{
return 0;
}
return ($aOrder < $bOrder ? -1 : 1);
});
return $keyedEntities;
}
/**
* @param int $propertyId
*
* @return \XF\Entity\StyleProperty[]
*/
public function getPropertiesDerivedFrom($propertyId)
{
$finder = $this->finder('XF:StylePropertyMap');
$finder->where('parent_property_id', $propertyId)
->with('Property', true)
->pluckFrom('Property', 'property_id');
return $finder->fetch()->toArray();
}
public function updatePropertyValues(\XF\Entity\Style $style, array $values, array $revertProperties = [])
{
$map = $this->findPropertyMapInStyle($style)->fetch();
$this->db()->beginTransaction();
foreach ($revertProperties AS $propertyName)
{
unset($values[$propertyName]);
if (isset($map[$propertyName]))
{
/** @var \XF\Entity\StylePropertyMap $propMap */
$propMap = $map[$propertyName];
if ($propMap->isRevertable())
{
$propMap->Property->delete();
}
}
}
foreach ($values AS $propertyName => $value)
{
if (isset($map[$propertyName]))
{
/** @var \XF\Entity\StyleProperty $property */
$property = $map[$propertyName]->Property;
if ($property->style_id != $style->style_id)
{
$property = $property->getPropertyCopyInStyle($style);
}
if ($property->updatePropertyValue($value))
{
$property->save();
}
}
}
$this->db()->commit();
}
public function castAndValidatePropertyValue($valueType, array $options, &$value, &$error = null)
{
if ($valueType == 'boolean')
{
$value = $value ? 1 : 0;
return true;
}
if ($valueType == 'template')
{
if (isset($options['type']))
{
switch ($options['type'])
{
case 'array':
if (!is_array($value))
{
$value = [];
}
break;
case 'scalar':
if (!is_scalar($value))
{
$value = '';
}
break;
default:
throw new \LogicException("Unknown template value type '$options[type]'");
}
}
return true;
}
if (!is_scalar($value))
{
$value = '';
}
if ($valueType == 'radio' || $valueType == 'select')
{
if (!isset($options[$value]))
{
return false;
}
}
if ($valueType == 'number' && preg_match('/^(\d+)[a-z]+$/iU', $value, $match))
{
// if we're expecting a number and what was entered was a clear unit, just pull the number part of the unit
$value = $match[1];
}
else if ($valueType == 'unit' && preg_match('/^(\d+)$/iU', $value, $match))
{
// if we're expecting a unit and someone has entered just a number, assume they meant pixels
$value = $match[1] . 'px';
}
return true;
}
public function getDefaultPropertyValue($valueType, array $options)
{
switch ($valueType)
{
case 'boolean': return 0;
case 'number': return 0;
case 'unit': return '0px';
case 'radio':
case 'select':
reset($options);
$key = key($options);
return $key === null ? '' : $key;
case 'template':
if (isset($options['type']))
{
switch ($options['type'])
{
case 'array':
return [];
case 'scalar':
return '';
default:
throw new \LogicException("Unknown template value type '$options[type]'");
}
}
return null;
default: return '';
}
}
public function buildPropertyLessCacheValue(array $css, array $allowedComponents)
{
foreach ($css AS $k => &$value)
{
$value = trim($value);
if ($value === '')
{
unset($css[$k]);
}
}
$output = [];
$outputSimple = function($name) use(&$output, $css)
{
if (isset($css[$name]))
{
$output[$name] = "$name: " . $css[$name] . ';';
}
};
// text
if (in_array('text', $allowedComponents))
{
$outputSimple('font-size');
$outputSimple('color');
$outputSimple('font-weight');
$outputSimple('font-style');
$outputSimple('text-decoration');
}
// background
if (in_array('background', $allowedComponents))
{
if (isset($css['background-color']) || isset($css['background-image']))
{
if (isset($css['background-image']))
{
$css['background-image'] = preg_replace('/^("|\')(.*)\\1$/', '\\2', $css['background-image']);
if (!preg_match('#^[a-z0-9-]+\(#i', $css['background-image']))
{
$css['background-image'] = 'url("' . $css['background-image'] . '")';
}
}
$output['background'] = 'background: '
. ($css['background-color'] ?? '')
. ' '
. ($css['background-image'] ?? '')
. ';';
}
}
// border
if (in_array('border', $allowedComponents))
{
$hasGeneralBorderStyle = false;
if (isset($css['border-width'], $css['border-color']))
{
$output['border'] = 'border: ' . $css['border-width'] . ' solid ' . $css['border-color'] . ';';
$hasGeneralBorderStyle = true;
}
else
{
$outputSimple('border-width');
if (isset($css['border-width']))
{
$output['border-style'] = 'border-style: solid;';
$hasGeneralBorderStyle = true;
}
$outputSimple('border-color');
}
$outputBorderSide = function($side) use (&$output, $css, $outputSimple, $hasGeneralBorderStyle)
{
$width = "border-{$side}-width";
$color = "border-{$side}-color";
$sideProperty = "border-{$side}";
if (isset($css[$width], $css[$color]))
{
$output[$sideProperty] = $sideProperty . ': ' . $css[$width] . ' solid ' . $css[$color] . ';';
}
else
{
$outputSimple($width);
if (isset($css[$width]) && !$hasGeneralBorderStyle)
{
$style = "border-{$side}-style";
$output[$style] = $style . ': solid;';
}
$outputSimple($color);
}
};
$outputBorderSide('top');
$outputBorderSide('right');
$outputBorderSide('bottom');
$outputBorderSide('left');
}
// border radius
if (in_array('border_radius', $allowedComponents))
{
$outputSimple('border-radius');
$outputSimple('border-top-left-radius');
$outputSimple('border-top-right-radius');
$outputSimple('border-bottom-right-radius');
$outputSimple('border-bottom-left-radius');
}
// padding
if (in_array('padding', $allowedComponents))
{
$outputSimple('padding');
$outputSimple('padding-top');
$outputSimple('padding-right');
$outputSimple('padding-bottom');
$outputSimple('padding-left');
}
// extra
if (in_array('extra', $allowedComponents) && isset($css['extra']))
{
$output['extra'] = $css['extra'];
}
$return = trim(implode("\n", $output));
$return = "\t" . str_replace("\n", "\n\t", $return);
return $return;
}
}