Skip to content

Latest commit

 

History

History
1467 lines (1257 loc) · 41.4 KB

README.md

File metadata and controls

1467 lines (1257 loc) · 41.4 KB

Social Share Privacy

Social Share Privacy is a jQuery plugin that lets you add social share buttons to your website that don't allow the social sites to track your users. The buttons are first disabled and a user needs to click them to enable them. So in order to e.g. like a site on facebook with these social share buttons a user needs to click two times. But in return for this extra click a user can only be tracked by this third party sites when he decides to enable the buttons. Using the settings menu a user can also permanently enable a social share button.

Supported share services:

Note that Tumblr and email are just normal links and thus always enabled.

This is a fork of socialSharePrivacy by Heise. In this fork the service support was made extensible, some services where added and some bugs fixed. It has some incompatible changes, though (consolidated option names, use of the boolean values true and false instead of the strings "on" and "off" etc.).

The original can be found here: http://www.heise.de/extras/socialshareprivacy/

The Delicious support was heavily inspired by the delicious button jQuery plugin: http://code.google.com/p/delicious-button/
The style for this button was atually copied and only slightly adapted from this plugin.

Overview

Dependencies

The jQuery cookies plugin is needed in order to enable services permanently. However, you can plug in your own replacement to store these options differently (e.g. via ajax in the user profile or in the browser's local store). For an example that stores the perma options in HTML5 local storage instead of cookies see the file localstorage.js.

How to use

<html>
<head><script type="text/javascript" src="jquery.js"></script> 
<script type="text/javascript" src="jquery.socialshareprivacy.min.js"></script>
<script type="text/javascript">
$(document).ready(function () {
	$('.share').socialSharePrivacy();
});
</script></head>
<body><div class="share"></div></body>
</html>

You only need to include the JavaScript files of the services you want to use. I recommend to pack all needed files into one using a JavaScript packer/compressor. The included build.sh script can do that for you, if you've got uglifyjs and uglifycss installed.

However, for your convenience I provide these precompiled versions of the scripts:

1 This file contains all JavaScripts except the jquery.socialshareprivacy.localstorage.js module and the translations.
2 This file contains the same as 1, but it also automatically initializes elements with the attribute data-social-share-privacy="true" set.
3 These files contain only translation strings and have to be included in addition to jquery.socialshareprivacy.min.js.

You can also asynchronously load the buttons if you use the jquery.socialshareprivacy.min.autoload.js script:

<html>
<head><script type="text/javascript" src="jquery.js"></script></head>
<body><div data-social-share-privacy="true"></div><div data-social-share-privacy="true"></div><script type="text/javascript">
(function () {
	var s = document.createElement('script');
    var t = document.getElementsByTagName('script')[0];

	s.type = 'text/javascript';
	s.async = true;
	s.src = 'jquery.socialshareprivacy.min.autoload.js';
	
    t.parentNode.insertBefore(s, t);
})();
</script>
</body>
</html>

Methods

socialSharePrivacy

.socialSharePrivacy([options])

Add social share buttons to all elements in the set. Returns this.

destroy

.socialSharePrivacy("destroy")

Remove all social share buttons. This will return all elements in the set back to their pre-init state. Returns this.

disable

.socialSharePrivacy("disable", [service_name])

Disable the named service or disable all services if no service_name is given. Returns this.

disabled

.socialSharePrivacy("disabled", [service_name])

Returns true if the given service is disabled, false otherwise. If service_name is not given then it will return an object that maps service names to their disabled-value.

enable

.socialSharePrivacy("enable", [service_name])

Enable the named service or enable all services if no service_name is given. Returns this.

enabled

.socialSharePrivacy("enabled", [service_name])

Returns true if the given service is enabled, false otherwise. If service_name is not given then it will return an object that maps service names to their enabled-value.

option

.socialSharePrivacy("option", option_name, [value])

Get or set an option. If no value is specified it will act as a getter. Returns this when acting as setter.

options

.socialSharePrivacy("options", [options])

Get or set all options. If no options are specified it will act as a getter. Returns this when acting as setter.

toggle

.socialSharePrivacy("toggle", [service_name])

Toggle the named service or toggle all services if no service_name is given. Returns this.

Events

socialshareprivacy:create

This event is emitted after the socialSharePrivacy method created a Social Share privacy widget. The event object will have an options attribute holding the option object of the initialized widget.

socialshareprivacy:destroy

This event is emitted before a Social Share Privacy widget is destroyed.

socialshareprivacy:disable

This event is emitted after a certain service was disabled. The event object will have a serviceName property, holding the name of the service that was disabled, and an isClick property, which is true if a click by a user caused this event (false if it was disabled via JavaScript).

socialshareprivacy:enable

This event is emitted after a certain service was enabled. The event object will have a serviceName property, holding the name of the service that was enabled, and an isClick property, which is true if a click by a user caused this event (false if it was enabled via JavaScript).

Options

Options can be set globally via $.fn.socialSharePrivacy.settings, via an options object passed to the socialSharePrivacy function or via data-* attributes of the share element. If options are defined in more than one way the data-* attributes will overwrite the options from the passed options object and the options from passed options object will overwrite the globally defined options.

data-* attributes

In order to pass the options as data-* attributes simply prepend data- to all option names. For the language option you can also use the standard lang attribute. If you want to set an option of a service just use a data-* attribute that includes dots (.) as if it where a JavaScript property expression:

<div class="share"
	lang="de"
	data-uri="http://example.com/"
	data-image="http://example.com/image.png"
	data-services.tumblr.type="photo"
	data-order="facebook twitter tumblr"></div>

If you want you can combine all options of a service and pass a JSON string as attribute value:

<div class="share"
	lang="de"
	data-uri="http://example.com/"
	data-image="http://example.com/image.png"
	data-services.tumblr='{"type":"photo"}'
	data-order="facebook twitter tumblr"></div>

You can also do this for all services:

<div class="share"
	lang="de"
	data-uri="http://example.com/"
	data-image="http://example.com/image.png"
	data-services='{"tumblr":{"type":"photo"}}'
	data-order="facebook twitter tumblr"></div>

Or even all options at once:

<div class="share"
	data-options='{
		"language" : "de",
		"uri"      : "http://example.com/",
		"image"    : "http://example.com/image.png",
		"services" : {
			"tumblr" : {
				"type" : "photo"
			}
		},
		"order"    : ["facebook", "twitter", "tumblr"]
	}'></div>

Actually these aren't JSON objects but JavaScript expressions. This way you can pass JavaScript code that will evaluate the option values when the socialSharePrivacy function is called. You can even pass a whole new service implementation inline, if you want:

<div class="share"
	data-options="{
		language : document.documentElement.lang,
		title    : document.title,
		services : {
			my_inline_service : {
				status         : true,
				dummy_line_img : 'dummy.png',
				dummy_alt      : 'DISABLED',
				display_name   : 'My Inline Service',
				txt_info       : 'Click to enable.',
				perma_option   : true,
				button         : function (options, uri, settings) {
					return $('<div>ENABLED</div>');
				}
			}
		}
	}"></div>

The main advantage of using the data-* attributes is, that you can easily render several different share elements on your webserver and then initialize them with one single JavaScript function call (no need for uniqe element IDs and separate JavaScript calls for each element).

NOTE: When passing service options via data-* attributes all option values (except the common service options) are treated as strings. If you need to pass values of other types (numbers, booleans, arrays or functions) you need to use the JavaScript object syntax.

Global Options

Set these options like this:

$.fn.socialSharePrivacy.settings.title = "Title of the thing to share.";

Or like this:

<script type="application/x-social-share-privacy-settings">
{
	path_prefix: "/socialshareprivacy",
	css_path:    "socialshareprivacy.css",}
</script>

The version using script tags uses again JavaScript expressions to enable inline service definitions.

Option Default Value Description
info_link http://panzi.github.io/SocialSharePrivacy/ The link of the i-icon that links users to more information about this.
info_link_target The target attribute of the info link. Possible values are _blank, _self, _parent, _top or a frame name.
txt_settings Settings The text of the settings icon.
txt_help [Text] Tooltip text of the settings menu.
settings_perma [Text] Headline of the settings menu.
layout line Possible values: line or box
set_perma_option function (service_name, settings) Function that stores the perma setting of the service specified by service_name.
del_perma_option function (service_name, settings) Function that removes the perma setting of the service specified by service_name.
get_perma_options function (settings) Function that gets the perma setting of all services in an object where the keys are the service names and the values are boolean. Services that are missing are assumed as false.
get_perma_option function (service_name, settings) Function that gets the perma setting of the service specified by service_name. Returns a boolean value.

Only one of the two functions get_perma_options and get_perma_option need to be implemented. In that case the respective other needs to be set to null.
perma_option true (if the jQuery cookies plugin is installed) Give users the posibility to permanently enable services. (Boolean)
cookie_path /
cookie_domain document.location.hostname
cookie_expires 365 Days until the cookie expires.
path_prefix Prefix to all paths (css_path, dummy_line_img, dummy_box_img)
css_path socialshareprivacy/socialshareprivacy.css
language en Tells the share service which language to use. (Does not affect the language displayed by Social Share Privacy.)
uri [Function] URI of the thing to share that is passed on to the share services. The default function uses the value of the first link element with the rel attribute canonical or the first meta element with the property attribute og:url it can find or location.href if there are no such elements. (Function or string)
title The title to pass to any share service that want's one.
description The description to pass to any share service that want's one.
image Image URL to pass to any share service that want's one.
embed HTML embed code to pass to any share service that want's one.
ignore_fragment true Ignore the #fragment part of the url. (Boolean)

Common Service Options

Option Default Value Description
status true Enable/disable this service. (Boolean)
class_name [service specific] The HTML class of the share button wrapper. Per default it is the key of the service as it is registered in jQuery.fn.socialSharePrivacy.settings.services.
button_class HTML class of the share button. Per default the same as class_name.
dummy_line_img Placeholder image for deactivated button in line layout.
dummy_box_img Placeholder image for deactivated button in box layout.
dummy_alt [Text] Alt text of the placeholder image.
txt_info [Text] Help text for deactivated button.
txt_off [Text] Status text if button is deactivated.
txt_on [Text] Status text if button is activated.
perma_option true Give users the posibility to permanently enable this service.
(Boolean)
display_name [Text] Name of the service.
referrer_track A string that is appended to the URI for this service, so you can track from where your users are coming.
language Override the global language just for this service.
path_prefix Override the global path_prefix just for this service.

Buffer Options (buffer)

See also: official documentation

Example:

$(document).ready(function () {
	$('#share').socialSharePrivacy({
		services: {
			buffer: {
				text : 'Some descriptive text...'
			}
		}
	});
});
Option Default Value Description
text jQuery.fn.socialSharePrivacy.getTitle Tweet text (excluding the URL). It will be truncated to 120 characters, leaving place for 20 characters for the shortened URL. (Function or string)
via Twitter username (without the leading @). (Function or string)
picture jQuery.fn.socialSharePrivacy.getImage URL of image that represents the thing to share. (Function or string)

Delicious Options (delicious)

See also: official documentation

Example:

$(document).ready(function () {
	$('#share').socialSharePrivacy({
		services: {
			delicious: {
				title : 'Bookmark title'
			}
		}
	});
});
Option Default Value Description
title jQuery.fn.socialSharePrivacy.getTitle Title of the new bookmark. (Function or string)

Disqus Options (disqus)

See also: official documentation

WARNING: This is a hack. Using this Disqus button will break any usage of the comment count code as shown on the linked page above. This button does of course not interfere with the main Disqus widget.

Example:

$(document).ready(function () {
	$('#share').socialSharePrivacy({
		services: {
			disqus: {
				shortname : 'myforumshortname',
				count     : 'reactions'
			}
		}
	});
});
Option Default Value Description
shortname Your Disqus forum shortname. If an empty string is given it tries to use window.disqus_shortname. (String)
count comments What count to show.
Possible values: comments or reactions
onclick Function to call when the Disqus button was clicked. (Function or String)

EMail Options (mail)

Option Default Value Description
subject jQuery.fn.socialSharePrivacy.getTitle Subject of the new email. (Function or string)
body [Function] Body of the new email. (Function or string)

Facebook Like/Recommend Options (facebook)

Note that facebook only supports certain languages and requires the region suffix (e.g. en_US). The facebook service ensures that only supported language strings are sent to facebook, because otherwise facebook fails to render anything.

See also: official documentation

Example:

$(document).ready(function () {
	$('#share').socialSharePrivacy({
		services: {
			facebook: {
				action      : 'recommend',
				colorscheme : 'dark'
			}
		}
	});
});
Option Default Value Description
action like Possible values: like or recommend
colorscheme light Possible values: light or dark
font Possible values: arial, lucida grande, segoe ui, tahoma, trebuchet ms or verdana

Facebook Share Options (fbshare)

There are no Facebook Share specific options.

See also: official documentation

Flattr Options (flattr)

See also: official documentation

Example:

$(document).ready(function () {
	$('#share').socialSharePrivacy({
		services: {
			flattr: {
				uid      : 'yourflattrid',
				category : 'Text'
			}
		}
	});
});
Option Default Value Description
title jQuery.fn.socialSharePrivacy.getTitle Title of the thing to share. (Function or string)
description jQuery.fn.socialSharePrivacy.getDescription Description of the thing to share. (Function or string)
uid Flattr username.
category Possible values: text, images, video, audio, software, people or rest
tags Multiple tags are seperated by a comma ,. Only alpha characters are supported in tags.
popout When set to 0 no popout will appear when the Flattr button is hovered.
hidden When set to 1 your content will not be publicly listed on Flattr.

Google+ Options (gplus)

There are no Google+ specific options.

See also: official documentation

Hacker News Options (hackernews)

See also: HNSearch API documentation

Option Default Value Description
title jQuery.fn.socialSharePrivacy.getTitle Title of the news to share. (Function or string)

Pinterest Options (pinterest)

See also: official documentation

Option Default Value Description
title jQuery.fn.socialSharePrivacy.getTitle Title of the thing to share. (Function or string)
description jQuery.fn.socialSharePrivacy.getDescription Description of the thing to share. (Function or string)
media jQuery.fn.socialSharePrivacy.getImage URL of image that represents the thing to share. (Function or string)

Linked in Options (linkedin)

See also: official documentation

Option Default Value Description
onsuccess Name of a callback function that shall invoked when the link was successfully shared. The shared url will be passed as a parameter. (String)
onerror Name of a callback function that shall invoked if link sharing failed. The shared url will be passed as a parameter. (String)
showzero false Even show count and no placeholder if there are zero shares. (Boolean)

Reddit Options (reddit)

See also: official documentation

Example:

$(document).ready(function () {
	$('#share').socialSharePrivacy({
		services: {
			reddit: {
				newwindow : false,
				bgcolor   : '#ffff00'
			}
		}
	});
});
Option Default Value Description
title jQuery.fn.socialSharePrivacy.getTitle Title of the thing to share. (Function or string)
target A cummunity to target.
newwindow 1 Opens reddit in a new window when set to 1. Set this option to an empty string or anything that evaluates to false to open reddit in the same window.
bgcolor transparent HTML color.
bordercolor HTML color.

Stumble Upon Options (stumbleupon)

There are no Stumble Upon specific options.

See also: official documentation

Tumblr Options (tumblr)

See also: official documentation

Example:

$(document).ready(function () {
	$('#share').socialSharePrivacy({
		services: {
			tumblr: {
				type  : 'photo',
				photo : 'http://example.com/example.png'
			}
		}
	});
});
Option Default Value Description
type link Possible values: link, quote, photo or video
name jQuery.fn.socialSharePrivacy.getTitle Title of the thing to share. (Function or string)

This option is only defined for the type link.
description jQuery.fn.socialSharePrivacy.getDescription Description of the thing to share. (Function or string)

This option is only defined for the type link.
quote [Function] Quote to share. (Function or string)

This option is only defined for the type quote.
photo jQuery.fn.socialSharePrivacy.getImage Image URL of the thing to share. (Function or string)

This option is only defined for the type photo.
clickthrou [Function] The URL to where you get when you click the image. Per default it's the shared URI including the referrer_track. (Function or string)

This option is only defined for the type photo.
embed jQuery.fn.socialSharePrivacy.getEmbed Embed code of the thing to share. (Function or string)

This option is only defined for the type video.
caption jQuery.fn.socialSharePrivacy.getDescription Caption of the thing to share. (Function or string)

This option is only defined for the types photo and video.

Twitter Options (twitter)

See also: official documentation

Example:

$(document).ready(function () {
	$('#share').socialSharePrivacy({
		services: {
			twitter: {
				hashtags : 'win'
			}
		}
	});
});
Option Default Value Description
text jQuery.fn.socialSharePrivacy.getTitle Tweet text (excluding the URL). It will be truncated to 120 characters, leaving place for 20 characters for the shortened URL. (Function or string)
via Twitter username (without the leading @).
related Twitter username (without the leading @).
hashtags Hashtag to add to the tweet (without the leading #).
dnt true Do not tailor.

XING Options (xing)

There are no XING specific options.

Note that the view counter will not work unless the XING button is enabled by the user.

See also: official documentation

Custom Services

(function ($, undefined) {
	$.fn.socialSharePrivacy.settings.services.myservice = {
		/* default values for common service options... */
		'button': function (options, uri, settings) {
			return $('<iframe scrolling="no" frameborder="0" allowtransparency="true"></iframe>').attr(
				'src', 'http://myservice.example/?' + $.param({
					url: uri + options.referrer_track
				});
		}
	};
})(jQuery);

Helper Functions (jQuery.fn.socialSharePrivacy.*)

Some helper functions that might be handy to use in your custom service.

absurl(url [, baseurl])

Build an absolute url using a base url. The provided base url has to be a valid absolute url. It will not be validated! If no base url is given the documents base url/location is used. Schemes that behave other than http might not work. This function tries to support file:-urls, but might fail in some cases. email:-urls aren't supported at all (don't make sense anyway).

abbreviateText(text, length)

Abbreviate at last blank before length and add "\u2026" (…, horizontal ellipsis). The length is the number of UTF-8 encoded bytes, not the number of unicode code points, because twitters 140 "characters" are actually bytes.

escapeHtml(text)

Escapes text so it can be used safely in HTML strings.

Character Replacement
< &lt;
> &gt;
& &amp;
" &quot;
' &#39;

formatNumber(number)

Format a number to be displayed in a typical number bubble. It will abbreviate numbers bigger than 9999 using the K suffix, rounding the number to the closest thousand and it inserts thousands delimeter characters.

Example:

$.fn.socialSharePrivacy.formatNumber(1234)    => "1,234"
$.fn.socialSharePrivacy.formatNumber(12345)   => "12K"
$.fn.socialSharePrivacy.formatNumber(1234567) => "1,235K"

getTitle(options, uri, settings)

Lookup title of shared thing in several places:

  • settings.title, which may be a string or a function with the same parameters.
  • $('meta[name="DC.title"]').attr('content') + ' - ' + $('meta[name="DC.creator"]').attr('content')
  • $('meta[name="DC.title"]').attr('content')
  • $('meta[property="og:title"]').attr('content')
  • $('title').text()

The element of the share button is passed as this.

getImage(options, uri, settings)

Lookup image URL of shared thing in several places:

  • settings.image, which may be a string or a function with the same parameters.
  • $('meta[property="image"], meta[property="og:image"], meta[property="og:image:url"], ' +
    'meta[name="twitter:image"], link[rel="image_src"], itemscope *[itemprop="image"]').
    first().attr('content' / 'src' / 'href')
  • $('img').filter(':visible').filter(function () { return $(this).parents('.social_share_privacy_area').length === 0; }), using the image with the biggest area.
  • $('link[rel~="shortcut"][rel~="icon"]').attr('href')
  • 'http://www.google.com/s2/favicons?'+$.param({domain:location.hostname})

The element of the share button is passed as this.

getEmbed(options, uri, settings)

Lookup image URL of shared thing in several places:

  • settings.embed, which may be a string or a function with the same parameters.

If there is no embed code found it will construct it's own embed code. For this it first searches for a meta element with the name twitter:player and use it's content as the src of an iframe element. If meta tags with the names twitter:player:width and twitter:player:height are found they are used for the width and height attributes of the iframe. If no twitter:player meta elements is found the url of the current page will be used as the iframe src (uri + options.referrer_track).

The element of the share button is passed as this.

getDescription(options, uri, settings)

Lookup description of shared thing in several places:

  • settings.description, which may be a string or a function with the same parameters.
  • $('meta[name="twitter:description"]').attr('content')
  • $('meta[itemprop="description"]').attr('content')
  • $('meta[name="description"]').attr('content')
  • $('article, p').first().text()
  • $('body').text()

If not defined in settings.description the found text is truncated at 3500 bytes.

The element of the share button is passed as this.

Build.sh

You can use build.sh to pack the modules and languages you want. This requires uglifyjs and uglifycss and extend to be installed.

In case you haven't done so already you might need to checkout the git submodules with

git submodule update --init --recursive

Example:

./build.sh -m twitter,facebook,gplus -l de,fr

This generates these files:

build/jquery.socialshareprivacy.min.js
build/jquery.socialshareprivacy.min.autoload.js
build/jquery.socialshareprivacy.min.de.js
build/jquery.socialshareprivacy.min.fr.js
build/jquery.socialshareprivacy.min.css

These files then contain only the JavaScript/CSS code for Twitter, Facebook and Google+. jquery.socialshareprivacy.min.de.js and jquery.socialshareprivacy.min.fr.js only contain translation strings, so you need to include them after jquery.socialshareprivacy.min.js in your HTML document.

Usage

Usage:
 ./build.sh [options]

Options:
 -h              Print this help message.
 -m <modules>    Comma separated list of JavaScript modules to pack. Possible values:
                     all, none, buffer, delicious, disqus, facebook, flattr,
                     gplus, hackernews, linkedin, mail, pinterest, reddit,
                     stumbleupon, tumblr, twitter, xing
                 default: all

 -l <languages>  Comma separated list of languages to pack. Possible values:
                     all, none, de, es, fr, nl, pl, pt, ru
                 default: all

 -a <enabled>    Autoload. Possible values: on, off (default: on)
 -c <enabled>    Pack stylesheets. Possible values: on, off (default: on)
 -i <enabled>    Pack images. Possible values: on, off (default: on)
 -p <path>       Prefix to stylesheet and dummy image paths. (empty per default)
 -s <path>       Stylesheet path in the generated JavaScript file.
                 default: stylesheets/jquery.socialshareprivacy.min.css
 -o <directory>  Output directory. (default: build)

Known Issues

In Internet Explorer <= 8 the Disqus widget doesn't work the first time you enable it. You have to disable and then enable it again. I could not figure out what might cause this.

It is recommended to declare a compatibility mode of Internet Explorer >= 9. E.g. add this to the head of your HTML documents:

<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>

Internet Explorer <= 7 is not supported.

License

Most of this plugin is licensed under the MIT license:

Copyright (c) 2012 Mathias Panzenböck
Copyright (c) 2011 Hilko Holweg, Sebastian Hilbig, Nicolas Heiringhoff, Juergen Schmidt, Heise Zeitschriften Verlag GmbH & Co. KG, http://www.heise.de

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

The file stylesheets/jquery.socialshareprivacy.delicious.css is licensed under the Apache License, Version 2.0:

Copyright (c) 2012 Mathias Panzenböck
Copyright (c) 2010 [Mike @ moretechtips.net]

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.