1<?php
2/**
3 * Copyright 2014-2017 Horde LLC (http://www.horde.org/)
4 *
5 * See the enclosed file COPYING for license information (LGPL). If you
6 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
7 *
8 * @category  Horde
9 * @copyright 2014-2017 Horde LLC
10 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
11 * @package   Mime
12 */
13
14/**
15 * Provides methods to manipulate/query MIME IDs.
16 *
17 * @author    Michael Slusarz <slusarz@horde.org>
18 * @category  Horde
19 * @copyright 2014-2017 Horde LLC
20 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
21 * @package   Mime
22 * @since     2.5.0
23 */
24class Horde_Mime_Id
25{
26    /* Constants for idArithmetic() method. */
27    const ID_DOWN = 1;
28    const ID_NEXT = 2;
29    const ID_PREV = 3;
30    const ID_UP = 4;
31
32    /**
33     * MIME ID.
34     *
35     * @var string
36     */
37    public $id;
38
39    /**
40     * Constructor.
41     *
42     * @param string $id  MIME ID.
43     */
44    public function __construct($id)
45    {
46        $this->id = $id;
47    }
48
49    /**
50     */
51    public function __toString()
52    {
53        return $this->id;
54    }
55
56    /**
57     * Performs MIME ID "arithmetic".
58     *
59     * @param string $action  One of:
60     *   - ID_DOWN: ID of child. Note: ID_DOWN will first traverse to "$id.0"
61     *              if given an ID *NOT* of the form "$id.0". If given an ID of
62     *              the form "$id.0", ID_DOWN will traverse to "$id.1". This
63     *              behavior can be avoided if 'no_rfc822' option is set.
64     *   - ID_NEXT: ID of next sibling.
65     *   - ID_PREV: ID of previous sibling.
66     *   - ID_UP: ID of parent. Note: ID_UP will first traverse to "$id.0" if
67     *            given an ID *NOT* of the form "$id.0". If given an ID of the
68     *            form "$id.0", ID_UP will traverse to "$id". This behavior can
69     *            be avoided if 'no_rfc822' option is set.
70     * @param array $options  Additional options:
71     *   - count: (integer) How many levels to traverse.
72     *            DEFAULT: 1
73     *   - no_rfc822: (boolean) Don't traverse RFC 822 sub-levels.
74     *                DEFAULT: false
75     *
76     * @return mixed  The resulting ID string, or null if that ID can not
77     *                exist.
78     */
79    public function idArithmetic($action, array $options = array())
80    {
81        return $this->_idArithmetic($this->id, $action, array_merge(array(
82            'count' => 1
83        ), $options));
84    }
85
86    /**
87     * @see idArithmetic()
88     */
89    protected function _idArithmetic($id, $action, $options)
90    {
91        $pos = strrpos($id, '.');
92        $end = ($pos === false) ? $id : substr($id, $pos + 1);
93
94        switch ($action) {
95        case self::ID_DOWN:
96            if ($end == '0') {
97                $id = ($pos === false) ? 1 : substr_replace($id, '1', $pos + 1);
98            } else {
99                $id .= empty($options['no_rfc822']) ? '.0' : '.1';
100            }
101            break;
102
103        case self::ID_NEXT:
104            ++$end;
105            $id = ($pos === false) ? $end : substr_replace($id, $end, $pos + 1);
106            break;
107
108        case self::ID_PREV:
109            if (($end == '0') ||
110                (empty($options['no_rfc822']) && ($end == '1'))) {
111                $id = null;
112            } elseif ($pos === false) {
113                $id = --$end;
114            } else {
115                $id = substr_replace($id, --$end, $pos + 1);
116            }
117            break;
118
119        case self::ID_UP:
120            if ($pos === false) {
121                $id = ($end == '0') ? null : '0';
122            } elseif (!empty($options['no_rfc822']) || ($end == '0')) {
123                $id = substr($id, 0, $pos);
124            } else {
125                $id = substr_replace($id, '0', $pos + 1);
126            }
127            break;
128        }
129
130        return (!is_null($id) && --$options['count'])
131            ? $this->_idArithmetic($id, $action, $options)
132            : $id;
133    }
134
135    /**
136     * Determines if a given MIME ID lives underneath a base ID.
137     *
138     * @param string $id  The MIME ID to query.
139     *
140     * @return boolean  Whether $id lives under the base ID ($this->id).
141     */
142    public function isChild($id)
143    {
144        $base = (substr($this->id, -2) == '.0')
145            ? substr($this->id, 0, -1)
146            : rtrim($this->id, '.') . '.';
147
148        return ((($base == 0) && ($id != 0)) ||
149                (strpos(strval($id), strval($base)) === 0));
150    }
151
152}
153