Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F5970001
Comparator.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
23 KB
Referenced Files
None
Subscribers
None
Comparator.php
View Options
<?php
namespace
Doctrine\DBAL\Schema
;
use
BadMethodCallException
;
use
Doctrine\DBAL\Exception
;
use
Doctrine\DBAL\Platforms\AbstractPlatform
;
use
Doctrine\DBAL\Types
;
use
Doctrine\Deprecations\Deprecation
;
use
function
array_intersect_key
;
use
function
array_key_exists
;
use
function
array_keys
;
use
function
array_map
;
use
function
array_merge
;
use
function
array_unique
;
use
function
assert
;
use
function
count
;
use
function
get_class
;
use
function
sprintf
;
use
function
strtolower
;
/**
* Compares two Schemas and return an instance of SchemaDiff.
*
* @method SchemaDiff compareSchemas(Schema $fromSchema, Schema $toSchema)
*/
class
Comparator
{
private
?
AbstractPlatform
$platform
;
/** @internal The comparator can be only instantiated by a schema manager. */
public
function
__construct
(?
AbstractPlatform
$platform
=
null
)
{
if
(
$platform
===
null
)
{
Deprecation
::
triggerIfCalledFromOutside
(
'doctrine/dbal'
,
'https://github.com/doctrine/dbal/pull/4746'
,
'Not passing a $platform to %s is deprecated.'
.
' Use AbstractSchemaManager::createComparator() to instantiate the comparator.'
,
__METHOD__
,
);
}
$this
->
platform
=
$platform
;
}
/** @param list<mixed> $args */
public
function
__call
(
string
$method
,
array
$args
):
SchemaDiff
{
if
(
$method
!==
'compareSchemas'
)
{
throw
new
BadMethodCallException
(
sprintf
(
'Unknown method "%s"'
,
$method
));
}
return
$this
->
doCompareSchemas
(...
$args
);
}
/** @param list<mixed> $args */
public
static
function
__callStatic
(
string
$method
,
array
$args
):
SchemaDiff
{
if
(
$method
!==
'compareSchemas'
)
{
throw
new
BadMethodCallException
(
sprintf
(
'Unknown method "%s"'
,
$method
));
}
Deprecation
::
trigger
(
'doctrine/dbal'
,
'https://github.com/doctrine/dbal/pull/4707'
,
'Calling %s::%s() statically is deprecated.'
,
self
::
class
,
$method
,
);
$comparator
=
new
self
();
return
$comparator
->
doCompareSchemas
(...
$args
);
}
/**
* Returns a SchemaDiff object containing the differences between the schemas $fromSchema and $toSchema.
*
* This method should be called non-statically since it will be declared as non-static in the next major release.
*
* @return SchemaDiff
*
* @throws SchemaException
*/
private
function
doCompareSchemas
(
Schema
$fromSchema
,
Schema
$toSchema
)
{
$createdSchemas
=
[];
$droppedSchemas
=
[];
$createdTables
=
[];
$alteredTables
=
[];
$droppedTables
=
[];
$createdSequences
=
[];
$alteredSequences
=
[];
$droppedSequences
=
[];
$orphanedForeignKeys
=
[];
$foreignKeysToTable
=
[];
foreach
(
$toSchema
->
getNamespaces
()
as
$namespace
)
{
if
(
$fromSchema
->
hasNamespace
(
$namespace
))
{
continue
;
}
$createdSchemas
[
$namespace
]
=
$namespace
;
}
foreach
(
$fromSchema
->
getNamespaces
()
as
$namespace
)
{
if
(
$toSchema
->
hasNamespace
(
$namespace
))
{
continue
;
}
$droppedSchemas
[
$namespace
]
=
$namespace
;
}
foreach
(
$toSchema
->
getTables
()
as
$table
)
{
$tableName
=
$table
->
getShortestName
(
$toSchema
->
getName
());
if
(!
$fromSchema
->
hasTable
(
$tableName
))
{
$createdTables
[
$tableName
]
=
$toSchema
->
getTable
(
$tableName
);
}
else
{
$tableDifferences
=
$this
->
diffTable
(
$fromSchema
->
getTable
(
$tableName
),
$toSchema
->
getTable
(
$tableName
),
);
if
(
$tableDifferences
!==
false
)
{
$alteredTables
[
$tableName
]
=
$tableDifferences
;
}
}
}
/* Check if there are tables removed */
foreach
(
$fromSchema
->
getTables
()
as
$table
)
{
$tableName
=
$table
->
getShortestName
(
$fromSchema
->
getName
());
$table
=
$fromSchema
->
getTable
(
$tableName
);
if
(!
$toSchema
->
hasTable
(
$tableName
))
{
$droppedTables
[
$tableName
]
=
$table
;
}
// also remember all foreign keys that point to a specific table
foreach
(
$table
->
getForeignKeys
()
as
$foreignKey
)
{
$foreignTable
=
strtolower
(
$foreignKey
->
getForeignTableName
());
if
(!
isset
(
$foreignKeysToTable
[
$foreignTable
]))
{
$foreignKeysToTable
[
$foreignTable
]
=
[];
}
$foreignKeysToTable
[
$foreignTable
][]
=
$foreignKey
;
}
}
foreach
(
$droppedTables
as
$tableName
=>
$table
)
{
if
(!
isset
(
$foreignKeysToTable
[
$tableName
]))
{
continue
;
}
foreach
(
$foreignKeysToTable
[
$tableName
]
as
$foreignKey
)
{
if
(
isset
(
$droppedTables
[
strtolower
(
$foreignKey
->
getLocalTableName
())]))
{
continue
;
}
$orphanedForeignKeys
[]
=
$foreignKey
;
}
// deleting duplicated foreign keys present on both on the orphanedForeignKey
// and the removedForeignKeys from changedTables
foreach
(
$foreignKeysToTable
[
$tableName
]
as
$foreignKey
)
{
// strtolower the table name to make if compatible with getShortestName
$localTableName
=
strtolower
(
$foreignKey
->
getLocalTableName
());
if
(!
isset
(
$alteredTables
[
$localTableName
]))
{
continue
;
}
foreach
(
$alteredTables
[
$localTableName
]->
getDroppedForeignKeys
()
as
$droppedForeignKey
)
{
assert
(
$droppedForeignKey
instanceof
ForeignKeyConstraint
);
// We check if the key is from the removed table if not we skip.
if
(
$tableName
!==
strtolower
(
$droppedForeignKey
->
getForeignTableName
()))
{
continue
;
}
$alteredTables
[
$localTableName
]->
unsetDroppedForeignKey
(
$droppedForeignKey
);
}
}
}
foreach
(
$toSchema
->
getSequences
()
as
$sequence
)
{
$sequenceName
=
$sequence
->
getShortestName
(
$toSchema
->
getName
());
if
(!
$fromSchema
->
hasSequence
(
$sequenceName
))
{
if
(!
$this
->
isAutoIncrementSequenceInSchema
(
$fromSchema
,
$sequence
))
{
$createdSequences
[]
=
$sequence
;
}
}
else
{
if
(
$this
->
diffSequence
(
$sequence
,
$fromSchema
->
getSequence
(
$sequenceName
)))
{
$alteredSequences
[]
=
$toSchema
->
getSequence
(
$sequenceName
);
}
}
}
foreach
(
$fromSchema
->
getSequences
()
as
$sequence
)
{
if
(
$this
->
isAutoIncrementSequenceInSchema
(
$toSchema
,
$sequence
))
{
continue
;
}
$sequenceName
=
$sequence
->
getShortestName
(
$fromSchema
->
getName
());
if
(
$toSchema
->
hasSequence
(
$sequenceName
))
{
continue
;
}
$droppedSequences
[]
=
$sequence
;
}
$diff
=
new
SchemaDiff
(
$createdTables
,
$alteredTables
,
$droppedTables
,
$fromSchema
,
$createdSchemas
,
$droppedSchemas
,
$createdSequences
,
$alteredSequences
,
$droppedSequences
,
);
$diff
->
orphanedForeignKeys
=
$orphanedForeignKeys
;
return
$diff
;
}
/**
* @deprecated Use non-static call to {@see compareSchemas()} instead.
*
* @return SchemaDiff
*
* @throws SchemaException
*/
public
function
compare
(
Schema
$fromSchema
,
Schema
$toSchema
)
{
Deprecation
::
trigger
(
'doctrine/dbal'
,
'https://github.com/doctrine/dbal/pull/4707'
,
'Method compare() is deprecated. Use a non-static call to compareSchemas() instead.'
,
);
return
$this
->
compareSchemas
(
$fromSchema
,
$toSchema
);
}
/**
* @param Schema $schema
* @param Sequence $sequence
*/
private
function
isAutoIncrementSequenceInSchema
(
$schema
,
$sequence
):
bool
{
foreach
(
$schema
->
getTables
()
as
$table
)
{
if
(
$sequence
->
isAutoIncrementsFor
(
$table
))
{
return
true
;
}
}
return
false
;
}
/** @return bool */
public
function
diffSequence
(
Sequence
$sequence1
,
Sequence
$sequence2
)
{
if
(
$sequence1
->
getAllocationSize
()
!==
$sequence2
->
getAllocationSize
())
{
return
true
;
}
return
$sequence1
->
getInitialValue
()
!==
$sequence2
->
getInitialValue
();
}
/**
* Returns the difference between the tables $fromTable and $toTable.
*
* If there are no differences this method returns the boolean false.
*
* @deprecated Use {@see compareTables()} and, optionally, {@see TableDiff::isEmpty()} instead.
*
* @return TableDiff|false
*
* @throws Exception
*/
public
function
diffTable
(
Table
$fromTable
,
Table
$toTable
)
{
Deprecation
::
triggerIfCalledFromOutside
(
'doctrine/dbal'
,
'https://github.com/doctrine/dbal/pull/5770'
,
'%s is deprecated. Use compareTables() instead.'
,
__METHOD__
,
);
$diff
=
$this
->
compareTables
(
$fromTable
,
$toTable
);
if
(
$diff
->
isEmpty
())
{
return
false
;
}
return
$diff
;
}
/**
* Compares the tables and returns the difference between them.
*
* @throws Exception
*/
public
function
compareTables
(
Table
$fromTable
,
Table
$toTable
):
TableDiff
{
$addedColumns
=
[];
$modifiedColumns
=
[];
$droppedColumns
=
[];
$addedIndexes
=
[];
$modifiedIndexes
=
[];
$droppedIndexes
=
[];
$addedForeignKeys
=
[];
$modifiedForeignKeys
=
[];
$droppedForeignKeys
=
[];
$fromTableColumns
=
$fromTable
->
getColumns
();
$toTableColumns
=
$toTable
->
getColumns
();
/* See if all the columns in "from" table exist in "to" table */
foreach
(
$toTableColumns
as
$columnName
=>
$column
)
{
if
(
$fromTable
->
hasColumn
(
$columnName
))
{
continue
;
}
$addedColumns
[
$columnName
]
=
$column
;
}
/* See if there are any removed columns in "to" table */
foreach
(
$fromTableColumns
as
$columnName
=>
$column
)
{
// See if column is removed in "to" table.
if
(!
$toTable
->
hasColumn
(
$columnName
))
{
$droppedColumns
[
$columnName
]
=
$column
;
continue
;
}
$toColumn
=
$toTable
->
getColumn
(
$columnName
);
// See if column has changed properties in "to" table.
$changedProperties
=
$this
->
diffColumn
(
$column
,
$toColumn
);
if
(
$this
->
platform
!==
null
)
{
if
(
$this
->
columnsEqual
(
$column
,
$toColumn
))
{
continue
;
}
}
elseif
(
count
(
$changedProperties
)
===
0
)
{
continue
;
}
$modifiedColumns
[
$column
->
getName
()]
=
new
ColumnDiff
(
$column
->
getName
(),
$toColumn
,
$changedProperties
,
$column
,
);
}
$renamedColumns
=
$this
->
detectRenamedColumns
(
$addedColumns
,
$droppedColumns
);
$fromTableIndexes
=
$fromTable
->
getIndexes
();
$toTableIndexes
=
$toTable
->
getIndexes
();
/* See if all the indexes in "from" table exist in "to" table */
foreach
(
$toTableIndexes
as
$indexName
=>
$index
)
{
if
((
$index
->
isPrimary
()
&&
$fromTable
->
getPrimaryKey
()
!==
null
)
||
$fromTable
->
hasIndex
(
$indexName
))
{
continue
;
}
$addedIndexes
[
$indexName
]
=
$index
;
}
/* See if there are any removed indexes in "to" table */
foreach
(
$fromTableIndexes
as
$indexName
=>
$index
)
{
// See if index is removed in "to" table.
if
(
(
$index
->
isPrimary
()
&&
$toTable
->
getPrimaryKey
()
===
null
)
||
!
$index
->
isPrimary
()
&&
!
$toTable
->
hasIndex
(
$indexName
)
)
{
$droppedIndexes
[
$indexName
]
=
$index
;
continue
;
}
// See if index has changed in "to" table.
$toTableIndex
=
$index
->
isPrimary
()
?
$toTable
->
getPrimaryKey
()
:
$toTable
->
getIndex
(
$indexName
);
assert
(
$toTableIndex
instanceof
Index
);
if
(!
$this
->
diffIndex
(
$index
,
$toTableIndex
))
{
continue
;
}
$modifiedIndexes
[
$indexName
]
=
$toTableIndex
;
}
$renamedIndexes
=
$this
->
detectRenamedIndexes
(
$addedIndexes
,
$droppedIndexes
);
$fromForeignKeys
=
$fromTable
->
getForeignKeys
();
$toForeignKeys
=
$toTable
->
getForeignKeys
();
foreach
(
$fromForeignKeys
as
$fromKey
=>
$fromConstraint
)
{
foreach
(
$toForeignKeys
as
$toKey
=>
$toConstraint
)
{
if
(
$this
->
diffForeignKey
(
$fromConstraint
,
$toConstraint
)
===
false
)
{
unset
(
$fromForeignKeys
[
$fromKey
],
$toForeignKeys
[
$toKey
]);
}
else
{
if
(
strtolower
(
$fromConstraint
->
getName
())
===
strtolower
(
$toConstraint
->
getName
()))
{
$modifiedForeignKeys
[]
=
$toConstraint
;
unset
(
$fromForeignKeys
[
$fromKey
],
$toForeignKeys
[
$toKey
]);
}
}
}
}
foreach
(
$fromForeignKeys
as
$fromConstraint
)
{
$droppedForeignKeys
[]
=
$fromConstraint
;
}
foreach
(
$toForeignKeys
as
$toConstraint
)
{
$addedForeignKeys
[]
=
$toConstraint
;
}
return
new
TableDiff
(
$toTable
->
getName
(),
$addedColumns
,
$modifiedColumns
,
$droppedColumns
,
$addedIndexes
,
$modifiedIndexes
,
$droppedIndexes
,
$fromTable
,
$addedForeignKeys
,
$modifiedForeignKeys
,
$droppedForeignKeys
,
$renamedColumns
,
$renamedIndexes
,
);
}
/**
* Try to find columns that only changed their name, rename operations maybe cheaper than add/drop
* however ambiguities between different possibilities should not lead to renaming at all.
*
* @param array<string,Column> $addedColumns
* @param array<string,Column> $removedColumns
*
* @return array<string,Column>
*
* @throws Exception
*/
private
function
detectRenamedColumns
(
array
&
$addedColumns
,
array
&
$removedColumns
):
array
{
$candidatesByName
=
[];
foreach
(
$addedColumns
as
$addedColumnName
=>
$addedColumn
)
{
foreach
(
$removedColumns
as
$removedColumn
)
{
if
(!
$this
->
columnsEqual
(
$addedColumn
,
$removedColumn
))
{
continue
;
}
$candidatesByName
[
$addedColumn
->
getName
()][]
=
[
$removedColumn
,
$addedColumn
,
$addedColumnName
];
}
}
$renamedColumns
=
[];
foreach
(
$candidatesByName
as
$candidates
)
{
if
(
count
(
$candidates
)
!==
1
)
{
continue
;
}
[
$removedColumn
,
$addedColumn
]
=
$candidates
[
0
];
$removedColumnName
=
$removedColumn
->
getName
();
$addedColumnName
=
strtolower
(
$addedColumn
->
getName
());
if
(
isset
(
$renamedColumns
[
$removedColumnName
]))
{
continue
;
}
$renamedColumns
[
$removedColumnName
]
=
$addedColumn
;
unset
(
$addedColumns
[
$addedColumnName
],
$removedColumns
[
strtolower
(
$removedColumnName
)],
);
}
return
$renamedColumns
;
}
/**
* Try to find indexes that only changed their name, rename operations maybe cheaper than add/drop
* however ambiguities between different possibilities should not lead to renaming at all.
*
* @param array<string,Index> $addedIndexes
* @param array<string,Index> $removedIndexes
*
* @return array<string,Index>
*/
private
function
detectRenamedIndexes
(
array
&
$addedIndexes
,
array
&
$removedIndexes
):
array
{
$candidatesByName
=
[];
// Gather possible rename candidates by comparing each added and removed index based on semantics.
foreach
(
$addedIndexes
as
$addedIndexName
=>
$addedIndex
)
{
foreach
(
$removedIndexes
as
$removedIndex
)
{
if
(
$this
->
diffIndex
(
$addedIndex
,
$removedIndex
))
{
continue
;
}
$candidatesByName
[
$addedIndex
->
getName
()][]
=
[
$removedIndex
,
$addedIndex
,
$addedIndexName
];
}
}
$renamedIndexes
=
[];
foreach
(
$candidatesByName
as
$candidates
)
{
// If the current rename candidate contains exactly one semantically equal index,
// we can safely rename it.
// Otherwise, it is unclear if a rename action is really intended,
// therefore we let those ambiguous indexes be added/dropped.
if
(
count
(
$candidates
)
!==
1
)
{
continue
;
}
[
$removedIndex
,
$addedIndex
]
=
$candidates
[
0
];
$removedIndexName
=
strtolower
(
$removedIndex
->
getName
());
$addedIndexName
=
strtolower
(
$addedIndex
->
getName
());
if
(
isset
(
$renamedIndexes
[
$removedIndexName
]))
{
continue
;
}
$renamedIndexes
[
$removedIndexName
]
=
$addedIndex
;
unset
(
$addedIndexes
[
$addedIndexName
],
$removedIndexes
[
$removedIndexName
],
);
}
return
$renamedIndexes
;
}
/**
* @internal The method should be only used from within the {@see Comparator} class hierarchy.
*
* @return bool
*/
public
function
diffForeignKey
(
ForeignKeyConstraint
$key1
,
ForeignKeyConstraint
$key2
)
{
if
(
array_map
(
'strtolower'
,
$key1
->
getUnquotedLocalColumns
())
!==
array_map
(
'strtolower'
,
$key2
->
getUnquotedLocalColumns
())
)
{
return
true
;
}
if
(
array_map
(
'strtolower'
,
$key1
->
getUnquotedForeignColumns
())
!==
array_map
(
'strtolower'
,
$key2
->
getUnquotedForeignColumns
())
)
{
return
true
;
}
if
(
$key1
->
getUnqualifiedForeignTableName
()
!==
$key2
->
getUnqualifiedForeignTableName
())
{
return
true
;
}
if
(
$key1
->
onUpdate
()
!==
$key2
->
onUpdate
())
{
return
true
;
}
return
$key1
->
onDelete
()
!==
$key2
->
onDelete
();
}
/**
* Compares the definitions of the given columns
*
* @internal The method should be only used from within the {@see Comparator} class hierarchy.
*
* @throws Exception
*/
public
function
columnsEqual
(
Column
$column1
,
Column
$column2
):
bool
{
if
(
$this
->
platform
===
null
)
{
return
$this
->
diffColumn
(
$column1
,
$column2
)
===
[];
}
return
$this
->
platform
->
columnsEqual
(
$column1
,
$column2
);
}
/**
* Returns the difference between the columns
*
* If there are differences this method returns the changed properties as a
* string array, otherwise an empty array gets returned.
*
* @deprecated Use {@see columnsEqual()} instead.
*
* @return string[]
*/
public
function
diffColumn
(
Column
$column1
,
Column
$column2
)
{
Deprecation
::
triggerIfCalledFromOutside
(
'doctrine/dbal'
,
'https://github.com/doctrine/dbal/pull/5650'
,
'%s is deprecated. Use diffTable() instead.'
,
__METHOD__
,
);
$properties1
=
$column1
->
toArray
();
$properties2
=
$column2
->
toArray
();
$changedProperties
=
[];
if
(
get_class
(
$properties1
[
'type'
])
!==
get_class
(
$properties2
[
'type'
]))
{
$changedProperties
[]
=
'type'
;
}
foreach
([
'notnull'
,
'unsigned'
,
'autoincrement'
]
as
$property
)
{
if
(
$properties1
[
$property
]
===
$properties2
[
$property
])
{
continue
;
}
$changedProperties
[]
=
$property
;
}
// Null values need to be checked additionally as they tell whether to create or drop a default value.
// null != 0, null != false, null != '' etc. This affects platform's table alteration SQL generation.
if
(
(
$properties1
[
'default'
]
===
null
)
!==
(
$properties2
[
'default'
]
===
null
)
||
$properties1
[
'default'
]
!=
$properties2
[
'default'
]
)
{
$changedProperties
[]
=
'default'
;
}
if
(
(
$properties1
[
'type'
]
instanceof
Types\StringType
&&
!
$properties1
[
'type'
]
instanceof
Types\GuidType
)
||
$properties1
[
'type'
]
instanceof
Types\BinaryType
)
{
// check if value of length is set at all, default value assumed otherwise.
$length1
=
$properties1
[
'length'
]
??
255
;
$length2
=
$properties2
[
'length'
]
??
255
;
if
(
$length1
!==
$length2
)
{
$changedProperties
[]
=
'length'
;
}
if
(
$properties1
[
'fixed'
]
!==
$properties2
[
'fixed'
])
{
$changedProperties
[]
=
'fixed'
;
}
}
elseif
(
$properties1
[
'type'
]
instanceof
Types\DecimalType
)
{
if
((
$properties1
[
'precision'
]
??
10
)
!==
(
$properties2
[
'precision'
]
??
10
))
{
$changedProperties
[]
=
'precision'
;
}
if
(
$properties1
[
'scale'
]
!==
$properties2
[
'scale'
])
{
$changedProperties
[]
=
'scale'
;
}
}
// A null value and an empty string are actually equal for a comment so they should not trigger a change.
if
(
$properties1
[
'comment'
]
!==
$properties2
[
'comment'
]
&&
!
(
$properties1
[
'comment'
]
===
null
&&
$properties2
[
'comment'
]
===
''
)
&&
!
(
$properties2
[
'comment'
]
===
null
&&
$properties1
[
'comment'
]
===
''
)
)
{
$changedProperties
[]
=
'comment'
;
}
$customOptions1
=
$column1
->
getCustomSchemaOptions
();
$customOptions2
=
$column2
->
getCustomSchemaOptions
();
foreach
(
array_merge
(
array_keys
(
$customOptions1
),
array_keys
(
$customOptions2
))
as
$key
)
{
if
(!
array_key_exists
(
$key
,
$properties1
)
||
!
array_key_exists
(
$key
,
$properties2
))
{
$changedProperties
[]
=
$key
;
}
elseif
(
$properties1
[
$key
]
!==
$properties2
[
$key
])
{
$changedProperties
[]
=
$key
;
}
}
$platformOptions1
=
$column1
->
getPlatformOptions
();
$platformOptions2
=
$column2
->
getPlatformOptions
();
foreach
(
array_keys
(
array_intersect_key
(
$platformOptions1
,
$platformOptions2
))
as
$key
)
{
if
(
$properties1
[
$key
]
===
$properties2
[
$key
])
{
continue
;
}
$changedProperties
[]
=
$key
;
}
return
array_unique
(
$changedProperties
);
}
/**
* Finds the difference between the indexes $index1 and $index2.
*
* Compares $index1 with $index2 and returns true if there are any
* differences or false in case there are no differences.
*
* @internal The method should be only used from within the {@see Comparator} class hierarchy.
*
* @return bool
*/
public
function
diffIndex
(
Index
$index1
,
Index
$index2
)
{
return
!
(
$index1
->
isFulfilledBy
(
$index2
)
&&
$index2
->
isFulfilledBy
(
$index1
));
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sat, Oct 3, 14:09 (9 h, 35 m ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
81/51/12dacd55e93a473419efbea758d7
Default Alt Text
Comparator.php (23 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment