Completed
Pull Request — master (#243)
by Дмитрий
05:05
created

Sessions::serializePHP()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 20
Code Lines 14

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 4
eloc 14
nc 3
nop 1
dl 0
loc 20
rs 9.2
1
<?php
2
namespace PHPDaemon\Traits;
3
4
use PHPDaemon\Core\Daemon;
5
use PHPDaemon\Core\Debug;
6
use PHPDaemon\Core\CallbackWrapper;
7
use PHPDaemon\FS\File;
8
use PHPDaemon\FS\FileSystem;
9
10
/**
11
 * Sessions
12
 * @package PHPDaemon\Traits
13
 * @author  Vasily Zorin <[email protected]>
14
 */
15
trait Sessions
16
{
17
    /**
18
     * @var string Session ID
19
     */
20
    protected $sessionId;
21
22
    /**
23
     * @var integer
24
     */
25
    protected $sessionStartTimeout = 10;
26
27
    /**
28
     * @var boolean
29
     */
30
    protected $sessionStarted = false;
31
    
32
    /**
33
     * @var boolean
34
     */
35
    protected $sessionFlushing = false;
36
    
37
    /**
38
     * @var resource
39
     */
40
    protected $sessionFp;
41
42
    /**
43
     * @var string
44
     */
45
    protected $sessionPrefix = 'sess_';
46
47
    /**
48
     * Is session started?
49
     * @return boolean
50
     */
51
    public function sessionStarted()
52
    {
53
        return $this->sessionStarted;
54
    }
55
56
    /**
57
     * Deferred event 'onSessionStart'
58
     * @return callable
59
     */
60
    public function onSessionStartEvent()
61
    {
62
        return function ($sessionStartEvent) {
63
            /** @var \PHPDaemon\Core\DeferredEvent $sessionStartEvent */
64
            $name = ini_get('session.name');
65
            $sid = $this->getCookieStr($name);
0 ignored issues
show
Bug introduced by
It seems like getCookieStr() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
66
            if ($sid === '') {
67
                $this->sessionStartNew(function ($success) use ($sessionStartEvent) {
68
                    $sessionStartEvent->setResult($success);
69
                });
70
                return;
71
            }
72
            $this->onSessionRead(function ($session) use ($sessionStartEvent) {
0 ignored issues
show
Bug introduced by
The method onSessionRead() does not exist on PHPDaemon\Traits\Sessions. Did you maybe mean onSessionReadEvent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
Unused Code introduced by
The parameter $session is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
73
                if ($this->getSessionState() === null) {
74
                    $this->sessionStartNew(function ($success) use ($sessionStartEvent) {
75
                        $sessionStartEvent->setResult($success);
76
                    });
77
                    return;
78
                }
79
                $sessionStartEvent->setResult(true);
80
            });
81
        };
82
    }
83
84
    /**
85
     * Deferred event 'onSessionRead'
86
     * @return callable
87
     */
88
    public function onSessionReadEvent()
89
    {
90
        return function ($sessionEvent) {
91
            /** @var \PHPDaemon\Core\DeferredEvent $sessionEvent */
92
            $name = ini_get('session.name');
93
            $sid  = $this->getCookieStr($name);
0 ignored issues
show
Bug introduced by
It seems like getCookieStr() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
94
            if ($sid === '') {
95
                $sessionEvent->setResult(false);
96
                return;
97
            }
98
            if ($this->getSessionState() !== null) {
99
                $sessionEvent->setResult(true);
100
                return;
101
            }
102
            $this->sessionRead($sid, function ($data) use ($sessionEvent, $sid) {
103
                $canDecode = $data !== false && $this->sessionDecode($data);
104
                $sessionEvent->setResult($canDecode);
105
            });
106
        };
107
    }
108
109
    /**
110
     * Reads session data
111
     * @param  string   $sid Session ID
112
     * @param  callable $cb  Callback
0 ignored issues
show
Documentation introduced by
Should the type for parameter $cb not be callable|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
113
     * @return void
114
     */
115
    public function sessionRead($sid, $cb = null)
116
    {
117
        FileSystem::open(FileSystem::genRndTempnamPrefix(session_save_path(), $this->sessionPrefix) . basename($sid), 'r+!', function ($fp) use ($cb) {
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 151 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
118
            if (!$fp) {
119
                $cb(false);
120
                return;
121
            }
122
            $fp->readAll(function ($fp, $data) use ($cb) {
123
                $this->sessionFp = $fp;
124
                $cb($data);
125
            });
126
        });
127
    }
128
129
    /**
130
     * Commmit session data
131
     * @param  callable $cb Callback
0 ignored issues
show
Documentation introduced by
Should the type for parameter $cb not be callable|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
132
     * @return void
133
     */
134
    public function sessionCommit($cb = null)
135
    {
136
        if (!$this->sessionFp || $this->sessionFlushing) {
137
            if ($cb) {
138
                $cb(false);
139
            }
140
            return;
141
        }
142
        $this->sessionFlushing = true;
143
        $data                  = $this->sessionEncode();
144
        $l                     = mb_orig_strlen($data);
0 ignored issues
show
Security Bug introduced by
It seems like $data defined by $this->sessionEncode() on line 143 can also be of type false; however, mb_orig_strlen() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
145
        $cb = CallbackWrapper::wrap($cb);
0 ignored issues
show
Bug introduced by
It seems like $cb defined by \PHPDaemon\Core\CallbackWrapper::wrap($cb) on line 145 can also be of type null; however, PHPDaemon\Core\CallbackWrapper::wrap() does only seem to accept callable, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
146
        $this->sessionFp->write($data, function ($file, $result) use ($l, $cb) {
0 ignored issues
show
Unused Code introduced by
The parameter $result is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
147
            $file->truncate($l, function ($file, $result) use ($cb) {
0 ignored issues
show
Unused Code introduced by
The parameter $file is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $result is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
148
                $this->sessionFlushing = false;
149
                if ($cb) {
150
                    $cb(true);
151
                }
152
            });
153
        });
154
    }
155
156
    /**
157
     * Session start
158
     * @param  boolean $force_start
159
     * @return void
160
     */
161
    protected function sessionStart($force_start = true)
0 ignored issues
show
Unused Code introduced by
The parameter $force_start is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
162
    {
163
        if ($this->sessionStarted) {
164
            return;
165
        }
166
        $this->sessionStarted = true;
167
        if (!$this instanceof \PHPDaemon\HTTPRequest\Generic) {
168
            Daemon::log('Called ' . get_class($this). '(trait \PHPDaemon\Traits\Sessions)->sessionStart() outside of Request. You should use onSessionStart.');
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 159 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
169
            return;
170
        }
171
        $f = true; // hack to avoid a sort of "race condition"
172
        $this->onSessionStart(function ($event) use (&$f) {
0 ignored issues
show
Bug introduced by
The method onSessionStart() does not exist on PHPDaemon\HTTPRequest\Generic. Did you maybe mean onSessionStartEvent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
Unused Code introduced by
The parameter $event is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
173
            $f = false;
174
            $this->wakeup();
0 ignored issues
show
Bug introduced by
It seems like wakeup() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
175
        });
176
        if ($f) {
177
            $this->sleep($this->sessionStartTimeout);
0 ignored issues
show
Documentation introduced by
The property $sessionStartTimeout is declared protected in PHPDaemon\Traits\Sessions. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
178
        }
179
    }
180
181
    /**
182
     * Start new session
183
     * @param  callable $cb Callback
0 ignored issues
show
Documentation introduced by
Should the type for parameter $cb not be callable|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
184
     * @return void
185
     */
186
    protected function sessionStartNew($cb = null)
187
    {
188
        FileSystem::tempnam(session_save_path(), $this->sessionPrefix, function ($fp) use ($cb) {
189
            if (!$fp) {
190
                $cb(false);
191
                return;
192
            }
193
194
            $this->sessionFp = $fp;
195
            $this->sessionId = substr(basename($fp->path), mb_orig_strlen($this->sessionPrefix));
196
            $this->setcookie(
0 ignored issues
show
Bug introduced by
It seems like setcookie() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
197
                ini_get('session.name'),
198
                $this->sessionId,
199
                ini_get('session.cookie_lifetime'),
200
                ini_get('session.cookie_path'),
201
                ini_get('session.cookie_domain'),
202
                ini_get('session.cookie_secure'),
203
                ini_get('session.cookie_httponly')
204
            );
205
206
            $cb(true);
207
        });
208
    }
209
210
    /**
211
     * Encodes session data
212
     * @return string|false
213
     */
214
    protected function sessionEncode()
215
    {
216
        $type = ini_get('session.serialize_handler');
217
        if ($type === 'php') {
218
            return $this->serializePHP($this->getSessionState());
219
        }
220
        if ($type === 'php_binary') {
221
            return igbinary_serialize($this->getSessionState());
222
        }
223
        return false;
224
    }
225
    
226
    /**
227
     * Set session state
228
     * @param mixed $var
229
     * @return void
230
     */
231
    protected function setSessionState($var)
232
    {
233
        $this->attrs->session = $var;
0 ignored issues
show
Bug introduced by
The property attrs does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
234
    }
235
236
    /**
237
     * Get session state
238
     * @return mixed
239
     */
240
    protected function getSessionState()
241
    {
242
        return $this->attrs->session;
243
    }
244
245
    /**
246
     * Decodes session data
247
     * @param  string  $str Data
248
     * @return boolean
249
     */
250
    protected function sessionDecode($str)
251
    {
252
        $type = ini_get('session.serialize_handler');
253
        if ($type === 'php') {
254
            $this->setSessionState($this->unserializePHP($str));
255
            return true;
256
        }
257
        if ($type === 'php_binary') {
258
            $this->setSessionState(igbinary_unserialize($str));
259
            return true;
260
        }
261
        return false;
262
    }
263
264
    /**
265
     * session_encode() - clone, which not require session_start()
266
     * @see    http://www.php.net/manual/en/function.session-encode.php
267
     * @param  array  $array
268
     * @return string
269
     */
270
    public function serializePHP($array)
271
    {
272
        $raw = '';
273
        $line = 0;
274
        $keys = array_keys($array);
275
276
        foreach ($keys as $key) {
277
            $value = $array[$key];
278
            $line++;
279
            $raw .= $key . '|';
280
            if (is_array($value) && isset($value['huge_recursion_blocker_we_hope'])) {
281
                $raw .= 'R:' . $value['huge_recursion_blocker_we_hope'] . ';';
282
            } else {
283
                $raw .= serialize($value);
284
            }
285
            $array[$key] = array('huge_recursion_blocker_we_hope' => $line);
286
        }
287
288
        return $raw;
289
    }
290
291
    /**
292
     * session_decode() - clone, which not require session_start()
293
     * @see    http://www.php.net/manual/en/function.session-decode.php#108037
294
     * @param  string $session_data
295
     * @return array
296
     */
297
    protected function unserializePHP($session_data)
298
    {
299
        $return_data = array();
300
        $offset = 0;
301
302
        while ($offset < mb_orig_strlen($session_data)) {
303
            if (!strstr(substr($session_data, $offset), "|")) {
304
                return $return_data;
305
                //throw new \Exception("invalid session data, remaining: " . substr($session_data, $offset));
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
306
            }
307
            $pos = mb_orig_strpos($session_data, "|", $offset);
308
            $num = $pos - $offset;
309
            $varname = substr($session_data, $offset, $num);
310
            $offset += $num + 1;
311
            $data = unserialize(substr($session_data, $offset));
312
            $return_data[$varname] = $data;
313
            $offset += mb_orig_strlen(serialize($data));
314
        }
315
316
        return $return_data;
317
    }
318
}
319