From 91498b56448c114bf1a555292339f33cf7b32753 Mon Sep 17 00:00:00 2001 From: root <root@rivolta.investici.org> Date: Tue, 7 Jun 2011 10:03:00 +0000 Subject: [PATCH] aggiunto Wp-statusnet --- wp-content/plugins/wp-statusnet/readme.txt | 77 ++ .../plugins/wp-statusnet/wp-status-net.php | 1084 +++++++++++++++++ 2 files changed, 1161 insertions(+) create mode 100644 wp-content/plugins/wp-statusnet/readme.txt create mode 100644 wp-content/plugins/wp-statusnet/wp-status-net.php diff --git a/wp-content/plugins/wp-statusnet/readme.txt b/wp-content/plugins/wp-statusnet/readme.txt new file mode 100644 index 000000000..49cb565b0 --- /dev/null +++ b/wp-content/plugins/wp-statusnet/readme.txt @@ -0,0 +1,77 @@ +=== WP-Status.net === +Contributors: Xavier Media +Tags: Status.net, Identica, Twitter, status updates, Oauth, Facebook, Google Buzz, Ping.fm +Requires at least: 2.7.0 +Tested up to: 3.0.1 +Stable tag: 1.4.0 + +Posts your blog posts to one or multiple Status.net servers and even to Twitter, Facebook, Google Buzz and Ping.fm + +== Description == + +Every time you make a new blog post this plugin will post a status update to the Status.net servers, facebook, Ping.fm, +Google Buzz and Twitter accounts you have specified. You can set as many acounts on as many servers you like. You can +even have the plugin to post to different account on the same [Status.net](http://status.net) server. + +The plugin post to Facebook, Google Buzz and Ping.fm via SocialOomph, so an account with SocialOomph is required to +post to Facebook, Google Buzz and Ping.fm. + +The links to your blog can be shortened by one of seven different link shortener services like TinyURL.com and RT.nu. + +== Installation == + +1. Upload `wp-status-net/wp-status-net.php` to the `/wp-content/plugins/` directory +2. Activate the plugin through the 'Plugins' menu in WordPress +3. Go to the 'WP-Status.net' menu option under 'Settings' to specify the accounts and servers + +== Frequently Asked Questions == + += How to use Oauth with Twitter? = + +1. Register a new application at http://dev.twitter.com/apps/new + * Application Type must be set on Browser + * The Callback URL should be the URL of your blog + * Default Access type MUST be set to Read & Write +2. Fill in the Consumer Key and Consumer Secret in the correct fields (will show up as soon as you select Server Type "Twitter" and "Oauth" in the server list) +3. Click on the link called "My Access Tokens" at http://dev.twitter.com (right menu) +4. Fill in your Access Token and the Access Token Secret in the correct fields +5. Now you should be able to post to Twitter + += How do I get access to the 2ve.org link shortener API? = + +The 2ve.org service is at the moment only open for Xavier Media, but you can choose any of the other link shorteners instead. + += How can I suggest a new feature or report a bug? = + +Visit our support forum at http://www.xavierforum.com/php-&-cgi-scripts-f3.html + +== Changelog == + += 1.4.0 = +* SocialOomph support added +* Google Buzz support (via SocialOomph) +* Facebook support (via SocialOomph) +* Ping.fm support (via SocialOomph) +* RT.nu removed as link shortener service (since they don't provide this service any more) +* If an error mesage is received from a StatusNet server that will show up in the list of servers so you know what went wrong + += 1.3.1 = +* Minor bug fix in Oauth for Twitter +* Fixed problem with bit.ly links + += 1.3 = +* Oauth is now available for Twitter servers. For StatusNet server that will be available in a later version +* Optional suffix possible for posts + += 1.1 = +* Added possibility to have a unique prefix for each server when posting blog posts to a StatusNet server + += 1.0 = +* The first version + +== Upgrade Notice == + += 1.0 = +* The first version + +`<?php code(); // goes in backticks ?>` \ No newline at end of file diff --git a/wp-content/plugins/wp-statusnet/wp-status-net.php b/wp-content/plugins/wp-statusnet/wp-status-net.php new file mode 100644 index 000000000..90248ed0f --- /dev/null +++ b/wp-content/plugins/wp-statusnet/wp-status-net.php @@ -0,0 +1,1084 @@ +<?php +/* +Plugin Name: WP Status.net +Plugin URI: http://www.xaviermedia.com/wordpress/plugins/wp-status-net.php +Description: Posts your blog posts to one or multiple Status.net servers +Author: Xavier Media +Version: 1.4.0 +Author URI: http://www.xaviermedia.com/ +*/ + +add_action('publish_post', 'wpstatusnet_poststatus'); +add_action('comment_form', 'wpstatusnet_commentform', 5, 0); + +class EpiOAuth +{ + public $version = '1.0'; + + protected $requestTokenUrl; + protected $accessTokenUrl; + protected $authorizeUrl; + protected $consumerKey; + protected $consumerSecret; + protected $token; + protected $tokenSecret; + protected $signatureMethod; + + public function getAccessToken() + { + $resp = $this->httpRequest('GET', $this->accessTokenUrl); + return new EpiOAuthResponse($resp); + } + + public function getAuthorizationUrl() + { + $retval = "{$this->authorizeUrl}?"; + + $token = $this->getRequestToken(); + return $this->authorizeUrl . '?oauth_token=' . $token->oauth_token; + } + + public function getRequestToken() + { + $resp = $this->httpRequest('GET', $this->requestTokenUrl); + return new EpiOAuthResponse($resp); + } + + public function httpRequest($method = null, $url = null, $params = null) + { + if(empty($method) || empty($url)) + return false; + + if(empty($params['oauth_signature'])) + $params = $this->prepareParameters($method, $url, $params); + + switch($method) + { + case 'GET': + return $this->httpGet($url, $params); + break; + case 'POST': + return $this->httpPost($url, $params); + break; + } + } + + public function setToken($token = null, $secret = null) + { + $params = func_get_args(); + $this->token = $token; + $this->tokenSecret = $secret; + } + + public function encode($string) + { + return rawurlencode(utf8_encode($string)); + } + + protected function addOAuthHeaders(&$ch, $url, $oauthHeaders) + { + $_h = array('Expect:'); + $urlParts = parse_url($url); + $oauth = 'Authorization: OAuth realm="' . $urlParts['path'] . '",'; + foreach($oauthHeaders as $name => $value) + { + $oauth .= "{$name}=\"{$value}\","; + } + $_h[] = substr($oauth, 0, -1); + + curl_setopt($ch, CURLOPT_HTTPHEADER, $_h); + } + + protected function generateNonce() + { + if(isset($this->nonce)) // for unit testing + return $this->nonce; + + return md5(uniqid(rand(), true)); + } + + protected function generateSignature($method = null, $url = null, $params = null) + { + if(empty($method) || empty($url)) + return false; + + + // concatenating + $concatenatedParams = ''; + foreach($params as $k => $v) + { + $v = $this->encode($v); + $concatenatedParams .= "{$k}={$v}&"; + } + $concatenatedParams = $this->encode(substr($concatenatedParams, 0, -1)); + + // normalize url + $normalizedUrl = $this->encode($this->normalizeUrl($url)); + $method = $this->encode($method); // don't need this but why not? + + $signatureBaseString = "{$method}&{$normalizedUrl}&{$concatenatedParams}"; + return $this->signString($signatureBaseString); + } + + protected function httpGet($url, $params = null) + { + if(count($params['request']) > 0) + { + $url .= '?'; + foreach($params['request'] as $k => $v) + { + $url .= "{$k}={$v}&"; + } + $url = substr($url, 0, -1); + } + $ch = curl_init($url); + $this->addOAuthHeaders($ch, $url, $params['oauth']); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $resp = $this->curl->addCurl($ch); + + return $resp; + } + + protected function httpPost($url, $params = null) + { + $ch = curl_init($url); + $this->addOAuthHeaders($ch, $url, $params['oauth']); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params['request'])); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $resp = $this->curl->addCurl($ch); + return $resp; + } + + protected function normalizeUrl($url = null) + { + $urlParts = parse_url($url); + $scheme = strtolower($urlParts['scheme']); + $host = strtolower($urlParts['host']); + $port = intval($urlParts['port']); + + $retval = "{$scheme}://{$host}"; + if($port > 0 && ($scheme === 'http' && $port !== 80) || ($scheme === 'https' && $port !== 443)) + { + $retval .= ":{$port}"; + } + $retval .= $urlParts['path']; + if(!empty($urlParts['query'])) + { + $retval .= "?{$urlParts['query']}"; + } + + return $retval; + } + + protected function prepareParameters($method = null, $url = null, $params = null) + { + if(empty($method) || empty($url)) + return false; + + $oauth['oauth_consumer_key'] = $this->consumerKey; + $oauth['oauth_token'] = $this->token; + $oauth['oauth_nonce'] = $this->generateNonce(); + $oauth['oauth_timestamp'] = !isset($this->timestamp) ? time() : $this->timestamp; // for unit test + $oauth['oauth_signature_method'] = $this->signatureMethod; + $oauth['oauth_version'] = $this->version; + + // encoding + array_walk($oauth, array($this, 'encode')); + if(is_array($params)) + array_walk($params, array($this, 'encode')); + $encodedParams = array_merge($oauth, (array)$params); + + // sorting + ksort($encodedParams); + + // signing + $oauth['oauth_signature'] = $this->encode($this->generateSignature($method, $url, $encodedParams)); + return array('request' => $params, 'oauth' => $oauth); + } + + protected function signString($string = null) + { + $retval = false; + switch($this->signatureMethod) + { + case 'HMAC-SHA1': + $key = $this->encode($this->consumerSecret) . '&' . $this->encode($this->tokenSecret); + $retval = base64_encode(hash_hmac('sha1', $string, $key, true)); + break; + } + + return $retval; + } + + public function __construct($consumerKey, $consumerSecret, $signatureMethod='HMAC-SHA1') + { + $this->consumerKey = $consumerKey; + $this->consumerSecret = $consumerSecret; + $this->signatureMethod = $signatureMethod; + $this->curl = EpiCurl::getInstance(); + } +} + +class EpiOAuthResponse +{ + private $__resp; + + public function __construct($resp) + { + $this->__resp = $resp; + } + + public function __get($name) + { + if($this->__resp->code < 200 || $this->__resp->code > 299) + return false; + + parse_str($this->__resp->data, $result); + foreach($result as $k => $v) + { + $this->$k = $v; + } + + return $result[$name]; + } +} + +class EpiCurl +{ + const timeout = 3; + static $inst = null; + static $singleton = 0; + private $mc; + private $msgs; + private $running; + private $requests = array(); + private $responses = array(); + private $properties = array(); + + function __construct() + { + if(self::$singleton == 0) + { + throw new Exception('This class cannot be instantiated by the new keyword. You must instantiate it using: $obj = EpiCurl::getInstance();'); + } + + $this->mc = curl_multi_init(); + $this->properties = array( + 'code' => CURLINFO_HTTP_CODE, + 'time' => CURLINFO_TOTAL_TIME, + 'length'=> CURLINFO_CONTENT_LENGTH_DOWNLOAD, + 'type' => CURLINFO_CONTENT_TYPE + ); + } + + public function addCurl($ch) + { + $key = (string)$ch; + $this->requests[$key] = $ch; + + $res = curl_multi_add_handle($this->mc, $ch); + + // (1) + if($res === CURLM_OK || $res === CURLM_CALL_MULTI_PERFORM) + { + do { + $mrc = curl_multi_exec($this->mc, $active); + } while ($mrc === CURLM_CALL_MULTI_PERFORM); + + return new EpiCurlManager($key); + } + else + { + return $res; + } + } + + public function getResult($key = null) + { + if($key != null) + { + if(isset($this->responses[$key])) + { + return $this->responses[$key]; + } + + $running = null; + do + { + $resp = curl_multi_exec($this->mc, $runningCurrent); + if($running !== null && $runningCurrent != $running) + { + $this->storeResponses($key); + if(isset($this->responses[$key])) + { + return $this->responses[$key]; + } + } + $running = $runningCurrent; + }while($runningCurrent > 0); + } + + return false; + } + + private function storeResponses() + { + while($done = curl_multi_info_read($this->mc)) + { + $key = (string)$done['handle']; + $this->responses[$key]['data'] = curl_multi_getcontent($done['handle']); + foreach($this->properties as $name => $const) + { + $this->responses[$key][$name] = curl_getinfo($done['handle'], $const); + curl_multi_remove_handle($this->mc, $done['handle']); + } + } + } + + static function getInstance() + { + if(self::$inst == null) + { + self::$singleton = 1; + self::$inst = new EpiCurl(); + } + + return self::$inst; + } +} + +class EpiCurlManager +{ + private $key; + private $epiCurl; + + function __construct($key) + { + $this->key = $key; + $this->epiCurl = EpiCurl::getInstance(); + } + + function __get($name) + { + $responses = $this->epiCurl->getResult($this->key); + return $responses[$name]; + } +} + +/* + * Credits: + * - (1) Alistair pointed out that curl_multi_add_handle can return CURLM_CALL_MULTI_PERFORM on success. + */ + +class EpiTwitter extends EpiOAuth +{ + const EPITWITTER_SIGNATURE_METHOD = 'HMAC-SHA1'; + protected $requestTokenUrl = 'http://twitter.com/oauth/request_token'; + protected $accessTokenUrl = 'http://twitter.com/oauth/access_token'; + protected $authorizeUrl = 'http://twitter.com/oauth/authorize'; + protected $apiUrl = 'http://twitter.com'; + + public function __call($name, $params = null) + { + $parts = explode('_', $name); + $method = strtoupper(array_shift($parts)); + $parts = implode('_', $parts); + $url = $this->apiUrl . '/' . preg_replace('/[A-Z]|[0-9]+/e', "'/'.strtolower('\\0')", $parts) . '.json'; + if(!empty($params)) + $args = array_shift($params); + + return new EpiTwitterJson(call_user_func(array($this, 'httpRequest'), $method, $url, $args)); + } + + public function __construct($consumerKey = null, $consumerSecret = null, $oauthToken = null, $oauthTokenSecret = null, $oauthServer = "twitter.com") + { + $requestTokenUrl = 'http://'. $oauthServer .'/oauth/request_token'; + $accessTokenUrl = 'http://'. $oauthServer .'/oauth/access_token'; + $authorizeUrl = 'http://'. $oauthServer .'/oauth/authorize'; + $apiUrl = 'http://'. $oauthServer .''; + + parent::__construct($consumerKey, $consumerSecret, self::EPITWITTER_SIGNATURE_METHOD); + $this->setToken($oauthToken, $oauthTokenSecret); + } +} + +class EpiTwitterJson +{ + private $resp; + + public function __construct($resp) + { + $this->resp = $resp; + } + + public function __get($name) + { + $this->responseText = $this->resp->data; + $this->response = (array)json_decode($this->responseText, 1); + foreach($this->response as $k => $v) + { + $this->$k = $v; + } + + return $this->$name; + } +} + +class CurlRequest +{ + private $ch; + /** + * Init curl session + * + * $params = array('url' => '', + * 'host' => '', + * 'header' => '', + * 'method' => '', + * 'referer' => '', + * 'cookie' => '', + * 'post_fields' => '', + * ['login' => '',] + * ['password' => '',] + * 'timeout' => 0 + * ); + */ + public function init($params) + { + $this->ch = curl_init(); + $user_agent = 'Mozilla/5.0 (Windows; U;Windows NT 5.1; en-us; rv:1.8.0.9) Gecko/20061206 Firefox/1.5.0.9'; + $header = array( + "Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5", + "Accept-Language: en-us;q=0.7,en;q=0.3", + "Accept-Charset: windows-1251,utf-8;q=0.7,*;q=0.7", + "Keep-Alive: 300"); + if (isset($params['host']) && $params['host']) $header[]="Host: ".$host; + if (isset($params['header']) && $params['header']) $header[]=$params['header']; + + @curl_setopt ( $this -> ch , CURLOPT_RETURNTRANSFER , 1 ); + @curl_setopt ( $this -> ch , CURLOPT_VERBOSE , 1 ); + @curl_setopt ( $this -> ch , CURLOPT_HEADER , 1 ); + + if ($params['method'] == "HEAD") @curl_setopt($this -> ch,CURLOPT_NOBODY,1); + @curl_setopt ( $this -> ch, CURLOPT_FOLLOWLOCATION, 1); + @curl_setopt ( $this -> ch , CURLOPT_HTTPHEADER, $header ); + if ($params['referer']) @curl_setopt ($this -> ch , CURLOPT_REFERER, $params['referer'] ); + @curl_setopt ( $this -> ch , CURLOPT_USERAGENT, $user_agent); + if ($params['cookie']) @curl_setopt ($this -> ch , CURLOPT_COOKIE, $params['cookie']); + + if ( $params['method'] == "POST" ) + { + curl_setopt( $this -> ch, CURLOPT_POST, true ); + curl_setopt( $this -> ch, CURLOPT_POSTFIELDS, $params['post_fields'] ); + } + @curl_setopt( $this -> ch, CURLOPT_URL, $params['url']); + @curl_setopt ( $this -> ch , CURLOPT_SSL_VERIFYPEER, 0 ); + @curl_setopt ( $this -> ch , CURLOPT_SSL_VERIFYHOST, 0 ); + if (isset($params['login']) & isset($params['password'])) + @curl_setopt($this -> ch , CURLOPT_USERPWD,$params['login'].':'.$params['password']); + @curl_setopt ( $this -> ch , CURLOPT_TIMEOUT, $params['timeout']); + } + + /** + * Make curl request + * + * @return array 'header','body','curl_error','http_code','last_url' + */ + public function exec() + { + $response = curl_exec($this->ch); + $error = curl_error($this->ch); + $result = array( 'header' => '', + 'body' => '', + 'curl_error' => '', + 'http_code' => '', + 'last_url' => ''); + if ( $error != "" ) + { + $result['curl_error'] = $error; + return $result; + } + + $header_size = curl_getinfo($this->ch,CURLINFO_HEADER_SIZE); + $result['header'] = substr($response, 0, $header_size); + $result['body'] = substr( $response, $header_size ); + $result['http_code'] = curl_getinfo($this -> ch,CURLINFO_HTTP_CODE); + $result['last_url'] = curl_getinfo($this -> ch,CURLINFO_EFFECTIVE_URL); + return $result; + } +} + +function wpstatusnet_poststatus($post_id) +{ + + $status_net = get_post_meta($post_id, 'status_net', true); + if (!($status_net == 'yes')) { + query_posts('p=' . $post_id); + + if (have_posts()) + { + $opt = get_option('wpstatusnetoptions'); + $options = unserialize($opt); + + the_post(); + + $link = get_permalink(); + + if ($options[apitype] == "is.gd" || $options[apitype] == "rt.nu") + { + $link = file_get_contents('http://is.gd/api.php?longurl='. urlencode($link)); + } + else if ($options[apitype] == "metamark.net") + { + $link = file_get_contents('http://metamark.net/api/rest/simple?long_url='. urlencode($link)); + } + else if ($options[apitype] == "mrte.ch") + { + $jsonstring = file_get_contents('http://api.mrte.ch/go.php?action=shorturl&format=json&url='. urlencode($link)); + + $json = json_decode($jsonstring,true); + + if ($json[statusCode] == "200") + { + $link = $json[shorturl]; + } + } + else if ($options[apitype] == "tinyurl.com") + { + $link = file_get_contents('http://tinyurl.com/api-create.php?url=' . $link); + } + else if ($options[apitype] == "2ve.org") + { + $jsonstring = file_get_contents('http://api.2ve.org/api.php?action=makeshorter&fileformat=json&longlink='. urlencode($link) .'&api='. $options[apiid] .'&key='. $options[apikey]); + + $json = json_decode($jsonstring,true); + + if ($json[responsecode] == "200") + { + $link = $json[shortlink]; + } + } + else if ($options[apitype] == "bit.ly") + { + $jsonstring = file_get_contents('http://api.bit.ly/shorten?version=2.0.1&longUrl='. urlencode($link) .'&login='. $options[apiid] .'&apiKey='. $options[apikey]); + + $json = json_decode($jsonstring,true); + + if ($json[statusCode] == "OK") + { + $link = $json[results][$link][shortUrl]; + } + } + + $title = get_the_title(); + + + $posting = new CurlRequest(); + + $num = count($options[statusserver]); + for ($i = 0; $i < $num; $i++) + { + if ($options[statususer][$i] != "" && $options[statuserrormsg][$i] == "") + { + + if ($options[statusprefix][$i] == "") + { + $statuspost = ''; + } + else + { + $statuspost = $options[statusprefix][$i] .' '; + } + + + if ($title > (134 - strlen($link) - strlen($options[statusprefix][$i]))) + { + $statuspost .= $options[statusprefix][$i] .' '. substr($title,0,(134 - strlen($link) - strlen($options[statusprefix][$i]))) .'... - '. $link; + } + else + { + $statuspost .= $title .' - '. $link; + } + + if ($options[statussuffix][$i] == "") + { + $statuspost .= ''; + } + else + { + $statuspost .= ' '. $options[statussuffix][$i]; + } + + if ($options[statusauthtype][$i] == "oauth") + { + + if ($options[statuspath][$i] == "" || $options[statustype][$i] == "twitter") + { + $oauthServer = $options[statusserver][$i]; + } + else + { + $oauthServer = $options[statusserver][$i] ."/". $options[statuspath][$i]; + } + + $OauthObj = new EpiTwitter($options[statususer][$i], $options[statususer2][$i],$options[statuspwd][$i],$options[statuspwd2][$i],$oauthServer); + + $OauthInfo= $OauthObj->get_accountVerify_credentials(); + $OauthInfo->response; + + $OauthInfo= $OauthObj->post_statusesUpdate(array('status' => $statuspost)); + $statusid = $OauthInfo->response['id']; + + } + else if ($options[statustype][$i] == "socialoomph") + { + $server = "http://socialoomphapi.com/tweets/add/json/"; + + $params = array('url' => $server, + 'host' => '', + 'header' => '', + 'method' => 'POST', // 'POST','HEAD' + 'referer' => '', + 'cookie' => '', + 'post_fields' => 'account='. $options[statususer][$i] .'&text='. $statuspost .'&apikey='. $options[socialoomphapi] .'&time='. date('Y-m-d H:i:s') .'Z', + 'login' => '', + 'password' => '', + 'timeout' => 20 + ); + + $posting->init($params); + + $postingstatus = $posting->exec(); + } + else + { + if ($options[statuspath][$i] == "") + { + $server = "http://". $options[statusserver][$i] ."/statuses/update.json"; + } + else + { + $server = "http://". $options[statusserver][$i] ."/". $options[statuspath][$i] ."/statuses/update.json"; + } + + $params = array('url' => $server, + 'host' => '', + 'header' => '', + 'method' => 'POST', // 'POST','HEAD' + 'referer' => '', + 'cookie' => '', + 'post_fields' => 'status='. urlencode($statuspost) .'&source=WP-status-net', + 'login' => $options[statususer][$i], + 'password' => $options[statuspwd][$i], + 'timeout' => 20 + ); + + $posting->init($params); + + $postingstatus = $posting->exec(); + + $postingjson = json_decode($postingstatus[body],true); + + if ($postingjson[error] != "") + { + $options[statuserrormsg][$i] = $postingjson[error]; + } + + +// var_dump($postingstatus); + } + + } + } + + + add_post_meta($post_id, 'status_net', 'yes'); + + $opt = serialize($options); + update_option('wpstatusnetoptions', $opt); + + } + } +} + +function wpstatusnet_commentform() +{ + $opt = get_option('wpstatusnetoptions'); + $options = unserialize($opt); + if ($options[pluginlink] == "poweredby") + { + echo '<p>Powered by <a href="http://www.xaviermedia.com/wordpress/plugins/wp-status-net.php">WP Status.net plugin</A>.</p>'; + } +} + +function wpstatusnet_test() +{ + echo 'Test'; + +} + +function wpstatusnet_options() +{ + + if ( 'save' == $_REQUEST['action'] ) + { + + $opt = get_option('wpstatusnetoptions'); + $optionsold = unserialize($opt); + + + $options = array( + "apitype" => $_REQUEST[apitype], + "apiid" => $_REQUEST[apiid], + "apikey" => $_REQUEST[apikey], + "pluginlink" => $_REQUEST[pluginlink], + "socialoomphapi" => $_REQUEST[socialoomphapi], + "socialoomphaccounts" => array(), + "statustype" => array(), + "statusauthtype" => array(), + "statusservers" => array(), + "statuspath" => array(), + "statususer" => array(), + "statuspwd" => array(), + "statusprefix" => array(), + "statussuffix" => array(), + "statuserrormsg" => array() + ); + + if ($_REQUEST[socialoomphreload] == "yes") + { + $getsocialoomphaccounts = new CurlRequest(); + + $params = array('url' => 'http://socialoomphapi.com/accounts/fetch/json/', + 'host' => '', + 'header' => '', + 'method' => 'POST', // 'POST','HEAD' + 'referer' => '', + 'cookie' => '', + 'post_fields' => 'apikey='. $options[socialoomphapi], + 'timeout' => 20 + ); + + $getsocialoomphaccounts->init($params); + + $getsocialoomphaccountsstatus = $getsocialoomphaccounts->exec(); + + $json_results = json_decode($getsocialoomphaccountsstatus[body],true); + + $num = count($json_results[accounts]); + for ($i = 0; $i < $num; $i++) + { + if ($json_results[accounts][$i][account][type] == "N") + { + $options[socialoompaccounts][$i] = $json_results[accounts][$i][account][id] ."|". str_replace('http://','',$json_results[accounts][$i][account][site_url]) ."/". $json_results[accounts][$i][account][name]; + } + else if ($json_results[accounts][$i][account][type] == "T") + { + $options[socialoompaccounts][$i] = $json_results[accounts][$i][account][id] ."|twitter.com/". $json_results[accounts][$i][account][name]; + } + else + { + $options[socialoompaccounts][$i] = $json_results[accounts][$i][account][id] ."|". $json_results[accounts][$i][account][name]; + } + } + } + else + { + $options[socialoompaccounts] = $optionsold[socialoompaccounts]; + } + + $statustype = $_REQUEST[statustype]; + $statusauthtype = $_REQUEST[statusauthtype]; + $statusserver = $_REQUEST[statusserver]; + $statuspath = $_REQUEST[statuspath]; + $statususer = $_REQUEST[statususer]; + $statuspwd = $_REQUEST[statuspwd]; + $statususer2 = $_REQUEST[statususer2]; + $statuspwd2 = $_REQUEST[statuspwd2]; + $statusprefix = $_REQUEST[statusprefix]; + $statussuffix = $_REQUEST[statussuffix]; + $statuserrormsg = $_REQUEST[statuserrormsg]; + + $num = count($statusserver); + for ($i = 0; $i < $num; $i++) + { + if ($statususer[$i] != "") + { + + if ($statustype[$i] == "twitter") + { + $statusserver[$i] = "twitter.com"; + $statuspath[$i] = ""; + $statusauthtype[$i] = "oauth"; + } + else if ($statustype[$i] == "socialoomph") + { + $statusserver[$i] = "socialoompapi.com"; + $statuspath[$i] = ""; + $statusauthtype[$i] = "basic"; + } + + $statusserver[$i] = str_replace("http://","",$statusserver[$i]); + if (substr($statusserver[$i],-1,1) == "/") + { + $statusserver[$i] = substr($statusserver[$i],0,-1); + } + if (substr($statuspath[$i],-1,1) == "/") + { + $statuspath[$i] = substr($statuspath[$i],0,-1); + } + if (substr($statuspath[$i],0,1) == "/") + { + $statuspath[$i] = substr($statuspath[$i],1); + } + +// if ($statusserver[$i] == "myxavier.com" && $options[pluginlink] == "") +// { +// $options[pluginlink] = "poweredby"; +// } + + $options[statustype][] = $statustype[$i]; + $options[statusauthtype][] = $statusauthtype[$i]; + $options[statusserver][] = $statusserver[$i]; + $options[statuspath][] = $statuspath[$i]; + $options[statususer][] = $statususer[$i]; + $options[statuspwd][] = $statuspwd[$i]; + $options[statususer2][] = $statususer2[$i]; + $options[statuspwd2][] = $statuspwd2[$i]; + $options[statusprefix][] = $statusprefix[$i]; + $options[statussuffix][] = $statussuffix[$i]; + $options[statuserrormsg][] = $statuserrormsg[$i]; + } + } + + $opt = serialize($options); + update_option('wpstatusnetoptions', $opt); + } + else + { + $opt = get_option('wpstatusnetoptions'); + $options = unserialize($opt); + } + + + $num = count($options[socialoompaccounts]); + $socialoomphhtml = '<OPTION VALUE=""></OPTION>'; + for ($i = 0; $i < $num; $i++) + { + $temp = explode('|',$options[socialoompaccounts][$i]); + + $socialoomphhtml .= '<OPTION VALUE="'. $temp[0] .'">'. $temp[1] .'</OPTION>'; + } + + ?> + <STYLE> + .hiddenfield + { + display:none; + } + .nothiddenfield + { + } + </STYLE> + + <div class="updated fade-ff0000"><p><strong>Need web hosting for your blog?</strong> Get 10 Gb web space and unlimited bandwidth for only $3.40/month at <a href="http://2ve.org/xMY3/" target="_blank">eXavier.com</a>, or get the Ultimate Plan with unlimited space and bandwidth for only $14.99/month.</p></div> + + + <form action="<?php echo $_SERVER['REQUEST_URI'] ?>" method="post" name=pf> + <input type="hidden" name="action" value="save" /> + <h1>WP StatusNet Options</h2> + If you get stuck on any of these options, please have a look at the <a href="http://www.xaviermedia.com/wordpress/plugins/wp-status-net.php">WP Status.net plugin page</a> or visit the <a href="http://www.xavierforum.com/php-&-cgi-scripts-f3.html">support forum</a>. + <h2>Link Shortener</h3> + <p>Select the link shortener you would like to use.</p> + <p> + <INPUT TYPE=radio NAME=apitype VALUE="" <?php if ($options[apitype] == "") { echo ' CHECKED'; } ?> onClick="javascript:document.getElementById('apikeys').className = 'hiddenfield';"> <B>Don't</B> use any service to get short links<BR /> + + <INPUT TYPE=radio NAME=apitype VALUE="is.gd" <?php if ($options[apitype] == "is.gd" || $options[apitype] == "rt.nu") { echo ' CHECKED'; } ?> onClick="javascript:document.getElementById('apikeys').className = 'hiddenfield';"> <A HREF="http://is.gd/" TARGET="_blank">is.gd</A><BR /> + + <INPUT TYPE=radio NAME=apitype VALUE="metamark.net" <?php if ($options[apitype] == "metamark.net") { echo ' CHECKED'; } ?> onClick="javascript:document.getElementById('apikeys').className = 'hiddenfield';"> <A HREF="http://metamark.net/" TARGET="_blank">metamark.net</A><BR /> + + <INPUT TYPE=radio NAME=apitype VALUE="mrte.ch" <?php if ($options[apitype] == "mrte.ch") { echo ' CHECKED'; } ?> onClick="javascript:document.getElementById('apikeys').className = 'hiddenfield';"> <A HREF="http://mrte.ch/" TARGET="_blank">mrte.ch</A><BR /> + + <INPUT TYPE=radio NAME=apitype VALUE="tinyurl.com" <?php if ($options[apitype] == "tinyurl.com") { echo ' CHECKED'; } ?> onClick="javascript:document.getElementById('apikeys').className = 'hiddenfield';"> <A HREF="http://tinyurl.com/" TARGET="_blank">tinyurl.com</A><BR /> + + <INPUT TYPE=radio NAME=apitype VALUE="2ve.org" <?php if ($options[apitype] == "2ve.org") { echo ' CHECKED'; } ?> onClick="javascript:alert('Don\'t forget to fill in the API ID and API key fields below for this link shortener');document.getElementById('apikeys').className = 'nothiddenfield';"> <A HREF="http://2ve.org/" TARGET="_blank">2ve.org</A> <B>*</B><BR /> + + <INPUT TYPE=radio NAME=apitype VALUE="bit.ly" <?php if ($options[apitype] == "bit.ly") { echo ' CHECKED'; } ?> onClick="javascript:alert('Don\'t forget to fill in the API ID and API key fields below for this link shortener');document.getElementById('apikeys').className = 'nothiddenfield';"> <A HREF="http://bit.ly/" TARGET="_blank">bit.ly</A> <B>*</B><BR /> + + <BR /><B>*</B> = This link shortener service require an <B>API ID</B> and/or an <B>API Key</B> to work. Please see the documentation at the link shorteners web site. + + <p id=apikeys class=<?php if($options[apitype] == "2ve.org" || $options[apitype] == "bit.ly") { echo 'nothiddenfield'; } else { echo 'hiddenfield'; } ?>> + <B>Link Shortener API ID and API Key:</B><BR /> + Depending on what you selected above, some link shorteners require that you sign up at their web site to get an API ID (or API login) and/or an API key. For more information on what's required to use the link shortener you've selected, please see the documentation at the web site of the link shortener.<BR /> + API ID: <INPUT TYPE=text NAME=apiid VALUE="<?php echo $options[apiid]; ?>" SIZE=40> (this may sometimes be called "login")<BR /> + API Key: <INPUT TYPE=text NAME=apikey VALUE="<?php echo $options[apikey]; ?>" SIZE=40> (if just a key is required, leave the ID field blank)<BR /> + </p> + + <h2>Link to Xavier Media®</h3> + + <P>To support our work, please add a link to us in your blog. </P> + + <P><INPUT TYPE=checkbox VALUE="poweredby" NAME=pluginlink <?php if ($options[pluginlink] == "poweredby") { echo ' CHECKED'; } ?>> "Powered by WP-Status.net plugin"</P> + + <h2>SocialOomph API key</h3> + <p><a href="http://www.socialoomph.com/93609.html" TARGET="_blank">SocialOomph</a> is a service that lets you post status updates to multiple accounts like Twitter, StatusNet servers, Facebook, Google Buzz and Ping.fm. Fill in the API key you get from SocialOomph and check the checkbox called "Load accounts from SocialOomph" to get your account information. The API key <a href="http://www.socialoomph.com/93609.html" TARGET="_blank">can be found</a> under "My S.O Account" > "View API Key".</p> + + <p><b>SocialOomph API key:</b> <INPUT TYPE=text NAME=socialoomphapi VALUE="<?php echo $options[socialoomphapi]; ?>" SIZE=45 onChange="javascript:document.getElementById('socialoomphreload').checked=true;document.pf.submit();"> <INPUT TYPE=checkbox VALUE=yes NAME="socialoomphreload" ID="socialoomphreload" onClick="javascript:document.pf.submit();"> Load accounts from <a href="http://www.socialoomph.com/93609.html" TARGET="_blank">SocialOomph</a></p> + + <p>If you've added accounts to <a href="http://www.socialoomph.com/93609.html" TARGET="_blank">SocialOomph</a> that don't show up in the selection list below, then please check the checkbox above to re-load the account list from <a href="http://www.socialoomph.com/93609.html" TARGET="_blank">SocialOomph</a>.</p> + + <h2>StatusNet servers and user accounts</h3> + + <p>Fill in the StatusNet servers you would like to post status updates to. To post to for example <A HREF="http://identi.ca/" TARGET="_blank">identi.ca</A> just fill in <B>identi.ca</B> as server, <B>api</B> as path and your user name and password.</p> + <p>To turn off updates to a server, just remove the user name for that server and update options.</p> + <p>Post prefix and post suffix are optional, but if you would like to post some text or perhaps a hash tag before/after all your posts you can specify a unique prefix/suffix for each server/account.</p> + + <p id=howtousetwitter1> + <a href="#howtousetwitter" onClick="javascript:document.getElementById('howtousetwitter1').className='hiddenfield';document.getElementById('howtousetwitter2').className='nothiddenfield';">How to use Oauth with Twitter?</a> + </p> + + <p class=hiddenfield id=howtousetwitter2> + <a name="howtousetwitter"></a><b>How to use Oauth with Twitter?</b><br /> + 1. Register a new application at <a href="http://dev.twitter.com/apps/new" target="_blank">dev.twitter.com/apps/new</a><br /> + * Application Type must be set to Browser<br /> + * The Callback URL should be the URL of your blog<br /> + * Default Access type MUST be set to Read & Write<br /> + 2. Fill in the Consumer Key and Consumer Secret in the correct fields (will show up as soon as you select Server Type "Twitter" and "Oauth" in the server list (user name column))<br /> + 3. Click on the link called "My Access Tokens" at http://dev.twitter.com (right menu)<br /> + 4. Fill in your Access Token and the Access Token Secret in the correct fields (password column)<br /> + 5. Now you should be able to post to Twitter<br /> + </p> + +<script type='text/javascript'> +//<![CDATA[ +function verifyline(linenum) +{ + if (document.getElementById('statustype' + linenum).options[document.getElementById('statustype' + linenum).selectedIndex].value == 'twitter') + { + document.getElementById('statusauthtype' + linenum).className = 'nothiddenfield'; + document.getElementById('statusserver' + linenum).className = 'hiddenfield'; + document.getElementById('statuspath' + linenum).className = 'hiddenfield'; + document.getElementById('statususer' + linenum).className = 'nothiddenfield'; + document.getElementById('statuspwd' + linenum).className = 'nothiddenfield'; + document.getElementById('statussocialoomphuser' + linenum).className = 'hiddenfield'; + document.getElementById('statusauthtype' + linenum).selectedIndex = 1; + } + else if (document.getElementById('statustype' + linenum).options[document.getElementById('statustype' + linenum).selectedIndex].value == 'status') + { + document.getElementById('statusauthtype' + linenum).className = 'nothiddenfield'; + document.getElementById('statusserver' + linenum).className = 'nothiddenfield'; + document.getElementById('statuspath' + linenum).className = 'nothiddenfield'; + document.getElementById('statususer' + linenum).className = 'nothiddenfield'; + document.getElementById('statuspwd' + linenum).className = 'nothiddenfield'; + document.getElementById('statussocialoomphuser' + linenum).className = 'hiddenfield'; + document.getElementById('statusauthtype' + linenum).selectedIndex = 0; + } + else + { + document.getElementById('statusauthtype' + linenum).className = 'hiddenfield'; + document.getElementById('statusserver' + linenum).className = 'hiddenfield'; + document.getElementById('statuspath' + linenum).className = 'hiddenfield'; + document.getElementById('statususer' + linenum).className = 'hiddenfield'; + document.getElementById('statuspwd' + linenum).className = 'hiddenfield'; + document.getElementById('statussocialoomphuser' + linenum).className = 'nothiddenfield'; + document.getElementById('statusauthtype' + linenum).selectedIndex = 0; + } + + if (document.getElementById('statusauthtype' + linenum).options[document.getElementById('statusauthtype' + linenum).selectedIndex].value == 'oauth') + { + document.getElementById('oauthA' + linenum).className = 'nothiddenfield'; + document.getElementById('oauthB' + linenum).className = 'nothiddenfield'; + document.getElementById('oauthC' + linenum).className = 'nothiddenfield'; + document.getElementById('oauthD' + linenum).className = 'nothiddenfield'; + } + else + { + document.getElementById('oauthA' + linenum).className = 'hiddenfield'; + document.getElementById('oauthB' + linenum).className = 'hiddenfield'; + document.getElementById('oauthC' + linenum).className = 'hiddenfield'; + document.getElementById('oauthD' + linenum).className = 'hiddenfield'; + } +} +//]]> +</script> + + + + + + + + <table class="widefat post fixed" cellspacing="0"> + <thead> + <tr> + <th id="server" class="manage-column column-title" style="" scope="col">Type</th> + <th id="server" class="manage-column column-title" style="" scope="col">Server</th> + <th id="path" class="manage-column column-title" style="" scope="col">Path</th> + <th id="user" class="manage-column column-title" style="" scope="col">User Name</th> + <th id="pwd" class="manage-column column-title" style="" scope="col">Password</th> + <th id="prefix" class="manage-column column-title" style="" scope="col">Post Prefix</th> + <th id="suffix" class="manage-column column-title" style="" scope="col">Post Suffix</th> + </tr> + </thead> + <tfoot> + <tr> + <th id="server" class="manage-column column-title" style="" scope="col">Type</th> + <th id="server" class="manage-column column-title" style="" scope="col">Server</th> + <th id="path" class="manage-column column-title" style="" scope="col">Path</th> + <th id="user" class="manage-column column-title" style="" scope="col">User Name</th> + <th id="pwd" class="manage-column column-title" style="" scope="col">Password</th> + <th id="prefix" class="manage-column column-title" style="" scope="col">Post Prefix</th> + <th id="suffix" class="manage-column column-title" style="" scope="col">Post Suffix</th> + </tr> + </tfoot> + <tbody> +<?php + $num = count($options[statusserver]) + 5; + if ($num == 5) + { + $options[statustype][0] = "status"; + $options[statusserver][0] = "identi.ca"; + $options[statuspath][0] = "api"; + + $num = 5; + } + for ($i = 0; $i < $num; $i++) + { + ?> + <tr> + <th id="type" class="<?php if($options[statuserrormsg][$i] != "") { echo 'error'; } else { echo 'manage-column column-title'; } ?>" style="" scope="col"><?php if($options[statuserrormsg][$i] == "") { echo '<INPUT TYPE=hidden NAME=statuserrormsg['. $i .'] VALUE="">'; } else { echo 'Error message: <BR /><SELECT NAME=statuserrormsg['. $i .']><OPTION VALUE="">Re-activate account</OPTION><OPTION VALUE="'. $options[statuserrormsg][$i] .'" style="background-color: 770000; width: 190px;" SELECTED>'. $options[statuserrormsg][$i] .'</OPTION></SELECT><BR />'; } ?> + + + <SELECT ID=statustype<?php echo $i ?> onChange="javascript:verifyline('<?php echo $i ?>');" NAME=statustype[<?php echo $i ?>]><OPTION VALUE="status" <?php if($options[statustype][$i] == "status") { echo ' SELECTED'; } ?>>Status.net</OPTION><OPTION VALUE="twitter" <?php if($options[statustype][$i] == "twitter") { echo ' SELECTED'; } ?>>Twitter</OPTION><OPTION VALUE="socialoomph" <?php if($options[statustype][$i] == "socialoomph") { echo ' SELECTED'; } ?>>SocialOomph</OPTION></SELECT> + <SELECT ID=statusauthtype<?php echo $i ?> NAME=statusauthtype[<?php echo $i ?>] <?php if ($options[statustype][$i] == "socialoomph") { echo 'CLASS=hiddenfield'; } ?> onChange="javascript:if(document.getElementById('statustype<?php echo $i ?>').options[document.getElementById('statustype<?php echo $i ?>').selectedIndex].value == 'twitter') { document.getElementById('statusauthtype<?php echo $i ?>').selectedIndex = 1; } else { document.getElementById('statusauthtype<?php echo $i ?>').selectedIndex = 0; }; if(document.getElementById('statusauthtype<?php echo $i ?>').options[document.getElementById('statusauthtype<?php echo $i ?>').selectedIndex].value == 'oauth') { document.getElementById('oauthA<?php echo $i ?>').className = 'nothiddenfield'; document.getElementById('oauthB<?php echo $i ?>').className = 'nothiddenfield'; document.getElementById('oauthC<?php echo $i ?>').className = 'nothiddenfield'; document.getElementById('oauthD<?php echo $i ?>').className = 'nothiddenfield';} else { document.getElementById('oauthA<?php echo $i ?>').className = 'hiddenfield'; document.getElementById('oauthB<?php echo $i ?>').className = 'hiddenfield'; document.getElementById('oauthC<?php echo $i ?>').className = 'hiddenfield'; document.getElementById('oauthD<?php echo $i ?>').className = 'hiddenfield'; }"><OPTION VALUE="basic" <?php if($options[statusauthtype][$i] == "basic") { echo ' SELECTED'; } ?>>Basic Auth</OPTION><OPTION VALUE="oauth" <?php if($options[statusauthtype][$i] == "oauth") { echo ' SELECTED'; } ?>>Oauth</OPTION></SELECT> + </th> + <th id="server" class="<?php if($options[statuserrormsg][$i] != "") { echo 'error'; } else { echo 'manage-column column-title'; } ?>" style="" scope="col"><INPUT TYPE=text ID=statusserver<?php echo $i ?> <?php if ($options[statustype][$i] == "twitter" || $options[statustype][$i] == "socialoomph") { echo 'CLASS=hiddenfield'; } ?> NAME=statusserver[<?php echo $i ?>] VALUE="<?php echo $options[statusserver][$i]; ?>" SIZE=20></th> + <th id="path" class="<?php if($options[statuserrormsg][$i] != "") { echo 'error'; } else { echo 'manage-column column-title'; } ?>" style="" scope="col"><INPUT TYPE=text ID=statuspath<?php echo $i ?> <?php if ($options[statustype][$i] == "twitter" || $options[statustype][$i] == "socialoomph") { echo 'CLASS=hiddenfield'; } ?> NAME=statuspath[<?php echo $i ?>] VALUE="<?php echo $options[statuspath][$i]; ?>" SIZE=20></th> + <th id="user" class="<?php if($options[statuserrormsg][$i] != "") { echo 'error'; } else { echo 'manage-column column-title'; } ?>" style="" scope="col"><SELECT NAME=statussocialoomphuser[<?php echo $i ?>] ID=statussocialoomphuser<?php echo $i ?> <?php if ($options[statustype][$i] == "socialoomph") { echo 'CLASS=nothiddenfield'; } else { echo 'CLASS=hiddenfield'; } ?> onChange="javascript:document.getElementById('statususer<?php echo $i ?>').value=document.getElementById('statussocialoomphuser<?php echo $i ?>').options[document.getElementById('statussocialoomphuser<?php echo $i ?>').selectedIndex].value;"><?php echo str_replace('VALUE="'. $options[statususer][$i] .'"','VALUE="'. $options[statususer][$i] .'" SELECTED',$socialoomphhtml); ?></SELECT><B ID=oauthA<?php echo $i ?> <?php if ($options[statusauthtype][$i] != "oauth") { echo 'CLASS=hiddenfield'; } ?>>Consumer key:</B><INPUT TYPE=text NAME=statususer[<?php echo $i ?>] <?php if ($options[statustype][$i] == "socialoomph") { echo 'CLASS=hiddenfield'; } ?> ID=statususer<?php echo $i ?> VALUE="<?php echo $options[statususer][$i]; ?>" SIZE=20><BR /><B ID=oauthC<?php echo $i ?> <?php if ($options[statusauthtype][$i] != "oauth") { echo 'CLASS=hiddenfield'; } ?>>Consumer Secret:<INPUT TYPE=text NAME=statususer2[<?php echo $i ?>] VALUE="<?php echo $options[statususer2][$i]; ?>" SIZE=20></B></th> + <th id="pwd" class="<?php if($options[statuserrormsg][$i] != "") { echo 'error'; } else { echo 'manage-column column-title'; } ?>" style="" scope="col"><B ID=oauthB<?php echo $i ?> <?php if ($options[statusauthtype][$i] != "oauth") { echo 'CLASS=hiddenfield'; } ?>>Access Token:</B><INPUT TYPE=password NAME=statuspwd[<?php echo $i ?>] ID=statuspwd<?php echo $i ?> <?php if ($options[statustype][$i] == "socialoomph") { echo 'CLASS=hiddenfield'; } ?> VALUE="<?php echo $options[statuspwd][$i]; ?>" SIZE=20><BR /><B ID=oauthD<?php echo $i ?> <?php if ($options[statusauthtype][$i] != "oauth") { echo 'CLASS=hiddenfield'; } ?>>Access Token Secret:<INPUT TYPE=text NAME=statuspwd2[<?php echo $i ?>] VALUE="<?php echo $options[statuspwd2][$i]; ?>" SIZE=20></B></th> + <th id="prefix" class="<?php if($options[statuserrormsg][$i] != "") { echo 'error'; } else { echo 'manage-column column-title'; } ?>" style="" scope="col"><INPUT TYPE=text NAME=statusprefix[<?php echo $i ?>] VALUE="<?php echo $options[statusprefix][$i]; ?>" SIZE=20></th> + <th id="suffix" class="<?php if($options[statuserrormsg][$i] != "") { echo 'error'; } else { echo 'manage-column column-title'; } ?>" style="" scope="col"><INPUT TYPE=text NAME=statussuffix[<?php echo $i ?>] VALUE="<?php echo $options[statussuffix][$i]; ?>" SIZE=20></th> + </tr> + <?php + } +?> </tbody> + </table> + + <div class="submit"><input type="submit" name="info_update" value="Update Options" class="button-primary" /></div></form> + <a target="_blank" href="http://feed.xaviermedia.com/xm-wordpress-stuff/"><img src="http://feeds.feedburner.com/xm-wordpress-stuff.1.gif" alt="XavierMedia.com - Wordpress Stuff" style="border:0"></a><BR/> + + <?php + +} + +function wpstatusnet_addoption() +{ + if (function_exists('add_options_page')) + { + add_options_page('WP-Status.net', 'WP-Status.net', 0, basename(__FILE__), 'wpstatusnet_options'); + } +} + +add_action('admin_menu', 'wpstatusnet_addoption'); + +?> -- GitLab