├── hm-post-repeat.css
├── phpunit.xml
├── hm-post-repeat.js
├── .travis.yml
├── tests
├── bootstrap.php
└── test-repeat-posts.php
├── README.md
├── .scrutinizer.yml
├── LICENSE
└── hm-post-repeat.php
/hm-post-repeat.css:
--------------------------------------------------------------------------------
1 | .misc-pub-hm-post-repeat {
2 | line-height: 25px;
3 | min-height: 25px;
4 | }
5 |
6 | .misc-pub-hm-post-repeat span.dashicons.dashicons-controls-repeat {
7 | margin-top: 2px;
8 | }
9 |
10 | .misc-pub-hm-post-repeat select {
11 | margin: 0;
12 | }
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
14 |
15 |
48 |
49 | _Repeating and repeat posts are highlighted as such on the edit posts page._
50 |
51 | -------
52 |
53 |
54 |
55 |
56 | _When editing a post you can easily set whether it should repeat._
57 |
58 | -------
59 |
60 |
61 |
62 | _Repeat posts show a convenient link back to the original._
63 |
64 | ## Changelog
65 |
66 | ### 0.4
67 |
68 | - Increase test coverage above 85%
69 | - Introduce additional daily & monthly repeating intervals. Props @noplanman.
70 |
71 | ### 0.3
72 |
73 | - Introduce unit tests
74 | - Fix several critical bugs
75 |
76 | ### 0.2
77 |
78 | - Refactor based on feedback from Human Made team
79 |
80 | ### 0.1
81 |
82 | - Initial release
83 |
84 | ## License
85 |
86 | Repeatable Posts is licensed under the GPLv2 or later.
87 |
88 | ## Credits
89 |
90 | Created by Human Made.
91 |
92 | Written and maintained by [Tom Willmot](https://github.com/willmot).
93 |
--------------------------------------------------------------------------------
/.scrutinizer.yml:
--------------------------------------------------------------------------------
1 | filter:
2 | excluded_paths: [ 'tests/*' ]
3 |
4 | tools:
5 | php_code_sniffer:
6 | config: { standard: WordPress }
7 |
8 | php_changetracking:
9 | enabled: true
10 | bug_patterns:
11 | - '\bfix(?:es|ed)?\b'
12 | feature_patterns:
13 | - '\badd(?:s|ed)?\b'
14 | - '\bimplement(?:s|ed)?\b'
15 |
16 | js_hint: true
17 |
18 | php_mess_detector:
19 | config:
20 | naming_rules: { boolean_method_name: true }
21 | controversial_rules: { superglobals: false }
22 |
23 | sensiolabs_security_checker: true
24 |
25 | php_loc: true
26 |
27 | php_hhvm:
28 | enabled: true
29 | command: hhvm
30 | extensions:
31 | - php
32 |
33 | php_analyzer:
34 | enabled: true
35 | extensions:
36 | - php
37 | config:
38 | parameter_reference_check: { enabled: false }
39 | checkstyle: { enabled: false }
40 | unreachable_code: { enabled: true }
41 | check_access_control: { enabled: false }
42 | typo_checks: { enabled: true }
43 | check_variables: { enabled: true }
44 | check_calls: { enabled: true, too_many_arguments: true, missing_argument: true, argument_type_checks: lenient }
45 | suspicious_code: { enabled: true, non_existent_class_in_instanceof_check: true, non_existent_class_in_catch_clause: true, non_commented_switch_fallthrough: true, non_commented_empty_catch_block: true, precedence_in_condition_assignment: true, overriding_parameter: false, overriding_closure_use: false, parameter_closure_use_conflict: false, parameter_multiple_times: false, assignment_of_null_return: false, overriding_private_members: false, use_statement_alias_conflict: false }
46 | dead_assignments: { enabled: true }
47 | verify_php_doc_comments: { enabled: true, parameters: true, return: true, suggest_more_specific_types: true, ask_for_return_if_not_inferrable: true, ask_for_param_type_annotation: true }
48 | loops_must_use_braces: { enabled: false }
49 | check_usage_context: { enabled: true, foreach: { value_as_reference: true, traversable: true } }
50 | simplify_boolean_return: { enabled: true }
51 | phpunit_checks: { enabled: false }
52 | reflection_checks: { enabled: false }
53 | precedence_checks: { enabled: true, assignment_in_condition: true, comparison_of_bit_result: true }
54 | basic_semantic_checks: { enabled: true }
55 | unused_code: { enabled: true }
56 | deprecation_checks: { enabled: true }
57 | useless_function_calls: { enabled: true }
58 | metrics_lack_of_cohesion_methods: { enabled: true }
59 | metrics_coupling: { enabled: true, stable_code: { namespace_prefixes: { }, classes: { } } }
60 | doctrine_parameter_binding: { enabled: false }
61 | doctrine_entity_manager_injection: { enabled: false }
62 | symfony_request_injection: { enabled: false }
63 | doc_comment_fixes: { enabled: false }
64 | reflection_fixes: { enabled: false }
65 | use_statement_fixes: { enabled: true, remove_unused: true, preserve_multiple: false, preserve_blanklines: false, order_alphabetically: false }
66 |
67 | # PHP Similarity Analyzer and Copy/paste Detector cannot be used at
68 | # the same time right now. Make sure to either remove, or disable one.
69 | php_cpd: false
70 |
71 | php_pdepend: true
72 |
73 | php_sim:
74 | enabled: true
75 | min_mass: 50
76 |
77 | checks:
78 | php:
79 | code_rating: true
80 | duplication: true
81 |
--------------------------------------------------------------------------------
/tests/test-repeat-posts.php:
--------------------------------------------------------------------------------
1 | factory->post->create();
14 | $this->assertFalse( is_repeating_post( $post_id ) );
15 |
16 | save_post_repeating_status( $post_id, 'no' );
17 | $this->assertFalse( is_repeating_post( $post_id ) );
18 |
19 | }
20 |
21 | function test_setting_post_repeating_status_yes() {
22 |
23 | $post_id = $this->factory->post->create();
24 | $this->assertFalse( is_repeating_post( $post_id ) );
25 |
26 | save_post_repeating_status( $post_id, 'weekly' );
27 | $this->assertTrue( is_repeating_post( $post_id ) );
28 |
29 | }
30 |
31 | function test_setting_post_repeating_status_blank() {
32 |
33 | $post_id = $this->factory->post->create();
34 | $this->assertFalse( is_repeating_post( $post_id ) );
35 |
36 | save_post_repeating_status( $post_id, 'weekly' );
37 | $this->assertTrue( is_repeating_post( $post_id ) );
38 |
39 | save_post_repeating_status( $post_id );
40 | $this->assertTrue( is_repeating_post( $post_id ) );
41 |
42 | }
43 |
44 | function test_repeating_post_types_filter() {
45 |
46 | $this->assertContains( 'post', repeating_post_types() );
47 |
48 | add_filter( 'hm_post_repeat_post_types', function( $post_types ) {
49 | $post_types[] = 'page';
50 | return $post_types;
51 | } );
52 |
53 | $this->assertContains( 'page', repeating_post_types() );
54 |
55 | }
56 |
57 | function test_is_repeating_post_save_post_hook() {
58 |
59 | $post_id = $this->factory->post->create();
60 | $this->assertFalse( is_repeating_post( $post_id ) );
61 |
62 | $_POST['ID'] = $post_id;
63 | $_POST['hm-post-repeat'] = 'weekly';
64 | $this->assertTrue( is_repeating_post( $post_id ) );
65 |
66 | }
67 |
68 | function test_repeat_post_no() {
69 |
70 | $post_id = $this->factory->post->create();
71 | $this->assertFalse( is_repeat_post( $post_id ) );
72 |
73 | }
74 |
75 | function test_repeat_post_yes() {
76 |
77 | $parent_post_id = $this->factory->post->create();
78 | $post_id = $this->factory->post->create( array( 'post_parent' => $parent_post_id ) );
79 |
80 | $this->assertFalse( is_repeat_post( $post_id ) );
81 | $this->assertEquals( $parent_post_id, get_post( $post_id )->post_parent );
82 |
83 | save_post_repeating_status( $parent_post_id, 'weekly' );
84 |
85 | $this->assertTrue( is_repeat_post( $post_id ) );
86 |
87 | }
88 |
89 | function test_post_states() {
90 |
91 | global $post_states;
92 |
93 | $parent_post_id = $this->factory->post->create();
94 | $post_id = $this->factory->post->create( array( 'post_parent' => $parent_post_id ) );
95 | save_post_repeating_status( $parent_post_id, 'weekly' );
96 |
97 | // Hack to allow us access to $post_states so we can test it
98 | add_filter( 'display_post_states', function( $states, $post ) {
99 |
100 | global $post_states;
101 | $post_states = $states;
102 |
103 | return $states;
104 |
105 | }, 11, 2 );
106 |
107 | // We need to output buffer _post_states as it echo's
108 | ob_start();
109 | _post_states( get_post( $post_id ) );
110 | ob_end_clean();
111 |
112 | $this->assertEquals( array( 'hm-post-repeat' => __( 'Repeat', 'hm-post-repeat' ) ), $post_states );
113 |
114 | ob_start();
115 | _post_states( get_post( $parent_post_id ) );
116 | ob_end_clean();
117 |
118 | $this->assertEquals( array( 'hm-post-repeat' => __( 'Repeating', 'hm-post-repeat' ) ), $post_states );
119 |
120 | }
121 |
122 | function test_create_get_repeating_post() {
123 |
124 | $parent_post_id = $this->factory->post->create();
125 | $post_id = $this->factory->post->create( array( 'post_parent' => $parent_post_id ) );
126 | save_post_repeating_status( $parent_post_id, 'weekly' );
127 |
128 | $this->assertEquals( $parent_post_id, get_repeating_post( $parent_post_id ) );
129 | $this->assertEquals( $parent_post_id, get_repeating_post( $post_id ) );
130 |
131 | }
132 |
133 | function test_create_repeat_post_from_not_repeating() {
134 |
135 | $post_id = $this->factory->post->create();
136 | $this->assertFalse( create_next_repeat_post( $post_id ) );
137 | $this->assertCount( 0, get_posts( array( 'post_status' => 'future' ) ) );
138 |
139 | }
140 |
141 | function test_create_repeat_post_from_published_repeating_post() {
142 |
143 | $post_id = $this->factory->post->create();
144 | save_post_repeating_status( $post_id, 'weekly' );
145 |
146 | $this->assertCount( 0, get_posts( array( 'post_status' => 'future' ) ) );
147 |
148 | $this->assertEquals( $post_id + 1, create_next_repeat_post( $post_id ) );
149 | $this->assertCount( 1, get_posts( array( 'post_status' => 'future' ) ) );
150 |
151 | // Prove that another repeat post isn't created
152 | $this->assertFalse( create_next_repeat_post( $post_id ) );
153 | $this->assertCount( 1, get_posts( array( 'post_status' => 'future' ) ) );
154 |
155 | }
156 |
157 | function test_create_repeat_post_from_unpublished_repeating_post() {
158 |
159 | $post_id = $this->factory->post->create( array( 'post_status' => 'draft' ) );
160 | save_post_repeating_status( $post_id, 'weekly' );
161 |
162 | $this->assertCount( 0, get_posts( array( 'post_status' => 'future' ) ) );
163 |
164 | $this->assertFalse( create_next_repeat_post( $post_id ) );
165 | $this->assertCount( 0, get_posts( array( 'post_status' => 'future' ) ) );
166 |
167 | wp_update_post( array( 'ID' => $post_id, 'post_status' => 'pending' ) );
168 | $this->assertFalse( create_next_repeat_post( $post_id ) );
169 | $this->assertCount( 0, get_posts( array( 'post_status' => 'future' ) ) );
170 |
171 | wp_update_post( array( 'ID' => $post_id, 'post_status' => 'custom' ) );
172 | $this->assertFalse( create_next_repeat_post( $post_id ) );
173 | $this->assertCount( 0, get_posts( array( 'post_status' => 'future' ) ) );
174 |
175 | // future post statuses are converted to publish if the post date is in the past
176 | wp_update_post( array( 'ID' => $post_id, 'post_status' => 'future' ) );
177 | $this->assertFalse( create_next_repeat_post( $post_id ) );
178 | $this->assertCount( 1, get_posts( array( 'post_status' => 'future' ) ) );
179 |
180 | }
181 |
182 | function test_create_repeat_post_from_repeating_post_publish_action() {
183 |
184 | $post_id = $this->factory->post->create( array( 'post_status' => 'draft' ) );
185 | save_post_repeating_status( $post_id, 'weekly' );
186 |
187 | $this->assertCount( 0, get_posts( array( 'post_status' => 'future' ) ) );
188 |
189 | wp_update_post( array( 'ID' => $post_id, 'post_status' => 'publish' ) );
190 | $this->assertCount( 1, get_posts( array( 'post_status' => 'future' ) ) );
191 |
192 | }
193 |
194 | function test_create_repeat_unsupported_repeating_post_type() {
195 |
196 | $post_id = $this->factory->post->create( array( 'post_type' => 'middle-out-encryption' ) );
197 | save_post_repeating_status( $post_id, 'weekly' );
198 |
199 | $this->assertFalse( create_next_repeat_post( $post_id ) );
200 |
201 | }
202 |
203 | function test_create_repeat_post_copies_meta() {
204 |
205 | $post_id = $this->factory->post->create();
206 | save_post_repeating_status( $post_id, 'weekly' );
207 |
208 | $meta = array( 'NonEmptyString' => 'Test', 'Int' => 134, 'EmptyString' => '', 'bool' => true, 'object' => get_post( $post_id ), 'array' => get_post( $post_id, ARRAY_A ) );
209 |
210 | foreach ( $meta as $key => $value ) {
211 | add_post_meta( $post_id, $key, $value );
212 | }
213 |
214 | $post_meta = get_post_meta( $post_id );
215 |
216 | // `hm-post-repeat` isn't copied over
217 | unset( $post_meta['hm-post-repeat'] );
218 |
219 | $repeat_post_id = create_next_repeat_post( $post_id );
220 | $repeat_post_meta = get_post_meta( $repeat_post_id );
221 | $this->assertEquals( $post_meta, $repeat_post_meta );
222 |
223 | }
224 |
225 | function test_create_repeat_post_copies_terms() {
226 |
227 | $post_id = $this->factory->post->create();
228 | $this->factory->term->add_post_terms( $post_id, array( 'Tag 1', 'Tag 2' ), 'post_tag' );
229 |
230 | $cat1 = $this->factory->term->create_object( array( 'taxonomy' => 'category', 'name' => 'Cat 1' ) );
231 | $cat2 = $this->factory->term->create_object( array( 'taxonomy' => 'category', 'name' => 'Cat 2' ) );
232 |
233 | $this->factory->term->add_post_terms( $post_id, array( $cat1, $cat2 ), 'category' );
234 |
235 | $this->assertTrue( has_tag( 'Tag 1', $post_id ) );
236 | $this->assertTrue( has_tag( 'Tag 2', $post_id ) );
237 |
238 | $this->assertTrue( has_category( $cat1, $post_id ) );
239 | $this->assertTrue( has_category( $cat2, $post_id ) );
240 |
241 | save_post_repeating_status( $post_id, 'weekly' );
242 |
243 | $repeat_post_id = create_next_repeat_post( $post_id );
244 |
245 | $this->assertTrue( has_tag( 'Tag 1', $repeat_post_id ) );
246 | $this->assertTrue( has_tag( 'Tag 2', $repeat_post_id ) );
247 |
248 | $this->assertTrue( has_category( $cat1, $repeat_post_id ) );
249 | $this->assertTrue( has_category( $cat2, $repeat_post_id ) );
250 |
251 | }
252 |
253 | /**
254 | * Specifically test that the repeating status is saved to post meta and the next
255 | * repeat post is created & scheduled when publishing a new repeating post
256 | */
257 | function test_publish_repeating_post_creates_repeat_post() {
258 |
259 | $_POST['hm-post-repeat'] = 'weekly';
260 | $post_id = $this->factory->post->create();
261 | $this->assertTrue( is_repeating_post( $post_id ) );
262 |
263 | $this->assertCount( 1, get_posts( array( 'post_status' => 'future' ) ) );
264 |
265 | }
266 |
267 | function test_repeating_post_interval_invalid() {
268 |
269 | $_POST['hm-post-repeat'] = 'some-day';
270 | $post = $this->factory->post->create_and_get();
271 | $this->assertFalse( is_repeating_post( $post->ID ) );
272 |
273 | $future_posts = get_posts( array( 'post_status' => 'future' ) );
274 | $this->assertCount( 0, $future_posts );
275 |
276 | }
277 |
278 | function test_add_custom_repeating_schedule() {
279 |
280 | add_filter( 'hm_post_repeat_schedules', function( $schedules ) {
281 |
282 | $schedules['yearly'] = array( 'interval' => '1 year', 'display' => 'Yearly' );
283 | return $schedules;
284 |
285 | } );
286 |
287 | $this->assertTrue( key_exists( 'yearly', get_repeating_schedules() ) );
288 |
289 | }
290 |
291 | function test_repeating_post_interval_custom() {
292 |
293 | add_filter( 'hm_post_repeat_schedules', function( $schedules ) {
294 |
295 | $schedules['3-days'] = array( 'interval' => '3 days', 'display' => 'Every 3 days' );
296 | return $schedules;
297 |
298 | } );
299 |
300 | $_POST['hm-post-repeat'] = '3-days';
301 | $post = $this->factory->post->create_and_get();
302 | $this->assertTrue( is_repeating_post( $post->ID ) );
303 |
304 | $future_posts = get_posts( array( 'post_status' => 'future' ) );
305 | $this->assertCount( 1, $future_posts );
306 |
307 | $repeat_post = reset( $future_posts );
308 | $this->assertTrue( is_repeat_post( $repeat_post->ID ) );
309 |
310 | $next_post_date = date( 'Y-m-d H:i:s', strtotime( $post->post_date . ' + 3 days' ) );
311 | $this->assertSame( $repeat_post->post_date, $next_post_date );
312 |
313 | }
314 |
315 | function test_repeating_post_interval_daily() {
316 |
317 | $_POST['hm-post-repeat'] = 'daily';
318 | $post = $this->factory->post->create_and_get();
319 | $this->assertTrue( is_repeating_post( $post->ID ) );
320 |
321 | $future_posts = get_posts( array( 'post_status' => 'future' ) );
322 | $this->assertCount( 1, $future_posts );
323 |
324 | $repeat_post = reset( $future_posts );
325 | $this->assertTrue( is_repeat_post( $repeat_post->ID ) );
326 |
327 | $next_post_date = date( 'Y-m-d H:i:s', strtotime( $post->post_date . ' + 1 day' ) );
328 | $this->assertSame( $repeat_post->post_date, $next_post_date );
329 |
330 | }
331 |
332 | function test_repeating_post_interval_weekly() {
333 |
334 | $_POST['hm-post-repeat'] = 'weekly';
335 | $post = $this->factory->post->create_and_get();
336 | $this->assertTrue( is_repeating_post( $post->ID ) );
337 |
338 | $future_posts = get_posts( array( 'post_status' => 'future' ) );
339 | $this->assertCount( 1, $future_posts );
340 |
341 | $repeat_post = reset( $future_posts );
342 | $this->assertTrue( is_repeat_post( $repeat_post->ID ) );
343 |
344 | $next_post_date = date( 'Y-m-d H:i:s', strtotime( $post->post_date . ' + 1 week' ) );
345 | $this->assertSame( $repeat_post->post_date, $next_post_date );
346 |
347 | }
348 |
349 | function test_repeating_post_interval_fortnightly() {
350 |
351 | $_POST['hm-post-repeat'] = 'fortnightly';
352 | $post = $this->factory->post->create_and_get();
353 | $this->assertTrue( is_repeating_post( $post->ID ) );
354 |
355 | $future_posts = get_posts( array( 'post_status' => 'future' ) );
356 | $this->assertCount( 1, $future_posts );
357 |
358 | $repeat_post = reset( $future_posts );
359 | $this->assertTrue( is_repeat_post( $repeat_post->ID ) );
360 |
361 | $next_post_date = date( 'Y-m-d H:i:s', strtotime( $post->post_date . ' + 2 weeks' ) );
362 | $this->assertSame( $repeat_post->post_date, $next_post_date );
363 |
364 | }
365 |
366 | function test_repeating_post_interval_monthly() {
367 |
368 | $_POST['hm-post-repeat'] = 'monthly';
369 | $post = $this->factory->post->create_and_get();
370 | $this->assertTrue( is_repeating_post( $post->ID ) );
371 |
372 | $future_posts = get_posts( array( 'post_status' => 'future' ) );
373 | $this->assertCount( 1, $future_posts );
374 |
375 | $repeat_post = reset( $future_posts );
376 | $this->assertTrue( is_repeat_post( $repeat_post->ID ) );
377 |
378 | $next_post_date = date( 'Y-m-d H:i:s', strtotime( $post->post_date . ' + 1 month' ) );
379 | $this->assertSame( $repeat_post->post_date, $next_post_date );
380 |
381 | }
382 |
383 | /**
384 | * This test assumes that the meta data was invalidly set directly in the database.
385 | */
386 | function test_get_repeating_schedule_invalid_direct_db_entry() {
387 |
388 | $post_id = $this->factory->post->create();
389 | add_post_meta( $post_id, 'hm-post-repeat', 'some-day' );
390 | $this->assertNull( get_repeating_schedule( $post_id ) );
391 |
392 | }
393 |
394 | function test_get_repeating_schedule_invalid() {
395 |
396 | $_POST['hm-post-repeat'] = 'some-day';
397 | $post_id = $this->factory->post->create();
398 | $this->assertNull( get_repeating_schedule( $post_id ) );
399 |
400 | }
401 |
402 | /**
403 | * This test assumes that the meta data was correctly set directly in the database.
404 | */
405 | function test_get_repeating_schedule_valid_direct_db_entry() {
406 |
407 | $post_id = $this->factory->post->create();
408 | add_post_meta( $post_id, 'hm-post-repeat', 'daily' );
409 | $this->assertSame( array(
410 | 'interval' => '1 day',
411 | 'display' => 'Daily',
412 | 'slug' => 'daily',
413 | ), get_repeating_schedule( $post_id ) );
414 |
415 | }
416 |
417 | function test_get_repeating_schedule_valid() {
418 |
419 | $_POST['hm-post-repeat'] = 'daily';
420 | $post_id = $this->factory->post->create();
421 | $this->assertSame( array(
422 | 'interval' => '1 day',
423 | 'display' => 'Daily',
424 | 'slug' => 'daily',
425 | ), get_repeating_schedule( $post_id ) );
426 |
427 | }
428 |
429 | /**
430 | * This method assumes an existing old schedule format in the post meta.
431 | */
432 | function test_get_repeating_schedule_backwards_compatible_old() {
433 |
434 | $post_id = $this->factory->post->create();
435 | add_post_meta( $post_id, 'hm-post-repeat', '1' );
436 | $this->assertSame( array(
437 | 'interval' => '1 week',
438 | 'display' => 'Weekly',
439 | 'slug' => 'weekly',
440 | ), get_repeating_schedule( $post_id ) );
441 |
442 | }
443 |
444 | /**
445 | * Tests that a repeat post is modified via filter before being saved/scheduled.
446 | * Only repeat posts with a specific schedule are modified.
447 | */
448 | function test_edit_repeat_post_before_scheduling() {
449 |
450 | // Edit repeat post title if it's on weekly schedule.
451 | add_filter( 'hm_post_repeat_edit_repeat_post', function( $next_post, $repeating_schedule, $original_post ) {
452 |
453 | if ( $repeating_schedule['slug'] === 'weekly' ) {
454 | $next_post['post_title'] = 'Repeat Post Week 1, 2018';
455 | }
456 |
457 | return $next_post;
458 | }, 10, 3 );
459 |
460 | // Create main repeating post and schedule to repeat it weekly.
461 | $_POST['hm-post-repeat'] = 'weekly';
462 | $post_id = $this->factory->post->create( array(
463 | 'post_title' => 'Repeating Post',
464 | 'post_status' => 'publish',
465 | ) );
466 |
467 | // Check repeating post is there with its original title.
468 | $this->assertTrue( is_repeating_post( $post_id ) );
469 | $this->assertSame( get_the_title( $post_id ) , 'Repeating Post' );
470 |
471 | // Check repeat post is scheduled with a new title.
472 | $repeat_posts = get_posts( array( 'post_status' => 'future' ) );
473 |
474 | $this->assertNotEmpty( $repeat_posts );
475 | $this->assertTrue( is_repeat_post( $repeat_posts[0]->ID ) );
476 | $this->assertSame( $repeat_posts[0]->post_title, 'Repeat Post Week 1, 2018' );
477 | }
478 |
479 | }
480 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,