['edit'=>TRUE,'mask'=>'*'], // Password 't'=>['edit'=>TRUE], // Text ]; // @todo Move this to the database private $header = RED.SPACE.'T'.BLUE.SPACE.'E'.GREEN.SPACE.'S'.YELLOW.SPACE.'T'.MAGENTA.SPACE.'!'; public function __construct(FrameModel $o) { $this->fo = $o; $startline = 1; if ($this->fo->exists) { if (! $this->hasFlag('ip') AND (! $this->isCUG(0) OR $this->type() !== self::FRAMETYPE_LOGIN)) { $startline = 2; } elseif ($this->isCUG(0) AND $this->type() === self::FRAMETYPE_LOGIN) { $startline = 2; } } // Our parser object $this->po = $this->parser($startline); // Set our first editable field $this->resetCurrentField(); } /** * Render the frame * * @return null|string * @throws \Exception */ public function __toString() { $output = $this->fo->cls ? CLS : HOME; if (! $this->hasFlag('ip') AND (! $this->isCUG(0) OR $this->type() !== self::FRAMETYPE_LOGIN)) { $output .= $this->render_header($this->header). $this->render_page($this->fo->frame,$this->fo->index). $this->render_cost($this->fo->cost); } elseif ($this->isCUG(0) AND $this->type() === self::FRAMETYPE_LOGIN) { $output .= str_repeat(DOWN,1); } return $output.(string)$this->po; } /** * Return a list of alternative versions of this frame. * * @todo: Need to adjust to not include access=0 frames unless owner */ public function alts(Mode $o) { return FrameModel::where('frame',$this->fo->frame) ->where('index',$this->index()) ->where('id','<>',$this->fo->id) ->where('mode_id',$o->id) ->where('access',1) ->limit(9); } /** * Frame Created Date */ public function created() { return $this->fo->created_at; } /** * Return fields within the frame. */ public function fields() { return $this->po->fields; } /** * Returns the current frame. */ public function frame() { return $this->fo->frame; } public function frame_length() { return static::$frame_length; } public function frame_width() { return static::$frame_width; } /** * Get the CUG for a frame * * Frame CUG are derived from their frame number. * EG: Frame 642 is a member of 642, or 64, or 6, or 0, whichever matches first. * * @return CUG */ public function getCUG() { $co = NULL; $frame = $this->fo->frame; while (! $co) { $co = CUG::find($frame); if (! $co) { $frame = substr($frame,0,strlen($frame)-1); if (! $frame) $frame = 0; } } return $co; } /** * Return the current field configuration * Will return false if no id specified and the current field is not active */ public function getField(int $id=NULL) { if (is_null($id) AND $this->field_active === FALSE) return FALSE; return $this->fields()->get(is_null($id) ? $this->field_active : $id); } /** * Get the input for the current field * * @return mixed */ public function getFieldCurrentInput() { return Arr::get($this->field_data,$this->field_active); } public function getFieldData() { return $this->field_data; } public function getFieldDataId(int $id) { return Arr::get($this->field_data,$id); } /** * Return the current field ID * * @return bool */ public function getFieldId() { return $this->field_active; } /** * Get a specific key of the field options that passes a filter test * * @param string $type * @param int $after * @return mixed */ public function getFieldNextId($type='edit',$after=0) { return $this->fields() ->search(function($item,$key) use ($type,$after) { return $key >= $after AND $this->isFieldEditable($item->type); }); } public function getFieldPrevId($type='edit',$before=FALSE) { if (! $before) $before = $this->fields()->count(); return $this->fields() ->reverse() ->search(function($item,$key) use ($type,$before) { return $key < $before AND $this->isFieldEditable($item->type); }); } /** * Return the flag for this page * * CLEAR: Clear Screen before rendering. * * @param $flag * @return bool */ public function hasFlag($flag) { return $this->fo->hasFlag($flag); } /** * Return the frame DB id * * @return mixed */ public function id() { return $this->fo->id; } /** * Current frame index * * @return mixed */ public function index() { return $this->fo->index; } /** * Return the next index */ public function index_next() { return chr(ord($this->fo->index)+1); } /** * Return the previous index */ public function index_prev() { return $this->fo->index == 'a' ? 'a' : chr(ord($this->fo->index)-1); } public function isAccessible():bool { return $this->fo->access ? TRUE : FALSE; } /** * Determine if the frame is a particular CUG * * @param int $cug * @return bool */ public function isCUG(int $cug): bool { return ($this->getCUG()->id == $cug); } /** * Determine if the current field should be masked during input * * @param string $field * @return mixed */ public function isFieldCurrentMask(int $id=NULL): string { if (! $x=$this->getField($id)) return FALSE; return array_get(array_get($this->fieldoptions,$x->type),'mask',''); } /** * Determine if a field is editable * * @param string $field * @return boolean */ public function isFieldEditable(string $field): bool { return array_get(array_get($this->fieldoptions,$field),'edit',FALSE); } /** * Is this frame Public * * @return bool */ public function isFramePublic(): bool { return $this->fo->public ? TRUE : FALSE; } // @todo To implement public function isOwner(User $o):bool { return FALSE; } /** * Return the Page Number */ public function page(bool $as_array=FALSE) { return $as_array ? ['frame'=>$this->fo->frame,'index'=>$this->fo->index] : $this->fo->page; } /** * Return the next page number. * * @param bool $as_array * @return mixed */ public function page_next(bool $as_array=FALSE) { return $as_array ? ['frame'=>$this->fo->frame,'index'=>$this->index_next()] : $this->fo->frame.$this->index_next(); } /** * Load the parser * * @param int $startline * @return Parser */ abstract protected function parser(int $startline): Parser; /** * Render the cost of the frame * * @param int $cost * @return string * @throws \Exception */ private function render_cost(int $cost) { if ($cost > 999) throw new \Exception('Price too high'); if ($cost > 100) $color = RED.SPACE; elseif ($cost > 0) $color = YELLOW.SPACE; else $color = GREEN.SPACE; return sprintf($color.'% '.static::$cost_length.'.0f%s',$cost,static::$cost_unit); } /** * Render the Site Header * * @param string $header * @return bool|string */ private function render_header(string $header) { $filler = ($this->strlenv($header) < static::$header_length) ? str_repeat(' ',static::$header_length-$this->strlenv($header)) : ''; return substr($header.$filler,0,static::$header_length+strlen($header)-$this->strlenv($header)); } /** * Render the Frame Number * * @param int $num * @param string $frame * @return string * @throws \Exception */ private function render_page(int $num,string $frame) { if ($num > (int)str_repeat(9,static::$pagenum_length)) throw new \Exception('Page Number too big',500); if (strlen($frame) !== 1) throw new \Exception('Frame invalid',500); return WHITE.SPACE.$num.$frame.(str_repeat(' ',static::$pagenum_length-strlen($num))); } /** * Reset the current active field to the first one */ public function resetCurrentField() { $this->field_active = $this->getFieldNextId('edit',0); $this->resetCurrentFieldData(); } /** * Clear the current field */ public function resetCurrentFieldData() { if ($this->field_active !== FALSE AND isset($this->field_data[$this->field_active])) unset($this->field_data[$this->field_active]); } /** * Get the route for the key press * * @param string $read * @return string * @throws \Exception */ public function route(string $read) { if (! preg_match('/^[0-9]$/',$read)) throw new \Exception('Routes are single digit'); // If we dont have a route record... if (! $this->fo->route) return '*'; $key = 'r'.$read; return $this->fo->route->{$key}; } /** * Set the field that is current in focus for input * * @param int $num */ public function setFieldCurrent(int $num) { $this->po->field_active = $num; } public function setFieldCurrentInput(string $read) { if ($this->field_active === FALSE) throw new \Exception('No field active?',500); if (! array_key_exists($this->field_active,$this->field_data)) $this->field_data[$this->field_active] = ''; $this->field_data[$this->field_active] .= $read; } /** * Delete the last character of the current Input field * * @return bool */ public function setFieldCurrentInputDelete(): bool { if (! $this->field_data[$this->field_active]) return FALSE; $this->field_data[$this->field_active] = substr($this->field_data[$this->field_active],0,-1); return TRUE; } /** * Set the next active field */ public function setFieldNext() { $this->field_active = $this->getFieldNextId('edit',$this->field_active+1); $this->resetCurrentFieldData(); } /** * Set the previous active field */ public function setFieldPrev() { $this->field_active = $this->getFieldPrevId('edit',$this->field_active); $this->resetCurrentFieldData(); } /** * Calculate the length of text * * ESC characters are two chars, and need to be counted as one. * * @param $text * @return int */ abstract public static function strlenv($text):int; public static function testFrame() { // Simulate a DB load $o = new FrameModel; $content = ''; $o->flags = ['ip']; $o->type = 'a'; $o->frame = 999; $o->index = 'a'; $o->access = 1; $o->public = 1; $o->cls = 1; // Header $sid = R_RED.'T'.R_BLUE.'E'.R_GREEN.'S'.R_YELLOW.'T'; $content .= substr($sid.'-'.str_repeat('12345678901234567890',4),0,static::$header_length+(strlen($sid)-static::strlenv($sid))). R_WHITE.str_repeat('9',static::$pagenum_length).'a'.R_RED.sprintf('%07.0f',999).'u'; $content .= R_WHITE.str_repeat('+-',static::$frame_width/2-3).' '.R_RED.'01'; $content .= R_WHITE.'Name: '.ESC.str_repeat('t',5).' |'.str_repeat('+-',static::$frame_width/2-8).'|'; $content .= R_WHITE.'Date: '.ESC.str_repeat('d',17).' |'.str_repeat('+-',static::$frame_width/2-14).'|'; $content .= R_WHITE.'Address: '.ESC.str_repeat('t',19).' |'.str_repeat('+-',static::$frame_width/2-17).'|'; $content .= R_WHITE.' : '.ESC.str_repeat('t',19).' |'.str_repeat('+-',static::$frame_width/2-17).'|'; $o->content = $content; return new static($o); } /** * Return the Frame Type */ public function type() { return $this->fo->type(); } }