Objective: We know most of the collaboration part of sharepoint has been pushed to Teams, with its planner, conversations, a dedicated site for document uploads and many other features.
So the sharepoint site is now used mostly for document management. Being said that how we can accomodate SPO API for other applications.
In this post we will use SP Online OOTB API to upload and download a file. We dont need to write a seperate application for this, instead we will use POSTMAN to interpret the calls.
When API is avilable OOTB why do we need this post?
Well Service API is available OOTB, but there is specific way we need to authenticate to use that API. That is what we are going to show here.
Let me give a highlevel overview of how authentication works in Office365 Sharepoint site in this case.
Step 1: Know your Tenant ID and Resource ID
It is very important to know your tenant ID for triggering any kind of service calls.
You can get your the Tenant ID in following ways:
1. Using Powershell
2. Making a call to "_vti_bin/client.svc"
3. This is the most easiest way browse "/_layouts/15/appprincipals.aspx"
We use 3rd way as it is the easiest way. When you browse that url under any SP Online site from your tenant. The part after "@" is your tenant ID and the part before @ is Resource ID. Make a note of it.
Step 2: Register a new app
You need to register a new addin/app in your Sharepoint site, this will generate a ClientID and a Client Secret, which we will use to authenticate. Lets see how to do it.
Go to "_layouts/15/appregnew.aspx" under the SP Online site which you want to use as document repository.
Use the "Generate" buttons to generate a unique ClientID and Client Secret. Give appropriate title to your app. You can make localhost as your domain name and redirect url. Click Create.
Note: Please copy and paste those ClientID and Client Secret in a sepearte notepad file as you cannot retrieve them after saving this information.
Step 3: Grant permissions
New Client app has been created in SP Online site, now its time to decide what permissions this app should have on your site. You can grant Site collection, web or even at list level read or write permissions.
Go to "/_layouts/15/appinv.aspx" and serach with ClientID we generated earlier. The application will fetch all other details based on your ClientID.
Add the below XML snippet specifying what kind of permissions you want this app to have on your site. I want to upload a document so i granted "Write" permission.
More details about the permissions are here.
Click "Create" button and you will be prompted if you trust the app to run with said permissions. Click Trust.
Note: App registration can also be done using Azure App services. But it requires a storage account and proper azure subscription and you need to pay for maintiaining app service in azure.
So we choose the cheap and easiest way.
Step 4: Get Access Token for the Office365 Tenant.
Open Postman and make a request for access token as below.
Header:
Content-Type : application/x-www-form-urlencoded
Body:
grant_type : client_credentials
client_id : ClientID@TenantID
client_secret : Client Secret
resource: ResourceID/<TenantName>.sharepoint.com@TenantID
ResourceID is common for all of SPO.
Now click send and you will receive an access token.
Copy that access token as we need to send it in headers for every API request.
Please note that every access token will be valid for 3600 seconds or 1 hour.
Step 5: Make a call to Sharepoint REST API
Its time to test the access to REST API using the OAuth access token.
First we will make a call to get the title of the site using REST API. Below is the url we need to make a call to get Title.
https://1yearsub.sharepoint.com/sites/DEV/_api/web?$select=Title
Make a postman request shown as below with below headers.
So the sharepoint site is now used mostly for document management. Being said that how we can accomodate SPO API for other applications.
In this post we will use SP Online OOTB API to upload and download a file. We dont need to write a seperate application for this, instead we will use POSTMAN to interpret the calls.
When API is avilable OOTB why do we need this post?
Well Service API is available OOTB, but there is specific way we need to authenticate to use that API. That is what we are going to show here.
Let me give a highlevel overview of how authentication works in Office365 Sharepoint site in this case.
Step 1: Know your Tenant ID and Resource ID
It is very important to know your tenant ID for triggering any kind of service calls.
You can get your the Tenant ID in following ways:
1. Using Powershell
2. Making a call to "_vti_bin/client.svc"
3. This is the most easiest way browse "/_layouts/15/appprincipals.aspx"
We use 3rd way as it is the easiest way. When you browse that url under any SP Online site from your tenant. The part after "@" is your tenant ID and the part before @ is Resource ID. Make a note of it.
Step 2: Register a new app
You need to register a new addin/app in your Sharepoint site, this will generate a ClientID and a Client Secret, which we will use to authenticate. Lets see how to do it.
Go to "_layouts/15/appregnew.aspx" under the SP Online site which you want to use as document repository.
Use the "Generate" buttons to generate a unique ClientID and Client Secret. Give appropriate title to your app. You can make localhost as your domain name and redirect url. Click Create.
Note: Please copy and paste those ClientID and Client Secret in a sepearte notepad file as you cannot retrieve them after saving this information.
Step 3: Grant permissions
New Client app has been created in SP Online site, now its time to decide what permissions this app should have on your site. You can grant Site collection, web or even at list level read or write permissions.
Go to "/_layouts/15/appinv.aspx" and serach with ClientID we generated earlier. The application will fetch all other details based on your ClientID.
Add the below XML snippet specifying what kind of permissions you want this app to have on your site. I want to upload a document so i granted "Write" permission.
<AppPermissionRequests AllowAppOnlyPolicy="true"> <AppPermissionRequest Scope="http://sharepoint/content/sitecollection/web" Right="Write" /> </AppPermissionRequests>
More details about the permissions are here.
Click "Create" button and you will be prompted if you trust the app to run with said permissions. Click Trust.
Note: App registration can also be done using Azure App services. But it requires a storage account and proper azure subscription and you need to pay for maintiaining app service in azure.
So we choose the cheap and easiest way.
Step 4: Get Access Token for the Office365 Tenant.
Open Postman and make a request for access token as below.
Header:
Content-Type : application/x-www-form-urlencoded
Body:
grant_type : client_credentials
client_id : ClientID@TenantID
client_secret : Client Secret
resource: ResourceID/<TenantName>.sharepoint.com@TenantID
ResourceID is common for all of SPO.
Now click send and you will receive an access token.
Copy that access token as we need to send it in headers for every API request.
Please note that every access token will be valid for 3600 seconds or 1 hour.
Its time to test the access to REST API using the OAuth access token.
First we will make a call to get the title of the site using REST API. Below is the url we need to make a call to get Title.
https://1yearsub.sharepoint.com/sites/DEV/_api/web?$select=Title
Make a postman request shown as below with below headers.
Header:
Accept : application/json;odata=verbose
Authorization : Bearer <Access Token>
We see the title fetched by a REST API call using POSTMAN.
Upload Document Using REST API:
Headers will be same as above and it will be a POST call this time. Attach the file in the body tab of the POSTMAN call.
Header:
Accept : application/json;odata=verbose
Authorization : Bearer <Access Token>
REST Request Url : https://1yearsub.sharepoint.com/sites/DEV/_api/web/GetFolderByServerRelativeUrl('/sites/dev/shared documents')/Files/add(url='testfile.pdf',overwrite=true)
Here is the result:
Download the Document using REST API:
Headers will be same as above and its a Get call now.
Header:
Accept : application/json;odata=verbose
Authorization : Bearer <Access Token>
REST Request Url: https://1yearsub.sharepoint.com/sites/DEV/_api/web/GetFolderByServerRelativeUrl('/sites/dev/shared documents')/Files('testfile.pdf')/$value
Looking at the response we see that the file is downloaded in binary.
Hope this post gave some better idea on Accessing REST API of Sharepoint Online site.
Happy Coding !
Accept : application/json;odata=verbose
Authorization : Bearer <Access Token>
We see the title fetched by a REST API call using POSTMAN.
Upload Document Using REST API:
Headers will be same as above and it will be a POST call this time. Attach the file in the body tab of the POSTMAN call.
Header:
Accept : application/json;odata=verbose
Authorization : Bearer <Access Token>
REST Request Url : https://1yearsub.sharepoint.com/sites/DEV/_api/web/GetFolderByServerRelativeUrl('/sites/dev/shared documents')/Files/add(url='testfile.pdf',overwrite=true)
Here is the result:
Download the Document using REST API:
Headers will be same as above and its a Get call now.
Header:
Accept : application/json;odata=verbose
Authorization : Bearer <Access Token>
REST Request Url: https://1yearsub.sharepoint.com/sites/DEV/_api/web/GetFolderByServerRelativeUrl('/sites/dev/shared documents')/Files('testfile.pdf')/$value
Looking at the response we see that the file is downloaded in binary.
Hope this post gave some better idea on Accessing REST API of Sharepoint Online site.
Happy Coding !
Thank you for detailed steps. I followed it, it worked!
ReplyDeleteI am glad it is helping others
DeleteHi,
DeleteI have Sharepoint Onprimises, and ReactNative App(for Mobile).My query is How i can get sharepoint onprimises data into ReactNative App or mobile App.
Please suggest me best solution for this.I am not able to achive this from 1 month.
Thanks
For sharpeoint Onprem . .. you dont need all this hassle. API calls are simple using what ever authentication the site prefers.
DeleteCould you please explain this in detail, Thanks
DeleteI would need this too on a on prem server.
DeleteExplain it plz.
thx
Thanks for detailed steps... I guess my request is failing at step 7 & 8. I got token and its working if i enter a wron one it says invalid token
ReplyDeleteHere is the error "Exception of type 'Microsoft.IdentityModel.Tokens.AudienceUriValidationFailedException' was thrown."
Any pointers ?
Please know that the OAUTH token will be expired in 60 Mins, so its better to generate based on a timer watch.
DeleteYou will get this error if there is a typo in the resource. I had a typo in the tenant name which led to me receiving this error.
DeleteI love you so much!
DeleteI can't get it to recognize the client secret. Every time I try I get this:
ReplyDelete"error": "invalid_client",
"error_description": "AADSTS7000215: Invalid client secret is provided. Trace ID: 15b8019a-f3be-4a99-8174-1ee409392100 Correlation ID: 9044679d-b380-4166-9318-63270f2fa19b Timestamp: 2019-03-03 18:56:48Z",
I've recreated the app credentials three times now; it just won't take the client secret. Have you ever run into this problem?
If its saying "Invalid client" while sending request, you need to check that you use same tenant ID in both the URI request and the parameter in the request body. Also did you registered and trusted the App as shown in Step3. Let me know how it goes.
DeleteAlso make sure you use UrlEncode for client secret. I use SPFx to send it as json to Flow Url with 'secretId': encodeURIComponent('/za1J....WtXQfc5rfvs//JsiPKW8='),
DeleteHi I get API like postman on android app but I got:
ReplyDelete{"error":"invalid_request","error_description":"AADSTS900144: The request body must contain the following parameter: 'grant_type'.\r\nTrace ID: 31842e84-cc80-4a12-9047-f42a262b2f00\r\nCorrelation ID: 0a4090f5-388f-4733-a178-b7874f557e69\r\nTimestamp: 2019-03-20 16:40:40Z","error_codes":[900144],"timestamp":"2019-03-20 16:40:40Z","trace_id":"31842e84-cc80-4a12-9047-f42a262b2f00","correlation_id":"0a4090f5-388f-4733-a178-b7874f557e69"}
2019-03-20 23:40:46.985 22006-22200/securechannels.com.sharepointchallenge D/OkHttp: <-- END HTTP (437-byte body)
Getting the same error... did you solve it?
DeleteI solved it! I was placing the parameters as headers and not within the body! The only header should be the content-type one
Deletei too got the same error but iam passing the parameters in body. did any solve this
DeleteIt seems like your request body parameters are not as shown in the step 4. I ran the project code one more time today and it works. Please recheck that you provided the same tenat ID in both request URI and the Request body.
ReplyDeleteFollowed these instructions to the letter but I can't seem to authenticate to get a token. Getting the following error and everything I google keeps pointing me to Azure AD stuff.
ReplyDeleteAADSTS500011: The resource principal named was not found in the tenant named . This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You might have sent your authentication request to the wrong tenant
Any ideas?
Did you made your SP admin to approve and trust the App in App Console. I am asking stupid question.. but you used your own Tenant ID and Tenant name and other unique identifiers... right?
DeleteYes, we went to /.../sites/..../appinv.aspx to give it permissions unless there is another place it needs permission. We did this on the main sharepoint.com site and then each on both sites and teams pages we wanted to have access.
DeleteWhen looking at enabled list of apps for each site/team page the one we created is there, but the odd thing is that everything on both apps all use the same resource id.
I know we're missing something I just dont know what. And I know nothing of sharepoint nor do I even have access to it. I have to get our IT people involved, and they dont know anything about it either.
I had this same issue. I realized I was using the Resource ID for my specific app instead of 00000003-0000-0ff1-ce00-000000000000 (the actual value highlighted in step 1). This is the principal id for SharePoint. https://docs.microsoft.com/en-us/archive/blogs/kaevans/inside-sharepoint-2013-oauth-context-tokens
DeletePerfect, thanks!
ReplyDeleteIs there a way to display the Title of the App in the 'Modified By ' Column instead of 'Sharepoint App'?
ReplyDeleteHello , is there any way to the refresh token or everytime i have to do the request ?
ReplyDeleteThose are two seperate calls . . . you dont need to do it for every call, token is valid for 1 hour by default. You can have the callee method to handle and retry if the token expires
DeleteHi, Thank you so much for the detail steps. I followed each step But unfortunately it generate the below error response.
ReplyDeleteCould not get any response
There was an error connecting to .
Why this might have happened:
The server couldn't send a response:
Ensure that the backend is working properly
Self-signed SSL certificates are being blocked:
Fix this by turning off 'SSL certificate verification' in Settings > General
Proxy configured incorrectly
Ensure that proxy is configured correctly in Settings > Proxy
Request timeout:
Change request timeout in Settings > General
The environment is O365. Any help for these Please?
I got this too. Help would be greatly appreciated.
DeleteDisabling the "Accept" header will show, this is a 401 Unauthorized response, though the the token is new and got it just fine. (I'm not the author of the original question, but the comment is mine)
DeleteHi Pratap,
ReplyDeleteThanks for sharing this. Its really helpful for us to understand how sharepoint API works. I followed all the steps and however i am unable make a call for step 7 and 8 to upload a file in sharepoint. I am getting the following error though i have used the fresh token.
{"error_description":"Exception of type 'Microsoft.IdentityModel.Tokens.AudienceUriValidationFailedException' was thrown."}
Could you please help me in resolving this.
Regards,
Khadar.
You may need to check your tenant ID in the request Url. That exception says you are hitting other tenant intead of your.
DeleteHi Pratap,
DeleteI used the tenant id to get token but i didn't use any tenant id while uploading a document to sharepoint. i don't see tenant id in your attachment also.
https://sharepoint/sites/dev/_api/web/GetFolderByServerRelativeUrl('/Shared Documents')/Files/add(url='test.xlsx',overwrite=true)
could you please help me where exactly i am missing it.
Regards,
Khadar.
You are missinf the site path in the GetfolderByServerRelativeUrl(), you should give starting from "/sites/dev/Shared Documents". Try that and let me know.
DeleteStill it is same Pratap :(
DeleteCan you chek are you able to get site title using the same token.
DeleteHi Pratap,
ReplyDeleteThanks for this steps. I am new to Spring rest and sharepoint api ,I am working on requirement to upload and download files in sharepoint using above mentioned APIs through spring rest + java. i have referred several post where i didnt find solution. can you guide in this
Hi The above project will help you orchestrate the API calls any falvor of code. This post will help you to see which url to be called , which parameters need to be passed and how to authenticate.
DeleteHello Pratap, thank you very much for this post. But, I'm having trouble getting the token because I don't understand this part: "resource: ResourceID/.sharepoint.com@TenantID"
ReplyDeleteWhat is ResourceID? Where do I get it?
you get the resource id from ""/_layouts/15/appprincipals.aspx""
DeleteI dont understand you, I dont find the ResourceID
DeleteHi Pratap, Thanks for sharing this article. Do you have similar example for creating folder in sharepoint?
ReplyDeleteHello Pratap,We are trying to fetch the Token from Sharepoint.I followed the POSTMAN steps above , from our end, and entered the credential given by Sharepoint folks , but I am getting below error:
ReplyDelete{
"error": "unauthorized_client",
"error_description": "AADSTS700016: Application with identifier '617fb736-c1a0-4a13-bc74-daf29daca596' was not found in the directory '68283f3b-8487-4c86-adb3-a5228f18b893'. This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You may have sent your authentication request to the wrong tenant.\r\nTrace ID: 77fc7f47-0cf9-479e-b2ae-048eb69c0800\r\nCorrelation ID: 3fc25fe1-9988-4a70-b0f4-60233384c60d\r\nTimestamp: 2019-11-07 06:06:38Z",
"error_codes": [
700016
],
"timestamp": "2019-11-07 06:06:38Z",
"trace_id": "77fc7f47-0cf9-479e-b2ae-048eb69c0800",
"correlation_id": "3fc25fe1-9988-4a70-b0f4-60233384c60d",
"error_uri": "https://accounts.accesscontrol.windows.net/error?code=700016"
}
Could you please help, at which step, the problem might be?
Hi Pratap,
ReplyDeleteI have a SharePoint site that's On premise as well . Can u guide me as well how to retrieve the data.
I tried your approach and was able to get the access token, but in the next request, when I try to hit the welcome page of the site, I get this error:
ReplyDelete{"error_description":"Invalid issuer or signature."}
Hi, Thanks for this blog..
ReplyDeleteI face one issue when trying to upload I get:
{"error":{"code":"-2147024891, System.UnauthorizedAccessException","message":{"lang":"en-US","value":"Access denied. You do not have permission to perform this action or access this resource."}}}
download worked ok....what am I missing?
Hi Pratap, I was able to upload the PDF file to SharePoint in the Binary mode by attaching the file and it worked well. I need to convert the PDF file to Base64 format and upload the same in the 'raw' mode in the Body.
ReplyDeleteCan you please help me with the Body structure for the 'raw' mode ?
How to find the ResourceId ?
ReplyDeletepublic static void main(String[] args) throws Exception{
ReplyDelete/**
* This function helps to get SharePoint Access Token. SharePoint Access
* Token is required to authenticate SharePoint REST service while performing Read/Write events.
* SharePoint REST-URL to get access token is as:
* https://accounts.accesscontrol.windows.net//tokens/OAuth/2
*
* Input required related to SharePoint are as:
* 1. shp_clientId
* 2. shp_tenantId
* 3. shp_clientSecret
*/
String accessToken = "";
String shp_clientId="f83e1c66-f64d-49a1-85e2-207e47092918";
String shp_tenantId="42150afd-062a-4a0f-81b9-0323526054cc";
String shp_clientSecret="28t3QzULzpYlhJhHqt8BTGn+xY/WmQQcokb1SxKryNI=";
try {
// AccessToken url
String wsURL = "https://accounts.accesscontrol.windows.net/" + shp_tenantId + "/tokens/OAuth/2";
URL url = new URL(wsURL);
URLConnection connection = url.openConnection();
HttpURLConnection httpConn = (HttpURLConnection) connection;
// Set header
httpConn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
httpConn.setRequestProperty("Content-Length", "0");
httpConn.setRequestProperty("Host", "");
httpConn.setDoOutput(true);
httpConn.setDoInput(true);
httpConn.setRequestMethod("POST");
// Prepare RequestData
String jsonParam = "grant_type=client_credentials"
+ "&client_id=" + shp_clientId + "@" + shp_tenantId
+ "&client_secret=" + shp_clientSecret
+ "&resource=00000003-0000-0ff1-ce00-000000000000/naveedtest.sharepoint.com@" + shp_tenantId;
//Here, is "Origanisations's Sharepoint Host"
// Send Request
DataOutputStream wr = new DataOutputStream(httpConn.getOutputStream());
wr.writeBytes(jsonParam);
wr.flush();
wr.close();
// Read the response.
InputStreamReader isr = null;
if (httpConn.getResponseCode() == 200) {
isr = new InputStreamReader(httpConn.getInputStream());
} else {
isr = new InputStreamReader(httpConn.getErrorStream());
}
BufferedReader in = new BufferedReader(isr);
String responseString = "";
String outputString = "";
// Write response to a String.
while ((responseString = in.readLine()) != null) {
outputString = outputString + responseString;
System.out.println(outputString);
}
// Extracting accessToken from string, here response (outputString)is a Json format string
if (outputString.indexOf("access_token\":\"") > -1) {
int i1 = outputString.indexOf("access_token\":\"");
String str1 = outputString.substring(i1 + 15);
int i2 = str1.indexOf("\"}");
String str2 = str1.substring(0, i2);
accessToken = str2;
}
} catch (Exception e) {
accessToken = "Error: " + e.getMessage();
}
}
I am getting error while trying to get access token
{"error":"invalid_client","error_description":"AADSTS7000215: Invalid client secret is provided.\r\nTrace ID: 86067e1a-5f5a-4783-8f56-29a1daa99700\r\nCorrelation ID: edadff7e-2c28-4f6b-bc4c-15b727762919\r\nTimestamp: 2020-06-19 18:03:20Z","error_codes":[7000215],"timestamp":"2020-06-19 18:03:20Z","trace_id":"86067e1a-5f5a-4783-8f56-29a1daa99700","correlation_id":"edadff7e-2c28-4f6b-bc4c-15b727762919","error_uri":"https://accounts.accesscontrol.windows.net/error?code=7000215"}
Using postman with the same configuration i.e. client secret,tenant id i am able to get the access token properly.
Encode your shp_clientSecret ( e.g. replace + by %2B , = by %3D etc)
DeleteThanks very much for the guide. followed step by step, got access token, but received error below when try to access the API:
ReplyDelete{"error":"invalid_request","error_description":"Token type is not allowed."}
Any help would be great appreciated!
Hi,
ReplyDeleteI followed the same steps which are mentioned in this blog. I am able to generate the access token. But I am getting "{"error":"invalid_request","error_description":"Token type is not allowed."}" while trying to access Sharepoint data using the access token. I have all kind of admin access to Sharepoint Site.I am the owner of this site. Can anyone help me on resolving this error.
Hi Me too getting the same error "{"error":"invalid_request","error_description":"Token type is not allowed."}" is there any solution please share.
DeleteSame error here. Please help...
DeleteI cannot figure out without a screenshot of the request. Please send me the request so that i can help.
DeleteHi I am too getting the same error please help
ReplyDelete{"error":"invalid_request","error_description":"Token type is not allowed."}
{"error":"invalid_request","error_description":"Token type is not allowed."} same error getting
ReplyDeleteDo you have any postman collection for this?
ReplyDeleteHi heller
DeleteYes i do, but cant share it as it has all the IDs to missuse my DEV tentant. Sorry.
why I cant run this link on my share point site..
ReplyDelete3. This is the most easiest way browse "/_layouts/15/appprincipals.aspx"
it display a message :
There are no apps having explicit access to the site collection
I can't this to work. I tried to different posts, multiple times, but I always get the Microsoft.IdentityModel.Tokens.AudienceUriValidationFailedException exception whatever I do.
ReplyDeleteI manage to get to the token, but can't do a request to https://siteURL/sites/siteName/_api/web?$select=Title
Anything I could be overlooking?
Thanks
Make sure you don't have a typo anywhere in tenant url.
DeleteHi, thanks for the great tutorial! Everything works for me except uploading the document. I receive the following
ReplyDelete{
"error": {
"code": "-2147024891, System.UnauthorizedAccessException",
"message": {
"lang": "en-US",
"value": "Access denied. You do not have permission to perform this action or access this resource."
}
}
}
Any ideas are much appreciated! Thanks
Same Error....
DeleteHi , I am able to get the token. But the Rest call is failing.
ReplyDelete{"error":"invalid_request","error_description":"Token type is not allowed."}
Hi I have same error, have you figured out the fix? thanks
DeleteHi Pratap,
ReplyDeleteI am getting below error
"error": "invalid_request",
"error_description": "AADSTS90002: Tenant 'baa501b8-c1cf-4376-ad5f-a02c9e7fac86' not found. This may happen if there are no active subscriptions for the tenant. Check to make sure you have the correct tenant ID. Check with your subscription administrator.\r\nTrace ID: 63b66b28-e489-4b4b-a92f-de8d2fdf3102\r\nCorrelation ID: 741c5664-4379-49f2-b83f-40d27c643b98\r\nTimestamp: 2021-07-07 10:40:46Z",
"error_codes": [
90002
],
I am accessing SharePoint 2019 On-premises site. I explicitly want to access SharePoint site using this approach in postman(Client requirement). Followed all steps provided from your article. Note: I dont wnat to use NTLM approach because of user name, password security. Please let me where I am wrong also let me know how to acces SharePoint on-premises in Postman using your approach.
Hi, I also get the error like others:
ReplyDelete{"error":"invalid_request","error_description":"Token type is not allowed."}
I think it's related to some change at Microsoft side for new Sharepoint site.
But I still trying to figure out how to fix this.
ReplyDeleteThanks for sharing this post with us.Wikivela