mirror of
https://github.com/yuzu-emu/breakpad.git
synced 2024-11-28 05:14:14 +01:00
Adding possibility for client to upload the file
This CL adds three features that will allow the client to upload the report file. Three main modifications are made : - Allow upload url to have a file:// scheme, and write the HTTP request to file in that case - Split the request in two parts in case of a file:// scheme, the request time and the response time. A new API [handleNetworkResponse] is added. - Give the opportunity to the client to get the configuration NSDictionary to be able to recreate the breakpad context at response time. Patch by Olivier Robin <olivierrobin@chromium.org> Review URL: https://breakpad.appspot.com/2764002/ git-svn-id: http://google-breakpad.googlecode.com/svn/trunk@1368 4c0a9323-5329-0410-9bdc-e9ce6186880e
This commit is contained in:
parent
8cde5c5152
commit
1335417f9f
@ -199,6 +199,9 @@ void BreakpadRemoveUploadParameter(BreakpadRef ref, NSString *key);
|
|||||||
// Returns the number of crash reports waiting to send to the server.
|
// Returns the number of crash reports waiting to send to the server.
|
||||||
int BreakpadGetCrashReportCount(BreakpadRef ref);
|
int BreakpadGetCrashReportCount(BreakpadRef ref);
|
||||||
|
|
||||||
|
// Returns the next upload configuration. The report file is deleted.
|
||||||
|
NSDictionary *BreakpadGetNextReportConfiguration(BreakpadRef ref);
|
||||||
|
|
||||||
// Upload next report to the server.
|
// Upload next report to the server.
|
||||||
void BreakpadUploadNextReport(BreakpadRef ref);
|
void BreakpadUploadNextReport(BreakpadRef ref);
|
||||||
|
|
||||||
@ -207,6 +210,25 @@ void BreakpadUploadNextReport(BreakpadRef ref);
|
|||||||
void BreakpadUploadNextReportWithParameters(BreakpadRef ref,
|
void BreakpadUploadNextReportWithParameters(BreakpadRef ref,
|
||||||
NSDictionary *server_parameters);
|
NSDictionary *server_parameters);
|
||||||
|
|
||||||
|
// Upload a report to the server.
|
||||||
|
// |server_parameters| is additional server parameters to send.
|
||||||
|
// |configuration| is the configuration of the breakpad report to send.
|
||||||
|
void BreakpadUploadReportWithParametersAndConfiguration(
|
||||||
|
BreakpadRef ref,
|
||||||
|
NSDictionary *server_parameters,
|
||||||
|
NSDictionary *configuration);
|
||||||
|
|
||||||
|
// Handles the network response of a breakpad upload. This function is needed if
|
||||||
|
// the actual upload is done by the Breakpad client.
|
||||||
|
// |configuration| is the configuration of the upload. It must contain the same
|
||||||
|
// fields as the configuration passed to
|
||||||
|
// BreakpadUploadReportWithParametersAndConfiguration.
|
||||||
|
// |data| and |error| contain the network response.
|
||||||
|
void BreakpadHandleNetworkResponse(BreakpadRef ref,
|
||||||
|
NSDictionary *configuration,
|
||||||
|
NSData *data,
|
||||||
|
NSError *error);
|
||||||
|
|
||||||
// Upload a file to the server. |data| is the content of the file to sent.
|
// Upload a file to the server. |data| is the content of the file to sent.
|
||||||
// |server_parameters| is additional server parameters to send.
|
// |server_parameters| is additional server parameters to send.
|
||||||
void BreakpadUploadData(BreakpadRef ref, NSData *data, NSString *name,
|
void BreakpadUploadData(BreakpadRef ref, NSData *data, NSString *name,
|
||||||
|
@ -152,9 +152,15 @@ class Breakpad {
|
|||||||
void RemoveKeyValue(NSString *key);
|
void RemoveKeyValue(NSString *key);
|
||||||
NSArray *CrashReportsToUpload();
|
NSArray *CrashReportsToUpload();
|
||||||
NSString *NextCrashReportToUpload();
|
NSString *NextCrashReportToUpload();
|
||||||
|
NSDictionary *NextCrashReportConfiguration();
|
||||||
void UploadNextReport(NSDictionary *server_parameters);
|
void UploadNextReport(NSDictionary *server_parameters);
|
||||||
|
void UploadReportWithConfiguration(NSDictionary *configuration,
|
||||||
|
NSDictionary *server_parameters);
|
||||||
void UploadData(NSData *data, NSString *name,
|
void UploadData(NSData *data, NSString *name,
|
||||||
NSDictionary *server_parameters);
|
NSDictionary *server_parameters);
|
||||||
|
void HandleNetworkResponse(NSDictionary *configuration,
|
||||||
|
NSData *data,
|
||||||
|
NSError *error);
|
||||||
NSDictionary *GenerateReport(NSDictionary *server_parameters);
|
NSDictionary *GenerateReport(NSDictionary *server_parameters);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@ -448,19 +454,39 @@ NSString *Breakpad::NextCrashReportToUpload() {
|
|||||||
return [NSString stringWithFormat:@"%@/%@", directory, config];
|
return [NSString stringWithFormat:@"%@/%@", directory, config];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
NSDictionary *Breakpad::NextCrashReportConfiguration() {
|
||||||
|
return [Uploader readConfigurationDataFromFile:NextCrashReportToUpload()];
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
void Breakpad::HandleNetworkResponse(NSDictionary *configuration,
|
||||||
|
NSData *data,
|
||||||
|
NSError *error) {
|
||||||
|
Uploader *uploader = [[[Uploader alloc]
|
||||||
|
initWithConfig:configuration] autorelease];
|
||||||
|
[uploader handleNetworkResponse:data withError:error];
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
void Breakpad::UploadReportWithConfiguration(NSDictionary *configuration,
|
||||||
|
NSDictionary *server_parameters) {
|
||||||
|
Uploader *uploader = [[[Uploader alloc]
|
||||||
|
initWithConfig:configuration] autorelease];
|
||||||
|
if (!uploader)
|
||||||
|
return;
|
||||||
|
for (NSString *key in server_parameters) {
|
||||||
|
[uploader addServerParameter:[server_parameters objectForKey:key]
|
||||||
|
forKey:key];
|
||||||
|
}
|
||||||
|
[uploader report];
|
||||||
|
}
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
void Breakpad::UploadNextReport(NSDictionary *server_parameters) {
|
void Breakpad::UploadNextReport(NSDictionary *server_parameters) {
|
||||||
NSString *configFile = NextCrashReportToUpload();
|
NSDictionary *configuration = NextCrashReportConfiguration();
|
||||||
if (configFile) {
|
if (configuration) {
|
||||||
Uploader *uploader = [[[Uploader alloc]
|
return UploadReportWithConfiguration(configuration, server_parameters);
|
||||||
initWithConfigFile:[configFile UTF8String]] autorelease];
|
|
||||||
if (uploader) {
|
|
||||||
for (NSString *key in server_parameters) {
|
|
||||||
[uploader addServerParameter:[server_parameters objectForKey:key]
|
|
||||||
forKey:key];
|
|
||||||
}
|
|
||||||
[uploader report];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -794,18 +820,65 @@ void BreakpadUploadNextReport(BreakpadRef ref) {
|
|||||||
BreakpadUploadNextReportWithParameters(ref, nil);
|
BreakpadUploadNextReportWithParameters(ref, nil);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
NSDictionary *BreakpadGetNextReportConfiguration(BreakpadRef ref) {
|
||||||
|
try {
|
||||||
|
Breakpad *breakpad = (Breakpad *)ref;
|
||||||
|
if (breakpad)
|
||||||
|
return breakpad->NextCrashReportConfiguration();
|
||||||
|
} catch(...) { // don't let exceptions leave this C API
|
||||||
|
fprintf(stderr, "BreakpadGetNextReportConfiguration() : error\n");
|
||||||
|
}
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
void BreakpadUploadReportWithParametersAndConfiguration(
|
||||||
|
BreakpadRef ref,
|
||||||
|
NSDictionary *server_parameters,
|
||||||
|
NSDictionary *configuration) {
|
||||||
|
try {
|
||||||
|
Breakpad *breakpad = (Breakpad *)ref;
|
||||||
|
if (!breakpad || !configuration)
|
||||||
|
return;
|
||||||
|
breakpad->UploadReportWithConfiguration(configuration, server_parameters);
|
||||||
|
} catch(...) { // don't let exceptions leave this C API
|
||||||
|
fprintf(stderr,
|
||||||
|
"BreakpadUploadReportWithParametersAndConfiguration() : error\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
void BreakpadUploadNextReportWithParameters(BreakpadRef ref,
|
void BreakpadUploadNextReportWithParameters(BreakpadRef ref,
|
||||||
NSDictionary *server_parameters) {
|
NSDictionary *server_parameters) {
|
||||||
|
try {
|
||||||
|
Breakpad *breakpad = (Breakpad *)ref;
|
||||||
|
if (!breakpad)
|
||||||
|
return;
|
||||||
|
NSDictionary *configuration = breakpad->NextCrashReportConfiguration();
|
||||||
|
if (!configuration)
|
||||||
|
return;
|
||||||
|
return BreakpadUploadReportWithParametersAndConfiguration(ref,
|
||||||
|
server_parameters,
|
||||||
|
configuration);
|
||||||
|
} catch(...) { // don't let exceptions leave this C API
|
||||||
|
fprintf(stderr, "BreakpadUploadNextReportWithParameters() : error\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BreakpadHandleNetworkResponse(BreakpadRef ref,
|
||||||
|
NSDictionary *configuration,
|
||||||
|
NSData *data,
|
||||||
|
NSError *error) {
|
||||||
try {
|
try {
|
||||||
// Not called at exception time
|
// Not called at exception time
|
||||||
Breakpad *breakpad = (Breakpad *)ref;
|
Breakpad *breakpad = (Breakpad *)ref;
|
||||||
|
if (breakpad && configuration)
|
||||||
|
breakpad->HandleNetworkResponse(configuration,data, error);
|
||||||
|
|
||||||
if (breakpad) {
|
|
||||||
breakpad->UploadNextReport(server_parameters);
|
|
||||||
}
|
|
||||||
} catch(...) { // don't let exceptions leave this C API
|
} catch(...) { // don't let exceptions leave this C API
|
||||||
fprintf(stderr, "BreakpadUploadNextReport() : error\n");
|
fprintf(stderr, "BreakpadHandleNetworkResponse() : error\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,6 +122,20 @@
|
|||||||
// Get the number of crash reports waiting to upload.
|
// Get the number of crash reports waiting to upload.
|
||||||
- (void)getCrashReportCount:(void(^)(int))callback;
|
- (void)getCrashReportCount:(void(^)(int))callback;
|
||||||
|
|
||||||
|
// Get the next report to upload.
|
||||||
|
// - If upload is disabled, callback will be called with (nil, -1).
|
||||||
|
// - If a delay is to be waited before sending, callback will be called with
|
||||||
|
// (nil, n), with n (> 0) being the number of seconds to wait.
|
||||||
|
// - if no delay is needed, callback will be called with (0, configuration),
|
||||||
|
// configuration being next report to upload, or nil if none is pending.
|
||||||
|
- (void)getNextReportConfigurationOrSendDelay:
|
||||||
|
(void(^)(NSDictionary*, int))callback;
|
||||||
|
|
||||||
|
// Sends synchronously the report specified by |configuration|. This method is
|
||||||
|
// NOT thread safe and must be called from the breakpad thread.
|
||||||
|
- (void)threadUnsafeSendReportWithConfiguration:(NSDictionary*)configuration
|
||||||
|
withBreakpadRef:(BreakpadRef)ref;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
#endif // CLIENT_IOS_HANDLER_IOS_BREAKPAD_CONTROLLER_H_
|
#endif // CLIENT_IOS_HANDLER_IOS_BREAKPAD_CONTROLLER_H_
|
||||||
|
@ -155,6 +155,18 @@ NSString* GetPlatform() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This method must be called from the breakpad queue.
|
||||||
|
- (void)threadUnsafeSendReportWithConfiguration:(NSDictionary*)configuration
|
||||||
|
withBreakpadRef:(BreakpadRef)ref {
|
||||||
|
NSAssert(started_, @"The controller must be started before "
|
||||||
|
"threadUnsafeSendReportWithConfiguration is called");
|
||||||
|
if (breakpadRef_) {
|
||||||
|
BreakpadUploadReportWithParametersAndConfiguration(breakpadRef_,
|
||||||
|
uploadTimeParameters_,
|
||||||
|
configuration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
- (void)setUploadingEnabled:(BOOL)enabled {
|
- (void)setUploadingEnabled:(BOOL)enabled {
|
||||||
NSAssert(started_,
|
NSAssert(started_,
|
||||||
@"The controller must be started before setUploadingEnabled is called");
|
@"The controller must be started before setUploadingEnabled is called");
|
||||||
@ -260,6 +272,25 @@ NSString* GetPlatform() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)getNextReportConfigurationOrSendDelay:
|
||||||
|
(void(^)(NSDictionary*, int))callback {
|
||||||
|
NSAssert(started_, @"The controller must be started before "
|
||||||
|
"getNextReportConfigurationOrSendDelay is called");
|
||||||
|
dispatch_async(queue_, ^{
|
||||||
|
if (!breakpadRef_) {
|
||||||
|
callback(nil, -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int delay = [self sendDelay];
|
||||||
|
if (delay != 0) {
|
||||||
|
callback(nil, delay);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[self reportWillBeSent];
|
||||||
|
callback(BreakpadGetNextReportConfiguration(breakpadRef_), 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark -
|
#pragma mark -
|
||||||
|
|
||||||
- (int)sendDelay {
|
- (int)sendDelay {
|
||||||
|
@ -67,6 +67,10 @@ extern NSString *const kDefaultServerType;
|
|||||||
|
|
||||||
- (id)initWithConfig:(NSDictionary *)config;
|
- (id)initWithConfig:(NSDictionary *)config;
|
||||||
|
|
||||||
|
// Reads the file |configFile| and returns the corresponding NSDictionary.
|
||||||
|
// |configFile| will be deleted after reading.
|
||||||
|
+ (NSDictionary *)readConfigurationDataFromFile:(NSString *)configFile;
|
||||||
|
|
||||||
- (NSMutableDictionary *)parameters;
|
- (NSMutableDictionary *)parameters;
|
||||||
|
|
||||||
- (void)report;
|
- (void)report;
|
||||||
@ -78,4 +82,8 @@ extern NSString *const kDefaultServerType;
|
|||||||
// will be uploaded to the crash server.
|
// will be uploaded to the crash server.
|
||||||
- (void)addServerParameter:(id)value forKey:(NSString *)key;
|
- (void)addServerParameter:(id)value forKey:(NSString *)key;
|
||||||
|
|
||||||
|
// This method process the HTTP response and renames the minidump file with the
|
||||||
|
// new ID.
|
||||||
|
- (void)handleNetworkResponse:(NSData *)data withError:(NSError *)error;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
@ -204,6 +204,11 @@ NSDictionary *readConfigurationData(const char *configFile) {
|
|||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
+ (NSDictionary *)readConfigurationDataFromFile:(NSString *)configFile {
|
||||||
|
return readConfigurationData([configFile fileSystemRepresentation]);
|
||||||
|
}
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
- (void)translateConfigurationData:(NSDictionary *)config {
|
- (void)translateConfigurationData:(NSDictionary *)config {
|
||||||
parameters_ = [[NSMutableDictionary alloc] init];
|
parameters_ = [[NSMutableDictionary alloc] init];
|
||||||
@ -486,6 +491,46 @@ NSDictionary *readConfigurationData(const char *configFile) {
|
|||||||
[extraServerVars_ setObject:value forKey:key];
|
[extraServerVars_ setObject:value forKey:key];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
- (void)handleNetworkResponse:(NSData *)data withError:(NSError *)error {
|
||||||
|
NSString *result = [[NSString alloc] initWithData:data
|
||||||
|
encoding:NSUTF8StringEncoding];
|
||||||
|
const char *reportID = "ERR";
|
||||||
|
if (error) {
|
||||||
|
fprintf(stderr, "Breakpad Uploader: Send Error: %s\n",
|
||||||
|
[[error description] UTF8String]);
|
||||||
|
} else {
|
||||||
|
NSCharacterSet *trimSet =
|
||||||
|
[NSCharacterSet whitespaceAndNewlineCharacterSet];
|
||||||
|
reportID = [[result stringByTrimmingCharactersInSet:trimSet] UTF8String];
|
||||||
|
[self logUploadWithID:reportID];
|
||||||
|
}
|
||||||
|
|
||||||
|
// rename the minidump file according to the id returned from the server
|
||||||
|
NSString *minidumpDir =
|
||||||
|
[parameters_ objectForKey:@kReporterMinidumpDirectoryKey];
|
||||||
|
NSString *minidumpID = [parameters_ objectForKey:@kReporterMinidumpIDKey];
|
||||||
|
|
||||||
|
NSString *srcString = [NSString stringWithFormat:@"%@/%@.dmp",
|
||||||
|
minidumpDir, minidumpID];
|
||||||
|
NSString *destString = [NSString stringWithFormat:@"%@/%s.dmp",
|
||||||
|
minidumpDir, reportID];
|
||||||
|
|
||||||
|
const char *src = [srcString fileSystemRepresentation];
|
||||||
|
const char *dest = [destString fileSystemRepresentation];
|
||||||
|
|
||||||
|
if (rename(src, dest) == 0) {
|
||||||
|
GTMLoggerInfo(@"Breakpad Uploader: Renamed %s to %s after successful " \
|
||||||
|
"upload",src, dest);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// can't rename - don't worry - it's not important for users
|
||||||
|
GTMLoggerDebug(@"Breakpad Uploader: successful upload report ID = %s\n",
|
||||||
|
reportID );
|
||||||
|
}
|
||||||
|
[result release];
|
||||||
|
}
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
- (void)report {
|
- (void)report {
|
||||||
NSURL *url = [NSURL URLWithString:[parameters_ objectForKey:@BREAKPAD_URL]];
|
NSURL *url = [NSURL URLWithString:[parameters_ objectForKey:@BREAKPAD_URL]];
|
||||||
@ -511,43 +556,16 @@ NSDictionary *readConfigurationData(const char *configFile) {
|
|||||||
// Send it
|
// Send it
|
||||||
NSError *error = nil;
|
NSError *error = nil;
|
||||||
NSData *data = [upload send:&error];
|
NSData *data = [upload send:&error];
|
||||||
NSString *result = [[NSString alloc] initWithData:data
|
|
||||||
encoding:NSUTF8StringEncoding];
|
|
||||||
const char *reportID = "ERR";
|
|
||||||
|
|
||||||
if (error) {
|
if (![url isFileURL]) {
|
||||||
fprintf(stderr, "Breakpad Uploader: Send Error: %s\n",
|
[self handleNetworkResponse:data withError:error];
|
||||||
[[error description] UTF8String]);
|
|
||||||
} else {
|
} else {
|
||||||
NSCharacterSet *trimSet =
|
if (error) {
|
||||||
[NSCharacterSet whitespaceAndNewlineCharacterSet];
|
fprintf(stderr, "Breakpad Uploader: Error writing request file: %s\n",
|
||||||
reportID = [[result stringByTrimmingCharactersInSet:trimSet] UTF8String];
|
[[error description] UTF8String]);
|
||||||
[self logUploadWithID:reportID];
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// rename the minidump file according to the id returned from the server
|
|
||||||
NSString *minidumpDir =
|
|
||||||
[parameters_ objectForKey:@kReporterMinidumpDirectoryKey];
|
|
||||||
NSString *minidumpID = [parameters_ objectForKey:@kReporterMinidumpIDKey];
|
|
||||||
|
|
||||||
NSString *srcString = [NSString stringWithFormat:@"%@/%@.dmp",
|
|
||||||
minidumpDir, minidumpID];
|
|
||||||
NSString *destString = [NSString stringWithFormat:@"%@/%s.dmp",
|
|
||||||
minidumpDir, reportID];
|
|
||||||
|
|
||||||
const char *src = [srcString fileSystemRepresentation];
|
|
||||||
const char *dest = [destString fileSystemRepresentation];
|
|
||||||
|
|
||||||
if (rename(src, dest) == 0) {
|
|
||||||
GTMLoggerInfo(@"Breakpad Uploader: Renamed %s to %s after successful " \
|
|
||||||
"upload",src, dest);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// can't rename - don't worry - it's not important for users
|
|
||||||
GTMLoggerDebug(@"Breakpad Uploader: successful upload report ID = %s\n",
|
|
||||||
reportID );
|
|
||||||
}
|
|
||||||
[result release];
|
|
||||||
} else {
|
} else {
|
||||||
// Minidump is missing -- upload just the log file.
|
// Minidump is missing -- upload just the log file.
|
||||||
if (logFileData_) {
|
if (logFileData_) {
|
||||||
|
@ -143,7 +143,7 @@
|
|||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
- (NSData *)send:(NSError **)error {
|
- (NSData *)send:(NSError **)error {
|
||||||
NSMutableURLRequest *req =
|
NSMutableURLRequest *req =
|
||||||
[[NSMutableURLRequest alloc]
|
[[NSMutableURLRequest alloc]
|
||||||
initWithURL:url_ cachePolicy:NSURLRequestUseProtocolCachePolicy
|
initWithURL:url_ cachePolicy:NSURLRequestUseProtocolCachePolicy
|
||||||
timeoutInterval:10.0 ];
|
timeoutInterval:10.0 ];
|
||||||
@ -190,12 +190,16 @@
|
|||||||
|
|
||||||
[response_ release];
|
[response_ release];
|
||||||
response_ = nil;
|
response_ = nil;
|
||||||
|
|
||||||
NSData *data = [NSURLConnection sendSynchronousRequest:req
|
|
||||||
returningResponse:&response_
|
|
||||||
error:error];
|
|
||||||
|
|
||||||
[response_ retain];
|
NSData *data;
|
||||||
|
if ([[req URL] isFileURL]) {
|
||||||
|
[[req HTTPBody] writeToURL:[req URL] options:0 error:error];
|
||||||
|
} else {
|
||||||
|
data = [NSURLConnection sendSynchronousRequest:req
|
||||||
|
returningResponse:&response_
|
||||||
|
error:error];
|
||||||
|
[response_ retain];
|
||||||
|
}
|
||||||
[req release];
|
[req release];
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
Loading…
Reference in New Issue
Block a user