Lines 100.00% 116 / 116
Methods 100.00% 10 / 10
Classes 100.00% 1 / 1
Covered by tests of size
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 getLatest 100.00% 15 / 15 100.00% 1 / 1 7
 getLatestPaginated 100.00% 21 / 21 100.00% 1 / 1 8
 countLatest 100.00% 10 / 10 100.00% 1 / 1 3
 getHeaders 100.00% 11 / 11 100.00% 1 / 1 5
 getById 100.00% 9 / 9 100.00% 1 / 1 4
 insert 100.00% 18 / 18 100.00% 1 / 1 3
 update 100.00% 17 / 17 100.00% 1 / 1 3
 delete 100.00% 7 / 7 100.00% 1 / 1 1
 activate 100.00% 7 / 7 100.00% 1 / 1 2
30final readonly class NewsRepository implements NewsRepositoryInterface
31{
32    public function __construct(
33        private Configuration $configuration,
34    ) {
35    }
36
37    /**
38     * Fetch list of news rows for a language, optionally filtered by active flag and limited.
39     * Rows are ordered by datum DESC.
40     *
41     * @return iterable<stdClass>
42     */
43    public function getLatest(string $language, bool $active = true, ?int $limit = null): iterable
44    {
45        $whereActive = $active ? "AND active = 'y'" : '';
46        $query = sprintf(
47            "SELECT * FROM %sfaqnews WHERE lang = '%s' %s ORDER BY datum DESC",
48            Database::getTablePrefix(),
49            $this->configuration->getDb()->escape($language),
50            $whereActive,
51        );
52        if ($limit !== null) {
53            $query .= sprintf(' LIMIT %d', $limit);
54        }
55
56        $result = $this->configuration->getDb()->query($query);
57        while (true) {
58            $row = $this->configuration->getDb()->fetchObject($result);
59            if ($row === false || $row === null || $row === []) {
60                break;
61            }
62
63            yield $row;
64        }
65    }
66
67    /**
68     * Fetch paginated news rows for a language with sorting support.
69     *
70     * @param string $language Language code
71     * @param bool $active Filter by active status
72     * @param int $limit Number of items per page
73     * @param int $offset Starting offset
74     * @param string $sortField Field to sort by (id, datum, header, author_name)
75     * @param string $sortOrder Sort direction (ASC, DESC)
76     * @return iterable<stdClass>
77     */
78    public function getLatestPaginated(
79        string $language,
80        bool $active = true,
81        int $limit = 25,
82        int $offset = 0,
83        string $sortField = 'datum',
84        string $sortOrder = 'DESC',
85    ): iterable {
86        // Whitelist validation for sort field
87        $allowedSortFields = ['id', 'datum', 'header', 'author_name', 'active'];
88        if (!in_array($sortField, $allowedSortFields, strict: true)) {
89            $sortField = 'datum';
90        }
91
92        // Both sort field and direction are interpolated as SQL identifiers, so the direction
93        // must be allowlisted too — never trust a caller-supplied value here.
94        $sortOrder = strtoupper($sortOrder) === 'ASC' ? 'ASC' : 'DESC';
95
96        $whereActive = $active ? "AND active = 'y'" : '';
97        $query = sprintf(
98            "SELECT * FROM %sfaqnews WHERE lang = '%s' %s ORDER BY %s %s LIMIT %d OFFSET %d",
99            Database::getTablePrefix(),
100            $this->configuration->getDb()->escape($language),
101            $whereActive,
102            $sortField,
103            $sortOrder,
104            $limit,
105            $offset,
106        );
107
108        $result = $this->configuration->getDb()->query($query);
109        while (true) {
110            $row = $this->configuration->getDb()->fetchObject($result);
111            if ($row === false || $row === null || $row === []) {
112                break;
113            }
114
115            yield $row;
116        }
117    }
118
119    /**
120     * Count total news entries for a language.
121     *
122     * @param string $language Language code
123     * @param bool $active Filter by active status
124     * @return int Total count
125     */
126    public function countLatest(string $language, bool $active = true): int
127    {
128        $whereActive = $active ? "AND active = 'y'" : '';
129        $query = sprintf(
130            "SELECT COUNT(*) as total FROM %sfaqnews WHERE lang = '%s' %s",
131            Database::getTablePrefix(),
132            $this->configuration->getDb()->escape($language),
133            $whereActive,
134        );
135
136        $result = $this->configuration->getDb()->query($query);
137        $row = $this->configuration->getDb()->fetchObject($result);
138
139        return $row instanceof \stdClass ? (int) ($row->total ?? 0) : 0;
140    }
141
142    /**
143     * Fetch headers for a language.
144     *
145     * @return iterable<stdClass>
146     */
147    public function getHeaders(string $language): iterable
148    {
149        $query = sprintf(
150            "SELECT id, datum, lang, header, active FROM %sfaqnews WHERE lang = '%s' ORDER BY datum DESC",
151            Database::getTablePrefix(),
152            $this->configuration->getDb()->escape($language),
153        );
154        $result = $this->configuration->getDb()->query($query);
155        while (true) {
156            $row = $this->configuration->getDb()->fetchObject($result);
157            if ($row === false || $row === null || $row === []) {
158                break;
159            }
160
161            yield $row;
162        }
163    }
164
165    public function getById(int $newsId, string $language): ?stdClass
166    {
167        $query = sprintf(
168            "SELECT * FROM %sfaqnews WHERE id = %d AND lang = '%s'",
169            Database::getTablePrefix(),
170            $newsId,
171            $this->configuration->getDb()->escape($language),
172        );
173        $result = $this->configuration->getDb()->query($query);
174        $row = $this->configuration->getDb()->fetchObject($result);
175        return $row !== false && $row !== null && $row !== [] ? $row : null;
176    }
177
178    public function insert(NewsMessage $newsMessage): bool
179    {
180        $id = $this->configuration->getDb()->nextId(Database::getTablePrefix() . 'faqnews', column: 'id');
181        $query = sprintf(
182            "\n            INSERT INTO %sfaqnews\n            (id, datum, lang, header, artikel, author_name, author_email, active, comment, link, linktitel, target)\n            VALUES\n            (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s')",
183            Database::getTablePrefix(),
184            $id,
185            $newsMessage->getCreated()->format(format: 'YmdHis'),
186            $this->configuration->getDb()->escape($newsMessage->getLanguage()),
187            $this->configuration->getDb()->escape($newsMessage->getHeader()),
188            $this->configuration->getDb()->escape($newsMessage->getMessage()),
189            $this->configuration->getDb()->escape($newsMessage->getAuthor()),
190            $this->configuration->getDb()->escape($newsMessage->getEmail()),
191            $newsMessage->isActive() ? 'y' : 'n',
192            $newsMessage->isComment() ? 'y' : 'n',
193            $this->configuration->getDb()->escape($newsMessage->getLink() ?? ''),
194            $this->configuration->getDb()->escape($newsMessage->getLinkTitle() ?? ''),
195            $this->configuration->getDb()->escape($newsMessage->getLinkTarget() ?? ''),
196        );
197        return (bool) $this->configuration->getDb()->query($query);
198    }
199
200    public function update(NewsMessage $newsMessage): bool
201    {
202        $query = sprintf(
203            "\n            UPDATE %sfaqnews SET\n                datum = '%s',\n                lang = '%s',\n                header = '%s',\n                artikel = '%s',\n                author_name = '%s',\n                author_email = '%s',\n                active = '%s',\n                comment = '%s',\n                link = '%s',\n                linktitel = '%s',\n                target = '%s'\n            WHERE id = %d",
204            Database::getTablePrefix(),
205            $newsMessage->getCreated()->format(format: 'YmdHis'),
206            $this->configuration->getDb()->escape($newsMessage->getLanguage()),
207            $this->configuration->getDb()->escape($newsMessage->getHeader()),
208            $this->configuration->getDb()->escape($newsMessage->getMessage()),
209            $this->configuration->getDb()->escape($newsMessage->getAuthor()),
210            $this->configuration->getDb()->escape($newsMessage->getEmail()),
211            $newsMessage->isActive() ? 'y' : 'n',
212            $newsMessage->isComment() ? 'y' : 'n',
213            $this->configuration->getDb()->escape($newsMessage->getLink() ?? ''),
214            $this->configuration->getDb()->escape($newsMessage->getLinkTitle() ?? ''),
215            $this->configuration->getDb()->escape($newsMessage->getLinkTarget() ?? ''),
216            $newsMessage->getId(),
217        );
218        return (bool) $this->configuration->getDb()->query($query);
219    }
220
221    public function delete(int $newsId, string $language): bool
222    {
223        $query = sprintf(
224            "DELETE FROM %sfaqnews WHERE id = %d AND lang = '%s'",
225            Database::getTablePrefix(),
226            $newsId,
227            $this->configuration->getDb()->escape($language),
228        );
229        return (bool) $this->configuration->getDb()->query($query);
230    }
231
232    public function activate(int $newsId, bool $status): bool
233    {
234        $query = sprintf(
235            "UPDATE %sfaqnews SET active = '%s' WHERE id = %d",
236            Database::getTablePrefix(),
237            $status ? 'y' : 'n',
238            $newsId,
239        );
240        return (bool) $this->configuration->getDb()->query($query);
241    }
242}