Вход Регистрация
Файл: library/XenForo/Model/StyleProperty.php
Строк: 3023
<?php

/**
 * Model for style properties and style property definitions.
 * Note that throughout, style ID -1 is the ACP master.
 *
 * @package XenForo_StyleProperty
 */
class XenForo_Model_StyleProperty extends XenForo_Model
{
    protected static 
$_tempProperties null;

    
/**
     * Gets the specified style property by its ID. Includes
     * definition info.
     *
     * @param integer $id
     *
     * @return array|false
     */
    
public function getStylePropertyById($id)
    {
        return 
$this->_getDb()->fetchRow('
            SELECT property_definition.*,
                style_property.*
            FROM xf_style_property AS style_property
            INNER JOIN xf_style_property_definition AS property_definition ON
                (property_definition.property_definition_id = style_property.property_definition_id)
            WHERE style_property.property_id = ?
        '
$id);
    }

    
/**
     * Gets the specified style property by its definition ID
     * and style ID. Includes definition info.
     *
     * @param integer $definitionId
     * @param integer $styleId
     *
     * @return array|false
     */
    
public function getStylePropertyByDefinitionAndStyle($definitionId$styleId)
    {
        return 
$this->_getDb()->fetchRow('
            SELECT property_definition.*,
                style_property.*
            FROM xf_style_property AS style_property
            INNER JOIN xf_style_property_definition AS property_definition ON
                (property_definition.property_definition_id = style_property.property_definition_id)
            WHERE style_property.property_definition_id = ?
                AND style_property.style_id = ?
        '
, array($definitionId$styleId));
    }

    
/**
     * Gets all style properties in the specified styles. This only
     * includes properties that have been customized or initially defined
     * in the specified styles. Includes definition info.
     *
     * @param array $styleIds
     *
     * @return array Format: [property id] => info
     */
    
public function getStylePropertiesInStyles(array $styleIds)
    {
        if (!
$styleIds)
        {
            return array();
        }

        return 
$this->fetchAllKeyed('
            SELECT property_definition.*,
                style_property.*
            FROM xf_style_property AS style_property
            INNER JOIN xf_style_property_definition AS property_definition ON
                (property_definition.property_definition_id = style_property.property_definition_id)
            WHERE style_property.style_id IN (' 
$this->_getDb()->quote($styleIds) . ')
            ORDER BY property_definition.display_order
        '
'property_id');
    }

    
/**
     * Gets all style properties in a style with the specified definition IDs
     * that have been customized or defined directly in the style.
     *
     * @param integer $styleId
     * @param array $definitionIds
     *
     * @return array Format: [definition id] => info
     */
    
public function getStylePropertiesInStyleByDefinitions($styleId, array $definitionIds)
    {
        if (!
$definitionIds)
        {
            return array();
        }

        return 
$this->fetchAllKeyed('
            SELECT property_definition.*,
                style_property.*
            FROM xf_style_property AS style_property
            INNER JOIN xf_style_property_definition AS property_definition ON
                (property_definition.property_definition_id = style_property.property_definition_id)
            WHERE style_property.style_id = ?
                AND style_property.property_definition_id IN (' 
$this->_getDb()->quote($definitionIds) . ')
            ORDER BY property_definition.display_order
        '
'property_definition_id'$styleId);
    }

    
/**
     * Gets the effective style properties in a style. This includes properties
     * that have been customized/created in a parent style.
     *
     * Includes effectiveState key in each property (customized, inherited, default).
     *
     * @param integer $styleId
     * @param array|null $path Path from style to root (earlier positions closer to style); if null, determined automatically
     * @param array|null $properties List of properties in this style and all parent styles; if null, determined automatically
     *
     * @return array Format: [definition id] => info
     */
    
public function getEffectiveStylePropertiesInStyle($styleId, array $path null, array $properties null)
    {
        if (
$path === null)
        {
            
$path $this->getParentPathFromStyle($styleId);
        }
        if (
$properties === null)
        {
            
$properties $this->getStylePropertiesInStyles($path);
        }

        
$effective = array();
        
$propertyPriorities = array();
        foreach (
$properties AS $property)
        {
            
$definitionId $property['property_definition_id'];
            
$propertyPriority array_search($property['style_id'], $path);

            if (!isset(
$propertyPriorities[$definitionId]) || $propertyPriority $propertyPriorities[$definitionId])
            {
                switch (
$property['style_id'])
                {
                    case 
$property['definition_style_id']:
                        
$property['effectiveState'] = 'default';
                        break;

                    case 
$styleId:
                        
$property['effectiveState'] = 'customized';
                        break;

                    default:
                        
$property['effectiveState'] = 'inherited';
                        break;
                }

                
$effective[$definitionId] = $property;
                
$propertyPriorities[$definitionId] = $propertyPriority;
            }
        }

        return 
$effective;
    }

    
/**
     * Gets the effective properties and groups in a style. Properties are organized
     * within the groups (in properties key).
     *
     * @param integer $styleId
     * @param array|null $path Path from style to root (earlier positions closer to style); if null, determined automatically
     * @param array|null $properties List of properties in this style and all parent styles; if null, determined automatically
     *
     * @return array Format: [group name] => group info, with [properties][definition id] => property info
     */
    
public function getEffectiveStylePropertiesByGroup($styleId, array $path null, array $properties null)
    {
        if (
$path === null)
        {
            
$path $this->getParentPathFromStyle($styleId);
        }

        if (
$properties === null)
        {
            
$properties $this->getEffectiveStylePropertiesInStyle($styleId$path$properties);
        }

        
$groups $this->getEffectiveStylePropertyGroupsInStyle($styleId$path);

        
$invalidGroupings = array();
        foreach (
$properties AS $definitionId => $property)
        {
            if (isset(
$groups[$property['group_name']]))
            {
                
$groups[$property['group_name']]['properties'][$definitionId] = $property;
            }
            else
            {
                
$invalidGroupings[$definitionId] = $property;
            }
        }

        if (
$invalidGroupings)
        {
            
$groups[''] = array(
                
'property_group_id' => 0,
                
'group_name' => '',
                
'group_style_id' => $styleId,
                
'title' => '(ungrouped)',
                
'description' => '',
                
'addon_id' => '',
                
'properties' => $invalidGroupings
            
);
        }

        return 
$groups;
    }

    
/**
     * Fetches all color palette properties for the specified style.
     *
     * @param integer $styleId
     * @param array|null $path Path from style to root (earlier positions closer to style); if null, determined automatically
     * @param array|null $properties List of properties in this style and all parent styles; if null, determined automatically
     *
     * @return array
     */
    
public function getColorPalettePropertiesInStyle($styleId, array $path null, array $properties null)
    {
        
$groups $this->getEffectiveStylePropertiesByGroup($styleId$path$properties);

        return 
$groups['color']['properties'];
    }

    
/**
     * Reorganizes a property list to key properties by name. This is only safe
     * to do when getting properties (effective or not) for a single style.
     *
     * @param array $properties
     *
     * @return array
     */
    
public function keyPropertiesByName(array $properties)
    {
        
$output = array();
        foreach (
$properties AS $property)
        {
            
$output[$property['property_name']] = $property;
        }

        return 
$output;
    }

    
/**
     * Filters a list of properties into 2 groups: scalar properties and css properties.
     *
     * @param array [scalar props, css props]
     */
    
public function filterPropertiesByType(array $properties)
    {
        
$scalar = array();
        
$css = array();

        foreach (
$properties AS $key => $property)
        {
            if (
$property['property_type'] == 'scalar')
            {
                
$scalar[$key] = $property;
            }
            else
            {
                
$css[$key] = $property;
            }
        }

        return array(
$scalar$css);
    }

    
/**
     * Gets the specified style property group.
     *
     * @param integer $groupId
     *
     * @return array|false
     */
    
public function getStylePropertyGroupById($groupId)
    {
        return 
$this->_getDb()->fetchRow('
            SELECT *
            FROM xf_style_property_group
            WHERE property_group_id = ?
        '
$groupId);
    }

    
/**
     * Gets all style property groups defined in the specified styles.
     *
     * @param array $styleIds
     *
     * @return array Format: [property group id] => info
     */
    
public function getStylePropertyGroupsInStyles(array $styleIds)
    {
        if (!
$styleIds)
        {
            return array();
        }

        return 
$this->fetchAllKeyed('
            SELECT *
            FROM xf_style_property_group
            WHERE group_style_id IN (' 
$this->_getDb()->quote($styleIds) . ')
            ORDER BY display_order
        '
'property_group_id');
    }

    
/**
     * Gets the effective list of groups that apply to a style.
     *
     * @param integer $styleId
     * @param array|null $path Path from style to root (earlier positions closer to style); if null, determined automatically
     *
     * @return array Format: [group name] => info
     */
    
public function getEffectiveStylePropertyGroupsInStyle($styleId, array $path null)
    {
        if (
$path === null)
        {
            
$path $this->getParentPathFromStyle($styleId);
        }

        
$groups $this->getStylePropertyGroupsInStyles($path);
        
$output = array();
        foreach (
$groups AS $group)
        {
            
$output[$group['group_name']] = $group;
        }

        return 
$output;
    }

    
/**
     * Gets name-value pairs of style property groups in a style.
     *
     * @param integer $styleId
     *
     * @return array Format: [name] => title
     */
    
public function getStylePropertyGroupOptions($styleId)
    {
        
$groups $this->prepareStylePropertyGroups($this->getEffectiveStylePropertyGroupsInStyle($styleId));
        
$output = array();
        foreach (
$groups AS $group)
        {
            
$output[$group['group_name']] = $group['title'];
        }

        return 
$output;
    }

    
/**
     * Prepares a style property group for display. If properties are found
     * within, they will be automatically prepared.
     *
     * @param array $property
     * @param integer|null $displayStyleId The ID of the style the groups/properties are being edited in
     *
     * @return array Prepared version
     */
    
public function prepareStylePropertyGroup(array $group$displayStyleId null)
    {
        
$group['masterTitle'] = $group['title'];
        
$group['masterDescription'] = $group['description'];
        if (
$group['addon_id'])
        {
            
$group['title'] = new XenForo_Phrase($this->getStylePropertyGroupTitlePhraseName($group));
            
$group['description'] = new XenForo_Phrase($this->getStylePropertyGroupDescriptionPhraseName($group));
        }

        if (!
$group['group_name'])
        {
            
$group['canEdit'] = false;
        }
        else if (
$displayStyleId === null)
        {
            
$group['canEdit'] = $this->canEditStylePropertyDefinition($group['group_style_id']);
        }
        else
        {
            
$group['canEdit'] = (
                
$this->canEditStylePropertyDefinition($group['group_style_id'])
                && 
$group['group_style_id'] == $displayStyleId
            
);
        }

        if (!empty(
$group['properties']))
        {
            
$group['properties'] = $this->prepareStyleProperties($group['properties'], $displayStyleId);
        }

        return 
$group;
    }

    
/**
     * Prepares a list of style property groups. If properties are found within,
     * they will be automatically prepared.
     *
     * @param array $groups
     * @param integer|null $displayStyleId The ID of the style the groups/properties are being edited in
     *
     * @return array
     */
    
public function prepareStylePropertyGroups(array $groups$displayStyleId null)
    {
        foreach (
$groups AS &$group)
        {
            
$group $this->prepareStylePropertyGroup($group$displayStyleId);
        }

        return 
$groups;
    }

    
/**
     * Gets the default style property group record.
     *
     * @param integer $styleId
     *
     * @return array
     */
    
public function getDefaultStylePropertyGroup($styleId)
    {
        return array(
            
'group_name' => '',
            
'group_style_id' => $styleId,
            
'title' => '',
            
'display_order' => 1,
            
'sub_group' => '',
            
'addon_id' => null,

            
'masterTitle' => '',
            
'masterDescription' => '',
        );
    }

    
/**
     * Gets the name of the style property group title phrase.
     *
     * @param array $group
     *
     * @return string
     */
    
public function getStylePropertyGroupTitlePhraseName(array $group)
    {
        switch (
$group['group_style_id'])
        {
            case -
1$suffix 'admin'; break;
            case 
0:  $suffix 'master'; break;
            default: return 
'';
        }

        return 
"style_property_group_$group[group_name]_$suffix";
    }

    
/**
     * Gets the name of the style property group description phrase.
     *
     * @param array $group
     *
     * @return string
     */
    
public function getStylePropertyGroupDescriptionPhraseName(array $group)
    {
        switch (
$group['group_style_id'])
        {
            case -
1$suffix 'admin'; break;
            case 
0:  $suffix 'master'; break;
            default: return 
'';
        }

        return 
"style_property_group_$group[group_name]_{$suffix}_desc";
    }

    
/**
     * Gets the parent path from the specified style. For real styles,
     * this is the parent list. However, this function can handle styles
     * 0 (master) and -1 (ACP).
     *
     * @param $styleId
     * @return array Parent list; earlier positions are more specific
     */
    
public function getParentPathFromStyle($styleId)
    {
        switch (
intval($styleId))
        {
            case 
0: return array(0);
            case -
1: return array(-10);

            default:
                
$style $this->_getStyleModel()->getStyleById($styleId);
                if (
$style)
                {
                    return 
explode(','$style['parent_list']);
                }
                else
                {
                    return array();
                }
        }
    }

    
/**
     * Gets style info in the style property-specific way.
     *
     * @param integer $styleId
     *
     * @return array|false
     */
    
public function getStyle($styleId)
    {
        if (
$styleId >= 0)
        {
            return 
$this->getModelFromCache('XenForo_Model_Style')->getStyleById($styleIdtrue);
        }
        else
        {
            return array(
                
'style_id' => -1,
                
'parent_list' => '-1,0',
                
'title' => new XenForo_Phrase('admin_control_panel')
            );
        }
    }

    
/**
     * Group a list of style properties by the style they belong to.
     * This uses the customization style (not definition style) for grouping.
     *
     * @param array $properties
     *
     * @return array Format: [style id][definition id] => info
     */
    
public function groupStylePropertiesByStyle(array $properties)
    {
        
$newProperties = array();
        foreach (
$properties AS $property)
        {
            
$newProperties[$property['style_id']][$property['property_definition_id']] = $property;
        }

        return 
$newProperties;
    }

    
/**
     * Rebuilds the property cache for all styles.
     */
    
public function rebuildPropertyCacheForAllStyles()
    {
        
$this->rebuildPropertyCacheInStyleAndChildren(0);
    }

    
/**
     * Rebuild the style property cache in the specified style and all
     * child/dependent styles.
     *
     * @param integer $styleId
     * @param boolean $rebuildThisOnly If true, only rebuilds this style
     *
     * @return array The property cache for the requested style
     */
    
public function rebuildPropertyCacheInStyleAndChildren($styleId$rebuildThisOnly false)
    {
        if (
$styleId == -1)
        {
            
$rebuildStyleIds = array(-1);
            
$dataStyleIds = array(-10);
            
$styles = array();
        }
        else
        {
            
$styleModel $this->_getStyleModel();

            
$styles $styleModel->getAllStyles();
            
$styleTree $styleModel->getStyleTreeAssociations($styles);

            
$rebuildStyleIds $styleModel->getAllChildStyleIdsFromTree($styleId$styleTree);
            
$rebuildStyleIds[] = $styleId;

            
$dataStyleIds array_keys($styles);
            
$dataStyleIds[] = 0;

            if (
$styleId == 0)
            {
                if (
$rebuildThisOnly)
                {
                    
$rebuildStyleIds = array(0);
                }
                else
                {
                    
array_unshift($rebuildStyleIds0);
                    
$rebuildStyleIds[] = -1;
                    
$dataStyleIds[] = -1;
                }
            }
        }

        
$properties $this->groupStylePropertiesByStyle(
            
$this->getStylePropertiesInStyles($dataStyleIds)
        );

        
$styleOutput false;

        foreach (
$rebuildStyleIds AS $rebuildStyleId)
        {
            
$sourceStyle = (isset($styles[$rebuildStyleId]) ? $styles[$rebuildStyleId] : array('parent_list' => ''));

            switch (
$rebuildStyleId)
            {
                case 
0:
                    
$sourceStyleIds = array(0);
                    break;

                case -
1:
                    
$sourceStyleIds = array(-10);
                    break;

                default:
                    
$sourceStyleIds explode(','$sourceStyle['parent_list']);
            }

            
$styleProperties = array();
            foreach (
$sourceStyleIds AS $sourceStyleId)
            {
                if (isset(
$properties[$sourceStyleId]))
                {
                    
$styleProperties array_merge($styleProperties$properties[$sourceStyleId]);
                }
            }

            
$effectiveProperties $this->getEffectiveStylePropertiesInStyle(
                
$rebuildStyleId$sourceStyleIds$styleProperties
            
);

            
$cacheOutput $this->updatePropertyCacheInStyle($rebuildStyleId$effectiveProperties$sourceStyle);
            if (
$rebuildStyleId == $styleId)
            {
                
$styleOutput $cacheOutput;
            }
        }

        return 
$styleOutput;
    }

    
/**
     * Updates the property cache in the specified style.
     *
     * @param integer $styleId
     * @param array $effectiveProperties List of effective properties in style.
     * @param array|null $style Style information; queried if needed
     *
     * @return array|false Compiled property cache
     */
    
public function updatePropertyCacheInStyle($styleId, array $effectiveProperties, array $style null)
    {
        
$propertyCache = array();
        foreach (
$effectiveProperties AS $property)
        {
            if (
$property['property_type'] == 'scalar')
            {
                
$propertyCache[$property['property_name']] = $property['property_value'];
            }
            else
            {
                
$propertyCache[$property['property_name']] = unserialize($property['property_value']);
            }
        }
        foreach (
$propertyCache AS &$propertyValue)
        {
            if (
is_array($propertyValue))
            {
                
$propertyValue $this->compileCssPropertyForCache($propertyValue$propertyCache);
            }
            else
            {
                
$propertyValue $this->compileScalarPropertyForCache($propertyValue$propertyCache);
            }
        }

        if (
$styleId == 0)
        {
            
$this->_getDataRegistryModel()->set('defaultStyleProperties'$propertyCache);
            
XenForo_Application::set('defaultStyleProperties'$propertyCache);

            return 
$propertyCache;
        }

        if (!
XenForo_Application::isRegistered('defaultStyleProperties'))
        {
            
$defaultPropertyCache $this->rebuildPropertyCacheInStyleAndChildren(0true);
            
XenForo_Application::set('defaultStyleProperties'$defaultPropertyCache);
        }
        else
        {
            
$defaultPropertyCache XenForo_Application::get('defaultStyleProperties');
        }

        foreach (
$defaultPropertyCache AS $key => $defaultValue)
        {
            if (!isset(
$propertyCache[$key]))
            {
                continue;
            }

            
$localValue $propertyCache[$key];

            if (
is_array($defaultValue))
            {
                
// css property
                
if (!is_array($localValue))
                {
                    continue;
                }

                if (
$defaultValue === $localValue)
                {
                    
// unchanged
                    
unset($propertyCache[$key]);
                }
                else
                {
                    foreach (
$defaultValue AS $innerKey => $innerDefaultValue)
                    {
                        if (!isset(
$localValue[$innerKey]))
                        {
                            
// we've blanked out a value that we had by default
                            
$propertyCache[$key][$innerKey] = null;
                        }
                        else if (
$localValue[$innerKey] === $innerDefaultValue)
                        {
                            
// unchanged
                            
unset($propertyCache[$key][$innerKey]);
                        }
                    }

                    if (isset(
$propertyCache[$key]) && count($propertyCache[$key]) == 0)
                    {
                        
// still exists but nothing customized
                        
unset($propertyCache[$key]);
                    }
                }
            }
            else
            {
                if (
is_array($localValue))
                {
                    continue;
                }

                if (
$localValue === $defaultValue)
                {
                    
// unchanged
                    
unset($propertyCache[$key]);
                }
            }
        }

        if (
$styleId == -1)
        {
            
$this->_getDataRegistryModel()->set('adminStyleModifiedDate'XenForo_Application::$time);
            
$this->_getDataRegistryModel()->set('adminStyleProperties'$propertyCache);
        }
        else if (
$styleId 0)
        {
            
$dw XenForo_DataWriter::create('XenForo_DataWriter_Style');
            if (
$style)
            {
                
$dw->setExistingData($styletrue);
            }
            else
            {
                
$dw->setExistingData($styleId);
            }
            
$dw->set('properties'$propertyCache);
            
$dw->save();
        }

        return 
$propertyCache;
    }

    
/**
     * Compiles a CSS property from it's user-input-based version to the property cache version.
     *
     * @param array $original Original, input-based CSS rule
     * @param array $properties A list of all properties, for resolving variable style references
     *
     * @return array CSS rule for cache
     */
    
public function compileCssPropertyForCache(array $original, array $properties = array())
    {
        
$output $original;
        
$output $this->compileCssProperty_sanitize($output$original);

        foreach (
$output AS &$outputValue)
        {
            
$outputValue $this->replaceVariablesInStylePropertyValue($outputValue$properties);
        }

        
$output $this->compileCssProperty_compileRules($output$original);
        
$output $this->compileCssProperty_cleanUp($output$original);

        return 
$output;
    }

    
/**
     * Sanitizes the values in the CSS property output array.
     *
     * @param array $output Output CSS property
     * @param array $original Original format of CSS property
     *
     * @return array Updated output CSS property
     */
    
public function compileCssProperty_sanitize(array $output, array $original)
    {
        
// remove empty properties so isset can be used (0 is a valid value in many places)
        
foreach ($output AS $key => &$value)
        {
            if (
is_array($value))
            {
                if (
count($value) == 0)
                {
                    unset(
$output[$key]);
                }
            }
            else if (
trim($value) === '')
            {
                unset(
$output[$key]);
            }
            else if (
$value !== '0' && strval(intval($value)) === $value)
            {
                
// not 0 and looks like an int, add "px" unit
                
$value $value 'px';
            }
        }

        
// translate array-based text decoration to css style
        
if (!empty($output['text-decoration']) && is_array($output['text-decoration']))
        {
            if (isset(
$output['text-decoration']['none']))
            {
                
$output['text-decoration'] = 'none';
            }
            else
            {
                
$output['text-decoration'] = implode(' '$output['text-decoration']);
            }
        }

        return 
$output;
    }

    
/**
     * Compiles all the rules of a CSS property.
     *
     * @param array $output Output CSS property
     * @param array $original Original format of CSS property
     *
     * @return array Updated output CSS property
     */
    
public function compileCssProperty_compileRules(array $output, array $original)
    {
        
// handle the font short cut (includes all text-related rules)
        
if (false && isset($output['font-size'], $output['font-family']))
        {
            
// font shortcut now disabled on account of the line-height issue
            
$output['font'] = 'font: ' $this->_getCssValue(
                
$output, array('font-style''font-variant''font-weight''font-size''font-family')
            ) . 
';';
        }
        else
        {
            
$output['font'] = $this->_getCssValueRule(
                
$output, array('font-style''font-variant''font-weight''font-size''font-family')
            );
        }

        
$output['font'] .= "n" $this->_getCssValueRule(
            
$output, array('color''text-decoration')
        );

        
// background shortcut
        
if (isset($output['background-image']) && $output['background-image'] != 'none')
        {
            
$output['background-image'] = trim($output['background-image']);
            if (!
preg_match('#^urls*(#'$output['background-image']))
            {
                
$output['background-image'] = preg_replace('/^("|')(.*)\1$/', '\2', $output['background-image']);
                $output['
background-image'] = 'url('' $output['background-image'] . '')';
            }
        }

        if (!empty($output['
background-none']))
        {
            $output['
background'] = 'backgroundnone;';
            $output['
background-color'] = 'none';
            $output['
background-image'] = 'none';
        }
        else if ( // force the background shortcut if a color + image is specified, OR if color = rgba
            isset($output['
background-color'], $output['background-image'])
            ||
            (isset($output['
background-color']) && substr(strtolower($output['background-color']), 0, 4) == 'rgba')
        )
        {
            $output['
background'] = 'background' . $this->_getCssValue(
                $output, array('
background-color', 'background-image', 'background-repeat', 'background-position')
            ) . '
;';
        }
        else
        {
            $output['
background'] = $this->_getCssValueRule($output,
                array('
background-color', 'background-image', 'background-repeat', 'background-position')
            );
        }

        // padding, margin shortcuts
        $this->_getPaddingMarginShortCuts('
padding', $output);
        $this->_getPaddingMarginShortCuts('
margin', $output);

        // border shortcut
        if (isset($output['
border-width'], $output['border-style'], $output['border-color']))
        {
            $output['
border'] = 'border' . $this->_getCssValue(
                $output, array('
border-width', 'border-style', 'border-color')
            ) . '
;';
        }
        else
        {
            $output['
border'] = $this->_getCssValueRule(
                $output, array('
border-width', 'border-style', 'border-color')
            );
        }

        foreach (array('
top', 'right', 'bottom', 'left') AS $borderSide)
        {
            $borderSideName = "border-$borderSide";

            if (isset($output["$borderSideName-width"], $output["$borderSideName-style"], $output["$borderSideName-color"]))
            {
                $borderSideCss = $borderSideName . '
' . $this->_getCssValue(
                    $output, array("$borderSideName-width", "$borderSideName-style", "$borderSideName-color")
                ) . '
;';
            }
            else
            {
                $borderSideCss = $this->_getCssValueRule(
                    $output, array("$borderSideName-width", "$borderSideName-style", "$borderSideName-color")
                );
            }

            if ($borderSideCss)
            {
                $output['
border'] .= "n" . $borderSideCss;
            }
        }

        // border radius shortcut, ties into border
        if (isset($output['
border-radius']))
        {
            $output['
border'] .= "nborder-radius: " . $output['border-radius'] . ';';
        }

        foreach (array('
top-left', 'top-right', 'bottom-right', 'bottom-left') AS $radiusCorner)
        {
            $radiusCornerName = "border-$radiusCorner-radius";
            if (isset($output[$radiusCornerName]))
            {
                $output['
border'] .= "n$radiusCornerName: " . $output[$radiusCornerName] . ';';
            }
        }

        return $output;
    }

    protected function _getPaddingMarginShortCuts($type, array &$output)
    {
        $test = $output;

        // push all the values into the test array for purposes of determining how to build the short cut
        if (isset($output[$type . '
-all']))
        {
            foreach (array('
top', 'left', 'bottom', 'right') AS $side)
            {
                if (!isset($output["{$type}-{$side}"]))
                {
                    $test["{$type}-{$side}"] = $output[$type . '
-all'];
                }
            }
        }

        if (isset($test[$type . '
-top'], $test[$type . '-right'], $test[$type . '-bottom'], $test[$type . '-left']))
        {
            if ($test[$type . '
-top'] == $test[$type . '-right']
                && $test[$type . '
-top'] == $test[$type . '-bottom']
                && $test[$type . '
-top'] == $test[$type . '-left'])
            {
                $output[$type] = $type . '
' . $test[$type . '-top'] . ';';
            }
            else if ($test[$type . '
-top'] == $test[$type . '-bottom'] && $test[$type . '-right'] == $test[$type . '-left'])
            {
                $output[$type] = $type . '
' . $this->_getCssValue(
                    $test, array($type . '
-top', $type . '-right')
                ) . '
;';
            }
            else if ($test[$type . '
-right'] == $test[$type . '-left'])
            {
                $output[$type] = $type . '
' . $this->_getCssValue(
                    $test, array($type . '
-top', $type . '-right', $type . '-bottom')
                ) . '
;';
            }
            else
            {
                $output[$type] = $type . '
' . $this->_getCssValue(
                    $test, array($type . '
-top', $type . '-right', $type . '-bottom', $type . '-left')
                ) . '
;';
            }
        }
        else
        {
            $output[$type] = $this->_getCssValueRule(
                $test, array($type . '
-top', $type . '-right', $type . '-bottom', $type . '-left')
            );
        }
    }

    /**
     * Cleans up the CSS property output after compilation.
     *
     * @param array $output Output CSS property
     * @param array $original Original format of CSS property
     *
     * @return array Updated output CSS property
     */
    public function compileCssProperty_cleanUp(array $output, array $original)
    {
        foreach ($output AS $key => &$value)
        {
            if (preg_match('
/^(
                
background-(none|image|position|repeat)
                |
font-(variant|weight|style)
                |
text-decoration
            
)/x', $key))
            {
                unset($output[$key]);
                continue;
            }

            $value = trim($value);
            if ($value === '')
            {
                unset($output[$key]);
                continue;
            }
        }

        return $output;
    }

    /**
     * Compiles a scalar property value for the cache.
     *
     * @param string $original Original property value
     * @param array $properties A list of all properties, for resolving variable style references
     *
     * @return string
     */
    public function compileScalarPropertyForCache($original, array $properties = array())
    {
        return $this->replaceVariablesInStylePropertyValue($original, $properties);
    }

    /**
     * Replaces variable references in a style property value.
     *
     * @param string $value Property value. This is an individual string value.
     * @param array $properties List of properites to read from.
     *
     * @return string
     */
    public function replaceVariablesInStylePropertyValue($value, array $properties, array $seenProperties = array())
    {
        if (!$properties)
        {
            return $value;
        }

        $outputValue = $this->convertAtPropertiesToTemplateSyntax($value, $properties);

        $varProperties = array();

        preg_match_all('
#{xen:propertys+("|'|)([a-z0-9_-]+)(.([a-z0-9_-]+))?\1s*}#i', $outputValue, $matches, PREG_SET_ORDER);
        
foreach ($matches AS $match)
        {
            
$varProperties[$match[0]] = (isset($match[4]) ? array($match[2], $match[4]) : array($match[2]));
        }

        foreach (
$varProperties AS $matchSearch => $match)
        {
            
$matchReplace '';

            
$matchName implode('.'$match);
            if (!isset(
$seenProperties[$matchName]))
            {
                
$matchProperty $match[0];
                if (isset(
$match[1]))
                {
                    
$matchSubProperty $match[1];
                    if (isset(
$properties[$matchProperty][$matchSubProperty]) && is_array($properties[$matchProperty]))
                    {
                        
$matchReplace $properties[$matchProperty][$matchSubProperty];
                    }
                }
                else if (isset(
$properties[$matchProperty]) && !is_array($properties[$matchProperty]))
                {
                    
$matchReplace $properties[$matchProperty];
                }
            }

            if (
$matchReplace)
            {
                
$newSeenProperties $seenProperties;
                
$newSeenProperties[$matchName] = true;

                
$matchReplace $this->replaceVariablesInStylePropertyValue($matchReplace$properties$newSeenProperties);
            }

            
$outputValue str_replace($matchSearch$matchReplace$outputValue);
        }

        return 
$outputValue;
    }

    
/**
     * Helper for CSS property cache compilation. Gets the value(s) for one or more
     * CSS rule keys. Multiple keys will be separated by a space.
     *
     * @param array $search Array to search for keys in
     * @param string|array $key One or more keys to search for
     *
     * @return string Values for matching keys; space separated
     */
    
protected function _getCssValue(array $search$key)
    {
        if (
is_array($key))
        {
            
$parts = array();
            foreach (
$key AS $searchKey)
            {
                if (isset(
$search[$searchKey]))
                {
                    
$parts[] = $search[$searchKey];
                }
            }
            return 
implode(' '$parts);
        }
        else
        {
            return (isset(
$search[$key]) ? $search[$key] : '');
        }
    }

    
/**
     * Helper for CSS property cache compilation. Gets the full rule(s) for one or more
     * CSS rule keys.
     *
     * @param array $search Array to search for keys in
     * @param string|array $key One or more keys to search for
     *
     * @return string Full CSS rules
     */
    
protected function _getCssValueRule(array $search$key)
    {
        if (
is_array($key))
        {
            
$parts = array();
            foreach (
$key AS $searchKey)
            {
                if (isset(
$search[$searchKey]))
                {
                    
$parts[] = "$searchKey: " $search[$searchKey] . ";";
                }
            }
            return 
implode("n"$parts);
        }
        else if (isset(
$search[$key]))
        {
            return 
"$key: " $search[$key] . ";";
        }
    }

    
/**
     * Updates the specified style property value.
     *
     * @param array $definition Style property definition
     * @param integer $styleId Style property is being changed in
     * @param mixed $newValue New value (string for scalar; array for css)
     * @param boolean $extraOptions Extra options to pass to the data writer
     * @param mixed $existingProperty If array/false, considered to be the property to be updated; otherwise, determined automatically
     * @param string $existingValue The existing value in the place of this property. This may
     *         come from the parent style (unlike $existingProperty). This prevents customization from
     *         occurring when a value isn't changed.
     *
     * @param string Returns the property value as it looks going into the DB
     */
    
public function updateStylePropertyValue(array $definition$styleId$newValue,
        array 
$extraOptions = array(), $existingProperty null$existingValue null
    
)
    {
        
$styleId intval($styleId);

        if (
$existingProperty !== false && !is_array($existingProperty))
        {
            
$existingProperty $this->getStylePropertyByDefinitionAndStyle(
                
$definition['property_definition_id'], $styleId
            
);
        }

        if (
$definition['property_type'] == 'scalar')
        {
            
$newValue strval($newValue);
        }
        else if (!
is_array($newValue))
        {
            
$newValue = array();
        }

        
$dw XenForo_DataWriter::create('XenForo_DataWriter_StyleProperty');
        
$dw->setOption(XenForo_DataWriter_StyleProperty::OPTION_VALUE_FORMAT$definition['property_type']);
        
$dw->setOption(XenForo_DataWriter_StyleProperty::OPTION_VALUE_COMPONENTSunserialize($definition['css_components']));
        
$dw->setOption(XenForo_DataWriter_StyleProperty::OPTION_REBUILD_CACHEtrue);
        foreach (
$extraOptions AS $option => $optionValue)
        {
            
$dw->setOption($option$optionValue);
        }
        
$dw->setExtraData(XenForo_DataWriter_StyleProperty::DATA_DEFINITION$definition);

        if (
$existingProperty)
        {
            
$dw->setExistingData($existingPropertytrue);
        }
        else
        {
            
$dw->set('property_definition_id'$definition['property_definition_id']);
            
$dw->set('style_id'$styleId);
        }

        
$dw->set('property_value'$newValue);
        
$dw->preSave();

        if (
$dw->get('property_value') === $existingValue)
        {
            return 
$dw->get('property_value');
        }

        
$dw->save();

        return 
$dw->get('property_value');
    }

    
/**
     * Saves a set of style property changes from the input format.
     *
     * @param integer $styleId Style to change properties in
     * @param array $properties Properties from input; keyed by definition ID
     * @param array $reset List of properties to reset if customized; keyed by definition ID
     */
    
public function saveStylePropertiesInStyleFromInput($styleId, array $properties, array $reset = array())
    {
        
$existingProperties $this->getEffectiveStylePropertiesInStyle($styleId);

        
XenForo_Db::beginTransaction($this->_getDb());

        foreach (
$properties AS $definitionId => $propertyValue)
        {
            if (!isset(
$existingProperties[$definitionId]))
            {
                continue;
            }

            
$propertyDefinition $existingProperties[$definitionId];
            if (
$propertyDefinition['style_id'] == $styleId)
            {
                
$existingProperty $propertyDefinition;
                if (!empty(
$reset[$definitionId]) && $propertyDefinition['definition_style_id'] != $styleId)
                {
                    
$dw XenForo_DataWriter::create('XenForo_DataWriter_StyleProperty');
                    
$dw->setOption(XenForo_DataWriter_StyleProperty::OPTION_REBUILD_CACHEfalse);
                    
$dw->setExtraData(XenForo_DataWriter_StyleProperty::DATA_DEFINITION$propertyDefinition);
                    
$dw->setExistingData($existingPropertytrue);
                    
$dw->delete();
                    continue;
                }
            }
            else
            {
                
$existingProperty false;
            }

            
$this->updateStylePropertyValue(
                
$propertyDefinition$styleId$propertyValue,
                array(
XenForo_DataWriter_StyleProperty::OPTION_REBUILD_CACHE => false),
                
$existingProperty$propertyDefinition['property_value'// this is the effective value
            
);
        }

        
$this->rebuildPropertyCacheInStyleAndChildren($styleId);

        
XenForo_Db::commit($this->_getDb());
    }

    
/**
     * Get the specified style property definition by ID. Includes default
     * property value.
     *
     * @param integer $propertyDefinitionId
     *
     * @return array|false
     */
    
public function getStylePropertyDefinitionById($propertyDefinitionId)
    {
        return 
$this->_getDb()->fetchRow('
            SELECT property_definition.*,
                property.property_value
            FROM xf_style_property_definition AS property_definition
            LEFT JOIN xf_style_property AS property ON
                (property.property_definition_id = property_definition.property_definition_id
                AND property.style_id = property_definition.definition_style_id)
            WHERE property_definition.property_definition_id = ?
        '
$propertyDefinitionId);
    }

    
/**
     * Gets the specified style property definition by its name and definition
     * style ID. Includes default property value.
     *
     * @param string $name
     * @param integer $styleId
     *
     * @return array|false
     */
    
public function getStylePropertyDefinitionByNameAndStyle($name$styleId)
    {
        return 
$this->_getDb()->fetchRow('
            SELECT property_definition.*,
                property.property_value
            FROM xf_style_property_definition AS property_definition
            LEFT JOIN xf_style_property AS property ON
                (property.property_definition_id = property_definition.property_definition_id
                AND property.style_id = property_definition.definition_style_id)
            WHERE property_definition.property_name = ?
                AND property_definition.definition_style_id = ?
        '
, array($name$styleId));
    }

    
/**
     * Get the specified style property definitions by their IDs.
     * Includes default property value.
     *
     * @param array $propertyDefinitionIds
     *
     * @return array Format: [property definition id] => info
     */
    
public function getStylePropertyDefinitionsByIds(array $propertyDefinitionIds)
    {
        if (!
$propertyDefinitionIds)
        {
            return array();
        }

        return 
$this->fetchAllKeyed('
            SELECT property_definition.*,
                property.property_value
            FROM xf_style_property_definition AS property_definition
            LEFT JOIN xf_style_property AS property ON
                (property.property_definition_id = property_definition.property_definition_id
                AND property.style_id = property_definition.definition_style_id)
            WHERE property_definition.property_definition_id IN (' 
$this->_getDb()->quote($propertyDefinitionIds) . ')
        '
'property_definition_id');
    }

    
/**
     * Get the specified style property definitions by their property names.
     * Does not include default property value.
     *
     * @param string $searchText
     * @param array $propertyNames
     *
     * @return array
     */
    
public function getStylePropertyDefinitionsForAdminQuickSearch($searchText, array $propertyNames)
    {
        
$searchStringSql 'CONVERT(property_name USING utf8) LIKE ' XenForo_Db::quoteLike($searchText'lr');

        if (!empty(
$propertyNames))
        {
            
$sqlConditions '(' $searchStringSql ' OR property_name IN (' $this->_getDb()->quote($propertyNames) . '))';
        }
        else
        {
            
$sqlConditions $searchStringSql;
        }

        return 
$this->fetchAllKeyed('
            SELECT *
            FROM xf_style_property_definition
            WHERE ' 
$sqlConditions,
        
'property_name');
    }

    
/**
     * Returns an array of all style property definitions in the specified group
     *
     * @param string $groupId
     * @param integer|null $styleId If specified, limits to definitions in a specified style
     *
     * @return array
     */
    
public function getStylePropertyDefinitionsByGroup($groupName$styleId null)
    {
        
$properties $this->fetchAllKeyed('
            SELECT *
            FROM xf_style_property_definition
            WHERE group_name = ?
                ' 
. ($styleId !== null 'AND definition_style_id = ' $this->_getDb()->quote($styleId) : '') . '
            ORDER BY display_order
        '
'property_name'$groupName);

        return 
$properties;
    }

    
/**
     * Create of update a style property definition. Input data
     * is named after fields in the style property definition, as well as
     * property_value_scalar and property_value_css.
     *
     * @param integer $definitionId Definition to update; if 0, creates a new one
     * @param array $input List of data from input to change in definition
     *
     * @return array Definition info after saving
     */
    
public function createOrUpdateStylePropertyDefinition($definitionId, array $input)
    {
        
XenForo_Db::beginTransaction($this->_getDb());

        
$dw XenForo_DataWriter::create('XenForo_DataWriter_StylePropertyDefinition');
        if (
$definitionId)
        {
            
$dw->setExistingData($definitionId);
        }
        else
        {
            
$dw->set('definition_style_id'$input['definition_style_id']);
        }
        
$dw->set('group_name'$input['group_name']);
        
$dw->set('property_name'$input['property_name']);
        
$dw->set('title'$input['title']);
        
$dw->set('description'$input['description']);
        
$dw->set('property_type'$input['property_type']);
        
$dw->set('css_components'$input['css_components']);
        
$dw->set('scalar_type'$input['scalar_type']);
        
$dw->set('scalar_parameters'$input['scalar_parameters']);
        
$dw->set('display_order'$input['display_order']);
        
$dw->set('sub_group'$input['sub_group']);
        
$dw->set('addon_id'$input['addon_id']);
        
$dw->save();

        
$definition $dw->getMergedData();
        if (
$input['property_type'] == 'scalar')
        {
            
$propertyValue $input['property_value_scalar'];

            
$newPropertyValue $this->updateStylePropertyValue(
                
$definition$definition['definition_style_id'], $propertyValue
            
);
        }
        else
        {
            
$propertyValue $input['property_value_css'];
            if (
$definitionId && !$dw->isChanged('property_type'))
            {
                
// TODO: update value when possible
            
}
            else
            {
                
$newPropertyValue $this->updateStylePropertyValue(
                    
$definition$definition['definition_style_id'], array()
                );
            }
        }

        
XenForo_Db::commit($this->_getDb());

        return 
$definition;
    }

    
/**
     * Prepares the title and description and group name phrases for a style property
     *
     * @param array $property
     *
     * @return array
     */
    
public function prepareStylePropertyPhrases(array $property)
    {
        
$property['masterTitle'] = $property['title'];
        if (
$property['addon_id'])
        {
            
$property['title'] = new XenForo_Phrase($this->getStylePropertyTitlePhraseName($property));
        }

        
$property['masterDescription'] = $property['description'];
        if (
$property['addon_id'])
        {
            
$property['description'] = new XenForo_Phrase($this->getStylePropertyDescriptionPhraseName($property));
        }

        
$fakeGroup = array(
            
'group_name' => $property['group_name'],
            
'group_style_id' => $property['definition_style_id']
        );
        
$property['masterGroupName'] = new XenForo_Phrase($this->getStylePropertyGroupTitlePhraseName($fakeGroup));

        return 
$property;
    }

    
/**
     * Prepares a style property (or definition) for display.
     *
     * @param array $property
     * @param integer|null $displayStyleId The ID of the style the properties are being edited in
     *
     * @return array Prepared version
     */
    
public function prepareStyleProperty(array $property$displayStyleId null)
    {
        
$property $this->prepareStylePropertyPhrases($property);

        
$property['cssComponents'] = unserialize($property['css_components']);

        if (
$property['property_type'] == 'scalar')
        {
            
$property['propertyValueScalar'] = $property['property_value'];
            
$property['propertyValueCss'] = array();
        }
        else
        {
            
$property['propertyValueScalar'] = '';
            
$property['propertyValueCss'] = unserialize($property['property_value']);
        }

        if (
$displayStyleId === null)
        {
            
$property['canEditDefinition'] = $this->canEditStylePropertyDefinition($property['definition_style_id']);
        }
        else
        {
            
$property['canEditDefinition'] = (
                
$this->canEditStylePropertyDefinition($property['definition_style_id'])
                && 
$property['definition_style_id'] == $displayStyleId
            
);
        }

        
$property['canReset'] = (
            isset(
$property['effectiveState'])
            && 
$property['effectiveState'] == 'customized'
        
);

        return 
$property;
    }

    
/**
     * Prepares a list of style properties.
     *
     * @param array $properties
     * @param integer|null $displayStyleId The ID of the style the properties are being edited in
     *
     * @return array
     */
    
public function prepareStyleProperties(array $properties$displayStyleId null)
    {
        foreach (
$properties AS &$property)
        {
            
$property $this->prepareStyleProperty($property$displayStyleId);
        }

        return 
$properties;
    }

    
/**
     * Gets the name of the style property title phrase.
     *
     * @param array $property
     *
     * @return string
     */
    
public function getStylePropertyTitlePhraseName(array $property)
    {
        switch (
$property['definition_style_id'])
        {
            case -
1$suffix 'admin'; break;
            case 
0:  $suffix 'master'; break;
            default: return 
'';
        }

        return 
"style_property_$property[property_name]_$suffix";
    }

    
/**
     * Gets the name of the style property description phrase.
     *
     * @param array $property
     *
     * @return string
     */
    
public function getStylePropertyDescriptionPhraseName(array $property)
    {
        switch (
$property['definition_style_id'])
        {
            case -
1$suffix 'admin'; break;
            case 
0:  $suffix 'master'; break;
            default: return 
'';
        }

        return 
"style_property_$property[property_name]_description_$suffix";
    }

    
/**
     * Gets the default style property definition.
     *
     * @param integer $styleId
     * @param string $groupName
     *
     * @return array
     */
    
public function getDefaultStylePropertyDefinition($styleId$groupName '')
    {
        
$components = array(
            
'text' => true,
            
'background' => true,
            
'border' => true,
            
'layout' => true,
            
'extra' => true
        
);

        return array(
            
'definition_style_id' => $styleId,
            
'group_name' => $groupName,
            
'title' => '',
            
'property_name' => '',
            
'property_type' => 'css',
            
'property_value' => '',
            
'css_components' => serialize($components),
            
'display_order' => 1,
            
'sub_group' => '',
            
'addon_id' => null,

            
'cssComponents' => $components,
            
'propertyValueScalar' => '',
            
'propertyValueCss' => array(),
            
'masterTitle' => ''
        
);
    }

    
/**
     * Determines if a style property definition can be edited in
     * the specified style.
     *
     * @param integer $styleId
     *
     * @return boolean
     */
    
public function canEditStylePropertyDefinition($styleId)
    {
        return 
XenForo_Application::debugMode();
    }

    
/**
     * Determines if a style property can be edited in the specified style.
     *
     * @param integer $styleId
     *
     * @return boolean
     */
    
public function canEditStyleProperty($styleId)
    {
        if (
$styleId 0)
        {
            return 
true;
        }
        else
        {
            return 
XenForo_Application::debugMode();
        }
    }

    
/**
     * Get the style property development directory based on the style a property/definition
     * is defined in. Returns an empty string for non-master properties.
     *
     * @param integer $styleId
     *
     * @return string
     */
    
public function getStylePropertyDevelopmentDirectory($styleId)
    {
        if (
$styleId 0)
        {
            return 
'';
        }

        
$config XenForo_Application::get('config');
        if (!
$config->debug || !$config->development->directory)
        {
            return 
'';
        }

        if (
$styleId == 0)
        {
            return 
XenForo_Application::getInstance()->getRootDir()
                . 
'/' $config->development->directory '/file_output/style_properties';
        }
        else
        {
            return 
XenForo_Application::getInstance()->getRootDir()
                . 
'/' $config->development->directory '/file_output/admin_style_properties';
        }
    }

    
/**
     * Gets the full path to a specific style property development file.
     * Ensures directory is writable.
     *
     * @param string $propertyName
     * @param integer $styleId
     *
     * @return string
     */
    
public function getStylePropertyDevelopmentFileName($propertyName$styleId)
    {
        
$dir $this->getStylePropertyDevelopmentDirectory($styleId);
        if (!
$dir)
        {
            throw new 
XenForo_Exception('Tried to write non-master/admin style property value to development directory, or debug mode is not enabled');
        }
        if (!
is_dir($dir) || !is_writable($dir))
        {
            throw new 
XenForo_Exception("Style property development directory $dir is not writable");
        }

        return (
$dir '/' $propertyName '.xml');
    }

    
/**
     * Writes out a style property development file.
     *
     * @param array $definition Property definition
     * @param array $property Property value
     */
    
public function writeStylePropertyDevelopmentFile(array $definition, array $property)
    {
        if (
$definition['addon_id'] != 'XenForo' && $property['style_id'] > 0)
        {
            return;
        }

        
$fileName $this->getStylePropertyDevelopmentFileName($definition['property_name'], $property['style_id']);

        
// TODO: in the future, the writing system could be split into writing out definition and values in separate functions.
        //         it would make a clearer code path.

        
$document = new DOMDocument('1.0''utf-8');
        
$document->formatOutput true;

        
$node $document->createElement('property');
        
$document->appendChild($node);

        
$includeDefinition = ($definition['definition_style_id'] == $property['style_id']);
        if (
$includeDefinition)
        {
            
$node->setAttribute('definition'1);
            
$node->setAttribute('group_name'$definition['group_name']);
            
$node->setAttribute('property_type'$definition['property_type']);
            
$node->setAttribute('scalar_type'$definition['scalar_type']);
            
$node->setAttribute('scalar_parameters'$definition['scalar_parameters']);

            
$components unserialize($definition['css_components']);
            
$node->setAttribute('css_components'implode(','array_keys($components)));

            
$node->setAttribute('display_order'$definition['display_order']);
            
$node->setAttribute('sub_group'$definition['sub_group']);

            
XenForo_Helper_DevelopmentXml::createDomElements($node, array(
                
'title' => isset($definition['masterTitle']) ? $definition['masterTitle'] : strval($definition['title']),
                
'description' => isset($definition['masterDescription']) ? $definition['masterDescription'] : strval($definition['description'])
            ));
        }
        else
        {
            
$node->setAttribute('property_type'$definition['property_type']);
        }

        
$valueNode $document->createElement('value');
        if (
$definition['property_type'] == 'scalar')
        {
            
$valueNode->appendChild($document->createCDATASection($property['property_value']));
        }
        else
        {
            
$jsonValue json_encode(unserialize($property['property_value']));
            
// format one value per line
            
$jsonValue preg_replace('/(?<!:|\\)","/''",' "n" '"'$jsonValue);

            
$valueNode->appendChild($document->createCDATASection($jsonValue));
        }
        
$node->appendChild($valueNode);

        
$document->save($fileName);
    }

    
/**
     * Moves a style property development file, for renames.
     *
     * @param array $oldDefinition
     * @param array $newDefinition
     */
    
public function moveStylePropertyDevelopmentFile(array $oldDefinition, array $newDefinition)
    {
        if (
$oldDefinition['addon_id'] != 'XenForo' || $oldDefinition['definition_style_id'] > 0)
        {
            return;
        }

        if (
$newDefinition['definition_style_id'] > 0)
        {
            
$this->deleteStylePropertyDevelopmentFile($oldDefinition['property_name'], $oldDefinition['definition_style_id']);
            return;
        }

        
$oldFile $this->getStylePropertyDevelopmentFileName($oldDefinition['property_name'], $oldDefinition['definition_style_id']);
        
$newFile $this->getStylePropertyDevelopmentFileName($newDefinition['property_name'], $newDefinition['definition_style_id']);

        if (
file_exists($oldFile))
        {
            
rename($oldFile$newFile);
        }
    }

    
/**
     * Updates the definition-related elements of the style property development file
     * without touching the property value.
     *
     * @param array $definition
     */
    
public function updateStylePropertyDevelopmentFile(array $definition)
    {
        if (
$definition['addon_id'] != 'XenForo' || $definition['definition_style_id'] > 0)
        {
            return;
        }

        
$fileName $this->getStylePropertyDevelopmentFileName($definition['property_name'], $definition['definition_style_id']);

        if (
file_exists($fileName))
        {
            
$document = new SimpleXMLElement($fileName0true);

            if ((string)
$document['definition'])
            {
                
$value = (string)$document->value;
                if ((string)
$document['property_type'] == 'css')
                {
                    
$value serialize(json_decode($valuetrue));
                }

                
$property $definition + array(
                    
'style_id' => $definition['definition_style_id'],
                    
'property_value' => $value
                
);

                
$this->writeStylePropertyDevelopmentFile($definition$property);
            }
        }
    }

    
/**
     * Deletes a style property development file.
     *
     * @param string $name
     * @param integer $styleId Definition style ID.
     */
    
public function deleteStylePropertyDevelopmentFile($name$styleId)
    {
        if (
$styleId 0)
        {
            return;
        }

        
$fileName $this->getStylePropertyDevelopmentFileName($name$styleId);
        if (
file_exists($fileName))
        {
            
unlink($fileName);
        }
    }

    
/**
     * Deletes the style property file if needed. This is used when reverting a
     * customized property; the file is only deleted if we're deleting the property
     * from a style other than the one it was created in.
     *
     * @param array $definition
     * @param array $property
     */
    
public function deleteStylePropertyDevelopmentFileIfNeeded(array $definition, array $property)
    {
        if (
$property['style_id'] > 0)
        {
            return;
        }
        if (
$property['style_id'] == $definition['definition_style_id'])
        {
            
// assume this will be deleted by the definition
            
return;
        }

        
$this->deleteStylePropertyDevelopmentFile($definition['property_name'], $property['style_id']);
    }

    
/**
     * Imports style properties/groups from the development location. This only imports
     * one style's worth of properties at a time.
     *
     * @param integer $styleId Style to import for
     */
    
public function importStylePropertiesFromDevelopment($styleId)
    {
        if (
$styleId 0)
        {
            return;
        }

        
$dir $this->getStylePropertyDevelopmentDirectory($styleId);
        if (!
$dir)
        {
            return;
        }

        if (!
is_dir($dir))
        {
            throw new 
XenForo_Exception("Style property development directory doesn't exist");
        }

        
$files glob("$dir/*.xml");

        
$newProperties = array();
        
$newGroups = array();
        foreach (
$files AS $fileName)
        {
            
$name basename($fileName'.xml');
            
$document = new SimpleXMLElement($fileName0true);

            if (
substr($name06) == 'group.')
            {
                
$newGroups[] = array(
                    
'group_name'    => substr($name6),
                    
'title'         => (string)$document->title,
                    
'description'   => (string)$document->description,
                    
'display_order' => (string)$document['display_order']
                );
            }
            else
            {
                if ((string)
$document['definition'])
                {
                    
$property = array(
                        
'title'             => (string)$document->title,
                        
'description'       => (string)$document->description,
                        
'definition'        => (string)$document['definition'],
                        
'group_name'        => (string)$document['group_name'],
                        
'property_type'     => (string)$document['property_type'],
                        
'scalar_type'       => (string)$document['scalar_type'],
                        
'scalar_parameters' => (string)$document['scalar_parameters'],
                        
'display_order'     => (string)$document['display_order'],
                        
'sub_group'         => (string)$document['sub_group']
                    );

                    
$components = (string)$document['css_components'];
                    if (
$components)
                    {
                        
$property['css_components'] = array_fill_keys(explode(','$components), true);
                    }
                    else
                    {
                        
$property['css_components'] = array();
                    }
                }
                else
                {
                    
$property = array(
                        
'property_type' => (string)$document['property_type']
                    );
                }

                
$property['property_name'] = $name;

                if (
$property['property_type'] == 'scalar')
                {
                    
$property['property_value'] = (string)$document->value;
                }
                else
                {
                    
$property['property_value'] = json_decode((string)$document->valuetrue);
                }

                
$newProperties[] = $property;
            }
        }

        
$this->importStylePropertiesFromArray($newProperties$newGroups$styleId'XenForo');
    }

    
/**
     * Deletes the style properties and definitions in a style (and possibly limited to an add-on).
     *
     * @param integer $styleId Style to delete from. 0 for master, -1 for admin
     * @param string|null $addOnId If not null, limits deletions to an ad-on
     * @param boolean $leaveChildCustomizations If true, child customizations of a deleted definition will be left
     */
    
public function deleteStylePropertiesAndDefinitionsInStyle($styleId$addOnId null$leaveChildCustomizations false)
    {
        
$properties $this->getStylePropertiesInStyles(array($styleId));

        
$delPropertyIds = array();
        
$delPropertyDefinitionIds = array();
        foreach (
$properties AS $property)
        {
            if (
$addOnId === null || $property['addon_id'] == $addOnId)
            {
                if (
$property['definition_style_id'] == $styleId)
                {
                    
$delPropertyDefinitionIds[] = $property['property_definition_id'];
                }
                else if (
$property['style_id'] == $styleId)
                {
                    
$delPropertyIds[] = $property['property_id'];
                }
            }
        }

        if (
$delPropertyIds)
        {
            
$this->_db->delete('xf_style_property',
                
'property_id IN (' $this->_db->quote($delPropertyIds) . ')'
            
);
        }

        if (
$delPropertyDefinitionIds)
        {
            
$this->_db->delete('xf_style_property_definition',
                
'property_definition_id IN (' $this->_db->quote($delPropertyDefinitionIds) . ')'
            
);

            if (
$leaveChildCustomizations)
            {
                
$this->_db->delete('xf_style_property',
                    
'property_definition_id IN (' $this->_db->quote($delPropertyDefinitionIds) . ')
                    AND style_id = ' 
$this->_db->quote($styleId)
                );
            }
            else
            {
                
$this->_db->delete('xf_style_property',
                    
'property_definition_id IN (' $this->_db->quote($delPropertyDefinitionIds) . ')'
                
);
            }
        }
    }

    
/**
     * Delete the style property groups in the specified style, matching the add-on if provided.
     *
     * @param integer $styleId Style to delete from. 0 for master, -1 for admin
     * @param string|null $addOnId If not null, limits deletions to an ad-on
     */
    
public function deleteStylePropertyGroupsInStyle($styleId$addOnId null)
    {
        
$db $this->_getDb();
        if (
$addOnId === null)
        {
            
$db->delete('xf_style_property_group''group_style_id = ' $db->quote($styleId));
        }
        else
        {
            
$db->delete('xf_style_property_group',
                
'group_style_id = ' $db->quote($styleId) . ' AND addon_id = ' $db->quote($addOnId)
            );
        }
    }

    
/**
     * Appends the style property list to an XML document.
     *
     * @param DOMElement $rootNode Node to append to
     * @param integer $styleId Style to read values/definitions from
     * @param string|null $addOnId If not null, limits to values/definitions in the specified add-on
     * @param boolean $independent If true, exports all customized properties to the root
     */
    
public function appendStylePropertyXml(DOMElement $rootNode$styleId$addOnId null$independent false)
    {
        
$document $rootNode->ownerDocument;

        if (!
$styleId)
        {
            
$independent false;
        }

        if (
$independent)
        {
            
$properties $this->getEffectiveStylePropertiesInStyle($styleId);
        }
        else
        {
            
$properties $this->getStylePropertiesInStyles(array($styleId));
        }
        
ksort($properties);

        foreach (
$properties AS $property)
        {
            if (
$addOnId !== null && $property['addon_id'] !== $addOnId)
            {
                continue;
            }

            if (
$independent && $property['definition_style_id'] == && $property['effectiveState'] == 'default')
            {
                continue;
            }

            
$node $document->createElement('property');
            
$node->setAttribute('property_name'$property['property_name']);
            
$node->setAttribute('property_type'$property['property_type']);

            if ((
$independent && $property['definition_style_id']) || $property['definition_style_id'] == $styleId)
            {
                
$node->setAttribute('definition'1);
                
$node->setAttribute('group_name'$property['group_name']);
                
$node->setAttribute('title', isset($property['masterTitle']) ? $property['masterTitle'] : strval($property['title']));
                
$node->setAttribute('description', isset($property['masterDescription']) ? $property['masterDescription'] : strval($property['description']));

                
$components unserialize($property['css_components']);
                
$node->setAttribute('css_components'implode(','array_keys($components)));
                
$node->setAttribute('scalar_type'$property['scalar_type']);
                
$node->setAttribute('scalar_parameters'$property['scalar_parameters']);
                
$node->setAttribute('display_order'$property['display_order']);
                
$node->setAttribute('sub_group'$property['sub_group']);
            }

            if (
$property['property_type'] == 'scalar')
            {
                
$node->appendChild($document->createCDATASection($property['property_value']));
            }
            else
            {
                
$node->appendChild($document->createCDATASection(json_encode(unserialize($property['property_value']))));
            }

            
$rootNode->appendChild($node);
        }

        if (
$independent)
        {
            
$groups $this->getEffectiveStylePropertyGroupsInStyle($styleId);
        }
        else
        {
            
$groups $this->getStylePropertyGroupsInStyles(array($styleId));
        }
        
ksort($groups);

        foreach (
$groups AS $group)
        {
            if (
$addOnId !== null && $group['addon_id'] !== $addOnId)
            {
                continue;
            }

            if (
$independent && $group['group_style_id'] == 0)
            {
                continue;
            }

            
$node $document->createElement('group');
            
$rootNode->appendChild($node);

            
$node->setAttribute('group_name'$group['group_name']);
            
$node->setAttribute('display_order'$group['display_order']);
            
XenForo_Helper_DevelopmentXml::createDomElements($node, array(
                
'title' => $group['title'],
                
'description' => $group['description']
            ));
        }
    }

    
/**
     * Gets the style property development XML.
     *
     * @param integer $styleId
     *
     * @return DOMDocument
     */
    
public function getStylePropertyDevelopmentXml($styleId)
    {
        
$rootTag = ($styleId == -'admin_style_properties' 'style_properties');

        
$document = new DOMDocument('1.0''utf-8');
        
$document->formatOutput true;
        
$rootNode $document->createElement($rootTag);
        
$document->appendChild($rootNode);

        
$this->appendStylePropertyXml($rootNode$styleId'XenForo');

        return 
$document;
    }

    
/**
     * Imports the development admin navigation XML data.
     *
     * @param string $fileName File to read the XML from
     */
    
public function importStylePropertyDevelopmentXml($fileName$styleId)
    {
        
$document = new SimpleXMLElement($fileName0true);
        
$this->importStylePropertyXml($document$styleId'XenForo');
    }

    
/**
     * Imports style properties and definitions from XML.
     *
     * @param SimpleXMLElement $xml XML node to search within
     * @param integer $styleId Target style ID
     * @param string|null $addOnId If not null, target add-on for definitions; if null, add-on is ''
     */
    
public function importStylePropertyXml(SimpleXMLElement $xml$styleId$addOnId null)
    {
        if (
$xml->property === null)
        {
            return;
        }

        
$newProperties = array();
        foreach (
$xml->property AS $xmlProperty)
        {
            
$property = array(
                
'property_name' => (string)$xmlProperty['property_name'],
                
'group_name' => (string)$xmlProperty['group_name'],
                
'title' => (string)$xmlProperty['title'],
                
'description' => (string)$xmlProperty['description'],
                
'definition' => (string)$xmlProperty['definition'],
                
'property_type' => (string)$xmlProperty['property_type'],
                
'scalar_type' => (string)$xmlProperty['scalar_type'],
                
'scalar_parameters' => (string)$xmlProperty['scalar_parameters'],
                
'display_order' => (string)$xmlProperty['display_order'],
                
'sub_group' => (string)$xmlProperty['sub_group']
            );

            
$components = (string)$xmlProperty['css_components'];
            if (
$components)
            {
                
$components array_fill_keys(explode(','$components), true);
                
$property['css_components'] = $components;
            }
            else
            {
                
$property['css_components'] = array();
            }

            if (
$property['property_type'] == 'scalar')
            {
                
$property['property_value'] = (string)$xmlProperty;
            }
            else
            {
                
$property['property_value'] = json_decode((string)$xmlPropertytrue);
            }

            
$newProperties[] = $property;
        }

        
$newGroups = array();
        foreach (
$xml->group AS $xmlGroup)
        {
            
$newGroups[] = array(
                
'group_name' => (string)$xmlGroup['group_name'],
                
'title' => (string)$xmlGroup->title,
                
'description' => (string)$xmlGroup->description,
                
'display_order' => (string)$xmlGroup['display_order']
            );
        }

        
$this->importStylePropertiesFromArray($newProperties$newGroups$styleId$addOnId);
    }

    
/**
     * Imports style properties and definitions from an array.
     *
     * @param array $newProperties List of properties and definitions to import
     * @param array $newGroups List of groups to import
     * @param integer $styleId Target style ID
     * @param string|null $addOnId If not null, only replaces properties with this add-on; otherwise, all in style
     */
    
public function importStylePropertiesFromArray(array $newProperties, array $newGroups$styleId$addOnId null)
    {
        
// must be run before delete to keep values accessible
        
$existingProperties $this->keyPropertiesByName($this->getEffectiveStylePropertiesInStyle($styleId));

        
$addOnIdString = ($addOnId !== null $addOnId '');
        
$db $this->_getDb();

        
XenForo_Db::beginTransaction($db);
        
$this->deleteStylePropertiesAndDefinitionsInStyle($styleId$addOnIdtrue);
        
$this->deleteStylePropertyGroupsInStyle($styleId$addOnId);

        
// run after the delete to not include removed data
        
$existingGroups $this->getEffectiveStylePropertyGroupsInStyle($styleId);

        foreach (
$newGroups AS $group)
        {
            if (isset(
$existingGroups[$group['group_name']]))
            {
                continue;
            }

            
$dw XenForo_DataWriter::create('XenForo_DataWriter_StylePropertyGroup');
            
$dw->setOption(XenForo_DataWriter_StylePropertyGroup::OPTION_UPDATE_MASTER_PHRASEfalse);
            
$dw->setOption(XenForo_DataWriter_StylePropertyGroup::OPTION_UPDATE_DEVELOPMENTfalse);
            
$dw->bulkSet(array(
                
'group_name' => $group['group_name'],
                
'group_style_id' => $styleId,
                
'title' => $group['title'],
                
'description' => $group['description'],
                
'display_order' => $group['display_order'],
                
'addon_id' => $addOnIdString
            
));
            
$dw->save();
        }

        foreach (
$newProperties AS $property)
        {
            
$propertyName $property['property_name'];
            
$propertyValue $property['property_value'];

            
$definition null;
            
$deletedDefinitionId 0;
            
$existingProperty null;

            if (isset(
$existingProperties[$propertyName]))
            {
                
$definition $existingProperties[$propertyName];
                if (
$definition['definition_style_id'] == $styleId && ($addOnId === null || $definition['addon_id'] == $addOnId))
                {
                    
$deletedDefinitionId $definition['property_definition_id'];
                    
$definition null;
                }
            }

            if (!empty(
$property['definition']))
            {
                if (!
$definition)
                {
                    
$dw XenForo_DataWriter::create('XenForo_DataWriter_StylePropertyDefinition');
                    
$dw->setOption(XenForo_DataWriter_StylePropertyDefinition::OPTION_UPDATE_MASTER_PHRASEfalse);
                    
$dw->setOption(XenForo_DataWriter_StylePropertyDefinition::OPTION_UPDATE_DEVELOPMENTfalse);
                    
$dw->setOption(XenForo_DataWriter_StylePropertyDefinition::OPTION_CHECK_DUPLICATEfalse);
                    
$dw->bulkSet(array(
                        
'property_name' => $propertyName,
                        
'group_name' => $property['group_name'],
                        
'title' => $property['title'],
                        
'description' => $property['description'],
                        
'definition_style_id' => $styleId,
                        
'property_type' => $property['property_type'],
                        
'css_components' => $property['css_components'],
                        
'scalar_type' => $property['scalar_type'],
                        
'scalar_parameters' => $property['scalar_parameters'],
                        
'display_order' => $property['display_order'],
                        
'sub_group' => $property['sub_group'],
                        
'addon_id' => $addOnIdString
                    
));
                    
$dw->save();

                    
$definition $dw->getMergedData();

                    if (
$deletedDefinitionId)
                    {
                        
$db->update('xf_style_property', array(
                            
'property_definition_id' => $definition['property_definition_id']
                        ), 
'property_definition_id = ' $db->quote($deletedDefinitionId));
                    }

                    
$existingProperty false;
                }
            }
            else if (
$definition)
            {
                if (
$definition['style_id'] == $styleId && $addOnId !== null && $definition['addon_id'] !== $addOnId)
                {
                    
$existingProperty $definition;
                }
                else
                {
                    
$existingProperty false;
                }
            }
            else
            {
                
// non-definition and no matching definition
                
continue;
            }

            
$this->updateStylePropertyValue(
                
$definition$styleId$propertyValue,
                array(
                    
XenForo_DataWriter_StyleProperty::OPTION_REBUILD_CACHE => false,
                    
XenForo_DataWriter_StyleProperty::OPTION_UPDATE_DEVELOPMENT => false
                
),
                
$existingProperty
            
);
        }

        
$this->rebuildPropertyCacheInStyleAndChildren($styleId);

        
XenForo_Db::commit($db);
    }

    
/**
     * Replaces {xen:property} references in a template with the @ property version.
     * This allows for easier editing and viewing of properties.
     *
     * @param string $templateText Template with {xen:property} references
     * @param integer $editStyleId The style the template is being edited in
     * @param array|null $properties A list of valid style properties; if null, grabbed automatically ([name] => property)
     * @param boolean $simpleOnly If true, don't expand into extended CSS output
     *
     * @return string Replaced template text
     */
    
public function replacePropertiesInTemplateForEditor($templateText$editStyleId, array $properties null$simpleOnly false)
    {
        if (
$properties === null)
        {
            
$properties $this->keyPropertiesByName($this->getEffectiveStylePropertiesInStyle($editStyleId));
        }

        
$validComponents = array('font''background''padding''margin''border''extra');

        if (!
$simpleOnly)
        {
            
preg_match_all('#(?P<leading_space>[ t]*){xen:propertys+("|'|)(?P<property>[a-z0-9._-]+)\2s*}#si', $templateText, $matches, PREG_SET_ORDER);
            
foreach ($matches AS $match)
            {
                
$propertyReference $match['property'];
                
$parts explode('.'$propertyReference2);

                
$propertyName $parts[0];
                
$propertyComponent = (count($parts) == $parts[1] : false);

                if (!isset(
$properties[$propertyName]) || $properties[$propertyName]['property_type'] == 'scalar')
                {
                    continue;
                }

                if (
$propertyComponent && !in_array($propertyComponent$validComponents))
                {
                    continue;
                }

                
$propertyValue unserialize($properties[$propertyName]['property_value']);
                
$outputValue $propertyValue;

                
$outputValue $this->compileCssProperty_sanitize($outputValue$propertyValue);
                
$outputValue $this->compileCssProperty_compileRules($outputValue$propertyValue);
                
$outputValue $this->compileCssProperty_cleanUp($outputValue$propertyValue);

                
$replacementRules '';
                if (
$propertyComponent)
                {
                    if (isset(
$outputValue[$propertyComponent]))
                    {
                        
$replacementRules $outputValue[$propertyComponent];
                    }
                }
                else
                {
                    foreach (
$validComponents AS $validComponent)
                    {
                        if (isset(
$outputValue[$validComponent]))
                        {
                            
$replacementRules .= "n" $outputValue[$validComponent];
                        }
                    }
                    if (isset(
$outputValue['width']))
                    {
                        
$replacementRules .= "nwidth: $outputValue[width];";
                    }
                    if (isset(
$outputValue['height']))
                    {
                        
$replacementRules .= "nheight: $outputValue[height];";
                    }
                }

                
$leadingSpace $match['leading_space'];

                
$replacementRules preg_replace('#(^|;(r?n)+)[ ]*([*a-z0-9_/-]+):s*#i'"\1{$leadingSpace}\3: "trim($replacementRules));

                
$replacement $leadingSpace '@property "' $propertyReference '";'
                    
"n" $replacementRules
                    
"n" $leadingSpace '@property "/' $propertyReference '";';

                
$templateText str_replace($match[0], $replacement$templateText);
            }
        }

        
self::$_tempProperties $properties;
        
$templateText preg_replace_callback(
            
'/{xen:propertys+("|'|)(?P<propertyName>[a-z0-9_]+)(?P<propertyComponent>.[a-z0-9._-]+)?\1s*}(?![a-z0-9._-])/si',
            array('
self', '_propertyToAtScalarCallback'),
            $templateText
        );
        self::$_tempProperties = null;

        return $templateText;
    }

    protected static function _propertyToAtScalarCallback(array $match)
    {
        $match['
propertyName'] = $match[2];
        $match['
propertyComponent'] = isset($match[3]) ? $match[3] : null;

        if (!in_array(strtolower($match['
propertyName']), XenForo_DataWriter_StylePropertyDefinition::$reservedNames)
            && isset(self::$_tempProperties[$match['
propertyName']])
        )
        {
            return '
@' . $match['propertyName'] . (empty($match['propertyComponent']) ? '' : $match['propertyComponent']);
        }
        else
        {
            return $match[0];
        }
    }

    protected static function _atToPropertyCallback($property)
    {
        $name = $property[1];

        if (!isset(self::$_tempProperties[$name]))
        {
            return $property[0];
        }

        if (!in_array(strtolower($name), XenForo_DataWriter_StylePropertyDefinition::$reservedNames))
        {
            return '
{xen:property ' . $name . (empty($property[2]) ? '' : $property[2]) . '}';
        }

        return $property[0];
    }

    /**
     * Converts @propertyName to {xen:property propertyName}
     *
     * @param string $text
     * @param array $properties
     *
     * @return string
     */
    public function convertAtPropertiesToTemplateSyntax($text, array $properties)
    {
        self::$_tempProperties = $properties;
        $text = preg_replace_callback(
            '
/(?<=[^a-z0-9_]|^)@([a-z0-9_]+)(.[a-z0-9._-]+)?/si',
            array('
self', '_atToPropertyCallback'),
            $text
        );
        self::$_tempProperties = null;

        return $text;
    }

    /**
     * Converts @propertyName to {xen:property propertyName} for search queries
     *
     * @param string $text
     * @param array $properties
     *
     * @return string
     */
    public function convertAtPropertiesForSearch($text, array $properties)
    {
        self::$_tempProperties = $properties;
        $text = preg_replace_callback(
            '
/(?<=[^a-z0-9_]|^)@([a-z0-9_]+)(.[a-z0-9._-]+)?/si',
            array('
self', '_atToPropertyCallback'),
            $text
        );
        $text = preg_replace_callback(
            '
/@propertys*"?([a-z0-9_]+)(.[a-z0-9._-]+)?"?;?/i',
            array('
self', '_atToPropertyCallback'),
            $text
        );
        self::$_tempProperties = null;

        return $text;
    }

    /**
     * Translates @ property style references from the template editor into a structured array,
     * and rewrites the template text to the standard {xen:property} format.
     *
     * @param string $templateText Template with @ property references
     * @param string $outputText By reference, the template with {xen:property} values instead
     * @param array $properties A list of valid style properties in the correct style; keyed by named
     *
     * @return array Property values from the template text. Change detection still needs to be run.
     */
    public function translateEditorPropertiesToArray($templateText, &$outputText, array $properties)
    {
        // replace @property '
foo'; .... @property '/foo'; with {xen:property foo}
        $outputText = $templateText;
        $outputProperties = array();

        preg_match_all('
/
            @
propertys+("|')(?P<name>[a-z0-9._-]+)\1;
            (?P<rules>([^@]*?|@(?!property))*)
            @propertys+("
|')/(?P=name)\5;
            /siUx'
$templateText$matchesPREG_SET_ORDER
        
);
        foreach (
$matches AS $match)
        {
            
// replace {xen:property propertyName} with @propertyName
            
$match['rules'] = preg_replace('/{xen:property ('|"|)(w+)(1)}/siU', '@2', $match['rules']);

            
$parts = explode('.', $match['name'], 2);
            
$propertyName = $parts[0];
            
$propertyComponent = (count($parts) == 2 ? $parts[1] : false);
            if (
$propertyComponent == 'font')
            {
                
$propertyComponent = 'text';
            }
            else if (
$propertyComponent == 'margin' || $propertyComponent == 'padding')
            {
                
$propertyComponent = 'layout';
            }

            if (!isset(
$properties[$propertyName]) || $properties[$propertyName]['property_type'] != 'css')
            {
                continue;
            }

            
$validComponents = unserialize($properties[$propertyName]['css_components']);
            if (
$propertyComponent && !isset($validComponents[$propertyComponent]))
            {
                continue;
            }

            
$set = array(
                'name' => 
$propertyName,
                'component' => 
$propertyComponent,
                'rules' => array()
            );
            
$extra = array();
            
$nonPropertyRules = array();
            
$paddingValues = array();
            
$marginValues = array();

            
$comments = array();
            preg_match_all('#/*(.+)(*/|$)#siU', 
$match['rules'], $commentMatches, PREG_SET_ORDER);
            foreach (
$commentMatches AS $commentMatch)
            {
                
$comments[] = $commentMatch[1];
                
$match['rules'] = str_replace($commentMatch[0], '', $match['rules']);
            }

            preg_match_all('/
                (?<=^|s|;)(?P<name>[a-z0-9-_*.]+)
                s*:s*
                (?P<value>[^;]*)
                (;|$)
                /siUx', 
$match['rules'], $ruleMatches, PREG_SET_ORDER
            );

            
$unparsableElements = $match['rules'];
            foreach (
$ruleMatches AS $ruleMatch)
            {
                
$unparsableElements = str_replace($ruleMatch[0], '', $unparsableElements);
            }
            
$unparsableElements = trim($unparsableElements);
            if (
$unparsableElements)
            {
                
$extra[] = $unparsableElements;
            }

            foreach (
$ruleMatches AS $ruleMatch)
            {
                
$value = trim($ruleMatch['value']);
                if (
$value === '')
                {
                    continue;
                }

                
$name = strtolower($ruleMatch['name']);

                switch (
$name)
                {
                    case 'color':
                    case 'text-decoration':
                        
$group = 'text';
                        break;

                    case 'width':
                    case 'height':
                        
$group = 'layout';
                        break;

                    default:
                        
$regex = '/^('
                            . 'font|font-(family|size|style|variant|weight)'
                            . '|background|background-(color|image|position|repeat)'
                            . '|padding|padding-.*|margin|margin-.*'
                            . '|border|border-.*-radius|border(-(top|right|bottom|left))?(-(color|style|width|radius))?'
                            . ')$/';
                        if (preg_match(
$regex$name$nameMatch))
                        {
                            
$ruleParts = explode('-', $nameMatch[1], 2);
                            
$group = $ruleParts[0];
                        }
                        else
                        {
                            
$group = 'extra';
                        }
                }

                // css references font, but the css components list references text
                if (
$group == 'font')
                {
                    
$group = 'text';
                }
                else if (
$group == 'padding' || $group == 'margin')
                {
                    
$group = 'layout';
                }

                if ((
$propertyComponent && $group != $propertyComponent) || !isset($validComponents[$group]))
                {
                    if (isset(
$validComponents['extra']) && (!$propertyComponent || $propertyComponent == 'extra'))
                    {
                        
$extra[] = $ruleMatch[0];
                    }
                    else
                    {
                        
$nonPropertyRules[] = $ruleMatch[0];
                    }
                }
                else
                {
                    
$isValidRule = false;

                    if (
$group == 'extra')
                    {
                        
$isValidRule = true;
                        
$extra[] = $ruleMatch[0];
                    }
                    else if (
$value == 'inherit')
                    {
                        
$isValidRule = false; // can't put inherit rules in properties
                        
$nonPropertyRules[] = $ruleMatch[0];
                    }
                    else
                    {
                        
$isValidRule = false;

                        switch (
$name)
                        {
                            case 'font':
                                
$ruleOutput = $this->parseFontCss($value);
                                if (is_array(
$ruleOutput))
                                {
                                    
$isValidRule = true;
                                    
$set['rules'] = array_merge($set['rules'], $ruleOutput);
                                }
                                break;

                            case 'font-weight':
                                
$isValidRule = true;
                                if (strtolower(
$value) == 'bold')
                                {
                                    
$set['rules']['font-weight'] = 'bold';
                                }
                                else
                                {
                                    
$extra['font-weight'] = $value;
                                }
                                break;

                            case 'font-style':
                                
$isValidRule = true;
                                if (strtolower(
$value) == 'italic')
                                {
                                    
$set['rules']['font-style'] = 'italic';
                                }
                                else
                                {
                                    
$extra['font-style'] = $value;
                                }
                                break;

                            case 'font-variant':
                                
$isValidRule = true;
                                if (strtolower(
$value) != 'normal')
                                {
                                    
$set['rules']['font-variant'] = 'small-caps';
                                }
                                else
                                {
                                    
$extra['font-variant'] = $value;
                                }
                                break;

                            case 'text-decoration':
                                
$isValidRule = true;

                                if (
$value == 'none')
                                {
                                    
$set['rules']['text-decoration'] = array('none' => 'none');
                                }
                                else
                                {
                                    
$decorations = preg_split('/s+/', strtolower($value), -1, PREG_SPLIT_NO_EMPTY);
                                    
$set['rules']['text-decoration'] = array_combine($decorations$decorations);
                                }
                                break;

                            case 'background':
                                
$ruleOutput = $this->parseBackgroundCss($value);
                                if (is_array(
$ruleOutput))
                                {
                                    
$isValidRule = true;
                                    
$set['rules'] = array_merge($set['rules'], $ruleOutput);
                                }
                                break;

                            case 'background-image':
                                if (preg_match('/^url(("
|'|)([^)]+)\1)$/iU'$value$imageMatch))
                                {
                                    
$set['rules']['background-image'] = $imageMatch[2];
                                    
$isValidRule true;
                                }
                                else if (
strtolower($value) == 'none')
                                {
                                    
$set['rules']['background-image'] = $value;
                                    
$isValidRule true;
                                }
                                break;

                            case 
'background-color':
                                if (
substr($value04) !== 'rgba')
                                {
                                    
$set['rules']['background-color'] = $value;
                                    
$isValidRule true;
                                }
                                break;

                            case 
'padding':
                                if (
$this->parsePaddingMarginCss($value$paddingValues))
                                {
                                    
$isValidRule true;
                                }
                                break;

                            case 
'padding-top':
                            case 
'padding-right':
                            case 
'padding-bottom':
                            case 
'padding-left':
                                
$paddingValues[substr($name8)] = $value;
                                unset(
$paddingValues['all']);
                                
$isValidRule true;
                                break;

                            case 
'margin':
                                if (
$this->parsePaddingMarginCss($value$marginValues))
                                {
                                    
$isValidRule true;
                                }
                                break;

                            case 
'margin-top':
                            case 
'margin-right':
                            case 
'margin-bottom':
                            case 
'margin-left':
                                
$marginValues[substr($name7)] = $value;
                                unset(
$marginValues['all']);
                                
$isValidRule true;
                                break;

                            case 
'border':
                            case 
'border-top':
                            case 
'border-right':
                            case 
'border-bottom':
                            case 
'border-left':
                                
$ruleOutput $this->parseBorderCss($value$name);
                                if (
is_array($ruleOutput))
                                {
                                    
$isValidRule true;
                                    
$set['rules'] = array_merge($set['rules'], $ruleOutput);
                                }
                                break;

                            case 
'border-color':
                            case 
'border-style':
                            case 
'border-width':
                                
$isValidRule true;
                                
$parts preg_split('/s+/'trim($value));
                                if (
count($parts) == 1)
                                {
                                    
$set['rules'][$name] = $value;
                                }
                                else
                                {
                                    
$suffix str_replace('border-'''$name);
                                    switch (
count($parts))
                                    {
                                        case 
2:
                                            
$set['rules']["border-top-$suffix"] = $parts[0];
                                            
$set['rules']["border-right-$suffix"] = $parts[1];
                                            
$set['rules']["border-bottom-$suffix"] = $parts[0];
                                            
$set['rules']["border-left-$suffix"] = $parts[1];
                                            break;

                                        case 
3:
                                            
$set['rules']["border-top-$suffix"] = $parts[0];
                                            
$set['rules']["border-right-$suffix"] = $parts[1];
                                            
$set['rules']["border-bottom-$suffix"] = $parts[2];
                                            
$set['rules']["border-left-$suffix"] = $parts[1];
                                            break;

                                        case 
4:
                                            
$set['rules']["border-top-$suffix"] = $parts[0];
                                            
$set['rules']["border-right-$suffix"] = $parts[1];
                                            
$set['rules']["border-bottom-$suffix"] = $parts[2];
                                            
$set['rules']["border-left-$suffix"] = $parts[3];
                                            break;

                                        default:
                                            
$isValidRule false;
                                    }
                                }
                                break;

                            case 
'border-radius':
                                if (
strpos($value'/') !== false)
                                {
                                    
// can't have 2 part radius rules in properties
                                    
break;
                                }

                                
$isValidRule true;
                                
$parts preg_split('/s+/'trim($value));
                                if (
count($parts) == 1)
                                {
                                    
$set['rules'][$name] = $value;
                                }
                                else
                                {
                                    switch (
count($parts))
                                    {
                                        case 
2:
                                            
$set['rules']["border-top-left-radius"] = $parts[0];
                                            
$set['rules']["border-top-right-radius"] = $parts[1];
                                            
$set['rules']["border-bottom-right-radius"] = $parts[0];
                                            
$set['rules']["border-bottom-left-radius"] = $parts[1];
                                            break;

                                        case 
3:
                                            
$set['rules']["border-top-left-radius"] = $parts[0];
                                            
$set['rules']["border-top-right-radius"] = $parts[1];
                                            
$set['rules']["border-bottom-right-radius"] = $parts[2];
                                            
$set['rules']["border-bottom-left-radius"] = $parts[1];
                                            break;

                                        case 
4:
                                            
$set['rules']["border-top-left-radius"] = $parts[0];
                                            
$set['rules']["border-top-right-radius"] = $parts[1];
                                            
$set['rules']["border-bottom-right-radius"] = $parts[2];
                                            
$set['rules']["border-bottom-left-radius"] = $parts[3];
                                            break;

                                        default:
                                            
$isValidRule false;
                                    }
                                }
                                break;

                            default:
                                
$isValidRule true;
                                
$set['rules'][$name] = $value;
                        }

                        if (!
$isValidRule)
                        {
                            if (isset(
$validComponents['extra']))
                            {
                                
$extra[] = $ruleMatch[0];
                            }
                            else
                            {
                                
$nonPropertyRules[] = $ruleMatch[0];
                            }
                        }
                    }
                }
            }

            if (
$paddingValues)
            {
                if (isset(
$paddingValues['all']))
                {
                    
$set['rules']['padding-all'] = $paddingValues['all'];
                }
                else
                {
                    foreach (array(
'top''right''bottom''left') AS $paddingSide)
                    {
                        if (isset(
$paddingValues[$paddingSide]))
                        {
                            
$set['rules']["padding-$paddingSide"] = $paddingValues[$paddingSide];
                        }
                    }
                }
            }
            if (
$marginValues)
            {
                if (isset(
$marginValues['all']))
                {
                    
$set['rules']['margin-all'] = $marginValues['all'];
                }
                else
                {
                    foreach (array(
'top''right''bottom''left') AS $marginSide)
                    {
                        if (isset(
$marginValues[$marginSide]))
                        {
                            
$set['rules']["margin-$marginSide"] = $marginValues[$marginSide];
                        }
                    }
                }
            }

            if (
$extra || $comments)
            {
                
$set['rules']['extra'] = '';

                if (
$extra)
                {
                    foreach (
$extra AS $extraRule => $extraValue)
                    {
                        if (
is_int($extraRule))
                        {
                            
$set['rules']['extra'] .= "n$extraValue"// whole rule
                        
}
                        else
                        {
                            
$set['rules']['extra'] .= "n$extraRule$extraValue;";
                        }
                    }
                }
                if (
$comments)
                {
                    foreach (
$comments AS $comment)
                    {
                        
$set['rules']['extra'] .= "n/*$comment*/";
                    }
                }

                
$set['rules']['extra'] = trim($set['rules']['extra']);

                if (!isset(
$validComponents['extra']))
                {
                    
$nonPropertyRules[] = $set['rules']['extra'];
                    unset(
$set['rules']['extra']);
                }
            }

            
$outputProperties[] = $set;

            
$replacement '{xen:property ' $match['name'] . '}';
            foreach (
$nonPropertyRules AS $nonPropertyRule)
            {
                
$replacement .= "nt$nonPropertyRule";
            }

            
$outputText str_replace($match[0], $replacement$outputText);
        }

        
$outputText $this->convertAtPropertiesToTemplateSyntax($outputText$properties);

        return 
$outputProperties;
    }

    
/**
     * Parses font shortcut CSS.
     *
     * @param string $value
     *
     * @return array|false List of property rules to apply or false if shortcut could not be parsed
     */
    
public function parseFontCss($value)
    {
        
preg_match('/
            ^
            ((?P<font_style>italic|oblique|normal)s+)?
            ((?P<font_variant>small-caps|normal)s+)?
            ((?P<font_weight>bold(?:er)?|lighter|[1-9]00|normal)s+)?
            (?P<font_size>
                xx-small|x-small|small|medium|large|x-large|xx-large|smaller|larger
                |0(%|[a-z]+)?|-?d+(.d+)?(%|[a-z]+)
                |{xen:propertys+("|'
|)([a-z0-9._-]+)("|'|)s*}
                |@[a-z0-9._-]+
            )
            s+
            (?P<font_family>S.*)
            $
            /siUx', 
$value$fontMatch
        );
        if (!
$fontMatch)
        {
            return false;
        }

        
$output = array();
        if (!empty(
$fontMatch['font_style']) && strtolower($fontMatch['font_style']) != 'normal')
        {
            
$output['font-style'] = 'italic';
        }
        else
        {
            
$output['font-style'] = '';
        }

        if (!empty(
$fontMatch['font_variant']) && strtolower($fontMatch['font_variant']) != 'normal')
        {
            
$output['font-variant'] = 'small-caps';
        }
        else
        {
            
$output['font-variant'] = '';
        }

        if (!empty(
$fontMatch['font_weight']) && strtolower($fontMatch['font_weight']) != 'normal')
        {
            
$output['font-weight'] = 'bold';
        }
        else
        {
            
$output['font-weight'] = '';
        }

        
$output['font-size'] = $fontMatch['font_size'];
        
$output['font-family'] = $fontMatch['font_family'];

        return 
$output;
    }

    /**
     * Parses background shortcut CSS.
     *
     * @param string 
$value
     *
     * @return array|false List of property rules to apply or false if shortcut could not be parsed
     */
    public function parseBackgroundCss(
$value)
    {
        if (strtolower(
$value) == 'none')
        {
            return array(
                'background-none' => '1',
                'background-color' => '',
                'background-image' => '',
                'background-repeat' => '',
                'background-position' => ''
            );
        }

        
$output = array();

        do
        {
            if (preg_match('/^(repeat-x|repeat-y|no-repeat|repeat)/i', 
$value$match))
            {
                if (isset(
$output['background-repeat']))
                {
                    return false;
                }
                
$output['background-repeat'] = $match[0];
            }
            else if (preg_match('/^(none|url(("
|'|)(?P<background_image_url>[^)]+)\2))/i'$value$match))
            {
                if (isset(
$output['background-image']))
                {
                    return 
false;
                }

                if (
$match[0] == 'none')
                {
                    
$output['background-image'] = 'none';
                }
                else
                {
                    
$output['background-image'] = $match['background_image_url'];
                }
            }
            else if (
preg_match('/^(
                    (
                        (left|center|right|0(%|[a-z]+)?|-?d+(.d+)?(%|[a-z]+))
                        (
                            s+(top|center|bottom|0(%|[a-z]+)?|-?d+(.d+)?(%|[a-z]+))
                        )?
                    )|top|center|bottom
                )/ix'
$value$match))
            {
                if (isset(
$output['background-position']))
                {
                    return 
false;
                }
                
$output['background-position'] = $match[0];
            }
            else if (
preg_match('/^(
                    rgb(s*d+%?s*,s*d+%?s*,s*d+%?s*)
                    |rgba(s*d+%?s*,s*d+%?s*,s*d+%?s*,s*[0-9.]+s*)
                    |#[a-f0-9]{6}|#[a-f0-9]{3}
                    |[a-z]+
                )/ix'
$value$match)
            )
            {
                if (isset(
$output['background-color']))
                {
                    return 
false;
                }
                
$output['background-color'] = $match[0];
            }
            else if (
preg_match('/^(
                    ({xen:propertys+("|'
|)([a-z0-9._-]+)("|'|)s*})
                    |@[a-z0-9._-]+
                )/ix', 
$value$match))
            {
                
$handled = false;
                foreach (array('background-color', 'background-image', 'background-position') AS 
$ruleName)
                {
                    if (!isset(
$output[$ruleName]))
                    {
                        
$output[$ruleName] = $match[0];
                        
$handled = true;
                        break;
                    }
                }
                if (!
$handled)
                {
                    return false;
                }
            }
            else
            {
                return false;
            }

            
$value = strval(substr($value, strlen($match[0])));

            if (preg_match('/^(s+|$)/', 
$value))
            {
                
$value = ltrim($value);
            }
            else
            {
                return false;
            }
        }
        while (
$value !== '');

        if (!
$output)
        {
            return false;
        }

        return array_merge(
            array(
                'background-color' => '',
                'background-image' => '',
                'background-repeat' => '',
                'background-position' => ''
            ),
            
$output
        );

    }

    /**
     * Parses padding/margin shortcut CSS.
     *
     * @param string 
$value
     * @param array 
$values By reference. Pushes out the effective padding/margin values to later be pulled together.
     *
     * @return boolean
     */
    public function parsePaddingMarginCss(
$value, array &$values)
    {
        
$value = preg_replace('#{xen:propertys+("|'|)([a-z0-9_-]+(.[a-z0-9_-]+)?)\1s*}#i''@\2'$value);

        
$paddingParts preg_split('/s+/'$value, -1PREG_SPLIT_NO_EMPTY);
        if (
count($paddingParts) > 4)
        {
            return 
false;
        }

        foreach (
$paddingParts AS $paddingPart)
        {
            if (
$paddingPart[0] !== '@' && !preg_match('#^0|auto|-?d+(.d+)?(%|[a-z]+)$#i'$paddingPart))
            {
                return 
false;
            }
        }

        switch (
count($paddingParts))
        {
            case 
1:
                
$values = array(
                    
'all' => $paddingParts[0],
                    
'top' => $paddingParts[0],
                    
'right' => $paddingParts[0],
                    
'bottom' => $paddingParts[0],
                    
'left' => $paddingParts[0],
                );
                break;

            case 
2:
                
$values = array(
                    
'top' => $paddingParts[0],
                    
'right' => $paddingParts[1],
                    
'bottom' => $paddingParts[0],
                    
'left' => $paddingParts[1],
                );
                break;

            case 
3:
                
$values = array(
                    
'top' => $paddingParts[0],
                    
'right' => $paddingParts[1],
                    
'bottom' => $paddingParts[2],
                    
'left' => $paddingParts[1],
                );
                break;

            case 
4:
                
$values = array(
                    
'top' => $paddingParts[0],
                    
'right' => $paddingParts[1],
                    
'bottom' => $paddingParts[2],
                    
'left' => $paddingParts[3],
                );
                break;
        }

        return 
true;
    }

    
/**
     * Parses border shortcut CSS.
     *
     * @param string $value
     * @param string $name The name of the shortcut (border, border-top, etc)
     *
     * @return array|false List of property rules to apply or false if shortcut could not be parsed
     */
    
public function parseBorderCss($value$name)
    {
        
$output = array();

        do
        {
            if (
preg_match('/^(thin|medium|thick|0(%|[a-z]+)?|-?d+(.d+)?[a-z]+)/i'$value$match))
            {
                if (isset(
$output["$name-width"]))
                {
                    return 
false;
                }
                
$output["$name-width"] = $match[0];
            }
            else if (
preg_match('/^(none|hidden|dashed|dotted|double|groove|inset|outset|ridge|solid)/i'$value$match))
            {
                if (isset(
$output["$name-style"]))
                {
                    return 
false;
                }
                
$output["$name-style"] = $match[0];
            }
            else if (
preg_match('/^(
                    rgb(s*d+%?s*,s*d+%?s*,s*d+%?s*)
                    |rgba(s*d+%?s*,s*d+%?s*,s*d+%?s*,s*[0-9.]+s*)
                    |#[a-f0-9]{6}|#[a-f0-9]{3}
                    |[a-z]+
                )/ix'
$value$match)
            )
            {
                if (isset(
$output["$name-color"]))
                {
                    return 
false;
                }
                
$output["$name-color"] = $match[0];
            }
            else if (
preg_match('/^(
                    ({xen:propertys+("|'
|)([a-z0-9._-]+)\2s*})
                    |@[
a-z0-9._-]+
                )/
ix', $value, $match))
            {
                $handled = false;
                foreach (array("$name-width", "$name-color") AS $ruleName)
                {
                    if (!isset($output[$ruleName]))
                    {
                        $output[$ruleName] = $match[0];
                        $handled = true;
                        break;
                    }
                }
                if (!$handled)
                {
                    return false;
                }
            }
            else
            {
                return false;
            }

            $value = strval(substr($value, strlen($match[0])));

            if (preg_match('
/^(s+|$)/', $value))
            {
                $value = ltrim($value);
            }
            else
            {
                return false;
            }
        }
        while ($value !== '');

        if (!$output)
        {
            return false;
        }

        return array_merge(
            array(
                "$name-width" => '
1px',
                "$name-style" => '
none',
                "$name-color" => '
black'
            ),
            $output
        );
    }

    /**
     * Saves style properties in the specified style based on the @ property
     * references that have been parsed out of the template(s).
     *
     * @param integer $styleId Style to save properties into
     * @param array $updates List of property data to update (return from translateEditorPropertiesToArray)
     * @param array $properties List of style properties available in this style. Keyed by name!
     */
    public function saveStylePropertiesInStyleFromTemplate($styleId, array $updates, array $properties)
    {
        $input = array();

        foreach ($updates AS $update)
        {
            if (!isset($properties[$update['
name']]))
            {
                continue;
            }

            $property = $properties[$update['
name']];

            $definitionId = $property['
property_definition_id'];

            if ($update['
component'])
            {
                if (isset($input[$definitionId]))
                {
                    $base = $input[$definitionId];
                }
                else
                {
                    $base = unserialize($property['
property_value']);
                }

                $input[$definitionId] = array_merge($base, $update['
rules']);
            }
            else
            {
                $input[$definitionId] = $update['
rules'];
            }
        }

        if ($input)
        {
            $this->saveStylePropertiesInStyleFromInput($styleId, $input);
        }
    }

    /**
     * Updates the group_name of all style property definitions in group $sourceGroup to $destinationGroup.
     *
     * @param string $sourceGroup
     * @param string $destinationGroup
     * @param integer $styleId
     */
    public function moveStylePropertiesBetweenGroups($sourceGroup, $destinationGroup, $styleId = 0)
    {
        XenForo_Db::beginTransaction($this->_getDb());

        foreach ($this->getStylePropertyDefinitionsByGroup($sourceGroup, $styleId) AS $property)
        {
            $dw = XenForo_DataWriter::create('
XenForo_DataWriter_StylePropertyDefinition', XenForo_DataWriter::ERROR_EXCEPTION);
            $dw->setExistingData($property['
property_definition_id']);
            $dw->set('
group_name', $destinationGroup);
            $dw->save();
        }

        XenForo_Db::commit($this->_getDb());
    }

    /**
     * @return XenForo_Model_Style
     */
    protected function _getStyleModel()
    {
        return $this->getModelFromCache('
XenForo_Model_Style');
    }
}
Онлайн: 1
Реклама