├── shaarli2twitter
├── shaarli2twitter.css
├── shaarli2twitter.meta
├── edit_link.html
├── TwitterApi
│ ├── LICENSE.md
│ └── TwitterAPIExchange.php
├── shaarli2twitter.js
└── shaarli2twitter.php
├── LICENSE.md
└── README.md
/shaarli2twitter/shaarli2twitter.css:
--------------------------------------------------------------------------------
1 | #tweet-textarea textarea {
2 | min-height: 70px;
3 | }
4 |
--------------------------------------------------------------------------------
/shaarli2twitter/shaarli2twitter.meta:
--------------------------------------------------------------------------------
1 | description="Automatically publish your Shaares to your Twitter account. Read the documentation for configuration."
2 | parameters="TWITTER_API_KEY;TWITTER_API_SECRET;TWITTER_ACCESS_TOKEN;TWITTER_ACCESS_TOKEN_SECRET;TWITTER_TWEET_FORMAT;TWITTER_HIDE_URL"
3 | parameter.TWITTER_API_KEY="Consumer Key (API Key)"
4 | parameter.TWITTER_API_SECRET="Consumer Secret (API Secret)"
5 | parameter.TWITTER_ACCESS_TOKEN="Access Token"
6 | parameter.TWITTER_ACCESS_TOKEN_SECRET="Access Token Secret"
7 | parameter.TWITTER_TWEET_FORMAT="Placeholders: \${url} \${permalink} \${title} \${tags} \${description}"
8 | parameter.TWITTER_HIDE_URL="Hide \${url} when sharing a note to long to hold in a tweet. ['yes' or 'no'] Default: no"
9 |
--------------------------------------------------------------------------------
/shaarli2twitter/edit_link.html:
--------------------------------------------------------------------------------
1 |
6 |
17 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT LICENSE
2 |
3 | Copyright 2016 Arthur Hoareau
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/shaarli2twitter/TwitterApi/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2013 James Mallison (j7mbo.co.uk)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a
6 | copy of this software and associated documentation files (the "Software"),
7 | to deal in the Software without restriction, including without limitation
8 | the rights to use, copy, modify, merge, publish, distribute, sublicense,
9 | and/or sell copies of the Software, and to permit persons to whom the
10 | Software is furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included
13 | in all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21 | IN THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/shaarli2twitter/shaarli2twitter.js:
--------------------------------------------------------------------------------
1 | function toggleLinkDisplay(linkZone, link, textareaZone, textarea, show) {
2 | if (show) {
3 | linkZone.classList.remove('hidden')
4 | } else {
5 | linkZone.classList.add('hidden')
6 | toggleEditZone(linkZone, link, textareaZone, textarea, false);
7 | }
8 | }
9 |
10 | function toggleEditZone(linkZone, link, textareaZone, textarea, show) {
11 | if (show) {
12 | textareaZone.classList.remove('hidden');
13 | textarea.disabled = false;
14 | link.innerHTML = 'cancel';
15 | } else {
16 | textareaZone.classList.add('hidden');
17 | textarea.disabled = true;
18 | link.innerHTML = 'edit';
19 | }
20 | }
21 |
22 | document.addEventListener('DOMContentLoaded', function(event) {
23 | var privateInput = document.getElementsByName('lf_private')[0];
24 | var tweetInput = document.getElementsByName('tweet')[0];
25 | if (tweetInput == null) {
26 | return;
27 | }
28 |
29 | var editLinkZone = document.getElementById('s2t-edit-zone');
30 | var editLink = document.getElementById('s2t-edit');
31 | var textareaZone = document.getElementById('tweet-textarea');
32 | var textarea = document.querySelector('#tweet-textarea textarea');
33 |
34 | tweetInput.disabled = privateInput.checked;
35 | if (privateInput.checked) {
36 | toggleLinkDisplay(editLinkZone, editLink, textareaZone, textarea, false);
37 | }
38 | privateInput.addEventListener('click', function (event) {
39 | tweetInput.disabled = privateInput.checked;
40 | toggleLinkDisplay(editLinkZone, editLink, textareaZone, textarea, !tweetInput.disabled && tweetInput.checked);
41 | });
42 |
43 | tweetInput.addEventListener('click', function (event) {
44 | toggleLinkDisplay(editLinkZone, editLink, textareaZone, textarea, event.target.checked);
45 | });
46 |
47 |
48 | editLink.addEventListener('click', function (event) {
49 | event.preventDefault();
50 | toggleEditZone(editLinkZone, editLink, textareaZone, textarea, textarea.disabled)
51 | });
52 |
53 | var injectors = document.querySelectorAll('.s2t-inject');
54 | [].slice.call(injectors).forEach((injector) => {
55 | injector.addEventListener('click', function (event) {
56 | event.preventDefault();
57 | textarea.value += ' '+ event.target.getAttribute('data-content');
58 | });
59 | });
60 | });
61 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Shaarli2Twitter plugin
2 |
3 | This plugin uses the Twitter API to automatically tweet public links published on
4 | [Shaarli](https://github.com/shaarli/Shaarli/).
5 |
6 | Everytime a new link is shaared, use the "Tweet" checkbox in the form
7 | to post it on Twitter. You can edit every tweet format manually if needed.
8 |
9 | > Note: private links and link edits can't be tweeted.
10 |
11 | ## Requirements
12 |
13 | - PHP 7.1+
14 | - PHP cURL extension
15 | - PHP mbstring extension
16 | - Shaarli >= v0.8.1 (checkout the [release page](https://github.com/arthurhoaro/shaarli2twitter/releases) for compatibility)
17 |
18 | ## Installation
19 |
20 | Download the latest [release](https://github.com/ArthurHoaro/shaarli2twitter/releases),
21 | and put the folder `shaarli2twitter` under your `plugins/` directory.
22 |
23 | Then you can enable the plugin in the plugin administration page `http://shaarli.tld/?do=pluginadmin`.
24 |
25 | > Note: the foldername **must** be `shaarli2twitter` to work.
26 |
27 | Example in command lines:
28 |
29 | ```bash
30 | wget .tar.gz
31 | tar xfz .tar.gz
32 | mv /shaarli2twitter /path/to/shaarli/plugins
33 | ```
34 |
35 | ## Configuration
36 |
37 | For this plugin to work, you need to register your Shaarli as a Twitter application in your account,
38 | and retrieve 4 keys used to authenticate API calls.
39 |
40 | You must set this keys in the plugin administration page
41 |
42 | ### Step 1: Create an application
43 |
44 | While authenticated to your Twitter account, reach this page: https://apps.twitter.com/app/
45 |
46 | And Create a new app: name/description are not important, but you may need to put a valid website.
47 | Leave "Callback URL" blank.
48 |
49 | ### Step 2: Generate an access token
50 |
51 | In your freshly new app page, go to the tab called "Keys and Access Tokens".
52 |
53 | Then click on "Create my access token" at the bottom.
54 |
55 | ### Step 3: Plugin configuration
56 |
57 | You now have everything required to set up shaarli2twitter plugin.
58 |
59 | 
60 |
61 | ## Settings
62 |
63 | ### TWITTER_TWEET_FORMAT
64 |
65 | This setting shows the format of tweets. You can use placeholders which will be filled
66 | until the 280 chars limit is reached. Values may be truncated if the limit is reached.
67 |
68 | Available placeholders, in order of priority:
69 |
70 | * `${url}`: Shaare URL (will be automatically replaced as `t.co` links).
71 | * `${permalink}`: Shaare permalink (will be automatically replaced as `t.co` links).
72 | * `${title}`: Shaare title.
73 | * `${tags}`: List of shaare tags displayed as hashtags (`#tag1 #tag2`...).
74 | * `${description}`: Shaare description.
75 |
76 | Default format: `#Shaarli: ${title} ${url} ${tags}`
77 |
78 | Which will render, for example as:
79 |
80 | #Shaarli: Wikipedia, the free encyclopedia https://en.wikipedia.org/wiki/Main_Page #crowdsourcing #knowledge
81 |
82 | ### TWITTER_HIDE_URL
83 |
84 | Hide ${url} and/or ${permalink} when sharing a note to long to hold in a tweet.
85 |
86 | Values: `yes` to hide URL or `no` to keep them. Default value is `no`.
87 |
88 | ## License
89 |
90 | MIT License, see LICENSE.md.
91 |
--------------------------------------------------------------------------------
/shaarli2twitter/TwitterApi/TwitterAPIExchange.php:
--------------------------------------------------------------------------------
1 |
13 | * @license MIT License
14 | * @version 1.0.4
15 | * @link http://github.com/j7mbo/twitter-api-php
16 | */
17 | class TwitterAPIExchange
18 | {
19 | /**
20 | * @var string
21 | */
22 | private $oauth_access_token;
23 |
24 | /**
25 | * @var string
26 | */
27 | private $oauth_access_token_secret;
28 |
29 | /**
30 | * @var string
31 | */
32 | private $consumer_key;
33 |
34 | /**
35 | * @var string
36 | */
37 | private $consumer_secret;
38 |
39 | /**
40 | * @var array
41 | */
42 | private $postfields;
43 |
44 | /**
45 | * @var string
46 | */
47 | private $getfield;
48 |
49 | /**
50 | * @var mixed
51 | */
52 | protected $oauth;
53 |
54 | /**
55 | * @var string
56 | */
57 | public $url;
58 |
59 | /**
60 | * @var string
61 | */
62 | public $requestMethod;
63 |
64 | /**
65 | * Create the API access object. Requires an array of settings::
66 | * oauth access token, oauth access token secret, consumer key, consumer secret
67 | * These are all available by creating your own application on dev.twitter.com
68 | * Requires the cURL library
69 | *
70 | * @throws \Exception When cURL isn't installed or incorrect settings parameters are provided
71 | *
72 | * @param array $settings
73 | */
74 | public function __construct(array $settings)
75 | {
76 | if (!in_array('curl', get_loaded_extensions())) {
77 | throw new Exception('You need to install cURL, see: http://curl.haxx.se/docs/install.html');
78 | }
79 |
80 | if (!isset($settings['oauth_access_token'])
81 | || !isset($settings['oauth_access_token_secret'])
82 | || !isset($settings['consumer_key'])
83 | || !isset($settings['consumer_secret'])) {
84 | throw new Exception('Make sure you are passing in the correct parameters');
85 | }
86 |
87 | $this->oauth_access_token = $settings['oauth_access_token'];
88 | $this->oauth_access_token_secret = $settings['oauth_access_token_secret'];
89 | $this->consumer_key = $settings['consumer_key'];
90 | $this->consumer_secret = $settings['consumer_secret'];
91 | }
92 |
93 | /**
94 | * Set postfields array, example: array('screen_name' => 'J7mbo')
95 | *
96 | * @param array $array Array of parameters to send to API
97 | *
98 | * @throws \Exception When you are trying to set both get and post fields
99 | *
100 | * @return TwitterAPIExchange Instance of self for method chaining
101 | */
102 | public function setPostfields(array $array)
103 | {
104 | if (!is_null($this->getGetfield())) {
105 | throw new Exception('You can only choose get OR post fields.');
106 | }
107 |
108 | if (isset($array['status']) && substr($array['status'], 0, 1) === '@') {
109 | $array['status'] = sprintf("\0%s", $array['status']);
110 | }
111 |
112 | foreach ($array as $key => &$value) {
113 | if (is_bool($value)) {
114 | $value = ($value === true) ? 'true' : 'false';
115 | }
116 | }
117 |
118 | $this->postfields = $array;
119 |
120 | // rebuild oAuth
121 | if (isset($this->oauth['oauth_signature'])) {
122 | $this->buildOauth($this->url, $this->requestMethod);
123 | }
124 |
125 | return $this;
126 | }
127 |
128 | /**
129 | * Set getfield string, example: '?screen_name=J7mbo'
130 | *
131 | * @param string $string Get key and value pairs as string
132 | *
133 | * @throws \Exception
134 | *
135 | * @return \TwitterAPIExchange Instance of self for method chaining
136 | */
137 | public function setGetfield($string)
138 | {
139 | if (!is_null($this->getPostfields())) {
140 | throw new Exception('You can only choose get OR post fields.');
141 | }
142 |
143 | $getfields = preg_replace('/^\?/', '', explode('&', $string));
144 | $params = array();
145 |
146 | foreach ($getfields as $field) {
147 | if ($field !== '') {
148 | list($key, $value) = explode('=', $field);
149 | $params[$key] = $value;
150 | }
151 | }
152 |
153 | $this->getfield = '?' . http_build_query($params);
154 |
155 | return $this;
156 | }
157 |
158 | /**
159 | * Get getfield string (simple getter)
160 | *
161 | * @return string $this->getfields
162 | */
163 | public function getGetfield()
164 | {
165 | return $this->getfield;
166 | }
167 |
168 | /**
169 | * Get postfields array (simple getter)
170 | *
171 | * @return array $this->postfields
172 | */
173 | public function getPostfields()
174 | {
175 | return $this->postfields;
176 | }
177 |
178 | /**
179 | * Build the Oauth object using params set in construct and additionals
180 | * passed to this method. For v1.1, see: https://dev.twitter.com/docs/api/1.1
181 | *
182 | * @param string $url The API url to use. Example: https://api.twitter.com/1.1/search/tweets.json
183 | * @param string $requestMethod Either POST or GET
184 | *
185 | * @throws \Exception
186 | *
187 | * @return \TwitterAPIExchange Instance of self for method chaining
188 | */
189 | public function buildOauth($url, $requestMethod)
190 | {
191 | if (!in_array(strtolower($requestMethod), array('post', 'get'))) {
192 | throw new Exception('Request method must be either POST or GET');
193 | }
194 |
195 | $consumer_key = $this->consumer_key;
196 | $consumer_secret = $this->consumer_secret;
197 | $oauth_access_token = $this->oauth_access_token;
198 | $oauth_access_token_secret = $this->oauth_access_token_secret;
199 |
200 | $oauth = array(
201 | 'oauth_consumer_key' => $consumer_key,
202 | 'oauth_nonce' => time(),
203 | 'oauth_signature_method' => 'HMAC-SHA1',
204 | 'oauth_token' => $oauth_access_token,
205 | 'oauth_timestamp' => time(),
206 | 'oauth_version' => '1.0'
207 | );
208 |
209 | $getfield = $this->getGetfield();
210 |
211 | if (!is_null($getfield)) {
212 | $getfields = str_replace('?', '', explode('&', $getfield));
213 |
214 | foreach ($getfields as $g) {
215 | $split = explode('=', $g);
216 |
217 | /** In case a null is passed through **/
218 | if (isset($split[1])) {
219 | $oauth[$split[0]] = urldecode($split[1]);
220 | }
221 | }
222 | }
223 |
224 | $postfields = $this->getPostfields();
225 |
226 | if (!is_null($postfields)) {
227 | foreach ($postfields as $key => $value) {
228 | $oauth[$key] = $value;
229 | }
230 | }
231 |
232 | $base_info = $this->buildBaseString($url, $requestMethod, $oauth);
233 | $composite_key = rawurlencode($consumer_secret) . '&' . rawurlencode($oauth_access_token_secret);
234 | $oauth_signature = base64_encode(hash_hmac('sha1', $base_info, $composite_key, true));
235 | $oauth['oauth_signature'] = $oauth_signature;
236 |
237 | $this->url = $url;
238 | $this->requestMethod = $requestMethod;
239 | $this->oauth = $oauth;
240 |
241 | return $this;
242 | }
243 |
244 | /**
245 | * Perform the actual data retrieval from the API
246 | *
247 | * @param boolean $return If true, returns data. This is left in for backward compatibility reasons
248 | * @param array $curlOptions Additional Curl options for this request
249 | *
250 | * @throws \Exception
251 | *
252 | * @return string json If $return param is true, returns json data.
253 | */
254 | public function performRequest($return = true, $curlOptions = array())
255 | {
256 | if (!is_bool($return)) {
257 | throw new Exception('performRequest parameter must be true or false');
258 | }
259 |
260 | $header = array($this->buildAuthorizationHeader($this->oauth), 'Expect:');
261 |
262 | $getfield = $this->getGetfield();
263 | $postfields = $this->getPostfields();
264 |
265 | $options = array(
266 | CURLOPT_HTTPHEADER => $header,
267 | CURLOPT_HEADER => false,
268 | CURLOPT_URL => $this->url,
269 | CURLOPT_RETURNTRANSFER => true,
270 | CURLOPT_TIMEOUT => 10,
271 | ) + $curlOptions;
272 |
273 | if (!is_null($postfields)) {
274 | $options[CURLOPT_POSTFIELDS] = http_build_query($postfields);
275 | } else {
276 | if ($getfield !== '') {
277 | $options[CURLOPT_URL] .= $getfield;
278 | }
279 | }
280 |
281 | $feed = curl_init();
282 | curl_setopt_array($feed, $options);
283 | $json = curl_exec($feed);
284 |
285 | if (($error = curl_error($feed)) !== '') {
286 | curl_close($feed);
287 |
288 | throw new \Exception($error);
289 | }
290 |
291 | curl_close($feed);
292 |
293 | return $json;
294 | }
295 |
296 | /**
297 | * Private method to generate the base string used by cURL
298 | *
299 | * @param string $baseURI
300 | * @param string $method
301 | * @param array $params
302 | *
303 | * @return string Built base string
304 | */
305 | private function buildBaseString($baseURI, $method, $params)
306 | {
307 | $return = array();
308 | ksort($params);
309 |
310 | foreach ($params as $key => $value) {
311 | $return[] = rawurlencode($key) . '=' . rawurlencode($value);
312 | }
313 |
314 | return $method . "&" . rawurlencode($baseURI) . '&' . rawurlencode(implode('&', $return));
315 | }
316 |
317 | /**
318 | * Private method to generate authorization header used by cURL
319 | *
320 | * @param array $oauth Array of oauth data generated by buildOauth()
321 | *
322 | * @return string $return Header used by cURL for request
323 | */
324 | private function buildAuthorizationHeader(array $oauth)
325 | {
326 | $return = 'Authorization: OAuth ';
327 | $values = array();
328 |
329 | foreach ($oauth as $key => $value) {
330 | if (in_array($key, array('oauth_consumer_key', 'oauth_nonce', 'oauth_signature',
331 | 'oauth_signature_method', 'oauth_timestamp', 'oauth_token', 'oauth_version'))) {
332 | $values[] = "$key=\"" . rawurlencode($value) . "\"";
333 | }
334 | }
335 |
336 | $return .= implode(', ', $values);
337 | return $return;
338 | }
339 |
340 | /**
341 | * Helper method to perform our request
342 | *
343 | * @param string $url
344 | * @param string $method
345 | * @param string $data
346 | * @param array $curlOptions
347 | *
348 | * @throws \Exception
349 | *
350 | * @return string The json response from the server
351 | */
352 | public function request($url, $method = 'get', $data = null, $curlOptions = array())
353 | {
354 | if (strtolower($method) === 'get') {
355 | $this->setGetfield($data);
356 | } else {
357 | $this->setPostfields($data);
358 | }
359 |
360 | return $this->buildOauth($url, $method)->performRequest(true, $curlOptions);
361 | }
362 | }
363 |
--------------------------------------------------------------------------------
/shaarli2twitter/shaarli2twitter.php:
--------------------------------------------------------------------------------
1 | get('plugins.TWITTER_TWEET_FORMAT');
56 | if (empty($format)) {
57 | $conf->set('plugins.TWITTER_TWEET_FORMAT', TWEET_DEFAULT_FORMAT);
58 | }
59 |
60 | $hide = $conf->get('plugins.TWITTER_HIDE_URL');
61 | if (empty($hide)) {
62 | $conf->set('plugins.TWITTER_HIDE_URL', TWEET_HIDE_URL);
63 | }
64 |
65 | if (!s2t_is_config_valid($conf)) {
66 | return ['Please set up your Twitter API and token keys in plugin administration page.'];
67 | }
68 | }
69 |
70 | /**
71 | * Add the CSS file for editlink page
72 | *
73 | * @param array $data - header data.
74 | *
75 | * @return mixed - header data with s2t CSS file added.
76 | */
77 | function hook_shaarli2twitter_render_includes($data)
78 | {
79 | if ($data['_PAGE_'] == TemplatePage::EDIT_LINK) {
80 | $data['css_files'][] = PluginManager::$PLUGINS_PATH . '/shaarli2twitter/shaarli2twitter.css';
81 | }
82 |
83 | return $data;
84 | }
85 |
86 | /**
87 | * Add the JS file: disable the tweet button if the link is set to private.
88 | *
89 | * @param array $data New link values.
90 | * @param ConfigManager $conf instance.
91 | *
92 | * @return array $data with the JS file.
93 | */
94 | function hook_shaarli2twitter_render_footer($data, $conf)
95 | {
96 | if ($data['_PAGE_'] == TemplatePage::EDIT_LINK) {
97 | $data['js_files'][] = PluginManager::$PLUGINS_PATH . '/shaarli2twitter/shaarli2twitter.js';
98 | }
99 |
100 | return $data;
101 | }
102 |
103 | /**
104 | * Hook save link: will automatically publish a tweet when a new public link is shaared.
105 | *
106 | * @param array $data New link values.
107 | * @param ConfigManager $conf instance.
108 | *
109 | * @return array $data not altered.
110 | */
111 | function hook_shaarli2twitter_save_link($data, $conf)
112 | {
113 | // No tweet without config, for private links, or on edit.
114 | if (!s2t_is_config_valid($conf)
115 | || (isset($data['updated']) && $data['updated'] != false)
116 | || $data['private']
117 | || !isset($_POST['tweet'])
118 | ) {
119 | return $data;
120 | }
121 |
122 | // We make sure not to alter data
123 | $link = $data;
124 |
125 | // We will use an array to generate hashtags, then restore original shaare tags.
126 | $data['tags'] = array_values(array_filter(explode(' ', $data['tags'])));
127 | for ($i = 0; $i < count($data['tags']); $i++) {
128 | // Keep tags strictly alphanumerical because Twitter only allows that.
129 | $data['tags'][$i] = s2t_get_tagify($data['tags'][$i]);
130 | }
131 |
132 |
133 | $data['permalink'] = index_url($_SERVER) . 'shaare/' . $data['shorturl'];
134 |
135 | // In case of note, we use permalink
136 | if (s2t_is_link_note($data)) {
137 | $data['url'] = $data['permalink'];
138 | }
139 |
140 | $hide = $conf->get('plugins.TWITTER_HIDE_URL', TWEET_HIDE_URL);
141 | if (! empty($_POST['s2t-content'])) {
142 | $format = escape($_POST['s2t-content']);
143 | } else {
144 | $format = $conf->get('plugins.TWITTER_TWEET_FORMAT', TWEET_DEFAULT_FORMAT);
145 | }
146 | $tweet = s2t_format_tweet($data, $format, $hide);
147 | $response = s2t_tweet($conf, $tweet);
148 | $response = json_decode($response, true);
149 | // If an error has occurred, not blocking: just log it.
150 | if (isset($response['errors'])) {
151 | foreach ($response['errors'] as $error) {
152 | error_log('Twitter error ' . $error['code'] . ': ' . $error['message']);
153 | error_log('Tweet: "' . $tweet . '"');
154 | }
155 | }
156 |
157 | return $link;
158 | }
159 |
160 | /**
161 | * Hook render_editlink: add a checkbox to tweet the new link or not.
162 | *
163 | * @param array $data New link values.
164 | * @param ConfigManager $conf instance.
165 | *
166 | * @return array $data with `edit_link_plugin` placeholder filled.
167 | */
168 | function hook_shaarli2twitter_render_editlink($data, $conf)
169 | {
170 | if (!$data['link_is_new'] || !s2t_is_config_valid($conf)) {
171 | return $data;
172 | }
173 |
174 | $private = $conf->get('privacy.default_private_links', false);
175 |
176 | $html = file_get_contents(PluginManager::$PLUGINS_PATH . '/shaarli2twitter/edit_link.html');
177 | $html = sprintf(
178 | $html,
179 | $private ? '' : 'checked="checked"',
180 | $conf->get('plugins.TWITTER_TWEET_FORMAT', TWEET_DEFAULT_FORMAT)
181 | );
182 |
183 | $data['edit_link_plugin'][] = $html;
184 |
185 | return $data;
186 | }
187 |
188 | /**
189 | * Use TwitterAPIExchange to publish the tweet.
190 | *
191 | * @param ConfigManager $conf
192 | * @param string $tweet
193 | *
194 | * @return string JSON response string.
195 | */
196 | function s2t_tweet($conf, $tweet)
197 | {
198 | require_once 'TwitterApi/TwitterAPIExchange.php';
199 |
200 | $endpoint = 'https://api.twitter.com/1.1/statuses/update.json';
201 | $postfields = [
202 | 'status' => $tweet,
203 | ];
204 | $settings = [
205 | 'consumer_key' => $conf->get('plugins.TWITTER_API_KEY'),
206 | 'consumer_secret' => $conf->get('plugins.TWITTER_API_SECRET'),
207 | 'oauth_access_token' => $conf->get('plugins.TWITTER_ACCESS_TOKEN'),
208 | 'oauth_access_token_secret' => $conf->get('plugins.TWITTER_ACCESS_TOKEN_SECRET'),
209 | ];
210 | $twitter = new \TwitterAPIExchange($settings);
211 |
212 | return $twitter->buildOauth($endpoint, 'POST')
213 | ->setPostfields($postfields)
214 | ->performRequest();
215 | }
216 |
217 | /**
218 | * This function will put link data in format placeholders, without overreaching 280 char.
219 | * Placeholders have priorities, and will be replace until the limit is reached:
220 | * 1. URL
221 | * 2. Title
222 | * 3. Tags
223 | * 4. Description
224 | *
225 | * @param array $link Link data.
226 | * @param string $format Tweet format with placeholders.
227 | * @param bool $hideUrl Hide URL if it's a note and the tweet is too long.
228 | *
229 | * @return string Message to tweet.
230 | */
231 | function s2t_format_tweet($link, $format, $hideUrl)
232 | {
233 | // Tweets are limited to 280 chars, we need to prioritize what will be displayed
234 | $priorities = TWEET_ALLOWED_PLACEHOLDERS;
235 |
236 | // Hide URL when sharing a note (microblog mode)
237 | if ($hideUrl == 'yes' && s2t_is_link_note($link)) {
238 | unset($priorities[array_search('url', $priorities)]);
239 | unset($priorities[array_search('permalink', $priorities)]);
240 | $priorities[] = 'url';
241 | $priorities[] = 'permalink';
242 | }
243 |
244 | // We remove URL from description, title and tags.
245 | // It breaks the length, and often creates an unreadable tweet, with broken links.
246 | $link = s2t_strip_url_from_link($link);
247 |
248 | $tweet = $format;
249 | foreach ($priorities as $priority) {
250 | if (s2t_get_current_length($tweet) >= TWEET_LENGTH) {
251 | return s2t_remove_remaining_placeholders($tweet);
252 | }
253 |
254 | $tweet = s2t_replace_placeholder($tweet, $priority, $link[$priority]);
255 | }
256 |
257 | return trim($tweet);
258 | }
259 |
260 | /**
261 | * Replace a single placeholder in format.
262 | *
263 | * @param string $tweet Current tweet still containing placeholders.
264 | * @param string $placeholder Placeholder to replace.
265 | * @param array|string $value Value to replace placeholder (can be an array for tags).
266 | *
267 | * @return string $tweet with $placeholder replaced by $value.
268 | */
269 | function s2t_replace_placeholder($tweet, $placeholder, $value)
270 | {
271 | if (is_array($value)) {
272 | return s2t_replace_placeholder_array($tweet, $placeholder, $value);
273 | }
274 |
275 | $current = s2t_get_current_length($tweet);
276 | // Tweets URL have a fixed size due to t.co
277 | $valueLength = ($placeholder != 'url' && $placeholder != 'permalink') ? strlen($value) : TWEET_URL_LENGTH;
278 | if ($current + $valueLength > TWEET_LENGTH) {
279 | if ($placeholder != 'url' && $placeholder != 'permalink' && TWEET_LENGTH - $current > 3) {
280 | $value = mb_strcut($value, 0, TWEET_LENGTH - $current - 3) . '…';
281 | } else {
282 | $value = '';
283 | }
284 | }
285 |
286 | return str_replace('${' . $placeholder . '}', $value, $tweet);
287 | }
288 |
289 | /**
290 | * Replace a single placeholder with an array value.
291 | * Use for tags.
292 | *
293 | * @param string $tweet Current tweet still containing placeholders.
294 | * @param string $placeholder Placeholder to replace.
295 | * @param array $value Values to replace placeholder (will be separated by a space).
296 | *
297 | * @return string $tweet with $placeholder replace by the list of $value.
298 | */
299 | function s2t_replace_placeholder_array($tweet, $placeholder, $value)
300 | {
301 | $items = '';
302 | for ($i = 0; $i < count($value); $i++) {
303 | $current = s2t_get_current_length($tweet);
304 | $space = $i == 0 ? '' : ' ';
305 | if ($current + strlen($items) + strlen($value[$i] . $space) > TWEET_LENGTH) {
306 | break;
307 | }
308 | $items .= $space . $value[$i];
309 | }
310 |
311 | return str_replace('${' . $placeholder . '}', $items, $tweet);
312 | }
313 |
314 | /**
315 | * Get the current length of the tweet without any placeholder.
316 | *
317 | * @param string $tweet Current state of the tweet (with or without placeholders left).
318 | *
319 | * @return int Tweet length.
320 | */
321 | function s2t_get_current_length($tweet)
322 | {
323 | return strlen(s2t_remove_remaining_placeholders(s2t_replace_url_by_tco($tweet)));
324 | }
325 |
326 | /**
327 | * Remove remaining placeholders from the tweet.
328 | *
329 | * @param string $tweet Current string for the tweet.
330 | *
331 | * @return string $tweet without any placeholder.
332 | */
333 | function s2t_remove_remaining_placeholders($tweet)
334 | {
335 | return preg_replace('#\${\w+}#', '', $tweet);
336 | }
337 |
338 | /**
339 | * Replace all URL by a default string of TWEET_URL_LENGTH characters.
340 | *
341 | * @param string $tweet Current string for the tweet.
342 | *
343 | * @return string $tweet without any URL.
344 | */
345 | function s2t_replace_url_by_tco($tweet)
346 | {
347 | $regex = '!https?://\S+[[:alnum:]]/?!si';
348 |
349 | return preg_replace($regex, str_repeat('#', TWEET_URL_LENGTH), $tweet);
350 | }
351 |
352 | /**
353 | * Make sure that all config keys has been set.
354 | *
355 | * @param ConfigManager $conf instance.
356 | *
357 | * @return bool true if the config is valid, false otherwise.
358 | */
359 | function s2t_is_config_valid($conf)
360 | {
361 | $mandatory = [
362 | 'TWITTER_API_KEY',
363 | 'TWITTER_API_SECRET',
364 | 'TWITTER_ACCESS_TOKEN',
365 | 'TWITTER_ACCESS_TOKEN_SECRET',
366 | ];
367 | foreach ($mandatory as $value) {
368 | $setting = $conf->get('plugins.' . $value);
369 | if (empty($setting)) {
370 | return false;
371 | }
372 | }
373 |
374 | return true;
375 | }
376 |
377 | /**
378 | * Determines if the link is a note.
379 | * From kalvn's shaarli2mastodon - https://github.com/kalvn/shaarli2mastodon
380 | *
381 | * @param array $link The link to check.
382 | *
383 | * @return boolean Whether the link is a note or not.
384 | */
385 | function s2t_is_link_note($link)
386 | {
387 | return strpos($link['url'], $link['shorturl']) !== false;
388 | }
389 |
390 | /**
391 | * Modifies a tag to make them real Tweet tags.
392 | * From kalvn's shaarli2mastodon - https://github.com/kalvn/shaarli2mastodon
393 | *
394 | * @param string $tag The tag to change.
395 | *
396 | * @return string The tag modified to be valid.
397 | */
398 | function s2t_get_tagify($tag)
399 | {
400 | // Regex inspired by https://gist.github.com/janogarcia/3946583
401 | return '#' . preg_replace('/[^0-9_\p{L}]/u', '', $tag);
402 | }
403 |
404 | /**
405 | * Remove links in description, title and tags to prevent
406 | * length errors, broken links and unreadable tweets.
407 | *
408 | * @param array $link A link array with all its fields
409 | *
410 | * @return array the link with URL stripped
411 | */
412 | function s2t_strip_url_from_link($link)
413 | {
414 | foreach (['description', 'title', 'tags'] as $field) {
415 | if (is_array($link[$field])) {
416 | $link[$field] = array_map('s2t_strip_url', $link[$field]);
417 | } else {
418 | $link[$field] = s2t_strip_url($link[$field]);
419 | }
420 | }
421 | return $link;
422 | }
423 |
424 | function s2t_strip_url($field)
425 | {
426 | $regex = '!https?://\S+[[:alnum:]]/?!si';
427 | return preg_replace($regex, '', $field);
428 | }
429 |
--------------------------------------------------------------------------------