Repository: laravel/framework (php)

Issue:
`CursorPaginator::onLastPage` method mentioned in docs but does not exist
<!-- DO NOT THROW THIS AWAY -->
<!-- Fill out the FULL versions with patch versions -->

- Laravel Version: 9.11.0
- PHP Version: 8.1.4
- Database Driver & Version: MariaDB 10.7.3

### Description:

The method `onLastPage` is mentioned in [pagination docs](https://laravel.com/docs/9.x/pagination#cursor-paginator-instance-methods) but when calling it on the cursor instance it throws an error. `onFirstPage` works just fine.


### Steps To Reproduce:

- Use cursor paginator on any model and try to access the `onLastPage` method like so:
```php
App\Models\Repository::orderBy('id')->cursorPaginate()->onFirstPage(); // Works fine
App\Models\Repository::orderBy('id')->cursorPaginate()->onLastPage(); // Throws 'Method Illuminate\Database\Eloquent\Collection::onLastPage does not exist.'
```


Pull request:
[9.x] Add missing onLastPage to CursorPaginator
<!--
Please only send a pull request to branches that are currently supported: https://laravel.com/docs/releases#support-policy 

If you are unsure which branch your pull request should be sent to, please read: https://laravel.com/docs/contributions#which-branch

Pull requests without a descriptive title, thorough description, or tests will be closed.

In addition, please describe the benefit to end users; the reasons it does not break any existing features; how it makes building web applications easier, etc.
-->


Closes #42296

As stated on issue #42296 the docs on `CursorPaginator`'s available methods states it has a `onLastPage` method, but the implementation of that method is missing on `Illuminate\Pagination\CursorPaginator` and its parent abstract class.

This PR:

- Adds the missing `onLastPage` implementation to `Illuminate\Pagination\CursorPaginator`
- Adds a test case covering both `onLastPage` and `onFirstPage`

Notes:

- Sent to 9.x and not 8.x, as the missing method is only on 9.x docs (see https://github.com/laravel/framework/pull/42299#issuecomment-1120358299)
- `onLastPage` implementation is borrowed from `Illuminate\Pagination\AbstractPaginator`

Patch:
diff --git a/src/Illuminate/Console/Concerns/InteractsWithIO.php b/src/Illuminate/Console/Concerns/InteractsWithIO.php
index 4593332c20..bdb594b078 100644
--- a/src/Illuminate/Console/Concerns/InteractsWithIO.php
+++ b/src/Illuminate/Console/Concerns/InteractsWithIO.php
@@ -217,7 +217,7 @@ public function choice($question, array $choices, $default = null, $attempts = n
      *
      * @param  array  $headers
      * @param  \Illuminate\Contracts\Support\Arrayable|array  $rows
-     * @param  string  $tableStyle
+     * @param  \Symfony\Component\Console\Helper\TableStyle|string  $tableStyle
      * @param  array  $columnStyles
      * @return void
      */
diff --git a/src/Illuminate/Foundation/Console/StubPublishCommand.php b/src/Illuminate/Foundation/Console/StubPublishCommand.php
index 44eb417811..ada1394616 100644
--- a/src/Illuminate/Foundation/Console/StubPublishCommand.php
+++ b/src/Illuminate/Foundation/Console/StubPublishCommand.php
@@ -46,8 +46,8 @@ public function handle()
         }
 
         $files = [
-            __DIR__.'/stubs/cast.stub' => $stubsPath.'/cast.stub',
             __DIR__.'/stubs/cast.inbound.stub' => $stubsPath.'/cast.inbound.stub',
+            __DIR__.'/stubs/cast.stub' => $stubsPath.'/cast.stub',
             __DIR__.'/stubs/console.stub' => $stubsPath.'/console.stub',
             __DIR__.'/stubs/event.stub' => $stubsPath.'/event.stub',
             __DIR__.'/stubs/job.queued.stub' => $stubsPath.'/job.queued.stub',
@@ -64,8 +64,8 @@ public function handle()
             __DIR__.'/stubs/policy.stub' => $stubsPath.'/policy.stub',
             __DIR__.'/stubs/provider.stub' => $stubsPath.'/provider.stub',
             __DIR__.'/stubs/request.stub' => $stubsPath.'/request.stub',
-            __DIR__.'/stubs/resource-collection.stub' => $stubsPath.'/resource-collection.stub',
             __DIR__.'/stubs/resource.stub' => $stubsPath.'/resource.stub',
+            __DIR__.'/stubs/resource-collection.stub' => $stubsPath.'/resource-collection.stub',
             __DIR__.'/stubs/rule.stub' => $stubsPath.'/rule.stub',
             __DIR__.'/stubs/scope.stub' => $stubsPath.'/scope.stub',
             __DIR__.'/stubs/test.stub' => $stubsPath.'/test.stub',
diff --git a/src/Illuminate/Pagination/CursorPaginator.php b/src/Illuminate/Pagination/CursorPaginator.php
index 76f42ab841..4c0a8965c0 100644
--- a/src/Illuminate/Pagination/CursorPaginator.php
+++ b/src/Illuminate/Pagination/CursorPaginator.php
@@ -121,6 +121,16 @@ public function onFirstPage()
         return is_null($this->cursor) || ($this->cursor->pointsToPreviousItems() && ! $this->hasMore);
     }
 
+    /**
+     * Determine if the paginator is on the last page.
+     *
+     * @return bool
+     */
+    public function onLastPage()
+    {
+        return ! $this->hasMorePages();
+    }
+
     /**
      * Get the instance as an array.
      *
diff --git a/src/Illuminate/Support/Str.php b/src/Illuminate/Support/Str.php
index 51a108290c..28b29f1e96 100644
--- a/src/Illuminate/Support/Str.php
+++ b/src/Illuminate/Support/Str.php
@@ -495,10 +495,18 @@ public static function mask($string, $character, $index, $length = null, $encodi
             return $string;
         }
 
-        $start = mb_substr($string, 0, mb_strpos($string, $segment, 0, $encoding), $encoding);
-        $end = mb_substr($string, mb_strpos($string, $segment, 0, $encoding) + mb_strlen($segment, $encoding));
+        $strlen = mb_strlen($string, $encoding);
+        $startIndex = $index;
 
-        return $start.str_repeat(mb_substr($character, 0, 1, $encoding), mb_strlen($segment, $encoding)).$end;
+        if ($index < 0) {
+            $startIndex = $index < -$strlen ? 0 : $strlen + $index;
+        }
+
+        $start = mb_substr($string, 0, $startIndex, $encoding);
+        $segmentLen = mb_strlen($segment, $encoding);
+        $end = mb_substr($string, $startIndex + $segmentLen);
+
+        return $start.str_repeat(mb_substr($character, 0, 1, $encoding), $segmentLen).$end;
     }
 
     /**


Test patch:
diff --git a/tests/Pagination/CursorPaginatorTest.php b/tests/Pagination/CursorPaginatorTest.php
index 8b30202c37..d3524abe7f 100644
--- a/tests/Pagination/CursorPaginatorTest.php
+++ b/tests/Pagination/CursorPaginatorTest.php
@@ -78,6 +78,24 @@ public function testCanTransformPaginatorItems()
         $this->assertSame([['id' => 6], ['id' => 7]], $p->items());
     }
 
+    public function testLengthAwarePaginatorisOnFirstAndLastPage()
+    {
+        $paginator = new CursorPaginator([['id' => 1], ['id' => 2], ['id' => 3], ['id' => 4]], 2, null, [
+            'parameters' => ['id'],
+        ]);
+
+        $this->assertTrue($paginator->onFirstPage());
+        $this->assertFalse($paginator->onLastPage());
+
+        $cursor = new Cursor(['id' => 3]);
+        $paginator = new CursorPaginator([['id' => 3], ['id' => 4]], 2, $cursor, [
+            'parameters' => ['id'],
+        ]);
+
+        $this->assertFalse($paginator->onFirstPage());
+        $this->assertTrue($paginator->onLastPage());
+    }
+
     protected function getCursor($params, $isNext = true)
     {
         return (new Cursor($params, $isNext))->encode();
diff --git a/tests/Support/SupportStrTest.php b/tests/Support/SupportStrTest.php
index 808fc7d02a..0b897ec055 100755
--- a/tests/Support/SupportStrTest.php
+++ b/tests/Support/SupportStrTest.php
@@ -592,6 +592,19 @@ public function testMask()
 
         $this->assertSame('这是一***', Str::mask('这是一段中文', '*', 3));
         $this->assertSame('**一段中文', Str::mask('这是一段中文', '*', 0, 2));
+
+        $this->assertSame('ma*n@email.com', Str::mask('maan@email.com', '*', 2, 1));
+        $this->assertSame('ma***email.com', Str::mask('maan@email.com', '*', 2, 3));
+        $this->assertSame('ma************', Str::mask('maan@email.com', '*', 2));
+
+        $this->assertSame('mari*@email.com', Str::mask('maria@email.com', '*', 4, 1));
+        $this->assertSame('tamar*@email.com', Str::mask('tamara@email.com', '*', 5, 1));
+
+        $this->assertSame('*aria@email.com', Str::mask('maria@email.com', '*', 0, 1));
+        $this->assertSame('maria@email.co*', Str::mask('maria@email.com', '*', -1, 1));
+        $this->assertSame('maria@email.co*', Str::mask('maria@email.com', '*', -1));
+        $this->assertSame('***************', Str::mask('maria@email.com', '*', -15));
+        $this->assertSame('***************', Str::mask('maria@email.com', '*', 0));
     }
 
     public function testMatch()