aboutsummaryrefslogtreecommitdiffhomepage
path: root/application/api/ApiUtils.php
blob: eb1ca9bc2b6230e944350759c33386acced2770f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
<?php
namespace Shaarli\Api;

use Shaarli\Api\Exceptions\ApiAuthorizationException;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Http\Base64Url;

/**
 * REST API utilities
 */
class ApiUtils
{
    /**
     * Validates a JWT token authenticity.
     *
     * @param string $token  JWT token extracted from the headers.
     * @param string $secret API secret set in the settings.
     *
     * @return bool true on success
     *
     * @throws ApiAuthorizationException the token is not valid.
     */
    public static function validateJwtToken($token, $secret)
    {
        $parts = explode('.', $token);
        if (count($parts) != 3 || strlen($parts[0]) == 0 || strlen($parts[1]) == 0) {
            throw new ApiAuthorizationException('Malformed JWT token');
        }

        $genSign = Base64Url::encode(hash_hmac('sha512', $parts[0] .'.'. $parts[1], $secret, true));
        if ($parts[2] != $genSign) {
            throw new ApiAuthorizationException('Invalid JWT signature');
        }

        $header = json_decode(Base64Url::decode($parts[0]));
        if ($header === null) {
            throw new ApiAuthorizationException('Invalid JWT header');
        }

        $payload = json_decode(Base64Url::decode($parts[1]));
        if ($payload === null) {
            throw new ApiAuthorizationException('Invalid JWT payload');
        }

        if (empty($payload->iat)
            || $payload->iat > time()
            || time() - $payload->iat > ApiMiddleware::$TOKEN_DURATION
        ) {
            throw new ApiAuthorizationException('Invalid JWT issued time');
        }

        return true;
    }

    /**
     * Format a Link for the REST API.
     *
     * @param Bookmark $bookmark Bookmark data read from the datastore.
     * @param string   $indexUrl Shaarli's index URL (used for relative URL).
     *
     * @return array Link data formatted for the REST API.
     */
    public static function formatLink($bookmark, $indexUrl)
    {
        $out['id'] = $bookmark->getId();
        // Not an internal link
        if (! $bookmark->isNote()) {
            $out['url'] = $bookmark->getUrl();
        } else {
            $out['url'] = rtrim($indexUrl, '/') . '/' . ltrim($bookmark->getUrl(), '/');
        }
        $out['shorturl'] = $bookmark->getShortUrl();
        $out['title'] = $bookmark->getTitle();
        $out['description'] = $bookmark->getDescription();
        $out['tags'] = $bookmark->getTags();
        $out['private'] = $bookmark->isPrivate();
        $out['created'] = $bookmark->getCreated()->format(\DateTime::ATOM);
        if (! empty($bookmark->getUpdated())) {
            $out['updated'] = $bookmark->getUpdated()->format(\DateTime::ATOM);
        } else {
            $out['updated'] = '';
        }
        return $out;
    }

    /**
     * Convert a link given through a request, to a valid Bookmark for the datastore.
     *
     * If no URL is provided, it will generate a local note URL.
     * If no title is provided, it will use the URL as title.
     *
     * @param array|null  $input          Request Link.
     * @param bool        $defaultPrivate Setting defined if a bookmark is private by default.
     *
     * @return Bookmark instance.
     */
    public static function buildBookmarkFromRequest(?array $input, bool $defaultPrivate): Bookmark
    {
        $bookmark = new Bookmark();
        $url = ! empty($input['url']) ? cleanup_url($input['url']) : '';
        if (isset($input['private'])) {
            $private = filter_var($input['private'], FILTER_VALIDATE_BOOLEAN);
        } else {
            $private = $defaultPrivate;
        }

        $bookmark->setTitle(! empty($input['title']) ? $input['title'] : '');
        $bookmark->setUrl($url);
        $bookmark->setDescription(! empty($input['description']) ? $input['description'] : '');
        $bookmark->setTags(! empty($input['tags']) ? $input['tags'] : []);
        $bookmark->setPrivate($private);

        $created = \DateTime::createFromFormat(\DateTime::ATOM, $input['created'] ?? '');
        if ($created instanceof \DateTimeInterface) {
            $bookmark->setCreated($created);
        }
        $updated = \DateTime::createFromFormat(\DateTime::ATOM, $input['updated'] ?? '');
        if ($updated instanceof \DateTimeInterface) {
            $bookmark->setUpdated($updated);
        }

        return $bookmark;
    }

    /**
     * Update link fields using an updated link object.
     *
     * @param Bookmark $oldLink data
     * @param Bookmark $newLink data
     *
     * @return Bookmark $oldLink updated with $newLink values
     */
    public static function updateLink($oldLink, $newLink)
    {
        $oldLink->setTitle($newLink->getTitle());
        $oldLink->setUrl($newLink->getUrl());
        $oldLink->setDescription($newLink->getDescription());
        $oldLink->setTags($newLink->getTags());
        $oldLink->setPrivate($newLink->isPrivate());

        return $oldLink;
    }

    /**
     * Format a Tag for the REST API.
     *
     * @param string $tag         Tag name
     * @param int    $occurrences Number of bookmarks using this tag
     *
     * @return array Link data formatted for the REST API.
     */
    public static function formatTag($tag, $occurences)
    {
        return [
            'name'       => $tag,
            'occurrences' => $occurences,
        ];
    }
}