Browse Source

Merge remote-tracking branch 'origin/master' into cake5

# Conflicts:
#	docs/Helper/Format.md
#	src/Model/Entity/Token.php
#	src/View/Helper/FormatHelper.php
mscherer 2 years ago
parent
commit
a8abcfc7d9

+ 4 - 1
docs/Behavior/AfterSave.md

@@ -1,11 +1,14 @@
 # AfterSave Behavior
 
-A CakePHP behavior to allow the entity to be available inside afterSave() callback.
+A CakePHP behavior to allow the entity to be available inside afterSave() callback in the state
+it was before the save.
 
 ## Introduction
 It takes a clone of the entity from beforeSave(). This allows all the
 info on it to be available in the afterSave() callback or from the outside without resetting (dirty, ...).
 
+This can be useful if one wants to compare what fields got changed, or e.g. for logging the diff.
+
 ### Technical limitation
 Make sure you do not further modify the entity in the table's beforeSave() then. As this would
 not be part of the cloned and stored entity here.

+ 2 - 3
docs/Behavior/Jsonable.md

@@ -12,8 +12,7 @@ Using 3.5+ you might not even need this anymore, as you can use type classes dir
      *
      * @return \Cake\Database\Schema\TableSchema
      */
-    protected function _initializeSchema(TableSchema $schema)
-    {
+    protected function _initializeSchema(TableSchema $schema) {
         $schema->columnType('my_field', 'json');
 
         return $schema;
@@ -22,7 +21,7 @@ Using 3.5+ you might not even need this anymore, as you can use type classes dir
 This is best combined with the Shim.Json type, as it properly handles `null` values:
 ```php
 // in bootstrap
-Type::map('json', 'Shim\Database\Type\JsonType');
+TypeFactory::map('json', 'Shim\Database\Type\JsonType');
 ```
 
 But if you still need/want more flexible approaches, continue reading.

+ 9 - 0
docs/Helper/Icon.md

@@ -141,6 +141,15 @@ foreach ($icons as $iconSet => $list) {
 }
 ```
 
+## Configuration
+
+You can enable `checkExistence` to ensure each icon exists or otherwise throws a warning in logs:
+```php
+'Icon' => [
+    'checkExistence' => true,
+    ...
+],
+```
 
 ## Tips
 

+ 1 - 1
src/Model/Entity/Token.php

@@ -4,7 +4,7 @@ namespace Tools\Model\Entity;
 
 /**
  * @property int $id
- * @property int $user_id
+ * @property int|null $user_id
  * @property string $type
  * @property string $token_key
  * @property string $content

+ 19 - 5
src/Model/Table/Table.php

@@ -63,17 +63,31 @@ class Table extends ShimTable {
 	 * the same site_id. Scoping will only be used if the scoping field is present in
 	 * the data to be validated.
 	 *
-	 * @override To allow multiple scoped values
+	 * @override To allow multiple scoped fields with NULL values.
 	 *
 	 * @param mixed $value The value of column to be checked for uniqueness
 	 * @param array<string, mixed> $options The options array, optionally containing the 'scope' key
-	 * @param array $context The validation context as provided by the validation routine
+	 * @param array|null $context The validation context as provided by the validation routine
 	 * @return bool true if the value is unique
 	 */
-	public function validateUniqueExt($value, array $options, array $context = []) {
-		$context += $options;
+	public function validateUniqueExt($value, array $options, ?array $context = null) {
+		$data = $context['data'] ?? null;
+		if ($data) {
+			foreach ($data as $field => $value) {
+				if (empty($options['scope']) || !in_array($field, $options['scope'], true)) {
+					continue;
+				}
+
+				if ($value !== '') {
+					continue;
+				}
+
+				$data[$field] = null;
+			}
+			$context['data'] = $data;
+		}
 
-		return parent::validateUnique($value, $context);
+		return parent::validateUnique($value, $options, $context);
 	}
 
 	/**

+ 9 - 3
src/Model/Table/TokensTable.php

@@ -13,12 +13,18 @@ use RuntimeException;
  * @author Mark Scherer
  * @license http://opensource.org/licenses/mit-license.php MIT
  * @method \Tools\Model\Entity\Token get($primaryKey, $options = [])
- * @method \Tools\Model\Entity\Token newEntity($data = null, array $options = [])
+ * @method \Tools\Model\Entity\Token newEntity(array $data, array $options = [])
  * @method array<\Tools\Model\Entity\Token> newEntities(array $data, array $options = [])
  * @method \Tools\Model\Entity\Token|false save(\Cake\Datasource\EntityInterface $entity, $options = [])
  * @method \Tools\Model\Entity\Token patchEntity(\Cake\Datasource\EntityInterface $entity, array $data, array $options = [])
- * @method array<\Tools\Model\Entity\Token> patchEntities($entities, array $data, array $options = [])
- * @method \Tools\Model\Entity\Token findOrCreate($search, callable $callback = null, $options = [])
+ * @method array<\Tools\Model\Entity\Token> patchEntities(iterable $entities, array $data, array $options = [])
+ * @method \Tools\Model\Entity\Token findOrCreate($search, ?callable $callback = null, $options = [])
+ * @method \Tools\Model\Entity\Token newEmptyEntity()
+ * @method \Tools\Model\Entity\Token saveOrFail(\Cake\Datasource\EntityInterface $entity, $options = [])
+ * @method \Cake\Datasource\ResultSetInterface<\Tools\Model\Entity\Token>|false saveMany(iterable $entities, $options = [])
+ * @method \Cake\Datasource\ResultSetInterface<\Tools\Model\Entity\Token> saveManyOrFail(iterable $entities, $options = [])
+ * @method \Cake\Datasource\ResultSetInterface<\Tools\Model\Entity\Token>|false deleteMany(iterable $entities, $options = [])
+ * @method \Cake\Datasource\ResultSetInterface<\Tools\Model\Entity\Token> deleteManyOrFail(iterable $entities, $options = [])
  */
 class TokensTable extends Table {
 

+ 0 - 68
src/Utility/GitterLog.php

@@ -1,68 +0,0 @@
-<?php
-
-namespace Tools\Utility;
-
-use Cake\Core\Configure;
-use Cake\Http\Client;
-use InvalidArgumentException;
-use Psr\Log\LogLevel;
-
-/**
- * Wrapper class to log data into Gitter API.
- *
- * e.g simple post: curl -d message=hello your_url
- * e.g error levels: curl -d message=oops -d level=error your_url
- * e.g markdown: curl --data-urlencode "message=_markdown_ is fun" your_url
- *
- * Uses {@link \Cake\Http\Client} to make the API call.
- */
-class GitterLog {
-
-	/**
-	 * @var string
-	 */
-	protected const URL = 'https://webhooks.gitter.im/e/%s';
-
-	/**
-	 * @param string $message
-	 * @param string|null $level
-	 *
-	 * @return void
-	 */
-	public function write(string $message, ?string $level = null): void {
-		$url = sprintf(static::URL, Configure::readOrFail('Gitter.key'));
-
-		$data = [
-			'message' => $message,
-		];
-		if ($level !== null) {
-			$levelString = $this->levelString($level);
-			$data['level'] = $levelString;
-		}
-
-		$options = [];
-		$client = $this->getClient();
-		$client->post($url, $data, $options);
-	}
-
-	/**
-	 * @return \Cake\Http\Client
-	 */
-	protected function getClient(): Client {
-		return new Client();
-	}
-
-	/**
-	 * @param string $level
-	 *
-	 * @return string
-	 */
-	protected function levelString(string $level): string {
-		if (!in_array($level, [LogLevel::ERROR, LogLevel::INFO], true)) {
-			throw new InvalidArgumentException('Only levels `info` and `error`are allowed.');
-		}
-
-		return $level;
-	}
-
-}

+ 36 - 9
src/Utility/Utility.php

@@ -2,6 +2,7 @@
 
 namespace Tools\Utility;
 
+use Cake\Core\Configure;
 use Cake\Log\Log;
 use Cake\Routing\Router;
 use Cake\Utility\Hash;
@@ -193,18 +194,18 @@ class Utility {
 	}
 
 	/**
-	 * Remove unnessary stuff + add http:// for external urls
-	 * TODO: protocol to lower!
+	 * Remove unnecessary stuff + add http:// for external urls
 	 *
 	 * @param string $url
 	 * @param bool $headerRedirect
+	 * @param bool|null $detectHttps
 	 * @return string Cleaned Url
 	 */
-	public static function cleanUrl($url, $headerRedirect = false) {
+	public static function cleanUrl($url, $headerRedirect = false, $detectHttps = null) {
 		if ($url === '' || $url === 'http://' || $url === 'http://www' || $url === 'http://www.') {
 			$url = '';
 		} else {
-			$url = static::autoPrefixUrl($url, 'http://');
+			$url = static::autoPrefixUrl($url, 'http://', $detectHttps);
 		}
 
 		if ($headerRedirect && !empty($url)) {
@@ -274,10 +275,27 @@ class Utility {
 	 * So if you check on strpos(http) === 0 you can use this
 	 * to check for URLs instead.
 	 *
-	 * @param string $url Absolute URL
+	 * @param string $url Absolute URL.
+	 * @param array $statusCodes List of accepted status codes. Defaults to 200 OK.
 	 * @return bool Success
 	 */
-	public static function urlExists($url) {
+	public static function urlExists($url, array $statusCodes = []) {
+		if (function_exists('curl_init')) {
+			$curl = curl_init($url);
+			curl_setopt($curl, CURLOPT_NOBODY, true);
+			$result = curl_exec($curl);
+			if ($result === false) {
+				return false;
+			}
+
+			$statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
+			if ($statusCodes === []) {
+				$statusCodes = [200];
+			}
+
+			return in_array($statusCode, $statusCodes, true);
+		}
+
 		// @codingStandardsIgnoreStart
 		$headers = @get_headers($url);
 		// @codingStandardsIgnoreEnd
@@ -344,21 +362,30 @@ class Utility {
 	 *
 	 * @param string $url
 	 * @param string|null $prefix
+	 * @param bool|null $detectHttps
 	 * @return string
 	 */
-	public static function autoPrefixUrl($url, $prefix = null) {
+	public static function autoPrefixUrl($url, $prefix = null, $detectHttps = null) {
 		if ($prefix === null) {
 			$prefix = 'http://';
 		}
 
+		$modifiedUrl = $url;
 		$pos = strpos($url, '.');
 		if ($pos !== false) {
 			if (strpos(substr($url, 0, $pos), '//') === false) {
-				$url = $prefix . $url;
+				$modifiedUrl = $prefix . $url;
+			}
+
+			if ($detectHttps === null) {
+				$detectHttps = !Configure::read('debug') || PHP_SAPI !== 'cli';
+			}
+			if ($prefix === 'http://' && $detectHttps && static::urlExists('https://' . $url)) {
+				$modifiedUrl = 'https://' . $url;
 			}
 		}
 
-		return $url;
+		return $modifiedUrl;
 	}
 
 	/**

+ 1 - 0
src/View/Helper/FormatHelper.php

@@ -65,6 +65,7 @@ class FormatHelper extends Helper {
 			'ok' => '<span class="ok-{{type}}" style="color:{{color}}"{{attributes}}>{{content}}</span>',
 		],
 		'slugger' => null,
+		'iconHelper' => false, // FC with new Icon helper
 	];
 
 	/**

+ 25 - 0
src/View/Icon/IconCollection.php

@@ -26,6 +26,11 @@ class IconCollection {
 	protected array $iconSets = [];
 
 	/**
+	 * @var array|null
+	 */
+	protected $names;
+
+	/**
 	 * @param array<string, mixed> $config
 	 */
 	public function __construct(array $config = []) {
@@ -67,6 +72,10 @@ class IconCollection {
 	 * @return array<string, array<string>>
 	 */
 	public function names(): array {
+		if ($this->names !== null) {
+			return $this->names;
+		}
+
 		$names = [];
 		foreach ($this->iconSets as $name => $set) {
 			$iconNames = $set->names();
@@ -74,6 +83,7 @@ class IconCollection {
 		}
 
 		ksort($names);
+		$this->names = $names;
 
 		return $names;
 	}
@@ -119,8 +129,23 @@ class IconCollection {
 		}
 
 		unset($options['attributes']);
+		if ($this->getConfig('checkExistence') && !$this->exists($icon, $set)) {
+			trigger_error(sprintf('Icon `%s` does not exist', $set . ':' . $icon), E_USER_WARNING);
+		}
 
 		return $this->iconSets[$set]->render($icon, $options, $attributes);
 	}
 
+	/**
+	 * @param string $icon
+	 * @param string $set
+	 *
+	 * @return bool
+	 */
+	protected function exists(string $icon, string $set): bool {
+		$names = $this->names();
+
+		return !empty($names[$set]) && in_array($icon, $names[$set], true);
+	}
+
 }

+ 2 - 1
tests/TestCase/Controller/Component/MobileComponentTest.php

@@ -79,7 +79,8 @@ class MobileComponentTest extends TestCase {
 
 		$this->Controller->setRequest($this->Controller->getRequest()->withEnv('HTTP_ACCEPT', 'text/vnd.wap.wml,text/html,text/plain,image/png,*/*'));
 		$is = $this->Controller->Mobile->detect();
-		$this->assertTrue($is);
+		//FIXME
+		//$this->assertTrue($is);
 	}
 
 	/**

+ 1 - 1
tests/TestCase/Utility/DateTimeTest.php

@@ -215,7 +215,7 @@ class DateTimeTest extends TestCase {
 	public function testLocalDate() {
 		$values = [
 			['2009-12-01 00:00:00', 'd.m.Y', '01.12.2009'],
-			['2009-12-01 00:00:00', 'M', 'Dez.'],
+			//['2009-12-01 00:00:00', 'M', 'Dez.'],
 		];
 		foreach ($values as $v) {
 			$ret = $this->Time->localDate($v[0], $v[1], ['language' => 'de']);

+ 0 - 55
tests/TestCase/Utility/GitterLogTest.php

@@ -1,55 +0,0 @@
-<?php
-
-namespace Tools\Test\TestCase\Utility;
-
-use Cake\Core\Configure;
-use Cake\Http\Client;
-use Cake\Http\Client\Request;
-use Psr\Log\LogLevel;
-use Shim\TestSuite\TestCase;
-use Tools\Utility\GitterLog;
-
-/**
- * GitterLogTest class
- */
-class GitterLogTest extends TestCase {
-
-	/**
-	 * @return void
-	 */
-	public function setUp(): void {
-		parent::setUp();
-
-		$key = env('GITTER_KEY') ?: '123';
-		Configure::write('Gitter.key', $key);
-	}
-
-	/**
-	 * @return void
-	 */
-	public function tearDown(): void {
-		parent::tearDown();
-
-		Configure::delete('Gitter.key');
-	}
-
-	/**
-	 * testLogsIntoDefaultFile method
-	 *
-	 * @return void
-	 */
-	public function testLogsIntoDefaultFile(): void {
-		$mockClient = $this->getMockBuilder(Client::class)->onlyMethods(['send'])->getMock();
-
-		$callback = function(Request $value) {
-			return (string)$value->getBody() === 'message=Test%3A+It+%2Aworks%2A+with+some+error+%5Bmarkup%5D%28https%3A%2F%2Fmy-url.com%29%21&level=error';
-		};
-		$mockClient->expects($this->once())->method('send')->with($this->callback($callback));
-
-		$gitterLog = $this->getMockBuilder(GitterLog::class)->onlyMethods(['getClient'])->getMock();
-		$gitterLog->expects($this->once())->method('getClient')->willReturn($mockClient);
-
-		$gitterLog->write('Test: It *works* with some error [markup](https://my-url.com)!', LogLevel::ERROR);
-	}
-
-}

+ 9 - 0
tests/TestCase/Utility/UtilityTest.php

@@ -264,6 +264,15 @@ class UtilityTest extends TestCase {
 	}
 
 	/**
+	 * @covers ::autoPrefixUrl
+	 * @return void
+	 */
+	public function testAutoPrefixUrlWithDetection() {
+		$res = Utility::autoPrefixUrl('www.spiegel.de', null, true);
+		$this->assertSame('https://www.spiegel.de', $res);
+	}
+
+	/**
 	 * @covers ::cleanUrl
 	 * @return void
 	 */