1<?php 2 3namespace dokuwiki\Search\Index; 4 5use dokuwiki\Logger; 6use dokuwiki\Search\Exception\IndexLockException; 7use dokuwiki\Search\Exception\IndexWriteException; 8 9/** 10 * Access to a single index file 11 * 12 * Access using this class always happens by loading the full index into memory. 13 * Changes can be made permanent explicitly via save(), but will also be 14 * auto-saved on destruction to prevent data loss when indexes are used in tandem 15 * (a new RID in one index may already be referenced by another). 16 * Should be used for small indexes that receive many changes at once. 17 */ 18class MemoryIndex extends AbstractIndex 19{ 20 /** @var string the raw data lines of the index, no newlines */ 21 protected $data; 22 23 /** @var bool has the index been modified? */ 24 protected $dirty = false; 25 26 /** 27 * Loads the full contents of the index into memory 28 * 29 * @inheritdoc 30 */ 31 public function __construct($idx, $suffix = '', $isWritable = false) 32 { 33 parent::__construct($idx, $suffix, $isWritable); 34 35 $this->data = []; 36 if (!file_exists($this->filename)) { 37 return; 38 } 39 $this->data = file($this->filename, FILE_IGNORE_NEW_LINES); 40 } 41 42 /** 43 * Auto-save dirty data before releasing the lock 44 * 45 * When indexes are used in tandem, a new RID written to one index may already 46 * be referenced by other indexes that were saved. Losing unsaved data here 47 * would leave dangling references, causing silent index corruption. 48 * 49 * The try/catch is necessary because unlock() is called from __destruct() 50 * (in the parent class), and PHP destructors must not throw — a throw 51 * during exception unwinding causes a fatal error. 52 * 53 * @inheritdoc 54 */ 55 public function unlock() 56 { 57 if ($this->isDirty()) { 58 try { 59 $this->save(); 60 } catch (\Exception $e) { 61 Logger::error('MemoryIndex failed to save on unlock: ' . $e->getMessage()); 62 } 63 } 64 parent::unlock(); 65 } 66 67 /** 68 * @inheritdoc 69 * @throws IndexLockException 70 */ 71 public function changeRow($rid, $value) 72 { 73 if (!$this->isWritable) throw new IndexLockException(); 74 75 if ($rid > count($this->data)) { 76 $this->data = array_pad($this->data, $rid, ''); 77 } 78 $this->data[$rid] = $value; 79 $this->dirty = true; 80 } 81 82 /** 83 * @inheritdoc 84 * @throws IndexLockException 85 */ 86 public function retrieveRow($rid) 87 { 88 if (isset($this->data[$rid])) { 89 return $this->data[$rid]; 90 } 91 if ($this->isWritable) { 92 $this->changeRow($rid, ''); // add to index 93 } 94 return ''; 95 } 96 97 /** @inheritdoc */ 98 public function retrieveRows($rids) 99 { 100 $result = []; 101 foreach ($rids as $rid) { 102 if (isset($this->data[$rid])) $result[$rid] = $this->data[$rid]; 103 } 104 105 return $result; 106 } 107 108 /** @inheritdoc */ 109 public function getRowIDs($values) 110 { 111 $values = array_map('trim', $values); 112 $values = array_fill_keys($values, 1); // easier access as associative array 113 114 $result = []; 115 $count = count($this->data); 116 for ($ln = 0; $ln < $count; $ln++) { 117 $line = $this->data[$ln]; 118 if (isset($values[$line])) { 119 $result[$line] = $ln; 120 unset($values[$line]); 121 } 122 } 123 124 if (!$this->isWritable) return $result; 125 126 // if there are still values, they have not been found and will be appended 127 foreach (array_keys($values) as $value) { 128 $this->data[] = $value; 129 $result[$value] = $ln++; 130 $this->dirty = true; 131 } 132 133 return $result; 134 } 135 136 /** @inheritdoc */ 137 public function search($re) 138 { 139 return preg_grep($re, $this->data); 140 } 141 142 /** 143 * Save the changed index back to its file 144 * 145 * The method will check the internal dirty state and will only write when the index has actually been changed 146 * 147 * @throws IndexWriteException 148 * @throws IndexLockException 149 */ 150 public function save() 151 { 152 global $conf; 153 154 if (!$this->isDirty()) { 155 return; 156 } 157 158 if (!$this->isWritable) throw new IndexLockException(); 159 160 $tempname = $this->filename . '.tmp'; 161 162 $fh = @fopen($tempname, 'w'); 163 if (!$fh) { 164 throw new IndexWriteException("Failed to write $tempname"); 165 } 166 fwrite($fh, implode("\n", $this->data)); 167 if (count($this->data)) { 168 fwrite($fh, "\n"); 169 } 170 fclose($fh); 171 172 if ($conf['fperm']) { 173 chmod($tempname, $conf['fperm']); 174 } 175 176 if (!io_rename($tempname, $this->filename)) { 177 throw new IndexWriteException("Failed to write {$this->filename}"); 178 } 179 180 $this->dirty = false; 181 } 182 183 /** 184 * Check if the index has been modified and needs to be saved 185 * @return bool 186 */ 187 public function isDirty() 188 { 189 return $this->dirty; 190 } 191 192 /** @inheritdoc */ 193 public function count(): int 194 { 195 return count($this->data); 196 } 197 198 /** @inheritdoc */ 199 public function getIterator(): \ArrayIterator 200 { 201 return new \ArrayIterator($this->data); 202 } 203} 204