Skip to content
Snippets Groups Projects
LinkElement.php 7.8 KiB
Newer Older
<?php
/**
 * @author  Jan-Hendrik Willms <tleilax+studip@gmail.com>
 * @license GPL2 or any later version
 */
class LinkElement extends WidgetElement implements ArrayAccess
{
    /**
     * Create link by parsing a html chunk.
     *
     * @param String $html HTML chunk to parse
     * @param Icon $icon Optional icon
     * @return LinkElement Link element from parsed html
     * @throws Exception if html can not be parsed
     */
    public static function fromHTML($html, \Icon $icon = null)
    {
        $matched = preg_match('~(<a(?P<attributes>(?:\s+\w+=".*?")+)>\s*(?P<label>.*?)\s*</a>)~s', $html, $match);
        if (!$matched) {
            throw new Exception('Could not parse html');
        }

        $attributes = self::parseAttributes($match['attributes']);
        $url        = $attributes['href'] ?: '#';
        unset($attributes['href']);

        return new self(html_entity_decode($match['label']), html_entity_decode($url), $icon, $attributes);
    }

    /**
     * Parse a string of html attributes into an associative array.
     *
     * @param String $text String of html attributes
     * @return Array parsed attributes as key => value pairs
     * @see https://gist.github.com/rodneyrehm/3070128
     */
    protected static function parseAttributes($text)
    {
        $attributes = [];
        $pattern = '#(?(DEFINE)
                       (?<name>[a-zA-Z][a-zA-Z0-9-:]*)
                       (?<value_double>"[^"]+")
                       (?<value_single>\'[^\']+\')
                       (?<value_none>[^\s>]+)
                       (?<value>((?&value_double)|(?&value_single)|(?&value_none)))
                     )
                     (?<n>(?&name))(=(?<v>(?&value)))?#xs';

        if (preg_match_all($pattern, $text, $matches, PREG_SET_ORDER)) {
            foreach ($matches as $match) {
                $attributes[$match['n']] = isset($match['v'])
                                         ? html_entity_decode(trim($match['v'], '\'"'))
                                         : null;
            }
        }
        return $attributes;
    }

    public $url;
    public $label;
    public $icon;
    public $active = false;
    public $attributes = [];
    public $as_button = false;

    /**
     * create a link for a widget
     *
     * @param String $label    Label/content of the link
     * @param String $url      URL/Location of the link (raw url, no entities)
     * @param Icon $icon       Icon for the link
     * @param array  $attributes HTML-attributes for the a-tag in an associative array.
     */
    public function __construct($label, $url, \Icon $icon = null, $attributes = [])
    {
        parent::__construct();

        // TODO: Remove this some versions after 5.0
        $url = html_entity_decode($url);

        $this->label      = $label;
        $this->url        = $url;
        $this->attributes = $attributes;
        $this->icon       = $icon;
    }

    /**
     * Sets the active state of the element.
     *
     * @param bool $active Active state (optional, defaults to true)
     * @return LinkElement instance to allow chaining
     */
    public function setActive($active = true)
    {
        $this->active = $active;
        return $this;
    }

    /**
     * Sets the dialog options for the element. Passing false as $state will
     * reset the dialog options to "none".
     *
     * @param mixed $active Dialog options (optional, defaults to blank/standard
     *                      dialog)
     * @return LinkElement instance to allow chaining
     */
    public function asDialog($state = '')
    {
        if ($state !== false) {
            $this->attributes['data-dialog'] = $state;
        } else {
            unset($this->attributes['data-dialog']);
        }
        return $this;
    }

    /**
     * Defines whether the link should be rendered as a button/form with POST
     * method.
     *
     * @param bool $active State (optional, defaults to true)
     * @return LinkElement instance to allow chaining
     */
    public function asButton($state = true)
    {
        $this->as_button = $state;
        return $this;
    }

    /**
     * Sets the target attribute of the element.
     *
     * @param string $target Target attribute
     * @return LinkElement instance to allow chaining
     */
    public function setTarget($target)
    {
        if ($target) {
            $this->attributes['target'] = $target;
        } else {
            unset($this->attributes['target']);
        }
        return $this;
    }

    /**
     * Adds a css class to the rendered element.
     *
     * @param string $clas CSS class to add
     * @return LinkElement instance to allow chaining
     */
    public function addClass($class)
    {
Moritz Strohm's avatar
Moritz Strohm committed
        $this->attributes['class'] = isset($this->attributes['class'])
            ? $this->attributes['class'] . " " . $class
            : $class;
        return $this;
    }

    /**
     * Set disabled state
     *
     * @param boolean $state Disabled state
     * @return LinkElement instance to allow chaining
     */
    public function setDisabled($state = true)
    {
        if ($state) {
            $this->attributes['disabled'] = true;
        } else {
            unset($this->attributes['disabled']);
        }

        return $this;
    }

    /**
     * Returns whether the element is disabled.
     *
     * @return bool
     */
    public function isDisabled()
    {
        return isset($this->attributes['disabled']) && $this->attributes['disabled'] !== false;
    }

    /**
     * Renders the element.
     *
     * @return string
     */
    public function render()
    {
        $disabled = $this->isDisabled();

        if ($this->as_button && !$disabled) {
            return $this->renderButton();
        }

        if ($this->active) {
            $this->addClass('active');
        }

        $attributes = (array) $this->attributes;

        if ($disabled) {
            $tag = 'span';
        } else {
            $tag = 'a';
            $attributes['href'] = $this->url;
        }

        return sprintf(
            '<%1$s %2$s>%3$s %4$s</%1$s>',
            $tag,
            arrayToHtmlAttributes($attributes),
            $this->icon ? $this->icon->asImg(null, ['class' => 'text-bottom']) : '',
            htmlReady($this->label)
        );
    }

    /**
     * Renders the element as a button/form.
     *
     * @return string
     */
    protected function renderButton()
    {
        return sprintf(
Ron Lucke's avatar
Ron Lucke committed
            '<form action="%1$s" method="post" %2$s>%3$s<button type="submit">%4$s</button></form>',
            htmlReady($this->url),
            arrayToHtmlAttributes((array) $this->attributes),
            CSRFProtection::tokenTag(),
            htmlReady($this->label)
        );
    }

    /**
     * Returns whether the given url is valid.
     *
     * @param string $url URL to test
     * @return bool
     */
    protected function isURL($url)
    {
        return filter_var($url, FILTER_VALIDATE_URL) !== false;
    }

    // Array access for attributes

    /**
     * @todo Add bool return type when Stud.IP requires PHP8 minimal
     */
    #[ReturnTypeWillChange]
    public function offsetExists($offset)
    {
        return isset($this->attributes[$offset]);
    }

    /**
     * @param $offset
     * @return mixed
     *
     * @todo Add mixed return type when Stud.IP requires PHP8 minimal
     */
    #[ReturnTypeWillChange]
    public function offsetGet($offset)
    {
        return $this->attributes[$offset];
    }

    /**
     * @todo Add void return type when Stud.IP requires PHP8 minimal
     */
    #[ReturnTypeWillChange]
    public function offsetSet($offset, $value)
    {
        $this->attributes[$offset] = $value;
    }

    /**
     * @todo Add void return type when Stud.IP requires PHP8 minimal
     */
    #[ReturnTypeWillChange]
    public function offsetUnset($offset)
    {
        unset($this->attributes[$offset]);
    }
}