Completed
Push — master ( 92ff78...57630a )
by Steve
20:35 queued 09:38
created

ElggFileCache::clear()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
cc 5
eloc 11
nc 5
nop 0
dl 0
loc 19
ccs 0
cts 10
cp 0
crap 30
rs 8.8571
c 0
b 0
f 0
1
<?php
2
/**
3
 * \ElggFileCache
4
 * Store cached data in a file store.
5
 *
6
 * @package    Elgg.Core
7
 * @subpackage Caches
8
 */
9
class ElggFileCache extends \ElggCache {
10
	/**
11
	 * Set the Elgg cache.
12
	 *
13
	 * @param string $cache_path The cache path.
14
	 * @param int    $max_age    Maximum age in seconds, 0 if no limit.
15
	 * @param int    $max_size   Maximum size of cache in seconds, 0 if no limit.
16
	 *
17
	 * @throws ConfigurationException
18
	 */
19 1
	public function __construct($cache_path, $max_age = 0, $max_size = 0) {
20 1
		$this->setVariable("cache_path", $cache_path);
21 1
		$this->setVariable("max_age", $max_age);
22 1
		$this->setVariable("max_size", $max_size);
23
24 1
		if ($cache_path == "") {
25
			throw new \ConfigurationException("Cache path set to nothing!");
26
		}
27 1
	}
28
29
	/**
30
	 * Create and return a handle to a file.
31
	 *
32
	 * @param string $filename Filename to save as
33
	 * @param string $rw       Write mode
34
	 *
35
	 * @return mixed
36
	 */
37
	protected function createFile($filename, $rw = "rb") {
38
		// Create a filename matrix
39
		$matrix = "";
40
		$depth = strlen($filename);
41
		if ($depth > 5) {
42
			$depth = 5;
0 ignored issues
show
Unused Code introduced by
$depth is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
43
		}
44
45
		// Create full path
46
		$path = $this->getVariable("cache_path") . $matrix;
47
		if (!is_dir($path)) {
48
			mkdir($path, 0700, true);
49
		}
50
51
		// Open the file
52
		if ((!file_exists($path . $filename)) && ($rw == "rb")) {
53
			return false;
54
		}
55
56
		return fopen($path . $filename, $rw);
57
	}
58
59
	/**
60
	 * Create a sanitised filename for the file.
61
	 *
62
	 * @param string $key The filename
63
	 *
64
	 * @return string
65
	 */
66
	protected function sanitizeFilename($key) {
67
		// handles all keys in use by core
68
		if (preg_match('~^[a-zA-Z0-9\-_\.]{1,250}$~', $key)) {
69
			return $key;
70
		}
71
72
		$key = md5($key);
73
74
		// prevent collision with un-hashed keys
75
		$key = "=" . $key;
76
77
		return $key;
78
	}
79
80
	/**
81
	 * Save a key
82
	 *
83
	 * @param string $key  Name
84
	 * @param string $data Value
85
	 *
86
	 * @return boolean
87
	 */
88
	public function save($key, $data) {
89
		$f = $this->createFile($this->sanitizeFilename($key), "wb");
90
		if ($f) {
91
			$result = fwrite($f, $data);
92
			fclose($f);
93
94
			return $result;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $result; (integer) is incompatible with the return type declared by the abstract method ElggCache::save of type boolean.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
95
		}
96
97
		return false;
98
	}
99
100
	/**
101
	 * Load a key
102
	 *
103
	 * @param string $key    Name
104
	 * @param int    $offset Offset
105
	 * @param int    $limit  Limit
106
	 *
107
	 * @return string
108
	 */
109
	public function load($key, $offset = 0, $limit = null) {
110
		$f = $this->createFile($this->sanitizeFilename($key));
111
		if ($f) {
112
			if (!$limit) {
1 ignored issue
show
Bug Best Practice introduced by
The expression $limit of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
113
				$limit = -1;
114
			}
115
116
			$data = stream_get_contents($f, $limit, $offset);
117
118
			fclose($f);
119
120
			return $data;
121
		}
122
123
		return false;
124
	}
125
126
	/**
127
	 * Invalidate a given key.
128
	 *
129
	 * @param string $key Name
130
	 *
131
	 * @return bool
132
	 */
133
	public function delete($key) {
134
		$dir = $this->getVariable("cache_path");
135
136
		if (file_exists($dir . $key)) {
137
			return unlink($dir . $key);
138
		}
139
		return true;
140
	}
141
142
	/**
143
	 * Delete all files in the directory of this file cache
144
	 *
145
	 * @return void
146
	 */
147
	public function clear() {
148
		$dir = $this->getVariable("cache_path");
149
		if (!is_dir($dir)) {
150
			return;
151
		}
152
153
		$exclude = [".", ".."];
154
155
		$files = scandir($dir);
156
		if (!$files) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $files of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
157
			return;
158
		}
159
160
		foreach ($files as $f) {
161
			if (!in_array($f, $exclude)) {
162
				unlink($dir . $f);
163
			}
164
		}
165
	}
166
167
	/**
168
	 * Preform cleanup and invalidates cache upon object destruction
169
	 *
170
	 * @throws IOException
171
	 */
172
	public function __destruct() {
173
		// @todo Check size and age, clean up accordingly
174
		$size = 0;
175
		$dir = $this->getVariable("cache_path");
176
177
		// Short circuit if both size and age are unlimited
178
		if (($this->getVariable("max_age") == 0) && ($this->getVariable("max_size") == 0)) {
179
			return;
180
		}
181
182
		$exclude = [".", ".."];
183
184
		$files = scandir($dir);
185
		if (!$files) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $files of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
186
			throw new \IOException($dir . " is not a directory.");
187
		}
188
189
		// Perform cleanup
190
		foreach ($files as $f) {
191
			if (!in_array($f, $exclude)) {
192
				$stat = stat($dir . $f);
193
194
				// Add size
195
				$size .= $stat['size'];
196
197
				// Is this older than my maximum date?
198
				if (($this->getVariable("max_age") > 0) && (time() - $stat['mtime'] > $this->getVariable("max_age"))) {
199
					unlink($dir . $f);
200
				}
201
202
				// @todo Size
203
			}
204
		}
205
	}
206
}
207