1<?php
2/**
3 * Zend Framework (http://framework.zend.com/)
4 *
5 * @link      http://github.com/zendframework/zf2 for the canonical source repository
6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
7 * @license   http://framework.zend.com/license/new-bsd New BSD License
8 */
9
10namespace Zend\Session\Config;
11
12use Traversable;
13use Zend\Session\Exception;
14use Zend\Validator\Hostname as HostnameValidator;
15
16/**
17 * Standard session configuration
18 */
19class StandardConfig implements ConfigInterface
20{
21    /**
22     * session.name
23     *
24     * @var string
25     */
26    protected $name;
27
28    /**
29     * session.save_path
30     *
31     * @var string
32     */
33    protected $savePath;
34
35    /**
36     * session.cookie_lifetime
37     *
38     * @var int
39     */
40    protected $cookieLifetime;
41
42    /**
43     * session.cookie_path
44     *
45     * @var string
46     */
47    protected $cookiePath;
48
49    /**
50     * session.cookie_domain
51     *
52     * @var string
53     */
54    protected $cookieDomain;
55
56    /**
57     * session.cookie_secure
58     *
59     * @var bool
60     */
61    protected $cookieSecure;
62
63    /**
64     * session.cookie_httponly
65     *
66     * @var bool
67     */
68    protected $cookieHttpOnly;
69
70    /**
71     * remember_me_seconds
72     *
73     * @var int
74     */
75    protected $rememberMeSeconds;
76
77    /**
78     * session.use_cookies
79     *
80     * @var bool
81     */
82    protected $useCookies;
83
84    /**
85     * All options
86     *
87     * @var array
88     */
89    protected $options = array();
90
91    /**
92     * Set many options at once
93     *
94     * If a setter method exists for the key, that method will be called;
95     * otherwise, a standard option will be set with the value provided via
96     * {@link setOption()}.
97     *
98     * @param  array|Traversable $options
99     * @return StandardConfig
100     * @throws Exception\InvalidArgumentException
101     */
102    public function setOptions($options)
103    {
104        if (!is_array($options) && !$options instanceof Traversable) {
105            throw new Exception\InvalidArgumentException(sprintf(
106                'Parameter provided to %s must be an array or Traversable',
107                __METHOD__
108            ));
109        }
110
111        foreach ($options as $key => $value) {
112            $setter = 'set' . str_replace(' ', '', ucwords(str_replace('_', ' ', $key)));
113            if (method_exists($this, $setter)) {
114                $this->{$setter}($value);
115            } else {
116                $this->setOption($key, $value);
117            }
118        }
119        return $this;
120    }
121
122    /**
123     * Get all options set
124     *
125     * @return array
126     */
127    public function getOptions()
128    {
129        return $this->options;
130    }
131
132    /**
133     * Set an individual option
134     *
135     * Keys are normalized to lowercase. After setting internally, calls
136     * {@link setStorageOption()} to allow further processing.
137     *
138     *
139     * @param  string $option
140     * @param  mixed $value
141     * @return StandardConfig
142     */
143    public function setOption($option, $value)
144    {
145        $option                 = strtolower($option);
146        $this->options[$option] = $value;
147        $this->setStorageOption($option, $value);
148        return $this;
149    }
150
151    /**
152     * Get an individual option
153     *
154     * Keys are normalized to lowercase. If the option is not found, attempts
155     * to retrieve it via {@link getStorageOption()}; if a value is returned
156     * from that method, it will be set as the internal value and returned.
157     *
158     * Returns null for unfound options
159     *
160     * @param  string $option
161     * @return mixed
162     */
163    public function getOption($option)
164    {
165        $option = strtolower($option);
166        if (array_key_exists($option, $this->options)) {
167            return $this->options[$option];
168        }
169
170        $value = $this->getStorageOption($option);
171        if (null !== $value) {
172            $this->setOption($option, $value);
173            return $value;
174        }
175
176        return;
177    }
178
179    /**
180     * Check to see if an internal option has been set for the key provided.
181     *
182     * @param  string $option
183     * @return bool
184     */
185    public function hasOption($option)
186    {
187        $option = strtolower($option);
188        return array_key_exists($option, $this->options);
189    }
190
191    /**
192     * Set storage option in backend configuration store
193     *
194     * Does nothing in this implementation; others might use it to set things
195     * such as INI settings.
196     *
197     * @param  string $storageName
198     * @param  mixed $storageValue
199     * @return StandardConfig
200     */
201    public function setStorageOption($storageName, $storageValue)
202    {
203        return $this;
204    }
205
206    /**
207     * Retrieve a storage option from a backend configuration store
208     *
209     * Used to retrieve default values from a backend configuration store.
210     *
211     * @param  string $storageOption
212     * @return mixed
213     */
214    public function getStorageOption($storageOption)
215    {
216        return;
217    }
218
219    /**
220     * Set session.save_path
221     *
222     * @param  string $savePath
223     * @return StandardConfig
224     * @throws Exception\InvalidArgumentException on invalid path
225     */
226    public function setSavePath($savePath)
227    {
228        if (!is_dir($savePath)) {
229            throw new Exception\InvalidArgumentException('Invalid save_path provided; not a directory');
230        }
231        if (!is_writable($savePath)) {
232            throw new Exception\InvalidArgumentException('Invalid save_path provided; not writable');
233        }
234
235        $this->savePath = $savePath;
236        $this->setStorageOption('save_path', $savePath);
237        return $this;
238    }
239
240    /**
241     * Set session.save_path
242     *
243     * @return string|null
244     */
245    public function getSavePath()
246    {
247        if (null === $this->savePath) {
248            $this->savePath = $this->getStorageOption('save_path');
249        }
250        return $this->savePath;
251    }
252
253    /**
254     * Set session.name
255     *
256     * @param  string $name
257     * @return StandardConfig
258     * @throws Exception\InvalidArgumentException
259     */
260    public function setName($name)
261    {
262        $this->name = (string) $name;
263        if (empty($this->name)) {
264            throw new Exception\InvalidArgumentException('Invalid session name; cannot be empty');
265        }
266        $this->setStorageOption('name', $this->name);
267        return $this;
268    }
269
270    /**
271     * Get session.name
272     *
273     * @return null|string
274     */
275    public function getName()
276    {
277        if (null === $this->name) {
278            $this->name = $this->getStorageOption('name');
279        }
280        return $this->name;
281    }
282
283    /**
284     * Set session.gc_probability
285     *
286     * @param  int $gcProbability
287     * @return StandardConfig
288     * @throws Exception\InvalidArgumentException
289     */
290    public function setGcProbability($gcProbability)
291    {
292        if (!is_numeric($gcProbability)) {
293            throw new Exception\InvalidArgumentException('Invalid gc_probability; must be numeric');
294        }
295        $gcProbability = (int) $gcProbability;
296        if (0 > $gcProbability || 100 < $gcProbability) {
297            throw new Exception\InvalidArgumentException('Invalid gc_probability; must be a percentage');
298        }
299        $this->setOption('gc_probability', $gcProbability);
300        $this->setStorageOption('gc_probability', $gcProbability);
301        return $this;
302    }
303
304    /**
305     * Get session.gc_probability
306     *
307     * @return int
308     */
309    public function getGcProbability()
310    {
311        if (!isset($this->options['gc_probability'])) {
312            $this->options['gc_probability'] = $this->getStorageOption('gc_probability');
313        }
314
315        return $this->options['gc_probability'];
316    }
317
318    /**
319     * Set session.gc_divisor
320     *
321     * @param  int $gcDivisor
322     * @return StandardConfig
323     * @throws Exception\InvalidArgumentException
324     */
325    public function setGcDivisor($gcDivisor)
326    {
327        if (!is_numeric($gcDivisor)) {
328            throw new Exception\InvalidArgumentException('Invalid gc_divisor; must be numeric');
329        }
330        $gcDivisor = (int) $gcDivisor;
331        if (1 > $gcDivisor) {
332            throw new Exception\InvalidArgumentException('Invalid gc_divisor; must be a positive integer');
333        }
334        $this->setOption('gc_divisor', $gcDivisor);
335        $this->setStorageOption('gc_divisor', $gcDivisor);
336        return $this;
337    }
338
339    /**
340     * Get session.gc_divisor
341     *
342     * @return int
343     */
344    public function getGcDivisor()
345    {
346        if (!isset($this->options['gc_divisor'])) {
347            $this->options['gc_divisor'] = $this->getStorageOption('gc_divisor');
348        }
349
350        return $this->options['gc_divisor'];
351    }
352
353    /**
354     * Set gc_maxlifetime
355     *
356     * @param  int $gcMaxlifetime
357     * @return StandardConfig
358     * @throws Exception\InvalidArgumentException
359     */
360    public function setGcMaxlifetime($gcMaxlifetime)
361    {
362        if (!is_numeric($gcMaxlifetime)) {
363            throw new Exception\InvalidArgumentException('Invalid gc_maxlifetime; must be numeric');
364        }
365
366        $gcMaxlifetime = (int) $gcMaxlifetime;
367        if (1 > $gcMaxlifetime) {
368            throw new Exception\InvalidArgumentException('Invalid gc_maxlifetime; must be a positive integer');
369        }
370
371        $this->setOption('gc_maxlifetime', $gcMaxlifetime);
372        $this->setStorageOption('gc_maxlifetime', $gcMaxlifetime);
373        return $this;
374    }
375
376    /**
377     * Get session.gc_maxlifetime
378     *
379     * @return int
380     */
381    public function getGcMaxlifetime()
382    {
383        if (!isset($this->options['gc_maxlifetime'])) {
384            $this->options['gc_maxlifetime'] = $this->getStorageOption('gc_maxlifetime');
385        }
386
387        return $this->options['gc_maxlifetime'];
388    }
389
390    /**
391     * Set session.cookie_lifetime
392     *
393     * @param  int $cookieLifetime
394     * @return StandardConfig
395     * @throws Exception\InvalidArgumentException
396     */
397    public function setCookieLifetime($cookieLifetime)
398    {
399        if (!is_numeric($cookieLifetime)) {
400            throw new Exception\InvalidArgumentException('Invalid cookie_lifetime; must be numeric');
401        }
402        if (0 > $cookieLifetime) {
403            throw new Exception\InvalidArgumentException(
404                'Invalid cookie_lifetime; must be a positive integer or zero'
405            );
406        }
407
408        $this->cookieLifetime = (int) $cookieLifetime;
409        $this->setStorageOption('cookie_lifetime', $this->cookieLifetime);
410        return $this;
411    }
412
413    /**
414     * Get session.cookie_lifetime
415     *
416     * @return int
417     */
418    public function getCookieLifetime()
419    {
420        if (null === $this->cookieLifetime) {
421            $this->cookieLifetime = $this->getStorageOption('cookie_lifetime');
422        }
423        return $this->cookieLifetime;
424    }
425
426    /**
427     * Set session.cookie_path
428     *
429     * @param  string $cookiePath
430     * @return StandardConfig
431     * @throws Exception\InvalidArgumentException
432     */
433    public function setCookiePath($cookiePath)
434    {
435        $cookiePath = (string) $cookiePath;
436
437        $test = parse_url($cookiePath, PHP_URL_PATH);
438        if ($test != $cookiePath || '/' != $test[0]) {
439            throw new Exception\InvalidArgumentException('Invalid cookie path');
440        }
441
442        $this->cookiePath = $cookiePath;
443        $this->setStorageOption('cookie_path', $cookiePath);
444        return $this;
445    }
446
447    /**
448     * Get session.cookie_path
449     *
450     * @return string
451     */
452    public function getCookiePath()
453    {
454        if (null === $this->cookiePath) {
455            $this->cookiePath = $this->getStorageOption('cookie_path');
456        }
457        return $this->cookiePath;
458    }
459
460    /**
461     * Set session.cookie_domain
462     *
463     * @param  string $cookieDomain
464     * @return StandardConfig
465     * @throws Exception\InvalidArgumentException
466     */
467    public function setCookieDomain($cookieDomain)
468    {
469        if (!is_string($cookieDomain)) {
470            throw new Exception\InvalidArgumentException('Invalid cookie domain: must be a string');
471        }
472
473        $validator = new HostnameValidator(HostnameValidator::ALLOW_ALL);
474
475        if (!empty($cookieDomain) && !$validator->isValid($cookieDomain)) {
476            throw new Exception\InvalidArgumentException(
477                'Invalid cookie domain: ' . implode('; ', $validator->getMessages())
478            );
479        }
480
481        $this->cookieDomain = $cookieDomain;
482        $this->setStorageOption('cookie_domain', $cookieDomain);
483        return $this;
484    }
485
486    /**
487     * Get session.cookie_domain
488     *
489     * @return string
490     */
491    public function getCookieDomain()
492    {
493        if (null === $this->cookieDomain) {
494            $this->cookieDomain = $this->getStorageOption('cookie_domain');
495        }
496        return $this->cookieDomain;
497    }
498
499    /**
500     * Set session.cookie_secure
501     *
502     * @param  bool $cookieSecure
503     * @return StandardConfig
504     */
505    public function setCookieSecure($cookieSecure)
506    {
507        $this->cookieSecure = (bool) $cookieSecure;
508        $this->setStorageOption('cookie_secure', $this->cookieSecure);
509        return $this;
510    }
511
512    /**
513     * Get session.cookie_secure
514     *
515     * @return bool
516     */
517    public function getCookieSecure()
518    {
519        if (null === $this->cookieSecure) {
520            $this->cookieSecure = $this->getStorageOption('cookie_secure');
521        }
522        return $this->cookieSecure;
523    }
524
525    /**
526     * Set session.cookie_httponly
527     *
528     * case sensitive method lookups in setOptions means this method has an
529     * unusual casing
530     *
531     * @param  bool $cookieHttpOnly
532     * @return StandardConfig
533     */
534    public function setCookieHttpOnly($cookieHttpOnly)
535    {
536        $this->cookieHttpOnly = (bool) $cookieHttpOnly;
537        $this->setStorageOption('cookie_httponly', $this->cookieHttpOnly);
538        return $this;
539    }
540
541    /**
542     * Get session.cookie_httponly
543     *
544     * @return bool
545     */
546    public function getCookieHttpOnly()
547    {
548        if (null === $this->cookieHttpOnly) {
549            $this->cookieHttpOnly = $this->getStorageOption('cookie_httponly');
550        }
551        return $this->cookieHttpOnly;
552    }
553
554    /**
555     * Set session.use_cookies
556     *
557     * @param  bool $useCookies
558     * @return StandardConfig
559     */
560    public function setUseCookies($useCookies)
561    {
562        $this->useCookies = (bool) $useCookies;
563        $this->setStorageOption('use_cookies', $this->useCookies);
564        return $this;
565    }
566
567    /**
568     * Get session.use_cookies
569     *
570     * @return bool
571     */
572    public function getUseCookies()
573    {
574        if (null === $this->useCookies) {
575            $this->useCookies = $this->getStorageOption('use_cookies');
576        }
577        return $this->useCookies;
578    }
579
580    /**
581     * Set session.entropy_file
582     *
583     * @param  string $entropyFile
584     * @return StandardConfig
585     * @throws Exception\InvalidArgumentException
586     */
587    public function setEntropyFile($entropyFile)
588    {
589        if (!is_readable($entropyFile)) {
590            throw new Exception\InvalidArgumentException(sprintf(
591                "Invalid entropy_file provided: '%s'; doesn't exist or not readable",
592                $entropyFile
593            ));
594        }
595
596        $this->setOption('entropy_file', $entropyFile);
597        $this->setStorageOption('entropy_file', $entropyFile);
598        return $this;
599    }
600
601    /**
602     * Get session.entropy_file
603     *
604     * @return string
605     */
606    public function getEntropyFile()
607    {
608        if (!isset($this->options['entropy_file'])) {
609            $this->options['entropy_file'] = $this->getStorageOption('entropy_file');
610        }
611
612        return $this->options['entropy_file'];
613    }
614
615    /**
616     * set session.entropy_length
617     *
618     * @param  int $entropyLength
619     * @return StandardConfig
620     * @throws Exception\InvalidArgumentException
621     */
622    public function setEntropyLength($entropyLength)
623    {
624        if (!is_numeric($entropyLength)) {
625            throw new Exception\InvalidArgumentException('Invalid entropy_length; must be numeric');
626        }
627        if (0 > $entropyLength) {
628            throw new Exception\InvalidArgumentException('Invalid entropy_length; must be a positive integer or zero');
629        }
630
631        $this->setOption('entropy_length', $entropyLength);
632        $this->setStorageOption('entropy_length', $entropyLength);
633        return $this;
634    }
635
636    /**
637     * Get session.entropy_length
638     *
639     * @return string
640     */
641    public function getEntropyLength()
642    {
643        if (!isset($this->options['entropy_length'])) {
644            $this->options['entropy_length'] = $this->getStorageOption('entropy_length');
645        }
646
647        return $this->options['entropy_length'];
648    }
649
650    /**
651     * Set session.cache_expire
652     *
653     * @param  int $cacheExpire
654     * @return StandardConfig
655     * @throws Exception\InvalidArgumentException
656     */
657    public function setCacheExpire($cacheExpire)
658    {
659        if (!is_numeric($cacheExpire)) {
660            throw new Exception\InvalidArgumentException('Invalid cache_expire; must be numeric');
661        }
662
663        $cacheExpire = (int) $cacheExpire;
664        if (1 > $cacheExpire) {
665            throw new Exception\InvalidArgumentException('Invalid cache_expire; must be a positive integer');
666        }
667
668        $this->setOption('cache_expire', $cacheExpire);
669        $this->setStorageOption('cache_expire', $cacheExpire);
670        return $this;
671    }
672
673    /**
674     * Get session.cache_expire
675     *
676     * @return string
677     */
678    public function getCacheExpire()
679    {
680        if (!isset($this->options['cache_expire'])) {
681            $this->options['cache_expire'] = $this->getStorageOption('cache_expire');
682        }
683
684        return $this->options['cache_expire'];
685    }
686
687    /**
688     * Set session.hash_bits_per_character
689     *
690     * @param  int $hashBitsPerCharacter
691     * @return StandardConfig
692     * @throws Exception\InvalidArgumentException
693     */
694    public function setHashBitsPerCharacter($hashBitsPerCharacter)
695    {
696        if (!is_numeric($hashBitsPerCharacter)) {
697            throw new Exception\InvalidArgumentException('Invalid hash bits per character provided');
698        }
699        $hashBitsPerCharacter = (int) $hashBitsPerCharacter;
700        $this->setOption('hash_bits_per_character', $hashBitsPerCharacter);
701        $this->setStorageOption('hash_bits_per_character', $hashBitsPerCharacter);
702        return $this;
703    }
704
705    /**
706     * Get session.hash_bits_per_character
707     *
708     * @return string
709     */
710    public function getHashBitsPerCharacter()
711    {
712        if (!isset($this->options['hash_bits_per_character'])) {
713            $this->options['hash_bits_per_character'] = $this->getStorageOption('hash_bits_per_character');
714        }
715
716        return $this->options['hash_bits_per_character'];
717    }
718
719    /**
720     * Set remember_me_seconds
721     *
722     * @param  int $rememberMeSeconds
723     * @return StandardConfig
724     * @throws Exception\InvalidArgumentException
725     */
726    public function setRememberMeSeconds($rememberMeSeconds)
727    {
728        if (!is_numeric($rememberMeSeconds)) {
729            throw new Exception\InvalidArgumentException('Invalid remember_me_seconds; must be numeric');
730        }
731
732        $rememberMeSeconds = (int) $rememberMeSeconds;
733        if (1 > $rememberMeSeconds) {
734            throw new Exception\InvalidArgumentException('Invalid remember_me_seconds; must be a positive integer');
735        }
736
737        $this->rememberMeSeconds = $rememberMeSeconds;
738        $this->setStorageOption('remember_me_seconds', $rememberMeSeconds);
739        return $this;
740    }
741
742    /**
743     * Get remember_me_seconds
744     *
745     * @return int
746     */
747    public function getRememberMeSeconds()
748    {
749        if (null === $this->rememberMeSeconds) {
750            $this->rememberMeSeconds = $this->getStorageOption('remember_me_seconds');
751        }
752        return $this->rememberMeSeconds;
753    }
754
755    /**
756     * Cast configuration to an array
757     *
758     * @return array
759     */
760    public function toArray()
761    {
762        $extraOpts = array(
763            'cookie_domain'       => $this->getCookieDomain(),
764            'cookie_httponly'     => $this->getCookieHttpOnly(),
765            'cookie_lifetime'     => $this->getCookieLifetime(),
766            'cookie_path'         => $this->getCookiePath(),
767            'cookie_secure'       => $this->getCookieSecure(),
768            'name'                => $this->getName(),
769            'remember_me_seconds' => $this->getRememberMeSeconds(),
770            'save_path'           => $this->getSavePath(),
771            'use_cookies'         => $this->getUseCookies(),
772        );
773        return array_merge($this->options, $extraOpts);
774    }
775
776    /**
777     * Intercept get*() and set*() methods
778     *
779     * Intercepts getters and setters and passes them to getOption() and setOption(),
780     * respectively.
781     *
782     * @param  string $method
783     * @param  array $args
784     * @return mixed
785     * @throws Exception\BadMethodCallException on non-getter/setter method
786     */
787    public function __call($method, $args)
788    {
789        $prefix = substr($method, 0, 3);
790        $option = substr($method, 3);
791        $key    = strtolower(preg_replace('#(?<=[a-z])([A-Z])#', '_\1', $option));
792
793        if ($prefix === 'set') {
794            $value  = array_shift($args);
795            return $this->setOption($key, $value);
796        } elseif ($prefix === 'get') {
797            return $this->getOption($key);
798        } else {
799            throw new Exception\BadMethodCallException(sprintf(
800                'Method "%s" does not exist in %s',
801                $method,
802                get_class($this)
803            ));
804        }
805    }
806}
807