Completed
Push — master ( 66f370...d91c66 )
by Sam
08:21
created

HTMLEditorFieldTest::testNullSaving()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 0
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Forms\Tests\HTMLEditor;
4
5
use Page;
6
use SilverStripe\Assets\File;
7
use SilverStripe\Assets\FileNameFilter;
8
use SilverStripe\Assets\Filesystem;
9
use SilverStripe\Assets\Folder;
10
use SilverStripe\Assets\Image;
11
use SilverStripe\Assets\Tests\Storage\AssetStoreTest\TestAssetStore;
12
use SilverStripe\Control\Controller;
13
use SilverStripe\Control\HTTPRequest;
14
use SilverStripe\Core\Config\Config;
15
use SilverStripe\Dev\CSSContentParser;
16
use SilverStripe\Dev\FunctionalTest;
17
use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
18
use SilverStripe\Forms\HTMLEditor\HTMLEditorField_Toolbar;
19
use SilverStripe\Forms\HTMLEditor\HTMLEditorField_Image;
20
use SilverStripe\Forms\HTMLReadonlyField;
21
use SilverStripe\Forms\Tests\HTMLEditor\HTMLEditorFieldTest\DummyMediaFormFieldExtension;
22
use SilverStripe\Forms\Tests\HTMLEditor\HTMLEditorFieldTest\TestObject;
23
use SilverStripe\ORM\FieldType\DBHTMLText;
24
25
class HTMLEditorFieldTest extends FunctionalTest
26
{
27
28
    protected static $fixture_file = 'HTMLEditorFieldTest.yml';
29
30
    protected static $use_draft_site = true;
31
32
    protected $requiredExtensions = array(
33
        HTMLEditorField_Toolbar::class => array(
34
            DummyMediaFormFieldExtension::class
35
        )
36
    );
37
38
    protected $extraDataObjects = array(TestObject::class);
39
40
    public function setUp()
41
    {
42
        parent::setUp();
43
44
        // Set backend root to /HTMLEditorFieldTest
45
        TestAssetStore::activate('HTMLEditorFieldTest');
46
47
        // Set the File Name Filter replacements so files have the expected names
48
        Config::inst()->update(
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface SilverStripe\Config\Coll...nfigCollectionInterface as the method update() does only exist in the following implementations of said interface: SilverStripe\Config\Coll...\MemoryConfigCollection.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
49
            FileNameFilter::class,
50
            'default_replacements',
51
            array(
52
            '/\s/' => '-', // remove whitespace
53
            '/_/' => '-', // underscores to dashes
54
            '/[^A-Za-z0-9+.\-]+/' => '', // remove non-ASCII chars, only allow alphanumeric plus dash and dot
55
            '/[\-]{2,}/' => '-', // remove duplicate dashes
56
            '/^[\.\-_]+/' => '', // Remove all leading dots, dashes or underscores
57
            )
58
        );
59
60
        // Create a test files for each of the fixture references
61
        $files = File::get()->exclude('ClassName', Folder::class);
62
        foreach ($files as $file) {
63
            $fromPath = __DIR__ . '/HTMLEditorFieldTest/images/' . $file->Name;
64
            $destPath = TestAssetStore::getLocalPath($file); // Only correct for test asset store
65
            Filesystem::makeFolder(dirname($destPath));
66
            copy($fromPath, $destPath);
67
        }
68
    }
69
70
    public function tearDown()
71
    {
72
        TestAssetStore::reset();
73
        parent::tearDown();
74
    }
75
76
    public function testCasting()
77
    {
78
        // Test special characters
79
        $inputText = "These are some unicodes: ä, ö, & ü";
80
        $field = new HTMLEditorField("Test", "Test");
81
        $field->setValue($inputText);
82
        $this->assertContains('These are some unicodes: &auml;, &ouml;, &amp; &uuml;', $field->Field());
83
        // Test shortcodes
84
        $inputText = "Shortcode: [file_link id=4]";
85
        $field = new HTMLEditorField("Test", "Test");
86
        $field->setValue($inputText);
87
        $this->assertContains('Shortcode: [file_link id=4]', $field->Field());
88
    }
89
90
    public function testBasicSaving()
91
    {
92
        $obj = new TestObject();
93
        $editor   = new HTMLEditorField('Content');
94
95
        $editor->setValue('<p class="foo">Simple Content</p>');
96
        $editor->saveInto($obj);
97
        $this->assertEquals('<p class="foo">Simple Content</p>', $obj->Content, 'Attributes are preserved.');
98
99
        $editor->setValue('<p>Unclosed Tag');
100
        $editor->saveInto($obj);
101
        $this->assertEquals('<p>Unclosed Tag</p>', $obj->Content, 'Unclosed tags are closed.');
102
    }
103
104
    public function testNullSaving()
105
    {
106
        $obj = new TestObject();
107
        $editor = new HTMLEditorField('Content');
108
109
        $editor->setValue(null);
110
        $editor->saveInto($obj);
111
        $this->assertEquals('', $obj->Content, "Doesn't choke on empty/null values.");
112
    }
113
114
    public function testResizedImageInsertion()
115
    {
116
        $obj = new TestObject();
117
        $editor = new HTMLEditorField('Content');
118
119
        $fileID = $this->idFromFixture(Image::class, 'example_image');
120
        $editor->setValue(
121
            sprintf(
122
                '[image src="assets/example.jpg" width="10" height="20" id="%d"]',
123
                $fileID
124
            )
125
        );
126
        $editor->saveInto($obj);
127
128
        $parser = new CSSContentParser($obj->dbObject('Content')->forTemplate());
129
        $xml = $parser->getByXpath('//img');
130
        $this->assertEquals(
131
            'example',
132
            (string)$xml[0]['alt'],
133
            'Alt tags are added by default based on filename'
134
        );
135
        $this->assertEquals('', (string)$xml[0]['title'], 'Title tags are added by default.');
136
        $this->assertEquals(10, (int)$xml[0]['width'], 'Width tag of resized image is set.');
137
        $this->assertEquals(20, (int)$xml[0]['height'], 'Height tag of resized image is set.');
138
139
        $neededFilename
140
            = '/assets/HTMLEditorFieldTest/f5c7c2f814/example__ResizedImageWyIxMCIsIjIwIl0.jpg';
141
142
        $this->assertEquals($neededFilename, (string)$xml[0]['src'], 'Correct URL of resized image is set.');
143
        $this->assertTrue(file_exists(BASE_PATH.DIRECTORY_SEPARATOR.$neededFilename), 'File for resized image exists');
144
        $this->assertEquals(false, $obj->HasBrokenFile, 'Referenced image file exists.');
145
    }
146
147
    public function testMultiLineSaving()
148
    {
149
        $obj = $this->objFromFixture(TestObject::class, 'home');
150
        $editor   = new HTMLEditorField('Content');
151
        $editor->setValue('<p>First Paragraph</p><p>Second Paragraph</p>');
152
        $editor->saveInto($obj);
0 ignored issues
show
Bug introduced by
It seems like $obj defined by $this->objFromFixture(\S...tObject::class, 'home') on line 149 can be null; however, SilverStripe\Forms\HTMLE...EditorField::saveInto() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
153
        $this->assertEquals('<p>First Paragraph</p><p>Second Paragraph</p>', $obj->Content);
154
    }
155
156
    public function testSavingLinksWithoutHref()
157
    {
158
        $obj = $this->objFromFixture(TestObject::class, 'home');
159
        $editor   = new HTMLEditorField('Content');
160
161
        $editor->setValue('<p><a name="example-anchor"></a></p>');
162
        $editor->saveInto($obj);
0 ignored issues
show
Bug introduced by
It seems like $obj defined by $this->objFromFixture(\S...tObject::class, 'home') on line 158 can be null; however, SilverStripe\Forms\HTMLE...EditorField::saveInto() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
163
164
        $this->assertEquals(
165
            '<p><a name="example-anchor"></a></p>',
166
            $obj->Content,
167
            'Saving a link without a href attribute works'
168
        );
169
    }
170
171
    public function testGetAnchors()
172
    {
173
        if (!class_exists('Page')) {
174
            $this->markTestSkipped();
175
        }
176
        $linkedPage = new Page();
177
        $linkedPage->Title = 'Dummy';
178
        $linkedPage->write();
179
180
        $html = <<<EOS
181
<div name="foo"></div>
182
<div name='bar'></div>
183
<div id="baz"></div>
184
[sitetree_link id="{$linkedPage->ID}"]
185
<div id='bam'></div>
186
<div id = "baz"></div>
187
<div id = ""></div>
188
<div id="some'id"></div>
189
<div id=bar></div>
190
EOS
191
        ;
192
        $expected = array(
193
            'foo',
194
            'bar',
195
            'baz',
196
            'bam',
197
            "some&#039;id",
198
        );
199
        $page = new Page();
200
        $page->Title = 'Test';
201
        $page->Content = $html;
202
        $page->write();
203
        $this->useDraftSite(true);
204
205
        $request = new HTTPRequest(
206
            'GET',
207
            '/',
208
            array(
209
            'PageID' => $page->ID,
210
            )
211
        );
212
213
        $toolBar = new HTMLEditorField_Toolbar(new Controller(), 'test');
214
        $toolBar->setRequest($request);
215
216
        $results = json_decode($toolBar->getanchors(), true);
217
        $this->assertEquals($expected, $results);
218
    }
219
220
    public function testHTMLEditorFieldFileLocal()
221
    {
222
        $file = new HTMLEditorField_Image('http://domain.com/folder/my_image.jpg?foo=bar');
223
        $this->assertEquals('http://domain.com/folder/my_image.jpg?foo=bar', $file->URL);
224
        $this->assertEquals('my_image.jpg', $file->Name);
225
        $this->assertEquals('jpg', $file->Extension);
0 ignored issues
show
Bug introduced by
The property Extension does not seem to exist. Did you mean extensions?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
226
        // TODO Can't easily test remote file dimensions
227
    }
228
229
    public function testHTMLEditorFieldFileRemote()
230
    {
231
        $fileFixture = new File(array('Name' => 'my_local_image.jpg', 'Filename' => 'folder/my_local_image.jpg'));
232
        $file = new HTMLEditorField_Image('http://localdomain.com/folder/my_local_image.jpg', $fileFixture);
233
        $this->assertEquals('http://localdomain.com/folder/my_local_image.jpg', $file->URL);
234
        $this->assertEquals('my_local_image.jpg', $file->Name);
235
        $this->assertEquals('jpg', $file->Extension);
0 ignored issues
show
Bug introduced by
The property Extension does not seem to exist. Did you mean extensions?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
236
    }
237
238
    public function testReadonlyField()
239
    {
240
        $editor = new HTMLEditorField('Content');
241
        $fileID = $this->idFromFixture(Image::class, 'example_image');
242
        $editor->setValue(
243
            sprintf(
244
                '[image src="assets/example.jpg" width="10" height="20" id="%d"]',
245
                $fileID
246
            )
247
        );
248
        /**
249
 * @var HTMLReadonlyField $readonly
250
*/
251
        $readonly = $editor->performReadonlyTransformation();
252
        /**
253
 * @var DBHTMLText $readonlyContent
254
*/
255
        $readonlyContent = $readonly->Field();
256
257
        $this->assertEquals(
258
            <<<EOS
259
<span class="readonly typography" id="Content">
260
	<img src="/assets/HTMLEditorFieldTest/f5c7c2f814/example__ResizedImageWyIxMCIsIjIwIl0.jpg" alt="example" width="10" height="20">
261
</span>
262
263
264
EOS
265
            ,
266
            $readonlyContent->getValue()
267
        );
268
269
        // Test with include input tag
270
        $readonly = $editor->performReadonlyTransformation()
271
            ->setIncludeHiddenField(true);
272
        /**
273
 * @var DBHTMLText $readonlyContent
274
*/
275
        $readonlyContent = $readonly->Field();
276
        $this->assertEquals(
277
            <<<EOS
278
<span class="readonly typography" id="Content">
279
	<img src="/assets/HTMLEditorFieldTest/f5c7c2f814/example__ResizedImageWyIxMCIsIjIwIl0.jpg" alt="example" width="10" height="20">
280
</span>
281
282
	<input type="hidden" name="Content" value="[image src=&quot;/assets/HTMLEditorFieldTest/f5c7c2f814/example.jpg&quot; width=&quot;10&quot; height=&quot;20&quot; id=&quot;{$fileID}&quot;]" />
283
284
285
EOS
286
            ,
287
            $readonlyContent->getValue()
288
        );
289
    }
290
}
291