1<?php
2/**
3 * DokuWiki Plugin vimeo (Syntax Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Michael Große <dokuwiki@cosmocode.de>
7 */
8
9class syntax_plugin_vimeo extends DokuWiki_Syntax_Plugin
10{
11    /**
12     * @return string Syntax mode type
13     */
14    public function getType()
15    {
16        return 'substition';
17    }
18
19    /**
20     * @return string Paragraph type
21     */
22    public function getPType()
23    {
24        return 'block';
25    }
26
27    /**
28     * @return int Sort order - Low numbers go before high numbers
29     */
30    public function getSort()
31    {
32        return 100;
33    }
34
35    /**
36     * Connect lookup pattern to lexer.
37     *
38     * @param string $mode Parser mode
39     */
40    public function connectTo($mode)
41    {
42        $this->Lexer->addSpecialPattern('{{vimeoAlbum>.+?}}', $mode, 'plugin_vimeo');
43    }
44
45    /**
46     * Handle matches of the vimeo syntax
47     *
48     * @param string       $match   The match of the syntax
49     * @param int          $state   The state of the handler
50     * @param int          $pos     The position in the document
51     * @param Doku_Handler $handler The handler
52     *
53     * @return array Data for the renderer
54     */
55    public function handle($match, $state, $pos, Doku_Handler $handler)
56    {
57        $albumID = substr($match, strlen('{{vimeoAlbum>'), -2);
58        try {
59            $data = $this->getAlbumVideos($albumID);
60        } catch (Exception $e) {
61            $data = ['errors' => [$e->getMessage() . '; Code: ' . $e->getCode()]];
62        }
63
64        return $data;
65    }
66
67    /**
68     * Render xhtml output or metadata
69     *
70     * @param string        $mode     Renderer mode (supported modes: xhtml)
71     * @param Doku_Renderer $renderer The renderer
72     * @param array         $data     The data from the handler() function
73     *
74     * @return bool If rendering was successful.
75     */
76    public function render($mode, Doku_Renderer $renderer, $data)
77    {
78        if ($mode !== 'xhtml') {
79            return false;
80        }
81
82        $renderer->doc .= '<div class="plugin-vimeo-album">';
83
84        if (!empty($data['errors'])) {
85            foreach ($data['errors'] as $error) {
86                msg('Vimeo Plugin Error: ' . hsc($error), -1);
87            }
88        }
89
90        if (!empty($data['videos'])) {
91            $videos = $data['videos'];
92            foreach ($videos as $video) {
93                $this->renderVideo($renderer, $video);
94            }
95        }
96
97        $renderer->doc .= '</div>';
98
99        return true;
100    }
101
102    /**
103     * Get all video data for the given album id
104     *
105     * The albumID must be owned be the user that provided the configured access token
106     *
107     * This also gets the paged videos in further requests if there are more than 100 videos
108     *
109     * @param string $albumID
110     *
111     * @return array data for the videos in the album
112     */
113    protected function getAlbumVideos($albumID)
114    {
115        $accessToken = $this->getConf('accessToken');
116        if (empty($accessToken)) {
117            throw new RuntimeException('Vimeo access token not configured! Please see documentation.');
118        }
119
120        $fields = 'name,description,embed.html,pictures.sizes,privacy,release_time';
121        $endpoint = '/me/albums/' . $albumID . '/videos?sort=manual&per_page=100&fields=' . $fields;
122        $errors = [];
123        $respData = $this->sendVimeoRequest($accessToken, $endpoint, $errors);
124        $videos = $respData['data'];
125
126        if (!empty($respData['paging']['next'])) {
127            while (true) {
128                $respData = $this->sendVimeoRequest($accessToken, $respData['paging']['next'], $errors);
129                $videos = array_merge($videos, $respData['videos']);
130                if (empty($respData['paging']['next'])) {
131                    break;
132                }
133            }
134        }
135
136        return [
137            'videos' => $videos,
138            'errors' => $errors,
139        ];
140    }
141
142    /**
143     * Make a single request to Vimeo and return the parsed body
144     *
145     * @param string $accessToken The access token
146     * @param string $endpoint    The endpoint to which to connect, must begin with a /
147     * @param array  $errors      If the rate-limit is hit, then an error-message is written in here
148     *
149     * @return mixed
150     *
151     * @throws RuntimeException  If the server returns an error
152     */
153    protected function sendVimeoRequest($accessToken, $endpoint, &$errors)
154    {
155        $http = new \DokuHTTPClient();
156        $http->headers['Authorization'] = 'Bearer ' . $accessToken;
157        $http->agent = 'DokuWiki HTTP Client (Vimeo Plugin)';
158        $http->keep_alive = false;
159        $base = 'https://api.vimeo.com';
160        $url = $base . $endpoint;
161        $http->sendRequest($url);
162
163        $body = $http->resp_body;
164        $respData = json_decode($body, true);
165
166        if (!empty($respData['error'])) {
167            dbglog($http->resp_headers, __FILE__ . ': ' . __LINE__);
168            throw new RuntimeException(
169                $respData['error'] . ' ' . $respData['developer_message'],
170                $respData['error_code']
171            );
172        }
173
174        $remainingRateLimit = $http->resp_headers['x-ratelimit-remaining'];
175        if ($remainingRateLimit < 10) {
176            dbglog($http->resp_headers, __FILE__ . ': ' . __LINE__);
177            $errors[] = 'The remaining Vimeo rate-limit is very low. Please check back in 15min or later';
178        }
179
180        return $respData;
181    }
182
183    /**
184     * Render a preview image and put the video iframe-html into a data attribute
185     *
186     * This offers all available images in a srcset, so the browser can decide which to load
187     *
188     * @param Doku_Renderer $renderer
189     * @param array         $video    The video data
190     */
191    protected function renderVideo(Doku_Renderer $renderer, $video)
192    {
193        $title = hsc($video['name']);
194        if ($video['privacy']['embed'] === 'private') {
195            msg(sprintf($this->getLang('embed_deactivated'), $title), 2);
196            return;
197        }
198        $thumbnailWidthPercent = $this->getConf('thumbnailWidthPercent');
199        $widthAttr = 'style="width: ' . $thumbnailWidthPercent . '%;"';
200        $renderer->doc .= '<div class="plugin-vimeo-video"' . $widthAttr . ' data-videoiframe="' . hsc($video['embed']['html']) . '">';
201        $renderer->doc .= '<figure>';
202        $src = $video['pictures']['sizes'][2]['link_with_play_button'];
203        $srcset = [];
204        foreach ($video['pictures']['sizes'] as $picture) {
205            $srcset [] = $picture['link_with_play_button'] . ' ' . $picture['width'] . 'w';
206        }
207        $renderer->doc .= '<img srcset="' . implode(',', $srcset) . '" src="' . $src . '" alt="' . $title . '">';
208        $caption = $this->createCaption($video);
209        $renderer->doc .= '<figcaption>' . $caption . '</figcaption>';
210        $renderer->doc .= '</figure>';
211        $renderer->doc .= '</div>';
212    }
213
214    /**
215     * Build the caption for a video
216     *
217     * @param array $video the video data
218     *
219     * @return string HTML for the video caption
220     */
221    protected function createCaption($video) {
222        $title = '<span class="vimeo-video-title">' . hsc($video['name']) . '</span>';
223
224        $releaseDateObject = new \DateTime($video['release_time']);
225        $releaseTime = dformat($releaseDateObject->format('U'));
226        $releaseString = '<span class="vimeo-video-releaseTime">'
227            . $this->getLang('released')
228            . ' <time>' . $releaseTime .'</time></span>';
229
230        $description = "<span class='vimeo-video-description'>" . hsc($video['description']) . '</span>';
231
232        return $title . $releaseString . $description;
233
234    }
235}
236
237