Sign in Join
Wiki copytemporary

macOS NSURLSession: what changed

Revision 1 to 2. Removed lines have a minus sign; added lines have a plus sign.

OldNewLine
11## Overview
22
33The NSURLSession class and related classes provide an API for downloading content via HTTP and HTTPS. The downloading is performed asynchronously, so your application can remain responsive and handle incoming data or errors as they arrive. This class replaces the [[macOS NSURLConnection]] class which was deprecated by Apple in macOS 10.11 (El Capitan) in 2015. NSURLSession is available from macOS 10.9 (Mavericks) onwards.
44
55The NSURLSession API is highly asynchronous. If you use the default, system-provided delegate, you must provide a completion handler block that returns data to your application when a transfer finishes successfully or with an error. Alternatively, if you provide your own custom delegate objects, the task objects call those delegates’ methods with data as it is received from the server (or, for file downloads, when the transfer is complete). The NSURLSession API provides status and progress properties, in addition to delivering this information to delegates. It supports cancelling, resuming, and suspending tasks, and it provides the ability to resume suspended, cancelled, or failed downloads where they left off.
66
77The behaviour of the tasks in a session depends on three things:
88
991. the type of session (determined by the type of configuration object used to create it),
10102. the type of task, and
11113. whether the application was in the foreground when the task was created.
1212
1313### Types of sessions
1414
1515The NSURLSession API supports three types of sessions, as determined by the type of configuration object used to create the session:
1616
17171. Default sessions behave similarly to other Foundation methods for downloading URLs. They use a persistent disk-based cache and store credentials in the user’s keychain. A default session configuration object results in a session that behaves like an NSURLConnection object in its standard configuration. Altering a session configuration obtained through the defaultSessionConfiguration method does not change the default session configuration of which it is a copy.
18182. Ephemeral sessions do not store any data to disk; all caches, credential stores, and so on are kept in RAM and tied to the session. Thus, when your application invalidates the session, they are purged automatically. Ephemeral sessions are therefore ideal if you need to implement private browsing.
19193. Background sessions are similar to default sessions, except that a separate process handles all data transfers. Background sessions have some additional limitations:
2020 1. The session must provide a delegate for uploads and downloads.
2121 2. Only HTTP and HTTPS protocols are supported.
2222 3. Only upload and download tasks are supported (no data tasks).
2323 4. Redirects are always followed.
2424 5. If the background transfer is initiated while the app is in the background, the configuration object’s discretionary property is treated as being true.
2525 6. Background configurations cannot be reused (ie you cannot use two background session configurations using the same configuration identifier).
2626
2727### Types of Tasks
2828
2929An NSURLSession supports four types of tasks:
3030
3131- data tasks,
3232- download tasks,
3333- upload tasks, and
3434- webSocket tasks.
3535
3636Data tasks send and receive data using NSData objects. Data tasks are intended for short, often interactive requests to a server. Data tasks can return data to your application one piece at a time after each piece of data is received, or all at once through a completion handler. As data tasks do not store the data to a file, they are not supported in background sessions.
3737
3838Download tasks retrieve data in the form of a file, and support background downloads.
3939
4040Upload tasks send data (usually in the form of a file), and support background uploads.
4141
4242Web socket tasks exchange messages over TCP and TLS, using the WebSocket protocol defined in [RFC 6455](https://tools.ietf.org/html/rfc6455).
4343
4444### Asynchronicity
4545
4646The NSURLSession API is highly asynchronous. It returns data to your application in one of two ways, depending on the methods you call:
4747
4848- by calling a completion handler block when a transfer finishes successfully or with an error; or
4949- by calling methods on the session’s delegate as data arrives and when the transfer is complete.
5050
5151In addition to delivering this information to delegates, the NSURLSession provides status and progress properties that you can query if you need to make programmatic decisions based on the current state of the task (with the caveat that its state can change at any time).
5252
5353### Protocol support
5454
5555An NSURLSession natively supports the data, file, ftp, http, and https URL schemes, with transparent support for proxy servers and SOCKS gateways, as configured in the user’s system preferences. It supports HTTP/1.1 and HTTP/2 protocols. HTTP/2, described in [RFC 7540](https://tools.ietf.org/html/rfc7540), requires a server that supports Application-Layer Protocol Negotiation (ALPN). macOS 10.11 (El Capitan) and later use App Transport Security (ATS) for all HTTP connections made with NSURLSession. ATS requires that HTTP connections use HTTPS ([RFC 2818](https://tools.ietf.org/html/rfc2818)).
5656
5757### Creating a session
5858
5959The NSURLSession API provides a wide range of configuration options:
6060
6161- Private storage support for caches, cookies, credentials, and protocols in a way that is specific to a single session.
6262- Authentication, tied to a specific request (task) or group of requests (session).
6363- File uploads and downloads by URL, which encourages separation of the data (the file’s contents) from the metadata (the URL and settings).
6464- Configuration of the maximum number of connections per host.
6565- Per-resource timeouts that are triggered if an entire resource cannot be downloaded in a certain amount of time.
6666- Minimum and maximum TLS version support Custom proxy dictionaries.
6767- Control over cookie policies.
6868- Control over HTTP pipelining behaviour.
6969
7070When you instantiate a session object, you specify the following:
7171
7272- A configuration object that governs the behaviour of that session and the tasks within it
7373- Optionally, a delegate object to process incoming data as it is received and handle other events specific to the session and the tasks within it, such as server authentication, determining whether a resource load request should be converted into a download, and so on.
7474- If you do not provide a delegate, the NSURLSession object uses a system-provided delegate. In this way, you can readily use NSURLSession in place of existing code that uses the *sendAsynchronousRequest:queue:completionHandler:* convenience method on NSURLSession.
7575
7676As these settings are contained in a separate configuration object, you can reuse commonly used settings in a configuration object in other sessions provided they are not background sessions. When you create a session, the session copies the configuration object and, as a result, modifications of the configuration object only affect new sessions and not existing sessions. However, you cannot reuse background session configuration objects because the behaviour of two background session objects sharing the same identifier is undefined.
7777
7878### Caching policies
7979
8080Network caching reduces the number of requests that need to be made to the server, and improve the experience of using an application offline or under slow network conditions. An NSURLRequest instance specifies how the local cache is used by setting the cache policy to one of the **NSURLRequestCachePolicy** values. The default cache policy for an NSURLRequest instance is **NSURLRequestUseProtocolCachePolicy** which is protocol specific and is defined as being the best conforming policy for the protocol.
8181
8282There are a number of different NSURLRequest caching policies from which to choose:
8383
8484- **NSURLRequestUseProtocolCachePolicy** Use the caching logic defined in the protocol implementation, if any, for a particular URL load request.
8585- **NSURLRequestReloadIgnoringLocalCacheData** The URL load should be loaded only from the originating source.
8686- **NSURLRequestReloadIgnoringLocalAndRemoteCacheData** Ignore local cache data, and instruct proxies and other intermediates to disregard their caches so far as the protocol allows.
8787- **NSURLRequestReturnCacheDataElseLoad** Use existing cache data, regardless or age or expiration date, loading from originating source only if there is no cached data.
8888- **NSURLRequestReturnCacheDataDontLoad** Use existing cache data, regardless or age or expiration date, and fail if no cached data is available.
8989- **NSURLRequestReloadRevalidatingCacheData** Use cache data if the origin source can validate it; otherwise, load from the origin.
9090
9191The default cache policy is **NSURLRequestUseProtocolCachePolicy** and is almost always the best choice rather than implementing your own caching or changing the cache policy of the request. When a server responds to an HTTP request, the response includes information about how long and under what conditions the response will be valid. However, for debugging I always prefer **NSURLRequestReloadIgnoringLocalCacheData** ro ensure the data isn not coming from the local cache and that a request is actually made whether it is fulfilled by the origin server or an intermediate proxy.
9292
9393## Retrieving data using system-provided delegates
9494
9595The NSURLSession object does not need to have a delegate. If no delegate is assigned, a system-provided delegate is used, and you must provide a completion handler callback to obtain the data.
9696
9797This simple demonstration shows how to use NSURLSession and related classes to download and display a web page using NSURLSession as a drop-in replacement for the [[macOS NSURLConnection|NSURLConnection sendAsynchronousRequest:queue:completionHandler:]] method. Using this method, you need to provide only two pieces of code in your application:
9898
99991. code to create a configuration object and a session based on that object; and
1001002. code to implement a completion handler routine to do something with the data after it has been fully received.
101101
102102### Example code
103103
104104``` pascal
105105unit Unit1;
106106
107107{$mode objfpc}{$H+}
108108{$modeswitch objectivec1}
109109{$modeswitch cblocks}
110110
111111interface
112112
113113uses
114114 Forms, // for the main form
115115 Dialogs, // for ShowMessage
116116 StdCtrls, // for the button
117117 SysUtils, // for IntToStr()
118118 CocoaAll, // for NSData and other Cocoa types
119119 CocoaUtils; // for NSStringString
120120
121121type
122122 // setup cBlock for completion handler
123123 tblock = reference to procedure(data: NSData; response: NSURLResponse; connectionError: NSError); cdecl; cblock;
124124
125125 // redefine version from packages/cocoaint/src/foundation/NSURLSession.inc
126126 NSURLSession = objcclass external (NSObject)
127127 public
128128 class function sessionWithConfiguration(configuration: NSURLSessionConfiguration): NSURLSession; message 'sessionWithConfiguration:';
129129 end;
130130
131131 NSURLSessionAsynchronousConvenience = objccategory external (NSURLSession)
132132 function dataTaskWithURL_completionHandler(url: NSURL; completionHandler: tBlock): NSURLSessionDataTask; message 'dataTaskWithURL:completionHandler:';
133133 end;
134134
135135 { TForm1 }
136136
137137 TForm1 = class(TForm)
138138 Button1: TButton;
139139 procedure Button1Click(Sender: TObject);
140140 private
141141
142142 public
143143
144144 end;
145145
146146var
147147 Form1: TForm1;
148148 myCache: NSURLcache;
149149 webData: String;
150150 webResponse: String;
151151 webHTML: String;
152152 webStatusCode: Integer;
153153 webError: String;
154154 webErrorReason: String;
155155 didFinish: PRTLEvent;
156156
157157implementation
158158
159159{$R *.lfm}
160160
161161{ TForm1 }
162162
163163{$DEFINE DEBUG}
164164
165165// Completion handler: Executed after URL has been retrieved or retrieval fails
166166procedure myCompletionHandler(data: NSData; response: NSURLResponse; connectionError: NSError);
167167var
168168 httpResponse: NSHTTPURLResponse;
169169begin
170170 {$IFDEF DEBUG}
171171 NSLog(NSStr('Completion handler called'));
172172
173173 if (NSThread.currentThread.isMainThread) then
174174 NSLog(NSStr('In main thread--completion handler'))
175175 else
176176 NSLog(NSStr('Not in main thread--completion handler'));
177177 {$ENDIF}
178178
179179 // if no error
180180 if((data.Length > 0) and (connectionError = Nil)) then
181181 begin
182182 {$IFDEF DEBUG}
183183 NSLog(NSStr('Data desciption: %@'),data.description);
184184 NSLog(NSStr('Response description: %@'),response.description);
185185 NSLog(NSStr('Data: %@'),NSString.alloc.initWithData_encoding(data,NSASCIIStringEncoding).autorelease);
186186 {$ENDIF}
187187 webData := NSStringToString(data.description);
188188 {$IFDEF DEBUG}
189189 NSLog(NSStr('Web data' + webData));
190190 {$ENDIF}
191191 // The NSHTTPURLResponse class is a subclass of NSURLResponse
192192 // so we can cast an NSURLResponse as an NSHTTPURLResponse
193193 httpResponse := NSHTTPURLResponse(response);
194194 {$IFDEF DEBUG}
195195 // Extract status code from response header
196196 NSLog(NSStr('HTTP status code: %@'),NSStr(IntToStr(httpResponse.statusCode)));
197197 {$ENDIF}
198198 webStatusCode := httpResponse.statusCode;
199199 webResponse := NSStringToString(response.description);
200200 webHTML := NSSTringToString(NSString.alloc.initWithData_encoding(data,NSASCIIStringEncoding));
201201 end
202202 // o/w return error
203203 else
204204 begin
205205 {$IFDEF DEBUG}
206206 NSLog(NSStr('Error %@'), connectionError.userInfo);
207207 {$ENDIF}
208208 webError := 'Error description: ' + LineEnding + NSStringToString(connectionError.description);
209209 webErrorReason := 'Error retrieving: ' + NSStringToString(connectionError.userInfo.valueForKey(NSErrorFailingUrlStringKey))
210210 + LineEnding + LineEnding + 'Reason: ' + NSStringToString(connectionError.localizedDescription);
211211 end;
212212
213213 // notify main thread that completion handler thread has finished
214214 RTLEventSetEvent(didFinish);
215215
216216 {$IFDEF DEBUG}
217217 NSLog(NSStr('leaving handler'));
218218 {$ENDIF}
219219end;
220220
221221procedure TForm1.Button1Click(Sender: TObject);
222222var
223223 urlSessionConfig: NSURLSessionConfiguration = Nil;
224224 cachePath: NSString = Nil;
225225 urlSession: NSURLSession = Nil;
226226 URL: NSURL = Nil;
227227begin
228228 // create event to synchronise completion handler background thread
229229 // with the GUI main thread
230230 didFinish := RTLEventCreate;
231231
232232 // create default url session config
233233 urlSessionConfig := NSURLSessionConfiguration.defaultSessionConfiguration;
234234
235235 // configure caching behavior for the default session
236236 cachePath := NSTemporaryDirectory.stringByAppendingPathComponent(NSStr('/nsurlsessiondemo.cache'));
237237
238238 {$IFDEF DEBUG}
239239 NSLog(NSStr('Cache path: %@'), cachePath);
240240 {$ENDIF}
241241
242242 myCache := NSURLCache.alloc.initWithMemoryCapacity_diskCapacity_diskPath(16384, 268435456, cachePath);
243243 urlSessionConfig.setURLCache(myCache);
244244
245245 // set cache policy
246246 {$IFDEF DEBUG}
247247 urlSessionConfig.setRequestCachePolicy(NSURLRequestReloadIgnoringLocalCacheData);
248248 {$ELSE}
249249 urlSessionConfig.setRequestCachePolicy(NSURLRequestUseProtocolCachePolicy);
250250 {$ENDIF}
251251
252252 // create a session for configuration
253253 urlSession := NSURLSession.sessionWithConfiguration(urlSessionConfig);
254254
255255 // create NSURL
256256 URL := NSURL.URLWithString(NSSTR(PAnsiChar('https://sentinel.sentry.org/')));
257257 if(Url = Nil) then
258258 ShowMessage('NSURL.URLWithString failed!');
259259
260260 // setup and execute data task
261261 urlSession.dataTaskWithURL_completionHandler(URL, @myCompletionHandler).resume;
262262
263263 // wait for completion handler to finish
264264 RTLEventWaitFor(didFinish);
265265
266266 // display results
267267 if(webErrorReason <> '') then
268268 begin
269269 ShowMessage(webError);
270270 ShowMessage(webErrorReason);
271271 end
272272 else
273273 begin
274274 ShowMessage('HTTP status code: ' + IntToStr(webStatusCode) + LineEnding
275275 + LineEnding + 'Raw data: ' + LineEnding + webData);
276276 ShowMessage('Response: ' + LineEnding + LineEnding + webResponse);
277277 ShowMessage('Web page: ' + LineEnding + LineEnding + webHTML);
278278 end;
279279
280280 // housekeeping
281281 RTLeventdestroy(didFinish);
282282 myCache.release;
283283end;
284284
285285end.
286286```
287287
288288Full project source code is available from [SourceForge](https://sourceforge.net/p/lazarus-wiki-projects/code/ci/master/tree/laz_nsurlsession/).
289289
290290##### Compilation note
291291
292292To compile the above code successfully you are going to have to add the missing *initWithData_encoding* function to the *NSString* class by editing `/usr/local/share/fpcsrc/fpc-[3.0.4|3.2.0|3.3.1]/packages/cocoaint/src/foundation/NSString.inc` to add the missing (highlighted) function as follows:
293293
294294``` pascal
295295--- NSString.inc (revision 45778)
296296+++ NSString.inc (working copy)
297297@@ -105,6 +105,7 @@
298298 function characterAtIndex (index: NSUInteger): unichar; message 'characterAtIndex:';
299299 function init: instancetype; message 'init'; { NS_DESIGNATED_INITIALIZER }
300300 function initWithCoder (aDecoder: NSCoder): instancetype; message 'initWithCoder:'; { NS_DESIGNATED_INITIALIZER }
301301+ function initWithData_encoding(data: NSData; encoding: NSStringEncoding) : instancetype; message 'initWithData:encoding:';
302302
303303 { Adopted protocols }
304304 function copyWithZone (zone: NSZonePtr): id; message 'copyWithZone:';
305305```
306306
307307and then recompile the FPC source. This has been tested with FPC 3.0.4, FPC 3.2.0 and FPC 3.3.1. Note that for FPC 3.0.4 you need to replace "instancetype" in the highlighted line with "id"
308308
309309I use the following script (for FPC 3.3.1; substitute your FPC version numbers as appropriate) to recompile FPC:
310310
311311``` sh
312312#!/bin/sh
313313cd /usr/local/share/fpcsrc/fpc-3.3.1/
314314make clean all FPC=/usr/local/lib/fpc/3.3.1/ppcx64 OS_TARGET=darwin CPU_TARGET=x86_64 OPT="-XR/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/"
315315make install FPC=$PWD/compiler/ppcx64 OS_TARGET=darwin CPU_TARGET=x86_64
316316```
317317
318318- Note 1: The highlighted line(s) above should be all on one line.
319319- Note 2: You will almost certainly have to use *sudo* to execute that script successfully or face permissions problems.
320320
321321## Retrieving data using a custom delegate
322322
323323When using a custom delegate to retrieve data, the delegate must implement at least the following methods:
324324
325325- URLSession:dataTask:didReceiveData: Tells the delegate that the data task has received some of the expected data.
326326- URLSession:task:didCompleteWithError: Tells the delegate that the data task finished transferring the data.
327327
328328If you don’t need the features provided by a delegate, you can use this API without providing one by passing nil when you create a session.
329329
330330### Example code
331331
332332``` pascal
333333unit Unit1;
334334
335335{$mode objfpc}{$H+}
336336{$modeswitch objectivec1}
337337
338338interface
339339
340340uses
341341 Forms, // for the main form
342342 Dialogs, // for ShowMessage
343343 StdCtrls, // for the button
344344 SysUtils, // for IntToStr()
345345 CocoaAll, // for NSData and other Cocoa types
346346 CocoaUtils, Classes; // for NSStringString
347347
348348type
349349 // redefine version from packages/cocoaint/src/foundation/NSURLSession.inc
350350 NSURLSession = objcclass external (NSObject)
351351 public
352352 class function sessionWithConfiguration(configuration: NSURLSessionConfiguration): NSURLSession;
353353 message 'sessionWithConfiguration:';
354354 class function sessionWithConfiguration_delegate_delegateQueue(configuration: NSURLSessionConfiguration; delegate: NSURLSessionDelegateProtocol; queue: NSOperationQueue): NSURLSession;
355355 message 'sessionWithConfiguration:delegate:delegateQueue:';
356356 function dataTaskWithURL(url: NSURL): NSURLSessionDataTask;
357357 message 'dataTaskWithURL:';
358358 end;
359359
360360 { TMyDelegate }
361361 TMyDelegate = objcclass(NSObject, NSURLSessionDelegateProtocol)
362362 private
363363
364364 public
365365 procedure URLSession_dataTask_didReceiveData (session: NSURLSession; dataTask: NSURLSessionDataTask; data: NSData);
366366 message 'URLSession:dataTask:didReceiveData:';
367367 procedure URLSession_task_didCompleteWithError (session: NSURLSession; task: NSURLSessionTask; error: NSError);
368368 message 'URLSession:task:didCompleteWithError:';
369369 end;
370370
371371 { TForm1 }
372372
373373 TForm1 = class(TForm)
374374 Button1: TButton;
375375 procedure Button1Click(Sender: TObject);
376376 private
377377
378378 public
379379
380380 end;
381381
382382var
383383 Form1: TForm1;
384384 myCache: NSURLcache;
385385 responseData: NSMutableData;
386386 queue: NSOperationQueue;
387387 sessionDelegate: TMyDelegate;
388388 webData: NSString;
389389 webError: String;
390390 ThreadDidFinish: PRTLEvent;
391391
392392implementation
393393
394394{$R *.lfm}
395395
396396{ TForm1 }
397397
398398{$DEFINE DEBUG}
399399
400400procedure TMyDelegate.URLSession_dataTask_didReceiveData(session: NSURLSession; dataTask: NSURLSessionDataTask; data: NSData);
401401begin
402402 {$IFDEF DEBUG}
403403 NSLog(NSStr('didReceiveData %i'), data.length);
404404 NSLog(NSStr('didReceiveData %@'), NSString.alloc.initWithData_encoding(data,NSASCIIStringEncoding).autorelease);
405405 {$ENDIF}
406406
407407 // Append the new data to the response variable
408408 responseData.appendData(data);
409409end;
410410
411411procedure TMyDelegate.URLSession_task_didCompleteWithError(session: NSURLSession; task: NSURLSessionTask; error: NSError);
412412begin
413413 {$IFDEF DEBUG}
414414 NSLog(NSStr('didCompleteWithError start'));
415415 NSLog(NSStr('responseData.length %i'), responseData.length);
416416 NSLog(NSStr('responseData %@'), NSString.alloc.initWithData_encoding(responseData,NSASCIIStringEncoding).autorelease);
417417 {$ENDIF}
418418
419419 // Is there an error?
420420 if(responseData.length = 0) then
421421 begin
422422 NSLog(NSStr('Error %@'), error.description);
423423 webError := 'Error retrieving: ' + NSStringToString(error.userInfo.valueForKey(NSErrorFailingUrlStringKey))
424424 + LineEnding + LineEnding + 'Reason: ' + NSStringToString(error.localizedDescription);
425425 end
426426 else
427427 begin
428428 webError := '';
429429 webData := NSString.alloc.initWithData_encoding(responseData,NSASCIIStringEncoding);
430430 end;
431431
432432 // notify main thread that data retrieval thread has finished
433433 RTLEventSetEvent(ThreadDidFinish);
434434
435435 {$IFDEF DEBUG}
436436 NSLog(NSStr('didCompleteWithError finish'));
437437 {$ENDIF}
438438end;
439439
440440procedure TForm1.Button1Click(Sender: TObject);
441441var
442442 urlSessionConfig: NSURLSessionConfiguration = Nil;
443443 cachePath: NSString = Nil;
444444 urlSession: NSURLSession = Nil;
445445 URL: NSURL = Nil;
446446begin
447447 // create event to synchronise data retrieval background thread
448448 // with the GUI main thread
449449 ThreadDidFinish := RTLEventCreate;
450450
451451 // create default url session config
452452 urlSessionConfig := NSURLSessionConfiguration.defaultSessionConfiguration;
453453
454454 // create connection delegate
455455 sessionDelegate := TMyDelegate.alloc.init;
456456
457457 // create responseData
458458 responseData := NSMutableData.alloc.init;
459459
460460 // configure caching behavior for the default session
461461 cachePath := NSTemporaryDirectory.stringByAppendingPathComponent(NSStr('/nsurlsessiondemo.cache'));
462462
463463 {$IFDEF DEBUG}
464464 NSLog(NSStr('Cache path: %@'), cachePath);
465465 {$ENDIF}
466466
467467 myCache := NSURLCache.alloc.initWithMemoryCapacity_diskCapacity_diskPath(16384, 268435456, cachePath);
468468 urlSessionConfig.setURLCache(myCache);
469469
470470 // set cache policy
471471 {$IFDEF DEBUG}
472472 urlSessionConfig.setRequestCachePolicy(NSURLRequestReloadIgnoringLocalCacheData);
473473 {$ELSE}
474474 urlSessionConfig.setRequestCachePolicy(NSURLRequestUseProtocolCachePolicy);
475475 {$ENDIF}
476476
477477 // create a session for configuration
478478 urlSession := NSURLSession.sessionWithConfiguration_delegate_delegateQueue(urlSessionConfig, sessionDelegate, queue);
479479
480480 // create NSURL
481481 URL := NSURL.URLWithString(NSSTR(PAnsiChar('https://sentinel.sentry.org/')));
482482 if(Url = Nil) then
483483 ShowMessage('NSURL.URLWithString failed!');
484484
485485 // setup and execute data task
486486 urlSession.dataTaskWithURL(URL).resume;
487487
488488 // wait for data retrieval thread to finish
489489 RTLEventWaitFor(ThreadDidFinish);
490490
491491 // Show result
492492 if(webError <> '') then
493493 ShowMessage(webError)
494494 else
495495 ShowMessage(NSStringToString(webData));
496496
497497 // housekeeping
498498 RTLeventdestroy(ThreadDidFinish);
499499 sessionDelegate.release;
500500 responseData.release;
501501 myCache.release;
502502 webData.release;
503503end;
504504
505505end.
506506```
507507
508508Full project source code is available from [SourceForge](https://sourceforge.net/p/lazarus-wiki-projects/code/ci/master/tree/laz_nsurlsession2/).
509509
510510##### Compilation note
511511
512512See the previous Compilation note above.
513513
514514## Downloading files
515515
516516At a high level, downloading a file is similar to retrieving data. Your application should implement the following delegate methods:
517517
518518- **URLSession:downloadTask:didFinishDownloadingToURL**: provides your application with the URL to a temporary file where the downloaded content is stored.
519519
520520- **URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite**: provides your application with status information about the progress of the download.
521521
522522- **URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes**: tells your application that its attempt to resume a previously failed download was successful.
523523
524524- **URLSession:task:didCompleteWithError**: tells your application that the download failed.
525525
526526If you schedule the download in a background session, the download continues when your application is not running. If you schedule the download in a default or ephemeral session, the download must begin anew when your application is relaunched.
527527
528528While the example below only implements a few of the delegate methods, it should be simple enough for anyone to understand how to add the missing ones. The example application downloads a file to the user's Downloads folder. If the downloaded file already exists in the Downloads folder, it is removed and the just downloaded file will take its place.
529529
530530### Example code
531531
532532``` pascal
533533unit Unit1;
534534
535535{$mode objfpc}{$H+}
536536{$modeswitch objectivec1}
537537
538538interface
539539
540540uses
541541 Forms, // for the main form
542542 Dialogs, // for ShowMessage
543543 StdCtrls, // for the button
544544 SysUtils, // for IntToStr()
545545 CocoaAll, // for NSData and other Cocoa types
546546 CTypes; // for cInt64
547547
548548type
549549 { TMyDlDelegate }
550550 TMyDlDelegate = objcclass(NSObject, NSURLSessionDelegateProtocol)
551551 private
552552
553553 public
554554 procedure URLSession_downloadtask_didFinishDownloadingToURL(session: NSURLSession; downloadTask: NSURLSessionDownloadTask;
555555 location: NSURL);
556556 message 'URLSession:downloadTask:didFinishDownloadingToURL:';
557557 procedure URLSession_downloadTask_didWriteData_totalBytesWritten_totalBytesExpectedToWrite (session: NSURLSession;
558558 downloadTask: NSURLSessionDownloadTask; bytesWritten: cint64; totalBytesWritten: cint64; totalBytesExpectedToWrite: cint64);
559559 message 'URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:';
560560 {procedure URLSession_downloadTask_didResumeAtOffset_expectedTotalBytes (session: NSURLSession;
561561 downloadTask: NSURLSessionDownloadTask; fileOffset: cint64; expectedTotalBytes: cint64);
562562 message 'URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:';}
563563 end;
564564
565565 { TForm1 }
566566
567567 TForm1 = class(TForm)
568568 Button1: TButton;
569569 procedure Button1Click(Sender: TObject);
570570 private
571571
572572 public
573573
574574 end;
575575
576576var
577577 Form1: TForm1;
578578
579579implementation
580580
581581{$R *.lfm}
582582
583583{ TForm1 }
584584
585585{$DEFINE DEBUG}
586586
587587//
588588// Delegate methods
589589//
590590procedure TMyDlDelegate.URLSession_downloadtask_didFinishDownloadingToURL(session: NSURLSession;
591591 downloadTask: NSURLSessionDownloadTask; location: NSURL);
592592var
593593 downloadsPath: NSString;
594594 downloadsDirectoryURL: NSURL;
595595 downloadURL: NSURL;
596596 error: NSError;
597597 filePath: NSString;
598598begin
599599 downloadsPath := NSSearchPathForDirectoriesInDomains(NSDownloadsDirectory, NSUserDomainMask, True).firstObject;
600600 downloadsDirectoryURL := NSURL.fileURLWithPath(downloadsPath);
601601 downloadURL := downloadsDirectoryURL.URLByAppendingPathComponent(downloadTask.response.suggestedFilename);
602602 filePath := downloadsPath.stringByAppendingPathComponent(downloadTask.response.suggestedFilename);
603603
604604 {$IFDEF DEBUG}
605605 NSLog(NSStr('Session %@ Download task %@ Finished downloading to URL %@'), session, downloadTask, location);
606606 NSLog(NSStr('-------------------------------------------'));
607607 NSLog(NSStr('downloads path: %@'), downloadsPath);
608608 NSLog(NSStr('downloads directory URL: %@'), downloadsDirectoryURL);
609609 NSLog(NSStr('download URL: %@'), downloadURL);
610610 NSLog(NSStr('file path: %@'), filePath);
611611 NSLog(NSStr('-------------------------------------------'));
612612 {$ENDIF}
613613
614614 // remove any old file from the destination directory
615615 if(NSFileManager.defaultManager.fileExistsAtPath(filePath)) then
616616 begin
617617 NSLog(NSStr('Oh no, file already exists in downloads directory! Will remove for you :-)'));
618618 if (NSFileManager.defaultManager.removeItemAtPath_error(filePath, @error) <> True) then
619619 NSLog(NSStr('Oh no, removal of %@ failed, giving up :-('), filePath);
620620 end;
621621
622622 // file move error
623623 if(NSFileManager.defaultManager.moveItemAtURL_toURL_error(location,downloadURL, @Error) <> True) then
624624 NSLog(NSStr('Download error %@'), Error.localizedDescription);
625625end;
626626
627627procedure TMyDlDelegate.URLSession_downloadTask_didWriteData_totalBytesWritten_totalBytesExpectedToWrite(session: NSURLSession;
628628 downloadTask: NSURLSessionDownloadTask; bytesWritten: cint64; totalBytesWritten: cint64; totalBytesExpectedToWrite: cint64);
629629begin
630630 NSLog(NSStr('Session %@ download task %@ wrote an additional %lld bytes (total %lld bytes) out of an expected %lld bytes.'),
631631 session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite);
632632end;
633633
634634//
635635// Download button handler
636636//
637637procedure TForm1.Button1Click(Sender: TObject);
638638var
639639 dlReq: NSURL = Nil;
640640 dlSessionConfig: NSURLSessionConfiguration = Nil;
641641 dlSession: NSURLSession = Nil;
642642 dlTask: NSURLSessionDownloadTask = Nil;
643643 dlcache: NSURLcache;
644644 dlcachePath: NSString = Nil;
645645 dlQueue: NSOperationQueue = Nil;
646646 dlDelegate: TMyDlDelegate = Nil;
647647begin
648648 // create request
649649 dlReq := NSURL.URLWithString(NSStr('https://www.sentry.org/microlab/ASM_19930531.ZIP'));
650650 if(dlReq = Nil) then
651651 begin
652652 ShowMessage('NSURL.URLWithString failed! Aborting...');
653653 Exit;
654654 end;
655655
656656 // create download delegate
657657 dlDelegate := TMyDlDelegate.alloc.init;
658658
659659 // create background session config
660660 dlSessionConfig := NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(NSStr('dtask01'));
661661
662662 // create a session with background session configuration
663663 dlSession := NSURLSession.sessionWithConfiguration_delegate_delegateQueue(dlSessionConfig, dlDelegate, dlQueue);
664664
665665 // configure caching behavior for the session configuration
666666 dlCachePath := NSTemporaryDirectory.stringByAppendingPathComponent(NSStr('/nsurlsessiondemo.cache'));
667667 dlCache := NSURLCache.alloc.initWithMemoryCapacity_diskCapacity_diskPath(16384, 268435456, dlCachePath);
668668 dlSessionConfig.setURLCache(dlCache);
669669
670670 // log cache path
671671 {$IFDEF DEBUG}
672672 NSLog(NSStr('Cache path: %@'), dlCachePath);
673673 {$ENDIF}
674674
675675 // set cache policy
676676 {$IFDEF DEBUG}
677677 dlSessionConfig.setRequestCachePolicy(NSURLRequestReloadIgnoringLocalCacheData);
678678 {$ELSE}
679679 dlSessionConfig.setRequestCachePolicy(NSURLRequestUseProtocolCachePolicy);
680680 {$ENDIF}
681681
682682 // setup download task
683683 dlTask := dlSession.downloadTaskWithURL(dlReq);
684684
685685 // execute download task
686686 dlTask.resume;
687687
688688 // housekeeping
689689 dlDelegate.release;
690690 dlCache.release;
691691 dlSession.finishTasksAndInvalidate;
692692end;
693693
694694end.
695695```
696696
697697Full project source code is available from [SourceForge](https://sourceforge.net/p/lazarus-wiki-projects/code/ci/master/tree/laz_nsurlsession3/).
698698
699699## Downloading files with graphical user interface feedback
700700
701701You may have noticed that all the user feedback in the previous example has been relegated to NSLog entries. There is a reason for this. Any attempt to provide user feedback to the graphical user interface from within a delegate method running on a secondary or background thread will cause the application to die with a log entry similar to this:
702702
703-` *** Terminating app due to uncaught exception 'NSInternalInconsistencyException',`\
704-` reason: 'NSWindow drag regions should only be invalidated on the Main Thread!'`
703+```text
704+ *** Terminating app due to uncaught exception 'NSInternalInconsistencyException',
705+ reason: 'NSWindow drag regions should only be invalidated on the Main Thread!'
706+```
705707
706708The cause? Apple’s UI frameworks, and Cocoa in particular, can only be called from the main thread. This means that if you have a background threading process, it cannot interact with the user interface because all user interface objects are marked as not safe for threading. Touching the user interface from a background thread can cause all sorts of issues, including corruption of internal state, crashes, or inexplicable incorrect behaviour.
707709
708710The solution? *performSelectorOnMainThread:withObject:waitUntilDone:*. The selector you specify will be dispatched when the run loop of the main thread is in the default mode. When the run loop is in another mode, the selector will only be executed when the run loop is switched to the default mode. You can use *performSelectorOnMainThread:withObject:waitUntilDone:modes:* to specify the mode(s) if necessary.
709711
710712The example below implements *performSelectorOnMainThread:withObject:waitUntilDone:* in two places and uses an application-wide object (TdlStatsuObj) to update the content of a memo on the form with status information about the progress of the download.
711713
712714### Example code
713715
714716``` pascal
715717unit Unit1;
716718
717719{$mode objfpc}{$H+}
718720{$modeswitch objectivec1}
719721
720722interface
721723
722724uses
723725 Forms, // for the main form
724726 Dialogs, // for ShowMessage
725727 StdCtrls, // for the button
726728 SysUtils, // for IntToStr()
727729 CocoaAll, // for NSData and other Cocoa types
728730 CocoaUtils, // for NSTringToString
729731 CTypes; // for cInt64
730732
731733type
732734 { TDownloadlDelegate }
733735 TDownloadlDelegate = objcclass(NSObject, NSURLSessionDelegateProtocol)
734736 private
735737
736738 public
737739 procedure URLSession_downloadtask_didFinishDownloadingToURL(session: NSURLSession; downloadTask: NSURLSessionDownloadTask;
738740 location: NSURL);
739741 message 'URLSession:downloadTask:didFinishDownloadingToURL:';
740742 procedure URLSession_downloadTask_didWriteData_totalBytesWritten_totalBytesExpectedToWrite (session: NSURLSession;
741743 downloadTask: NSURLSessionDownloadTask; bytesWritten: cint64; totalBytesWritten: cint64; totalBytesExpectedToWrite: cint64);
742744 message 'URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:';
743745 procedure updateProgress;
744746 message 'updateProgess';
745747 procedure downloadFinished;
746748 message 'downloadFinished';
747749 end;
748750
749751 { TdlStatsuObj - application-wide object to pass to main thread for GUI status updates}
750752 TdlStatusObj = objcclass(NSObject)
751753 private
752754 _BytesRx: Int64;
753755 _TotalBytesRx: Int64;
754756 _ExpectedTotalBytes: Int64;
755757 _Location: NSString;
756758 public
757759 procedure setBytesRx(newValue: Int64);
758760 message 'setBytesRx:';
759761 function getBytesRx: Int64;
760762 message 'getBytesRx';
761763 procedure setTotalBytesRx(newValue: Int64);
762764 message 'setTotalBytesRx:';
763765 function getTotalBytesRx: Int64;
764766 message 'getTotalBytesRx';
765767 procedure setExpectedTotalBytes(newValue: Int64);
766768 message 'setExpectedTotalBytes:';
767769 function getExpectedTotalBytes: Int64;
768770 message 'getExpectedTotalBytes';
769771 procedure setLocation(newValue: NSString);
770772 message 'setLocation:';
771773 function getLocation: NSString;
772774 message 'getLocation';
773775 end;
774776
775777 { TForm1 }
776778
777779 TForm1 = class(TForm)
778780 Button1: TButton;
779781 Memo1: TMemo;
780782 procedure Button1Click(Sender: TObject);
781783 private
782784
783785 public
784786
785787 end;
786788
787789var
788790 Form1: TForm1;
789791 dlStatusObj: TdlStatusObj;
790792
791793implementation
792794
793795{$R *.lfm}
794796
795797{ TForm1 }
796798
797799//{$DEFINE DEBUG}
798800
799801//
800802// TdlStatusObj methods
801803//
802804procedure TdlStatusObj.setBytesRx(newValue: Int64);
803805begin
804806 _BytesRx := newValue;
805807end;
806808
807809function TdlStatusObj.getBytesRx: Int64;
808810begin
809811 getBytesRx := _BytesRx;
810812end;
811813
812814procedure TdlStatusObj.setTotalBytesRx(newValue: Int64);
813815begin
814816 _TotalBytesRx := newValue;
815817end;
816818
817819function TdlStatusObj.getTotalBytesRx: Int64;
818820begin
819821 getTotalBytesRx := _TotalBytesRx;
820822end;
821823
822824procedure TdlStatusObj.setExpectedTotalBytes(newValue: Int64);
823825begin
824826 _ExpectedTotalBytes := newValue;
825827end;
826828
827829function TdlStatusObj.getExpectedTotalBytes: Int64;
828830begin
829831 getExpectedTotalBytes := _ExpectedTotalBytes;
830832end;
831833
832834procedure TdlStatusObj.setLocation(newValue: NSString);
833835begin
834836 _Location := newValue;
835837end;
836838
837839function TdlStatusObj.getLocation: NSString;
838840begin
839841 getLocation := _Location;
840842end;
841843
842844//
843845// Delegate methods
844846//
845847procedure TDownloadlDelegate.URLSession_downloadtask_didFinishDownloadingToURL(session: NSURLSession;
846848 downloadTask: NSURLSessionDownloadTask; location: NSURL);
847849var
848850 downloadsPath: NSString;
849851 downloadsDirectoryURL: NSURL;
850852 downloadURL: NSURL;
851853 error: NSError;
852854 filePath: NSString;
853855 aSel: SEL;
854856begin
855857 downloadsPath := NSSearchPathForDirectoriesInDomains(NSDownloadsDirectory, NSUserDomainMask, True).firstObject;
856858 downloadsDirectoryURL := NSURL.fileURLWithPath(downloadsPath);
857859 downloadURL := downloadsDirectoryURL.URLByAppendingPathComponent(downloadTask.response.suggestedFilename);
858860 filePath := downloadsPath.stringByAppendingPathComponent(downloadTask.response.suggestedFilename);
859861
860862 {$IFDEF DEBUG}
861863 NSLog(NSStr('-------------------------------------------'));
862864 NSLog(NSStr('downloads path: %@'), downloadsPath);
863865 NSLog(NSStr('downloads directory URL: %@'), downloadsDirectoryURL);
864866 NSLog(NSStr('download URL: %@'), downloadURL);
865867 NSLog(NSStr('file path: %@'), filePath);
866868 NSLog(NSStr('-------------------------------------------'));
867869 NSLog(NSStr('Session %@ Download task %@ Finished downloading to URL %@'), session, downloadTask, location);
868870 {$ENDIF}
869871
870872 // populate download status object
871873 dlStatusObj.setLocation(filePath);
872874
873875 // update the user interface on the main thread.
874876 aSel := ObjCSelector(TDownloadlDelegate.downloadFinished);
875877 self.performSelectorOnMainThread_withObject_waitUntilDone(aSel, dlStatusObj, true);
876878
877879 // remove any old file from the destination directory
878880 if(NSFileManager.defaultManager.fileExistsAtPath(filePath)) then
879881 begin
880882 {$IFDEF DEBUG}
881883 NSLog(NSStr('Oh no, file already exists in downloads directory! Will remove for you :-)'));
882884 {$ENDIF}
883885 if (NSFileManager.defaultManager.removeItemAtPath_error(filePath, @error) <> True) then
884886 begin
885887 {$IFDEF DEBUG}
886888 NSLog(NSStr('Oh no, removal of %@ failed with erro %@'), filePath, error.localizedDescription);
887889 {$ENDIF}
888890 end;
889891 end;
890892
891893 // file move error
892894 if(NSFileManager.defaultManager.moveItemAtURL_toURL_error(location,downloadURL, @Error) <> True) then
893895 begin
894896 {$IFDEF DEBUG}
895897 NSLog(NSStr('Download error %@'), Error.localizedDescription);
896898 {$ENDIF}
897899 end;
898900end;
899901
900902procedure TDownloadlDelegate.URLSession_downloadTask_didWriteData_totalBytesWritten_totalBytesExpectedToWrite(session: NSURLSession;
901903 downloadTask: NSURLSessionDownloadTask; bytesWritten: cint64; totalBytesWritten: cint64; totalBytesExpectedToWrite: cint64);
902904var
903905 aSel: SEL;
904906begin
905907 {$IFDEF DEBUG}
906908 NSLog(NSStr('Session %@ download task %@ wrote an additional %lld bytes (total %lld bytes) out of an expected %lld bytes.'),
907909 session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite);
908910 {$ENDIF}
909911
910912 // populate status object
911913 dlStatusObj.setBytesRx(bytesWritten);
912914 dlStatusObj.setTotalBytesRx(totalBytesWritten);
913915 dlStatusObj.setExpectedTotalBytes(totalBytesExpectedToWrite);
914916
915917 // update the user interface on the main thread.
916918 aSel := ObjCSelector(TDownloadlDelegate.updateProgress);
917919 self.performSelectorOnMainThread_withObject_waitUntilDone(aSel, dlStatusObj, true);
918920end;
919921
920922procedure TDownloadlDelegate.updateProgress;
921923begin
922924 Form1.Memo1.Append('Bytes received: ' + IntToStr(dlStatusObj.getBytesRx));
923925 Form1.Memo1.Append('Total bytes received: ' + IntToStr(dlStatusObj.getTotalBytesRx));
924926 Form1.Memo1.Append('Expected total bytes: ' + IntToStr(dlStatusObj.getExpectedTotalBytes));
925927 Form1.Memo1.Append('---');
926928end;
927929
928930procedure TDownloadlDelegate.downloadFinished;
929931begin
930932 Form1.Memo1.Append('Download of ' + NSStringToString(dlStatusObj.getLocation) + ' finished!');
931933end;
932934
933935//
934936// Download button handler
935937//
936938procedure TForm1.Button1Click(Sender: TObject);
937939var
938940 dlReq: NSURL = Nil;
939941 dlSessionConfig: NSURLSessionConfiguration = Nil;
940942 dlSession: NSURLSession = Nil;
941943 dlTask: NSURLSessionDownloadTask = Nil;
942944 dlcache: NSURLcache;
943945 dlcachePath: NSString = Nil;
944946 dlQueue: NSOperationQueue = Nil;
945947 dlDelegate: TDownloadlDelegate = Nil;
946948begin
947949 // create request
948950 dlReq := NSURL.URLWithString(NSStr('https://www.sentry.org/microlab/Microlab_Teach-In-93-Pt12.pdf'));
949951 if(dlReq = Nil) then
950952 begin
951953 ShowMessage('NSURL.URLWithString failed! Aborting...');
952954 Exit;
953955 end;
954956
955957 // create download delegate
956958 dlDelegate := TDownloadlDelegate.alloc.init;
957959
958960 // create background session config
959961 dlSessionConfig := NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(NSStr('dtask01'));
960962
961963 // create a session with background session configuration
962964 dlSession := NSURLSession.sessionWithConfiguration_delegate_delegateQueue(dlSessionConfig, dlDelegate, dlQueue);
963965
964966 // configure caching behaviour for the session configuration
965967 dlCachePath := NSTemporaryDirectory.stringByAppendingPathComponent(NSStr('/nsurlsessiondemo.cache'));
966968 dlCache := NSURLCache.alloc.initWithMemoryCapacity_diskCapacity_diskPath(16384, 268435456, dlCachePath);
967969 dlSessionConfig.setURLCache(dlCache);
968970
969971 // log cache path
970972 {$IFDEF DEBUG}
971973 NSLog(NSStr('Cache path: %@'), dlCachePath);
972974 {$ENDIF}
973975
974976 // set cache policy
975977 {$IFDEF DEBUG}
976978 dlSessionConfig.setRequestCachePolicy(NSURLRequestReloadIgnoringLocalCacheData);
977979 {$ELSE}
978980 dlSessionConfig.setRequestCachePolicy(NSURLRequestUseProtocolCachePolicy);
979981 {$ENDIF}
980982
981983 // setup download task
982984 dlTask := dlSession.downloadTaskWithURL(dlReq);
983985
984986 // create download status object
985987 dlStatusObj := TdlStatusObj.alloc.init;
986988
987989 // execute download task
988990 dlTask.resume;
989991
990992 // housekeeping
991993 dlDelegate.release;
992994 dlCache.release;
993995 dlSession.finishTasksAndInvalidate;
994996end;
995997
996998end.
997999```
9981000
9991001Note that this is a very minimal demonstration application. In a real application you would want to, for example, extend the application-wide object (TdlStatus) to include any error messages which are still being handled by NSlog in the above example.
10001002
10011003Full project source code is available from [SourceForge](https://sourceforge.net/p/lazarus-wiki-projects/code/ci/master/tree/laz_nsurlsession4/).
10021004
10031005## See also
10041006
10051007- [[macOS NSURLConnection]] deprecated in favour of [[macOS NSURLSession]].
10061008- [[fpwebview]] - Free Pascal binding for webview, a cross-platform library that links Cocoa/WebKit on macOS.
10071009
10081010## External links
10091011
10101012- [Apple: NSString](https://developer.apple.com/documentation/foundation/nsstring).
10111013- [Apple: NSURL](https://developer.apple.com/documentation/foundation/nsurl?language=objc).
10121014- [Apple: NSURLCache](https://developer.apple.com/documentation/foundation/nsurlcache).
10131015- [Apple: NSURLSession](https://developer.apple.com/documentation/foundation/nsurlsession).
10141016- [Apple: NSURLRequest](https://developer.apple.com/documentation/foundation/nsurlrequest).
10151017- [Apple: NSURLSessionDownloadTask](https://developer.apple.com/documentation/foundation/nsurlsessiondownloadtask).
10161018- [Apple: NSURLSessionConfiguration](https://developer.apple.com/documentation/foundation/nsurlsessionconfiguration).
10171019- [Apple: NSURLResponse](https://developer.apple.com/documentation/foundation/nsurlresponse).
10181020- [Apple: NSHTTPURLResponse](https://developer.apple.com/documentation/foundation/nshttpurlresponse).
10191021- [Apple: performSelectorOnMainThread:withObject:waitUntilDone](https://developer.apple.com/documentation/objectivec/nsobject/1414900-performselectoronmainthread).
10201022- [Apple: Thread Programming Guide - Threads and Your User Interface](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/AboutThreads/AboutThreads.html#//apple_ref/doc/uid/10000057i-CH6-SW21).
10211023- [Apple: Network Framework](https://developer.apple.com/documentation/network) on which NSURLSession is built.
10221024- [Apple: NSAppTransportSecurity](https://developer.apple.com/documentation/bundleresources/information_property_list/nsapptransportsecurity).
10231025
10241026---
10251027*From the Free Pascal wiki, page "macOS NSURLSession", as it stood on 2022-02-21 after a change by Trev, out of the wiki's dump of 2024-05-26. Copyright stays with the author who wrote it, according to the old wiki's [[Licensing]] page. If you have concerns about this material, [message the admin](/wiki-contact).*

Back to history