Completed
Push — namespace-model ( c67c40...018a87 )
by Sam
07:37
created

SS_HTML4Value   A

Complexity

Total Complexity 2

Size/Duplication

Total Lines 24
Duplicated Lines 0 %

Coupling/Cohesion

Components 0
Dependencies 0
Metric Value
wmc 2
lcom 0
cbo 0
dl 0
loc 24
rs 10
1
<?php
2
3
namespace SilverStripe\Model;
4
use DOMXPath;
5
use Convert;
6
use DOMDocument;
7
use ViewableData;
8
9
10
/**
11
 * This class handles the converting of HTML fragments between a string and a DOMDocument based
12
 * representation.
13
 *
14
 * It's designed to allow dependancy injection to replace the standard HTML4 version with one that
15
 * handles XHTML or HTML5 instead
16
 *
17
 * @package framework
18
 * @subpackage integration
19
 */
20
abstract class HTMLValue extends ViewableData {
21
22
	public function __construct($fragment = null) {
23
		if ($fragment) $this->setContent($fragment);
24
		parent::__construct();
25
	}
26
27
	abstract public function setContent($fragment);
28
29
	/**
30
	 * @return string
31
	 */
32
	public function getContent() {
33
		$doc = clone $this->getDocument();
34
		$xp = new DOMXPath($doc);
35
36
		// If there's no body, the content is empty string
37
		if (!$doc->getElementsByTagName('body')->length) return '';
38
39
		// saveHTML Percentage-encodes any URI-based attributes. We don't want this, since it interferes with
40
		// shortcodes. So first, save all the attribute values for later restoration.
41
		$attrs = array(); $i = 0;
42
43
		foreach ($xp->query('//body//@*') as $attr) {
44
			$key = "__HTMLVALUE_".($i++);
45
			$attrs[$key] = $attr->value;
46
			$attr->value = $key;
47
		}
48
49
		// Then, call saveHTML & extract out the content from the body tag
50
		$res = preg_replace(
51
			array(
52
				'/^(.*?)<body>/is',
53
				'/<\/body>(.*?)$/isD',
54
			),
55
			'',
56
			$doc->saveHTML()
57
		);
58
59
		// Then replace the saved attributes with their original versions
60
		$res = preg_replace_callback('/__HTMLVALUE_(\d+)/', function($matches) use ($attrs) {
61
			return Convert::raw2att($attrs[$matches[0]]);
62
		}, $res);
63
64
		// Prevent &nbsp; being encoded as literal utf-8 characters
65
		// Possible alternative solution: http://stackoverflow.com/questions/2142120/php-encoding-with-domdocument
66
		$from = mb_convert_encoding('&nbsp;', 'utf-8', 'html-entities');
67
		$res = str_replace($from, '&nbsp;', $res);
68
69
		return $res;
70
	}
71
72
	/** @see HTMLValue::getContent() */
73
	public function forTemplate() {
74
		return $this->getContent();
75
	}
76
77
	/** @var DOMDocument */
78
	private $document = null;
79
	/** @var bool */
80
	private $valid = true;
81
82
	/**
83
	 * Get the DOMDocument for the passed content
84
	 * @return DOMDocument | false - Return false if HTML not valid, the DOMDocument instance otherwise
85
	 */
86
	public function getDocument() {
87
		if (!$this->valid) {
88
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by SilverStripe\Model\HTMLValue::getDocument of type DOMDocument.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
89
		}
90
		else if ($this->document) {
91
			return $this->document;
92
		}
93
		else {
94
			$this->document = new DOMDocument('1.0', 'UTF-8');
95
			$this->document->strictErrorChecking = false;
96
			$this->document->formatOutput = false;
97
98
			return $this->document;
99
		}
100
	}
101
102
	/**
103
	 * Is this HTMLValue in an errored state?
104
	 * @return bool
105
	 */
106
	public function isValid() {
107
		return $this->valid;
108
	}
109
110
	/**
111
	 * @param DOMDocument $document
112
	 */
113
	public function setDocument($document) {
114
		$this->document = $document;
115
		$this->valid = true;
116
	}
117
118
	public function setInvalid() {
119
		$this->document = $this->valid = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->valid = false of type false is incompatible with the declared type object<DOMDocument> of property $document.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
120
	}
121
122
	/**
123
	 * Pass through any missed method calls to DOMDocument (if they exist)
124
	 * so that HTMLValue can be treated mostly like an instance of DOMDocument
125
	 */
126
	public function __call($method, $arguments) {
127
		$doc = $this->getDocument();
128
129
		if(method_exists($doc, $method)) {
130
			return call_user_func_array(array($doc, $method), $arguments);
131
		}
132
		else {
133
			return parent::__call($method, $arguments);
134
		}
135
	}
136
137
	/**
138
	 * Get the body element, or false if there isn't one (we haven't loaded any content
139
	 * or this instance is in an invalid state)
140
	 */
141
	public function getBody() {
142
		$doc = $this->getDocument();
143
		if (!$doc) return false;
144
145
		$body = $doc->getElementsByTagName('body');
146
		if (!$body->length) return false;
147
148
		return $body->item(0);
149
	}
150
151
	/**
152
	 * Make an xpath query against this HTML
153
	 *
154
	 * @param $query string - The xpath query string
155
	 * @return DOMNodeList
156
	 */
157
	public function query($query) {
158
		$xp = new DOMXPath($this->getDocument());
159
		return $xp->query($query);
160
	}
161
}
162
163
class HTML4Value extends HTMLValue {
164
165
	/**
166
	 * @param string $content
167
	 * @return bool
168
	 */
169
	public function setContent($content) {
170
		// Ensure that \r (carriage return) characters don't get replaced with "&#13;" entity by DOMDocument
171
		// This behaviour is apparently XML spec, but we don't want this because it messes up the HTML
172
		$content = str_replace(chr(13), '', $content);
173
174
		// Reset the document if we're in an invalid state for some reason
175
		if (!$this->isValid()) $this->setDocument(null);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a object<DOMDocument>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
176
177
		$errorState = libxml_use_internal_errors(true);
178
		$result = $this->getDocument()->loadHTML(
179
			'<html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"></head>' .
180
			"<body>$content</body></html>"
181
		);
182
		libxml_clear_errors();
183
		libxml_use_internal_errors($errorState);
184
		return $result;
185
	}
186
}
187