src/Entity/Page.php line 32

Open in your IDE?
  1. <?php
  2. namespace App\Entity;
  3. use App\Config;
  4. use App\Entity\Menu\Item;
  5. use App\Entity\Utils\HistoryInterface;
  6. use App\Entity\Utils\IntlInterface;
  7. use App\Entity\Utils\ReleaseInterface;
  8. use App\Entity\Utils\Traits\SeoTrait;
  9. use App\Entity\Utils\Traits\TemplateTrait;
  10. use App\Entity\Utils\Traits\TimestampTrait;
  11. use App\Entity\Utils\Traits\VisibilityTrait;
  12. use App\Entity\Utils\VersionInterface;
  13. use App\Model\ContentProperties;
  14. use App\Model\Slice;
  15. use App\Repository\PageRepository;
  16. use App\Utils\Helper;
  17. use DateTime;
  18. use Doctrine\Common\Collections\ArrayCollection;
  19. use Doctrine\Common\Collections\Collection;
  20. use Doctrine\DBAL\Types\Types;
  21. use Doctrine\ORM\Mapping as ORM;
  22. use Stringable;
  23. use Symfony\Component\Validator\Constraints as Assert;
  24. /**
  25.  * Class Page.
  26.  */
  27. #[ORM\Table(name'page')]
  28. #[ORM\Entity(repositoryClassPageRepository::class)]
  29. class Page implements VersionInterfaceReleaseInterfaceHistoryInterfaceIntlInterfaceStringable
  30. {
  31.     use TimestampTrait;
  32.     use TemplateTrait;
  33.     use VisibilityTrait;
  34.     use SeoTrait;
  35.     final public const STATUS_DRAFT     'draft';
  36.     final public const STATUS_PENDING   'pending';
  37.     final public const STATUS_PUBLISHED 'published';
  38.     final public const STATUS_ARCHIVED  'archived';
  39.     final public const STATUS_TRASH     'trash';
  40.     final public const CACHING_NO_CACHE   'no-cache';
  41.     final public const CACHING_BASIC      'basic';
  42.     final public const CACHING_SIMPLE     'simple';
  43.     final public const CACHING_AGGRESSIVE 'aggressive';
  44.     #[ORM\Id]
  45.     #[ORM\Column(typeTypes::INTEGER)]
  46.     #[ORM\GeneratedValue]
  47.     protected ?int $id null;
  48.     #[ORM\Column(typeTypes::STRINGlength255)]
  49.     #[Assert\NotBlank(message'form.error.required')]
  50.     #[Assert\Length(min2max255minMessage'form.error.short'maxMessage'form.error.long')]
  51.     protected string $type;
  52.     #[ORM\Column(typeTypes::STRINGlength255)]
  53.     #[Assert\NotBlank(message'form.error.required')]
  54.     #[Assert\Length(min2max255minMessage'form.error.short'maxMessage'form.error.long')]
  55.     protected string $cachingLevel self::CACHING_BASIC;
  56.     #[ORM\Column(typeTypes::STRINGlength255nullabletrue)]
  57.     #[Assert\Length(min2max255minMessage'form.error.short'maxMessage'form.error.long')]
  58.     protected ?string $globalTitle;
  59.     #[ORM\Column(typeTypes::TEXTnullabletrue)]
  60.     #[Assert\Length(min2max10000minMessage'form.error.short'maxMessage'form.error.long')]
  61.     protected ?string $globalDescription null;
  62.     #[ORM\Column(typeTypes::STRINGlength255)]
  63.     #[Assert\NotBlank(message'form.error.required')]
  64.     #[Assert\Length(min2max255minMessage'form.error.short'maxMessage'form.error.long')]
  65.     protected string $title;
  66.     #[ORM\Column(typeTypes::TEXTnullabletrue)]
  67.     #[Assert\Length(min2max10000minMessage'form.error.short'maxMessage'form.error.long')]
  68.     protected ?string $description null;
  69.     #[ORM\Column(typeTypes::JSONnullabletrue)]
  70.     protected ?array $contentProperties = [];
  71.     #[ORM\Column(typeTypes::STRINGlength255)]
  72.     #[Assert\NotBlank(message'form.error.required')]
  73.     protected string $locale 'en';
  74.     #[ORM\OneToMany(mappedBy'master'targetEntity'Page'cascade: ['detach'])]
  75.     protected Collection $translations;
  76.     #[ORM\ManyToOne(targetEntity'Page'cascade: ['detach'], inversedBy'translations')]
  77.     #[ORM\JoinColumn(name'master_id'onDelete'SET NULL')]
  78.     protected $master;
  79.     #[ORM\Column(typeTypes::JSONnullabletrue)]
  80.     protected ?array $slices = [];
  81.     #[Assert\Valid]
  82.     #[ORM\OneToMany(mappedBy'page'targetEntity'Path'cascade: ['all'])]
  83.     protected Collection $paths;
  84.     #[ORM\Column(typeTypes::JSONnullabletrue)]
  85.     protected ?array $breadcrumb;
  86.     #[ORM\Column(typeTypes::STRING)]
  87.     #[Assert\NotBlank(message'form.error.required')]
  88.     protected string $status self::STATUS_DRAFT;
  89.     #[ORM\Column(typeTypes::TEXTnullabletrue)]
  90.     #[Assert\Length(min2max2000minMessage'form.error.short'maxMessage'form.error.long')]
  91.     protected ?string $updateComment null;
  92.     #[ORM\Column(typeTypes::DATETIME_MUTABLEnullabletrue)]
  93.     protected ?DateTime $published null;
  94.     #[ORM\ManyToOne(targetEntity'BackUser'cascade: ['persist'])]
  95.     #[ORM\JoinColumn(name'lastPublisher_id')]
  96.     protected ?BackUser $lastPublisher null;
  97.     protected bool $hasBeenPublished false;
  98.     #[ORM\Column(typeTypes::DATETIME_MUTABLEnullabletrue)]
  99.     protected ?DateTime $expired null;
  100.     #[ORM\ManyToMany(targetEntity'Tag'inversedBy'pages'cascade: ['detach'])]
  101.     #[ORM\JoinTable(name'page_tags'joinColumns: [new ORM\JoinColumn(name'page_id'referencedColumnName'id')], inverseJoinColumns: [new ORM\JoinColumn(name'tag_id'referencedColumnName'id')])]
  102.     protected Collection $tags;
  103.     #[ORM\OneToMany(mappedBy'page'targetEntityItem::class, cascade: ['detach'])]
  104.     protected Collection $menuItems;
  105.     /**
  106.      * Page constructor.
  107.      */
  108.     public function __construct(string $title, ?string $type nullstring $locale 'en', ?string $template null, ?string $description null)
  109.     {
  110.         $this->tags         = new ArrayCollection();
  111.         $this->translations = new ArrayCollection();
  112.         $this->menuItems    = new ArrayCollection();
  113.         $this->paths        = new ArrayCollection();
  114.         $this->setTitle($title);
  115.         $this->setGlobalTitle($title);
  116.         if ($locale) {
  117.             $this->setLocale($locale);
  118.         }
  119.         if ($type) {
  120.             $this->setType($type);
  121.             if ($template) {
  122.                 $this->setTemplate($template);
  123.             }
  124.             $this->setDescription($description);
  125.             $this->setGlobalDescription($description);
  126.             if ($this->canBeSeen()) {
  127.                 if ($this->isMulti()) {
  128.                     $path = new Path(Config::getContentDefaultPath($type).Helper::toSlug($this->getTitle()), $this->getLocale());
  129.                 } else {
  130.                     $path = new Path(Config::getContentDefaultPath($type), $this->getLocale());
  131.                 }
  132.                 $this->addPath($path);
  133.             }
  134.         }
  135.     }
  136.     public function __toString(): string
  137.     {
  138.         return (string) $this->globalTitle;
  139.     }
  140.     public function setId(int $id): void
  141.     {
  142.         $this->id $id;
  143.     }
  144.     public function getId(): int
  145.     {
  146.         return $this->id;
  147.     }
  148.     public function getType(): ?string
  149.     {
  150.         return $this->type;
  151.     }
  152.     public function setType(string $type): void
  153.     {
  154.         $this->type $type;
  155.     }
  156.     public function isMulti(): bool
  157.     {
  158.         return Config::isContentMulti($this->type);
  159.     }
  160.     public function getCachingLevel(): string
  161.     {
  162.         return $this->cachingLevel;
  163.     }
  164.     public function setCachingLevel(string $cachingLevel): void
  165.     {
  166.         $this->cachingLevel $cachingLevel;
  167.     }
  168.     public function getGlobalTitle(): string
  169.     {
  170.         return $this->globalTitle;
  171.     }
  172.     public function setGlobalTitle(string $globalTitle): void
  173.     {
  174.         $this->globalTitle $globalTitle;
  175.         if ($this->isMaster()) {
  176.             /** @var Page $translation */
  177.             foreach ($this->translations as $translation) {
  178.                 $translation->setGlobalTitle($globalTitle);
  179.             }
  180.         }
  181.     }
  182.     public function getGlobalDescription(): ?string
  183.     {
  184.         return $this->globalDescription;
  185.     }
  186.     public function setGlobalDescription(?string $globalDescription): void
  187.     {
  188.         $this->globalDescription $globalDescription;
  189.     }
  190.     public function getTitle(): string
  191.     {
  192.         return $this->title;
  193.     }
  194.     public function setTitle(string $title): void
  195.     {
  196.         $this->title $title;
  197.     }
  198.     public function getDescription(): ?string
  199.     {
  200.         return $this->description;
  201.     }
  202.     public function setDescription(?string $description): void
  203.     {
  204.         $this->description $description;
  205.     }
  206.     public function getContentProperties(): ContentProperties
  207.     {
  208.         return new ContentProperties(array_merge($this->contentProperties ?? [], [
  209.             'locale' => $this->getLocale(),
  210.         ]));
  211.     }
  212.     public function setContentProperties(ContentProperties $contentProperties): void
  213.     {
  214.         $this->contentProperties $contentProperties->export();
  215.     }
  216.     public function addSlice(Slice $slice): Page
  217.     {
  218.         $this->slices[$slice->getPosition()] = $slice->export();
  219.         return $this;
  220.     }
  221.     public function removeSlice(Slice $slice): void
  222.     {
  223.         unset($this->slices[$slice->getPosition()]);
  224.     }
  225.     public function getSlices(): ?array
  226.     {
  227.         $slices = [];
  228.         if ($this->slices) {
  229.             foreach ($this->slices as $slice) {
  230.                 $slices[] = new Slice(array_merge($slice, [
  231.                     'locale' => $this->getLocale(),
  232.                 ]));
  233.             }
  234.         }
  235.         return $slices;
  236.     }
  237.     public function getSlicesFront(): array
  238.     {
  239.         $slices = [];
  240.         if ($this->slices) {
  241.             foreach ($this->slices as $slice) {
  242.                 if ('public' == $slice['visibility']) {
  243.                     $slices[] = new Slice(array_merge($slice, [
  244.                         'context' => 'front',
  245.                     ]));
  246.                 }
  247.             }
  248.         }
  249.         return $slices;
  250.     }
  251.     public function getSlicesType(): ?array
  252.     {
  253.         $slices = [];
  254.         if ($this->slices) {
  255.             foreach ($this->slices as $slice) {
  256.                 $slices[] = $slice['type'];
  257.             }
  258.         }
  259.         return $slices;
  260.     }
  261.     public function setSlices(array $slices): void
  262.     {
  263.         foreach ($slices as $b) {
  264.             if (!($b instanceof Slice)) {
  265.                 $b = new Slice($b);
  266.             }
  267.             $this->addSlice($b);
  268.         }
  269.     }
  270.     public function removeAllSlices(): void
  271.     {
  272.         if ($this->slices && is_countable($this->slices)) {
  273.             foreach ($this->slices as $b) {
  274.                 if (!($b instanceof Slice)) {
  275.                     $b = new Slice($b);
  276.                 }
  277.                 $this->removeSlice($b);
  278.             }
  279.         }
  280.     }
  281.     public function import(array $databool $partial false): void
  282.     {
  283.         $fields $this->getArrayModel($partial);
  284.         foreach ($fields as $field) {
  285.             $setter 'set'.ucfirst((string) $field);
  286.             if (isset($data[$field])) {
  287.                 $fields[$field] = $this->{$setter}($data[$field]);
  288.             }
  289.         }
  290.         if (!$partial) {
  291.             if (isset($data['slices'])) {
  292.                 $this->setSlices($data['slices']);
  293.             }
  294.             if (isset($data['contentProperties'])) {
  295.                 $this->setContentProperties(new ContentProperties($data['contentProperties']));
  296.             }
  297.             if (isset($data['path']) && $this->getCurrentPath() != $data['path']) {
  298.                 $oldPath $this->hasPath($data['path']);
  299.                 if (!$oldPath) {
  300.                     $path = new Path($data['path'], $this->getLocale());
  301.                     $this->addPath($path);
  302.                 } else {
  303.                     $this->getCurrentPath(false)->setType(Path::TYPE_REDIRECT);
  304.                     $oldPath->setType(Path::TYPE_CURRENT);
  305.                 }
  306.             }
  307.         }
  308.     }
  309.     public function export(string $locale null): array
  310.     {
  311.         $fields = [];
  312.         foreach ($this->getArrayModel() as $field) {
  313.             $getter 'get'.ucfirst((string) $field);
  314.             $fields[$field] = $this->{$getter}();
  315.         }
  316.         if ($this->canBeSeen()) {
  317.             $fields['path'] = $this->getCurrentPath();
  318.         }
  319.         if ($locale) {
  320.             $fields['locale'] = $locale;
  321.         }
  322.         return array_merge(
  323.             $fields,
  324.             [
  325.                 'slices'            => $this->slices,
  326.                 'contentProperties' => $this->contentProperties,
  327.             ]
  328.         );
  329.     }
  330.     public function getArrayModel(bool $partial false): array
  331.     {
  332.         if ($partial) {
  333.             $data array_merge(
  334.                 [
  335.                     'type',
  336.                     'globalTitle',
  337.                     'title',
  338.                     'locale',
  339.                 ],
  340.                 $this->getTemplateArrayModel()
  341.             );
  342.         } else {
  343.             $data array_merge(
  344.                 [
  345.                     'type',
  346.                     'globalTitle',
  347.                     'globalDescription',
  348.                     'title',
  349.                     'description',
  350.                     'locale',
  351.                 ],
  352.                 $this->getVisibilityArrayModel(),
  353.                 $this->getSeoArrayModel(),
  354.                 $this->getTemplateArrayModel()
  355.             );
  356.         }
  357.         return $data;
  358.     }
  359.     public function getLocale(): string
  360.     {
  361.         return $this->locale;
  362.     }
  363.     public function setLocale(string $locale): void
  364.     {
  365.         $this->locale $locale;
  366.     }
  367.     public function getExistingLocales(): array
  368.     {
  369.         $locales = [];
  370.         $master    $this->getMaster();
  371.         $locales[] = $master->getLocale();
  372.         /** @var Page $translation */
  373.         foreach ($this->getTranslations() as $translation) {
  374.             $locales[] = $translation->getLocale();
  375.         }
  376.         return $locales;
  377.     }
  378.     public function getTranslations(): Collection
  379.     {
  380.         $master $this->getMaster();
  381.         return $master->translations;
  382.     }
  383.     public function getTranslation(string $locale): mixed
  384.     {
  385.         $master $this->getMaster();
  386.         if ($master->getLocale() == $locale) {
  387.             return $master;
  388.         }
  389.         if (is_countable($master->getTranslations()) ? count($master->getTranslations()) : 0) {
  390.             /** @var Page $translation */
  391.             foreach ($master->getTranslations() as $translation) {
  392.                 if ($translation->getLocale() == $locale) {
  393.                     return $translation;
  394.                 }
  395.             }
  396.         }
  397.         return null;
  398.     }
  399.     public function addTranslation(object $translation): void
  400.     {
  401.         $this->translations[] = $translation;
  402.         $translation->setMaster($this);
  403.     }
  404.     public function removeTranslation(object $translation): void
  405.     {
  406.         $this->translations->removeElement($translation);
  407.         $translation->setMaster(null);
  408.     }
  409.     public function removeAllTranslation(): void
  410.     {
  411.         foreach ($this->translations as $translation) {
  412.             $this->removeTranslation($translation);
  413.         }
  414.     }
  415.     public function isMaster(): bool
  416.     {
  417.         return !$this->master;
  418.     }
  419.     public function getMaster()
  420.     {
  421.         if (!$this->master) {
  422.             return $this;
  423.         }
  424.         return $this->master;
  425.     }
  426.     public function setMaster(?object $master): void
  427.     {
  428.         $this->master $master;
  429.     }
  430.     public function addPath(Path $path): Page
  431.     {
  432.         $this->paths[] = $path;
  433.         $path->setPage($this);
  434.         return $this;
  435.     }
  436.     public function removePath(Path $path): void
  437.     {
  438.         $this->paths->removeElement($path);
  439.         $path->setPage(null);
  440.     }
  441.     public function getPaths(bool $string false): ArrayCollection|Collection|array
  442.     {
  443.         if ($string) {
  444.             $paths = [];
  445.             /** @var Path $path */
  446.             foreach ($this->paths as $path) {
  447.                 $paths[] = $path->getPath();
  448.             }
  449.             return $paths;
  450.         }
  451.         return $this->paths;
  452.     }
  453.     public function getRedirections(): array
  454.     {
  455.         $paths = [];
  456.         if (isset($this->paths)) {
  457.             /** @var Path $path */
  458.             foreach ($this->paths as $path) {
  459.                 if (Path::TYPE_REDIRECT == $path->getType()) {
  460.                     $paths[] = $path;
  461.                 }
  462.             }
  463.         }
  464.         return $paths;
  465.     }
  466.     public function getCurrentPath(bool $string true): string|Path|null
  467.     {
  468.         if (isset($this->paths)) {
  469.             /** @var Path $path */
  470.             foreach ($this->paths as $path) {
  471.                 if (Path::TYPE_CURRENT == $path->getType()) {
  472.                     return $string $path->getPath() : $path;
  473.                 }
  474.             }
  475.         }
  476.         return null;
  477.     }
  478.     public function hasPath(string $checkPath): ?Path
  479.     {
  480.         if (isset($this->paths)) {
  481.             /** @var Path $path */
  482.             foreach ($this->paths as $path) {
  483.                 if ($checkPath == $path->getPath()) {
  484.                     return $path;
  485.                 }
  486.             }
  487.         }
  488.         return null;
  489.     }
  490.     public function getBreadcrumb(): ?array
  491.     {
  492.         return $this->breadcrumb;
  493.     }
  494.     public function setBreadcrumb($breadcrumb): void
  495.     {
  496.         $this->breadcrumb $breadcrumb;
  497.     }
  498.     public function isVisible(): bool
  499.     {
  500.         return 'public' == $this->visibility && Page::STATUS_PUBLISHED == $this->status;
  501.     }
  502.     public static function getPublicationStatusesHuman(): array
  503.     {
  504.         $statusesHmn = [];
  505.         $statuses = [
  506.             self::STATUS_DRAFT,
  507.             self::STATUS_PENDING,
  508.             'soon_published',
  509.             self::STATUS_PUBLISHED,
  510.             'soon_expired',
  511.             'expired',
  512.             self::STATUS_ARCHIVED,
  513.         ];
  514.         foreach ($statuses as $status) {
  515.             $statusesHmn['publication.status.'.$status] = $status;
  516.         }
  517.         return $statusesHmn;
  518.     }
  519.     public static function getPublicationStatusesString(): string
  520.     {
  521.         return implode(','self::getPublicationStatusesHuman());
  522.     }
  523.     public function isPublishedSoon(): bool
  524.     {
  525.         if (!$this->getPublished()) {
  526.             return false;
  527.         }
  528.         $today     = new DateTime();
  529.         $inTwoDays = new DateTime();
  530.         $inTwoDays->add(new \DateInterval('P2D'));
  531.         if ($this->getPublished() > $today && $this->getPublished() < $inTwoDays) {
  532.             return true;
  533.         }
  534.         return false;
  535.     }
  536.     public function isExpiredSoon(): bool
  537.     {
  538.         if (!$this->getExpired()) {
  539.             return false;
  540.         }
  541.         $today     = new DateTime();
  542.         $inTwoDays = new DateTime();
  543.         $inTwoDays->add(new \DateInterval('P2D'));
  544.         if ($this->getExpired() > $today && $this->getExpired() < $inTwoDays) {
  545.             return true;
  546.         }
  547.         return false;
  548.     }
  549.     public function getStatus(): string
  550.     {
  551.         return $this->status;
  552.     }
  553.     public function setStatus($status): void
  554.     {
  555.         $this->status $status;
  556.         if (Page::STATUS_PUBLISHED == $status) {
  557.             $this->hasBeenPublished true;
  558.         }
  559.     }
  560.     public function getUpdateComment(): ?string
  561.     {
  562.         return $this->updateComment;
  563.     }
  564.     public function setUpdateComment(?string $updateComment): void
  565.     {
  566.         $this->updateComment $updateComment;
  567.     }
  568.     public function getPublished(): ?DateTime
  569.     {
  570.         return $this->published;
  571.     }
  572.     public function setPublished(?DateTime $published null): void
  573.     {
  574.         $this->published $published;
  575.     }
  576.     public function getLastPublisher(): ?BackUser
  577.     {
  578.         return $this->lastPublisher;
  579.     }
  580.     public function setLastPublisher(?BackUser $lastPublisher null): void
  581.     {
  582.         $this->lastPublisher $lastPublisher;
  583.     }
  584.     /**
  585.      * @throws \Exception
  586.      */
  587.     public function updatePublisher(BackUser $lastPublisher): bool
  588.     {
  589.         if ($this->hasBeenPublished) {
  590.             $this->setLastPublisher($lastPublisher);
  591.             $this->setPublished(new DateTime());
  592.             $this->hasBeenPublished false;
  593.             return true;
  594.         }
  595.         return false;
  596.     }
  597.     public function getExpired(): ?DateTime
  598.     {
  599.         return $this->expired;
  600.     }
  601.     public function setExpired(?DateTime $expired null): void
  602.     {
  603.         $this->expired $expired;
  604.     }
  605.     public function addTag(Tag $tag): void
  606.     {
  607.         $this->tags[] = $tag;
  608.         $tag->addPage($this);
  609.     }
  610.     public function removeTag(Tag $tag): void
  611.     {
  612.         $this->tags->removeElement($tag);
  613.         $tag->removePage($this);
  614.     }
  615.     public function removeAllTags(string $type): void
  616.     {
  617.         if (is_countable($this->tags)) {
  618.             foreach ($this->tags as $tag) {
  619.                 if ($tag->getType() === $type) {
  620.                     $this->removeTag($tag);
  621.                 }
  622.             }
  623.         }
  624.     }
  625.     public function getTags(?string $type null): Collection
  626.     {
  627.         if ($type) {
  628.             $tags = new ArrayCollection();
  629.             /** @var Tag $tag */
  630.             foreach ($this->tags as $tag) {
  631.                 if ($tag->getType() == $type) {
  632.                     $tags->add($tag);
  633.                 }
  634.             }
  635.             return $tags;
  636.         }
  637.         return $this->tags;
  638.     }
  639.     public function hasTag(int $tagId): bool
  640.     {
  641.         foreach ($this->tags as $tag) {
  642.             if ($tag->getId() == $tagId) {
  643.                 return true;
  644.             }
  645.         }
  646.         return false;
  647.     }
  648.     public function addMenuItem(Item $item): void
  649.     {
  650.         $this->menuItems[] = $item;
  651.         $item->setPage($this);
  652.     }
  653.     public function removeMenuItem(Item $item): void
  654.     {
  655.         $this->menuItems->removeElement($item);
  656.         $item->setPage($this);
  657.     }
  658.     public function getMenuItems(): Collection
  659.     {
  660.         return $this->menuItems;
  661.     }
  662.     public function cleanJson(): bool
  663.     {
  664.         $contentProperties $this->getContentProperties();
  665.         $contentProperties->clean(Config::getContentProperties($this->type));
  666.         $this->setContentProperties($contentProperties);
  667.         $slices $this->getSlices();
  668.         $cleanedSlices = [];
  669.         /** @var Slice $slice */
  670.         foreach ($slices as $key => $slice) {
  671.             if (Config::getSliceFields($slice->getType())) {
  672.                 $slice->clean();
  673.                 $cleanedSlices[] = $slice;
  674.             }
  675.         }
  676.         $this->setSlices($cleanedSlices);
  677.         return true;
  678.     }
  679.     public function hasSlicesAllowed(): bool
  680.     {
  681.         return 'free-html' != $this->getTemplate() && count(Config::getSlices($this->getTemplate()));
  682.     }
  683.     public function canBeSeen(): bool
  684.     {
  685.         return !Config::isContentHidden(self::getType());
  686.     }
  687.     public function getEntityType(): ?string
  688.     {
  689.         return 'page';
  690.     }
  691. }