├── LICENSE.md ├── README.md └── shaarli2twitter ├── TwitterApi ├── LICENSE.md └── TwitterAPIExchange.php ├── edit_link.html ├── shaarli2twitter.css ├── shaarli2twitter.js ├── shaarli2twitter.meta └── shaarli2twitter.php /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. -------------------------------------------------------------------------------- /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 | ![](https://cloud.githubusercontent.com/assets/1962678/20008438/ddfa0326-a2a0-11e6-87a7-44319da34d1d.png) 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/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/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/edit_link.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | 17 | -------------------------------------------------------------------------------- /shaarli2twitter/shaarli2twitter.css: -------------------------------------------------------------------------------- 1 | #tweet-textarea textarea { 2 | min-height: 70px; 3 | } 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | --------------------------------------------------------------------------------