Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions DemoData/Page/Cypher.wikitext
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,13 @@ MATCH (n:Product) RETURN n.name, n.id

<syntaxhighlight lang="cypher">
MATCH (source:Company)-[r]->(target)
RETURN source.name, target.name, r.id
RETURN source.name, type(r), target.name
LIMIT 10
</syntaxhighlight>

{{#cypher_raw: MATCH (source:Company)-[r]->(target) RETURN source.name, target.name, r.id LIMIT 10}}
{{#cypher_raw: MATCH (source:Company)-[r]->(target) RETURN source.name, type(r), target.name LIMIT 10}}

== Notes ==

* Only read-only queries are allowed. Write operations like <code>CREATE</code>, <code>SET</code>, <code>DELETE</code> are rejected.
* Function calls are currently not supported.
* Query results are displayed as formatted JSON.
2 changes: 1 addition & 1 deletion DemoData/Page/Main_Page.wikitext
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Terminology is explained in [https://github.com/ProfessionalWiki/NeoWiki/blob/ma
'''As a developer:'''

* View Subject JSON: [[Special:NeoJson/ACME_Inc]] (developer UI, normal users will not see JSON. [https://github.com/ProfessionalWiki/NeoWiki/blob/master/docs/SubjectFormat.md View docs])
* Query the graph database: [[Cypher raw example]]
* Query the graph database: [[Cypher|Cypher raw example]]
* [[#REST_API_endpoints|Explore the REST API]]

== Demo pages ==
Expand Down
42 changes: 8 additions & 34 deletions src/CypherQueryFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,14 @@
class CypherQueryFilter {

private const array WRITE_KEYWORDS = [
'CREATE', 'SET', 'DELETE', 'REMOVE', 'MERGE', 'DROP'
'CREATE', 'SET', 'DELETE', 'REMOVE', 'MERGE', 'DROP',
'CALL', 'LOAD', 'FOREACH'
];

public function isReadQuery( string $query ): bool {
$normalizedQuery = $this->normalizeQuery( $query );

if ( $this->containsWriteOperations( $normalizedQuery ) ) {
return false;
}

if ( $this->containsFunctionCalls( $normalizedQuery ) ) {
return false;
}

return true;
return !$this->containsWriteOperations( $normalizedQuery );
}

/**
Expand All @@ -32,10 +25,13 @@ public function isReadQuery( string $query ): bool {
*/
private function normalizeQuery( string $query ): string {
// Remove inline comments
$query = preg_replace( '/\/\/.*$/m', '', $query );
$query = preg_replace( '/\/\/.*$/m', '', $query ) ?? $query;

// Remove multi-line comments
$query = preg_replace( '/\/\*.*?\*\//s', '', $query );
$query = preg_replace( '/\/\*.*?\*\//s', '', $query ) ?? $query;

// Normalize unicode escape sequences that could be used for obfuscation
$query = preg_replace( '/\\\\u[0-9A-Fa-f]{4}/', ' ', $query ) ?? $query;

// Convert to uppercase for easier keyword matching
return strtoupper( $query );
Expand All @@ -59,26 +55,4 @@ private function containsWriteOperations( string $query ): bool {
return false;
}

/**
* Check if the query contains any function calls
*
* @param string $query The normalized query to check
* @return bool True if function calls are found, false otherwise
*/
private function containsFunctionCalls( string $query ): bool {
// Remove string literals to avoid false positives
$queryWithoutStrings = preg_replace( '/([\'"])((?:\\\\\1|.)*?)\1/', '', $query );

// List of common Cypher keywords that might be followed by parentheses but are not functions
$nonFunctionKeywords = [
'MATCH', 'WHERE', 'RETURN', 'WITH', 'UNWIND', 'CASE', 'WHEN', 'THEN', 'ELSE',
'AND', 'OR', 'XOR', 'NOT'
];

// Pattern to match function calls, excluding the common Cypher keywords
$pattern = '/\b(?!(' . implode( '|', $nonFunctionKeywords ) . ')\b)[A-Z][A-Z0-9_]*\s*\(/';

return preg_match( $pattern, $queryWithoutStrings ) === 1;
}

}
Loading