xref: /web-php/src/News/Entry.php (revision 9709f819)
1<?php
2
3namespace phpweb\News;
4
5class Entry
6{
7    public const CATEGORIES = [
8        'frontpage' => 'PHP.net frontpage news',
9        'releases' => 'New PHP release',
10        'conferences' => 'Conference announcement',
11        'cfp' => 'Call for Papers',
12    ];
13
14    public const WEBROOT = "https://www.php.net";
15
16    public const PHPWEB = __DIR__ . '/../../';
17
18    public const ARCHIVE_FILE_REL = 'archive/archive.xml';
19
20    public const ARCHIVE_FILE_ABS = self::PHPWEB . self::ARCHIVE_FILE_REL;
21
22    public const ARCHIVE_ENTRIES_REL = 'archive/entries/';
23
24    public const ARCHIVE_ENTRIES_ABS = self::PHPWEB . self::ARCHIVE_ENTRIES_REL;
25
26    public const IMAGE_PATH_REL = 'images/news/';
27
28    public const IMAGE_PATH_ABS = self::PHPWEB . self::IMAGE_PATH_REL;
29
30    protected $title = '';
31
32    protected $categories = [];
33
34    protected $conf_time = 0;
35
36    protected $image = [];
37
38    protected $content = '';
39
40    protected $id = '';
41
42    public function setTitle(string $title): self {
43        $this->title = $title;
44        return $this;
45    }
46
47    public function setCategories(array $cats): self {
48        foreach ($cats as $cat) {
49            if (!isset(self::CATEGORIES[$cat])) {
50                throw new \Exception("Unknown category: $cat");
51            }
52        }
53        $this->categories = $cats;
54        return $this;
55    }
56
57    public function addCategory(string $cat): self {
58        if (!isset(self::CATEGORIES[$cat])) {
59            throw new \Exception("Unknown category: $cat");
60        }
61        if (!in_array($cat, $this->categories, false)) {
62            $this->categories[] = $cat;
63        }
64        return $this;
65    }
66
67    public function isConference(): bool {
68        return (bool)array_intersect($this->categories, ['cfp', 'conferences']);
69    }
70
71    public function setConfTime(int $time): self {
72        $this->conf_time = $time;
73        return $this;
74    }
75
76    public function setImage(string $path, string $title, ?string $link): self {
77        if (basename($path) !== $path) {
78            throw new \Exception('path must be a simple file name under ' . self::IMAGE_PATH_REL);
79        }
80        if (!file_exists(self::IMAGE_PATH_ABS . $path)) {
81            throw new \Exception('Image not found at web-php/' . self::IMAGE_PATH_REL . $path);
82        }
83        $this->image = [
84            'path' => $path,
85            'title' => $title,
86            'link' => $link,
87        ];
88        return $this;
89    }
90
91    public function setContent(string $content): self {
92        if (empty($content)) {
93            throw new \Exception('Content must not be empty');
94        }
95        $this->content = $content;
96        return $this;
97    }
98
99    public function getId(): string {
100        return $this->id;
101    }
102
103    public function save(): self {
104        if (empty($this->id)) {
105            $this->id = self::selectNextId();
106        }
107
108        // Create the XML document.
109        $dom = new \DOMDocument("1.0", "utf-8");
110        $dom->formatOutput = true;
111        $dom->preserveWhiteSpace = false;
112        $item = $dom->createElementNs("http://www.w3.org/2005/Atom", "entry");
113
114        $href = self::WEBROOT . ($this->isConference() ? '/conferences/index.php' : '/index.php');
115        $archive = self::WEBROOT . "/archive/" . date('Y', $_SERVER['REQUEST_TIME']) . ".php#{$this->id}";
116        $link = ($this->image['link'] ?? null) ?: $archive;
117
118        self::ce($dom, "title", $this->title, [], $item);
119        self::ce($dom, "id", $archive, [], $item);
120        self::ce($dom, "published", date(DATE_ATOM), [], $item);
121        self::ce($dom, "updated", date(DATE_ATOM), [], $item);
122        self::ce($dom, "link", null, ['href' => "{$href}#{$this->id}", "rel" => "alternate", "type" => "text/html"], $item);
123        self::ce($dom, "link", null, ['href' => $link, 'rel' => 'via', 'type' => 'text/html'], $item);
124
125        if (!empty($this->conf_time)) {
126            $item->appendChild($dom->createElementNs("http://php.net/ns/news", "finalTeaserDate", date("Y-m-d", $this->conf_time)));
127        }
128
129        foreach ($this->categories as $cat) {
130            self::ce($dom, "category", null, ['term' => $cat, "label" => self::CATEGORIES[$cat]], $item);
131        }
132
133        if ($this->image['path'] ?? '') {
134            $image = $item->appendChild($dom->createElementNs("http://php.net/ns/news", "newsImage", $this->image['path']));
135            $image->setAttribute("link", $this->image['link']);
136            $image->setAttribute("title", $this->image['title']);
137        }
138
139        $content = self::ce($dom, "content", null, [], $item);
140
141        // Slurp content into our DOM.
142        $tdoc = new \DOMDocument("1.0", "utf-8");
143        $tdoc->formatOutput = true;
144        if ($tdoc->loadXML("<div>{$this->content}    </div>")) {
145            $content->setAttribute("type", "xhtml");
146            $div = $content->appendChild($dom->createElement("div"));
147            $div->setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
148            foreach ($tdoc->firstChild->childNodes as $node) {
149                $div->appendChild($dom->importNode($node, true));
150            }
151        } else {
152            fwrite(STDERR, "There is something wrong with your xhtml, falling back to html");
153            $content->setAttribute("type", "html");
154            $content->nodeValue = $this->content;
155        }
156
157        $dom->appendChild($item);
158        $dom->save(self::ARCHIVE_ENTRIES_ABS . $this->id . ".xml");
159
160        return $this;
161    }
162
163    public function updateArchiveXML(): self {
164        if (empty($this->id)) {
165            throw new \Exception('Entry must be saved before updating archive XML');
166        }
167
168        $arch = new \DOMDocument("1.0", "utf-8");
169        $arch->formatOutput = true;
170        $arch->preserveWhiteSpace = false;
171        $arch->load(self::ARCHIVE_FILE_ABS);
172
173        $first = $arch->createElementNs("http://www.w3.org/2001/XInclude", "xi:include");
174        $first->setAttribute("href", "entries/{$this->id}.xml");
175
176        $second = $arch->getElementsByTagNameNs("http://www.w3.org/2001/XInclude", "include")->item(0);
177        $arch->documentElement->insertBefore($first, $second);
178        $arch->save(self::ARCHIVE_FILE_ABS);
179
180        return $this;
181    }
182
183    private static function selectNextId(): string {
184        $filename = date("Y-m-d", $_SERVER["REQUEST_TIME"]);
185        $count = 0;
186        do {
187            $count++;
188            $id = $filename . "-" . $count;
189            $basename = "{$id}.xml";
190        } while (file_exists(self::ARCHIVE_ENTRIES_ABS . $basename));
191
192        return $id;
193    }
194
195    private static function ce(\DOMDocument $d, string $name, $value, array $attrs = [], ?\DOMNode $to = null) {
196        if ($value) {
197            $n = $d->createElement($name, $value);
198        } else {
199            $n = $d->createElement($name);
200        }
201        foreach ($attrs as $k => $v) {
202            $n->setAttribute($k, $v);
203        }
204        if ($to) {
205            return $to->appendChild($n);
206        }
207        return $n;
208    }
209}
210
211