1: <?php
2: /**
3: * Class Logentry
4: */
5:
6: namespace Jlab\Eloglib;
7:
8: use Dotenv\Dotenv;
9: use \XMLWriter;
10:
11: /**
12: * Class Logentry
13: *
14: * An electronic log book log entry.
15: *
16: * @package Jlab\Eloglib
17: *
18: *
19: * Significant changes from Logentry class in elog Drupal module:
20: * - $body_type renamed to $bodyType
21: *
22: */
23: class Logentry
24: {
25:
26: /**
27: * @var Dotenv
28: */
29: protected $config;
30:
31:
32: /**
33: * The log number
34: *
35: * This number is null for a new entry and non-null for an entry retrieved
36: * from the logbook server.
37: *
38: * @var integer
39: */
40: protected $lognumber;
41:
42: /**
43: * The log entry title.
44: * 255 character limit imposed by Drupal database.
45: * @var string
46: */
47: protected $title;
48:
49: /**
50: * The author of the logentry
51: * @var \stdClass
52: */
53: protected $author;
54:
55: /**
56: * The log entry time
57: * Needs to be in ISO 8601 date format (ex: 2004-02-12T15:19:21+00:00)
58: * @var string
59: */
60: protected $created;
61:
62: /**
63: * Whether the logentry should be sticky at the top of lists
64: * @var integer (0/1 representing boolean)
65: */
66: protected $sticky;
67:
68: /**
69: * The log entry body
70: * @var string
71: */
72: protected $body;
73:
74:
75: /**
76: * Indicates the formatting of the text in $body.
77: *
78: * Valid values will correspond to text formats defined in Drupal
79: * examples: plain_text, filtered_html, full_html, etc.
80: *
81: * @var string
82: */
83: protected $bodyType;
84:
85: /**
86: * The list of logbooks for the entry
87: * @var array
88: */
89: protected $logbooks = array();
90:
91: /**
92: * The list of attachments for the entry
93: * @var array
94: */
95: protected $attachments = array();
96:
97: /**
98: * The list of users credited with making the entry
99: * @var array of stdClass {username, firstname, lastname, etc.}
100: */
101: protected $entrymakers = array();
102:
103: /**
104: * The list of tags associated with the log entry.
105: * Must be valid terms from the tags vocabulary
106: * @var array
107: */
108: protected $tags = array();
109:
110: /**
111: * The possible list of opspr events
112: * @var array
113: */
114: protected $opspr_events = array();
115:
116: /**
117: * The object that holds fields of a new-style ProblemReport (PR)
118: */
119: protected $pr;
120:
121: /**
122: * The possible downtime fields
123: * @var array
124: */
125: protected $downtime = array();
126:
127: /**
128: * References to external databases
129: * @var array [type][]=>[id]
130: */
131: protected $references = array();
132:
133: /**
134: * Notifications to send
135: * @var array
136: */
137: protected $notifications = array();
138:
139: /**
140: * Comments attached to the Logentry
141: * @var array of Comments
142: */
143: protected $comments = array();
144:
145: /**
146: * Text to log as reason for a new revision
147: */
148: protected $revision_reason;
149:
150:
151: /**
152: * Instantiate a Logentry
153: *
154: * For maximum flexibility, the constructor can accept any of
155: * the following arguments:
156: * <ol>
157: * <li> A DOMDocument or DOMElement object in Logentry.xsd format </li>
158: * <li> The name of an XML file in Logentry.xsd format </li>
159: * <li> A title and the name of a logbook (e.g. ELOG, TLOG, etc.) </li>
160: * <li> A title and an array of logbook names </li>
161: * </ol>
162: * @link https://logbooks.jlab.org/schema/Logentry.xsd
163: * @link https://github.com/JeffersonLab/elog
164: */
165: public function __construct()
166: {
167: $args = func_get_args();
168: if (count($args) > 2 or count($args) < 1) {
169: throw new LogentryException("Invalid arguments");
170: }
171:
172: if (count($args) == 1) {
173: // if (is_a($args[0], 'DOMDocument')) {
174: // $this->constructFromDom($args[0]);
175: // } elseif (is_a($args[0], 'DOMElement')) {
176: // //Must convert to a DOMDocument in order to use DOMXpath
177: // $dom = new DOMDocument('1.0', 'UTF-8');
178: // $dom->appendChild($dom->importNode($args[0], TRUE));
179: // //echo $dom->saveXML();
180: // $this->constructFromDom($dom);
181: // } elseif (is_a($args[0], 'Elog')) {
182: // $this->constructFromElog($args[0]);
183: // }
184: } elseif (count($args) == 2) {
185: if (is_string($args[0])) {
186: $this->constructFromScratch($args[0], $args[1]);
187: }
188: }
189:
190: $this->setConfig(__DIR__, '.env');
191: }
192:
193: /**
194: * Minimal initialization private constructor.
195: *
196: * Automatically sets the created and author fields based
197: * on the system clock and os username respectively.
198: *
199: * @param $title
200: * @param $logbooks
201: */
202: protected function constructFromScratch($title, $logbooks)
203: {
204: $this->setTitle($title);
205: $this->setLogbooks($logbooks);
206: $this->setCreated(time());
207: $this->setDefaultAuthor();
208: }
209:
210: /**
211: * Sets the title.
212: *
213: * The title is limited to max 255 characters.
214: *
215: * @param string $title
216: * @throws LogentryException
217: */
218: public function setTitle($title)
219: {
220: // Would it be kinder to simply truncate?
221: if (strlen($title) > 255) {
222: throw new LogentryException("Title exceeds limit of 255 characters");
223: }
224: $this->title = $title;
225: }
226:
227: /**
228: * Sets the logbook(s) to which entry belongs
229: *
230: * @param mixed $logbooks (logbook name or array of logbook names)
231: */
232: public function setLogbooks($logbooks)
233: {
234: $this->logbooks = array();
235: is_array($logbooks) ? $bookList = $logbooks : $bookList = array($logbooks);
236: foreach ($bookList as $bookName) {
237: $this->addLogbook($bookName);
238: }
239: }
240:
241: /**
242: * Adds a logbook to the list of logbooks for the entry.
243: *
244: * @param string $logbook
245: *
246: * @link https://logbooks.jlab.org/logbooks
247: */
248: public function addLogbook($logbook)
249: {
250: $key = strtoupper($logbook);
251: $this->logbooks[$key] = $logbook;
252: }
253:
254:
255: /**
256: * Sets the internal timestamp of the entry.
257: *
258: * Stores it as string in ISO 8601 date format
259: * ex: 2004-02-12T15:19:21+00:00
260: *
261: * @param mixed $date unix integer timestamp or string parsable by php strtotime()
262: */
263: public function setCreated($date)
264: {
265: if (is_numeric($date)) {
266: $this->created = date('c', $date);
267: } else {
268: $this->created = date('c', strtotime($date));
269: }
270: }
271:
272: /**
273: * Defaults the author to the user who owns the current CPU process.
274: */
275: protected function setDefaultAuthor()
276: {
277: $os_user = posix_getpwuid(posix_getuid());
278: $this->setAuthor($os_user['name']);
279: }
280:
281: /**
282: * Sets the author.
283: *
284: * @param string $username
285: * @param array $attributes associative array of additional user attributes
286: *
287: */
288: public function setAuthor($username, array $attributes = array())
289: {
290: $this->author = new User($username, $attributes);
291: }
292:
293: /**
294: * Loads configuration from a .env file.
295: *
296: * Defaults to the .env file included with the package.
297: * Throws if required environment variables are not set.
298: *
299: * @param string $dir
300: * @param string $file
301: * @param bool $overload whether config file should replace existing settings (def: FALSE)
302: * @throws LogentryException if required environment variables not present
303: */
304: function setConfig($dir, $file, $overload = false)
305: {
306: try {
307: $this->config = new Dotenv($dir, $file);
308: if ($overload) {
309: $this->config->overload();
310: } else {
311: $this->config->load();
312: }
313: // Throws if any required env variables are missing
314: $this->config->required(array(
315: 'LOG_ENTRY_SCHEMA_URL',
316: 'SUBMIT_URL',
317: 'ELOGCERT_FILE',
318: 'DEFAULT_UNIX_QUEUE_PATH',
319: 'DEFAULT_WINDOWS_QUEUE_PATH',
320: 'EMAIL_DOMAIN'
321: ));
322: } catch (\Exception $e) {
323: throw new LogentryException($e->getMessage());
324: }
325: }
326:
327: /**
328: * Adds an entry maker.
329: *
330: * @param string $username
331: * @param array $attributes associative array of additional user attributes
332: *
333: */
334: public function addEntryMaker($username, array $attributes = array())
335: {
336: $Maker = new User($username, $attributes);
337: $this->entrymakers[$Maker->username] = $Maker;
338: }
339:
340: /**
341: * Adds an email adress to notify of the logentry
342: *
343: * @param $email
344: */
345: public function addNotify($email)
346: {
347: if (is_string($email)) {
348: if (!stristr($email, '@')) {
349: $email .= getenv('EMAIL_DOMAIN');
350: }
351: $addr = strtolower($email);
352: $this->notifications[$addr] = $email;
353: }
354: }
355:
356: /**
357: * Adds a reference to external data
358: *
359: * @param string $type (lognumber, atlis, etc.)
360: * @param integer $ref (numeric elog_id, task_id, etc.)
361: */
362: public function addReference($type, $ref)
363: {
364: if ($type && $ref) {
365: $key = strtolower($type);
366: $this->references[$key][$ref] = $ref;
367: }
368: }
369:
370: /**
371: * Adds a tag from the tags vocabulary
372: *
373: * @param string $tag
374: * @link https://logbooks.jlab.org/tags
375: */
376: public function addTag($tag)
377: {
378: if ($tag) {
379: $key = strtolower($tag);
380: $this->tags[$key] = $tag;
381: }
382: }
383:
384: /**
385: * Sets the body and its content type.
386: *
387: * @param string $text The content to place in the body
388: * @param string $type Specifies the formatting of text: text|html
389: */
390: public function setBody($text, $type = 'text')
391: {
392: $this->body = $text;
393: $this->bodyType = $type;
394: }
395:
396: /**
397: * sets the lognumber.
398: *
399: * The lognumber will generally be set only when a logentry is retrieved from the
400: * logbook server, not during creation of a new entry.
401: *
402: * The server will reject submissions of new entries with non-null lognumber except from
403: * a small set of privileged users. Those users would typically specify the lognumber
404: * for a new entry only in situations such as importing legacy data.
405: *
406: * @param integer $num
407: */
408: public function setLognumber($num)
409: {
410: if (is_numeric($num)) {
411: $this->lognumber = (int)$num;
412: }
413: }
414:
415: /**
416: * Adds an attachment from a file.
417: *
418: * Stores it in the object's attachments array as a base64 encoded string.
419: *
420: * @param string $filename
421: * @param string $caption
422: * @param string $type Specify a mime_type (defaults to autodetect)
423: * @throws
424: */
425: public function addAttachment($filename, $caption = '', $type = '')
426: {
427: $this->attachments[] = new FileAttachment($filename, $caption, $type);
428: }
429:
430:
431: /**
432: * Adds an attachment as a URL reference.
433: *
434: * @param string $url
435: * @param string $caption
436: * @param string $type mimeType
437: * @throws
438: */
439: public function addAttachmentURL($url, $caption = '', $type = '')
440: {
441: $this->attachments[] = new URLAttachment($url, $caption, $type);
442: }
443:
444:
445: /**
446: * Return Logentry object as an XML text string
447: *
448: * @param string $name A name to use for the encompassing XML tag.
449: * @return string
450: * @todo Comment, PR, Downtime
451: */
452: function getXML($name = 'Logentry')
453: {
454:
455: /* Note that calls to xmlWriter::writeElement seem to implicitly encode
456: * html entitites and so we don't want to call htmlspecialchars ourselves
457: * because that results in double-encoding and is a problem.
458: */
459: $xw = new xmlWriter();
460: $xw->openMemory();
461: $xw->setIndent(true);
462: $xw->startElement($name);
463:
464: $this->xmlWriteLognumber($xw);
465: $this->xmlWriteCreated($xw);
466: $this->xmlWriteTitle($xw);
467: $this->xmlWriteAuthor($xw);
468: $this->xmlWriteLogbooks($xw);
469: $this->xmlWriteTags($xw);
470: $this->xmlWriteEntrymakers($xw);
471: $this->xmlWriteBody($xw);
472: $this->xmlWriteNotifications($xw);
473: $this->xmlWriteReferences($xw);
474:
475: //Placing attachments at the end make it easier on someone
476: //who might try and read the xml file in a terminal or editor.
477: $this->xmlWriteAttachments($xw);
478:
479:
480: //
481:
482: // if ($n == 'comments' && count($this->comments) > 0) {
483: // $xw->startElement('Comments');
484: // //$xw->writeRaw("\n");
485: // foreach ($var as $comment) {
486: // if (method_exists($comment, 'getXML')) {
487: // $xw->writeRaw($comment->getXML());
488: // }
489: // }
490: // $xw->endElement();
491: // continue;
492: // }
493: // if ($n == 'opspr_events' && count($this->opspr_events) > 0) {
494: // $xw->startElement('OPSPREvents');
495: // foreach ($this->opspr_events as $pr_event) {
496: // $xw->startElement('OPSPREvent');
497: // foreach (get_object_vars($pr_event) as $mprop => $mval) {
498: // $xw->writeElement($mprop, $mval);
499: // }
500: // $xw->endElement();
501: // }
502: // $xw->endElement();
503: // continue;
504: // }
505: // if ($n == 'downtime' && !empty($this->downtime)) {
506: // $xw->startElement('Downtime');
507: // foreach ($this->downtime as $key => $value) {
508: // $xw->writeElement($key, $value);
509: // }
510: // $xw->endElement();
511: // continue;
512: // }
513:
514: // if ($n == 'pr' && is_a($this->pr, 'ElogPR')) {
515: // $xw->writeRaw($this->pr->getXML());
516: // }
517:
518: $xw->endElement();
519: return $xw->outputMemory(true);
520: }
521:
522:
523: /**
524: * Write the lognumber to XML.
525: *
526: * @param \xmlWriter $xw
527: */
528: protected function xmlWriteLognumber(\xmlWriter $xw)
529: {
530: if ($this->lognumber){
531: $xw->writeElement('lognumber', $this->lognumber);
532: }
533: }
534:
535: /**
536: * Write the title to XML.
537: *
538: * @param \xmlWriter $xw
539: */
540: protected function xmlWriteTitle(\xmlWriter $xw)
541: {
542: $xw->writeElement('title', $this->title);
543: }
544:
545: /**
546: * Write the created timestamp to XML.
547: * @param \xmlWriter $xw
548: */
549: protected function xmlWriteCreated(\xmlWriter $xw)
550: {
551: $xw->writeElement('created', $this->created);
552: }
553:
554:
555: /**
556: * Write the author timestamp to XML.
557: *
558: * @param \xmlWriter $xw
559: */
560: protected function xmlWriteAuthor(\xmlWriter $xw)
561: {
562: $xw->writeRaw($this->author->getXML('Author'));
563: }
564:
565: /**
566: * Write the logbook names to XML.
567: *
568: * @param \xmlWriter $xw
569: */
570: protected function xmlWriteLogbooks(\xmlWriter $xw)
571: {
572: $xw->startElement('Logbooks');
573: foreach ($this->logbooks as $logbook) {
574: $xw->writeElement('logbook', $logbook);
575: }
576: $xw->endElement();
577: }
578:
579: /**
580: * Write the tag names to XML.
581: *
582: * @param \xmlWriter $xw
583: */
584: protected function xmlWriteTags(\xmlWriter $xw)
585: {
586: if (count($this->tags) > 0) {
587: $xw->startElement('Tags');
588: foreach ($this->tags as $tag) {
589: $xw->writeElement('tag', $tag);
590: }
591: $xw->endElement();
592: }
593: }
594:
595: /**
596: * Write the entrymakers to XML.
597: *
598: * @param \xmlWriter $xw
599: */
600: protected function xmlWriteEntrymakers(\xmlWriter $xw)
601: {
602: if (count($this->entrymakers) > 0) {
603: $xw->startElement('Entrymakers');
604: foreach ($this->entrymakers as $maker) {
605: $xw->writeRaw($maker->getXML('Entrymaker'));
606: }
607: $xw->endElement();
608: }
609: }
610:
611: /**
612: * Write the body content and type to XML.
613: *
614: * @param \xmlWriter $xw
615: */
616: protected function xmlWriteBody(\xmlWriter $xw)
617: {
618: if ($this->body != '') {
619: $xw->startElement('body');
620: switch ($this->bodyType) {
621: case 'elog_text' : //Really was HTML
622: case 'text/html' :
623: case 'html' :
624: case 'trusted_html' :
625: case 'full_html' :
626: $body_type = 'html';
627: break;
628: default :
629: $body_type = 'text';
630: }
631: $xw->writeAttribute('type', $body_type);
632: $xw->writeCData($this->body);
633: $xw->endElement();
634: }
635: }
636:
637: /**
638: * Write the email notification recipients to XML.
639: *
640: * @param \xmlWriter $xw
641: */
642: protected function xmlWriteNotifications(\xmlWriter $xw)
643: {
644: if (count($this->notifications) > 0) {
645: $xw->startElement('Notifications');
646: foreach ($this->notifications as $email) {
647: $xw->writeElement('email', $email);
648: }
649: $xw->endElement();
650: }
651: }
652:
653: /**
654: * Write the external data references to XML.
655: *
656: * @param \xmlWriter $xw
657: */
658: protected function xmlWriteReferences(\xmlWriter $xw)
659: {
660: if (count($this->references) > 0) {
661: $xw->startElement('References');
662: foreach ($this->references as $type => $ref) {
663: foreach ($ref as $r) {
664: $xw->startElement('reference');
665: $xw->writeAttribute('type', $type);
666: $xw->text($r);
667: $xw->endElement();
668: }
669: }
670: $xw->endElement();
671: }
672: }
673:
674: /**
675: * Write the attachments to XML.
676: *
677: * @param XMLWriter $xw
678: */
679: protected function xmlWriteAttachments(\xmlWriter $xw){
680: if (count($this->attachments) > 0) {
681: $xw->startElement('Attachments');
682: foreach ($this->attachments as $attachment) {
683: $xw->writeRaw($attachment->getXML('Attachment'));
684: }
685: $xw->endElement();
686: }
687: }
688:
689: /**
690: * Submit the log item directly to the server and return the assigned log number, but
691: * fall back to use the queue mechanism as plan B.
692: *
693: * If the log number returned is zero it indicates the submission was queued instead of being accepted by the server.
694: * You can use the whyQueued method to obtain the ServerException encountered if any while attempting
695: * to submit directly to the server.
696: *
697: * @return int The log number, zero means queued
698: * @throws LogentryException
699: */
700: public function submit(){
701: // First attempt is to submit the entry and get back immediate
702: // confirmation in the form of the log number assigned.
703: try {
704: return $this->submitNow();
705: } catch ( ServerException $e){
706: error_log($e->getMessage());
707: }
708:
709: // Then fall back and attempt queueing instead.
710: // Pass false to second parameter to prevent second redundant
711: // XML validation attempt.
712: $queueFile = LogentryUtil::saveToQueue($this, false);
713: if (file_exists($queueFile)){
714: return 0;
715: }
716: }
717:
718: /**
719: * Submit the log item using the queue mechanism only.
720: *
721: * @return mixed the filename that was queued or false for failure.
722: * @throws IOException if unable to write queue file
723: * @throws InvalidXMLException
724: */
725: public function queue(){
726: $queueFile = LogentryUtil::saveToQueue($this);
727: if (file_exists($queueFile)){
728: return $queueFile;
729: }
730: return false;
731: }
732:
733: /**
734: * Submit the log item using only the direct submission method with no queue fallback.
735: *
736: * If an error occurs during submission then an Exception will be thrown
737: * instead of falling back to the queue method.
738: *
739: * @return integer The log number
740: * @throws
741: */
742: public function submitNow(){
743: if (! LogentryUtil::isValidEntry($this)){
744: throw new InvalidXMLException("Schema validation of the entry fails: \n" . LogentryUtil::validationErrors());
745: }
746: return LogentryUtil::saveToServer($this);
747: }
748:
749: /**
750: * Return the ServerException which prevented direct submission to the server on the most recent attempt, or null if none.
751: *
752: * This method allows access to the exception which is masked when the submit method is called and returns
753: * with a zero value indicating the submission was queued.
754: */
755: public function whyQueued(){
756: //TODO implement method
757: }
758:
759:
760:
761: /**
762: * Magic method controls access to class properties.
763: *
764: * @param string $var
765: * @return mixed
766: */
767: function __get($var)
768: {
769: if (property_exists($this, $var)) {
770: $getterFunction = 'get' . ucfirst($var);
771: if (method_exists($this, $getterFunction)) {
772: return $this->$getterFunction();
773: } else {
774: return $this->$var;
775: }
776: }
777:
778: $trace = debug_backtrace();
779: trigger_error(
780: 'Undefined property via __get(): ' . $var .
781: ' in ' . $trace[0]['file'] .
782: ' on line ' . $trace[0]['line'], E_USER_NOTICE);
783: return null;
784: }
785:
786:
787: }