Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.10% covered (success)
97.10%
67 / 69
87.50% covered (success)
87.50%
7 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Comment
97.10% covered (success)
97.10%
67 / 69
87.50% covered (success)
87.50%
7 / 8
28
0.00% covered (danger)
0.00%
0 / 1
 store
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
11
 exists
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 setPaste
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getPaste
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setParentId
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getParentId
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 _validate
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 _sanitize
92.31% covered (success)
92.31%
24 / 26
0.00% covered (danger)
0.00%
0 / 1
7.02
1<?php declare(strict_types=1);
2/**
3 * PrivateBin
4 *
5 * a zero-knowledge paste bin
6 *
7 * @link      https://github.com/PrivateBin/PrivateBin
8 * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
9 * @license   https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
10 */
11
12namespace PrivateBin\Model;
13
14use Identicon\Identicon;
15use Jdenticon\Identicon as Jdenticon;
16use PrivateBin\Exception\TranslatedException;
17use PrivateBin\Persistence\TrafficLimiter;
18use PrivateBin\Vizhash16x16;
19
20/**
21 * Comment
22 *
23 * Model of a PrivateBin comment.
24 */
25class Comment extends AbstractModel
26{
27    /**
28     * Instance's parent.
29     *
30     * @access private
31     * @var Paste
32     */
33    private $_paste;
34
35    /**
36     * Store the comment's data.
37     *
38     * @access public
39     * @throws TranslatedException
40     */
41    public function store()
42    {
43        // Make sure paste exists.
44        $pasteid = $this->getPaste()->getId();
45        if (!$this->getPaste()->exists()) {
46            throw new TranslatedException(self::INVALID_DATA_ERROR, 67);
47        }
48
49        // Make sure the discussion is opened in this paste and allowed in the configuration.
50        if (!$this->getPaste()->isOpendiscussion() || !$this->_conf->getKey('discussion')) {
51            throw new TranslatedException(self::INVALID_DATA_ERROR, 68);
52        }
53
54        // Check for improbable collision.
55        if ($this->exists()) {
56            throw new TranslatedException(self::COLLISION_ERROR, 69);
57        }
58
59        // Comment IDs must be unique within the paste, regardless of the
60        // parent, and the parent must be either the paste or an existing comment.
61        $commentid = $this->getId();
62        $parentid = $this->getParentId();
63        $parentExists = $parentid === $pasteid;
64        if ($commentid === $pasteid) {
65            throw new TranslatedException(self::COLLISION_ERROR, 72);
66        }
67        foreach ($this->_store->readComments($pasteid) as $comment) {
68            if ($comment['id'] === $commentid) {
69                throw new TranslatedException(self::COLLISION_ERROR, 69);
70            }
71            if ($comment['id'] === $parentid) {
72                $parentExists = true;
73            }
74        }
75        if (!$parentExists) {
76            throw new TranslatedException(self::INVALID_DATA_ERROR, 71);
77        }
78
79        $this->_data['meta']['created'] = time();
80
81        // store comment
82        if (
83            $this->_store->createComment(
84                $pasteid,
85                $this->getParentId(),
86                $this->getId(),
87                $this->_data
88            ) === false
89        ) {
90            throw new TranslatedException('Error saving comment. Sorry.', 70);
91        }
92    }
93
94    /**
95     * Test if comment exists in store.
96     *
97     * @access public
98     * @return bool
99     */
100    public function exists()
101    {
102        return $this->_store->existsComment(
103            $this->getPaste()->getId(),
104            $this->getParentId(),
105            $this->getId()
106        );
107    }
108
109    /**
110     * Set paste.
111     *
112     * @access public
113     * @param Paste $paste
114     */
115    public function setPaste(Paste &$paste)
116    {
117        $this->_paste = $paste;
118        $this->_data['pasteid'] = $paste->getId();
119    }
120
121    /**
122     * Get paste.
123     *
124     * @access public
125     * @return Paste
126     */
127    public function getPaste()
128    {
129        return $this->_paste;
130    }
131
132    /**
133     * Set parent ID.
134     *
135     * @access public
136     * @param string $id
137     * @throws TranslatedException
138     */
139    public function setParentId($id)
140    {
141        if (!self::isValidId($id)) {
142            throw new TranslatedException('Invalid document ID.', 65);
143        }
144        $this->_data['parentid'] = $id;
145    }
146
147    /**
148     * Get parent ID.
149     *
150     * @access public
151     * @return string
152     */
153    public function getParentId()
154    {
155        if (!array_key_exists('parentid', $this->_data)) {
156            $this->_data['parentid'] = $this->getPaste()->getId();
157        }
158        return $this->_data['parentid'];
159    }
160
161    /**
162     * Validate data.
163     *
164     * @access protected
165     * @param  array $data
166     * @throws TranslatedException
167     */
168    protected function _validate(array &$data)
169    {
170        // reject comments without authenticated IDs, if required
171        if ($this->_conf->getKey('discussionrequireauthenticatedids') && count($data['adata']) !== 10) {
172            throw new TranslatedException(self::INVALID_DATA_ERROR, 77);
173        }
174    }
175
176    /**
177     * Sanitizes data to conform with current configuration.
178     *
179     * @access protected
180     * @param  array $data
181     */
182    protected function _sanitize(array &$data)
183    {
184        // we generate an icon based on a SHA512 HMAC of the users IP, if configured
185        $icon = $this->_conf->getKey('icon');
186        if ($icon !== 'none') {
187            $pngdata = '';
188            $hmac = TrafficLimiter::getHash();
189            if ($icon === 'identicon') {
190                $identicon = new Identicon();
191                $pngdata = $identicon->getImageDataUri($hmac, 16);
192            } elseif ($icon === 'jdenticon') {
193                $jdenticon = new Jdenticon([
194                    'hash'  => $hmac,
195                    'size'  => 16,
196                    'style' => [
197                        'backgroundColor'   => '#fff0', // fully transparent, for dark mode
198                        'padding'           => 0,
199                    ],
200                ]);
201                $pngdata = $jdenticon->getImageDataUri('png');
202            } elseif ($icon === 'vizhash') {
203                $vh = new Vizhash16x16();
204                $pngdata = 'data:image/png;base64,' . base64_encode(
205                    $vh->generate($hmac)
206                );
207            }
208            if (!empty($pngdata)) {
209                if (!array_key_exists('meta', $data)) {
210                    $data['meta'] = [];
211                }
212                $data['meta']['icon'] = $pngdata;
213            }
214        }
215    }
216}